diff --git a/cli/src/commands/cvms/list/command.ts b/cli/src/commands/cvms/list/command.ts index 1f5ac55b..6331aa0b 100644 --- a/cli/src/commands/cvms/list/command.ts +++ b/cli/src/commands/cvms/list/command.ts @@ -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: [ { diff --git a/cli/src/commands/cvms/list/index.ts b/cli/src/commands/cvms/list/index.ts index 21878d83..0aad9f57 100644 --- a/cli/src/commands/cvms/list/index.ts +++ b/cli/src/commands/cvms/list/index.ts @@ -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"; @@ -13,6 +12,7 @@ import { type CvmsListCommandInput, } from "./command"; + async function runCvmsListCommand( input: CvmsListCommandInput, context: CommandContext, @@ -24,54 +24,63 @@ 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" }, @@ -79,7 +88,7 @@ async function runCvmsListCommand( 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; diff --git a/js/src/actions/apps/get_apps_list.e2e.test.ts b/js/src/actions/apps/get_apps_list.e2e.test.ts new file mode 100644 index 00000000..f5e247e7 --- /dev/null +++ b/js/src/actions/apps/get_apps_list.e2e.test.ts @@ -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); + } + }); +}); diff --git a/js/src/actions/apps/get_apps_list.test.ts b/js/src/actions/apps/get_apps_list.test.ts new file mode 100644 index 00000000..3bc5f2c2 --- /dev/null +++ b/js/src/actions/apps/get_apps_list.test.ts @@ -0,0 +1,255 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { createClient } from "../../client"; +import { + getAppsList, + safeGetAppsList, + type GetAppsListResponse, + type DstackApp, + type AppCvmInfo, +} from "./get_apps_list"; + +// Mock CVM data matching the API structure +const mockCvmInfo: AppCvmInfo = { + vm_uuid: "c48d9363-21b2-4f4a-87d9-3496623f020d", + app_id: "2a7b4d0d0c0883d0d3119c36eb25a1f72122a7c4", + name: "test-app", + status: "running", + vcpu: 4, + memory: 8192, + disk_size: 20, + teepod_id: 11, + teepod_name: "prod6", + region_identifier: "EU-WEST-1", + kms_type: "phala", + instance_type: "tdx.large", + listed: true, + base_image: "dstack-dev-0.5.4.1", + kms_slug: "phala-prod6", + kms_id: "kms_dA2M76mq", + instance_id: null, +}; + +const mockDstackApp: DstackApp = { + id: "prj_A6eez86X", + name: "test-app", + app_id: "2a7b4d0d0c0883d0d3119c36eb25a1f72122a7c4", + app_provision_type: null, + app_icon_url: null, + created_at: "2025-11-25T02:24:51.954322+00:00", + kms_type: "phala", + current_cvm: mockCvmInfo, + cvms: [mockCvmInfo], + cvm_count: 1, +}; + +const mockAppsListData: GetAppsListResponse = { + dstack_apps: [mockDstackApp], + page: 1, + page_size: 20, + total: 1, + total_pages: 1, +}; + +describe("getAppsList", () => { + let client: ReturnType; + let mockGet: ReturnType; + + beforeEach(() => { + client = createClient({ + apiKey: "test-api-key", + baseURL: "https://api.test.com", + }); + mockGet = vi.spyOn(client, "get"); + }); + + describe("API routing & basic success", () => { + it("should call correct endpoint with query params", async () => { + mockGet.mockResolvedValue(mockAppsListData); + + const result = await getAppsList(client, { + page: 2, + page_size: 50, + }); + + expect(mockGet).toHaveBeenCalledWith("/apps", { + params: { + page: 2, + page_size: 50, + }, + }); + expect(result).toEqual(mockAppsListData); + expect(result.dstack_apps).toHaveLength(1); + }); + + it("should support search parameter", async () => { + mockGet.mockResolvedValue(mockAppsListData); + + await getAppsList(client, { + search: "my-app", + }); + + expect(mockGet).toHaveBeenCalledWith("/apps", { + params: { + search: "my-app", + }, + }); + }); + + it("should support filtering parameters", async () => { + mockGet.mockResolvedValue(mockAppsListData); + + await getAppsList(client, { + status: ["running", "stopped"], + listed: true, + region: "US-WEST-1", + instance_type: "tdx.large", + }); + + expect(mockGet).toHaveBeenCalledWith("/apps", { + params: { + status: ["running", "stopped"], + listed: true, + region: "US-WEST-1", + instance_type: "tdx.large", + }, + }); + }); + }); + + describe("error handling", () => { + it("should throw on API errors", async () => { + const apiError = new Error("API Error"); + mockGet.mockRejectedValue(apiError); + + await expect(getAppsList(client)).rejects.toThrow("API Error"); + }); + }); + + describe("edge cases", () => { + it("should work without parameters", async () => { + mockGet.mockResolvedValue(mockAppsListData); + + const result = await getAppsList(client); + + expect(mockGet).toHaveBeenCalledWith("/apps", { params: {} }); + expect(result).toEqual(mockAppsListData); + }); + + it("should handle empty apps list", async () => { + const emptyResponse: GetAppsListResponse = { + dstack_apps: [], + page: 1, + page_size: 20, + total: 0, + total_pages: 0, + }; + mockGet.mockResolvedValue(emptyResponse); + + const result = await getAppsList(client); + + expect(result.dstack_apps).toHaveLength(0); + expect(result.total).toBe(0); + }); + + it("should handle apps with multiple CVMs", async () => { + const secondCvm: AppCvmInfo = { + ...mockCvmInfo, + vm_uuid: "d59e4474-32c3-5g5b-98e0-4507734g131e", + name: "test-app-replica", + }; + const multiCvmApp: DstackApp = { + ...mockDstackApp, + cvms: [mockCvmInfo, secondCvm], + cvm_count: 2, + }; + const responseWithMultipleCvms: GetAppsListResponse = { + dstack_apps: [multiCvmApp], + page: 1, + page_size: 20, + total: 1, + total_pages: 1, + }; + mockGet.mockResolvedValue(responseWithMultipleCvms); + + const result = await getAppsList(client); + + expect(result.dstack_apps[0].cvms).toHaveLength(2); + expect(result.dstack_apps[0].cvm_count).toBe(2); + }); + }); + + describe("safeGetAppsList", () => { + it("should return SafeResult on success", async () => { + mockGet.mockResolvedValue(mockAppsListData); + + const result = await safeGetAppsList(client); + + expect(mockGet).toHaveBeenCalledWith("/apps", { params: {} }); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data).toEqual(mockAppsListData); + expect(result.data.dstack_apps).toHaveLength(1); + } + }); + + it("should handle query parameters correctly in safe version", async () => { + mockGet.mockResolvedValue(mockAppsListData); + + await safeGetAppsList(client, { + page: 2, + page_size: 50, + search: "test", + }); + + expect(mockGet).toHaveBeenCalledWith("/apps", { + params: { + page: 2, + page_size: 50, + search: "test", + }, + }); + }); + + it("should return SafeResult with error on failure", async () => { + const apiError = new Error("Network Error"); + mockGet.mockRejectedValue(apiError); + + const result = await safeGetAppsList(client); + + expect(result.success).toBe(false); + }); + }); + + describe("response structure validation", () => { + it("should correctly parse app with current_cvm", async () => { + mockGet.mockResolvedValue(mockAppsListData); + + const result = await getAppsList(client); + + const app = result.dstack_apps[0]; + expect(app.id).toBe("prj_A6eez86X"); + expect(app.name).toBe("test-app"); + expect(app.current_cvm).not.toBeNull(); + expect(app.current_cvm?.status).toBe("running"); + expect(app.current_cvm?.vm_uuid).toBe("c48d9363-21b2-4f4a-87d9-3496623f020d"); + }); + + it("should correctly parse CVM info within app", async () => { + mockGet.mockResolvedValue(mockAppsListData); + + const result = await getAppsList(client); + + const cvm = result.dstack_apps[0].cvms[0]; + expect(cvm.vm_uuid).toBe("c48d9363-21b2-4f4a-87d9-3496623f020d"); + expect(cvm.app_id).toBe("2a7b4d0d0c0883d0d3119c36eb25a1f72122a7c4"); + expect(cvm.status).toBe("running"); + expect(cvm.vcpu).toBe(4); + expect(cvm.memory).toBe(8192); + expect(cvm.disk_size).toBe(20); + expect(cvm.teepod_id).toBe(11); + expect(cvm.teepod_name).toBe("prod6"); + expect(cvm.region_identifier).toBe("EU-WEST-1"); + expect(cvm.instance_type).toBe("tdx.large"); + }); + }); +}); diff --git a/js/src/actions/apps/get_apps_list.ts b/js/src/actions/apps/get_apps_list.ts new file mode 100644 index 00000000..a79081f9 --- /dev/null +++ b/js/src/actions/apps/get_apps_list.ts @@ -0,0 +1,108 @@ +import { z } from "zod"; +import { type Client } from "../../client"; +import { defineAction } from "../../utils/define-action"; + +// CVM info within the apps response +export const AppCvmInfoSchema = z.object({ + vm_uuid: z.string(), + app_id: z.string(), + name: z.string(), + status: z.string(), + vcpu: z.number(), + memory: z.number(), + disk_size: z.number(), + teepod_id: z.number(), + teepod_name: z.string(), + region_identifier: z.string(), + kms_type: z.string(), + instance_type: z.string(), + listed: z.boolean(), + base_image: z.string(), + kms_slug: z.string(), + kms_id: z.string(), + instance_id: z.string().nullable(), +}); + +// DStack App schema +export const DstackAppSchema = z.object({ + id: z.string(), + name: z.string(), + app_id: z.string(), + app_provision_type: z.string().nullable(), + app_icon_url: z.string().nullable(), + created_at: z.string(), + kms_type: z.string(), + current_cvm: AppCvmInfoSchema.nullable(), + cvms: z.array(AppCvmInfoSchema), + cvm_count: z.number(), +}); + +export const GetAppsListRequestSchema = z.object({ + page: z.number().int().min(1).optional(), + page_size: z.number().int().min(1).max(100).optional(), + search: z.string().optional(), + status: z.array(z.string()).optional(), + listed: z.boolean().optional(), + base_image: z.string().optional(), + instance_type: z.string().optional(), + kms_slug: z.string().optional(), + kms_type: z.string().optional(), + teepod_name: z.string().optional(), + region: z.string().optional(), + debug_user_id: z.number().optional(), +}); + +export const GetAppsListSchema = z.object({ + dstack_apps: z.array(DstackAppSchema), + page: z.number(), + page_size: z.number(), + total: z.number(), + total_pages: z.number(), +}); + +export type GetAppsListRequest = z.infer; +export type GetAppsListResponse = z.infer; +export type DstackApp = z.infer; +export type AppCvmInfo = z.infer; + +/** + * Get a paginated list of apps with their CVMs. + * This endpoint returns only the current user's apps (even for admin users). + * + * @param client - The API client + * @param request - Optional request parameters for pagination and filtering + * @param request.page - Page number (1-based), default 1 + * @param request.page_size - Number of items per page (1-100), default 20 + * @param request.search - Search in name, app_id, vm_uuid, instance_id + * @param request.status - Filter by CVM status + * @param request.listed - Filter by listed status + * @param request.base_image - Filter by Docker image version + * @param request.instance_type - Filter by instance type + * @param request.kms_slug - Filter by KMS slug + * @param request.kms_type - Filter by KMS type + * @param request.teepod_name - Filter by node name + * @param request.region - Filter by region + * @param request.debug_user_id - Admin only: impersonate user for debugging + * @returns Paginated list of apps with their CVMs + * + * @example + * ```typescript + * // Get first page with default size + * const list = await getAppsList(client, { page: 1 }) + * + * // Get with custom page size + * const list = await getAppsList(client, { page: 1, page_size: 50 }) + * + * // Search for specific apps + * const list = await getAppsList(client, { search: "my-app" }) + * ``` + */ +const { action: getAppsList, safeAction: safeGetAppsList } = defineAction< + GetAppsListRequest | undefined, + typeof GetAppsListSchema +>(GetAppsListSchema, async (client, request) => { + const validatedRequest = GetAppsListRequestSchema.parse(request ?? {}); + return await client.get("/apps", { params: validatedRequest }); +}); + +export { getAppsList, safeGetAppsList }; diff --git a/js/src/actions/apps/index.ts b/js/src/actions/apps/index.ts new file mode 100644 index 00000000..ed557e3a --- /dev/null +++ b/js/src/actions/apps/index.ts @@ -0,0 +1,12 @@ +export { + getAppsList, + safeGetAppsList, + GetAppsListRequestSchema, + GetAppsListSchema, + AppCvmInfoSchema, + DstackAppSchema, + type GetAppsListRequest, + type GetAppsListResponse, + type DstackApp, + type AppCvmInfo, +} from "./get_apps_list"; diff --git a/js/src/actions/cvms/get_cvm_compose_file.test.ts b/js/src/actions/cvms/get_cvm_compose_file.test.ts index d81d34ac..a442be87 100644 --- a/js/src/actions/cvms/get_cvm_compose_file.test.ts +++ b/js/src/actions/cvms/get_cvm_compose_file.test.ts @@ -83,7 +83,7 @@ describe("getCvmComposeFile", () => { describe("request validation", () => { it("should validate identifier requirements", async () => { - await expect(getCvmComposeFile(mockClient as Client, {})).rejects.toThrow("One of id, uuid, app_id, or instance_id must be provided"); + await expect(getCvmComposeFile(mockClient as Client, {})).rejects.toThrow("One of id, uuid, app_id, instance_id, or name must be provided"); // Invalid UUID format await expect(getCvmComposeFile(mockClient as Client, { uuid: "invalid-uuid" })).rejects.toThrow("Invalid"); diff --git a/js/src/actions/cvms/get_cvm_info.test.ts b/js/src/actions/cvms/get_cvm_info.test.ts index c15a8e88..2c31e995 100644 --- a/js/src/actions/cvms/get_cvm_info.test.ts +++ b/js/src/actions/cvms/get_cvm_info.test.ts @@ -1,4 +1,5 @@ import { describe, it, expect, vi, beforeEach } from "vitest"; +import { mainnet } from "viem/chains"; import { createClient } from "../../client"; import { getCvmInfo, @@ -8,6 +9,7 @@ import { import type { CvmLegacyDetail } from "../../types/cvm_info"; // Mock response data matching the CvmLegacyDetailSchema structure +// Use mainnet from viem to ensure test stays in sync with library updates const mockCvmInfoData: CvmLegacyDetail = { id: 123, name: "test-cvm", @@ -42,38 +44,7 @@ const mockCvmInfoData: CvmLegacyDetail = { chain_id: 1, kms_contract_address: "0x1234567890123456789012345678901234567890", gateway_app_id: "gateway-123", - chain: { - id: 1, - name: "Ethereum", - nativeCurrency: { - name: "Ether", - symbol: "ETH", - decimals: 18 - }, - rpcUrls: { - default: { - http: ["https://eth.merkle.io"] - } - }, - blockExplorers: { - default: { - name: "Etherscan", - url: "https://etherscan.io", - apiUrl: "https://api.etherscan.io/api" - } - }, - blockTime: 12000, - contracts: { - ensUniversalResolver: { - address: "0xeeeeeeee14d718c2b47d9923deab1335e144eeee", - blockCreated: 23085558 - }, - multicall3: { - address: "0xca11bde05977b3631167028862be2a173976ca11", - blockCreated: 14353601 - } - } - } + chain: mainnet, }, contract_address: "0x9876543210987654321098765432109876543210", deployer_address: "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd", @@ -157,7 +128,7 @@ describe("getCvmInfo", () => { describe("request validation", () => { it("should validate identifier requirements", async () => { // No identifier provided - await expect(getCvmInfo(client, {})).rejects.toThrow("One of id, uuid, app_id, or instance_id must be provided"); + await expect(getCvmInfo(client, {})).rejects.toThrow("One of id, uuid, app_id, instance_id, or name must be provided"); // Invalid UUID format await expect(getCvmInfo(client, { uuid: "invalid-uuid" })).rejects.toThrow(); diff --git a/js/src/actions/cvms/get_cvm_list.test.ts b/js/src/actions/cvms/get_cvm_list.test.ts index 7d8af134..3288a942 100644 --- a/js/src/actions/cvms/get_cvm_list.test.ts +++ b/js/src/actions/cvms/get_cvm_list.test.ts @@ -1,4 +1,5 @@ import { describe, it, expect, vi, beforeEach } from "vitest"; +import { mainnet } from "viem/chains"; import { createClient } from "../../client"; import { getCvmList, @@ -8,6 +9,7 @@ import { import type { CvmInfo } from "../../types/cvm_info"; // Mock response data matching the API structure +// Use mainnet from viem to ensure test stays in sync with library updates const mockCvmData: CvmInfo = { hosted: { id: "vm-123", @@ -51,38 +53,7 @@ const mockCvmData: CvmInfo = { chain_id: 1, kms_contract_address: "0x1234567890123456789012345678901234567890", gateway_app_id: "gateway-123", - chain: { - id: 1, - name: "Ethereum", - nativeCurrency: { - name: "Ether", - symbol: "ETH", - decimals: 18 - }, - rpcUrls: { - default: { - http: ["https://eth.merkle.io"] - } - }, - blockExplorers: { - default: { - name: "Etherscan", - url: "https://etherscan.io", - apiUrl: "https://api.etherscan.io/api" - } - }, - blockTime: 12000, - contracts: { - ensUniversalResolver: { - address: "0xeeeeeeee14d718c2b47d9923deab1335e144eeee", - blockCreated: 23085558 - }, - multicall3: { - address: "0xca11bde05977b3631167028862be2a173976ca11", - blockCreated: 14353601 - } - } - } + chain: mainnet, }, vcpu: 2, memory: 4096, diff --git a/js/src/actions/index.ts b/js/src/actions/index.ts index 27d2a575..000bd506 100644 --- a/js/src/actions/index.ts +++ b/js/src/actions/index.ts @@ -338,3 +338,17 @@ export { GetCvmPreLaunchScriptRequestSchema, type GetCvmPreLaunchScriptRequest, } from "./cvms/get_cvm_prelaunch_script"; + +// Apps +export { + getAppsList, + safeGetAppsList, + GetAppsListRequestSchema, + GetAppsListSchema, + AppCvmInfoSchema, + DstackAppSchema, + type GetAppsListRequest, + type GetAppsListResponse, + type DstackApp, + type AppCvmInfo, +} from "./apps";