Skip to content

feature: introduce global --json flag#342

Draft
kfirstri wants to merge 3 commits intomainfrom
support-json-flag
Draft

feature: introduce global --json flag#342
kfirstri wants to merge 3 commits intomainfrom
support-json-flag

Conversation

@kfirstri
Copy link
Collaborator

@kfirstri kfirstri commented Feb 25, 2026

Note

Description

This PR introduces a global --json flag that switches the CLI from human-readable output to structured JSON, enabling commands to be piped into tools like jq. When active, runCommand mutes all clack UI output (intro, outro, spinners, log.*) by patching process.stdout.write, and writes only the serialized result.data object to stdout. Errors are emitted to stderr as structured JSON objects.

Related Issue

None

Type of Change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Documentation update
  • Refactoring (no functional changes)
  • Other (please describe):

Changes Made

  • Added isJsonMode field to CLIContext and initialize it to false in runCLI()
  • Registered a global --json option and preAction hook in program.ts that sets context.isJsonMode = true
  • Refactored runCommand to mute process.stdout.write in JSON mode, output result.data (or { message: outroMessage } fallback) to stdout, and emit structured error objects to stderr
  • Added data?: Record<string, unknown> field to RunCommandResult for commands to return structured output
  • Migrated logs command from its own --json option to the global flag, returning { logs: entries } as data
  • Added preAction validation hooks to create, link, deploy, eject, and site/deploy commands so they fail fast with clear error messages in JSON/non-interactive mode when required flags are missing
  • Added JSON mode test cases for the logs command covering structured output, empty results, UI suppression, and the fallback message format
  • Updated docs/commands.md with a comprehensive JSON mode guide including usage examples, error format, and patterns for interactive commands

Testing

  • I have tested these changes locally
  • I have added/updated tests as needed
  • All tests pass (npm test)

Checklist

  • My code follows the project's style guidelines
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation (if applicable)
  • My changes generate no new warnings
  • I have updated docs/ (AGENTS.md) if I made architectural changes

Additional Notes

The stdout-muting approach means existing commands get JSON-mode silence for free — no per-command changes needed. Only commands that return machine-readable output need to add a data field. The preAction validation pattern ensures auth and API calls are never wasted when required flags are absent in non-interactive or JSON sessions.


🤖 Generated by Claude | 2026-02-25 15:30 UTC

@kfirstri kfirstri requested a review from Paveltarno February 25, 2026 14:55
@kfirstri kfirstri self-assigned this Feb 25, 2026
@github-actions
Copy link
Contributor

github-actions bot commented Feb 25, 2026

🚀 Package Preview Available!


Install this PR's preview build with npm:

npm i @base44-preview/cli@0.0.36-pr.342.1919b84

Prefer not to change any import paths? Install using npm alias so your code still imports base44:

npm i "base44@npm:@base44-preview/cli@0.0.36-pr.342.1919b84"

Or add it to your package.json dependencies:

{
  "dependencies": {
    "base44": "npm:@base44-preview/cli@0.0.36-pr.342.1919b84"
  }
}

Preview published to npm registry — try new features instantly!

```typescript
await runCommand(myAction, { requireAuth: true, interactive: true }, context);
```

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe this is too mcuh

fullBanner: true,
requireAuth: true,
requireAppConfig: false,
interactive: true,
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this new "interactive: true" basically blocks --json

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe it's weird since we can pass flags to it? although why would we want --json for create? not sure

}
await printUpgradeNotificationIfAvailable();
const json = context.isJsonMode;
let savedStdoutWrite: typeof process.stdout.write | undefined;
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is our current trick to mute log.info message and such.. clack uses process.stdout.write, so we patch it..
a bit hacky solution, might want to think about a log layer

@kfirstri
Copy link
Collaborator Author

@claude review this pr, evaluate the code, compare it to the docs/.

@claude
Copy link

claude bot commented Feb 25, 2026

Claude finished @kfirstri's task in 3m 23s —— View job


PR Review: feature: introduce global --json flag

Tasks:

  • Gather context
  • Read changed files
  • Read relevant docs
  • Analyze implementation vs docs
  • Post review

Overall Assessment

The architecture is sound and aligns well with the documented patterns in docs/commands.md. The global preAction hook approach for setting isJsonMode, the stdout muting, and the data field on RunCommandResult are all consistent with the coding conventions. The docs were also updated in the same PR, which is great. A few issues worth addressing before merging:


Bug: eject.ts — unguarded confirm prompt in JSON/non-interactive mode

src/cli/commands/project/eject.ts:142

const shouldDeploy = options.yes
  ? true
  : await confirm({ message: "Would you like to deploy your project now?" });

The validateNonInteractiveMode hook requires --project-id and --path in JSON mode, but does not require --yes. If the project has installCommand and buildCommand in its config, this confirm() will be reached in JSON mode (when --yes is absent), causing the CLI to hang waiting for user input.

Fix: add --yes to the required flags list in validateNonInteractiveMode for eject, or default shouldDeploy to false in JSON/non-interactive mode. Fix this →


Issue: Error format in docs omits the context field

src/cli/utils/runCommand.ts:132-143 vs docs/commands.md:234-243

The implementation always emits a context field in JSON errors:

const jsonError: Record<string, unknown> = {
  error: true,
  message: ...,
  context: errorContext,  // always included
};

But the docs example shows:

{
  "error": true,
  "code": "API_ERROR",
  "message": "...",
  "hints": [...]
}

No context. Consumers who parse the error schema by example will be surprised. Either document the context field or omit it when it's empty. Fix this →


Issue: create.ts — error message says "Non-interactive mode" in interactive mode

src/cli/commands/project/create.ts:53-54

if (opts.path && !(opts.name ?? name)) {
  command.error("Non-interactive mode requires all flags: --name, --path");
}

This check fires even in interactive mode (when a user provides --path but omits --name). The error message referencing "Non-interactive mode" will confuse interactive users. Suggest a cleaner message like "--path requires --name". Fix this →


Design Notes (non-blocking)

stdout-patching acknowledged as hacky (see your own review comment on runCommand.ts:72)
The process.stdout.write patch is effective but brittle — it will break if clack ever uses a different output path (e.g., direct fd writes). A proper log abstraction layer would be safer long-term. The current approach works, but flagging it as tech debt would be valuable (// TODO: replace with a proper log-suppression layer).

JSON mode with --json but no data field on deploy/link/create/eject
These commands fall back to { "message": outroMessage }. That's functional per the spec, but the primary use case for --json on deploy is CI pipelines that want structured output (exit code, deployed URL, resource counts, etc.). Consider at minimum returning { success: true } or a richer object — the fallback path exists but limits utility. You mentioned this is intentional for now ("only commands that need machine-readable output add a data field"), which is fine.

Tests only cover logs for JSON mode
The JSON mode tests for logs are comprehensive and well-structured. But there are no tests for deploy, link, create, or eject in JSON mode — particularly for the new preAction validation hooks. The eject bug above is exactly the kind of thing a test would catch.

savedStdoutWrite! non-null assertion (runCommand.ts:122)
Logically safe (the if (json) block at line 75 ensures it's set before line 118's if (json) branch), but a minor code smell. Could restructure as:

if (json && savedStdoutWrite) {
  savedStdoutWrite(`${JSON.stringify(output, null, 2)}\n`);
}

Docs vs Implementation: Alignment ✅

The docs/commands.md update is accurate and comprehensive for everything it describes. The isJsonMode field, preAction guard pattern, data field contract, fallback behavior, and error structure are all correctly documented. The "Interactive commands" section (lines 245–265) maps directly to the validateNonInteractiveMode pattern used in deploy.ts, eject.ts, and site/deploy.ts. The only gap is the context field in the error format (noted above).


Summary: One real bug (eject.ts unguarded confirm), one docs gap (error context field), and a few minor code quality issues. The core architecture is clean and consistent with the existing codebase conventions.

@kfirstri kfirstri marked this pull request as draft March 1, 2026 16:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Backlog

Development

Successfully merging this pull request may close these issues.

1 participant