Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
da28b04
feat: add typed client runtime foundation
0xpolarzero May 25, 2026
6a71ed4
fix: keep runtime foundation typegen output unchanged
0xpolarzero May 26, 2026
0e0fac3
test: harden streaming duration snapshots
0xpolarzero May 26, 2026
4972267
refactor: flatten client transport capabilities
0xpolarzero May 26, 2026
f528bdb
refactor: remove unused transport context
0xpolarzero May 26, 2026
aca3ab7
test: keep client runtime OpenAPI fixture scoped
0xpolarzero May 26, 2026
5f92ad8
refactor(client): namespace transport modules
0xpolarzero May 26, 2026
a548dc6
test(client): expand transport route coverage
0xpolarzero May 26, 2026
b51681e
refactor(client): split request discover local runtimes
0xpolarzero May 27, 2026
2499cfa
refactor(client): wrap local runtime capability
0xpolarzero May 27, 2026
e2e6687
refactor(client): inline runtime methods
0xpolarzero May 27, 2026
93af911
refactor(client): reuse cli command tree internals
0xpolarzero May 27, 2026
ba15725
refactor(client): rename runtime context module
0xpolarzero May 27, 2026
1afb8d5
refactor(client): move request status mapping
0xpolarzero May 27, 2026
d3bd9e3
fix(client): expose rpc output metadata
0xpolarzero May 27, 2026
c7dc375
fix(client): share rendered output default
0xpolarzero May 27, 2026
8d78ee4
fix(client): canonicalize runtime client contracts
0xpolarzero May 27, 2026
60a8029
fix(client): preserve HTTP transport error metadata
0xpolarzero May 27, 2026
dc1cd11
refactor(client): share structured command collection
0xpolarzero May 27, 2026
2f1d7f9
fix(typegen): emit exact optional property types
0xpolarzero May 27, 2026
4e8a8a8
fix(typegen): emit command output metadata
0xpolarzero May 27, 2026
4eede40
fix: reuse cli discovery projection
0xpolarzero May 27, 2026
0be074c
fix(client): preserve runtime cli metadata
0xpolarzero May 27, 2026
a1acb15
refactor(client): rename request and discover type namespaces
0xpolarzero May 27, 2026
f00ba52
refactor(client): align internal handler names
0xpolarzero May 27, 2026
1147a6d
test(client): cover internal handlers
0xpolarzero May 27, 2026
3c9e469
refactor(client): rename local methods type
0xpolarzero May 27, 2026
c5d2832
refactor(client): expose rpc error type
0xpolarzero May 27, 2026
20fc913
chore(client): add client path alias
0xpolarzero May 27, 2026
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
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,11 @@
"types": "./dist/index.d.ts",
"src": "./src/index.ts",
"default": "./dist/index.js"
},
"./client": {
"types": "./dist/client/index.d.ts",
"src": "./src/client/index.ts",
"default": "./dist/client/index.js"
}
}
}
22 changes: 22 additions & 0 deletions src/Cli.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,28 @@ test('Cta accepts object form', () => {
expectTypeOf<{ command: 'auth login'; description: 'Log in' }>().toMatchTypeOf<Cli.Cta>()
})

test('OpenAPI-mounted operations are included in CLI command map type', () => {
const cli = Cli.create('test').command('api', {
fetch: () => new Response('{}'),
openapi: {
paths: {
'/users': {
get: {
operationId: 'listUsers',
responses: { '200': { description: 'ok' } },
},
},
},
},
})

expectTypeOf<typeof cli>().toMatchTypeOf<
Cli.Cli<{
'api listUsers': { args: Record<string, unknown>; options: Record<string, unknown> }
}>
>()
})

test('Cta narrows strings and objects to registered commands', () => {
type Commands = {
get: { args: { id: number }; options: {} }
Expand Down
174 changes: 174 additions & 0 deletions src/Cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4175,6 +4175,7 @@ describe('Command.execute', () => {
async function fetchJson(cli: Cli.Cli<any, any, any>, req: Request) {
const res = await cli.fetch(req)
const body = await res.json()
expect(body.meta.duration).toMatch(/^\d+ms$/)
body.meta.duration = '<stripped>'
return { status: res.status, body }
}
Expand Down Expand Up @@ -4243,6 +4244,179 @@ describe('fetch', () => {
expect(res.body.error.message).toContain("Did you mean 'health'?")
})

test('RPC route maps protocol failures to HTTP statuses', async () => {
const cli = Cli.create('app').command(
Cli.create('group').command('leaf', {
run() {
return null
},
}),
)
cli.command('raw', { fetch: () => new Response('{}') })

expect(
await fetchJson(
cli,
new Request('http://localhost/_incur/rpc', {
method: 'POST',
body: JSON.stringify({ command: '' }),
}),
),
).toMatchInlineSnapshot(`
{
"body": {
"error": {
"code": "INVALID_RPC_REQUEST",
"message": "RPC command is required.",
},
"meta": {
"command": "",
"duration": "<stripped>",
},
"ok": false,
},
"status": 400,
}
`)

expect(
await fetchJson(
cli,
new Request('http://localhost/_incur/rpc', {
method: 'POST',
body: JSON.stringify({ command: 'group' }),
}),
),
).toMatchInlineSnapshot(`
{
"body": {
"error": {
"code": "COMMAND_GROUP",
"message": "'group' is a command group. Specify a subcommand.",
},
"meta": {
"command": "group",
"duration": "<stripped>",
},
"ok": false,
},
"status": 400,
}
`)

expect(
await fetchJson(
cli,
new Request('http://localhost/_incur/rpc', {
method: 'POST',
body: JSON.stringify({ command: 'raw' }),
}),
),
).toMatchInlineSnapshot(`
{
"body": {
"error": {
"code": "FETCH_GATEWAY",
"message": "'raw' is a raw fetch gateway and cannot be called with structured RPC.",
},
"meta": {
"command": "raw",
"duration": "<stripped>",
},
"ok": false,
},
"status": 400,
}
`)

expect(
await fetchJson(
cli,
new Request('http://localhost/_incur/rpc', {
method: 'POST',
body: JSON.stringify({ command: 'missing' }),
}),
),
).toMatchInlineSnapshot(`
{
"body": {
"error": {
"code": "COMMAND_NOT_FOUND",
"message": "'missing' is not a command for 'app'.",
},
"meta": {
"command": "missing",
"duration": "<stripped>",
},
"ok": false,
},
"status": 404,
}
`)
})

test('discovery routes map failures to envelopes', async () => {
const cli = Cli.create('app').command('status', {
run() {
return { ok: true }
},
})

expect(await fetchJson(cli, new Request('http://localhost/_incur/help?command=missing')))
.toMatchInlineSnapshot(`
{
"body": {
"error": {
"code": "COMMAND_NOT_FOUND",
"message": "Unknown command 'missing'.",
},
"meta": {
"duration": "<stripped>",
"resource": "help",
},
"ok": false,
},
"status": 404,
}
`)

expect(await fetchJson(cli, new Request('http://localhost/_incur/skill?name=../x')))
.toMatchInlineSnapshot(`
{
"body": {
"error": {
"code": "INVALID_SKILL_NAME",
"message": "Unsafe skill name.",
},
"meta": {
"duration": "<stripped>",
"resource": "skill",
},
"ok": false,
},
"status": 400,
}
`)

expect(await fetchJson(cli, new Request('http://localhost/_incur/skill?name=missing')))
.toMatchInlineSnapshot(`
{
"body": {
"error": {
"code": "SKILL_NOT_FOUND",
"message": "Unknown skill 'missing'.",
},
"meta": {
"duration": "<stripped>",
"resource": "skill",
},
"ok": false,
},
"status": 404,
}
`)
})

test('GET / with root command → 200', async () => {
const cli = Cli.create('test', { run: () => ({ root: true }) })
expect(await fetchJson(cli, new Request('http://localhost/'))).toMatchInlineSnapshot(`
Expand Down
Loading