Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"dependencies": {
"@clack/prompts": "^0.9.1",
"@insforge/shared-schemas": "^1.1.58",
"@toon-format/toon": "^4.1.0",
"archiver": "^7.0.1",
"cli-table3": "^0.6.5",
"commander": "^13.1.0",
Expand Down
9 changes: 8 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { Command } from 'commander';
import * as clack from '@clack/prompts';
import * as prompts from './lib/prompts.js';
import { getCredentials, getProjectConfig } from './lib/config.js';
import { outputJson } from './lib/output.js';
import { outputJson, setToonMode } from './lib/output.js';
import { registerLoginCommand } from './commands/login.js';
import { registerLogoutCommand } from './commands/logout.js';
import { registerWhoamiCommand } from './commands/whoami.js';
Expand Down Expand Up @@ -116,6 +116,7 @@ program
// Global options
program
.option('--json', 'Output in JSON format')
.option('--toon', 'Output in TOON format (token-optimized notation, see toonformat.dev)')

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1: Using --toon alone can produce an empty, human-readable, or raw response instead of TOON. The new flag is parsed by the preAction hook, but command handlers still branch only on getRootOpts(cmd).json: whoami --toon takes its outputInfo branch and both messages are then suppressed, while commands such as functions invoke and metadata write directly with console.log and bypass the TOON renderer. This makes the advertised global format inconsistent unless callers already know to add --json; treating TOON as an effective structured-output mode (or explicitly rejecting/documenting it without --json) would avoid these silent/mixed responses.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/index.ts, line 119:

<comment>Using `--toon` alone can produce an empty, human-readable, or raw response instead of TOON. The new flag is parsed by the preAction hook, but command handlers still branch only on `getRootOpts(cmd).json`: `whoami --toon` takes its `outputInfo` branch and both messages are then suppressed, while commands such as `functions invoke` and `metadata` write directly with `console.log` and bypass the TOON renderer. This makes the advertised global format inconsistent unless callers already know to add `--json`; treating TOON as an effective structured-output mode (or explicitly rejecting/documenting it without `--json`) would avoid these silent/mixed responses.</comment>

<file context>
@@ -116,6 +116,7 @@ program
 // Global options
 program
   .option('--json', 'Output in JSON format')
+  .option('--toon', 'Output in TOON format (token-optimized notation, see toonformat.dev)')
   .option('--forger', 'Play the Forger animation (root command only) and return to the interactive menu')
   .option('--api-url <url>', 'Override Platform API URL')
</file context>

.option('--forger', 'Play the Forger animation (root command only) and return to the interactive menu')
.option('--api-url <url>', 'Override Platform API URL')
.option('-y, --yes', 'Skip confirmation prompts')
Expand Down Expand Up @@ -153,6 +154,12 @@ program.hook('preAction', async (_thisCommand, actionCommand) => {
// (Claude Code, Cursor, scripts, CI, humans) automatically.
program.hook('preAction', guardHook);

// TOON output format: set module-level flag before any action runs
program.hook('preAction', (thisCommand: Command, actionCommand: Command) => {
const opts = actionCommand.optsWithGlobals() as { toon?: boolean };
setToonMode(!!opts.toon);
});

// Top-level commands
registerLoginCommand(program);
registerLogoutCommand(program);
Expand Down
2 changes: 1 addition & 1 deletion src/lib/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ export function getRootOpts(cmd: Command): { json: boolean; apiUrl?: string; yes
}
const opts = root.opts();
return {
json: opts.json ?? false,
json: opts.json ?? opts.toon ?? false,
apiUrl: opts.apiUrl,
yes: opts.yes ?? false,
};
Expand Down
78 changes: 78 additions & 0 deletions src/lib/output.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,28 @@
import Table from 'cli-table3';
import { encode } from '@toon-format/toon';

// Module-level state set by preAction hook
let toonMode = false;

export function setToonMode(enabled: boolean): void {
toonMode = enabled;
}

// ── Public output functions ──────────────────────────────────────────────

export function outputJson(data: unknown): void {
if (toonMode) {
console.log(encode(data));
return;
}
console.log(JSON.stringify(data, null, 2));
}

export function outputTable(headers: string[], rows: string[][]): void {
if (toonMode) {
outputToon(headers, rows);
return;
}
const table = new Table({
head: headers,
style: { head: ['cyan'] },
Expand All @@ -16,9 +34,69 @@ export function outputTable(headers: string[], rows: string[][]): void {
}

export function outputSuccess(message: string): void {
if (toonMode) {
// Emit structured TOON instead of suppressing
console.log(encode({ type: 'success', message }));
Comment thread
greptile-apps[bot] marked this conversation as resolved.
Outdated
return;
}
console.log(`✓ ${message}`);
}

export function outputInfo(message: string): void {
if (toonMode) {
// Emit structured TOON instead of suppressing
console.log(encode({ type: 'info', message }));
return;
}
console.log(message);
}

// ── TOON output implementations ──────────────────────────────────────────

function toonEscapeKey(k: string): string {
if (/^[A-Za-z_][A-Za-z0-9_.]*$/.test(k)) return k;
return '"' + k
.replace(/\\/g, '\\\\')
.replace(/"/g, '\\"')
.replace(/\n/g, '\\n')
.replace(/\r/g, '\\r')
.replace(/\t/g, '\\t') + '"';
}

function toonEscapeValue(v: unknown): string {
if (v === null || v === undefined || (typeof v === 'number' && !isFinite(v))) return 'null';
if (typeof v === 'boolean') return v ? 'true' : 'false';
if (typeof v === 'number') return String(v);
const s = String(v);
const needsQuoting =
s === '' ||
s === 'true' || s === 'false' || s === 'null' ||
/^[+-]?[0-9]+(\.[0-9]+)?([eE][+-]?[0-9]+)?$/.test(s) ||
/^[ \t]|[ \t]$/.test(s) ||
/^[-#]/.test(s) ||
/[:,\\"{}\\[\\]]/.test(s) ||
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
Outdated
// eslint-disable-next-line no-control-regex
/[\x00-\x1f]/.test(s);
if (!needsQuoting) return s;
return '"' + s
.replace(/\\/g, '\\\\')
.replace(/"/g, '\\"')
.replace(/\n/g, '\\n')
.replace(/\r/g, '\\r')
.replace(/\t/g, '\\t')
// eslint-disable-next-line no-control-regex
.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f]/g, c => '\\u' + c.charCodeAt(0).toString(16).padStart(4, '0')) + '"';
}

function outputToon(headers: string[], rows: string[][]): void {
if (headers && rows && rows.length > 0) {
const keyStr = headers.map(toonEscapeKey).join(',');
let out = `[${rows.length}]{${keyStr}}:`;
for (const row of rows) {
out += '\n ' + row.map(toonEscapeValue).join(',');
}
console.log(out);
} else if (headers && headers.length > 0) {
console.log(`[0]{${headers.map(toonEscapeKey).join(',')}}:`);
}
}