-
Notifications
You must be signed in to change notification settings - Fork 1.8k
fix: suppress UI spinners and telemetry notices when --json is used #742
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| schema: spec-driven | ||
| created: 2026-02-22 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| # Design: Clean JSON Output for OpenSpec CLI | ||
|
|
||
| ## Goals | ||
|
|
||
| Ensure that when the `--json` flag is provided to any OpenSpec CLI command, the `stdout` stream contains ONLY valid, parseable JSON. | ||
|
|
||
| - **Suppress UI Spinners**: Spinners (`ora`) must not start when `json` output is requested. | ||
| - **Silence Telemetry Notices**: The first-run notice must be suppressed during JSON output. | ||
| - **Cross-Platform Silence**: Both `stdout` and `stderr` should be managed to avoid pollution, though primarily focusing on `stdout` for JSON parsing. | ||
|
|
||
| ## Design Approach | ||
|
|
||
| ### 1. Global JSON Detection | ||
|
|
||
| The CLI entry point (`src/cli/index.ts`) will detect the `--json` flag globally using the `commander` hook. | ||
|
|
||
| - **Hook**: `program.hook('preAction', ...)` | ||
| - **Logic**: Check if the `actionCommand` has the `json` option set. | ||
|
|
||
| ### 2. Telemetry Silencing | ||
|
|
||
| Update the telemetry module to support a "silent" mode. | ||
|
|
||
| - **Update**: `maybeShowTelemetryNotice()` will check for a global silent state or accept an argument. | ||
| - **Implementation**: If silenced, `console.log()` for the notice will be skipped. | ||
|
|
||
| ### 3. Command Logic Pattern | ||
|
|
||
| Every command using `ora` for UI feedback will adopt a standard conditional start pattern: | ||
|
|
||
| ```typescript | ||
| const spinner = ora('Doing work...'); | ||
| if (!options.json) { | ||
| spinner.start(); | ||
| } | ||
|
|
||
| try { | ||
| // ... logic ... | ||
| } finally { | ||
| if (!options.json) spinner.stop(); | ||
| } | ||
| ``` | ||
|
|
||
| ### 4. Target Files | ||
|
|
||
| | Component | File Path | Action | | ||
| | --- | --- | --- | | ||
| | CLI Entry | `src/cli/index.ts` | Pass `json` status to telemetry notice. | | ||
| | Telemetry | `src/telemetry/index.ts` | Add silence support to `maybeShowTelemetryNotice`. | | ||
| | Status Cmd | `src/commands/workflow/status.ts` | Conditional spinner. | | ||
| | Instructions Cmd | `src/commands/workflow/instructions.ts` | Conditional spinner for both `instructions` and `applyInstructions`. | | ||
| | Templates Cmd | `src/commands/workflow/templates.ts` | Conditional spinner. | | ||
| | New Change Cmd | `src/commands/workflow/new-change.ts` | Conditional spinner. | | ||
|
|
||
| ## Risks / Trade-offs | ||
|
|
||
| - **Error Visibility**: Errors should still go to `stderr`. If an error occurs during JSON generation, it might still produce a partial JSON or mixed output if not handled carefully. We will use `try/catch` and ensure `ora().fail()` is only called in non-JSON mode or use `console.error` for raw errors. | ||
| - **Breaking Change**: None identified, as this only affects programmatic consumers (who want clean JSON anyway). |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| ## Why | ||
|
|
||
| AI agents and automated tools rely on the `--json` flag for programmatic interaction with the OpenSpec CLI. Currently, the CLI prints UI elements like spinners and telemetry notices to `stdout` even when the `--json` flag is active. This pollution breaks standard JSON parsers and forces manual cleanup of the output stream. | ||
|
|
||
| ## What Changes | ||
|
|
||
| - Modify workflow commands to suppress the `ora` spinner when `options.json` is provided. | ||
| - Update the global CLI hook and telemetry module to silence the first-run notice when outputting machine-readable data. | ||
| - Ensure that only valid JSON is printed to `stdout` on successful command execution, while maintaining `stderr` for errors. | ||
|
|
||
| ## Capabilities | ||
|
|
||
| ### New Capabilities | ||
|
|
||
| - `machine-readable-output`: Ensures all CLI commands providing a `--json` flag produce clean, parseable stdout without terminal visual effects or side-channel messages. | ||
|
|
||
| ### Modified Capabilities | ||
|
|
||
| - None. | ||
|
|
||
| ## Impact | ||
|
|
||
| - **Core CLI**: `src/cli/index.ts` will need to pass silence flags down to hooks. | ||
| - **Workflow Commands**: `src/commands/workflow/*.ts` files will need conditional spinner starts. | ||
| - **Telemetry**: `src/telemetry/index.ts` needs a silence mechanism for the usage notice. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| # Capability: Machine Readable Output | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same hyphenation nit as the canonical spec. Title should be 🧰 Tools🪛 LanguageTool[grammar] ~1-~1: Use a hyphen to join words. (QB_NEW_EN_HYPHEN) 🤖 Prompt for AI Agents
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This archived change spec looks like a full future-state document. Current conventions for change specs use delta sections like |
||
|
|
||
| ## Purpose | ||
|
|
||
| Allow automated tools and AI agents to consume OpenSpec CLI output without stream corruption from UI elements. | ||
|
|
||
| ## Requirements | ||
|
|
||
| ### 1. Spinner Suppression | ||
| The `ora` spinner must not be started if the `--json` flag is provided. | ||
|
|
||
| #### Scenario: Status with JSON | ||
| - **Given** an OpenSpec project with active changes | ||
| - **When** running `openspec status --change my-change --json` | ||
| - **Then** the `stdout` contains only the JSON object | ||
| - **And** no spinner characters are present in the output stream | ||
|
|
||
| ### 2. Telemetry Notice Suppression | ||
| The "Note: OpenSpec collects anonymous usage stats" message must not be printed if the `--json` flag is provided. | ||
|
|
||
| #### Scenario: First run with JSON | ||
| - **Given** a new environment where the telemetry notice hasn't been shown | ||
| - **When** running `openspec list --json` | ||
| - **Then** the telemetry notice is NOT printed to stdout | ||
| - **And** only the JSON change list is printed | ||
|
|
||
| ### 3. Error Handling | ||
| Errors should be printed to `stderr` and not pollute the `stdout` JSON stream. | ||
|
|
||
| #### Scenario: Command failure with JSON | ||
| - **Given** an invalid change name | ||
| - **When** running `openspec status --change invalid --json` | ||
| - **Then** the exit code is non-zero | ||
| - **And** the error message is printed to `stderr` | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| # Tasks: Clean JSON Output | ||
|
|
||
| ## Phase 1: Global State & Telemetry | ||
|
|
||
| - [x] 1.1 Update `src/telemetry/index.ts` to allow silencing the telemetry notice via a parameter or environment check. | ||
| - [x] 1.2 Update the `preAction` hook in `src/cli/index.ts` to detect the `--json` flag on the action command and trigger telemetry silence. | ||
|
|
||
| ## Phase 2: Command UI Silence | ||
|
|
||
| - [x] 2.1 Update `src/commands/workflow/status.ts` to only start the `ora` spinner if `options.json` is false. | ||
| - [x] 2.2 Update `src/commands/workflow/instructions.ts` to only start the `ora` spinner in `instructionsCommand` and `applyInstructionsCommand` if `options.json` is false. | ||
| - [x] 2.3 Update `src/commands/workflow/templates.ts` to only start the `ora` spinner if `options.json` is false. | ||
| - [x] 2.4 Update `src/commands/workflow/new-change.ts` to only start the `ora` spinner if `options.json` is false (for metadata prep). | ||
|
|
||
| ## Phase 3: Verification | ||
|
|
||
| - [x] 3.1 Verify `openspec status --json` produces clean JSON on Windows. | ||
| - [x] 3.2 Verify `openspec instructions apply --json` produces clean JSON on Windows. | ||
| - [x] 3.3 Verify first-run telemetry notice is suppressed when using `--json`. | ||
| - [x] 3.4 (Optional) Verify cross-platform silence if environment allows. |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,34 @@ | ||||||
| # Capability: Machine Readable Output | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
"Machine Readable Output" should be "Machine-Readable Output" when used as a compound modifier (title/heading). 📝 Proposed fix-# Capability: Machine Readable Output
+# Capability: Machine-Readable Output📝 Committable suggestion
Suggested change
🧰 Tools🪛 LanguageTool[grammar] ~1-~1: Use a hyphen to join words. (QB_NEW_EN_HYPHEN) 🤖 Prompt for AI Agents |
||||||
|
|
||||||
| ## Purpose | ||||||
|
|
||||||
| Allow automated tools and AI agents to consume OpenSpec CLI output without stream corruption from UI elements. | ||||||
|
|
||||||
| ## Requirements | ||||||
|
|
||||||
| ### 1. Spinner Suppression | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we switch these numbered sections to the standard requirement format? Using headings like |
||||||
| The `ora` spinner must not be started if the `--json` flag is provided. | ||||||
|
|
||||||
| #### Scenario: Status with JSON | ||||||
| - **Given** an OpenSpec project with active changes | ||||||
| - **When** running `openspec status --change my-change --json` | ||||||
| - **Then** the `stdout` contains only the JSON object | ||||||
| - **And** no spinner characters are present in the output stream | ||||||
|
|
||||||
| ### 2. Telemetry Notice Suppression | ||||||
| The "Note: OpenSpec collects anonymous usage stats" message must not be printed if the `--json` flag is provided. | ||||||
|
|
||||||
| #### Scenario: First run with JSON | ||||||
| - **Given** a new environment where the telemetry notice hasn't been shown | ||||||
| - **When** running `openspec list --json` | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree with suppressing the notice in JSON mode. Can we make deferred notice behavior explicit here too: suppress on JSON runs, do not mark noticeSeen yet, and show it on the first later non-JSON run. That keeps JSON clean and still guarantees user disclosure. |
||||||
| - **Then** the telemetry notice is NOT printed to stdout | ||||||
| - **And** only the JSON change list is printed | ||||||
|
|
||||||
| ### 3. Error Handling | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could we define stdout behavior on failure more explicitly for JSON mode? Suggest: stdout stays empty, error text goes to stderr, and exit code is non-zero. That gives consumers a clear contract. |
||||||
| Errors should be printed to `stderr` and not pollute the `stdout` JSON stream. | ||||||
|
|
||||||
| #### Scenario: Command failure with JSON | ||||||
| - **Given** an invalid change name | ||||||
| - **When** running `openspec status --change invalid --json` | ||||||
| - **Then** the exit code is non-zero | ||||||
| - **And** the error message is printed to `stderr` | ||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -74,8 +74,12 @@ program.hook('preAction', async (thisCommand, actionCommand) => { | |
| process.env.NO_COLOR = '1'; | ||
| } | ||
|
|
||
| // Check if action command is requesting JSON output | ||
| const actionOpts = actionCommand.opts(); | ||
| const isJson = actionOpts.json === true; | ||
|
|
||
| // Show first-run telemetry notice (if not seen) | ||
| await maybeShowTelemetryNotice(); | ||
| await maybeShowTelemetryNotice({ silent: isJson }); | ||
|
Comment on lines
+77
to
+82
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Telemetry notice is permanently suppressed for users whose first run uses
Fix in // In maybeShowTelemetryNotice:
if (!options.silent) {
console.log(
'Note: OpenSpec collects anonymous usage stats. Opt out: OPENSPEC_TELEMETRY=0'
);
await updateTelemetryConfig({ noticeSeen: true }); // ← move inside this block
}
// Remove the unconditional call below🤖 Prompt for AI Agents |
||
|
|
||
| // Track command execution (use actionCommand to get the actual subcommand) | ||
| const commandPath = getCommandPath(actionCommand); | ||
|
|
@@ -497,6 +501,7 @@ newCmd | |
| .description('Create a new change directory') | ||
| .option('--description <text>', 'Description to add to README.md') | ||
| .option('--schema <name>', `Workflow schema to use (default: ${DEFAULT_SCHEMA})`) | ||
| .option('--json', 'Output as JSON (success/failure details)') | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nice addition of |
||
| .action(async (name: string, options: NewChangeOptions) => { | ||
| try { | ||
| await newChangeCommand(name, options); | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -16,6 +16,7 @@ import { validateSchemaExists } from './shared.js'; | |||||||||||||||||||||||||||||||||
| export interface NewChangeOptions { | ||||||||||||||||||||||||||||||||||
| description?: string; | ||||||||||||||||||||||||||||||||||
| schema?: string; | ||||||||||||||||||||||||||||||||||
| json?: boolean; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // ----------------------------------------------------------------------------- | ||||||||||||||||||||||||||||||||||
|
|
@@ -40,7 +41,10 @@ export async function newChangeCommand(name: string | undefined, options: NewCha | |||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| const schemaDisplay = options.schema ? ` with schema '${options.schema}'` : ''; | ||||||||||||||||||||||||||||||||||
| const spinner = ora(`Creating change '${name}'${schemaDisplay}...`).start(); | ||||||||||||||||||||||||||||||||||
| const spinner = ora(`Creating change '${name}'${schemaDisplay}...`); | ||||||||||||||||||||||||||||||||||
| if (!options.json) { | ||||||||||||||||||||||||||||||||||
| spinner.start(); | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||
| const result = await createChange(projectRoot, name, { schema: options.schema }); | ||||||||||||||||||||||||||||||||||
|
|
@@ -53,9 +57,20 @@ export async function newChangeCommand(name: string | undefined, options: NewCha | |||||||||||||||||||||||||||||||||
| await fs.writeFile(readmePath, `# ${name}\n\n${options.description}\n`, 'utf-8'); | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| spinner.succeed(`Created change '${name}' at openspec/changes/${name}/ (schema: ${result.schema})`); | ||||||||||||||||||||||||||||||||||
| if (options.json) { | ||||||||||||||||||||||||||||||||||
| console.log(JSON.stringify({ | ||||||||||||||||||||||||||||||||||
| success: true, | ||||||||||||||||||||||||||||||||||
| name, | ||||||||||||||||||||||||||||||||||
| schema: result.schema, | ||||||||||||||||||||||||||||||||||
| path: `openspec/changes/${name}/` | ||||||||||||||||||||||||||||||||||
| }, null, 2)); | ||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||
| spinner.succeed(`Created change '${name}' at openspec/changes/${name}/ (schema: ${result.schema})`); | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| } catch (error) { | ||||||||||||||||||||||||||||||||||
| spinner.fail(`Failed to create change '${name}'`); | ||||||||||||||||||||||||||||||||||
| if (!options.json) { | ||||||||||||||||||||||||||||||||||
| spinner.fail(`Failed to create change '${name}'`); | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| throw error; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
Comment on lines
70
to
75
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Error path in JSON mode doesn't emit a structured error body before rethrowing. When The cleanest fix is in the 🐛 Alternative fix (handle JSON error locally) } catch (error) {
- if (!options.json) {
- spinner.fail(`Failed to create change '${name}'`);
- }
- throw error;
+ if (options.json) {
+ console.log(JSON.stringify({ success: false, error: (error as Error).message }, null, 2));
+ process.exitCode = 1;
+ return;
+ }
+ spinner.fail(`Failed to create change '${name}'`);
+ throw error;
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -119,7 +119,7 @@ export async function trackCommand(commandName: string, version: string): Promis | |
| /** | ||
| * Show first-run telemetry notice if not already seen. | ||
| */ | ||
| export async function maybeShowTelemetryNotice(): Promise<void> { | ||
| export async function maybeShowTelemetryNotice(options: { silent?: boolean } = {}): Promise<void> { | ||
| if (!isTelemetryEnabled()) { | ||
| return; | ||
| } | ||
|
|
@@ -130,10 +130,12 @@ export async function maybeShowTelemetryNotice(): Promise<void> { | |
| return; | ||
| } | ||
|
|
||
| // Display notice | ||
| console.log( | ||
| 'Note: OpenSpec collects anonymous usage stats. Opt out: OPENSPEC_TELEMETRY=0' | ||
| ); | ||
| if (!options.silent) { | ||
| // Display notice | ||
| console.log( | ||
| 'Note: OpenSpec collects anonymous usage stats. Opt out: OPENSPEC_TELEMETRY=0' | ||
| ); | ||
| } | ||
|
|
||
| // Mark as seen | ||
| await updateTelemetryConfig({ noticeSeen: true }); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Small logic issue here: |
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice fix direction. One scope detail: this looks like a modification of existing capabilities, not a brand new one. We are changing telemetry notice behavior and workflow command output behavior. Could we move this to Modified Capabilities and list telemetry plus cli-artifact-workflow?