From 1b4d19beaf00bf7a632e9a91b16555ab662313d2 Mon Sep 17 00:00:00 2001 From: Ilyas Salikhov Date: Mon, 31 Aug 2026 19:02:44 +0300 Subject: [PATCH] fix(reference): serialize CLI defaults safely --- packages/cli/src/utils/commands2md.ts | 23 ++++++++-- packages/cli/tests/utils/commands2md.test.ts | 46 ++++++++++++++++++++ reference/manifest.json | 2 +- reference/sdk/cli/sandbox.md | 6 +-- scripts/test-reference-contract.py | 5 +++ 5 files changed, 75 insertions(+), 7 deletions(-) create mode 100644 packages/cli/tests/utils/commands2md.test.ts diff --git a/packages/cli/src/utils/commands2md.ts b/packages/cli/src/utils/commands2md.ts index 98c3b445..9ec1f9d9 100644 --- a/packages/cli/src/utils/commands2md.ts +++ b/packages/cli/src/utils/commands2md.ts @@ -9,11 +9,28 @@ function escapeHtml(value: string): string { .replaceAll('>', '>') } +function renderDefaultValue(value: unknown): string | undefined { + if (value === undefined || typeof value === 'function') return undefined + if (typeof value === 'symbol') return undefined + if (typeof value !== 'object' || value === null) return String(value) + + const serialized = JSON.stringify(value, (_key, item: unknown) => { + if (item && typeof item === 'object' && !Array.isArray(item)) { + return Object.fromEntries( + Object.entries(item).sort(([left], [right]) => + left.localeCompare(right) + ) + ) + } + return item + }) + return serialized === '{}' ? undefined : serialized +} + function renderOption(option: Option): string { + const defaultValue = renderDefaultValue(option.defaultValue) const fallback = - option.defaultValue === undefined - ? '' - : ` Default: \`${String(option.defaultValue)}\`.` + defaultValue === undefined ? '' : ` Default: \`${defaultValue}\`.` return `- \`${escapeHtml(option.flags)}\`: ${escapeHtml(option.description || 'No description.')}${fallback}` } diff --git a/packages/cli/tests/utils/commands2md.test.ts b/packages/cli/tests/utils/commands2md.test.ts new file mode 100644 index 00000000..af14c79a --- /dev/null +++ b/packages/cli/tests/utils/commands2md.test.ts @@ -0,0 +1,46 @@ +import { Command, Option } from 'commander' +import { readFileSync, rmSync } from 'node:fs' +import { tmpdir } from 'node:os' +import path from 'node:path' +import { afterEach, describe, expect, test } from 'vitest' +import { commands2md } from '../../src/utils/commands2md' + +const outputDir = path.join( + tmpdir(), + `agentbox-cli-reference-${process.pid}-${Date.now()}` +) + +afterEach(() => rmSync(outputDir, { recursive: true, force: true })) + +describe('commands2md defaults', () => { + test('omits empty objects and serializes structured defaults', () => { + const configure = new Command('configure') + const sandbox = new Command('sandbox') + .addOption( + new Option('-e, --env ', 'environment variables').default({}) + ) + .addOption( + new Option('--labels ', 'labels').default({ zebra: 2, alpha: 1 }) + ) + .addOption(new Option('--ports ', 'ports').default([8080, 8081])) + .addOption(new Option('--format ', 'format').default('pretty')) + const template = new Command('template') + + commands2md([configure, sandbox, template], outputDir) + + const markdown = readFileSync(path.join(outputDir, 'sandbox.md'), 'utf8') + expect(markdown).not.toContain('[object Object]') + expect(markdown).toContain( + '- `--labels <JSON>`: labels Default: `{"alpha":1,"zebra":2}`.' + ) + expect(markdown).toContain( + '- `--ports <PORT>`: ports Default: `[8080,8081]`.' + ) + expect(markdown).toContain( + '- `--format <FORMAT>`: format Default: `pretty`.' + ) + expect(markdown).toContain( + '- `-e, --env <KEY=VALUE>`: environment variables\n' + ) + }) +}) diff --git a/reference/manifest.json b/reference/manifest.json index 39a07f32..798c3f9d 100644 --- a/reference/manifest.json +++ b/reference/manifest.json @@ -42,7 +42,7 @@ "openapi/markdown/uploadFile.md": "acf5f7029d2b4de2ce96b50bb24f5e4495c188dfed878851ffe2f91e5b7b24d3", "openapi/operations.json": "1593bd80b73d87117a5ebf8211d0ba014e0170c84fca5fe9f1a82ec457a6b575", "sdk/cli/auth.md": "0b2c75262b0c0670c7bc0f0f3cbed65fbe4a16d6fe4437789a5685b8bf2174a1", - "sdk/cli/sandbox.md": "0f59649197a6f42282a8b789c4384e8e41d2e525de414a22dffe073f0fca4d64", + "sdk/cli/sandbox.md": "16b64c5a4e932eca2452e400e0faf154eba1d3c7373513aa7cd9744e02908313", "sdk/cli/template.md": "f270b22b9ee10a9b954a14f24e04c5449b4d5823bb28002ffde3ed682594cfc5", "sdk/javascript/code-interpreter/README.md": "a787206eb86f81fc306b373ce20bc6c91464b0ab3c3ec7650eccbb6be5f676c5", "sdk/javascript/code-interpreter/classes/Sandbox.md": "0c380df735431ad98397ec32920159e4c6ba103e600603551ded77b2f472839a", diff --git a/reference/sdk/cli/sandbox.md b/reference/sdk/cli/sandbox.md index dd034105..90b268de 100644 --- a/reference/sdk/cli/sandbox.md +++ b/reference/sdk/cli/sandbox.md @@ -24,7 +24,7 @@ agentbox sandbox connect [options] - `-u, --user <user>`: user to start the terminal session as - `-c, --cwd <dir>`: working directory for the terminal session -- `-e, --env <KEY=VALUE>`: set environment variable for the terminal session (repeatable) Default: `[object Object]`. +- `-e, --env <KEY=VALUE>`: set environment variable for the terminal session (repeatable) ## agentbox sandbox create @@ -44,7 +44,7 @@ agentbox sandbox create [options] [template] - `--timeout <seconds>`: sandbox timeout in seconds - `-u, --user <user>`: user to start the terminal session as - `-c, --cwd <dir>`: working directory for the terminal session -- `-e, --env <KEY=VALUE>`: set environment variable for the terminal session (repeatable) Default: `[object Object]`. +- `-e, --env <KEY=VALUE>`: set environment variable for the terminal session (repeatable) ## agentbox sandbox exec @@ -61,7 +61,7 @@ agentbox sandbox exec [options] - `-b, --background`: run in background and return immediately - `-c, --cwd <dir>`: working directory - `-u, --user <user>`: run as specified user -- `-e, --env <KEY=VALUE>`: set environment variable (repeatable) Default: `[object Object]`. +- `-e, --env <KEY=VALUE>`: set environment variable (repeatable) ## agentbox sandbox info diff --git a/scripts/test-reference-contract.py b/scripts/test-reference-contract.py index c9178967..5f99868e 100644 --- a/scripts/test-reference-contract.py +++ b/scripts/test-reference-contract.py @@ -153,6 +153,11 @@ def main() -> None: for forbidden in FORBIDDEN_REFERENCE_PROPERTIES: assert forbidden not in rendered_markdown + cli_markdown = "\n".join( + path.read_text() for path in (REFERENCE / "sdk/cli").glob("*.md") + ) + assert "[object Object]" not in cli_markdown + javascript_files = { str(path.relative_to(REFERENCE)) for path in (REFERENCE / "sdk/javascript").rglob("*.md")