From c861a6a53c7f1d73af26a31bde8c345411011276 Mon Sep 17 00:00:00 2001 From: Yoav Farhi Date: Sat, 28 Mar 2026 23:37:55 +0300 Subject: [PATCH 1/3] feat(cli): add entity inspection commands and automation status Co-Authored-By: Claude Opus 4.6 (1M context) --- .../cli/src/cli/commands/automations/index.ts | 8 ++ .../src/cli/commands/automations/status.ts | 61 ++++++++++ .../cli/src/cli/commands/entities/count.ts | 26 +++++ packages/cli/src/cli/commands/entities/get.ts | 31 +++++ .../cli/src/cli/commands/entities/index.ts | 14 +++ .../cli/src/cli/commands/entities/list.ts | 106 ++++++++++++++++++ .../cli/src/cli/commands/entities/push.ts | 12 +- packages/cli/src/cli/program.ts | 8 +- .../cli/src/core/resources/entity/data-api.ts | 94 ++++++++++++++++ .../cli/src/core/resources/entity/index.ts | 1 + 10 files changed, 351 insertions(+), 10 deletions(-) create mode 100644 packages/cli/src/cli/commands/automations/index.ts create mode 100644 packages/cli/src/cli/commands/automations/status.ts create mode 100644 packages/cli/src/cli/commands/entities/count.ts create mode 100644 packages/cli/src/cli/commands/entities/get.ts create mode 100644 packages/cli/src/cli/commands/entities/index.ts create mode 100644 packages/cli/src/cli/commands/entities/list.ts create mode 100644 packages/cli/src/core/resources/entity/data-api.ts diff --git a/packages/cli/src/cli/commands/automations/index.ts b/packages/cli/src/cli/commands/automations/index.ts new file mode 100644 index 00000000..f02f31c9 --- /dev/null +++ b/packages/cli/src/cli/commands/automations/index.ts @@ -0,0 +1,8 @@ +import { Command } from "commander"; +import { getAutomationsStatusCommand } from "./status.js"; + +export function getAutomationsCommand(): Command { + return new Command("automations") + .description("Manage automations") + .addCommand(getAutomationsStatusCommand()); +} diff --git a/packages/cli/src/cli/commands/automations/status.ts b/packages/cli/src/cli/commands/automations/status.ts new file mode 100644 index 00000000..0233cdeb --- /dev/null +++ b/packages/cli/src/cli/commands/automations/status.ts @@ -0,0 +1,61 @@ +import type { Command } from "commander"; +import type { CLIContext, RunCommandResult } from "@/cli/types.js"; +import { Base44Command, runTask, theme } from "@/cli/utils/index.js"; +import { listDeployedFunctions } from "@/core/resources/function/api.js"; + +function formatAutomationType(automation: { + type: string; + schedule_mode?: string; + schedule_type?: string; +}): string { + if (automation.type === "entity") return "entity"; + if (automation.type === "scheduled") { + if (automation.schedule_mode === "one-time") return "scheduled (one-time)"; + if (automation.schedule_type === "cron") return "scheduled (cron)"; + return "scheduled (simple)"; + } + return automation.type; +} + +async function statusAction({ + log, +}: CLIContext): Promise { + const { functions } = await runTask( + "Fetching automations...", + async () => listDeployedFunctions(), + { errorMessage: "Failed to fetch automations" }, + ); + + let totalAutomations = 0; + + for (const fn of functions) { + if (fn.automations.length === 0) continue; + + for (const automation of fn.automations) { + totalAutomations++; + const active = automation.is_active; + const statusLabel = active + ? theme.styles.bold("active") + : theme.styles.dim("disabled"); + const typeLabel = formatAutomationType(automation); + + log.message( + ` ${fn.name} / ${automation.name} ${theme.styles.dim(typeLabel)} ${statusLabel}`, + ); + } + } + + if (totalAutomations === 0) { + return { outroMessage: "No automations found" }; + } + + return { + outroMessage: `${totalAutomations} automation${totalAutomations !== 1 ? "s" : ""} across ${functions.length} function${functions.length !== 1 ? "s" : ""}`, + }; +} + +export function getAutomationsStatusCommand(): Command { + return new Base44Command("status") + .description("Show status of all automations") + .action(statusAction); +} diff --git a/packages/cli/src/cli/commands/entities/count.ts b/packages/cli/src/cli/commands/entities/count.ts new file mode 100644 index 00000000..1bd34b33 --- /dev/null +++ b/packages/cli/src/cli/commands/entities/count.ts @@ -0,0 +1,26 @@ +import type { Command } from "commander"; +import type { CLIContext, RunCommandResult } from "@/cli/types.js"; +import { Base44Command, runTask } from "@/cli/utils/index.js"; +import { countEntityRecords } from "@/core/resources/entity/data-api.js"; + +async function countAction( + _ctx: CLIContext, + entityName: string, +): Promise { + const count = await runTask( + `Counting ${entityName} records...`, + async () => countEntityRecords(entityName), + { errorMessage: `Failed to count ${entityName} records` }, + ); + + return { + outroMessage: `${entityName} has ${count} record${count !== 1 ? "s" : ""}`, + }; +} + +export function getEntitiesCountCommand(): Command { + return new Base44Command("count") + .description("Count total records for an entity") + .argument("", "Name of the entity") + .action(countAction); +} diff --git a/packages/cli/src/cli/commands/entities/get.ts b/packages/cli/src/cli/commands/entities/get.ts new file mode 100644 index 00000000..f91bc41c --- /dev/null +++ b/packages/cli/src/cli/commands/entities/get.ts @@ -0,0 +1,31 @@ +import type { Command } from "commander"; +import type { CLIContext, RunCommandResult } from "@/cli/types.js"; +import { Base44Command, runTask } from "@/cli/utils/index.js"; +import { getEntityRecord } from "@/core/resources/entity/data-api.js"; + +async function getAction( + _ctx: CLIContext, + entityName: string, + id: string, +): Promise { + const record = await runTask( + `Fetching ${entityName} record...`, + async () => getEntityRecord(entityName, id), + { errorMessage: `Failed to fetch ${entityName} record "${id}"` }, + ); + + const formatted = JSON.stringify(record, null, 2); + + return { + outroMessage: `Fetched ${entityName} record ${id}`, + stdout: `${formatted}\n`, + }; +} + +export function getEntitiesGetCommand(): Command { + return new Base44Command("get") + .description("Get a single entity record by ID") + .argument("", "Name of the entity") + .argument("", "Record ID") + .action(getAction); +} diff --git a/packages/cli/src/cli/commands/entities/index.ts b/packages/cli/src/cli/commands/entities/index.ts new file mode 100644 index 00000000..bb1f2ee6 --- /dev/null +++ b/packages/cli/src/cli/commands/entities/index.ts @@ -0,0 +1,14 @@ +import { Command } from "commander"; +import { getEntitiesCountCommand } from "./count.js"; +import { getEntitiesGetCommand } from "./get.js"; +import { getEntitiesListCommand } from "./list.js"; +import { getEntitiesPushCommand } from "./push.js"; + +export function getEntitiesCommand(): Command { + return new Command("entities") + .description("Manage project entities") + .addCommand(getEntitiesPushCommand()) + .addCommand(getEntitiesListCommand()) + .addCommand(getEntitiesGetCommand()) + .addCommand(getEntitiesCountCommand()); +} diff --git a/packages/cli/src/cli/commands/entities/list.ts b/packages/cli/src/cli/commands/entities/list.ts new file mode 100644 index 00000000..1671b236 --- /dev/null +++ b/packages/cli/src/cli/commands/entities/list.ts @@ -0,0 +1,106 @@ +import type { Command } from "commander"; +import type { CLIContext, RunCommandResult } from "@/cli/types.js"; +import { Base44Command, runTask, theme } from "@/cli/utils/index.js"; +import { + type EntityRecord, + type ListEntityRecordsOptions, + listEntityRecords, +} from "@/core/resources/entity/data-api.js"; + +interface ListOptions { + limit?: string; + sort?: string; + skip?: string; +} + +function formatRecordRow(record: EntityRecord, fields: string[]): string { + const values = fields.map((field) => { + const value = record[field]; + if (value === undefined || value === null) return "-"; + if (typeof value === "object") return JSON.stringify(value); + return String(value); + }); + return values.join("\t"); +} + +function pickDisplayFields(records: EntityRecord[]): string[] { + const systemFields = ["id", "created_date"]; + if (records.length === 0) return systemFields; + + const skipFields = new Set([ + "id", + "created_date", + "updated_date", + "created_by", + "created_by_id", + "app_id", + "entity_name", + "is_deleted", + "deleted_date", + "environment", + "_id", + ]); + + const dataFields: string[] = []; + for (const key of Object.keys(records[0])) { + if (!skipFields.has(key) && dataFields.length < 4) { + dataFields.push(key); + } + } + + return [...systemFields, ...dataFields]; +} + +async function listAction( + { log }: CLIContext, + entityName: string, + options: ListOptions, +): Promise { + const apiOptions: ListEntityRecordsOptions = {}; + + if (options.limit) { + apiOptions.limit = Number.parseInt(options.limit, 10); + } else { + apiOptions.limit = 10; + } + + if (options.skip) { + apiOptions.skip = Number.parseInt(options.skip, 10); + } + + if (options.sort) { + apiOptions.sort = options.sort; + } + + const records = await runTask( + `Fetching ${entityName} records...`, + async () => listEntityRecords(entityName, apiOptions), + { errorMessage: `Failed to fetch ${entityName} records` }, + ); + + if (records.length === 0) { + return { outroMessage: `No records found for ${entityName}` }; + } + + const fields = pickDisplayFields(records); + const header = fields.join("\t"); + + log.message(theme.styles.dim(header)); + for (const record of records) { + log.message(` ${formatRecordRow(record, fields)}`); + } + + return { + outroMessage: `Showing ${records.length} ${entityName} record${records.length !== 1 ? "s" : ""}`, + }; +} + +export function getEntitiesListCommand(): Command { + return new Base44Command("list") + .description("List entity records") + .argument("", "Name of the entity") + .option("-n, --limit ", "Maximum number of records to return (default: 10)") + .option("--sort ", "Field to sort by") + .option("--skip ", "Number of records to skip") + .action(listAction); +} diff --git a/packages/cli/src/cli/commands/entities/push.ts b/packages/cli/src/cli/commands/entities/push.ts index 5e0851b5..d55345fd 100644 --- a/packages/cli/src/cli/commands/entities/push.ts +++ b/packages/cli/src/cli/commands/entities/push.ts @@ -1,4 +1,4 @@ -import { Command } from "commander"; +import type { Command } from "commander"; import type { CLIContext, RunCommandResult } from "@/cli/types.js"; import { Base44Command, runTask } from "@/cli/utils/index.js"; import { readProjectConfig } from "@/core/index.js"; @@ -42,11 +42,7 @@ async function pushEntitiesAction({ } export function getEntitiesPushCommand(): Command { - return new Command("entities") - .description("Manage project entities") - .addCommand( - new Base44Command("push") - .description("Push local entities to Base44") - .action(pushEntitiesAction), - ); + return new Base44Command("push") + .description("Push local entities to Base44") + .action(pushEntitiesAction); } diff --git a/packages/cli/src/cli/program.ts b/packages/cli/src/cli/program.ts index c5f8c3ff..aa6de4ee 100644 --- a/packages/cli/src/cli/program.ts +++ b/packages/cli/src/cli/program.ts @@ -6,7 +6,8 @@ import { getLogoutCommand } from "@/cli/commands/auth/logout.js"; 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 { getAutomationsCommand } from "@/cli/commands/automations/index.js"; +import { getEntitiesCommand } from "@/cli/commands/entities/index.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"; @@ -56,7 +57,7 @@ export function createProgram(context: CLIContext): Command { program.addCommand(getEjectCommand()); // Register entities commands - program.addCommand(getEntitiesPushCommand()); + program.addCommand(getEntitiesCommand()); // Register agents commands program.addCommand(getAgentsCommand()); @@ -79,6 +80,9 @@ export function createProgram(context: CLIContext): Command { // Register types command program.addCommand(getTypesCommand()); + // Register automations commands + program.addCommand(getAutomationsCommand()); + // Register exec command program.addCommand(getExecCommand()); diff --git a/packages/cli/src/core/resources/entity/data-api.ts b/packages/cli/src/core/resources/entity/data-api.ts new file mode 100644 index 00000000..db7e5fb4 --- /dev/null +++ b/packages/cli/src/core/resources/entity/data-api.ts @@ -0,0 +1,94 @@ +import type { KyResponse } from "ky"; +import { getAppClient } from "@/core/clients/index.js"; +import { ApiError } from "@/core/errors.js"; + +export interface EntityRecord { + id: string; + created_date: string; + updated_date?: string; + created_by?: string; + [key: string]: unknown; +} + +export interface ListEntityRecordsOptions { + limit?: number; + skip?: number; + sort?: string; +} + +export async function listEntityRecords( + entityName: string, + options: ListEntityRecordsOptions = {}, +): Promise { + const appClient = getAppClient(); + const searchParams = new URLSearchParams(); + + if (options.limit !== undefined) { + searchParams.set("limit", String(options.limit)); + } + if (options.skip !== undefined) { + searchParams.set("skip", String(options.skip)); + } + if (options.sort) { + searchParams.set("sort", options.sort); + } + + let response: KyResponse; + try { + response = await appClient.get( + `entities/${encodeURIComponent(entityName)}`, + { searchParams, timeout: 30_000 }, + ); + } catch (error) { + throw await ApiError.fromHttpError( + error, + `listing records for entity "${entityName}"`, + ); + } + + return (await response.json()) as EntityRecord[]; +} + +export async function getEntityRecord( + entityName: string, + id: string, +): Promise { + const appClient = getAppClient(); + + let response: KyResponse; + try { + response = await appClient.get( + `entities/${encodeURIComponent(entityName)}/${encodeURIComponent(id)}`, + { timeout: 30_000 }, + ); + } catch (error) { + throw await ApiError.fromHttpError( + error, + `fetching record "${id}" from entity "${entityName}"`, + ); + } + + return (await response.json()) as EntityRecord; +} + +export async function countEntityRecords( + entityName: string, +): Promise { + const appClient = getAppClient(); + + let response: KyResponse; + try { + response = await appClient.get( + `entities/${encodeURIComponent(entityName)}/count`, + { timeout: 30_000 }, + ); + } catch (error) { + throw await ApiError.fromHttpError( + error, + `counting records for entity "${entityName}"`, + ); + } + + const data = (await response.json()) as { count: number }; + return data.count; +} diff --git a/packages/cli/src/core/resources/entity/index.ts b/packages/cli/src/core/resources/entity/index.ts index 90b197a7..a873756e 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 "./data-api.js"; export * from "./deploy.js"; export * from "./resource.js"; export * from "./schema.js"; From 1190035d736a99ec2a3e01de0afbeb611592d1e7 Mon Sep 17 00:00:00 2001 From: Yoav Farhi Date: Sat, 28 Mar 2026 23:47:09 +0300 Subject: [PATCH 2/3] fix(cli): address code review feedback - Add Zod validation for entity data API responses - Fix automation status function count in outro message - Add input validation for --limit and --skip in entities list Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/cli/commands/automations/status.ts | 4 +- .../cli/src/cli/commands/entities/list.ts | 24 +++++++++ .../cli/src/core/resources/entity/data-api.ts | 52 ++++++++++++++----- 3 files changed, 67 insertions(+), 13 deletions(-) diff --git a/packages/cli/src/cli/commands/automations/status.ts b/packages/cli/src/cli/commands/automations/status.ts index 0233cdeb..b0b9d3a6 100644 --- a/packages/cli/src/cli/commands/automations/status.ts +++ b/packages/cli/src/cli/commands/automations/status.ts @@ -27,9 +27,11 @@ async function statusAction({ ); let totalAutomations = 0; + let functionsWithAutomations = 0; for (const fn of functions) { if (fn.automations.length === 0) continue; + functionsWithAutomations++; for (const automation of fn.automations) { totalAutomations++; @@ -50,7 +52,7 @@ async function statusAction({ } return { - outroMessage: `${totalAutomations} automation${totalAutomations !== 1 ? "s" : ""} across ${functions.length} function${functions.length !== 1 ? "s" : ""}`, + outroMessage: `${totalAutomations} automation${totalAutomations !== 1 ? "s" : ""} across ${functionsWithAutomations} function${functionsWithAutomations !== 1 ? "s" : ""}`, }; } diff --git a/packages/cli/src/cli/commands/entities/list.ts b/packages/cli/src/cli/commands/entities/list.ts index 1671b236..fe7bc414 100644 --- a/packages/cli/src/cli/commands/entities/list.ts +++ b/packages/cli/src/cli/commands/entities/list.ts @@ -1,6 +1,7 @@ import type { Command } from "commander"; import type { CLIContext, RunCommandResult } from "@/cli/types.js"; import { Base44Command, runTask, theme } from "@/cli/utils/index.js"; +import { InvalidInputError } from "@/core/errors.js"; import { type EntityRecord, type ListEntityRecordsOptions, @@ -51,11 +52,34 @@ function pickDisplayFields(records: EntityRecord[]): string[] { return [...systemFields, ...dataFields]; } +function validateLimit(limit: string | undefined): void { + if (limit === undefined) return; + const n = Number.parseInt(limit, 10); + if (Number.isNaN(n) || n < 1) { + throw new InvalidInputError( + `Invalid limit: "${limit}". Must be a positive integer.`, + ); + } +} + +function validateSkip(skip: string | undefined): void { + if (skip === undefined) return; + const n = Number.parseInt(skip, 10); + if (Number.isNaN(n) || n < 0) { + throw new InvalidInputError( + `Invalid skip: "${skip}". Must be a non-negative integer.`, + ); + } +} + async function listAction( { log }: CLIContext, entityName: string, options: ListOptions, ): Promise { + validateLimit(options.limit); + validateSkip(options.skip); + const apiOptions: ListEntityRecordsOptions = {}; if (options.limit) { diff --git a/packages/cli/src/core/resources/entity/data-api.ts b/packages/cli/src/core/resources/entity/data-api.ts index db7e5fb4..0c58ada7 100644 --- a/packages/cli/src/core/resources/entity/data-api.ts +++ b/packages/cli/src/core/resources/entity/data-api.ts @@ -1,14 +1,22 @@ import type { KyResponse } from "ky"; +import { z } from "zod"; import { getAppClient } from "@/core/clients/index.js"; -import { ApiError } from "@/core/errors.js"; +import { ApiError, SchemaValidationError } from "@/core/errors.js"; -export interface EntityRecord { - id: string; - created_date: string; - updated_date?: string; - created_by?: string; - [key: string]: unknown; -} +const EntityRecordSchema = z + .object({ + id: z.string(), + created_date: z.string(), + }) + .passthrough(); + +const EntityRecordListSchema = z.array(EntityRecordSchema); + +const CountResponseSchema = z.object({ + count: z.number(), +}); + +export type EntityRecord = z.infer; export interface ListEntityRecordsOptions { limit?: number; @@ -46,7 +54,14 @@ export async function listEntityRecords( ); } - return (await response.json()) as EntityRecord[]; + const result = EntityRecordListSchema.safeParse(await response.json()); + if (!result.success) { + throw new SchemaValidationError( + "Invalid response from server", + result.error, + ); + } + return result.data; } export async function getEntityRecord( @@ -68,7 +83,14 @@ export async function getEntityRecord( ); } - return (await response.json()) as EntityRecord; + const result = EntityRecordSchema.safeParse(await response.json()); + if (!result.success) { + throw new SchemaValidationError( + "Invalid response from server", + result.error, + ); + } + return result.data; } export async function countEntityRecords( @@ -89,6 +111,12 @@ export async function countEntityRecords( ); } - const data = (await response.json()) as { count: number }; - return data.count; + const result = CountResponseSchema.safeParse(await response.json()); + if (!result.success) { + throw new SchemaValidationError( + "Invalid response from server", + result.error, + ); + } + return result.data.count; } From 4a5c86baabefa30db52d0430e3ea15e97f7186fd Mon Sep 17 00:00:00 2001 From: Yoav Farhi Date: Sat, 28 Mar 2026 23:57:09 +0300 Subject: [PATCH 3/3] style: fix Biome lint formatting and import ordering Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/cli/src/cli/commands/automations/status.ts | 4 +--- packages/cli/src/cli/commands/entities/list.ts | 5 ++++- packages/cli/src/cli/program.ts | 2 +- packages/cli/src/core/resources/entity/data-api.ts | 4 +--- 4 files changed, 7 insertions(+), 8 deletions(-) diff --git a/packages/cli/src/cli/commands/automations/status.ts b/packages/cli/src/cli/commands/automations/status.ts index b0b9d3a6..de07e210 100644 --- a/packages/cli/src/cli/commands/automations/status.ts +++ b/packages/cli/src/cli/commands/automations/status.ts @@ -17,9 +17,7 @@ function formatAutomationType(automation: { return automation.type; } -async function statusAction({ - log, -}: CLIContext): Promise { +async function statusAction({ log }: CLIContext): Promise { const { functions } = await runTask( "Fetching automations...", async () => listDeployedFunctions(), diff --git a/packages/cli/src/cli/commands/entities/list.ts b/packages/cli/src/cli/commands/entities/list.ts index fe7bc414..20d8bfab 100644 --- a/packages/cli/src/cli/commands/entities/list.ts +++ b/packages/cli/src/cli/commands/entities/list.ts @@ -123,7 +123,10 @@ export function getEntitiesListCommand(): Command { return new Base44Command("list") .description("List entity records") .argument("", "Name of the entity") - .option("-n, --limit ", "Maximum number of records to return (default: 10)") + .option( + "-n, --limit ", + "Maximum number of records to return (default: 10)", + ) .option("--sort ", "Field to sort by") .option("--skip ", "Number of records to skip") .action(listAction); diff --git a/packages/cli/src/cli/program.ts b/packages/cli/src/cli/program.ts index aa6de4ee..07338f38 100644 --- a/packages/cli/src/cli/program.ts +++ b/packages/cli/src/cli/program.ts @@ -4,9 +4,9 @@ import { getAuthCommand } from "@/cli/commands/auth/index.js"; import { getLoginCommand } from "@/cli/commands/auth/login.js"; import { getLogoutCommand } from "@/cli/commands/auth/logout.js"; import { getWhoamiCommand } from "@/cli/commands/auth/whoami.js"; +import { getAutomationsCommand } from "@/cli/commands/automations/index.js"; import { getConnectorsCommand } from "@/cli/commands/connectors/index.js"; import { getDashboardCommand } from "@/cli/commands/dashboard/index.js"; -import { getAutomationsCommand } from "@/cli/commands/automations/index.js"; import { getEntitiesCommand } from "@/cli/commands/entities/index.js"; import { getFunctionsCommand } from "@/cli/commands/functions/index.js"; import { getCreateCommand } from "@/cli/commands/project/create.js"; diff --git a/packages/cli/src/core/resources/entity/data-api.ts b/packages/cli/src/core/resources/entity/data-api.ts index 0c58ada7..646d1434 100644 --- a/packages/cli/src/core/resources/entity/data-api.ts +++ b/packages/cli/src/core/resources/entity/data-api.ts @@ -93,9 +93,7 @@ export async function getEntityRecord( return result.data; } -export async function countEntityRecords( - entityName: string, -): Promise { +export async function countEntityRecords(entityName: string): Promise { const appClient = getAppClient(); let response: KyResponse;