Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cli/src/commands/cvms/list/command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { jsonOption } from "@/src/commands/status/command";
export const cvmsListCommandMeta: CommandMeta = {
name: "list",
aliases: ["ls"],
description: "List all CVMs",
description: "List your CVMs",
options: [jsonOption],
examples: [
{
Expand Down
71 changes: 40 additions & 31 deletions cli/src/commands/cvms/list/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import chalk from "chalk";
import { safeGetCvmList } from "@phala/cloud";
import { safeGetAppsList, type AppCvmInfo } from "@phala/cloud";
import { defineCommand } from "@/src/core/define-command";
import type { CommandContext } from "@/src/core/types";
import type { CvmListResponse } from "@/src/api/types";
import { getClient } from "@/src/lib/client";
import { CLOUD_URL } from "@/src/utils/constants";

Expand All @@ -13,6 +12,7 @@ import {
type CvmsListCommandInput,
} from "./command";


async function runCvmsListCommand(
input: CvmsListCommandInput,
context: CommandContext,
Expand All @@ -24,62 +24,71 @@ async function runCvmsListCommand(
const spinner = logger.startSpinner("Fetching CVMs");

const client = await getClient();
const result = await safeGetCvmList(client);

spinner.stop(true);
// Fetch all pages
const allCvms: AppCvmInfo[] = [];
let page = 1;
const pageSize = 100;

while (true) {
const result = await safeGetAppsList(client, {
page,
page_size: pageSize,
});

if (!result.success) {
spinner.stop(true);
context.fail(result.error.message);
return 1;
}

if (!result.success) {
context.fail(result.error.message);
return 1;
const cvms = result.data.dstack_apps.flatMap((app) => app.cvms);
allCvms.push(...cvms);

if (page >= result.data.total_pages) {
break;
}
page++;
}

const cvms = (result.data as CvmListResponse).items ?? [];
spinner.stop(true);

// Always return the list (context.success handles JSON vs human output)
if (input.json) {
context.success({ items: cvms });
context.success({ items: allCvms });
return 0;
}

// Human-readable output
if (cvms.length === 0) {
if (allCvms.length === 0) {
logger.info("No CVMs found");
return 0;
}

for (const cvm of cvms) {
const item = cvm as {
name?: string;
hosted?: { app_id?: string; id?: string; app_url?: string };
node?: { region_identifier?: string };
status?: string;
};

for (const cvm of allCvms) {
const formattedStatus =
item.status === "running"
? chalk.green(item.status)
: item.status === "stopped"
? chalk.red(item.status)
: chalk.yellow(item.status ?? "unknown");
cvm.status === "running"
? chalk.green(cvm.status)
: cvm.status === "stopped"
? chalk.red(cvm.status)
: chalk.yellow(cvm.status ?? "unknown");

logger.keyValueTable(
{
Name: item.name || "Unknown",
"App ID": `app_${item.hosted?.app_id || "unknown"}`,
"CVM ID": item.hosted?.id?.replace(/-/g, "") || "unknown",
Region: item.node?.region_identifier || "N/A",
Name: cvm.name || "Unknown",
"App ID": `app_${cvm.app_id || "unknown"}`,
"CVM ID": cvm.vm_uuid?.replace(/-/g, "") || "unknown",
Region: cvm.region_identifier || "N/A",
Status: formattedStatus,
"Node Info URL": item.hosted?.app_url || "N/A",
"App URL": `${CLOUD_URL}/dashboard/cvms/${
item.hosted?.id?.replace(/-/g, "") || "unknown"
cvm.vm_uuid?.replace(/-/g, "") || "unknown"
}`,
},
{ borderStyle: "rounded" },
);
logger.break();
}

logger.success(`Found ${cvms.length} CVMs`);
logger.success(`Found ${allCvms.length} CVMs`);
logger.break();
logger.info(`Go to ${CLOUD_URL}/dashboard/ to view your CVMs`);
return 0;
Expand Down
92 changes: 92 additions & 0 deletions js/src/actions/apps/get_apps_list.e2e.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { describe, it, expect } from "vitest";
import { createClient } from "../../client";
import { getAppsList, safeGetAppsList } from "./get_apps_list";

describe("getAppsList e2e", () => {
const client = createClient();

it("should fetch apps list from real API", async () => {
const result = await safeGetAppsList(client, { page: 1, page_size: 5 });

expect(result.success).toBe(true);
if (result.success) {
expect(result.data).toHaveProperty("dstack_apps");
expect(result.data).toHaveProperty("page");
expect(result.data).toHaveProperty("page_size");
expect(result.data).toHaveProperty("total");
expect(result.data).toHaveProperty("total_pages");
expect(Array.isArray(result.data.dstack_apps)).toBe(true);
}
});

it("should return apps with correct structure", async () => {
const result = await safeGetAppsList(client, { page: 1, page_size: 1 });

expect(result.success).toBe(true);
if (result.success && result.data.dstack_apps.length > 0) {
const app = result.data.dstack_apps[0];
expect(app).toHaveProperty("id");
expect(app).toHaveProperty("name");
expect(app).toHaveProperty("app_id");
expect(app).toHaveProperty("created_at");
expect(app).toHaveProperty("kms_type");
expect(app).toHaveProperty("cvms");
expect(app).toHaveProperty("cvm_count");
expect(Array.isArray(app.cvms)).toBe(true);
}
});

it("should return CVMs with correct structure within apps", async () => {
const result = await safeGetAppsList(client, { page: 1, page_size: 5 });

expect(result.success).toBe(true);
if (result.success) {
// Find an app with at least one CVM
const appWithCvm = result.data.dstack_apps.find(
(app) => app.cvms.length > 0
);
if (appWithCvm) {
const cvm = appWithCvm.cvms[0];
expect(cvm).toHaveProperty("vm_uuid");
expect(cvm).toHaveProperty("app_id");
expect(cvm).toHaveProperty("name");
expect(cvm).toHaveProperty("status");
expect(cvm).toHaveProperty("vcpu");
expect(cvm).toHaveProperty("memory");
expect(cvm).toHaveProperty("disk_size");
expect(cvm).toHaveProperty("teepod_id");
expect(cvm).toHaveProperty("teepod_name");
expect(cvm).toHaveProperty("region_identifier");
expect(cvm).toHaveProperty("instance_type");
}
}
});

it("should support pagination", async () => {
const page1 = await safeGetAppsList(client, { page: 1, page_size: 2 });
const page2 = await safeGetAppsList(client, { page: 2, page_size: 2 });

expect(page1.success).toBe(true);
expect(page2.success).toBe(true);

if (page1.success && page2.success) {
expect(page1.data.page).toBe(1);
expect(page2.data.page).toBe(2);
expect(page1.data.page_size).toBe(2);
expect(page2.data.page_size).toBe(2);
}
});

it("should return only current user's apps (not all platform apps)", async () => {
// This test verifies that the /apps endpoint returns only the current user's apps
// even for admin users (unlike /cvms/paginated which returns all CVMs for admins)
const result = await safeGetAppsList(client, { page: 1, page_size: 100 });

expect(result.success).toBe(true);
if (result.success) {
// For any authenticated user, total should be a reasonable number (not thousands)
// This is a sanity check - if we're getting thousands of apps, something is wrong
expect(result.data.total).toBeLessThan(500);
}
});
});
Loading
Loading