From 77758f0d6c1dda5747f81155f66dc38ce275dc7e Mon Sep 17 00:00:00 2001 From: yardend Date: Sun, 8 Mar 2026 12:11:02 +0200 Subject: [PATCH 1/9] feat(functions): add `functions list` command Add a new `functions list` CLI command that lists all deployed functions on the remote, showing automation counts. Restructure the functions command group to use an index.ts that registers subcommands. - Add ListFunctionsResponseSchema and FunctionInfo types to schema - Add listDeployedFunctions API client function - Create functions/list.ts command with automation count display - Refactor functions/deploy.ts export to getDeployCommand - Create functions/index.ts to register deploy + list subcommands - Update program.ts to use getFunctionsCommand - Add test mocks and 5 tests for the list command Co-Authored-By: Claude Opus 4.6 --- .../cli/src/cli/commands/functions/deploy.ts | 24 +++-- .../cli/src/cli/commands/functions/index.ts | 11 +++ .../cli/src/cli/commands/functions/list.ts | 38 ++++++++ packages/cli/src/cli/program.ts | 4 +- .../cli/src/core/resources/function/api.ts | 22 +++++ .../cli/src/core/resources/function/schema.ts | 20 +++++ packages/cli/tests/cli/functions_list.spec.ts | 89 +++++++++++++++++++ .../cli/tests/cli/testkit/Base44APIMock.ts | 29 ++++++ 8 files changed, 221 insertions(+), 16 deletions(-) create mode 100644 packages/cli/src/cli/commands/functions/index.ts create mode 100644 packages/cli/src/cli/commands/functions/list.ts create mode 100644 packages/cli/tests/cli/functions_list.spec.ts diff --git a/packages/cli/src/cli/commands/functions/deploy.ts b/packages/cli/src/cli/commands/functions/deploy.ts index 4c89ac38..c87c7d8b 100644 --- a/packages/cli/src/cli/commands/functions/deploy.ts +++ b/packages/cli/src/cli/commands/functions/deploy.ts @@ -51,18 +51,14 @@ async function deployFunctionsAction(): Promise { return { outroMessage: "Functions deployed to Base44" }; } -export function getFunctionsDeployCommand(context: CLIContext): Command { - return new Command("functions") - .description("Manage project functions") - .addCommand( - new Command("deploy") - .description("Deploy local functions to Base44") - .action(async () => { - await runCommand( - deployFunctionsAction, - { requireAuth: true }, - context, - ); - }), - ); +export function getDeployCommand(context: CLIContext): Command { + return new Command("deploy") + .description("Deploy local functions to Base44") + .action(async () => { + await runCommand( + deployFunctionsAction, + { requireAuth: true }, + context, + ); + }); } diff --git a/packages/cli/src/cli/commands/functions/index.ts b/packages/cli/src/cli/commands/functions/index.ts new file mode 100644 index 00000000..8d7c7f0e --- /dev/null +++ b/packages/cli/src/cli/commands/functions/index.ts @@ -0,0 +1,11 @@ +import { Command } from "commander"; +import type { CLIContext } from "@/cli/types.js"; +import { getDeployCommand } from "./deploy.js"; +import { getListCommand } from "./list.js"; + +export function getFunctionsCommand(context: CLIContext): Command { + return new Command("functions") + .description("Manage backend functions") + .addCommand(getDeployCommand(context)) + .addCommand(getListCommand(context)); +} diff --git a/packages/cli/src/cli/commands/functions/list.ts b/packages/cli/src/cli/commands/functions/list.ts new file mode 100644 index 00000000..e8a09113 --- /dev/null +++ b/packages/cli/src/cli/commands/functions/list.ts @@ -0,0 +1,38 @@ +import { log } from "@clack/prompts"; +import { Command } from "commander"; +import type { CLIContext } from "@/cli/types.js"; +import { runCommand } from "@/cli/utils/index.js"; +import type { RunCommandResult } from "@/cli/utils/runCommand.js"; +import { theme } from "@/cli/utils/theme.js"; +import { listDeployedFunctions } from "@/core/resources/function/api.js"; + +async function listFunctionsAction(): Promise { + const { functions } = await listDeployedFunctions(); + + if (functions.length === 0) { + return { outroMessage: "No functions on remote" }; + } + + for (const fn of functions) { + const autoCount = fn.automations.length; + const autoLabel = + autoCount > 0 + ? theme.styles.dim( + ` (${autoCount} automation${autoCount > 1 ? "s" : ""})`, + ) + : ""; + log.message(` ${fn.name}${autoLabel}`); + } + + return { + outroMessage: `${functions.length} function${functions.length !== 1 ? "s" : ""} on remote`, + }; +} + +export function getListCommand(context: CLIContext): Command { + return new Command("list") + .description("List all deployed functions") + .action(async () => { + await runCommand(listFunctionsAction, { requireAuth: true }, context); + }); +} diff --git a/packages/cli/src/cli/program.ts b/packages/cli/src/cli/program.ts index e83edde4..a6d4ec02 100644 --- a/packages/cli/src/cli/program.ts +++ b/packages/cli/src/cli/program.ts @@ -6,7 +6,7 @@ import { getWhoamiCommand } from "@/cli/commands/auth/whoami.js"; import { getConnectorsCommand } from "@/cli/commands/connectors/index.js"; import { getDashboardCommand } from "@/cli/commands/dashboard/index.js"; import { getEntitiesPushCommand } from "@/cli/commands/entities/push.js"; -import { getFunctionsDeployCommand } from "@/cli/commands/functions/deploy.js"; +import { getFunctionsCommand } from "@/cli/commands/functions/index.js"; import { getCreateCommand } from "@/cli/commands/project/create.js"; import { getDeployCommand } from "@/cli/commands/project/deploy.js"; import { getLinkCommand } from "@/cli/commands/project/link.js"; @@ -55,7 +55,7 @@ export function createProgram(context: CLIContext): Command { program.addCommand(getConnectorsCommand(context)); // Register functions commands - program.addCommand(getFunctionsDeployCommand(context)); + program.addCommand(getFunctionsCommand(context)); // Register secrets commands program.addCommand(getSecretsCommand(context)); diff --git a/packages/cli/src/core/resources/function/api.ts b/packages/cli/src/core/resources/function/api.ts index 43e0dbf8..9a71b976 100644 --- a/packages/cli/src/core/resources/function/api.ts +++ b/packages/cli/src/core/resources/function/api.ts @@ -6,10 +6,12 @@ import type { FunctionLogFilters, FunctionLogsResponse, FunctionWithCode, + ListFunctionsResponse, } from "@/core/resources/function/schema.js"; import { DeployFunctionsResponseSchema, FunctionLogsResponseSchema, + ListFunctionsResponseSchema, } from "@/core/resources/function/schema.js"; function toDeployPayloadItem(fn: FunctionWithCode) { @@ -111,3 +113,23 @@ export async function fetchFunctionLogs( return result.data; } + +export async function listDeployedFunctions(): Promise { + const appClient = getAppClient(); + + let response: KyResponse; + try { + response = await appClient.get("backend-functions", { timeout: 30_000 }); + } catch (error) { + throw await ApiError.fromHttpError(error, "listing deployed functions"); + } + + const result = ListFunctionsResponseSchema.safeParse(await response.json()); + if (!result.success) { + throw new SchemaValidationError( + "Invalid response from server", + result.error, + ); + } + return result.data; +} diff --git a/packages/cli/src/core/resources/function/schema.ts b/packages/cli/src/core/resources/function/schema.ts index 50f355cb..2b87f9a2 100644 --- a/packages/cli/src/core/resources/function/schema.ts +++ b/packages/cli/src/core/resources/function/schema.ts @@ -93,12 +93,32 @@ export const DeployFunctionsResponseSchema = z.object({ .nullable(), }); +const FunctionAutomationInfoSchema = z.object({ + name: z.string(), + type: z.string(), + is_active: z.boolean(), +}); + +const FunctionInfoSchema = z.object({ + name: z.string(), + deployment_id: z.string(), + entry: z.string(), + files: z.array(FunctionFileSchema), + automations: z.array(FunctionAutomationInfoSchema), +}); + +export const ListFunctionsResponseSchema = z.object({ + functions: z.array(FunctionInfoSchema), +}); + export type FunctionConfig = z.infer; export type BackendFunction = z.infer; export type FunctionFile = z.infer; export type DeployFunctionsResponse = z.infer< typeof DeployFunctionsResponseSchema >; +export type FunctionInfo = z.infer; +export type ListFunctionsResponse = z.infer; export type FunctionWithCode = Omit & { files: FunctionFile[]; diff --git a/packages/cli/tests/cli/functions_list.spec.ts b/packages/cli/tests/cli/functions_list.spec.ts new file mode 100644 index 00000000..6a39a1d8 --- /dev/null +++ b/packages/cli/tests/cli/functions_list.spec.ts @@ -0,0 +1,89 @@ +import { describe, it } from "vitest"; +import { fixture, setupCLITests } from "./testkit/index.js"; + +describe("functions list command", () => { + const t = setupCLITests(); + + it("shows message when no functions deployed", async () => { + await t.givenLoggedInWithProject(fixture("basic")); + t.api.mockFunctionsList({ functions: [] }); + + const result = await t.run("functions", "list"); + + t.expectResult(result).toSucceed(); + t.expectResult(result).toContain("No functions on remote"); + }); + + it("lists deployed functions", async () => { + await t.givenLoggedInWithProject(fixture("basic")); + t.api.mockFunctionsList({ + functions: [ + { + name: "func-a", + deployment_id: "d1", + entry: "index.ts", + files: [{ path: "index.ts", content: "" }], + automations: [], + }, + { + name: "func-b", + deployment_id: "d2", + entry: "index.ts", + files: [{ path: "index.ts", content: "" }], + automations: [], + }, + ], + }); + + const result = await t.run("functions", "list"); + + t.expectResult(result).toSucceed(); + t.expectResult(result).toContain("func-a"); + t.expectResult(result).toContain("func-b"); + t.expectResult(result).toContain("2 functions on remote"); + }); + + it("shows automation count for functions with automations", async () => { + await t.givenLoggedInWithProject(fixture("basic")); + t.api.mockFunctionsList({ + functions: [ + { + name: "func-a", + deployment_id: "d1", + entry: "index.ts", + files: [{ path: "index.ts", content: "" }], + automations: [ + { name: "auto1", type: "scheduled", is_active: true }, + ], + }, + ], + }); + + const result = await t.run("functions", "list"); + + t.expectResult(result).toSucceed(); + t.expectResult(result).toContain("func-a"); + t.expectResult(result).toContain("1 automation"); + t.expectResult(result).toContain("1 function on remote"); + }); + + it("fails when not in a project directory", async () => { + await t.givenLoggedIn({ email: "test@example.com", name: "Test User" }); + + const result = await t.run("functions", "list"); + + t.expectResult(result).toFail(); + }); + + it("fails when API returns error", async () => { + await t.givenLoggedInWithProject(fixture("basic")); + t.api.mockFunctionsListError({ + status: 500, + body: { error: "Server error" }, + }); + + const result = await t.run("functions", "list"); + + t.expectResult(result).toFail(); + }); +}); diff --git a/packages/cli/tests/cli/testkit/Base44APIMock.ts b/packages/cli/tests/cli/testkit/Base44APIMock.ts index 9c68f21b..8a55f20e 100644 --- a/packages/cli/tests/cli/testkit/Base44APIMock.ts +++ b/packages/cli/tests/cli/testkit/Base44APIMock.ts @@ -38,6 +38,16 @@ interface FunctionsPushResponse { errors: Array<{ name: string; message: string }> | null; } +interface FunctionsListResponse { + functions: Array<{ + name: string; + deployment_id: string; + entry: string; + files: Array<{ path: string; content: string }>; + automations: Array<{ name: string; type: string; is_active: boolean }>; + }>; +} + interface SiteDeployResponse { app_url: string; } @@ -189,6 +199,16 @@ export class Base44APIMock { return this; } + /** Mock GET /api/apps/{appId}/backend-functions - List deployed functions */ + mockFunctionsList(response: FunctionsListResponse): this { + this.handlers.push( + http.get(`${BASE_URL}/api/apps/${this.appId}/backend-functions`, () => + HttpResponse.json(response), + ), + ); + return this; + } + /** Mock POST /api/apps/{appId}/deploy-dist - Deploy site */ mockSiteDeploy(response: SiteDeployResponse): this { this.handlers.push( @@ -384,6 +404,15 @@ export class Base44APIMock { ); } + /** Mock functions list to return an error */ + mockFunctionsListError(error: ErrorResponse): this { + return this.mockError( + "get", + `/api/apps/${this.appId}/backend-functions`, + error, + ); + } + /** Mock site deploy to return an error */ mockSiteDeployError(error: ErrorResponse): this { return this.mockError("post", `/api/apps/${this.appId}/deploy-dist`, error); From 5c77e6057afa9e343d73e53656dc4cf7ad8c393d Mon Sep 17 00:00:00 2001 From: yardend Date: Mon, 9 Mar 2026 10:24:10 +0200 Subject: [PATCH 2/9] fix: lint formatting and remove unused FunctionInfo type Co-Authored-By: Claude Opus 4.6 --- packages/cli/src/cli/commands/functions/deploy.ts | 6 +----- packages/cli/src/core/resources/function/schema.ts | 1 - packages/cli/tests/cli/functions_list.spec.ts | 4 +--- 3 files changed, 2 insertions(+), 9 deletions(-) diff --git a/packages/cli/src/cli/commands/functions/deploy.ts b/packages/cli/src/cli/commands/functions/deploy.ts index c87c7d8b..9f57321a 100644 --- a/packages/cli/src/cli/commands/functions/deploy.ts +++ b/packages/cli/src/cli/commands/functions/deploy.ts @@ -55,10 +55,6 @@ export function getDeployCommand(context: CLIContext): Command { return new Command("deploy") .description("Deploy local functions to Base44") .action(async () => { - await runCommand( - deployFunctionsAction, - { requireAuth: true }, - context, - ); + await runCommand(deployFunctionsAction, { requireAuth: true }, context); }); } diff --git a/packages/cli/src/core/resources/function/schema.ts b/packages/cli/src/core/resources/function/schema.ts index 2b87f9a2..711335a9 100644 --- a/packages/cli/src/core/resources/function/schema.ts +++ b/packages/cli/src/core/resources/function/schema.ts @@ -117,7 +117,6 @@ export type FunctionFile = z.infer; export type DeployFunctionsResponse = z.infer< typeof DeployFunctionsResponseSchema >; -export type FunctionInfo = z.infer; export type ListFunctionsResponse = z.infer; export type FunctionWithCode = Omit & { diff --git a/packages/cli/tests/cli/functions_list.spec.ts b/packages/cli/tests/cli/functions_list.spec.ts index 6a39a1d8..fc549f5e 100644 --- a/packages/cli/tests/cli/functions_list.spec.ts +++ b/packages/cli/tests/cli/functions_list.spec.ts @@ -52,9 +52,7 @@ describe("functions list command", () => { deployment_id: "d1", entry: "index.ts", files: [{ path: "index.ts", content: "" }], - automations: [ - { name: "auto1", type: "scheduled", is_active: true }, - ], + automations: [{ name: "auto1", type: "scheduled", is_active: true }], }, ], }); From 4d7fddee695c536dbb5390dc066521bf12da867a Mon Sep 17 00:00:00 2001 From: yardend Date: Sun, 8 Mar 2026 12:35:58 +0200 Subject: [PATCH 3/9] feat(functions): add `functions pull` command Add a new `functions pull` CLI command that pulls deployed functions from Base44 to local directories. Supports pulling all functions or a specific one by name, with unchanged detection to skip up-to-date files. - Add ListFunctionsResponseSchema and FunctionInfo types to schema - Add listDeployedFunctions API client function - Create core pull logic with writeFunctions and isFunctionUnchanged - Create functions/pull.ts command with single-function support - Refactor functions command group with index.ts - Add test mocks and 6 tests for the pull command Co-Authored-By: Claude Opus 4.6 --- .../cli/src/cli/commands/functions/deploy.ts | 24 ++-- .../cli/src/cli/commands/functions/index.ts | 11 ++ .../cli/src/cli/commands/functions/pull.ts | 79 ++++++++++++++ packages/cli/src/cli/program.ts | 4 +- .../cli/src/core/resources/entity/index.ts | 1 + .../cli/src/core/resources/function/api.ts | 22 ++++ .../cli/src/core/resources/function/pull.ts | 103 ++++++++++++++++++ .../cli/src/core/resources/function/schema.ts | 20 ++++ packages/cli/tests/cli/functions_pull.spec.ts | 95 ++++++++++++++++ .../cli/tests/cli/testkit/Base44APIMock.ts | 29 +++++ 10 files changed, 372 insertions(+), 16 deletions(-) create mode 100644 packages/cli/src/cli/commands/functions/index.ts create mode 100644 packages/cli/src/cli/commands/functions/pull.ts create mode 100644 packages/cli/src/core/resources/function/pull.ts create mode 100644 packages/cli/tests/cli/functions_pull.spec.ts diff --git a/packages/cli/src/cli/commands/functions/deploy.ts b/packages/cli/src/cli/commands/functions/deploy.ts index 4c89ac38..c87c7d8b 100644 --- a/packages/cli/src/cli/commands/functions/deploy.ts +++ b/packages/cli/src/cli/commands/functions/deploy.ts @@ -51,18 +51,14 @@ async function deployFunctionsAction(): Promise { return { outroMessage: "Functions deployed to Base44" }; } -export function getFunctionsDeployCommand(context: CLIContext): Command { - return new Command("functions") - .description("Manage project functions") - .addCommand( - new Command("deploy") - .description("Deploy local functions to Base44") - .action(async () => { - await runCommand( - deployFunctionsAction, - { requireAuth: true }, - context, - ); - }), - ); +export function getDeployCommand(context: CLIContext): Command { + return new Command("deploy") + .description("Deploy local functions to Base44") + .action(async () => { + await runCommand( + deployFunctionsAction, + { requireAuth: true }, + context, + ); + }); } diff --git a/packages/cli/src/cli/commands/functions/index.ts b/packages/cli/src/cli/commands/functions/index.ts new file mode 100644 index 00000000..6d20c490 --- /dev/null +++ b/packages/cli/src/cli/commands/functions/index.ts @@ -0,0 +1,11 @@ +import { Command } from "commander"; +import type { CLIContext } from "@/cli/types.js"; +import { getDeployCommand } from "./deploy.js"; +import { getPullCommand } from "./pull.js"; + +export function getFunctionsCommand(context: CLIContext): Command { + return new Command("functions") + .description("Manage backend functions") + .addCommand(getDeployCommand(context)) + .addCommand(getPullCommand(context)); +} diff --git a/packages/cli/src/cli/commands/functions/pull.ts b/packages/cli/src/cli/commands/functions/pull.ts new file mode 100644 index 00000000..4ef97f75 --- /dev/null +++ b/packages/cli/src/cli/commands/functions/pull.ts @@ -0,0 +1,79 @@ +import { dirname, join } from "node:path"; +import { log } from "@clack/prompts"; +import { Command } from "commander"; +import type { CLIContext } from "@/cli/types.js"; +import { runCommand, runTask } from "@/cli/utils/index.js"; +import type { RunCommandResult } from "@/cli/utils/runCommand.js"; +import { readProjectConfig } from "@/core/index.js"; +import { listDeployedFunctions } from "@/core/resources/function/api.js"; +import { writeFunctions } from "@/core/resources/function/pull.js"; + +async function pullFunctionsAction( + name: string | undefined, +): Promise { + const { project } = await readProjectConfig(); + + const configDir = dirname(project.configPath); + const functionsDir = join(configDir, project.functionsDir); + + const remoteFunctions = await runTask( + "Fetching functions from Base44", + async () => { + const { functions } = await listDeployedFunctions(); + return functions; + }, + { + successMessage: "Functions fetched successfully", + errorMessage: "Failed to fetch functions", + }, + ); + + const toPull = name + ? remoteFunctions.filter((f) => f.name === name) + : remoteFunctions; + + if (name && toPull.length === 0) { + return { + outroMessage: `Function "${name}" not found on remote`, + }; + } + + if (toPull.length === 0) { + return { outroMessage: "No functions found on remote" }; + } + + const { written, skipped } = await runTask( + "Writing function files", + async () => { + return await writeFunctions(functionsDir, toPull); + }, + { + successMessage: "Function files written successfully", + errorMessage: "Failed to write function files", + }, + ); + + for (const name of written) { + log.success(`${name.padEnd(25)} written`); + } + for (const name of skipped) { + log.info(`${name.padEnd(25)} unchanged`); + } + + return { + outroMessage: `Pulled ${toPull.length} function${toPull.length !== 1 ? "s" : ""} to ${functionsDir}`, + }; +} + +export function getPullCommand(context: CLIContext): Command { + return new Command("pull") + .description("Pull deployed functions from Base44") + .argument("[name]", "Pull a single function by name") + .action(async (name: string | undefined) => { + await runCommand( + () => pullFunctionsAction(name), + { requireAuth: true }, + context, + ); + }); +} diff --git a/packages/cli/src/cli/program.ts b/packages/cli/src/cli/program.ts index e83edde4..a6d4ec02 100644 --- a/packages/cli/src/cli/program.ts +++ b/packages/cli/src/cli/program.ts @@ -6,7 +6,7 @@ import { getWhoamiCommand } from "@/cli/commands/auth/whoami.js"; import { getConnectorsCommand } from "@/cli/commands/connectors/index.js"; import { getDashboardCommand } from "@/cli/commands/dashboard/index.js"; import { getEntitiesPushCommand } from "@/cli/commands/entities/push.js"; -import { getFunctionsDeployCommand } from "@/cli/commands/functions/deploy.js"; +import { getFunctionsCommand } from "@/cli/commands/functions/index.js"; import { getCreateCommand } from "@/cli/commands/project/create.js"; import { getDeployCommand } from "@/cli/commands/project/deploy.js"; import { getLinkCommand } from "@/cli/commands/project/link.js"; @@ -55,7 +55,7 @@ export function createProgram(context: CLIContext): Command { program.addCommand(getConnectorsCommand(context)); // Register functions commands - program.addCommand(getFunctionsDeployCommand(context)); + program.addCommand(getFunctionsCommand(context)); // Register secrets commands program.addCommand(getSecretsCommand(context)); diff --git a/packages/cli/src/core/resources/entity/index.ts b/packages/cli/src/core/resources/entity/index.ts index 90b197a7..08b63274 100644 --- a/packages/cli/src/core/resources/entity/index.ts +++ b/packages/cli/src/core/resources/entity/index.ts @@ -1,5 +1,6 @@ export * from "./api.js"; export * from "./config.js"; export * from "./deploy.js"; +export * from "./pull.js"; export * from "./resource.js"; export * from "./schema.js"; diff --git a/packages/cli/src/core/resources/function/api.ts b/packages/cli/src/core/resources/function/api.ts index 43e0dbf8..9a71b976 100644 --- a/packages/cli/src/core/resources/function/api.ts +++ b/packages/cli/src/core/resources/function/api.ts @@ -6,10 +6,12 @@ import type { FunctionLogFilters, FunctionLogsResponse, FunctionWithCode, + ListFunctionsResponse, } from "@/core/resources/function/schema.js"; import { DeployFunctionsResponseSchema, FunctionLogsResponseSchema, + ListFunctionsResponseSchema, } from "@/core/resources/function/schema.js"; function toDeployPayloadItem(fn: FunctionWithCode) { @@ -111,3 +113,23 @@ export async function fetchFunctionLogs( return result.data; } + +export async function listDeployedFunctions(): Promise { + const appClient = getAppClient(); + + let response: KyResponse; + try { + response = await appClient.get("backend-functions", { timeout: 30_000 }); + } catch (error) { + throw await ApiError.fromHttpError(error, "listing deployed functions"); + } + + const result = ListFunctionsResponseSchema.safeParse(await response.json()); + if (!result.success) { + throw new SchemaValidationError( + "Invalid response from server", + result.error, + ); + } + return result.data; +} diff --git a/packages/cli/src/core/resources/function/pull.ts b/packages/cli/src/core/resources/function/pull.ts new file mode 100644 index 00000000..7c45803a --- /dev/null +++ b/packages/cli/src/core/resources/function/pull.ts @@ -0,0 +1,103 @@ +import { join } from "node:path"; +import type { FunctionInfo } from "@/core/resources/function/schema.js"; +import { + pathExists, + readJsonFile, + readTextFile, + writeFile, + writeJsonFile, +} from "@/core/utils/fs.js"; + +export interface WriteFunctionsResult { + written: string[]; + skipped: string[]; +} + +/** + * Writes remote function data to local function directories. + * Creates function.jsonc config and all source files for each function. + * Skips functions whose local files already match remote content. + */ +export async function writeFunctions( + functionsDir: string, + functions: FunctionInfo[], +): Promise { + const written: string[] = []; + const skipped: string[] = []; + + for (const fn of functions) { + const functionDir = join(functionsDir, fn.name); + const configPath = join(functionDir, "function.jsonc"); + + // Check if all files already match remote content + if (await isFunctionUnchanged(functionDir, fn)) { + skipped.push(fn.name); + continue; + } + + // Write function config + const config: Record = { + name: fn.name, + entry: fn.entry, + }; + if (fn.automations.length > 0) { + config.automations = fn.automations; + } + await writeJsonFile(configPath, config); + + // Write all source files + for (const file of fn.files) { + await writeFile(join(functionDir, file.path), file.content); + } + + written.push(fn.name); + } + + return { written, skipped }; +} + +async function isFunctionUnchanged( + functionDir: string, + fn: FunctionInfo, +): Promise { + if (!(await pathExists(functionDir))) { + return false; + } + + // Compare function config (entry, automations) + const configPath = join(functionDir, "function.jsonc"); + try { + const localConfig = (await readJsonFile(configPath)) as Record< + string, + unknown + >; + if (localConfig.entry !== fn.entry) { + return false; + } + const localAuto = JSON.stringify(localConfig.automations ?? []); + const remoteAuto = JSON.stringify(fn.automations); + if (localAuto !== remoteAuto) { + return false; + } + } catch { + return false; + } + + // Compare source files + for (const file of fn.files) { + const filePath = join(functionDir, file.path); + if (!(await pathExists(filePath))) { + return false; + } + try { + const localContent = await readTextFile(filePath); + if (localContent !== file.content) { + return false; + } + } catch { + return false; + } + } + + return true; +} diff --git a/packages/cli/src/core/resources/function/schema.ts b/packages/cli/src/core/resources/function/schema.ts index 50f355cb..2b87f9a2 100644 --- a/packages/cli/src/core/resources/function/schema.ts +++ b/packages/cli/src/core/resources/function/schema.ts @@ -93,12 +93,32 @@ export const DeployFunctionsResponseSchema = z.object({ .nullable(), }); +const FunctionAutomationInfoSchema = z.object({ + name: z.string(), + type: z.string(), + is_active: z.boolean(), +}); + +const FunctionInfoSchema = z.object({ + name: z.string(), + deployment_id: z.string(), + entry: z.string(), + files: z.array(FunctionFileSchema), + automations: z.array(FunctionAutomationInfoSchema), +}); + +export const ListFunctionsResponseSchema = z.object({ + functions: z.array(FunctionInfoSchema), +}); + export type FunctionConfig = z.infer; export type BackendFunction = z.infer; export type FunctionFile = z.infer; export type DeployFunctionsResponse = z.infer< typeof DeployFunctionsResponseSchema >; +export type FunctionInfo = z.infer; +export type ListFunctionsResponse = z.infer; export type FunctionWithCode = Omit & { files: FunctionFile[]; diff --git a/packages/cli/tests/cli/functions_pull.spec.ts b/packages/cli/tests/cli/functions_pull.spec.ts new file mode 100644 index 00000000..26868294 --- /dev/null +++ b/packages/cli/tests/cli/functions_pull.spec.ts @@ -0,0 +1,95 @@ +import { describe, it } from "vitest"; +import { fixture, setupCLITests } from "./testkit/index.js"; + +describe("functions pull command", () => { + const t = setupCLITests(); + + it("reports no functions when remote has none", async () => { + await t.givenLoggedInWithProject(fixture("basic")); + t.api.mockFunctionsList({ functions: [] }); + + const result = await t.run("functions", "pull"); + + t.expectResult(result).toSucceed(); + t.expectResult(result).toContain("No functions found on remote"); + }); + + it("pulls functions successfully", async () => { + await t.givenLoggedInWithProject(fixture("basic")); + t.api.mockFunctionsList({ + functions: [ + { + name: "my-func", + deployment_id: "d1", + entry: "index.ts", + files: [{ path: "index.ts", content: "Deno.serve(() => {})" }], + automations: [], + }, + ], + }); + + const result = await t.run("functions", "pull"); + + t.expectResult(result).toSucceed(); + t.expectResult(result).toContain("Functions fetched successfully"); + t.expectResult(result).toContain("Function files written successfully"); + t.expectResult(result).toContain("Pulled 1 function"); + }); + + it("pulls a single function by name", async () => { + await t.givenLoggedInWithProject(fixture("basic")); + t.api.mockFunctionsList({ + functions: [ + { + name: "func-a", + deployment_id: "d1", + entry: "index.ts", + files: [{ path: "index.ts", content: "Deno.serve(() => {})" }], + automations: [], + }, + { + name: "func-b", + deployment_id: "d2", + entry: "index.ts", + files: [{ path: "index.ts", content: "Deno.serve(() => {})" }], + automations: [], + }, + ], + }); + + const result = await t.run("functions", "pull", "func-a"); + + t.expectResult(result).toSucceed(); + t.expectResult(result).toContain("Pulled 1 function"); + }); + + it("reports function not found on remote", async () => { + await t.givenLoggedInWithProject(fixture("basic")); + t.api.mockFunctionsList({ functions: [] }); + + const result = await t.run("functions", "pull", "nonexistent"); + + t.expectResult(result).toSucceed(); + t.expectResult(result).toContain("not found on remote"); + }); + + it("fails when not in a project directory", async () => { + await t.givenLoggedIn({ email: "test@example.com", name: "Test User" }); + + const result = await t.run("functions", "pull"); + + t.expectResult(result).toFail(); + }); + + it("fails when API returns error", async () => { + await t.givenLoggedInWithProject(fixture("basic")); + t.api.mockFunctionsListError({ + status: 500, + body: { error: "Server error" }, + }); + + const result = await t.run("functions", "pull"); + + t.expectResult(result).toFail(); + }); +}); diff --git a/packages/cli/tests/cli/testkit/Base44APIMock.ts b/packages/cli/tests/cli/testkit/Base44APIMock.ts index 9c68f21b..8a55f20e 100644 --- a/packages/cli/tests/cli/testkit/Base44APIMock.ts +++ b/packages/cli/tests/cli/testkit/Base44APIMock.ts @@ -38,6 +38,16 @@ interface FunctionsPushResponse { errors: Array<{ name: string; message: string }> | null; } +interface FunctionsListResponse { + functions: Array<{ + name: string; + deployment_id: string; + entry: string; + files: Array<{ path: string; content: string }>; + automations: Array<{ name: string; type: string; is_active: boolean }>; + }>; +} + interface SiteDeployResponse { app_url: string; } @@ -189,6 +199,16 @@ export class Base44APIMock { return this; } + /** Mock GET /api/apps/{appId}/backend-functions - List deployed functions */ + mockFunctionsList(response: FunctionsListResponse): this { + this.handlers.push( + http.get(`${BASE_URL}/api/apps/${this.appId}/backend-functions`, () => + HttpResponse.json(response), + ), + ); + return this; + } + /** Mock POST /api/apps/{appId}/deploy-dist - Deploy site */ mockSiteDeploy(response: SiteDeployResponse): this { this.handlers.push( @@ -384,6 +404,15 @@ export class Base44APIMock { ); } + /** Mock functions list to return an error */ + mockFunctionsListError(error: ErrorResponse): this { + return this.mockError( + "get", + `/api/apps/${this.appId}/backend-functions`, + error, + ); + } + /** Mock site deploy to return an error */ mockSiteDeployError(error: ErrorResponse): this { return this.mockError("post", `/api/apps/${this.appId}/deploy-dist`, error); From 01f45fa9648359ecea8d75fcf2d944955ec1839c Mon Sep 17 00:00:00 2001 From: yardend Date: Mon, 9 Mar 2026 10:28:12 +0200 Subject: [PATCH 4/9] fix: lint formatting and remove unused WriteFunctionsResult export Co-Authored-By: Claude Opus 4.6 --- packages/cli/src/cli/commands/functions/deploy.ts | 6 +----- packages/cli/src/core/resources/entity/index.ts | 1 - packages/cli/src/core/resources/function/index.ts | 1 + packages/cli/src/core/resources/function/pull.ts | 2 +- 4 files changed, 3 insertions(+), 7 deletions(-) diff --git a/packages/cli/src/cli/commands/functions/deploy.ts b/packages/cli/src/cli/commands/functions/deploy.ts index c87c7d8b..9f57321a 100644 --- a/packages/cli/src/cli/commands/functions/deploy.ts +++ b/packages/cli/src/cli/commands/functions/deploy.ts @@ -55,10 +55,6 @@ export function getDeployCommand(context: CLIContext): Command { return new Command("deploy") .description("Deploy local functions to Base44") .action(async () => { - await runCommand( - deployFunctionsAction, - { requireAuth: true }, - context, - ); + await runCommand(deployFunctionsAction, { requireAuth: true }, context); }); } diff --git a/packages/cli/src/core/resources/entity/index.ts b/packages/cli/src/core/resources/entity/index.ts index 08b63274..90b197a7 100644 --- a/packages/cli/src/core/resources/entity/index.ts +++ b/packages/cli/src/core/resources/entity/index.ts @@ -1,6 +1,5 @@ export * from "./api.js"; export * from "./config.js"; export * from "./deploy.js"; -export * from "./pull.js"; export * from "./resource.js"; export * from "./schema.js"; diff --git a/packages/cli/src/core/resources/function/index.ts b/packages/cli/src/core/resources/function/index.ts index 90b197a7..08b63274 100644 --- a/packages/cli/src/core/resources/function/index.ts +++ b/packages/cli/src/core/resources/function/index.ts @@ -1,5 +1,6 @@ export * from "./api.js"; export * from "./config.js"; export * from "./deploy.js"; +export * from "./pull.js"; export * from "./resource.js"; export * from "./schema.js"; diff --git a/packages/cli/src/core/resources/function/pull.ts b/packages/cli/src/core/resources/function/pull.ts index 7c45803a..b27c3eae 100644 --- a/packages/cli/src/core/resources/function/pull.ts +++ b/packages/cli/src/core/resources/function/pull.ts @@ -8,7 +8,7 @@ import { writeJsonFile, } from "@/core/utils/fs.js"; -export interface WriteFunctionsResult { +interface WriteFunctionsResult { written: string[]; skipped: string[]; } From cc7f93563c44e2f42cc09a6b2e9726929f304401 Mon Sep 17 00:00:00 2001 From: yardend Date: Mon, 9 Mar 2026 17:29:43 +0200 Subject: [PATCH 5/9] fix: transform snake_case fields to camelCase in function schemas Co-Authored-By: Claude Opus 4.6 --- .../cli/src/core/resources/function/schema.ts | 40 +++++++++++++------ 1 file changed, 27 insertions(+), 13 deletions(-) diff --git a/packages/cli/src/core/resources/function/schema.ts b/packages/cli/src/core/resources/function/schema.ts index 2b87f9a2..295dc152 100644 --- a/packages/cli/src/core/resources/function/schema.ts +++ b/packages/cli/src/core/resources/function/schema.ts @@ -93,19 +93,33 @@ export const DeployFunctionsResponseSchema = z.object({ .nullable(), }); -const FunctionAutomationInfoSchema = z.object({ - name: z.string(), - type: z.string(), - is_active: z.boolean(), -}); - -const FunctionInfoSchema = z.object({ - name: z.string(), - deployment_id: z.string(), - entry: z.string(), - files: z.array(FunctionFileSchema), - automations: z.array(FunctionAutomationInfoSchema), -}); +const FunctionAutomationInfoSchema = z + .object({ + name: z.string(), + type: z.string(), + is_active: z.boolean(), + }) + .transform((data) => ({ + name: data.name, + type: data.type, + isActive: data.is_active, + })); + +const FunctionInfoSchema = z + .object({ + name: z.string(), + deployment_id: z.string(), + entry: z.string(), + files: z.array(FunctionFileSchema), + automations: z.array(FunctionAutomationInfoSchema), + }) + .transform((data) => ({ + name: data.name, + deploymentId: data.deployment_id, + entry: data.entry, + files: data.files, + automations: data.automations, + })); export const ListFunctionsResponseSchema = z.object({ functions: z.array(FunctionInfoSchema), From 0777843355eb41ad0a48bb1612bb3c0dd31e7b6d Mon Sep 17 00:00:00 2001 From: yardend Date: Tue, 10 Mar 2026 15:28:08 +0200 Subject: [PATCH 6/9] =?UTF-8?q?fix:=20camelCase=20transforms,=20runTask=20?= =?UTF-8?q?spinner,=20rename=20auto=E2=86=92automation=20vars?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.6 --- .../cli/src/cli/commands/functions/list.ts | 16 ++++---- .../cli/src/core/resources/function/schema.ts | 40 +++++++++++++------ 2 files changed, 36 insertions(+), 20 deletions(-) diff --git a/packages/cli/src/cli/commands/functions/list.ts b/packages/cli/src/cli/commands/functions/list.ts index e8a09113..ab8bd45d 100644 --- a/packages/cli/src/cli/commands/functions/list.ts +++ b/packages/cli/src/cli/commands/functions/list.ts @@ -1,27 +1,29 @@ import { log } from "@clack/prompts"; import { Command } from "commander"; import type { CLIContext } from "@/cli/types.js"; -import { runCommand } from "@/cli/utils/index.js"; +import { runCommand, runTask } from "@/cli/utils/index.js"; import type { RunCommandResult } from "@/cli/utils/runCommand.js"; import { theme } from "@/cli/utils/theme.js"; import { listDeployedFunctions } from "@/core/resources/function/api.js"; async function listFunctionsAction(): Promise { - const { functions } = await listDeployedFunctions(); + const { functions } = await runTask("Fetching functions...", async () => + listDeployedFunctions(), + ); if (functions.length === 0) { return { outroMessage: "No functions on remote" }; } for (const fn of functions) { - const autoCount = fn.automations.length; - const autoLabel = - autoCount > 0 + const automationCount = fn.automations.length; + const automationLabel = + automationCount > 0 ? theme.styles.dim( - ` (${autoCount} automation${autoCount > 1 ? "s" : ""})`, + ` (${automationCount} automation${automationCount > 1 ? "s" : ""})`, ) : ""; - log.message(` ${fn.name}${autoLabel}`); + log.message(` ${fn.name}${automationLabel}`); } return { diff --git a/packages/cli/src/core/resources/function/schema.ts b/packages/cli/src/core/resources/function/schema.ts index 711335a9..854f57bb 100644 --- a/packages/cli/src/core/resources/function/schema.ts +++ b/packages/cli/src/core/resources/function/schema.ts @@ -93,19 +93,33 @@ export const DeployFunctionsResponseSchema = z.object({ .nullable(), }); -const FunctionAutomationInfoSchema = z.object({ - name: z.string(), - type: z.string(), - is_active: z.boolean(), -}); - -const FunctionInfoSchema = z.object({ - name: z.string(), - deployment_id: z.string(), - entry: z.string(), - files: z.array(FunctionFileSchema), - automations: z.array(FunctionAutomationInfoSchema), -}); +const FunctionAutomationInfoSchema = z + .object({ + name: z.string(), + type: z.string(), + is_active: z.boolean(), + }) + .transform((data) => ({ + name: data.name, + type: data.type, + isActive: data.is_active, + })); + +const FunctionInfoSchema = z + .object({ + name: z.string(), + deployment_id: z.string(), + entry: z.string(), + files: z.array(FunctionFileSchema), + automations: z.array(FunctionAutomationInfoSchema), + }) + .transform((data) => ({ + name: data.name, + deploymentId: data.deployment_id, + entry: data.entry, + files: data.files, + automations: data.automations, + })); export const ListFunctionsResponseSchema = z.object({ functions: z.array(FunctionInfoSchema), From 6318969f9027dbe5e761e8470cda2850470bd1db Mon Sep 17 00:00:00 2001 From: yardend Date: Tue, 10 Mar 2026 17:41:42 +0200 Subject: [PATCH 7/9] fix: remove unused mockSingleFunctionDelete methods from TestAPIServer These methods belong to the functions-delete branch, not functions-pull. They were accidentally included during parallel merge conflict resolution. Co-Authored-By: Claude Opus 4.6 --- .../cli/tests/cli/testkit/TestAPIServer.ts | 21 ------------------- 1 file changed, 21 deletions(-) diff --git a/packages/cli/tests/cli/testkit/TestAPIServer.ts b/packages/cli/tests/cli/testkit/TestAPIServer.ts index a096ae11..bf88b996 100644 --- a/packages/cli/tests/cli/testkit/TestAPIServer.ts +++ b/packages/cli/tests/cli/testkit/TestAPIServer.ts @@ -300,18 +300,6 @@ export class TestAPIServer { ); } - /** Mock DELETE /api/apps/{appId}/backend-functions/{name} - Delete single function */ - mockSingleFunctionDelete(): this { - this.pendingRoutes.push({ - method: "DELETE", - path: `/api/apps/${this.appId}/backend-functions/:name`, - handler: (_req, res) => { - res.status(204).end(); - }, - }); - return this; - } - mockSiteDeploy(response: SiteDeployResponse): this { return this.addRoute( "POST", @@ -469,15 +457,6 @@ export class TestAPIServer { ); } - /** Mock single function delete to return an error */ - mockSingleFunctionDeleteError(error: ErrorResponse): this { - return this.addErrorRoute( - "DELETE", - `/api/apps/${this.appId}/backend-functions/:name`, - error, - ); - } - mockSiteDeployError(error: ErrorResponse): this { return this.addErrorRoute( "POST", From 3d3b75dfbeb0aa20b4a3b760c6e02fc1d1f44db0 Mon Sep 17 00:00:00 2001 From: yardend Date: Tue, 10 Mar 2026 18:27:08 +0200 Subject: [PATCH 8/9] refactor: use isDeepStrictEqual for pull comparison, add pull tests - Replace JSON.stringify with isDeepStrictEqual from node:util for automations comparison, matching agents/connectors pattern - Add tests for: multiple files with imports, nested folders, automations, and skip-unchanged behavior Co-Authored-By: Claude Opus 4.6 --- .../cli/src/core/resources/function/pull.ts | 5 +- packages/cli/tests/cli/functions_pull.spec.ts | 139 +++++++++++++++++- 2 files changed, 140 insertions(+), 4 deletions(-) diff --git a/packages/cli/src/core/resources/function/pull.ts b/packages/cli/src/core/resources/function/pull.ts index b27c3eae..69abfc90 100644 --- a/packages/cli/src/core/resources/function/pull.ts +++ b/packages/cli/src/core/resources/function/pull.ts @@ -1,4 +1,5 @@ import { join } from "node:path"; +import { isDeepStrictEqual } from "node:util"; import type { FunctionInfo } from "@/core/resources/function/schema.js"; import { pathExists, @@ -74,9 +75,7 @@ async function isFunctionUnchanged( if (localConfig.entry !== fn.entry) { return false; } - const localAuto = JSON.stringify(localConfig.automations ?? []); - const remoteAuto = JSON.stringify(fn.automations); - if (localAuto !== remoteAuto) { + if (!isDeepStrictEqual(localConfig.automations ?? [], fn.automations)) { return false; } } catch { diff --git a/packages/cli/tests/cli/functions_pull.spec.ts b/packages/cli/tests/cli/functions_pull.spec.ts index 26868294..1fd28e25 100644 --- a/packages/cli/tests/cli/functions_pull.spec.ts +++ b/packages/cli/tests/cli/functions_pull.spec.ts @@ -1,4 +1,4 @@ -import { describe, it } from "vitest"; +import { describe, expect, it } from "vitest"; import { fixture, setupCLITests } from "./testkit/index.js"; describe("functions pull command", () => { @@ -92,4 +92,141 @@ describe("functions pull command", () => { t.expectResult(result).toFail(); }); + + it("pulls function with multiple files (imports)", async () => { + await t.givenLoggedInWithProject(fixture("basic")); + t.api.mockFunctionsList({ + functions: [ + { + name: "with-imports", + deployment_id: "d1", + entry: "index.ts", + files: [ + { + path: "index.ts", + content: + 'import { helper } from "./utils/helper.ts";\nDeno.serve(() => helper())', + }, + { + path: "utils/helper.ts", + content: "export function helper() { return 'ok'; }", + }, + ], + automations: [], + }, + ], + }); + + const result = await t.run("functions", "pull"); + + t.expectResult(result).toSucceed(); + t.expectResult(result).toContain("Pulled 1 function"); + const entry = await t.readProjectFile( + "base44/functions/with-imports/index.ts", + ); + expect(entry).toContain("import { helper }"); + const helper = await t.readProjectFile( + "base44/functions/with-imports/utils/helper.ts", + ); + expect(helper).toContain("export function helper"); + }); + + it("pulls function with nested folder structure", async () => { + await t.givenLoggedInWithProject(fixture("basic")); + t.api.mockFunctionsList({ + functions: [ + { + name: "nested-func", + deployment_id: "d1", + entry: "src/main.ts", + files: [ + { + path: "src/main.ts", + content: + 'import { db } from "./lib/db.ts";\nDeno.serve(() => db())', + }, + { + path: "src/lib/db.ts", + content: "export function db() { return 'connected'; }", + }, + { + path: "src/lib/types.ts", + content: "export type Config = { key: string };", + }, + ], + automations: [], + }, + ], + }); + + const result = await t.run("functions", "pull"); + + t.expectResult(result).toSucceed(); + expect(await t.fileExists("base44/functions/nested-func/src/main.ts")).toBe( + true, + ); + expect( + await t.fileExists("base44/functions/nested-func/src/lib/db.ts"), + ).toBe(true); + expect( + await t.fileExists("base44/functions/nested-func/src/lib/types.ts"), + ).toBe(true); + }); + + it("pulls function with automations", async () => { + await t.givenLoggedInWithProject(fixture("basic")); + t.api.mockFunctionsList({ + functions: [ + { + name: "scheduled-func", + deployment_id: "d1", + entry: "index.ts", + files: [{ path: "index.ts", content: "Deno.serve(() => {})" }], + automations: [ + { + name: "daily-run", + type: "scheduled", + is_active: true, + }, + ], + }, + ], + }); + + const result = await t.run("functions", "pull"); + + t.expectResult(result).toSucceed(); + const config = await t.readProjectFile( + "base44/functions/scheduled-func/function.jsonc", + ); + expect(config).toContain("automations"); + expect(config).toContain("daily-run"); + }); + + it("skips unchanged functions on second pull", async () => { + await t.givenLoggedInWithProject(fixture("basic")); + const mockData = { + functions: [ + { + name: "stable-func", + deployment_id: "d1", + entry: "index.ts", + files: [{ path: "index.ts", content: "Deno.serve(() => {})" }], + automations: [], + }, + ], + }; + + // First pull — writes files + t.api.mockFunctionsList(mockData); + const first = await t.run("functions", "pull"); + t.expectResult(first).toSucceed(); + t.expectResult(first).toContain("written"); + + // Second pull — should skip unchanged + t.api.mockFunctionsList(mockData); + const second = await t.run("functions", "pull"); + t.expectResult(second).toSucceed(); + t.expectResult(second).toContain("unchanged"); + }); }); From e8fa31eb3951236b34e02e005a1b251aa19b02cc Mon Sep 17 00:00:00 2001 From: yardend Date: Wed, 11 Mar 2026 13:03:45 +0200 Subject: [PATCH 9/9] fix: use full AutomationSchema for list endpoint response parsing Replace FunctionAutomationInfoSchema (3-field summary) with the existing full AutomationSchema so list response correctly parses all automation fields. Add entity automation to test mock alongside scheduled. Co-Authored-By: Claude Opus 4.6 --- .../cli/src/core/resources/function/schema.ts | 14 +------------ packages/cli/tests/cli/functions_list.spec.ts | 20 +++++++++++++++++-- .../cli/tests/cli/testkit/TestAPIServer.ts | 2 +- 3 files changed, 20 insertions(+), 16 deletions(-) diff --git a/packages/cli/src/core/resources/function/schema.ts b/packages/cli/src/core/resources/function/schema.ts index 854f57bb..d62d6f1e 100644 --- a/packages/cli/src/core/resources/function/schema.ts +++ b/packages/cli/src/core/resources/function/schema.ts @@ -93,25 +93,13 @@ export const DeployFunctionsResponseSchema = z.object({ .nullable(), }); -const FunctionAutomationInfoSchema = z - .object({ - name: z.string(), - type: z.string(), - is_active: z.boolean(), - }) - .transform((data) => ({ - name: data.name, - type: data.type, - isActive: data.is_active, - })); - const FunctionInfoSchema = z .object({ name: z.string(), deployment_id: z.string(), entry: z.string(), files: z.array(FunctionFileSchema), - automations: z.array(FunctionAutomationInfoSchema), + automations: z.array(AutomationSchema), }) .transform((data) => ({ name: data.name, diff --git a/packages/cli/tests/cli/functions_list.spec.ts b/packages/cli/tests/cli/functions_list.spec.ts index fc549f5e..9487a1f7 100644 --- a/packages/cli/tests/cli/functions_list.spec.ts +++ b/packages/cli/tests/cli/functions_list.spec.ts @@ -52,7 +52,23 @@ describe("functions list command", () => { deployment_id: "d1", entry: "index.ts", files: [{ path: "index.ts", content: "" }], - automations: [{ name: "auto1", type: "scheduled", is_active: true }], + automations: [ + { + name: "daily-cron", + type: "scheduled", + schedule_mode: "recurring", + schedule_type: "cron", + cron_expression: "0 9 * * *", + is_active: true, + }, + { + name: "on-order-create", + type: "entity", + entity_name: "orders", + event_types: ["create", "update"], + is_active: true, + }, + ], }, ], }); @@ -61,7 +77,7 @@ describe("functions list command", () => { t.expectResult(result).toSucceed(); t.expectResult(result).toContain("func-a"); - t.expectResult(result).toContain("1 automation"); + t.expectResult(result).toContain("2 automations"); t.expectResult(result).toContain("1 function on remote"); }); diff --git a/packages/cli/tests/cli/testkit/TestAPIServer.ts b/packages/cli/tests/cli/testkit/TestAPIServer.ts index f143513c..135b7ae1 100644 --- a/packages/cli/tests/cli/testkit/TestAPIServer.ts +++ b/packages/cli/tests/cli/testkit/TestAPIServer.ts @@ -81,7 +81,7 @@ interface FunctionsListResponse { deployment_id: string; entry: string; files: Array<{ path: string; content: string }>; - automations: Array<{ name: string; type: string; is_active: boolean }>; + automations: Record[]; }>; }