From bc90d186fe6cad99e564e29352ead1a7ea68d8fe Mon Sep 17 00:00:00 2001 From: Kfir Strikovsky Date: Mon, 16 Mar 2026 16:18:06 +0200 Subject: [PATCH 01/12] refactor runCommand --- docs/commands.md | 72 ++++---- packages/cli/src/cli/commands/agents/index.ts | 7 +- packages/cli/src/cli/commands/agents/pull.ts | 18 +- packages/cli/src/cli/commands/agents/push.ts | 18 +- .../cli/src/cli/commands/auth/login-flow.ts | 2 +- packages/cli/src/cli/commands/auth/login.ts | 16 +- packages/cli/src/cli/commands/auth/logout.ts | 14 +- packages/cli/src/cli/commands/auth/whoami.ts | 22 +-- .../cli/src/cli/commands/connectors/index.ts | 9 +- .../cli/commands/connectors/list-available.ts | 17 +- .../cli/src/cli/commands/connectors/pull.ts | 18 +- .../cli/src/cli/commands/connectors/push.ts | 23 ++- .../cli/src/cli/commands/dashboard/index.ts | 5 +- .../cli/src/cli/commands/dashboard/open.ts | 22 ++- packages/cli/src/cli/commands/dev.ts | 20 +-- .../cli/src/cli/commands/entities/push.ts | 16 +- .../cli/src/cli/commands/functions/delete.ts | 20 +-- .../cli/src/cli/commands/functions/deploy.ts | 25 ++- .../cli/src/cli/commands/functions/index.ts | 11 +- .../cli/src/cli/commands/functions/list.ts | 20 +-- .../cli/src/cli/commands/functions/pull.ts | 20 +-- .../cli/src/cli/commands/project/create.ts | 31 ++-- .../cli/src/cli/commands/project/deploy.ts | 26 ++- .../cli/src/cli/commands/project/eject.ts | 26 +-- packages/cli/src/cli/commands/project/link.ts | 17 +- packages/cli/src/cli/commands/project/logs.ts | 17 +- .../cli/src/cli/commands/secrets/delete.ts | 20 +-- .../cli/src/cli/commands/secrets/index.ts | 9 +- packages/cli/src/cli/commands/secrets/list.ts | 18 +- packages/cli/src/cli/commands/secrets/set.ts | 20 +-- packages/cli/src/cli/commands/site/deploy.ts | 29 ++-- packages/cli/src/cli/commands/site/index.ts | 7 +- packages/cli/src/cli/commands/site/open.ts | 18 +- .../cli/src/cli/commands/types/generate.ts | 22 +-- packages/cli/src/cli/commands/types/index.ts | 5 +- packages/cli/src/cli/program.ts | 42 +++-- .../src/cli/utils/command/Base44Command.ts | 159 ++++++++++++++++++ packages/cli/src/cli/utils/command/display.ts | 107 ++++++++++++ packages/cli/src/cli/utils/command/index.ts | 3 + .../cli/src/cli/utils/command/middleware.ts | 37 ++++ packages/cli/src/cli/utils/index.ts | 2 +- packages/cli/src/cli/utils/runCommand.ts | 147 ---------------- 42 files changed, 627 insertions(+), 530 deletions(-) create mode 100644 packages/cli/src/cli/utils/command/Base44Command.ts create mode 100644 packages/cli/src/cli/utils/command/display.ts create mode 100644 packages/cli/src/cli/utils/command/index.ts create mode 100644 packages/cli/src/cli/utils/command/middleware.ts delete mode 100644 packages/cli/src/cli/utils/runCommand.ts diff --git a/docs/commands.md b/docs/commands.md index 8086d3c1..94523875 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -1,6 +1,6 @@ # Adding & Modifying CLI Commands -**Keywords:** command, factory pattern, CLIContext, isNonInteractive, runCommand, runTask, spinner, theming, chalk, program.ts, register, banner, intro, outro +**Keywords:** command, factory pattern, CLIContext, isNonInteractive, Base44Command, runTask, spinner, theming, chalk, program.ts, register, banner, intro, outro Commands live in `src/cli/commands//`. They use a **factory pattern** with dependency injection via `CLIContext`. @@ -8,11 +8,10 @@ Commands live in `src/cli/commands//`. They use a **factory pattern** wi ```typescript // src/cli/commands//.ts -import { Command } from "commander"; import { log } from "@clack/prompts"; +import type { Command } from "commander"; import type { CLIContext } from "@/cli/types.js"; -import { runCommand, runTask, theme } from "@/cli/utils/index.js"; -import type { RunCommandResult } from "@/cli/utils/runCommand.js"; +import { Base44Command, type RunCommandResult, runTask, theme } from "@/cli/utils/index.js"; async function myAction(): Promise { const result = await runTask( @@ -33,20 +32,39 @@ async function myAction(): Promise { } export function getMyCommand(context: CLIContext): Command { - return new Command("") + return new Base44Command("", context) .description("") .option("-f, --flag", "Some flag") - .action(async (options) => { - await runCommand(myAction, { requireAuth: true }, context); - }); + .action(myAction); } ``` **Key rules**: - Export a **factory function** (`getMyCommand`), not a static command instance -- The factory receives `CLIContext` (contains `errorReporter` and `isNonInteractive`) -- Commands must NOT call `intro()` or `outro()` directly -- `runCommand()` handles both -- Always pass `context` as the third argument to `runCommand()` +- The factory receives `CLIContext` (contains `errorReporter`, `isNonInteractive`, `distribution`) +- Use `Base44Command` instead of `Command` — it automatically handles intro/outro, auth, app config, and error display +- Commands must NOT call `intro()` or `outro()` directly +- The action function must return `RunCommandResult` with an `outroMessage` + +## Base44Command Options + +Pass options as the third argument to the constructor: + +```typescript +new Base44Command("my-cmd", context) // All defaults +new Base44Command("my-cmd", context, { requireAuth: false }) // Skip auth check +new Base44Command("my-cmd", context, { requireAppConfig: false }) // Skip app config loading +new Base44Command("my-cmd", context, { fullBanner: true }) // ASCII art banner +new Base44Command("my-cmd", context, { requireAuth: false, requireAppConfig: false }) +``` + +| Option | Default | Description | +|--------|---------|-------------| +| `requireAuth` | `true` | Check authentication before running, auto-triggers login if needed | +| `requireAppConfig` | `true` | Load `.app.jsonc` and cache for sync access via `getAppConfig()` | +| `fullBanner` | `false` | Show ASCII art banner instead of simple intro tag | + +When `context.isNonInteractive` is `true` (CI, piped output), all clack UI (intro, outro, themed errors) is automatically skipped. Errors go to stderr as plain text. ## Registering a Command @@ -59,33 +77,19 @@ import { getMyCommand } from "@/cli/commands//.js"; program.addCommand(getMyCommand(context)); ``` -## runCommand Options - -```typescript -await runCommand(myAction, undefined, context); // Standard (loads app config) -await runCommand(myAction, { fullBanner: true }, context); // ASCII art banner -await runCommand(myAction, { requireAuth: true }, context); // Auto-login if needed -await runCommand(myAction, { requireAppConfig: false }, context); // Skip app config loading -await runCommand(myAction, { fullBanner: true, requireAuth: true }, context); -``` - -- `fullBanner` - Show ASCII art banner instead of simple tag (for special commands like `create`) -- `requireAuth` - Check authentication before running, auto-triggers login if needed -- `requireAppConfig` - Load `.app.jsonc` and cache for sync access (default: `true`) - ## CLIContext (Dependency Injection) ```typescript export interface CLIContext { errorReporter: ErrorReporter; isNonInteractive: boolean; + distribution: Distribution; } ``` - Created once in `runCLI()` at startup -- `isNonInteractive` is `true` when stdin/stdout are not a TTY (e.g., CI, piped output, AI agents). Use it to skip interactive prompts, browser opens, and animations. +- `isNonInteractive` is `true` when stdin/stdout are not a TTY (e.g., CI, piped output, AI agents). Use it to skip interactive prompts, browser opens, and animations. Also controls quiet mode — when true, all clack UI is suppressed. - Passed to `createProgram(context)`, which passes it to each command factory -- Commands pass it to `runCommand()` for error reporting integration ### Using `isNonInteractive` @@ -93,14 +97,10 @@ Pass `context.isNonInteractive` to your action when the command has interactive ```typescript export function getMyCommand(context: CLIContext): Command { - return new Command("open") + return new Base44Command("open", context) .description("Open something in browser") .action(async () => { - await runCommand( - () => myAction(context.isNonInteractive), - { requireAuth: true }, - context, - ); + return await myAction(context.isNonInteractive); }); } @@ -183,12 +183,12 @@ function validateInput(command: Command): void { } export function getMyCommand(context: CLIContext): Command { - return new Command("my-cmd") + return new Base44Command("my-cmd", context) .argument("[entries...]", "Input entries") .option("--flag-a ", "Alternative input") .hook("preAction", validateInput) .action(async (entries, options) => { - await runCommand(() => myAction(entries, options), { requireAuth: true }, context); + return await myAction(entries, options); }); } ``` @@ -198,7 +198,7 @@ Access `command.args` for positional arguments and `command.opts()` for options ## Rules (Command-Specific) - **Command factory pattern** - Commands export `getXCommand(context)` functions, not static instances -- **Command wrapper** - All commands use `runCommand(fn, options, context)` utility +- **Use `Base44Command`** - All commands use `new Base44Command(name, context, options)` instead of `new Command()`. The lifecycle (intro, auth, config, outro, error handling) is automatic. - **Task wrapper** - Use `runTask()` for async operations with spinners - **Use theme for styling** - Never use `chalk` directly; import `theme` from `@/cli/utils/` and use semantic names - **Use fs.ts utilities** - Always use `@/core/utils/fs.js` for file operations diff --git a/packages/cli/src/cli/commands/agents/index.ts b/packages/cli/src/cli/commands/agents/index.ts index 5bf65f1c..49d60bc5 100644 --- a/packages/cli/src/cli/commands/agents/index.ts +++ b/packages/cli/src/cli/commands/agents/index.ts @@ -1,11 +1,10 @@ import { Command } from "commander"; -import type { CLIContext } from "@/cli/types.js"; import { getAgentsPullCommand } from "./pull.js"; import { getAgentsPushCommand } from "./push.js"; -export function getAgentsCommand(context: CLIContext): Command { +export function getAgentsCommand(): Command { return new Command("agents") .description("Manage project agents") - .addCommand(getAgentsPushCommand(context)) - .addCommand(getAgentsPullCommand(context)); + .addCommand(getAgentsPushCommand()) + .addCommand(getAgentsPullCommand()); } diff --git a/packages/cli/src/cli/commands/agents/pull.ts b/packages/cli/src/cli/commands/agents/pull.ts index f92da337..9db072c1 100644 --- a/packages/cli/src/cli/commands/agents/pull.ts +++ b/packages/cli/src/cli/commands/agents/pull.ts @@ -1,11 +1,13 @@ import { dirname, join } from "node:path"; import { log } from "@clack/prompts"; -import { Command } from "commander"; -import type { CLIContext } from "@/cli/types.js"; +import type { Command } from "commander"; +import { + Base44Command, + type RunCommandResult, + runTask, +} from "@/cli/utils/index.js"; import { readProjectConfig } from "@/core/index.js"; import { fetchAgents, writeAgents } from "@/core/resources/agent/index.js"; -import { runCommand, runTask } from "../../utils/index.js"; -import type { RunCommandResult } from "../../utils/runCommand.js"; async function pullAgentsAction(): Promise { const { project } = await readProjectConfig(); @@ -50,12 +52,10 @@ async function pullAgentsAction(): Promise { }; } -export function getAgentsPullCommand(context: CLIContext): Command { - return new Command("pull") +export function getAgentsPullCommand(): Command { + return new Base44Command("pull") .description( "Pull agents from Base44 to local files (replaces all local agent configs)", ) - .action(async () => { - await runCommand(pullAgentsAction, { requireAuth: true }, context); - }); + .action(pullAgentsAction); } diff --git a/packages/cli/src/cli/commands/agents/push.ts b/packages/cli/src/cli/commands/agents/push.ts index 92f4c0c7..9136b3ca 100644 --- a/packages/cli/src/cli/commands/agents/push.ts +++ b/packages/cli/src/cli/commands/agents/push.ts @@ -1,10 +1,12 @@ import { log } from "@clack/prompts"; -import { Command } from "commander"; -import type { CLIContext } from "@/cli/types.js"; +import type { Command } from "commander"; +import { + Base44Command, + type RunCommandResult, + runTask, +} from "@/cli/utils/index.js"; import { readProjectConfig } from "@/core/index.js"; import { pushAgents } from "@/core/resources/agent/index.js"; -import { runCommand, runTask } from "../../utils/index.js"; -import type { RunCommandResult } from "../../utils/runCommand.js"; async function pushAgentsAction(): Promise { const { agents } = await readProjectConfig(); @@ -39,12 +41,10 @@ async function pushAgentsAction(): Promise { return { outroMessage: "Agents pushed to Base44" }; } -export function getAgentsPushCommand(context: CLIContext): Command { - return new Command("push") +export function getAgentsPushCommand(): Command { + return new Base44Command("push") .description( "Push local agents to Base44 (replaces all remote agent configs)", ) - .action(async () => { - await runCommand(pushAgentsAction, { requireAuth: true }, context); - }); + .action(pushAgentsAction); } diff --git a/packages/cli/src/cli/commands/auth/login-flow.ts b/packages/cli/src/cli/commands/auth/login-flow.ts index 01766e9f..34cc2893 100644 --- a/packages/cli/src/cli/commands/auth/login-flow.ts +++ b/packages/cli/src/cli/commands/auth/login-flow.ts @@ -1,7 +1,7 @@ import { log } from "@clack/prompts"; import pWaitFor from "p-wait-for"; +import type { RunCommandResult } from "@/cli/utils/index.js"; import { runTask } from "@/cli/utils/index.js"; -import type { RunCommandResult } from "@/cli/utils/runCommand.js"; import { theme } from "@/cli/utils/theme.js"; import type { DeviceCodeResponse, diff --git a/packages/cli/src/cli/commands/auth/login.ts b/packages/cli/src/cli/commands/auth/login.ts index 623d1d11..860921ab 100644 --- a/packages/cli/src/cli/commands/auth/login.ts +++ b/packages/cli/src/cli/commands/auth/login.ts @@ -1,12 +1,12 @@ -import { Command } from "commander"; -import type { CLIContext } from "@/cli/types.js"; -import { runCommand } from "@/cli/utils/index.js"; +import type { Command } from "commander"; +import { Base44Command } from "@/cli/utils/index.js"; import { login } from "./login-flow.js"; -export function getLoginCommand(context: CLIContext): Command { - return new Command("login") +export function getLoginCommand(): Command { + return new Base44Command("login", { + requireAuth: false, + requireAppConfig: false, + }) .description("Authenticate with Base44") - .action(async () => { - await runCommand(login, { requireAppConfig: false }, context); - }); + .action(login); } diff --git a/packages/cli/src/cli/commands/auth/logout.ts b/packages/cli/src/cli/commands/auth/logout.ts index 540c15fd..3d91748c 100644 --- a/packages/cli/src/cli/commands/auth/logout.ts +++ b/packages/cli/src/cli/commands/auth/logout.ts @@ -1,7 +1,5 @@ -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 type { Command } from "commander"; +import { Base44Command, type RunCommandResult } from "@/cli/utils/index.js"; import { deleteAuth } from "@/core/auth/index.js"; async function logout(): Promise { @@ -9,10 +7,8 @@ async function logout(): Promise { return { outroMessage: "Logged out successfully" }; } -export function getLogoutCommand(context: CLIContext): Command { - return new Command("logout") +export function getLogoutCommand(): Command { + return new Base44Command("logout", { requireAppConfig: false }) .description("Logout from current device") - .action(async () => { - await runCommand(logout, { requireAppConfig: false }, context); - }); + .action(logout); } diff --git a/packages/cli/src/cli/commands/auth/whoami.ts b/packages/cli/src/cli/commands/auth/whoami.ts index aad381c6..9c668433 100644 --- a/packages/cli/src/cli/commands/auth/whoami.ts +++ b/packages/cli/src/cli/commands/auth/whoami.ts @@ -1,7 +1,9 @@ -import { Command } from "commander"; -import type { CLIContext } from "@/cli/types.js"; -import { runCommand, theme } from "@/cli/utils/index.js"; -import type { RunCommandResult } from "@/cli/utils/runCommand.js"; +import type { Command } from "commander"; +import { + Base44Command, + type RunCommandResult, + theme, +} from "@/cli/utils/index.js"; import { readAuth } from "@/core/auth/index.js"; async function whoami(): Promise { @@ -9,14 +11,8 @@ async function whoami(): Promise { return { outroMessage: `Logged in as: ${theme.styles.bold(auth.email)}` }; } -export function getWhoamiCommand(context: CLIContext): Command { - return new Command("whoami") +export function getWhoamiCommand(): Command { + return new Base44Command("whoami", { requireAppConfig: false }) .description("Display current authenticated user") - .action(async () => { - await runCommand( - whoami, - { requireAuth: true, requireAppConfig: false }, - context, - ); - }); + .action(whoami); } diff --git a/packages/cli/src/cli/commands/connectors/index.ts b/packages/cli/src/cli/commands/connectors/index.ts index c4324722..62370189 100644 --- a/packages/cli/src/cli/commands/connectors/index.ts +++ b/packages/cli/src/cli/commands/connectors/index.ts @@ -1,13 +1,12 @@ import { Command } from "commander"; -import type { CLIContext } from "@/cli/types.js"; import { getConnectorsListAvailableCommand } from "./list-available.js"; import { getConnectorsPullCommand } from "./pull.js"; import { getConnectorsPushCommand } from "./push.js"; -export function getConnectorsCommand(context: CLIContext): Command { +export function getConnectorsCommand(): Command { return new Command("connectors") .description("Manage project connectors (OAuth integrations)") - .addCommand(getConnectorsListAvailableCommand(context)) - .addCommand(getConnectorsPullCommand(context)) - .addCommand(getConnectorsPushCommand(context)); + .addCommand(getConnectorsListAvailableCommand()) + .addCommand(getConnectorsPullCommand()) + .addCommand(getConnectorsPushCommand()); } diff --git a/packages/cli/src/cli/commands/connectors/list-available.ts b/packages/cli/src/cli/commands/connectors/list-available.ts index 10e7affc..32bde3f6 100644 --- a/packages/cli/src/cli/commands/connectors/list-available.ts +++ b/packages/cli/src/cli/commands/connectors/list-available.ts @@ -1,13 +1,12 @@ import { log } from "@clack/prompts"; -import { Command } from "commander"; -import type { CLIContext } from "@/cli/types.js"; +import type { Command } from "commander"; import { + Base44Command, formatYaml, - runCommand, + type RunCommandResult, runTask, YAML_INDENT, } from "@/cli/utils/index.js"; -import type { RunCommandResult } from "@/cli/utils/runCommand.js"; import { listAvailableIntegrations } from "@/core/resources/connector/index.js"; async function listAvailableAction(): Promise { @@ -37,12 +36,8 @@ async function listAvailableAction(): Promise { }; } -export function getConnectorsListAvailableCommand( - context: CLIContext, -): Command { - return new Command("list-available") +export function getConnectorsListAvailableCommand(): Command { + return new Base44Command("list-available") .description("List all available integration types") - .action(async () => { - await runCommand(listAvailableAction, { requireAuth: true }, context); - }); + .action(listAvailableAction); } diff --git a/packages/cli/src/cli/commands/connectors/pull.ts b/packages/cli/src/cli/commands/connectors/pull.ts index 8ae26f6e..753333cc 100644 --- a/packages/cli/src/cli/commands/connectors/pull.ts +++ b/packages/cli/src/cli/commands/connectors/pull.ts @@ -1,14 +1,16 @@ import { dirname, join } from "node:path"; import { log } from "@clack/prompts"; -import { Command } from "commander"; -import type { CLIContext } from "@/cli/types.js"; +import type { Command } from "commander"; +import { + Base44Command, + type RunCommandResult, + runTask, +} from "@/cli/utils/index.js"; import { readProjectConfig } from "@/core/index.js"; import { pullAllConnectors, writeConnectors, } from "@/core/resources/connector/index.js"; -import { runCommand, runTask } from "../../utils/index.js"; -import type { RunCommandResult } from "../../utils/runCommand.js"; async function pullConnectorsAction(): Promise { const { project } = await readProjectConfig(); @@ -53,12 +55,10 @@ async function pullConnectorsAction(): Promise { }; } -export function getConnectorsPullCommand(context: CLIContext): Command { - return new Command("pull") +export function getConnectorsPullCommand(): Command { + return new Base44Command("pull") .description( "Pull connectors from Base44 to local files (replaces all local connector configs)", ) - .action(async () => { - await runCommand(pullConnectorsAction, { requireAuth: true }, context); - }); + .action(pullConnectorsAction); } diff --git a/packages/cli/src/cli/commands/connectors/push.ts b/packages/cli/src/cli/commands/connectors/push.ts index 28f6b94c..1e75e176 100644 --- a/packages/cli/src/cli/commands/connectors/push.ts +++ b/packages/cli/src/cli/commands/connectors/push.ts @@ -1,8 +1,11 @@ import { log } from "@clack/prompts"; -import { Command } from "commander"; -import type { CLIContext } from "@/cli/types.js"; -import { runCommand, runTask, theme } from "@/cli/utils/index.js"; -import type { RunCommandResult } from "@/cli/utils/runCommand.js"; +import type { Command } from "commander"; +import { + Base44Command, + type RunCommandResult, + runTask, + theme, +} from "@/cli/utils/index.js"; import { getConnectorsUrl } from "@/cli/utils/urls.js"; import { readProjectConfig } from "@/core/index.js"; import { @@ -133,16 +136,12 @@ async function pushConnectorsAction( return { outroMessage }; } -export function getConnectorsPushCommand(context: CLIContext): Command { - return new Command("push") +export function getConnectorsPushCommand(): Command { + return new Base44Command("push") .description( "Push local connectors to Base44 (overwrites connectors on Base44)", ) - .action(async () => { - await runCommand( - () => pushConnectorsAction(context.isNonInteractive), - { requireAuth: true }, - context, - ); + .action(async (_options: unknown, command: Base44Command) => { + return await pushConnectorsAction(command.isNonInteractive); }); } diff --git a/packages/cli/src/cli/commands/dashboard/index.ts b/packages/cli/src/cli/commands/dashboard/index.ts index 7d63e9b0..5fa050ba 100644 --- a/packages/cli/src/cli/commands/dashboard/index.ts +++ b/packages/cli/src/cli/commands/dashboard/index.ts @@ -1,9 +1,8 @@ import { Command } from "commander"; -import type { CLIContext } from "@/cli/types.js"; import { getDashboardOpenCommand } from "./open.js"; -export function getDashboardCommand(context: CLIContext): Command { +export function getDashboardCommand(): Command { return new Command("dashboard") .description("Manage app dashboard") - .addCommand(getDashboardOpenCommand(context)); + .addCommand(getDashboardOpenCommand()); } diff --git a/packages/cli/src/cli/commands/dashboard/open.ts b/packages/cli/src/cli/commands/dashboard/open.ts index b679a969..991d57c7 100644 --- a/packages/cli/src/cli/commands/dashboard/open.ts +++ b/packages/cli/src/cli/commands/dashboard/open.ts @@ -1,8 +1,10 @@ -import { Command } from "commander"; +import type { Command } from "commander"; import open from "open"; -import type { CLIContext } from "@/cli/types.js"; -import { getDashboardUrl, runCommand } from "@/cli/utils/index.js"; -import type { RunCommandResult } from "@/cli/utils/runCommand.js"; +import { + Base44Command, + getDashboardUrl, + type RunCommandResult, +} from "@/cli/utils/index.js"; async function openDashboard( isNonInteractive: boolean, @@ -16,14 +18,10 @@ async function openDashboard( return { outroMessage: `Dashboard opened at ${dashboardUrl}` }; } -export function getDashboardOpenCommand(context: CLIContext): Command { - return new Command("open") +export function getDashboardOpenCommand(): Command { + return new Base44Command("open") .description("Open the app dashboard in your browser") - .action(async () => { - await runCommand( - () => openDashboard(context.isNonInteractive), - { requireAuth: true }, - context, - ); + .action(async (_options: unknown, command: Base44Command) => { + return await openDashboard(command.isNonInteractive); }); } diff --git a/packages/cli/src/cli/commands/dev.ts b/packages/cli/src/cli/commands/dev.ts index 90df2add..80610e18 100644 --- a/packages/cli/src/cli/commands/dev.ts +++ b/packages/cli/src/cli/commands/dev.ts @@ -1,8 +1,10 @@ -import { Command } from "commander"; +import type { Command } from "commander"; import { createDevServer } from "@/cli/dev/dev-server/main.js"; -import type { CLIContext } from "@/cli/types.js"; -import { runCommand, theme } from "@/cli/utils/index.js"; -import type { RunCommandResult } from "@/cli/utils/runCommand.js"; +import { + Base44Command, + type RunCommandResult, + theme, +} from "@/cli/utils/index.js"; import { getDenoWrapperPath } from "@/core/assets.js"; import { readProjectConfig } from "@/core/project/config.js"; @@ -26,15 +28,11 @@ async function devAction(options: DevOptions): Promise { }; } -export function getDevCommand(context: CLIContext): Command { - return new Command("dev") +export function getDevCommand(): Command { + return new Base44Command("dev") .description("Start the development server") .option("-p, --port ", "Port for the development server") .action(async (options: DevOptions) => { - await runCommand( - () => devAction(options), - { requireAuth: true }, - context, - ); + return await devAction(options); }); } diff --git a/packages/cli/src/cli/commands/entities/push.ts b/packages/cli/src/cli/commands/entities/push.ts index caabb4e3..68f90324 100644 --- a/packages/cli/src/cli/commands/entities/push.ts +++ b/packages/cli/src/cli/commands/entities/push.ts @@ -1,8 +1,10 @@ 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 { + Base44Command, + type RunCommandResult, + runTask, +} from "@/cli/utils/index.js"; import { readProjectConfig } from "@/core/index.js"; import { pushEntities } from "@/core/resources/entity/index.js"; @@ -41,14 +43,12 @@ async function pushEntitiesAction(): Promise { return { outroMessage: "Entities pushed to Base44" }; } -export function getEntitiesPushCommand(context: CLIContext): Command { +export function getEntitiesPushCommand(): Command { return new Command("entities") .description("Manage project entities") .addCommand( - new Command("push") + new Base44Command("push") .description("Push local entities to Base44") - .action(async () => { - await runCommand(pushEntitiesAction, { requireAuth: true }, context); - }), + .action(pushEntitiesAction), ); } diff --git a/packages/cli/src/cli/commands/functions/delete.ts b/packages/cli/src/cli/commands/functions/delete.ts index df1a995d..3c5d4fe5 100644 --- a/packages/cli/src/cli/commands/functions/delete.ts +++ b/packages/cli/src/cli/commands/functions/delete.ts @@ -1,7 +1,9 @@ -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 type { Command } from "commander"; +import { + Base44Command, + type RunCommandResult, + runTask, +} from "@/cli/utils/index.js"; import { ApiError } from "@/core/errors.js"; import { deleteSingleFunction } from "@/core/resources/function/api.js"; @@ -57,17 +59,13 @@ function validateNames(command: Command): void { } } -export function getDeleteCommand(context: CLIContext): Command { - return new Command("delete") +export function getDeleteCommand(): Command { + return new Base44Command("delete") .description("Delete deployed functions") .argument("", "Function names to delete") .hook("preAction", validateNames) .action(async (rawNames: string[]) => { const names = parseNames(rawNames); - await runCommand( - () => deleteFunctionsAction(names), - { requireAuth: true }, - context, - ); + return deleteFunctionsAction(names); }); } diff --git a/packages/cli/src/cli/commands/functions/deploy.ts b/packages/cli/src/cli/commands/functions/deploy.ts index 0d5ae969..f1179e31 100644 --- a/packages/cli/src/cli/commands/functions/deploy.ts +++ b/packages/cli/src/cli/commands/functions/deploy.ts @@ -1,11 +1,12 @@ import { log } from "@clack/prompts"; -import { Command } from "commander"; +import type { Command } from "commander"; import { formatDeployResult } from "@/cli/commands/functions/formatDeployResult.js"; import { parseNames } from "@/cli/commands/functions/parseNames.js"; -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 { + Base44Command, + type RunCommandResult, + theme, +} from "@/cli/utils/index.js"; import { InvalidInputError } from "@/core/errors.js"; import { readProjectConfig } from "@/core/index.js"; import { @@ -130,19 +131,13 @@ async function deployFunctionsAction( return { outroMessage: buildDeploySummary(results) }; } -export function getDeployCommand(context: CLIContext): Command { - return new Command("deploy") +export function getDeployCommand(): Command { + return new Base44Command("deploy") .description("Deploy functions to Base44") .argument("[names...]", "Function names to deploy (deploys all if omitted)") .option("--force", "Delete remote functions not found locally") .action(async (rawNames: string[], options: { force?: boolean }) => { - await runCommand( - () => { - const names = parseNames(rawNames); - return deployFunctionsAction(names, options); - }, - { requireAuth: true }, - context, - ); + const names = parseNames(rawNames); + return deployFunctionsAction(names, options); }); } diff --git a/packages/cli/src/cli/commands/functions/index.ts b/packages/cli/src/cli/commands/functions/index.ts index 1e724c69..010e23ff 100644 --- a/packages/cli/src/cli/commands/functions/index.ts +++ b/packages/cli/src/cli/commands/functions/index.ts @@ -1,15 +1,14 @@ import { Command } from "commander"; -import type { CLIContext } from "@/cli/types.js"; import { getDeleteCommand } from "./delete.js"; import { getDeployCommand } from "./deploy.js"; import { getListCommand } from "./list.js"; import { getPullCommand } from "./pull.js"; -export function getFunctionsCommand(context: CLIContext): Command { +export function getFunctionsCommand(): Command { return new Command("functions") .description("Manage backend functions") - .addCommand(getDeployCommand(context)) - .addCommand(getDeleteCommand(context)) - .addCommand(getListCommand(context)) - .addCommand(getPullCommand(context)); + .addCommand(getDeployCommand()) + .addCommand(getDeleteCommand()) + .addCommand(getListCommand()) + .addCommand(getPullCommand()); } diff --git a/packages/cli/src/cli/commands/functions/list.ts b/packages/cli/src/cli/commands/functions/list.ts index e0cae503..78cb2697 100644 --- a/packages/cli/src/cli/commands/functions/list.ts +++ b/packages/cli/src/cli/commands/functions/list.ts @@ -1,9 +1,11 @@ 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 { theme } from "@/cli/utils/theme.js"; +import type { Command } from "commander"; +import { + Base44Command, + type RunCommandResult, + runTask, + theme, +} from "@/cli/utils/index.js"; import { listDeployedFunctions } from "@/core/resources/function/api.js"; async function listFunctionsAction(): Promise { @@ -33,10 +35,8 @@ async function listFunctionsAction(): Promise { }; } -export function getListCommand(context: CLIContext): Command { - return new Command("list") +export function getListCommand(): Command { + return new Base44Command("list") .description("List all deployed functions") - .action(async () => { - await runCommand(listFunctionsAction, { requireAuth: true }, context); - }); + .action(listFunctionsAction); } diff --git a/packages/cli/src/cli/commands/functions/pull.ts b/packages/cli/src/cli/commands/functions/pull.ts index 3d952220..07814ad5 100644 --- a/packages/cli/src/cli/commands/functions/pull.ts +++ b/packages/cli/src/cli/commands/functions/pull.ts @@ -1,9 +1,11 @@ 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 type { Command } from "commander"; +import { + Base44Command, + type RunCommandResult, + runTask, +} from "@/cli/utils/index.js"; import { readProjectConfig } from "@/core/index.js"; import { listDeployedFunctions } from "@/core/resources/function/api.js"; import { writeFunctions } from "@/core/resources/function/pull.js"; @@ -65,15 +67,11 @@ async function pullFunctionsAction( }; } -export function getPullCommand(context: CLIContext): Command { - return new Command("pull") +export function getPullCommand(): Command { + return new Base44Command("pull") .description("Pull deployed functions from Base44") .argument("[name]", "Function name to pull (pulls all if omitted)") .action(async (name: string | undefined) => { - await runCommand( - () => pullFunctionsAction(name), - { requireAuth: true }, - context, - ); + return pullFunctionsAction(name); }); } diff --git a/packages/cli/src/cli/commands/project/create.ts b/packages/cli/src/cli/commands/project/create.ts index a6c5fb82..8f39ef57 100644 --- a/packages/cli/src/cli/commands/project/create.ts +++ b/packages/cli/src/cli/commands/project/create.ts @@ -1,18 +1,17 @@ import { basename, join, resolve } from "node:path"; import type { Option } from "@clack/prompts"; import { confirm, group, isCancel, log, select, text } from "@clack/prompts"; -import { Argument, Command } from "commander"; +import { Argument, type Command } from "commander"; import { execa } from "execa"; import kebabCase from "lodash/kebabCase"; -import type { CLIContext } from "@/cli/types.js"; import { + Base44Command, getDashboardUrl, onPromptCancel, - runCommand, + type RunCommandResult, runTask, theme, } from "@/cli/utils/index.js"; -import type { RunCommandResult } from "@/cli/utils/runCommand.js"; import { InvalidInputError } from "@/core/errors.js"; import { deploySite, isDirEmpty, pushEntities } from "@/core/index.js"; import type { Template } from "@/core/project/index.js"; @@ -283,8 +282,11 @@ async function executeCreate({ return { outroMessage: "Your project is set up and ready to use" }; } -export function getCreateCommand(context: CLIContext): Command { - return new Command("create") +export function getCreateCommand(): Command { + return new Base44Command("create", { + requireAppConfig: false, + fullBanner: true, + }) .description("Create a new Base44 project") .addArgument(new Argument("name", "Project name").argOptional()) .option("-p, --path ", "Path where to create the project") @@ -311,18 +313,11 @@ Examples: const isNonInteractive = !!(options.name ?? name) && !!options.path; if (isNonInteractive) { - await runCommand( - () => - createNonInteractive({ name: options.name ?? name, ...options }), - { requireAuth: true, requireAppConfig: false }, - context, - ); - } else { - await runCommand( - () => createInteractive({ name, ...options }), - { fullBanner: true, requireAuth: true, requireAppConfig: false }, - context, - ); + return await createNonInteractive({ + name: options.name ?? name, + ...options, + }); } + return await createInteractive({ name, ...options }); }); } diff --git a/packages/cli/src/cli/commands/project/deploy.ts b/packages/cli/src/cli/commands/project/deploy.ts index f0381b64..e1ba11d1 100644 --- a/packages/cli/src/cli/commands/project/deploy.ts +++ b/packages/cli/src/cli/commands/project/deploy.ts @@ -1,18 +1,17 @@ import { confirm, isCancel, log } from "@clack/prompts"; -import { Command } from "commander"; +import type { Command } from "commander"; import { filterPendingOAuth, promptOAuthFlows, } from "@/cli/commands/connectors/oauth-prompt.js"; import { formatDeployResult } from "@/cli/commands/functions/formatDeployResult.js"; -import type { CLIContext } from "@/cli/types.js"; import { + Base44Command, getConnectorsUrl, getDashboardUrl, - runCommand, + type RunCommandResult, theme, } from "@/cli/utils/index.js"; -import type { RunCommandResult } from "@/cli/utils/runCommand.js"; import { deployAll, hasResourcesToDeploy, @@ -124,22 +123,17 @@ export async function deployAction( return { outroMessage: "App deployed successfully" }; } -export function getDeployCommand(context: CLIContext): Command { - return new Command("deploy") +export function getDeployCommand(): Command { + return new Base44Command("deploy") .description( "Deploy all project resources (entities, functions, agents, connectors, and site)", ) .option("-y, --yes", "Skip confirmation prompt") - .action(async (options: DeployOptions) => { - await runCommand( - () => - deployAction({ - ...options, - isNonInteractive: context.isNonInteractive, - }), - { requireAuth: true }, - context, - ); + .action(async (options: DeployOptions, command: Base44Command) => { + return await deployAction({ + ...options, + isNonInteractive: command.isNonInteractive, + }); }); } diff --git a/packages/cli/src/cli/commands/project/eject.ts b/packages/cli/src/cli/commands/project/eject.ts index cd7e3fc6..e8009d37 100644 --- a/packages/cli/src/cli/commands/project/eject.ts +++ b/packages/cli/src/cli/commands/project/eject.ts @@ -1,14 +1,17 @@ import { resolve } from "node:path"; import type { Option } from "@clack/prompts"; import { cancel, confirm, isCancel, log, select, text } from "@clack/prompts"; -import { Command } from "commander"; +import type { Command } from "commander"; import { execa } from "execa"; import kebabCase from "lodash/kebabCase"; import { deployAction } from "@/cli/commands/project/deploy.js"; import { CLIExitError } from "@/cli/errors.js"; -import type { CLIContext } from "@/cli/types.js"; -import { runCommand, runTask, theme } from "@/cli/utils/index.js"; -import type { RunCommandResult } from "@/cli/utils/runCommand.js"; +import { + Base44Command, + type RunCommandResult, + runTask, + theme, +} from "@/cli/utils/index.js"; import type { Project } from "@/core/index.js"; import { createProject, @@ -169,8 +172,8 @@ async function eject(options: EjectOptions): Promise { return { outroMessage: "Your new project is set and ready to use" }; } -export function getEjectCommand(context: CLIContext): Command { - return new Command("eject") +export function getEjectCommand(): Command { + return new Base44Command("eject", { requireAppConfig: false }) .description("Download the code for an existing Base44 project") .option("-p, --path ", "Path where to write the project") .option( @@ -178,11 +181,10 @@ export function getEjectCommand(context: CLIContext): Command { "Project ID to eject (skips interactive selection)", ) .option("-y, --yes", "Skip confirmation prompts") - .action(async (options: EjectOptions) => { - await runCommand( - () => eject({ ...options, isNonInteractive: context.isNonInteractive }), - { requireAuth: true, requireAppConfig: false }, - context, - ); + .action(async (options: EjectOptions, command: Base44Command) => { + return await eject({ + ...options, + isNonInteractive: command.isNonInteractive, + }); }); } diff --git a/packages/cli/src/cli/commands/project/link.ts b/packages/cli/src/cli/commands/project/link.ts index 67b8d72c..31117809 100644 --- a/packages/cli/src/cli/commands/project/link.ts +++ b/packages/cli/src/cli/commands/project/link.ts @@ -1,16 +1,15 @@ import type { Option } from "@clack/prompts"; import { cancel, group, isCancel, log, select, text } from "@clack/prompts"; -import { Command } from "commander"; +import type { Command } from "commander"; import { CLIExitError } from "@/cli/errors.js"; -import type { CLIContext } from "@/cli/types.js"; import { + Base44Command, getDashboardUrl, onPromptCancel, - runCommand, + type RunCommandResult, runTask, theme, } from "@/cli/utils/index.js"; -import type { RunCommandResult } from "@/cli/utils/runCommand.js"; import { ConfigExistsError, ConfigNotFoundError, @@ -245,8 +244,8 @@ async function link(options: LinkOptions): Promise { return { outroMessage: "Project linked" }; } -export function getLinkCommand(context: CLIContext): Command { - return new Command("link") +export function getLinkCommand(): Command { + return new Base44Command("link", { requireAppConfig: false }) .description( "Link a local project to a Base44 project (create new or link existing)", ) @@ -262,10 +261,6 @@ export function getLinkCommand(context: CLIContext): Command { ) .hook("preAction", validateNonInteractiveFlags) .action(async (options: LinkOptions) => { - await runCommand( - () => link(options), - { requireAuth: true, requireAppConfig: false }, - context, - ); + return await link(options); }); } diff --git a/packages/cli/src/cli/commands/project/logs.ts b/packages/cli/src/cli/commands/project/logs.ts index 94a18a61..546d9297 100644 --- a/packages/cli/src/cli/commands/project/logs.ts +++ b/packages/cli/src/cli/commands/project/logs.ts @@ -1,7 +1,6 @@ -import { Command, Option } 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 type { Command } from "commander"; +import { Option } from "commander"; +import { Base44Command, type RunCommandResult } from "@/cli/utils/index.js"; import { ApiError, InvalidInputError } from "@/core/errors.js"; import { readProjectConfig } from "@/core/index.js"; import type { @@ -205,8 +204,8 @@ async function logsAction(options: LogsOptions): Promise { return { outroMessage: "Fetched logs", stdout: logsOutput }; } -export function getLogsCommand(context: CLIContext): Command { - return new Command("logs") +export function getLogsCommand(): Command { + return new Base44Command("logs") .description("Fetch function logs for this app") .option( "--function ", @@ -232,10 +231,6 @@ export function getLogsCommand(context: CLIContext): Command { new Option("--order ", "Sort order").choices(["asc", "desc"]), ) .action(async (options: LogsOptions) => { - await runCommand( - () => logsAction(options), - { requireAuth: true }, - context, - ); + return await logsAction(options); }); } diff --git a/packages/cli/src/cli/commands/secrets/delete.ts b/packages/cli/src/cli/commands/secrets/delete.ts index 5aa23a32..d6a1e061 100644 --- a/packages/cli/src/cli/commands/secrets/delete.ts +++ b/packages/cli/src/cli/commands/secrets/delete.ts @@ -1,8 +1,10 @@ -import { Command } from "commander"; -import type { CLIContext } from "@/cli/types.js"; +import type { Command } from "commander"; +import { + Base44Command, + type RunCommandResult, + runTask, +} from "@/cli/utils/index.js"; import { deleteSecret } from "@/core/resources/secret/index.js"; -import { runCommand, runTask } from "../../utils/index.js"; -import type { RunCommandResult } from "../../utils/runCommand.js"; async function deleteSecretAction(key: string): Promise { await runTask( @@ -21,15 +23,11 @@ async function deleteSecretAction(key: string): Promise { }; } -export function getSecretsDeleteCommand(context: CLIContext): Command { - return new Command("delete") +export function getSecretsDeleteCommand(): Command { + return new Base44Command("delete") .description("Delete a secret") .argument("", "Secret name to delete") .action(async (key: string) => { - await runCommand( - () => deleteSecretAction(key), - { requireAuth: true }, - context, - ); + return await deleteSecretAction(key); }); } diff --git a/packages/cli/src/cli/commands/secrets/index.ts b/packages/cli/src/cli/commands/secrets/index.ts index 46fb51ec..a146023d 100644 --- a/packages/cli/src/cli/commands/secrets/index.ts +++ b/packages/cli/src/cli/commands/secrets/index.ts @@ -1,13 +1,12 @@ import { Command } from "commander"; -import type { CLIContext } from "@/cli/types.js"; import { getSecretsDeleteCommand } from "./delete.js"; import { getSecretsListCommand } from "./list.js"; import { getSecretsSetCommand } from "./set.js"; -export function getSecretsCommand(context: CLIContext): Command { +export function getSecretsCommand(): Command { return new Command("secrets") .description("Manage project secrets (environment variables)") - .addCommand(getSecretsListCommand(context)) - .addCommand(getSecretsSetCommand(context)) - .addCommand(getSecretsDeleteCommand(context)); + .addCommand(getSecretsListCommand()) + .addCommand(getSecretsSetCommand()) + .addCommand(getSecretsDeleteCommand()); } diff --git a/packages/cli/src/cli/commands/secrets/list.ts b/packages/cli/src/cli/commands/secrets/list.ts index b5a7d975..64d8e21c 100644 --- a/packages/cli/src/cli/commands/secrets/list.ts +++ b/packages/cli/src/cli/commands/secrets/list.ts @@ -1,9 +1,11 @@ import { log } from "@clack/prompts"; -import { Command } from "commander"; -import type { CLIContext } from "@/cli/types.js"; +import type { Command } from "commander"; +import { + Base44Command, + type RunCommandResult, + runTask, +} from "@/cli/utils/index.js"; import { listSecrets } from "@/core/resources/secret/index.js"; -import { runCommand, runTask } from "../../utils/index.js"; -import type { RunCommandResult } from "../../utils/runCommand.js"; async function listSecretsAction(): Promise { const secrets = await runTask( @@ -32,10 +34,8 @@ async function listSecretsAction(): Promise { }; } -export function getSecretsListCommand(context: CLIContext): Command { - return new Command("list") +export function getSecretsListCommand(): Command { + return new Base44Command("list") .description("List secret names") - .action(async () => { - await runCommand(listSecretsAction, { requireAuth: true }, context); - }); + .action(listSecretsAction); } diff --git a/packages/cli/src/cli/commands/secrets/set.ts b/packages/cli/src/cli/commands/secrets/set.ts index 04c44fc0..b0a25d6d 100644 --- a/packages/cli/src/cli/commands/secrets/set.ts +++ b/packages/cli/src/cli/commands/secrets/set.ts @@ -1,12 +1,14 @@ import { resolve } from "node:path"; import { log } from "@clack/prompts"; -import { Command } from "commander"; -import type { CLIContext } from "@/cli/types.js"; +import type { Command } from "commander"; +import { + Base44Command, + type RunCommandResult, + runTask, +} from "@/cli/utils/index.js"; import { InvalidInputError } from "@/core/errors.js"; import { setSecrets } from "@/core/resources/secret/index.js"; import { parseEnvFile } from "@/core/utils/index.js"; -import { runCommand, runTask } from "../../utils/index.js"; -import type { RunCommandResult } from "../../utils/runCommand.js"; function parseEntries(entries: string[]): Record { const secrets: Record = {}; @@ -90,16 +92,12 @@ async function setSecretsAction( }; } -export function getSecretsSetCommand(context: CLIContext): Command { - return new Command("set") +export function getSecretsSetCommand(): Command { + return new Base44Command("set") .description("Set one or more secrets (KEY=VALUE format)") .argument("[entries...]", "KEY=VALUE pairs (e.g. KEY1=VALUE1 KEY2=VALUE2)") .option("--env-file ", "Path to .env file") .action(async (entries: string[], options: { envFile?: string }) => { - await runCommand( - () => setSecretsAction(entries, options), - { requireAuth: true }, - context, - ); + return await setSecretsAction(entries, options); }); } diff --git a/packages/cli/src/cli/commands/site/deploy.ts b/packages/cli/src/cli/commands/site/deploy.ts index 4d717986..18fd1952 100644 --- a/packages/cli/src/cli/commands/site/deploy.ts +++ b/packages/cli/src/cli/commands/site/deploy.ts @@ -1,9 +1,11 @@ import { resolve } from "node:path"; import { confirm, isCancel } 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 type { Command } from "commander"; +import { + Base44Command, + type RunCommandResult, + runTask, +} from "@/cli/utils/index.js"; import { ConfigNotFoundError } from "@/core/errors.js"; import { readProjectConfig } from "@/core/project/index.js"; import { deploySite } from "@/core/site/index.js"; @@ -53,19 +55,14 @@ async function deployAction(options: DeployOptions): Promise { return { outroMessage: `Visit your site at: ${result.appUrl}` }; } -export function getSiteDeployCommand(context: CLIContext): Command { - return new Command("deploy") +export function getSiteDeployCommand(): Command { + return new Base44Command("deploy") .description("Deploy built site files to Base44 hosting") .option("-y, --yes", "Skip confirmation prompt") - .action(async (options: DeployOptions) => { - await runCommand( - () => - deployAction({ - ...options, - isNonInteractive: context.isNonInteractive, - }), - { requireAuth: true }, - context, - ); + .action(async (options: DeployOptions, command: Base44Command) => { + return await deployAction({ + ...options, + isNonInteractive: command.isNonInteractive, + }); }); } diff --git a/packages/cli/src/cli/commands/site/index.ts b/packages/cli/src/cli/commands/site/index.ts index 775e816c..2850e360 100644 --- a/packages/cli/src/cli/commands/site/index.ts +++ b/packages/cli/src/cli/commands/site/index.ts @@ -1,11 +1,10 @@ import { Command } from "commander"; -import type { CLIContext } from "@/cli/types.js"; import { getSiteDeployCommand } from "./deploy.js"; import { getSiteOpenCommand } from "./open.js"; -export function getSiteCommand(context: CLIContext): Command { +export function getSiteCommand(): Command { return new Command("site") .description("Manage app site (frontend app)") - .addCommand(getSiteDeployCommand(context)) - .addCommand(getSiteOpenCommand(context)); + .addCommand(getSiteDeployCommand()) + .addCommand(getSiteOpenCommand()); } diff --git a/packages/cli/src/cli/commands/site/open.ts b/packages/cli/src/cli/commands/site/open.ts index 40d8d9e5..0f4cb31e 100644 --- a/packages/cli/src/cli/commands/site/open.ts +++ b/packages/cli/src/cli/commands/site/open.ts @@ -1,8 +1,6 @@ -import { Command } from "commander"; +import type { Command } from "commander"; import open from "open"; -import type { CLIContext } from "@/cli/types.js"; -import { runCommand } from "@/cli/utils/index.js"; -import type { RunCommandResult } from "@/cli/utils/runCommand.js"; +import { Base44Command, type RunCommandResult } from "@/cli/utils/index.js"; import { getSiteUrl } from "@/core/site/index.js"; async function openAction( @@ -17,14 +15,10 @@ async function openAction( return { outroMessage: `Site opened at ${siteUrl}` }; } -export function getSiteOpenCommand(context: CLIContext): Command { - return new Command("open") +export function getSiteOpenCommand(): Command { + return new Base44Command("open") .description("Open the published site in your browser") - .action(async () => { - await runCommand( - () => openAction(context.isNonInteractive), - { requireAuth: true }, - context, - ); + .action(async (_options: unknown, command: Base44Command) => { + return await openAction(command.isNonInteractive); }); } diff --git a/packages/cli/src/cli/commands/types/generate.ts b/packages/cli/src/cli/commands/types/generate.ts index 862c4d1a..be936c1d 100644 --- a/packages/cli/src/cli/commands/types/generate.ts +++ b/packages/cli/src/cli/commands/types/generate.ts @@ -1,7 +1,9 @@ -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 type { Command } from "commander"; +import { + Base44Command, + type RunCommandResult, + runTask, +} from "@/cli/utils/index.js"; import { readProjectConfig } from "@/core/index.js"; import { generateTypesFile, updateProjectConfig } from "@/core/types/index.js"; @@ -24,16 +26,10 @@ async function generateTypesAction(): Promise { }; } -export function getTypesGenerateCommand(context: CLIContext): Command { - return new Command("generate") +export function getTypesGenerateCommand(): Command { + return new Base44Command("generate", { requireAuth: false }) .description( "Generate TypeScript declaration file (types.d.ts) from project resources", ) - .action(async () => { - await runCommand( - () => generateTypesAction(), - { requireAuth: false }, - context, - ); - }); + .action(generateTypesAction); } diff --git a/packages/cli/src/cli/commands/types/index.ts b/packages/cli/src/cli/commands/types/index.ts index 91e86701..da17415b 100644 --- a/packages/cli/src/cli/commands/types/index.ts +++ b/packages/cli/src/cli/commands/types/index.ts @@ -1,9 +1,8 @@ import { Command } from "commander"; -import type { CLIContext } from "@/cli/types.js"; import { getTypesGenerateCommand } from "./generate.js"; -export function getTypesCommand(context: CLIContext): Command { +export function getTypesCommand(): Command { return new Command("types") .description("Manage TypeScript type generation") - .addCommand(getTypesGenerateCommand(context)); + .addCommand(getTypesGenerateCommand()); } diff --git a/packages/cli/src/cli/program.ts b/packages/cli/src/cli/program.ts index a6d4ec02..77901950 100644 --- a/packages/cli/src/cli/program.ts +++ b/packages/cli/src/cli/program.ts @@ -14,6 +14,7 @@ import { getLogsCommand } from "@/cli/commands/project/logs.js"; import { getSecretsCommand } from "@/cli/commands/secrets/index.js"; import { getSiteCommand } from "@/cli/commands/site/index.js"; import { getTypesCommand } from "@/cli/commands/types/index.js"; +import { Base44Command } from "@/cli/utils/index.js"; import packageJson from "../../package.json"; import { getDevCommand } from "./commands/dev.js"; import { getEjectCommand } from "./commands/project/eject.js"; @@ -33,44 +34,51 @@ export function createProgram(context: CLIContext): Command { sortSubcommands: true, }); + // Inject CLIContext into all Base44Command instances before action runs + program.hook("preAction", (_, actionCommand) => { + if (actionCommand instanceof Base44Command) { + actionCommand.setContext(context); + } + }); + // Register authentication commands - program.addCommand(getLoginCommand(context)); - program.addCommand(getWhoamiCommand(context)); - program.addCommand(getLogoutCommand(context)); + program.addCommand(getLoginCommand()); + program.addCommand(getWhoamiCommand()); + program.addCommand(getLogoutCommand()); // Register project commands - program.addCommand(getCreateCommand(context)); - program.addCommand(getDashboardCommand(context)); - program.addCommand(getDeployCommand(context)); - program.addCommand(getLinkCommand(context)); - program.addCommand(getEjectCommand(context)); + program.addCommand(getCreateCommand()); + program.addCommand(getDashboardCommand()); + program.addCommand(getDeployCommand()); + program.addCommand(getLinkCommand()); + program.addCommand(getEjectCommand()); // Register entities commands - program.addCommand(getEntitiesPushCommand(context)); + program.addCommand(getEntitiesPushCommand()); // Register agents commands - program.addCommand(getAgentsCommand(context)); + program.addCommand(getAgentsCommand()); // Register connectors commands - program.addCommand(getConnectorsCommand(context)); + program.addCommand(getConnectorsCommand()); // Register functions commands - program.addCommand(getFunctionsCommand(context)); + program.addCommand(getFunctionsCommand()); // Register secrets commands - program.addCommand(getSecretsCommand(context)); + program.addCommand(getSecretsCommand()); // Register site commands - program.addCommand(getSiteCommand(context)); + program.addCommand(getSiteCommand()); // Register types command - program.addCommand(getTypesCommand(context)); + program.addCommand(getTypesCommand()); // Register development commands - program.addCommand(getDevCommand(context), { hidden: true }); + program.addCommand(getDevCommand(), { hidden: true }); // Register logs command - program.addCommand(getLogsCommand(context), { hidden: true }); + program.addCommand(getLogsCommand(), { hidden: true }); return program; } diff --git a/packages/cli/src/cli/utils/command/Base44Command.ts b/packages/cli/src/cli/utils/command/Base44Command.ts new file mode 100644 index 00000000..38f8340a --- /dev/null +++ b/packages/cli/src/cli/utils/command/Base44Command.ts @@ -0,0 +1,159 @@ +import { Command } from "commander"; +import type { CLIContext } from "@/cli/types.js"; +import { + type RunCommandResult, + showError, + showIntro, + showOutro, +} from "@/cli/utils/command/display.js"; +import { ensureAppConfig, ensureAuth } from "@/cli/utils/command/middleware.js"; +import { startUpgradeCheck } from "@/cli/utils/upgradeNotification.js"; + +export type { RunCommandResult } from "@/cli/utils/command/display.js"; + +interface Base44CommandOptions { + /** + * Require user authentication before running this command. + * If the user is not logged in, they will be prompted to login. + * @default true + */ + requireAuth?: boolean; + /** + * Initialize app config before running this command. + * Reads .app.jsonc and caches the appId for sync access via getAppConfig(). + * @default true + */ + requireAppConfig?: boolean; + /** + * Use the full ASCII art banner instead of the simple intro tag. + * Useful for commands like `create` that want more visual impact. + * @default false + */ + fullBanner?: boolean; +} + +/** + * A Command subclass that automatically wraps the action with the Base44 + * lifecycle: intro/banner, auth check, app config, outro, and error handling. + * + * Command authors use this instead of `new Command()` and never need to + * manage the lifecycle manually. It runs transparently inside `.action()`. + * + * Context is injected automatically via a program-level `preAction` hook + * in `createProgram()`. Command files do not need to handle context at all. + * + * When `isNonInteractive` is true (CI / piped output), all clack UI + * (intro, outro, themed errors) is skipped. Errors go to stderr as plain text. + * Action functions that need this value can access it via `command.isNonInteractive` + * (Commander passes the command instance as the last argument to action handlers). + * + * **Important**: Commands should NOT call `intro()` or `outro()` directly. + * Commands can return an optional `outroMessage` and `stdout` via `RunCommandResult`. + * + * @param name - The command name (e.g. "deploy", "login") + * @param options - Optional configuration to override defaults + * + * @example + * // Standard command (auth required, loads app config) + * new Base44Command("deploy") + * + * @example + * // Skip auth and app config (e.g. login command) + * new Base44Command("login", { requireAuth: false, requireAppConfig: false }) + * + * @example + * // Full usage in a command file + * export function getMyCommand(): Command { + * return new Base44Command("my-command") + * .description("Does something") + * .option("-f, --flag", "Some flag") + * .action(async (options) => { + * // ... business logic ... + * return { outroMessage: "Done!" }; + * }); + * } + */ +export class Base44Command extends Command { + private _context?: CLIContext; + private _commandOptions: Required; + + constructor(name: string, options?: Base44CommandOptions) { + super(name); + this._commandOptions = { + requireAuth: options?.requireAuth ?? true, + requireAppConfig: options?.requireAppConfig ?? true, + fullBanner: options?.fullBanner ?? false, + }; + } + + /** + * Inject the CLI context. Called by the program-level `preAction` hook + * in `createProgram()` before any command action executes. + */ + setContext(context: CLIContext): void { + this._context = context; + } + + /** + * Whether the CLI is running in non-interactive mode (CI, piped output). + * Available for action functions that need to adjust behavior + * (e.g. skip browser opens, skip confirmation prompts). + */ + /** @public */ + get isNonInteractive(): boolean { + return this._context?.isNonInteractive ?? false; + } + + private get context(): CLIContext { + if (!this._context) { + throw new Error( + "Base44Command context not set. Ensure the command is registered via createProgram().", + ); + } + return this._context; + } + + /** @public - called by Commander internally via command dispatch */ + // biome-ignore lint/suspicious/noExplicitAny: must match Commander.js action() signature + override action( + fn: (...args: any[]) => void | Promise, + ): this { + // biome-ignore lint/suspicious/noExplicitAny: must match Commander.js action() signature + return super.action(async (...args: any[]) => { + const quiet = this.context.isNonInteractive; + + if (!quiet) { + await showIntro( + this._commandOptions.fullBanner, + this.context.isNonInteractive, + ); + } + + const upgradeCheckPromise = startUpgradeCheck(); + + try { + if (this._commandOptions.requireAuth) { + await ensureAuth(this.context.errorReporter); + } + if (this._commandOptions.requireAppConfig) { + await ensureAppConfig(this.context.errorReporter); + } + + const result = ((await fn(...args)) ?? {}) as RunCommandResult; + + if (!quiet) { + await showOutro( + result, + upgradeCheckPromise, + this.context.distribution, + ); + } else if (result.stdout) { + process.stdout.write(result.stdout); + } + } catch (error) { + showError(error, this.context, quiet); + throw error; + } + }); + } +} diff --git a/packages/cli/src/cli/utils/command/display.ts b/packages/cli/src/cli/utils/command/display.ts new file mode 100644 index 00000000..d83dc222 --- /dev/null +++ b/packages/cli/src/cli/utils/command/display.ts @@ -0,0 +1,107 @@ +import { intro, log, outro } from "@clack/prompts"; +import type { CLIContext } from "@/cli/types.js"; +import { printBanner } from "@/cli/utils/banner.js"; +import { theme } from "@/cli/utils/theme.js"; +import { printUpgradeNotification } from "@/cli/utils/upgradeNotification.js"; +import type { UpgradeInfo } from "@/cli/utils/version-check.js"; +import { isCLIError } from "@/core/errors.js"; + +export interface RunCommandResult { + outroMessage?: string; + /** + * Raw text to write to stdout after the command UI (intro/outro) finishes. + * Useful for commands that produce machine-readable or pipeable output. + */ + stdout?: string; +} + +/** + * Show the intro banner or simple tag. + */ +export async function showIntro( + fullBanner: boolean, + isNonInteractive: boolean, +): Promise { + if (fullBanner) { + await printBanner(isNonInteractive); + intro(""); + } else { + intro(theme.colors.base44OrangeBackground(" Base 44 ")); + } +} + +/** + * Show the outro: upgrade notification, outro message, and optional stdout. + */ +export async function showOutro( + result: RunCommandResult, + upgradeCheckPromise: Promise, + distribution: CLIContext["distribution"], +): Promise { + await printUpgradeNotification(upgradeCheckPromise, distribution); + outro(result.outroMessage || ""); + + if (result.stdout) { + process.stdout.write(result.stdout); + } +} + +/** + * Display an error to the user. + * + * When `quiet` is true (non-interactive / CI), writes a plain error message + * to stderr without clack formatting or ASCII codes. + * When `quiet` is false, uses clack log and themed formatting. + */ +export function showError( + error: unknown, + context: CLIContext, + quiet: boolean, +): void { + if (quiet) { + showPlainError(error); + } else { + showThemedError(error); + const errorContext = context.errorReporter.getErrorContext(); + outro(theme.format.errorContext(errorContext)); + } +} + +function showThemedError(error: unknown): void { + const errorMessage = error instanceof Error ? error.message : String(error); + log.error(errorMessage); + + if (isCLIError(error)) { + if (error.details.length > 0) { + log.info(theme.format.details(error.details)); + } + + const hints = theme.format.agentHints(error.hints); + if (hints) { + log.error(hints); + } + } + + if (process.env.DEBUG === "1" && error instanceof Error && error.stack) { + log.error(theme.styles.dim(error.stack)); + } +} + +function showPlainError(error: unknown): void { + const errorMessage = error instanceof Error ? error.message : String(error); + process.stderr.write(`Error: ${errorMessage}\n`); + + if (isCLIError(error)) { + for (const detail of error.details) { + process.stderr.write(` ${detail}\n`); + } + for (const hint of error.hints) { + const cmd = hint.command ? ` (${hint.command})` : ""; + process.stderr.write(` Hint: ${hint.message}${cmd}\n`); + } + } + + if (process.env.DEBUG === "1" && error instanceof Error && error.stack) { + process.stderr.write(`${error.stack}\n`); + } +} diff --git a/packages/cli/src/cli/utils/command/index.ts b/packages/cli/src/cli/utils/command/index.ts new file mode 100644 index 00000000..e5112d25 --- /dev/null +++ b/packages/cli/src/cli/utils/command/index.ts @@ -0,0 +1,3 @@ +export * from "./Base44Command.js"; +export * from "./display.js"; +export * from "./middleware.js"; diff --git a/packages/cli/src/cli/utils/command/middleware.ts b/packages/cli/src/cli/utils/command/middleware.ts new file mode 100644 index 00000000..c1db3453 --- /dev/null +++ b/packages/cli/src/cli/utils/command/middleware.ts @@ -0,0 +1,37 @@ +import { log } from "@clack/prompts"; +import { login } from "@/cli/commands/auth/login-flow.js"; +import type { ErrorReporter } from "@/cli/telemetry/error-reporter.js"; +import { isLoggedIn, readAuth } from "@/core/auth/index.js"; +import { initAppConfig } from "@/core/project/index.js"; + +/** + * Check authentication status and trigger login flow if needed. + * Sets user context on the error reporter after successful auth. + */ +export async function ensureAuth(errorReporter: ErrorReporter): Promise { + const loggedIn = await isLoggedIn(); + + if (!loggedIn) { + log.info("You need to login first to continue."); + await login(); + } + + try { + const userInfo = await readAuth(); + errorReporter.setContext({ + user: { email: userInfo.email, name: userInfo.name }, + }); + } catch { + // User info is optional context for error reporting + } +} + +/** + * Load app config (.app.jsonc) and set appId on the error reporter. + */ +export async function ensureAppConfig( + errorReporter: ErrorReporter, +): Promise { + const appConfig = await initAppConfig(); + errorReporter.setContext({ appId: appConfig.id }); +} diff --git a/packages/cli/src/cli/utils/index.ts b/packages/cli/src/cli/utils/index.ts index 62e46eb0..7be367a8 100644 --- a/packages/cli/src/cli/utils/index.ts +++ b/packages/cli/src/cli/utils/index.ts @@ -1,6 +1,6 @@ export * from "./banner.js"; +export * from "./command/index.js"; export * from "./prompts.js"; -export * from "./runCommand.js"; export * from "./runTask.js"; export * from "./theme.js"; export * from "./urls.js"; diff --git a/packages/cli/src/cli/utils/runCommand.ts b/packages/cli/src/cli/utils/runCommand.ts deleted file mode 100644 index 99f4dc1f..00000000 --- a/packages/cli/src/cli/utils/runCommand.ts +++ /dev/null @@ -1,147 +0,0 @@ -import { intro, log, outro } from "@clack/prompts"; -import { login } from "@/cli/commands/auth/login-flow.js"; -import type { CLIContext } from "@/cli/types.js"; -import { printBanner } from "@/cli/utils/banner.js"; -import { theme } from "@/cli/utils/theme.js"; -import { - printUpgradeNotification, - startUpgradeCheck, -} from "@/cli/utils/upgradeNotification.js"; -import { isLoggedIn, readAuth } from "@/core/auth/index.js"; -import { isCLIError } from "@/core/errors.js"; -import { initAppConfig } from "@/core/project/index.js"; - -interface RunCommandOptions { - /** - * Use the full ASCII art banner instead of the simple intro tag. - * Useful for commands like `create` that want more visual impact. - * @default false - */ - fullBanner?: boolean; - /** - * Require user authentication before running this command. - * If the user is not logged in, they will see an error message. - * @default false - */ - requireAuth?: boolean; - /** - * Initialize app config before running this command. - * Reads .app.jsonc and caches the appId for sync access via getAppConfig(). - * @default true - */ - requireAppConfig?: boolean; -} - -export interface RunCommandResult { - outroMessage?: string; - /** - * Raw text to write to stdout after the command UI (intro/outro) finishes. - * Useful for commands that produce machine-readable or pipeable output. - */ - stdout?: string; -} - -/** - * Wraps a command function with the Base44 intro/outro and error handling. - * All CLI commands should use this utility to ensure consistent branding. - * - * **Important**: Commands should NOT call `intro()` or `outro()` directly. - * This function handles both. Commands can return an optional `outroMessage` - * which will be displayed at the end. - * - * @param commandFn - The async function to execute. Returns `RunCommandResult` with optional `outroMessage`. - * @param options - Optional configuration for the command wrapper - * @param context - CLI context with dependencies (errorReporter, etc.) - * - * @example - * export function getMyCommand(context: CLIContext): Command { - * return new Command("my-command") - * .action(async () => { - * await runCommand( - * async () => { - * // ... do work ... - * return { outroMessage: "Done!" }; - * }, - * { requireAuth: true }, - * context - * ); - * }); - * } - */ -export async function runCommand( - commandFn: () => Promise, - options: RunCommandOptions | undefined, - context: CLIContext, -): Promise { - if (options?.fullBanner) { - await printBanner(context.isNonInteractive); - intro(""); - } else { - intro(theme.colors.base44OrangeBackground(" Base 44 ")); - } - const upgradeCheckPromise = startUpgradeCheck(); - - try { - // Check authentication if required - if (options?.requireAuth) { - const loggedIn = await isLoggedIn(); - - if (!loggedIn) { - log.info("You need to login first to continue."); - await login(); - } - - try { - const userInfo = await readAuth(); - context.errorReporter.setContext({ - user: { email: userInfo.email, name: userInfo.name }, - }); - } catch { - // User info is optional context for error reporting - } - } - - // Initialize app config unless explicitly disabled - if (options?.requireAppConfig !== false) { - const appConfig = await initAppConfig(); - context.errorReporter.setContext({ appId: appConfig.id }); - } - - const result = await commandFn(); - await printUpgradeNotification(upgradeCheckPromise, context.distribution); - - outro(result.outroMessage || ""); - - if (result.stdout) { - process.stdout.write(result.stdout); - } - } catch (error) { - displayError(error); - - const errorContext = context.errorReporter.getErrorContext(); - outro(theme.format.errorContext(errorContext)); - - // Re-throw for runCLI to handle (error reporting, exit code) - throw error; - } -} - -function displayError(error: unknown): void { - const errorMessage = error instanceof Error ? error.message : String(error); - log.error(errorMessage); - - if (isCLIError(error)) { - if (error.details.length > 0) { - log.info(theme.format.details(error.details)); - } - - const hints = theme.format.agentHints(error.hints); - if (hints) { - log.error(hints); - } - } - - if (process.env.DEBUG === "1" && error instanceof Error && error.stack) { - log.error(theme.styles.dim(error.stack)); - } -} From 3b3cfadbcc65eac65017a348fed1612c7417c881 Mon Sep 17 00:00:00 2001 From: Kfir Strikovsky Date: Mon, 16 Mar 2026 16:26:21 +0200 Subject: [PATCH 02/12] update docs --- docs/commands.md | 52 ++++++++++++++++++++++++------------------------ 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/docs/commands.md b/docs/commands.md index 94523875..5fc3d473 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -1,8 +1,8 @@ # Adding & Modifying CLI Commands -**Keywords:** command, factory pattern, CLIContext, isNonInteractive, Base44Command, runTask, spinner, theming, chalk, program.ts, register, banner, intro, outro +**Keywords:** command, factory pattern, Base44Command, isNonInteractive, runTask, spinner, theming, chalk, program.ts, register, banner, intro, outro -Commands live in `src/cli/commands//`. They use a **factory pattern** with dependency injection via `CLIContext`. +Commands live in `src/cli/commands//`. They use a **factory pattern** — each file exports a function that returns a `Base44Command`. ## Command File Template @@ -10,7 +10,6 @@ Commands live in `src/cli/commands//`. They use a **factory pattern** wi // src/cli/commands//.ts import { log } from "@clack/prompts"; import type { Command } from "commander"; -import type { CLIContext } from "@/cli/types.js"; import { Base44Command, type RunCommandResult, runTask, theme } from "@/cli/utils/index.js"; async function myAction(): Promise { @@ -31,8 +30,8 @@ async function myAction(): Promise { return { outroMessage: `Created ${theme.styles.bold(result.name)}` }; } -export function getMyCommand(context: CLIContext): Command { - return new Base44Command("", context) +export function getMyCommand(): Command { + return new Base44Command("") .description("") .option("-f, --flag", "Some flag") .action(myAction); @@ -41,21 +40,21 @@ export function getMyCommand(context: CLIContext): Command { **Key rules**: - Export a **factory function** (`getMyCommand`), not a static command instance -- The factory receives `CLIContext` (contains `errorReporter`, `isNonInteractive`, `distribution`) +- Factory functions take **no parameters** — context is injected automatically (see below) - Use `Base44Command` instead of `Command` — it automatically handles intro/outro, auth, app config, and error display - Commands must NOT call `intro()` or `outro()` directly - The action function must return `RunCommandResult` with an `outroMessage` ## Base44Command Options -Pass options as the third argument to the constructor: +Pass options as the second argument to the constructor: ```typescript -new Base44Command("my-cmd", context) // All defaults -new Base44Command("my-cmd", context, { requireAuth: false }) // Skip auth check -new Base44Command("my-cmd", context, { requireAppConfig: false }) // Skip app config loading -new Base44Command("my-cmd", context, { fullBanner: true }) // ASCII art banner -new Base44Command("my-cmd", context, { requireAuth: false, requireAppConfig: false }) +new Base44Command("my-cmd") // All defaults +new Base44Command("my-cmd", { requireAuth: false }) // Skip auth check +new Base44Command("my-cmd", { requireAppConfig: false }) // Skip app config loading +new Base44Command("my-cmd", { fullBanner: true }) // ASCII art banner +new Base44Command("my-cmd", { requireAuth: false, requireAppConfig: false }) ``` | Option | Default | Description | @@ -64,7 +63,7 @@ new Base44Command("my-cmd", context, { requireAuth: false, requireAppConfig: fal | `requireAppConfig` | `true` | Load `.app.jsonc` and cache for sync access via `getAppConfig()` | | `fullBanner` | `false` | Show ASCII art banner instead of simple intro tag | -When `context.isNonInteractive` is `true` (CI, piped output), all clack UI (intro, outro, themed errors) is automatically skipped. Errors go to stderr as plain text. +When the CLI runs in non-interactive mode (CI, piped output), all clack UI (intro, outro, themed errors) is automatically skipped. Errors go to stderr as plain text. ## Registering a Command @@ -74,10 +73,10 @@ Add the import and registration in `src/cli/program.ts`: import { getMyCommand } from "@/cli/commands//.js"; // Inside createProgram(context): -program.addCommand(getMyCommand(context)); +program.addCommand(getMyCommand()); ``` -## CLIContext (Dependency Injection) +## CLIContext (Automatic Injection) ```typescript export interface CLIContext { @@ -88,19 +87,19 @@ export interface CLIContext { ``` - Created once in `runCLI()` at startup -- `isNonInteractive` is `true` when stdin/stdout are not a TTY (e.g., CI, piped output, AI agents). Use it to skip interactive prompts, browser opens, and animations. Also controls quiet mode — when true, all clack UI is suppressed. -- Passed to `createProgram(context)`, which passes it to each command factory +- Injected into every `Base44Command` automatically via a program-level `preAction` hook in `createProgram()` — command files never handle context directly +- `isNonInteractive` is `true` when stdin/stdout are not a TTY (e.g., CI, piped output, AI agents). Controls quiet mode — when true, all clack UI is suppressed. ### Using `isNonInteractive` -Pass `context.isNonInteractive` to your action when the command has interactive behavior (browser opens, confirmation prompts, animations): +When a command needs to check `isNonInteractive` (e.g., to skip browser opens or confirmation prompts), access it from the command instance. Commander passes the command as the last argument to action handlers: ```typescript -export function getMyCommand(context: CLIContext): Command { - return new Base44Command("open", context) +export function getMyCommand(): Command { + return new Base44Command("open") .description("Open something in browser") - .action(async () => { - return await myAction(context.isNonInteractive); + .action(async (_options: unknown, command: Base44Command) => { + return await myAction(command.isNonInteractive); }); } @@ -182,8 +181,8 @@ function validateInput(command: Command): void { } } -export function getMyCommand(context: CLIContext): Command { - return new Base44Command("my-cmd", context) +export function getMyCommand(): Command { + return new Base44Command("my-cmd") .argument("[entries...]", "Input entries") .option("--flag-a ", "Alternative input") .hook("preAction", validateInput) @@ -197,8 +196,9 @@ Access `command.args` for positional arguments and `command.opts()` for options ## Rules (Command-Specific) -- **Command factory pattern** - Commands export `getXCommand(context)` functions, not static instances -- **Use `Base44Command`** - All commands use `new Base44Command(name, context, options)` instead of `new Command()`. The lifecycle (intro, auth, config, outro, error handling) is automatic. +- **Command factory pattern** - Commands export `getXCommand()` functions (no parameters), not static instances +- **Use `Base44Command`** - All commands use `new Base44Command(name, options?)` instead of `new Command()`. The lifecycle (intro, auth, config, outro, error handling) is automatic. +- **No context plumbing** - Context is injected automatically. Command files should never import `CLIContext`. - **Task wrapper** - Use `runTask()` for async operations with spinners - **Use theme for styling** - Never use `chalk` directly; import `theme` from `@/cli/utils/` and use semantic names - **Use fs.ts utilities** - Always use `@/core/utils/fs.js` for file operations From 6d1c9f5c589f3975f4c669712646a47f64627c44 Mon Sep 17 00:00:00 2001 From: Kfir Strikovsky Date: Mon, 16 Mar 2026 16:28:03 +0200 Subject: [PATCH 03/12] small docs changes --- packages/cli/src/cli/utils/command/Base44Command.ts | 9 --------- 1 file changed, 9 deletions(-) diff --git a/packages/cli/src/cli/utils/command/Base44Command.ts b/packages/cli/src/cli/utils/command/Base44Command.ts index 38f8340a..b3aff306 100644 --- a/packages/cli/src/cli/utils/command/Base44Command.ts +++ b/packages/cli/src/cli/utils/command/Base44Command.ts @@ -36,20 +36,11 @@ interface Base44CommandOptions { * A Command subclass that automatically wraps the action with the Base44 * lifecycle: intro/banner, auth check, app config, outro, and error handling. * - * Command authors use this instead of `new Command()` and never need to - * manage the lifecycle manually. It runs transparently inside `.action()`. - * - * Context is injected automatically via a program-level `preAction` hook - * in `createProgram()`. Command files do not need to handle context at all. - * * When `isNonInteractive` is true (CI / piped output), all clack UI * (intro, outro, themed errors) is skipped. Errors go to stderr as plain text. * Action functions that need this value can access it via `command.isNonInteractive` * (Commander passes the command instance as the last argument to action handlers). * - * **Important**: Commands should NOT call `intro()` or `outro()` directly. - * Commands can return an optional `outroMessage` and `stdout` via `RunCommandResult`. - * * @param name - The command name (e.g. "deploy", "login") * @param options - Optional configuration to override defaults * From fa370be87795327868ca28ffc2fe4dd2dd2f96c7 Mon Sep 17 00:00:00 2001 From: Kfir Strikovsky Date: Mon, 16 Mar 2026 17:35:00 +0200 Subject: [PATCH 04/12] clean up code --- docs/commands.md | 4 +- packages/cli/src/cli/commands/agents/pull.ts | 7 +-- packages/cli/src/cli/commands/agents/push.ts | 7 +-- .../cli/src/cli/commands/auth/login-flow.ts | 2 +- packages/cli/src/cli/commands/auth/logout.ts | 3 +- packages/cli/src/cli/commands/auth/whoami.ts | 7 +-- .../cli/commands/connectors/list-available.ts | 2 +- .../cli/src/cli/commands/connectors/pull.ts | 7 +-- .../cli/src/cli/commands/connectors/push.ts | 8 +-- .../cli/src/cli/commands/dashboard/open.ts | 7 +-- packages/cli/src/cli/commands/dev.ts | 7 +-- .../cli/src/cli/commands/entities/push.ts | 7 +-- .../cli/src/cli/commands/functions/delete.ts | 7 +-- .../cli/src/cli/commands/functions/deploy.ts | 7 +-- .../cli/src/cli/commands/functions/list.ts | 8 +-- .../cli/src/cli/commands/functions/pull.ts | 7 +-- .../cli/src/cli/commands/project/create.ts | 2 +- .../cli/src/cli/commands/project/deploy.ts | 2 +- .../cli/src/cli/commands/project/eject.ts | 8 +-- packages/cli/src/cli/commands/project/link.ts | 2 +- packages/cli/src/cli/commands/project/logs.ts | 3 +- .../cli/src/cli/commands/secrets/delete.ts | 7 +-- packages/cli/src/cli/commands/secrets/list.ts | 7 +-- packages/cli/src/cli/commands/secrets/set.ts | 7 +-- packages/cli/src/cli/commands/site/deploy.ts | 7 +-- packages/cli/src/cli/commands/site/open.ts | 3 +- .../cli/src/cli/commands/types/generate.ts | 7 +-- packages/cli/src/cli/types.ts | 9 ++++ packages/cli/src/cli/utils/banner.ts | 9 +--- .../src/cli/utils/command/Base44Command.ts | 36 +++++++------ packages/cli/src/cli/utils/command/display.ts | 54 ++++++------------- 31 files changed, 94 insertions(+), 166 deletions(-) diff --git a/docs/commands.md b/docs/commands.md index 5fc3d473..9a3d5a72 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -40,8 +40,7 @@ export function getMyCommand(): Command { **Key rules**: - Export a **factory function** (`getMyCommand`), not a static command instance -- Factory functions take **no parameters** — context is injected automatically (see below) -- Use `Base44Command` instead of `Command` — it automatically handles intro/outro, auth, app config, and error display +- Use `Base44Command` class - Commands must NOT call `intro()` or `outro()` directly - The action function must return `RunCommandResult` with an `outroMessage` @@ -87,7 +86,6 @@ export interface CLIContext { ``` - Created once in `runCLI()` at startup -- Injected into every `Base44Command` automatically via a program-level `preAction` hook in `createProgram()` — command files never handle context directly - `isNonInteractive` is `true` when stdin/stdout are not a TTY (e.g., CI, piped output, AI agents). Controls quiet mode — when true, all clack UI is suppressed. ### Using `isNonInteractive` diff --git a/packages/cli/src/cli/commands/agents/pull.ts b/packages/cli/src/cli/commands/agents/pull.ts index 9db072c1..6b5b262c 100644 --- a/packages/cli/src/cli/commands/agents/pull.ts +++ b/packages/cli/src/cli/commands/agents/pull.ts @@ -1,11 +1,8 @@ import { dirname, join } from "node:path"; import { log } from "@clack/prompts"; import type { Command } from "commander"; -import { - Base44Command, - type RunCommandResult, - runTask, -} from "@/cli/utils/index.js"; +import type { RunCommandResult } from "@/cli/types.js"; +import { Base44Command, runTask } from "@/cli/utils/index.js"; import { readProjectConfig } from "@/core/index.js"; import { fetchAgents, writeAgents } from "@/core/resources/agent/index.js"; diff --git a/packages/cli/src/cli/commands/agents/push.ts b/packages/cli/src/cli/commands/agents/push.ts index 9136b3ca..35a055b1 100644 --- a/packages/cli/src/cli/commands/agents/push.ts +++ b/packages/cli/src/cli/commands/agents/push.ts @@ -1,10 +1,7 @@ import { log } from "@clack/prompts"; import type { Command } from "commander"; -import { - Base44Command, - type RunCommandResult, - runTask, -} from "@/cli/utils/index.js"; +import type { RunCommandResult } from "@/cli/types.js"; +import { Base44Command, runTask } from "@/cli/utils/index.js"; import { readProjectConfig } from "@/core/index.js"; import { pushAgents } from "@/core/resources/agent/index.js"; diff --git a/packages/cli/src/cli/commands/auth/login-flow.ts b/packages/cli/src/cli/commands/auth/login-flow.ts index 34cc2893..5cb62f15 100644 --- a/packages/cli/src/cli/commands/auth/login-flow.ts +++ b/packages/cli/src/cli/commands/auth/login-flow.ts @@ -1,6 +1,6 @@ import { log } from "@clack/prompts"; import pWaitFor from "p-wait-for"; -import type { RunCommandResult } from "@/cli/utils/index.js"; +import type { RunCommandResult } from "@/cli/types.js"; import { runTask } from "@/cli/utils/index.js"; import { theme } from "@/cli/utils/theme.js"; import type { diff --git a/packages/cli/src/cli/commands/auth/logout.ts b/packages/cli/src/cli/commands/auth/logout.ts index 3d91748c..227191f9 100644 --- a/packages/cli/src/cli/commands/auth/logout.ts +++ b/packages/cli/src/cli/commands/auth/logout.ts @@ -1,5 +1,6 @@ import type { Command } from "commander"; -import { Base44Command, type RunCommandResult } from "@/cli/utils/index.js"; +import type { RunCommandResult } from "@/cli/types.js"; +import { Base44Command } from "@/cli/utils/index.js"; import { deleteAuth } from "@/core/auth/index.js"; async function logout(): Promise { diff --git a/packages/cli/src/cli/commands/auth/whoami.ts b/packages/cli/src/cli/commands/auth/whoami.ts index 9c668433..26a6e01f 100644 --- a/packages/cli/src/cli/commands/auth/whoami.ts +++ b/packages/cli/src/cli/commands/auth/whoami.ts @@ -1,9 +1,6 @@ import type { Command } from "commander"; -import { - Base44Command, - type RunCommandResult, - theme, -} from "@/cli/utils/index.js"; +import type { RunCommandResult } from "@/cli/types.js"; +import { Base44Command, theme } from "@/cli/utils/index.js"; import { readAuth } from "@/core/auth/index.js"; async function whoami(): Promise { diff --git a/packages/cli/src/cli/commands/connectors/list-available.ts b/packages/cli/src/cli/commands/connectors/list-available.ts index 32bde3f6..9d940f15 100644 --- a/packages/cli/src/cli/commands/connectors/list-available.ts +++ b/packages/cli/src/cli/commands/connectors/list-available.ts @@ -1,9 +1,9 @@ import { log } from "@clack/prompts"; import type { Command } from "commander"; +import type { RunCommandResult } from "@/cli/types.js"; import { Base44Command, formatYaml, - type RunCommandResult, runTask, YAML_INDENT, } from "@/cli/utils/index.js"; diff --git a/packages/cli/src/cli/commands/connectors/pull.ts b/packages/cli/src/cli/commands/connectors/pull.ts index 753333cc..b4ff3152 100644 --- a/packages/cli/src/cli/commands/connectors/pull.ts +++ b/packages/cli/src/cli/commands/connectors/pull.ts @@ -1,11 +1,8 @@ import { dirname, join } from "node:path"; import { log } from "@clack/prompts"; import type { Command } from "commander"; -import { - Base44Command, - type RunCommandResult, - runTask, -} from "@/cli/utils/index.js"; +import type { RunCommandResult } from "@/cli/types.js"; +import { Base44Command, runTask } from "@/cli/utils/index.js"; import { readProjectConfig } from "@/core/index.js"; import { pullAllConnectors, diff --git a/packages/cli/src/cli/commands/connectors/push.ts b/packages/cli/src/cli/commands/connectors/push.ts index 1e75e176..13048b89 100644 --- a/packages/cli/src/cli/commands/connectors/push.ts +++ b/packages/cli/src/cli/commands/connectors/push.ts @@ -1,11 +1,7 @@ import { log } from "@clack/prompts"; import type { Command } from "commander"; -import { - Base44Command, - type RunCommandResult, - runTask, - theme, -} from "@/cli/utils/index.js"; +import type { RunCommandResult } from "@/cli/types.js"; +import { Base44Command, runTask, theme } from "@/cli/utils/index.js"; import { getConnectorsUrl } from "@/cli/utils/urls.js"; import { readProjectConfig } from "@/core/index.js"; import { diff --git a/packages/cli/src/cli/commands/dashboard/open.ts b/packages/cli/src/cli/commands/dashboard/open.ts index 991d57c7..83af7941 100644 --- a/packages/cli/src/cli/commands/dashboard/open.ts +++ b/packages/cli/src/cli/commands/dashboard/open.ts @@ -1,10 +1,7 @@ import type { Command } from "commander"; import open from "open"; -import { - Base44Command, - getDashboardUrl, - type RunCommandResult, -} from "@/cli/utils/index.js"; +import type { RunCommandResult } from "@/cli/types.js"; +import { Base44Command, getDashboardUrl } from "@/cli/utils/index.js"; async function openDashboard( isNonInteractive: boolean, diff --git a/packages/cli/src/cli/commands/dev.ts b/packages/cli/src/cli/commands/dev.ts index 80610e18..1c7d4516 100644 --- a/packages/cli/src/cli/commands/dev.ts +++ b/packages/cli/src/cli/commands/dev.ts @@ -1,10 +1,7 @@ import type { Command } from "commander"; import { createDevServer } from "@/cli/dev/dev-server/main.js"; -import { - Base44Command, - type RunCommandResult, - theme, -} from "@/cli/utils/index.js"; +import type { RunCommandResult } from "@/cli/types.js"; +import { Base44Command, theme } from "@/cli/utils/index.js"; import { getDenoWrapperPath } from "@/core/assets.js"; import { readProjectConfig } from "@/core/project/config.js"; diff --git a/packages/cli/src/cli/commands/entities/push.ts b/packages/cli/src/cli/commands/entities/push.ts index 68f90324..ccc0eed8 100644 --- a/packages/cli/src/cli/commands/entities/push.ts +++ b/packages/cli/src/cli/commands/entities/push.ts @@ -1,10 +1,7 @@ import { log } from "@clack/prompts"; import { Command } from "commander"; -import { - Base44Command, - type RunCommandResult, - runTask, -} from "@/cli/utils/index.js"; +import type { RunCommandResult } from "@/cli/types.js"; +import { Base44Command, runTask } from "@/cli/utils/index.js"; import { readProjectConfig } from "@/core/index.js"; import { pushEntities } from "@/core/resources/entity/index.js"; diff --git a/packages/cli/src/cli/commands/functions/delete.ts b/packages/cli/src/cli/commands/functions/delete.ts index 3c5d4fe5..3f8b95ca 100644 --- a/packages/cli/src/cli/commands/functions/delete.ts +++ b/packages/cli/src/cli/commands/functions/delete.ts @@ -1,9 +1,6 @@ import type { Command } from "commander"; -import { - Base44Command, - type RunCommandResult, - runTask, -} from "@/cli/utils/index.js"; +import type { RunCommandResult } from "@/cli/types.js"; +import { Base44Command, runTask } from "@/cli/utils/index.js"; import { ApiError } from "@/core/errors.js"; import { deleteSingleFunction } from "@/core/resources/function/api.js"; diff --git a/packages/cli/src/cli/commands/functions/deploy.ts b/packages/cli/src/cli/commands/functions/deploy.ts index f1179e31..1fd32924 100644 --- a/packages/cli/src/cli/commands/functions/deploy.ts +++ b/packages/cli/src/cli/commands/functions/deploy.ts @@ -2,11 +2,8 @@ import { log } from "@clack/prompts"; import type { Command } from "commander"; import { formatDeployResult } from "@/cli/commands/functions/formatDeployResult.js"; import { parseNames } from "@/cli/commands/functions/parseNames.js"; -import { - Base44Command, - type RunCommandResult, - theme, -} from "@/cli/utils/index.js"; +import type { RunCommandResult } from "@/cli/types.js"; +import { Base44Command, theme } from "@/cli/utils/index.js"; import { InvalidInputError } from "@/core/errors.js"; import { readProjectConfig } from "@/core/index.js"; import { diff --git a/packages/cli/src/cli/commands/functions/list.ts b/packages/cli/src/cli/commands/functions/list.ts index 78cb2697..81c453eb 100644 --- a/packages/cli/src/cli/commands/functions/list.ts +++ b/packages/cli/src/cli/commands/functions/list.ts @@ -1,11 +1,7 @@ import { log } from "@clack/prompts"; import type { Command } from "commander"; -import { - Base44Command, - type RunCommandResult, - runTask, - theme, -} from "@/cli/utils/index.js"; +import type { RunCommandResult } from "@/cli/types.js"; +import { Base44Command, runTask, theme } from "@/cli/utils/index.js"; import { listDeployedFunctions } from "@/core/resources/function/api.js"; async function listFunctionsAction(): Promise { diff --git a/packages/cli/src/cli/commands/functions/pull.ts b/packages/cli/src/cli/commands/functions/pull.ts index 07814ad5..0892901a 100644 --- a/packages/cli/src/cli/commands/functions/pull.ts +++ b/packages/cli/src/cli/commands/functions/pull.ts @@ -1,11 +1,8 @@ import { dirname, join } from "node:path"; import { log } from "@clack/prompts"; import type { Command } from "commander"; -import { - Base44Command, - type RunCommandResult, - runTask, -} from "@/cli/utils/index.js"; +import type { RunCommandResult } from "@/cli/types.js"; +import { Base44Command, runTask } from "@/cli/utils/index.js"; import { readProjectConfig } from "@/core/index.js"; import { listDeployedFunctions } from "@/core/resources/function/api.js"; import { writeFunctions } from "@/core/resources/function/pull.js"; diff --git a/packages/cli/src/cli/commands/project/create.ts b/packages/cli/src/cli/commands/project/create.ts index 8f39ef57..c5e671a2 100644 --- a/packages/cli/src/cli/commands/project/create.ts +++ b/packages/cli/src/cli/commands/project/create.ts @@ -4,11 +4,11 @@ import { confirm, group, isCancel, log, select, text } from "@clack/prompts"; import { Argument, type Command } from "commander"; import { execa } from "execa"; import kebabCase from "lodash/kebabCase"; +import type { RunCommandResult } from "@/cli/types.js"; import { Base44Command, getDashboardUrl, onPromptCancel, - type RunCommandResult, runTask, theme, } from "@/cli/utils/index.js"; diff --git a/packages/cli/src/cli/commands/project/deploy.ts b/packages/cli/src/cli/commands/project/deploy.ts index e1ba11d1..7e48a5d3 100644 --- a/packages/cli/src/cli/commands/project/deploy.ts +++ b/packages/cli/src/cli/commands/project/deploy.ts @@ -5,11 +5,11 @@ import { promptOAuthFlows, } from "@/cli/commands/connectors/oauth-prompt.js"; import { formatDeployResult } from "@/cli/commands/functions/formatDeployResult.js"; +import type { RunCommandResult } from "@/cli/types.js"; import { Base44Command, getConnectorsUrl, getDashboardUrl, - type RunCommandResult, theme, } from "@/cli/utils/index.js"; import { diff --git a/packages/cli/src/cli/commands/project/eject.ts b/packages/cli/src/cli/commands/project/eject.ts index e8009d37..75afe04c 100644 --- a/packages/cli/src/cli/commands/project/eject.ts +++ b/packages/cli/src/cli/commands/project/eject.ts @@ -6,12 +6,8 @@ import { execa } from "execa"; import kebabCase from "lodash/kebabCase"; import { deployAction } from "@/cli/commands/project/deploy.js"; import { CLIExitError } from "@/cli/errors.js"; -import { - Base44Command, - type RunCommandResult, - runTask, - theme, -} from "@/cli/utils/index.js"; +import type { RunCommandResult } from "@/cli/types.js"; +import { Base44Command, runTask, theme } from "@/cli/utils/index.js"; import type { Project } from "@/core/index.js"; import { createProject, diff --git a/packages/cli/src/cli/commands/project/link.ts b/packages/cli/src/cli/commands/project/link.ts index 31117809..f70d3e41 100644 --- a/packages/cli/src/cli/commands/project/link.ts +++ b/packages/cli/src/cli/commands/project/link.ts @@ -2,11 +2,11 @@ import type { Option } from "@clack/prompts"; import { cancel, group, isCancel, log, select, text } from "@clack/prompts"; import type { Command } from "commander"; import { CLIExitError } from "@/cli/errors.js"; +import type { RunCommandResult } from "@/cli/types.js"; import { Base44Command, getDashboardUrl, onPromptCancel, - type RunCommandResult, runTask, theme, } from "@/cli/utils/index.js"; diff --git a/packages/cli/src/cli/commands/project/logs.ts b/packages/cli/src/cli/commands/project/logs.ts index 546d9297..960d07a1 100644 --- a/packages/cli/src/cli/commands/project/logs.ts +++ b/packages/cli/src/cli/commands/project/logs.ts @@ -1,6 +1,7 @@ import type { Command } from "commander"; import { Option } from "commander"; -import { Base44Command, type RunCommandResult } from "@/cli/utils/index.js"; +import type { RunCommandResult } from "@/cli/types.js"; +import { Base44Command } from "@/cli/utils/index.js"; import { ApiError, InvalidInputError } from "@/core/errors.js"; import { readProjectConfig } from "@/core/index.js"; import type { diff --git a/packages/cli/src/cli/commands/secrets/delete.ts b/packages/cli/src/cli/commands/secrets/delete.ts index d6a1e061..3e7a65d3 100644 --- a/packages/cli/src/cli/commands/secrets/delete.ts +++ b/packages/cli/src/cli/commands/secrets/delete.ts @@ -1,9 +1,6 @@ import type { Command } from "commander"; -import { - Base44Command, - type RunCommandResult, - runTask, -} from "@/cli/utils/index.js"; +import type { RunCommandResult } from "@/cli/types.js"; +import { Base44Command, runTask } from "@/cli/utils/index.js"; import { deleteSecret } from "@/core/resources/secret/index.js"; async function deleteSecretAction(key: string): Promise { diff --git a/packages/cli/src/cli/commands/secrets/list.ts b/packages/cli/src/cli/commands/secrets/list.ts index 64d8e21c..d44630c5 100644 --- a/packages/cli/src/cli/commands/secrets/list.ts +++ b/packages/cli/src/cli/commands/secrets/list.ts @@ -1,10 +1,7 @@ import { log } from "@clack/prompts"; import type { Command } from "commander"; -import { - Base44Command, - type RunCommandResult, - runTask, -} from "@/cli/utils/index.js"; +import type { RunCommandResult } from "@/cli/types.js"; +import { Base44Command, runTask } from "@/cli/utils/index.js"; import { listSecrets } from "@/core/resources/secret/index.js"; async function listSecretsAction(): Promise { diff --git a/packages/cli/src/cli/commands/secrets/set.ts b/packages/cli/src/cli/commands/secrets/set.ts index b0a25d6d..8ca2d477 100644 --- a/packages/cli/src/cli/commands/secrets/set.ts +++ b/packages/cli/src/cli/commands/secrets/set.ts @@ -1,11 +1,8 @@ import { resolve } from "node:path"; import { log } from "@clack/prompts"; import type { Command } from "commander"; -import { - Base44Command, - type RunCommandResult, - runTask, -} from "@/cli/utils/index.js"; +import type { RunCommandResult } from "@/cli/types.js"; +import { Base44Command, runTask } from "@/cli/utils/index.js"; import { InvalidInputError } from "@/core/errors.js"; import { setSecrets } from "@/core/resources/secret/index.js"; import { parseEnvFile } from "@/core/utils/index.js"; diff --git a/packages/cli/src/cli/commands/site/deploy.ts b/packages/cli/src/cli/commands/site/deploy.ts index 18fd1952..2eb520bf 100644 --- a/packages/cli/src/cli/commands/site/deploy.ts +++ b/packages/cli/src/cli/commands/site/deploy.ts @@ -1,11 +1,8 @@ import { resolve } from "node:path"; import { confirm, isCancel } from "@clack/prompts"; import type { Command } from "commander"; -import { - Base44Command, - type RunCommandResult, - runTask, -} from "@/cli/utils/index.js"; +import type { RunCommandResult } from "@/cli/types.js"; +import { Base44Command, runTask } from "@/cli/utils/index.js"; import { ConfigNotFoundError } from "@/core/errors.js"; import { readProjectConfig } from "@/core/project/index.js"; import { deploySite } from "@/core/site/index.js"; diff --git a/packages/cli/src/cli/commands/site/open.ts b/packages/cli/src/cli/commands/site/open.ts index 0f4cb31e..d1323229 100644 --- a/packages/cli/src/cli/commands/site/open.ts +++ b/packages/cli/src/cli/commands/site/open.ts @@ -1,6 +1,7 @@ import type { Command } from "commander"; import open from "open"; -import { Base44Command, type RunCommandResult } from "@/cli/utils/index.js"; +import type { RunCommandResult } from "@/cli/types.js"; +import { Base44Command } from "@/cli/utils/index.js"; import { getSiteUrl } from "@/core/site/index.js"; async function openAction( diff --git a/packages/cli/src/cli/commands/types/generate.ts b/packages/cli/src/cli/commands/types/generate.ts index be936c1d..fe3a80a3 100644 --- a/packages/cli/src/cli/commands/types/generate.ts +++ b/packages/cli/src/cli/commands/types/generate.ts @@ -1,9 +1,6 @@ import type { Command } from "commander"; -import { - Base44Command, - type RunCommandResult, - runTask, -} from "@/cli/utils/index.js"; +import type { RunCommandResult } from "@/cli/types.js"; +import { Base44Command, runTask } from "@/cli/utils/index.js"; import { readProjectConfig } from "@/core/index.js"; import { generateTypesFile, updateProjectConfig } from "@/core/types/index.js"; diff --git a/packages/cli/src/cli/types.ts b/packages/cli/src/cli/types.ts index b72d30f6..400958fd 100644 --- a/packages/cli/src/cli/types.ts +++ b/packages/cli/src/cli/types.ts @@ -7,3 +7,12 @@ export interface CLIContext { isNonInteractive: boolean; distribution: Distribution; } + +export interface RunCommandResult { + outroMessage?: string; + /** + * Raw text to write to stdout after the command UI (intro/outro) finishes. + * Useful for commands that produce machine-readable or pipeable output. + */ + stdout?: string; +} diff --git a/packages/cli/src/cli/utils/banner.ts b/packages/cli/src/cli/utils/banner.ts index d2b3ddc4..80ba8a60 100644 --- a/packages/cli/src/cli/utils/banner.ts +++ b/packages/cli/src/cli/utils/banner.ts @@ -1,5 +1,4 @@ import { printAnimatedLines } from "@/cli/utils/animate.js"; -import { theme } from "@/cli/utils/theme.js"; const BANNER_LINES = [ "██████╗ █████╗ ███████╗███████╗ ██╗ ██╗██╗ ██╗", @@ -14,10 +13,6 @@ const BANNER_LINES = [ * Print the Base44 banner with smooth animation if supported, * or fall back to static banner in non-interactive environments. */ -export async function printBanner(isNonInteractive: boolean): Promise { - if (isNonInteractive) { - console.log(theme.colors.base44Orange(BANNER_LINES.join("\n"))); - } else { - await printAnimatedLines(BANNER_LINES); - } +export async function printBanner(): Promise { + await printAnimatedLines(BANNER_LINES); } diff --git a/packages/cli/src/cli/utils/command/Base44Command.ts b/packages/cli/src/cli/utils/command/Base44Command.ts index b3aff306..f12e2844 100644 --- a/packages/cli/src/cli/utils/command/Base44Command.ts +++ b/packages/cli/src/cli/utils/command/Base44Command.ts @@ -1,16 +1,14 @@ import { Command } from "commander"; -import type { CLIContext } from "@/cli/types.js"; +import type { CLIContext, RunCommandResult } from "@/cli/types.js"; import { - type RunCommandResult, - showError, - showIntro, - showOutro, + showCommandEnd, + showCommandStart, + showPlainError, + showThemedError, } from "@/cli/utils/command/display.js"; import { ensureAppConfig, ensureAuth } from "@/cli/utils/command/middleware.js"; import { startUpgradeCheck } from "@/cli/utils/upgradeNotification.js"; -export type { RunCommandResult } from "@/cli/utils/command/display.js"; - interface Base44CommandOptions { /** * Require user authentication before running this command. @@ -105,8 +103,8 @@ export class Base44Command extends Command { } /** @public - called by Commander internally via command dispatch */ - // biome-ignore lint/suspicious/noExplicitAny: must match Commander.js action() signature override action( + // biome-ignore lint/suspicious/noExplicitAny: must match Commander.js action() signature fn: (...args: any[]) => void | Promise, ): this { // biome-ignore lint/suspicious/noExplicitAny: must match Commander.js action() signature @@ -114,10 +112,7 @@ export class Base44Command extends Command { const quiet = this.context.isNonInteractive; if (!quiet) { - await showIntro( - this._commandOptions.fullBanner, - this.context.isNonInteractive, - ); + await showCommandStart(this._commandOptions.fullBanner); } const upgradeCheckPromise = startUpgradeCheck(); @@ -133,16 +128,25 @@ export class Base44Command extends Command { const result = ((await fn(...args)) ?? {}) as RunCommandResult; if (!quiet) { - await showOutro( + await showCommandEnd( result, upgradeCheckPromise, this.context.distribution, ); - } else if (result.stdout) { - process.stdout.write(result.stdout); + } else { + if (result.outroMessage) { + process.stderr.write(`${result.outroMessage}\n`); + } + if (result.stdout) { + process.stdout.write(result.stdout); + } } } catch (error) { - showError(error, this.context, quiet); + if (quiet) { + showPlainError(error); + } else { + showThemedError(error, this.context); + } throw error; } }); diff --git a/packages/cli/src/cli/utils/command/display.ts b/packages/cli/src/cli/utils/command/display.ts index d83dc222..73541482 100644 --- a/packages/cli/src/cli/utils/command/display.ts +++ b/packages/cli/src/cli/utils/command/display.ts @@ -1,29 +1,17 @@ import { intro, log, outro } from "@clack/prompts"; -import type { CLIContext } from "@/cli/types.js"; +import type { CLIContext, RunCommandResult } from "@/cli/types.js"; import { printBanner } from "@/cli/utils/banner.js"; import { theme } from "@/cli/utils/theme.js"; import { printUpgradeNotification } from "@/cli/utils/upgradeNotification.js"; import type { UpgradeInfo } from "@/cli/utils/version-check.js"; import { isCLIError } from "@/core/errors.js"; -export interface RunCommandResult { - outroMessage?: string; - /** - * Raw text to write to stdout after the command UI (intro/outro) finishes. - * Useful for commands that produce machine-readable or pipeable output. - */ - stdout?: string; -} - /** - * Show the intro banner or simple tag. + * Show the command start UI: intro banner or simple tag. */ -export async function showIntro( - fullBanner: boolean, - isNonInteractive: boolean, -): Promise { +export async function showCommandStart(fullBanner: boolean): Promise { if (fullBanner) { - await printBanner(isNonInteractive); + await printBanner(); intro(""); } else { intro(theme.colors.base44OrangeBackground(" Base 44 ")); @@ -31,9 +19,9 @@ export async function showIntro( } /** - * Show the outro: upgrade notification, outro message, and optional stdout. + * Show the command end UI: upgrade notification, outro message, and stdout. */ -export async function showOutro( +export async function showCommandEnd( result: RunCommandResult, upgradeCheckPromise: Promise, distribution: CLIContext["distribution"], @@ -47,27 +35,9 @@ export async function showOutro( } /** - * Display an error to the user. - * - * When `quiet` is true (non-interactive / CI), writes a plain error message - * to stderr without clack formatting or ASCII codes. - * When `quiet` is false, uses clack log and themed formatting. + * Display an error with clack-themed formatting (interactive mode). */ -export function showError( - error: unknown, - context: CLIContext, - quiet: boolean, -): void { - if (quiet) { - showPlainError(error); - } else { - showThemedError(error); - const errorContext = context.errorReporter.getErrorContext(); - outro(theme.format.errorContext(errorContext)); - } -} - -function showThemedError(error: unknown): void { +export function showThemedError(error: unknown, context: CLIContext): void { const errorMessage = error instanceof Error ? error.message : String(error); log.error(errorMessage); @@ -85,9 +55,15 @@ function showThemedError(error: unknown): void { if (process.env.DEBUG === "1" && error instanceof Error && error.stack) { log.error(theme.styles.dim(error.stack)); } + + const errorContext = context.errorReporter.getErrorContext(); + outro(theme.format.errorContext(errorContext)); } -function showPlainError(error: unknown): void { +/** + * Display an error as plain text to stderr (non-interactive / CI mode). + */ +export function showPlainError(error: unknown): void { const errorMessage = error instanceof Error ? error.message : String(error); process.stderr.write(`Error: ${errorMessage}\n`); From 6f896bb542df395af7e3382d982363623421815d Mon Sep 17 00:00:00 2001 From: Kfir Strikovsky Date: Mon, 16 Mar 2026 18:03:29 +0200 Subject: [PATCH 05/12] upgrade notification changes --- packages/cli/src/cli/utils/command/Base44Command.ts | 13 +++++++++++-- packages/cli/src/cli/utils/upgradeNotification.ts | 11 +++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/packages/cli/src/cli/utils/command/Base44Command.ts b/packages/cli/src/cli/utils/command/Base44Command.ts index f12e2844..902cfc1f 100644 --- a/packages/cli/src/cli/utils/command/Base44Command.ts +++ b/packages/cli/src/cli/utils/command/Base44Command.ts @@ -7,7 +7,10 @@ import { showThemedError, } from "@/cli/utils/command/display.js"; import { ensureAppConfig, ensureAuth } from "@/cli/utils/command/middleware.js"; -import { startUpgradeCheck } from "@/cli/utils/upgradeNotification.js"; +import { + formatPlainUpgradeMessage, + startUpgradeCheck, +} from "@/cli/utils/upgradeNotification.js"; interface Base44CommandOptions { /** @@ -135,11 +138,17 @@ export class Base44Command extends Command { ); } else { if (result.outroMessage) { - process.stderr.write(`${result.outroMessage}\n`); + process.stdout.write(`${result.outroMessage}\n`); } if (result.stdout) { process.stdout.write(result.stdout); } + const upgradeInfo = await upgradeCheckPromise; + if (upgradeInfo) { + process.stderr.write( + `${formatPlainUpgradeMessage(upgradeInfo, this.context.distribution)}\n`, + ); + } } } catch (error) { if (quiet) { diff --git a/packages/cli/src/cli/utils/upgradeNotification.ts b/packages/cli/src/cli/utils/upgradeNotification.ts index 912a31ab..7e8fd468 100644 --- a/packages/cli/src/cli/utils/upgradeNotification.ts +++ b/packages/cli/src/cli/utils/upgradeNotification.ts @@ -48,6 +48,17 @@ function formatUpgradeMessage( ].join("\n"); } +/** + * Format upgrade info as a plain-text one-liner for non-interactive / CI output. + */ +export function formatPlainUpgradeMessage( + info: UpgradeInfo, + distribution: Distribution, +): string { + const instruction = getUpgradeInstruction(detectInstallMethod(distribution)); + return `Update available: ${info.currentVersion} → ${info.latestVersion}. ${instruction}`; +} + export async function printUpgradeNotification( upgradeCheckPromise: Promise, distribution: Distribution, From e934867daaf323f29c00f0982427af5d879b13b7 Mon Sep 17 00:00:00 2001 From: Kfir Strikovsky Date: Mon, 16 Mar 2026 18:19:55 +0200 Subject: [PATCH 06/12] small fix to create command --- .../cli/src/cli/commands/project/create.ts | 49 +++++++++++++------ 1 file changed, 34 insertions(+), 15 deletions(-) diff --git a/packages/cli/src/cli/commands/project/create.ts b/packages/cli/src/cli/commands/project/create.ts index c5e671a2..b26ef101 100644 --- a/packages/cli/src/cli/commands/project/create.ts +++ b/packages/cli/src/cli/commands/project/create.ts @@ -305,19 +305,38 @@ Examples: $ base44 create my-app --path ./projects/my-app --deploy Creates a base44 project at ./project/my-app and deploys it`, ) .hook("preAction", validateNonInteractiveFlags) - .action(async (name: string | undefined, options: CreateOptions) => { - if (name && !options.path) { - options.path = `./${kebabCase(name)}`; - } - - const isNonInteractive = !!(options.name ?? name) && !!options.path; - - if (isNonInteractive) { - return await createNonInteractive({ - name: options.name ?? name, - ...options, - }); - } - return await createInteractive({ name, ...options }); - }); + .action( + async ( + name: string | undefined, + options: CreateOptions, + command: Base44Command, + ) => { + if (name && !options.path) { + options.path = `./${kebabCase(name)}`; + } + + const skipPrompts = !!(options.name ?? name) && !!options.path; + + if (!skipPrompts && command.isNonInteractive) { + throw new InvalidInputError( + "Project name and --path are required in non-interactive mode", + { + hints: [ + { + message: "Usage: base44 create --path ", + }, + ], + }, + ); + } + + if (skipPrompts) { + return await createNonInteractive({ + name: options.name ?? name, + ...options, + }); + } + return await createInteractive({ name, ...options }); + }, + ); } From ca01860f3768e3026ff393e30f3f2d702e7a5e8d Mon Sep 17 00:00:00 2001 From: Kfir Strikovsky Date: Mon, 16 Mar 2026 18:24:34 +0200 Subject: [PATCH 07/12] update non interactivness --- packages/cli/src/cli/commands/project/eject.ts | 10 ++++++++++ packages/cli/src/cli/commands/project/link.ts | 8 +++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/packages/cli/src/cli/commands/project/eject.ts b/packages/cli/src/cli/commands/project/eject.ts index 75afe04c..3d86ccb6 100644 --- a/packages/cli/src/cli/commands/project/eject.ts +++ b/packages/cli/src/cli/commands/project/eject.ts @@ -178,6 +178,16 @@ export function getEjectCommand(): Command { ) .option("-y, --yes", "Skip confirmation prompts") .action(async (options: EjectOptions, command: Base44Command) => { + if (command.isNonInteractive && !options.projectId) { + throw new InvalidInputError( + "--project-id is required in non-interactive mode", + ); + } + if (command.isNonInteractive && !options.path) { + throw new InvalidInputError( + "--path is required in non-interactive mode", + ); + } return await eject({ ...options, isNonInteractive: command.isNonInteractive, diff --git a/packages/cli/src/cli/commands/project/link.ts b/packages/cli/src/cli/commands/project/link.ts index f70d3e41..4a23b880 100644 --- a/packages/cli/src/cli/commands/project/link.ts +++ b/packages/cli/src/cli/commands/project/link.ts @@ -260,7 +260,13 @@ export function getLinkCommand(): Command { "Project ID to link to an existing project (skips selection prompt)", ) .hook("preAction", validateNonInteractiveFlags) - .action(async (options: LinkOptions) => { + .action(async (options: LinkOptions, command: Base44Command) => { + const skipPrompts = !!options.create || !!options.projectId; + if (!skipPrompts && command.isNonInteractive) { + throw new InvalidInputError( + "Either --create --name or --projectId is required in non-interactive mode", + ); + } return await link(options); }); } From b1cb93ce51a94d1b5e52e43f9e72d4eb1a52f62a Mon Sep 17 00:00:00 2001 From: Kfir Strikovsky Date: Mon, 16 Mar 2026 18:29:06 +0200 Subject: [PATCH 08/12] small docs change --- docs/commands.md | 32 ++++++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/docs/commands.md b/docs/commands.md index 9a3d5a72..42b55203 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -10,7 +10,8 @@ Commands live in `src/cli/commands//`. They use a **factory pattern** // src/cli/commands//.ts import { log } from "@clack/prompts"; import type { Command } from "commander"; -import { Base44Command, type RunCommandResult, runTask, theme } from "@/cli/utils/index.js"; +import type { RunCommandResult } from "@/cli/types.js"; +import { Base44Command, runTask, theme } from "@/cli/utils/index.js"; async function myAction(): Promise { const result = await runTask( @@ -109,6 +110,32 @@ async function myAction(isNonInteractive: boolean): Promise { } ``` +### Guarding Interactive Commands + +Commands that use interactive prompts (`select`, `text`, `confirm`, `group` from `@clack/prompts`) **must** guard against non-interactive environments. If the user didn't provide enough flags to skip all prompts, error early: + +```typescript +export function getMyCommand(): Command { + return new Base44Command("my-cmd", { requireAppConfig: false }) + .option("-n, --name ", "Project name") + .option("-p, --path ", "Project path") + .action(async (options: MyOptions, command: Base44Command) => { + const skipPrompts = !!options.name && !!options.path; + if (!skipPrompts && command.isNonInteractive) { + throw new InvalidInputError( + "--name and --path are required in non-interactive mode", + ); + } + if (skipPrompts) { + return await createNonInteractive(options); + } + return await createInteractive(options); + }); +} +``` + +Commands that only use `log.*` (display-only, no input) don't need this guard. See `project/create.ts`, `project/link.ts`, and `project/eject.ts` for real examples. + ## runTask (Async Operations with Spinners) Use `runTask()` for any async operation that takes time: @@ -195,9 +222,10 @@ Access `command.args` for positional arguments and `command.opts()` for options ## Rules (Command-Specific) - **Command factory pattern** - Commands export `getXCommand()` functions (no parameters), not static instances -- **Use `Base44Command`** - All commands use `new Base44Command(name, options?)` instead of `new Command()`. The lifecycle (intro, auth, config, outro, error handling) is automatic. +- **Use `Base44Command`** - All commands use `new Base44Command(name, options?)` - **No context plumbing** - Context is injected automatically. Command files should never import `CLIContext`. - **Task wrapper** - Use `runTask()` for async operations with spinners - **Use theme for styling** - Never use `chalk` directly; import `theme` from `@/cli/utils/` and use semantic names - **Use fs.ts utilities** - Always use `@/core/utils/fs.js` for file operations +- **Guard interactive prompts** - Commands using `select`, `text`, `confirm`, or `group` from `@clack/prompts` must check `command.isNonInteractive` and throw `InvalidInputError` if required flags are missing. Never let prompts hang in CI. - **Consistent copy across related commands** - User-facing messages (errors, success, hints) for commands in the same group should use consistent language and structure. When writing validation errors, outro messages, or spinner text, check sibling commands for parity so the product voice stays coherent. From 1833587da3373799de6705ce7c6ff8025a7a4573 Mon Sep 17 00:00:00 2001 From: Kfir Strikovsky Date: Mon, 16 Mar 2026 18:34:22 +0200 Subject: [PATCH 09/12] version cehck test fix --- packages/cli/tests/cli/version-check.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/cli/tests/cli/version-check.spec.ts b/packages/cli/tests/cli/version-check.spec.ts index e9f826f5..63efab9f 100644 --- a/packages/cli/tests/cli/version-check.spec.ts +++ b/packages/cli/tests/cli/version-check.spec.ts @@ -11,7 +11,7 @@ describe("upgrade notification", () => { const result = await t.run("whoami"); t.expectResult(result).toSucceed(); - t.expectResult(result).toContain("Update available!"); + t.expectResult(result).toContain("Update available"); t.expectResult(result).toContain("1.0.0"); }); @@ -22,7 +22,7 @@ describe("upgrade notification", () => { const result = await t.run("whoami"); t.expectResult(result).toSucceed(); - t.expectResult(result).toNotContain("Update available!"); + t.expectResult(result).toNotContain("Update available"); }); it("does not display notification when check is not overridden", async () => { From 5ab1a506b3924bd73deb2eb9e49310c75627ce62 Mon Sep 17 00:00:00 2001 From: Kfir Strikovsky Date: Mon, 16 Mar 2026 18:38:02 +0200 Subject: [PATCH 10/12] fix tests --- packages/cli/src/cli/commands/auth/logout.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/cli/src/cli/commands/auth/logout.ts b/packages/cli/src/cli/commands/auth/logout.ts index 227191f9..2fd58a0e 100644 --- a/packages/cli/src/cli/commands/auth/logout.ts +++ b/packages/cli/src/cli/commands/auth/logout.ts @@ -9,7 +9,10 @@ async function logout(): Promise { } export function getLogoutCommand(): Command { - return new Base44Command("logout", { requireAppConfig: false }) + return new Base44Command("logout", { + requireAuth: false, + requireAppConfig: false, + }) .description("Logout from current device") .action(logout); } From 218641e8aeae72b213ab93768a9ccca28723c566 Mon Sep 17 00:00:00 2001 From: Kfir Strikovsky Date: Tue, 17 Mar 2026 00:05:19 +0200 Subject: [PATCH 11/12] handle review comments and added tests --- .../cli/src/cli/commands/project/deploy.ts | 6 ++++++ packages/cli/src/cli/commands/site/deploy.ts | 7 ++++++- .../cli/src/cli/utils/command/Base44Command.ts | 2 +- packages/cli/src/cli/utils/command/index.ts | 2 -- packages/cli/tests/cli/create.spec.ts | 10 ++++++++++ packages/cli/tests/cli/deploy.spec.ts | 12 ++++++++++++ packages/cli/tests/cli/eject.spec.ts | 18 ++++++++++++++++++ packages/cli/tests/cli/link.spec.ts | 9 +++++++++ packages/cli/tests/cli/site_deploy.spec.ts | 11 +++++++++++ 9 files changed, 73 insertions(+), 4 deletions(-) diff --git a/packages/cli/src/cli/commands/project/deploy.ts b/packages/cli/src/cli/commands/project/deploy.ts index 7e48a5d3..cbef0592 100644 --- a/packages/cli/src/cli/commands/project/deploy.ts +++ b/packages/cli/src/cli/commands/project/deploy.ts @@ -12,6 +12,7 @@ import { getDashboardUrl, theme, } from "@/cli/utils/index.js"; +import { InvalidInputError } from "@/core/errors.js"; import { deployAll, hasResourcesToDeploy, @@ -130,6 +131,11 @@ export function getDeployCommand(): Command { ) .option("-y, --yes", "Skip confirmation prompt") .action(async (options: DeployOptions, command: Base44Command) => { + if (command.isNonInteractive && !options.yes) { + throw new InvalidInputError( + "--yes is required in non-interactive mode", + ); + } return await deployAction({ ...options, isNonInteractive: command.isNonInteractive, diff --git a/packages/cli/src/cli/commands/site/deploy.ts b/packages/cli/src/cli/commands/site/deploy.ts index 2eb520bf..86ace905 100644 --- a/packages/cli/src/cli/commands/site/deploy.ts +++ b/packages/cli/src/cli/commands/site/deploy.ts @@ -3,7 +3,7 @@ import { confirm, isCancel } from "@clack/prompts"; import type { Command } from "commander"; import type { RunCommandResult } from "@/cli/types.js"; import { Base44Command, runTask } from "@/cli/utils/index.js"; -import { ConfigNotFoundError } from "@/core/errors.js"; +import { ConfigNotFoundError, InvalidInputError } from "@/core/errors.js"; import { readProjectConfig } from "@/core/project/index.js"; import { deploySite } from "@/core/site/index.js"; @@ -57,6 +57,11 @@ export function getSiteDeployCommand(): Command { .description("Deploy built site files to Base44 hosting") .option("-y, --yes", "Skip confirmation prompt") .action(async (options: DeployOptions, command: Base44Command) => { + if (command.isNonInteractive && !options.yes) { + throw new InvalidInputError( + "--yes is required in non-interactive mode", + ); + } return await deployAction({ ...options, isNonInteractive: command.isNonInteractive, diff --git a/packages/cli/src/cli/utils/command/Base44Command.ts b/packages/cli/src/cli/utils/command/Base44Command.ts index 902cfc1f..d4d6873c 100644 --- a/packages/cli/src/cli/utils/command/Base44Command.ts +++ b/packages/cli/src/cli/utils/command/Base44Command.ts @@ -90,8 +90,8 @@ export class Base44Command extends Command { * Whether the CLI is running in non-interactive mode (CI, piped output). * Available for action functions that need to adjust behavior * (e.g. skip browser opens, skip confirmation prompts). + * @public */ - /** @public */ get isNonInteractive(): boolean { return this._context?.isNonInteractive ?? false; } diff --git a/packages/cli/src/cli/utils/command/index.ts b/packages/cli/src/cli/utils/command/index.ts index e5112d25..70788e23 100644 --- a/packages/cli/src/cli/utils/command/index.ts +++ b/packages/cli/src/cli/utils/command/index.ts @@ -1,3 +1 @@ export * from "./Base44Command.js"; -export * from "./display.js"; -export * from "./middleware.js"; diff --git a/packages/cli/tests/cli/create.spec.ts b/packages/cli/tests/cli/create.spec.ts index c254cd86..02f33bac 100644 --- a/packages/cli/tests/cli/create.spec.ts +++ b/packages/cli/tests/cli/create.spec.ts @@ -7,6 +7,16 @@ describe("create command", () => { // ─── NON-INTERACTIVE MODE ───────────────────────────────────── + it("fails when name and path are missing in non-interactive mode", async () => { + await t.givenLoggedIn({ email: "test@example.com", name: "Test User" }); + t.api.mockCreateApp({ id: "app-123", name: "test" }); + + const result = await t.run("create"); + + t.expectResult(result).toFail(); + t.expectResult(result).toContain("required in non-interactive mode"); + }); + it("fails when --path is provided without name argument", async () => { await t.givenLoggedIn({ email: "test@example.com", name: "Test User" }); diff --git a/packages/cli/tests/cli/deploy.spec.ts b/packages/cli/tests/cli/deploy.spec.ts index c2f5cb92..bd59390d 100644 --- a/packages/cli/tests/cli/deploy.spec.ts +++ b/packages/cli/tests/cli/deploy.spec.ts @@ -4,6 +4,18 @@ import { fixture, setupCLITests } from "./testkit/index.js"; describe("deploy command (unified)", () => { const t = setupCLITests(); + it("fails when --yes is not provided in non-interactive mode", async () => { + await t.givenLoggedInWithProject(fixture("with-entities")); + t.api.mockEntitiesPush({ created: ["Task"], updated: [], deleted: [] }); + + const result = await t.run("deploy"); + + t.expectResult(result).toFail(); + t.expectResult(result).toContain( + "--yes is required in non-interactive mode", + ); + }); + it("reports no resources when project is empty", async () => { await t.givenLoggedInWithProject(fixture("basic")); diff --git a/packages/cli/tests/cli/eject.spec.ts b/packages/cli/tests/cli/eject.spec.ts index 75dfa57b..763cdc7c 100644 --- a/packages/cli/tests/cli/eject.spec.ts +++ b/packages/cli/tests/cli/eject.spec.ts @@ -31,6 +31,24 @@ async function createTarFromFixture(fixturePath: string): Promise { describe("eject command", () => { const t = setupCLITests(); + it("fails when --path is missing in non-interactive mode", async () => { + await t.givenLoggedIn({ email: "test@example.com", name: "Test User" }); + t.api.mockListProjects([ + { + id: "app-1", + name: "Test", + is_managed_source_code: true, + }, + ]); + + const result = await t.run("eject", "--project-id", "app-1"); + + t.expectResult(result).toFail(); + t.expectResult(result).toContain( + "--path is required in non-interactive mode", + ); + }); + it("fails when project ID not found", async () => { await t.givenLoggedIn({ email: "test@example.com", name: "Test User" }); t.api.mockListProjects([ diff --git a/packages/cli/tests/cli/link.spec.ts b/packages/cli/tests/cli/link.spec.ts index 80cc1d18..e6e95a44 100644 --- a/packages/cli/tests/cli/link.spec.ts +++ b/packages/cli/tests/cli/link.spec.ts @@ -4,6 +4,15 @@ import { fixture, setupCLITests } from "./testkit/index.js"; describe("link command", () => { const t = setupCLITests(); + it("fails when neither --create nor --projectId in non-interactive mode", async () => { + await t.givenLoggedInWithProject(fixture("no-app-config")); + + const result = await t.run("link"); + + t.expectResult(result).toFail(); + t.expectResult(result).toContain("required in non-interactive mode"); + }); + it("fails when not in a project directory", async () => { await t.givenLoggedIn({ email: "test@example.com", name: "Test User" }); diff --git a/packages/cli/tests/cli/site_deploy.spec.ts b/packages/cli/tests/cli/site_deploy.spec.ts index 4de0417f..578f2e95 100644 --- a/packages/cli/tests/cli/site_deploy.spec.ts +++ b/packages/cli/tests/cli/site_deploy.spec.ts @@ -4,6 +4,17 @@ import { fixture, setupCLITests } from "./testkit/index.js"; describe("site deploy command", () => { const t = setupCLITests(); + it("fails when --yes is not provided in non-interactive mode", async () => { + await t.givenLoggedInWithProject(fixture("with-site")); + + const result = await t.run("site", "deploy"); + + t.expectResult(result).toFail(); + t.expectResult(result).toContain( + "--yes is required in non-interactive mode", + ); + }); + it("fails when no site configuration found", async () => { await t.givenLoggedInWithProject(fixture("basic")); From a2209138ff44e9122bfc6370c6e64d945b185551 Mon Sep 17 00:00:00 2001 From: Kfir Strikovsky Date: Tue, 17 Mar 2026 15:50:43 +0200 Subject: [PATCH 12/12] address review comments --- packages/cli/src/cli/commands/dev.ts | 4 +--- packages/cli/src/cli/commands/functions/pull.ts | 4 +--- packages/cli/src/cli/commands/project/link.ts | 2 +- packages/cli/src/cli/commands/project/logs.ts | 4 +--- packages/cli/src/cli/commands/secrets/delete.ts | 4 +--- packages/cli/src/cli/commands/secrets/set.ts | 4 +--- packages/cli/src/cli/utils/command/Base44Command.ts | 4 ++-- packages/cli/src/cli/utils/command/{display.ts => render.ts} | 0 8 files changed, 8 insertions(+), 18 deletions(-) rename packages/cli/src/cli/utils/command/{display.ts => render.ts} (100%) diff --git a/packages/cli/src/cli/commands/dev.ts b/packages/cli/src/cli/commands/dev.ts index 1c7d4516..7dc7b8de 100644 --- a/packages/cli/src/cli/commands/dev.ts +++ b/packages/cli/src/cli/commands/dev.ts @@ -29,7 +29,5 @@ export function getDevCommand(): Command { return new Base44Command("dev") .description("Start the development server") .option("-p, --port ", "Port for the development server") - .action(async (options: DevOptions) => { - return await devAction(options); - }); + .action(devAction); } diff --git a/packages/cli/src/cli/commands/functions/pull.ts b/packages/cli/src/cli/commands/functions/pull.ts index 0892901a..c4bcda59 100644 --- a/packages/cli/src/cli/commands/functions/pull.ts +++ b/packages/cli/src/cli/commands/functions/pull.ts @@ -68,7 +68,5 @@ export function getPullCommand(): Command { return new Base44Command("pull") .description("Pull deployed functions from Base44") .argument("[name]", "Function name to pull (pulls all if omitted)") - .action(async (name: string | undefined) => { - return pullFunctionsAction(name); - }); + .action(pullFunctionsAction); } diff --git a/packages/cli/src/cli/commands/project/link.ts b/packages/cli/src/cli/commands/project/link.ts index 4a23b880..c3c17e00 100644 --- a/packages/cli/src/cli/commands/project/link.ts +++ b/packages/cli/src/cli/commands/project/link.ts @@ -264,7 +264,7 @@ export function getLinkCommand(): Command { const skipPrompts = !!options.create || !!options.projectId; if (!skipPrompts && command.isNonInteractive) { throw new InvalidInputError( - "Either --create --name or --projectId is required in non-interactive mode", + "--create with --name, or --projectId, is required in non-interactive mode", ); } return await link(options); diff --git a/packages/cli/src/cli/commands/project/logs.ts b/packages/cli/src/cli/commands/project/logs.ts index 960d07a1..52e190c4 100644 --- a/packages/cli/src/cli/commands/project/logs.ts +++ b/packages/cli/src/cli/commands/project/logs.ts @@ -231,7 +231,5 @@ export function getLogsCommand(): Command { .addOption( new Option("--order ", "Sort order").choices(["asc", "desc"]), ) - .action(async (options: LogsOptions) => { - return await logsAction(options); - }); + .action(logsAction); } diff --git a/packages/cli/src/cli/commands/secrets/delete.ts b/packages/cli/src/cli/commands/secrets/delete.ts index 3e7a65d3..d68ff7e2 100644 --- a/packages/cli/src/cli/commands/secrets/delete.ts +++ b/packages/cli/src/cli/commands/secrets/delete.ts @@ -24,7 +24,5 @@ export function getSecretsDeleteCommand(): Command { return new Base44Command("delete") .description("Delete a secret") .argument("", "Secret name to delete") - .action(async (key: string) => { - return await deleteSecretAction(key); - }); + .action(deleteSecretAction); } diff --git a/packages/cli/src/cli/commands/secrets/set.ts b/packages/cli/src/cli/commands/secrets/set.ts index 8ca2d477..ec773b70 100644 --- a/packages/cli/src/cli/commands/secrets/set.ts +++ b/packages/cli/src/cli/commands/secrets/set.ts @@ -94,7 +94,5 @@ export function getSecretsSetCommand(): Command { .description("Set one or more secrets (KEY=VALUE format)") .argument("[entries...]", "KEY=VALUE pairs (e.g. KEY1=VALUE1 KEY2=VALUE2)") .option("--env-file ", "Path to .env file") - .action(async (entries: string[], options: { envFile?: string }) => { - return await setSecretsAction(entries, options); - }); + .action(setSecretsAction); } diff --git a/packages/cli/src/cli/utils/command/Base44Command.ts b/packages/cli/src/cli/utils/command/Base44Command.ts index d4d6873c..44b780d9 100644 --- a/packages/cli/src/cli/utils/command/Base44Command.ts +++ b/packages/cli/src/cli/utils/command/Base44Command.ts @@ -1,12 +1,12 @@ import { Command } from "commander"; import type { CLIContext, RunCommandResult } from "@/cli/types.js"; +import { ensureAppConfig, ensureAuth } from "@/cli/utils/command/middleware.js"; import { showCommandEnd, showCommandStart, showPlainError, showThemedError, -} from "@/cli/utils/command/display.js"; -import { ensureAppConfig, ensureAuth } from "@/cli/utils/command/middleware.js"; +} from "@/cli/utils/command/render.js"; import { formatPlainUpgradeMessage, startUpgradeCheck, diff --git a/packages/cli/src/cli/utils/command/display.ts b/packages/cli/src/cli/utils/command/render.ts similarity index 100% rename from packages/cli/src/cli/utils/command/display.ts rename to packages/cli/src/cli/utils/command/render.ts