diff --git a/.changeset/gcf-output-format.md b/.changeset/gcf-output-format.md new file mode 100644 index 0000000..829679a --- /dev/null +++ b/.changeset/gcf-output-format.md @@ -0,0 +1,5 @@ +--- +'incur': patch +--- + +Added `gcf` as an output format (`--format gcf`) alongside `toon`, `json`, `yaml`, `md`, and `jsonl`. GCF (Graph Compact Format) is a lossless, zero-runtime-dependency encoding for structured data. diff --git a/package.json b/package.json index c514caa..5166b38 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "SKILL.md" ], "dependencies": { + "@blackwell-systems/gcf": "2.4.0", "@cfworker/json-schema": "^4.1.1", "@modelcontextprotocol/server": "^2.0.0-alpha.2", "@scalar/openapi-types": "^0.8.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f890daf..3bf9a3b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: .: dependencies: + '@blackwell-systems/gcf': + specifier: 2.4.0 + version: 2.4.0 '@cfworker/json-schema': specifier: ^4.1.1 version: 4.1.1 @@ -112,6 +115,10 @@ packages: resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} engines: {node: '>=18'} + '@blackwell-systems/gcf@2.4.0': + resolution: {integrity: sha512-lLEZCzNMYVk630iaVVPPhcQtmCjhPRETRlwk/JAptGE0SEL6ZMHfA9HZPornuMvvpGeyrimRjj9Yb2nYQNS1og==} + hasBin: true + '@cfworker/json-schema@4.1.1': resolution: {integrity: sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og==} @@ -1504,6 +1511,8 @@ snapshots: '@bcoe/v8-coverage@1.0.2': {} + '@blackwell-systems/gcf@2.4.0': {} + '@cfworker/json-schema@4.1.1': {} '@changesets/apply-release-plan@7.0.14': diff --git a/src/Cli.test-d.ts b/src/Cli.test-d.ts index 1de63ef..b493b30 100644 --- a/src/Cli.test-d.ts +++ b/src/Cli.test-d.ts @@ -271,7 +271,7 @@ test('middleware() without generic gives empty context', () => { middleware((c, _next) => { expectTypeOf(c.var).toEqualTypeOf<{}>() expectTypeOf(c.env).toEqualTypeOf<{}>() - expectTypeOf(c.format).toEqualTypeOf<'toon' | 'json' | 'yaml' | 'md' | 'jsonl'>() + expectTypeOf(c.format).toEqualTypeOf<'toon' | 'json' | 'yaml' | 'md' | 'jsonl' | 'gcf'>() expectTypeOf(c.formatExplicit).toEqualTypeOf() }) }) @@ -367,7 +367,7 @@ test('run() context exposes format metadata', () => { const cli = Cli.create('test') cli.command('ping', { run(c) { - expectTypeOf(c.format).toEqualTypeOf<'toon' | 'json' | 'yaml' | 'md' | 'jsonl'>() + expectTypeOf(c.format).toEqualTypeOf<'toon' | 'json' | 'yaml' | 'md' | 'jsonl' | 'gcf'>() expectTypeOf(c.formatExplicit).toEqualTypeOf() return { pong: true } }, diff --git a/src/Cli.ts b/src/Cli.ts index 9e7558e..6230f2a 100644 --- a/src/Cli.ts +++ b/src/Cli.ts @@ -2480,7 +2480,7 @@ declare namespace serveImpl { } /** @internal Extracts built-in flags (--full-output, --format, --json, --llms, --help, --version) from argv. */ -const validFormats = new Set(['toon', 'json', 'yaml', 'md', 'jsonl'] as const) +const validFormats = new Set(['toon', 'json', 'yaml', 'md', 'jsonl', 'gcf'] as const) function extractBuiltinFlags(argv: string[], options: extractBuiltinFlags.Options = {}) { let fullOutput = false diff --git a/src/Formatter.test.ts b/src/Formatter.test.ts index e1f0109..8ebe611 100644 --- a/src/Formatter.test.ts +++ b/src/Formatter.test.ts @@ -1,3 +1,4 @@ +import { decodeGeneric } from '@blackwell-systems/gcf' import { decode } from '@toon-format/toon' import { Formatter } from 'incur' @@ -53,6 +54,41 @@ describe('format', () => { expect(result).toMatchInlineSnapshot(`"message: hello"`) }) + test('formats as GCF (explicit)', () => { + const result = Formatter.format({ message: 'hello' }, 'gcf') + expect(result).toBe('GCF profile=generic\nmessage=hello\n') + }) + + test('formats an array of records as a GCF table', () => { + const result = Formatter.format( + { + ok: true, + data: { + users: [ + { id: 1, name: 'a' }, + { id: 2, name: 'b' }, + ], + }, + meta: { command: 'users' }, + }, + 'gcf', + ) + expect(result).toBe( + 'GCF profile=generic\nok=true\n## data\n ## users [2]{id,name}\n 1|a\n 2|b\n## meta\n command=users\n', + ) + }) + + test('round-trips through GCF decode', () => { + const envelope = { + ok: true, + data: { items: [1, 2, 3] }, + meta: { command: 'list', duration: '5ms' }, + } + + const result = decodeGeneric(Formatter.format(envelope, 'gcf')) + expect(result).toMatchObject(envelope) + }) + test('formats as JSON', () => { const result = Formatter.format({ message: 'hello' }, 'json') expect(result).toMatchInlineSnapshot(` diff --git a/src/Formatter.ts b/src/Formatter.ts index b0abe45..39f4aeb 100644 --- a/src/Formatter.ts +++ b/src/Formatter.ts @@ -1,10 +1,11 @@ +import { encodeGeneric } from '@blackwell-systems/gcf' import { encode } from '@toon-format/toon' import * as Json from './internal/json.js' import * as Yaml from './internal/yaml.js' /** Supported output formats. */ -export type Format = 'toon' | 'json' | 'yaml' | 'md' | 'jsonl' +export type Format = 'toon' | 'json' | 'yaml' | 'md' | 'jsonl' | 'gcf' /** Serializes a value to the specified format. Defaults to TOON. */ export function format(value: unknown, fmt: Format = 'toon'): string { @@ -26,6 +27,10 @@ export function format(value: unknown, fmt: Format = 'toon'): string { if (Array.isArray(value)) return value.map((v) => Json.stringify(v)).join('\n') return Json.stringify(value) } + if (fmt === 'gcf') { + if (isScalar(value)) return String(value) + return encodeGeneric(value) + } // toon (default) if (isScalar(value)) return String(value) return encode(value as Record) diff --git a/src/Help.ts b/src/Help.ts index 3c6e0d3..080b636 100644 --- a/src/Help.ts +++ b/src/Help.ts @@ -407,7 +407,7 @@ function globalOptionsLines( flag: '--filter-output ', desc: 'Filter output by key paths (e.g. foo,bar.baz,a[0,3])', }, - { flag: '--format ', desc: 'Output format' }, + { flag: '--format ', desc: 'Output format' }, { flag: '--help', desc: 'Show help' }, { flag: '--llms, --llms-full', desc: 'Print LLM-readable manifest' }, ...(root ? [{ flag: '--mcp', desc: 'Start as MCP stdio server' }] : []),