Skip to content

feat(cli): -o json now works on mutation commands (create/update/delete/...)#37

Merged
joalves merged 1 commit into
mainfrom
cli/json-output-for-mutations
May 20, 2026
Merged

feat(cli): -o json now works on mutation commands (create/update/delete/...)#37
joalves merged 1 commit into
mainfrom
cli/json-output-for-mutations

Conversation

@joalves
Copy link
Copy Markdown
Collaborator

@joalves joalves commented May 20, 2026

Summary

Closes the gap reported in PR #36 follow-up: mutation commands (create / update / delete / archive / activate / follow / grant / dismiss / …) used to print a plain console.log(chalk.green('✓ ...')) and ignore the global -o, --output flag, so abs metric create … -o json returned the green checkmark instead of JSON.

Adds a shared printResult(globalOptions, { message, id?, raw? }) helper in api-helper.ts that all mutation sites now route through. Behavior by output format:

Output Behavior
table / rendered (default) chalk.green(message) on stdout — unchanged interactive UX
json / yaml / markdown / plain / vertical / template structured { success, message, id? } via printFormatted — full API response when --raw
ids bare id on stdout
Piped without explicit -o bare id on stdout for chaining + green confirmation on stderr

~30 command files migrated. A few commands intentionally keep their multi-line interactive output (api key create showing the new key, reset-password, experiment create showing Name/Type, schedule create showing scheduled at) and only fall through to printResult when a structured -o is requested.

Out of scope (deferred to follow-up — they need a different design like NDJSON or array results):

  • experiments bulk (per-row updates)
  • experiments export (download progress)
  • experiments refresh-fields / estimate-participants (data display)
  • experiments generate-template (file write success)
  • interactive flows: auth, setup, doctor

Test plan

  • printResult helper has 11 dedicated unit tests covering every route (table, json, yaml, markdown, plain, vertical, template, ids, piped w/ id, piped w/o id, piped w/ explicit json, --raw with and without raw data).
  • Two existing integration tests updated where they previously asserted printFormatted on the success path (storageconfigs, actiondialogfields).
  • npm test — 2387 passing, 4 skipped.
  • npm run build — clean.
  • npm run lint — clean.
  • Manual smoke: a representative create command with -o json returns {"success":true,"message":"✓ …","id":N}, with --raw -o json returns the full API response, default emits the green checkmark.

Summary by CodeRabbit

Release Notes

  • New Features

    • Standardised output formatting across CLI commands with support for multiple formats (JSON, YAML, Markdown, Plain, Vertical, Template).
    • Enhanced handling of piped output and terminal detection for improved scripting compatibility.
  • Improvements

    • Consistent success messages and result formatting for create, update, and delete operations across all commands.

Review Change Stack

Mutation commands (create/update/delete/archive/activate/follow/...) used
to emit a plain `console.log(chalk.green('✓ ...'))` and ignore the global
`-o, --output` flag entirely, so `abs metric create … -o json` returned
the green message instead of JSON.

Centralize the mutation success path on a new printResult helper in
api-helper.ts. Routing by globalOptions:

  - structured output (json/yaml/markdown/plain/vertical/template):
    emits { success, message, id? }; with --raw it emits the full API
    response instead.
  - --output ids: bare id to stdout.
  - stdout piped without explicit --output: bare id to stdout for
    pipelining, the green confirmation to stderr so the user still sees
    what happened.
  - interactive default (table/rendered): chalk.green(message) on stdout
    — unchanged.

~30 command files migrated to printResult. A few commands intentionally
keep their existing interactive multi-line output (api key create showing
the new key, reset-password, experiment create showing Name/Type) and
fall through to printResult only when a structured -o is requested.
Bulk/stream/progress flows (experiments bulk/export/refresh-fields) are
left for a follow-up — they need a different design (NDJSON or array).

Helper has 11 dedicated tests covering every route, plus two integration
test files updated where the assertions previously expected
printFormatted on the success path.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 20, 2026

Walkthrough

This release standardises CLI command output across the entire codebase by introducing a new printResult helper that routes mutation confirmations through configurable formatters. The helper centralises output logic based on format mode (table, JSON, YAML, etc.), stdout piping state, and availability of raw response data. Approximately 50 command modules are refactored to use this helper instead of direct chalk-based console logging, removing import dependencies on chalk and consolidating success message patterns across create, update, archive, and delete operations. Tests are updated to verify console message emission rather than printFormatted calls. Package version is bumped to 1.7.0.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • absmartly/cli-ts#23: Extends GlobalOptions with flags (--show-request, --show-response, --curl) that configure the output helper introduced in this PR.
  • absmartly/cli-ts#2: Initial CLI command structure that this refactoring standardises output handling for.

Poem

A rabbit hops through files with care,
Replacing chalk with prints so fair,
Each command speaks with structured grace,
Output unified in one embrace! 🐰✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main change: adding support for the -o json flag to mutation commands (create/update/delete), which is the primary feature being implemented across the entire changeset.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch cli/json-output-for-mutations

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
src/lib/utils/api-helper.test.ts (1)

395-538: ⚡ Quick win

Consider adding test coverage for colorDisabled behaviour.

The test suite thoroughly verifies routing logic across formats and piping states. However, no test verifies that globalOptions.colorDisabled suppresses chalk colours in the output. Once the implementation respects colorDisabled (per the comment on api-helper.ts), adding a test case would ensure this user preference is honoured.

Example test case
it('respects colorDisabled and prints uncoloured message', () => {
  printResult(
    { output: 'table', outputExplicit: false, colorDisabled: true },
    { message: '✓ Metric 42 updated', id: 42 }
  );

  expect(consoleSpy).toHaveBeenCalledWith('✓ Metric 42 updated');
  // Verify no ANSI colour codes present in output
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/utils/api-helper.test.ts` around lines 395 - 538, Add a unit test for
printResult to verify globalOptions.colorDisabled suppresses chalk colours:
within the existing printResult describe block, mock console.log/console.error
and TTY as in beforeEach, call printResult with options including colorDisabled:
true (and output: 'table' / outputExplicit as appropriate) and a message
containing coloured characters, then assert console.log was called with the
plain string and that the output contains no ANSI escape sequences (e.g. assert
the printed string does not match /\u001b\[/). Reference the printResult
function and the globalOptions.colorDisabled flag when adding this test.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/commands/envs/index.ts`:
- Around line 54-57: The code currently asserts result.data as Record<string,
unknown> and directly reads id (newId) without guaranteeing it exists; update
the create command handlers (where CommandResult<unknown> is used) to ensure
type-safety by either tightening the CommandResult generic to include { id?:
unknown } or by extracting and validating the id before use: retrieve const data
= result.data as Record<string, unknown> (or a narrower type), check data?.id
(or typeof data.id !== 'undefined') and build the printResult call from that—use
a fallback message when id is missing and pass raw: result.data; apply the same
pattern in functions that call validateEntityResponse, printResult, and any
create X handlers to avoid unsafe assertions.

In `@src/lib/utils/api-helper.ts`:
- Around line 227-258: The printResult function currently calls chalk.green(...)
unconditionally; update it to respect globalOptions.colorDisabled by computing a
formatted message once (e.g., const formattedMessage =
globalOptions.colorDisabled ? args.message : chalk.green(args.message)) and use
that for both the stdout console.log and the stderr console.error paths (the
locations referencing chalk.green in printResult and in the isStdoutPiped
branch). Ensure the check uses the existing globalOptions.colorDisabled flag and
leave all other branching logic (shouldOutputIdsOnly, printFormatted,
isStdoutPiped) unchanged.

---

Nitpick comments:
In `@src/lib/utils/api-helper.test.ts`:
- Around line 395-538: Add a unit test for printResult to verify
globalOptions.colorDisabled suppresses chalk colours: within the existing
printResult describe block, mock console.log/console.error and TTY as in
beforeEach, call printResult with options including colorDisabled: true (and
output: 'table' / outputExplicit as appropriate) and a message containing
coloured characters, then assert console.log was called with the plain string
and that the output contains no ANSI escape sequences (e.g. assert the printed
string does not match /\u001b\[/). Reference the printResult function and the
globalOptions.colorDisabled flag when adding this test.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a5f32358-5766-4cc5-aa1f-21ad8ab71d90

📥 Commits

Reviewing files that changed from the base of the PR and between 5f73cec and 8ae1695.

📒 Files selected for processing (59)
  • package.json
  • src/commands/actiondialogfields/actiondialogfields.test.ts
  • src/commands/actiondialogfields/index.ts
  • src/commands/apikeys/index.ts
  • src/commands/apps/index.ts
  • src/commands/assetroles/index.ts
  • src/commands/cors/index.ts
  • src/commands/customfields/index.ts
  • src/commands/customsections/index.ts
  • src/commands/datasources/index.ts
  • src/commands/envs/index.ts
  • src/commands/events/index.ts
  • src/commands/experiments/access.ts
  • src/commands/experiments/activity.ts
  • src/commands/experiments/alerts.ts
  • src/commands/experiments/annotations.ts
  • src/commands/experiments/archive.ts
  • src/commands/experiments/create.ts
  • src/commands/experiments/custom-fields.ts
  • src/commands/experiments/development.ts
  • src/commands/experiments/follow.ts
  • src/commands/experiments/full-on.ts
  • src/commands/experiments/metrics.ts
  • src/commands/experiments/recommendations.ts
  • src/commands/experiments/request-update.ts
  • src/commands/experiments/restart.ts
  • src/commands/experiments/schedule.ts
  • src/commands/experiments/start.ts
  • src/commands/experiments/stop.ts
  • src/commands/experiments/update.ts
  • src/commands/exportconfigs/index.ts
  • src/commands/favorites/index.ts
  • src/commands/goals/access.ts
  • src/commands/goals/follow.ts
  • src/commands/goals/index.ts
  • src/commands/goaltags/index.ts
  • src/commands/metriccategories/index.ts
  • src/commands/metrics/access.ts
  • src/commands/metrics/follow.ts
  • src/commands/metrics/index.ts
  • src/commands/metrics/review.ts
  • src/commands/metrictags/index.ts
  • src/commands/notifications/index.ts
  • src/commands/platformconfig/index.ts
  • src/commands/roles/index.ts
  • src/commands/segments/index.ts
  • src/commands/storageconfigs/index.ts
  • src/commands/storageconfigs/storageconfigs.test.ts
  • src/commands/tags/index.ts
  • src/commands/teams/index.ts
  • src/commands/teams/members.ts
  • src/commands/units/index.ts
  • src/commands/updateschedules/index.ts
  • src/commands/users/api-keys.ts
  • src/commands/users/index.ts
  • src/commands/users/reset-password.ts
  • src/commands/webhooks/index.ts
  • src/lib/utils/api-helper.test.ts
  • src/lib/utils/api-helper.ts

Comment on lines +54 to +57
const newId = (result.data as Record<string, unknown>).id;
printResult(globalOptions, {
message: `✓ Environment created with ID: ${newId}`,
id: newId,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Verify unguarded id dereferences introduced in command handlers
rg -nP --type=ts -C2 '\(result\.data as Record<string, unknown>\)\.id' src/commands

# Inspect related create function declarations/typing contracts
rg -nP --type=ts -C4 'export\s+(async\s+)?(function|const)\s+create(Env|Role|Segment|Unit)\b' src/core

# Inspect call sites/contracts for create API methods to confirm whether body can be empty
rg -nP --type=ts -C4 '\b(createEnv|createRole|createSegment|createUnit)\b' src

Repository: absmartly/cli-ts

Length of output: 31382


🏁 Script executed:

# Check validateEntityResponse implementation to understand its contract
rg -nP --type=ts -A10 'validateEntityResponse\s*<' src/api-client/api-client.ts | head -50

Repository: absmartly/cli-ts

Length of output: 2268


🏁 Script executed:

# Check if there are any guards or null checks around result.data usage in commands
rg -nP --type=ts -B2 -A5 'result\.data.*\.id' src/commands --max-count=3

Repository: absmartly/cli-ts

Length of output: 11724


🏁 Script executed:

# Examine the return type of a create function in detail
sed -n '35,45p' src/core/envs/envs.ts

Repository: absmartly/cli-ts

Length of output: 354


🏁 Script executed:

# Check what the CommandResult type looks like
rg -nP --type=ts 'interface CommandResult' src

Repository: absmartly/cli-ts

Length of output: 116


🏁 Script executed:

# Get the full validateEntityResponse implementation
sed -n '147,180p' src/api-client/api-client.ts

Repository: absmartly/cli-ts

Length of output: 1323


🏁 Script executed:

# Check the CommandResult interface definition
rg -nP --type=ts -A3 'export interface CommandResult' src

Repository: absmartly/cli-ts

Length of output: 283


Improve type safety for API response entity extraction.

On line 54, the code assumes result.data has an id field but relies on a type assertion that provides no runtime protection. Although validateEntityResponse ensures the entity object exists, the CommandResult<unknown> type does not guarantee the presence of an id field. Use proper typing to make the contract explicit, or validate the presence of id before use.

Suggested approach

Strengthen the type by either:

  1. Explicitly type the CommandResult: Change CommandResult<unknown> to CommandResult<{ id: unknown }> in the create functions, or
  2. Add optional chaining: Use data?.id to handle cases where the field may not exist, or
  3. Validate before use: Check that id exists before constructing the success message.

Example with optional chaining:

const data = result.data as Record<string, unknown>;
const newId = data?.id;
printResult(globalOptions, {
  message: newId ? `✓ Environment created with ID: ${newId}` : `✓ Environment created`,
  id: newId,
  raw: result.data,
});

This pattern appears in multiple command handlers (envs, roles, segments, units, webhooks, custom fields, asset roles, apps). Consider applying the fix consistently across all create commands.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/commands/envs/index.ts` around lines 54 - 57, The code currently asserts
result.data as Record<string, unknown> and directly reads id (newId) without
guaranteeing it exists; update the create command handlers (where
CommandResult<unknown> is used) to ensure type-safety by either tightening the
CommandResult generic to include { id?: unknown } or by extracting and
validating the id before use: retrieve const data = result.data as
Record<string, unknown> (or a narrower type), check data?.id (or typeof data.id
!== 'undefined') and build the printResult call from that—use a fallback message
when id is missing and pass raw: result.data; apply the same pattern in
functions that call validateEntityResponse, printResult, and any create X
handlers to avoid unsafe assertions.

Comment on lines +227 to +258
export function printResult(
globalOptions: GlobalOptions,
args: { message: string; id?: unknown; raw?: unknown }
): void {
const format = (globalOptions.output ?? 'table') as OutputFormat;

if (globalOptions.outputExplicit && STRUCTURED_FORMATS.has(format)) {
const payload =
globalOptions.raw && args.raw !== undefined
? args.raw
: {
success: true,
message: args.message,
...(args.id !== undefined ? { id: args.id } : {}),
};
printFormatted(payload, globalOptions);
return;
}

if (shouldOutputIdsOnly(globalOptions)) {
if (args.id !== undefined) console.log(args.id);
// Piped (no explicit format) → also surface the confirmation on
// stderr so users running `cmd | xargs ...` still see success.
// Explicit `-o ids` stays quiet to keep the stream machine-clean.
if (isStdoutPiped() && !globalOptions.outputExplicit) {
console.error(chalk.green(args.message));
}
return;
}

console.log(chalk.green(args.message));
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Respect globalOptions.colorDisabled before applying chalk colours.

Lines 252 and 257 use chalk.green() unconditionally, ignoring the user's explicit --no-color preference captured in globalOptions.colorDisabled. When users disable colours for accessibility, logging, or other reasons, the output should honour that choice.

🎨 Proposed fix
   if (shouldOutputIdsOnly(globalOptions)) {
     if (args.id !== undefined) console.log(args.id);
     // Piped (no explicit format) → also surface the confirmation on
     // stderr so users running `cmd | xargs ...` still see success.
     // Explicit `-o ids` stays quiet to keep the stream machine-clean.
     if (isStdoutPiped() && !globalOptions.outputExplicit) {
-      console.error(chalk.green(args.message));
+      const message = globalOptions.colorDisabled ? args.message : chalk.green(args.message);
+      console.error(message);
     }
     return;
   }
 
-  console.log(chalk.green(args.message));
+  const message = globalOptions.colorDisabled ? args.message : chalk.green(args.message);
+  console.log(message);
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/utils/api-helper.ts` around lines 227 - 258, The printResult function
currently calls chalk.green(...) unconditionally; update it to respect
globalOptions.colorDisabled by computing a formatted message once (e.g., const
formattedMessage = globalOptions.colorDisabled ? args.message :
chalk.green(args.message)) and use that for both the stdout console.log and the
stderr console.error paths (the locations referencing chalk.green in printResult
and in the isStdoutPiped branch). Ensure the check uses the existing
globalOptions.colorDisabled flag and leave all other branching logic
(shouldOutputIdsOnly, printFormatted, isStdoutPiped) unchanged.

@joalves joalves added this pull request to the merge queue May 20, 2026
Merged via the queue into main with commit 690880f May 20, 2026
5 checks passed
@joalves joalves deleted the cli/json-output-for-mutations branch May 20, 2026 21:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant