|
1 | 1 | # Adding & Modifying CLI Commands |
2 | 2 |
|
3 | | -**Keywords:** command, factory pattern, CLIContext, isNonInteractive, runCommand, runTask, spinner, theming, chalk, program.ts, register, banner, intro, outro |
| 3 | +**Keywords:** command, factory pattern, CLIContext, isNonInteractive, isJsonMode, runCommand, runTask, spinner, theming, chalk, program.ts, register, banner, intro, outro, json, --json, data, piping |
4 | 4 |
|
5 | 5 | Commands live in `src/cli/commands/<domain>/`. They use a **factory pattern** with dependency injection via `CLIContext`. |
6 | 6 |
|
@@ -79,11 +79,13 @@ await runCommand(myAction, { fullBanner: true, requireAuth: true }, context); |
79 | 79 | export interface CLIContext { |
80 | 80 | errorReporter: ErrorReporter; |
81 | 81 | isNonInteractive: boolean; |
| 82 | + isJsonMode: boolean; |
82 | 83 | } |
83 | 84 | ``` |
84 | 85 |
|
85 | 86 | - Created once in `runCLI()` at startup |
86 | 87 | - `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. |
| 88 | +- `isJsonMode` is set by the global `--json` flag via a `preAction` hook. Commands don't need to check it directly -- `runCommand` handles all mode-switching. |
87 | 89 | - Passed to `createProgram(context)`, which passes it to each command factory |
88 | 90 | - Commands pass it to `runCommand()` for error reporting integration |
89 | 91 |
|
@@ -195,6 +197,73 @@ export function getMyCommand(context: CLIContext): Command { |
195 | 197 |
|
196 | 198 | Access `command.args` for positional arguments and `command.opts()` for options inside the hook. See `secrets/set.ts` and `project/create.ts` for real examples. |
197 | 199 |
|
| 200 | +## JSON Mode (`--json`) |
| 201 | + |
| 202 | +The CLI supports a global `--json` flag that outputs structured JSON instead of human-readable text. This enables piping output to tools like `jq`: |
| 203 | + |
| 204 | +```bash |
| 205 | +base44 logs --function my-fn --json | jq '.logs[] | .message' |
| 206 | +``` |
| 207 | + |
| 208 | +### How it works |
| 209 | + |
| 210 | +When `--json` is set, `runCommand` mutes `process.stdout.write` before calling `commandFn()`. This silences all clack output (intro, outro, `log.*`, spinners) automatically. Only the serialized `result.data` is written to stdout. Errors are written to stderr as JSON. |
| 211 | + |
| 212 | +### Adding JSON support to a command |
| 213 | + |
| 214 | +Return a `data` field from your action. Always use a top-level object (wrap arrays): |
| 215 | + |
| 216 | +```typescript |
| 217 | +async function listAction(): Promise<RunCommandResult> { |
| 218 | + const items = await fetchItems(); |
| 219 | + |
| 220 | + return { |
| 221 | + outroMessage: `Found ${items.length} items`, |
| 222 | + stdout: formatItems(items), // human mode |
| 223 | + data: { items }, // json mode: { "items": [...] } |
| 224 | + }; |
| 225 | +} |
| 226 | +``` |
| 227 | + |
| 228 | +- `data` is `Record<string, unknown>` -- always an object, never a raw array |
| 229 | +- If `data` is not set, `runCommand` falls back to `{ "message": outroMessage }` |
| 230 | +- Commands need zero changes to be silenced -- the stdout muting handles `log.*` and spinners |
| 231 | + |
| 232 | +### Error format |
| 233 | + |
| 234 | +On error in JSON mode, a JSON object is written to stderr: |
| 235 | + |
| 236 | +```json |
| 237 | +{ |
| 238 | + "error": true, |
| 239 | + "code": "API_ERROR", |
| 240 | + "message": "Request failed with status 500", |
| 241 | + "hints": [{ "message": "Check your network connection" }] |
| 242 | +} |
| 243 | +``` |
| 244 | + |
| 245 | +### Interactive commands |
| 246 | + |
| 247 | +Commands with interactive prompts (`select`, `confirm`, `text`) should validate required flags in a `preAction` hook. This fires before `runCommand` (no wasted auth/API calls) and works for both `--json` and piped/CI sessions: |
| 248 | + |
| 249 | +```typescript |
| 250 | +export function getMyCommand(context: CLIContext): Command { |
| 251 | + return new Command("my-cmd") |
| 252 | + .option("-y, --yes", "Skip confirmation prompt") |
| 253 | + .hook("preAction", (command) => { |
| 254 | + if (!context.isJsonMode && !context.isNonInteractive) return; |
| 255 | + if (!command.opts().yes) { |
| 256 | + command.error("Non-interactive mode requires: --yes"); |
| 257 | + } |
| 258 | + }) |
| 259 | + .action(async (options) => { |
| 260 | + await runCommand(() => myAction(options), { requireAuth: true }, context); |
| 261 | + }); |
| 262 | +} |
| 263 | +``` |
| 264 | + |
| 265 | +List specific missing flags so users know exactly what to add (e.g., `Missing: --project-id <id>, --path <path>`). |
| 266 | + |
198 | 267 | ## Rules (Command-Specific) |
199 | 268 |
|
200 | 269 | - **Command factory pattern** - Commands export `getXCommand(context)` functions, not static instances |
|
0 commit comments