Skip to content

Commit 21ffeb4

Browse files
TheodoreSpeaksclaude
authored andcommitted
feat(cli): generate the CLI's v2 API from the route contracts, add tables
The same endpoint was being described in three hand-maintained places: the Zod contracts the routes validate against, the OpenAPI documents, and the CLI's own TypeScript interfaces. Two of those are now derived. `scripts/generate-v2-cli-api.ts` reads `apps/sim/lib/api/contracts/v2/**` and emits `packages/sim-cli/src/generated/v2-api.ts`: request/response types for all 44 operations plus an operation table (method, path, path params) the client dispatches through, so a route that moves or changes verb moves the CLI with it. The contracts are the right source because the routes validate against them — a shape that disagrees with a contract is a shape the server would reject. Zod 4's `z.toJSONSchema()` handles all 110 schema slots; the JSON-Schema-to-TS emitter is hand-rolled over that known-narrow subset and throws on anything unrecognized rather than degrading to `any`, since silence is how a generated client drifts. `packages/*` must not import `apps/*`, so the generated file is plain type declarations with no imports and the script does the crossing at build time. `check:cli-api` fails CI when the file is stale. The generated directory is excluded from biome: the pre-commit hook runs `check --write`, which would otherwise reformat generated output and fail that check with an unrelated message. The docs specs carry ~1000 hand-written descriptions and ~400 examples that Zod schemas do not encode, so generating them would trade real documentation for mechanical accuracy. `check:openapi-drift` reconciles structure instead — every v2 path and method must exist on both sides — keeping the prose while still failing on divergence. Both currently agree on all 44 operations. `sim tables list|get|columns|rows|insert|delete-rows`, built on the generated types. Rows go through the POST query endpoint even unfiltered, since it is the only shape carrying the predicate. Row columns are discovered at runtime and unioned across the page, so a sparse row cannot hide a column. Deletion requires an explicit `--row`/`--filter` selector *and* `--yes`; an argument-less call would otherwise empty the table. Path params are percent-encoded — an id containing `/` or `?` would otherwise retarget the request. The four existing command groups drop their hand-written interfaces for the generated ones. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EsThUPqZXjwuuyRjBbmVkj
1 parent 055e893 commit 21ffeb4

13 files changed

Lines changed: 2468 additions & 87 deletions

File tree

biome.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
"!**/.venv",
3434
"!**/uploads",
3535
"!**/apps/sim/lib/execution/sandbox/bundles/*.cjs",
36+
"!**/packages/sim-cli/src/generated",
3637
"!**/test-results",
3738
"!**/playwright-report"
3839
]

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,9 @@
5050
"check:audits": "bun run scripts/run-audits.ts",
5151
"check:desktop-bridge": "bun run scripts/check-desktop-bridge-contract.ts --check",
5252
"check:desktop-ipc": "bun run scripts/check-desktop-ipc-contract.ts",
53+
"check:cli-api": "bun run scripts/generate-v2-cli-api.ts --check",
54+
"check:openapi-drift": "bun run scripts/generate-v2-cli-api.ts --check-openapi",
55+
"generate:cli-api": "bun run scripts/generate-v2-cli-api.ts",
5356
"desktop-bridge-contract:update": "bun run scripts/check-desktop-bridge-contract.ts --update",
5457
"mship-contracts:generate": "bun run scripts/sync-mothership-stream-contract.ts",
5558
"mship-contracts:check": "bun run scripts/sync-mothership-stream-contract.ts --check",

packages/sim-cli/README.md

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,13 @@ sim logs list [--level error] [--workflow <id>…] [--trigger <name>…] [--star
114114
sim logs get <id>
115115
sim logs execution <executionId>
116116

117+
sim tables list
118+
sim tables get <tableId>
119+
sim tables columns <tableId>
120+
sim tables rows <tableId> [--filter <json>] [--sort <field:dir>…] [--limit <n>]
121+
sim tables insert <tableId> --data <json>
122+
sim tables delete-rows <tableId> (--row <id>| --filter <json>) --yes
123+
117124
sim files list
118125
sim files download <fileId> [-o <path>]
119126
sim files delete <fileId>
@@ -124,18 +131,61 @@ sim knowledge documents <id> [--search <text>]
124131
sim knowledge search <query> --kb <id>
125132
```
126133

134+
### Filtering table rows
135+
136+
`--filter` takes the same predicate tree the API uses — `all` (AND) or `any`
137+
(OR) groups of `{field, op, value}` conditions, nestable. It's JSON because the
138+
grammar is a tree; there's no honest flag encoding for it.
139+
140+
```bash
141+
sim tables rows tbl_123 \
142+
--filter '{"all":[{"field":"status","op":"eq","value":"open"},
143+
{"field":"score","op":"gt","value":10}]}' \
144+
--sort score:desc --limit 50
145+
```
146+
147+
Row columns are discovered at runtime from the returned data, unioned across the
148+
page so a sparse row doesn't hide a column.
149+
150+
Deletions require an explicit selector *and* `--yes`; there is no "delete
151+
everything" default.
152+
127153
Every command takes `--output json` for scripting; the JSON is the API's own
128154
response shape, so it pipes cleanly into `jq`.
129155

130156
```bash
131157
sim logs list --level error --output json | jq -r '.[].executionId'
132158
```
133159

160+
## How this stays in sync with the API
161+
162+
`src/generated/v2-api.ts` is generated from the Zod route contracts in
163+
`apps/sim/lib/api/contracts/v2/**` — the same contracts the routes validate
164+
against, so a shape that disagrees with them is a shape the server would reject.
165+
It holds every response/request type plus the operation table (method, path,
166+
path params) the client dispatches through.
167+
168+
```bash
169+
bun run generate:cli-api # regenerate after changing a contract
170+
bun run check:cli-api # CI: fails if the generated file is stale
171+
bun run check:openapi-drift # CI: fails if the docs and contracts disagree
172+
```
173+
174+
The generated file contains only type declarations and one const — no imports —
175+
so the `packages/*` must not import `apps/*` boundary is preserved; the script
176+
does the crossing at build time.
177+
178+
The OpenAPI documents under `apps/docs` are deliberately **not** generated. They
179+
carry ~1000 hand-written descriptions and ~400 examples that Zod schemas don't
180+
encode, so regenerating them would trade real documentation for mechanical
181+
accuracy. `check:openapi-drift` reconciles their *structure* against the
182+
contracts instead — every v2 path and method must exist on both sides — so the
183+
prose survives while drift still fails the build.
184+
134185
## Notes
135186

136187
- Commands talk to the `/api/v2` surface, which returns `{ data }` and
137188
`{ data, nextCursor }`. List commands auto-page up to `--limit`.
138-
- `sim tables` is not here yet — the tables v2 surface is still changing.
139189

140190
## License
141191

packages/sim-cli/src/commands/files.ts

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,11 @@ import { basename } from 'node:path'
44
import chalk from 'chalk'
55
import { Command } from 'commander'
66
import { clientFrom } from '../context.js'
7+
import type { ListFilesResponse } from '../generated/v2-api.js'
78
import { SimApiError } from '../http/client.js'
89
import { bytes, type Column, printList, timestamp } from '../output/render.js'
910

10-
interface WorkspaceFile {
11-
id: string
12-
name: string
13-
size: number
14-
type: string
15-
key: string
16-
uploadedBy: string
17-
uploadedAt: string
18-
}
11+
type WorkspaceFile = ListFilesResponse['data'][number]
1912

2013
/**
2114
* Streams a fetch body to disk, honouring backpressure.

packages/sim-cli/src/commands/knowledge.ts

Lines changed: 8 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,15 @@
11
import { Command } from 'commander'
22
import { clientFrom } from '../context.js'
3+
import type {
4+
ListKnowledgeBasesResponse,
5+
ListKnowledgeDocumentsResponse,
6+
SearchKnowledgeResponse,
7+
} from '../generated/v2-api.js'
38
import { bytes, type Column, printList, printRecord, text, timestamp } from '../output/render.js'
49

5-
interface KnowledgeBase {
6-
id: string
7-
name: string
8-
description: string | null
9-
docCount: number
10-
tokenCount: number
11-
embeddingModel: string
12-
createdAt: string | null
13-
updatedAt: string | null
14-
}
15-
16-
interface KnowledgeDocument {
17-
id: string
18-
knowledgeBaseId: string
19-
filename: string
20-
fileSize: number
21-
mimeType: string
22-
processingStatus: string
23-
chunkCount: number
24-
tokenCount: number
25-
enabled: boolean
26-
createdAt: string | null
27-
}
28-
29-
interface SearchHit {
30-
documentId: string
31-
documentName: string | null
32-
content: string
33-
chunkIndex: number
34-
similarity: number
35-
}
10+
type KnowledgeBase = ListKnowledgeBasesResponse['data'][number]
11+
type KnowledgeDocument = ListKnowledgeDocumentsResponse['data'][number]
12+
type SearchHit = SearchKnowledgeResponse['data']['results'][number]
3613

3714
const BASE_COLUMNS: Column<KnowledgeBase>[] = [
3815
{ header: 'id', value: (kb) => kb.id },

packages/sim-cli/src/commands/logs.ts

Lines changed: 7 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,12 @@
11
import chalk from 'chalk'
22
import { Command } from 'commander'
33
import { clientFrom } from '../context.js'
4+
import type { GetExecutionResponse, GetLogResponse, ListLogsResponse } from '../generated/v2-api.js'
45
import { type Column, duration, printList, printRecord, text, timestamp } from '../output/render.js'
56

6-
interface LogListItem {
7-
id: string
8-
workflowId: string | null
9-
executionId: string
10-
level: string
11-
trigger: string
12-
startedAt: string
13-
endedAt: string | null
14-
totalDurationMs: number | null
15-
cost: { total: number } | null
16-
workflow?: { id: string | null; name: string; deleted: boolean }
17-
}
18-
19-
interface LogDetail extends LogListItem {
20-
executionData: unknown
21-
createdAt: string
22-
}
7+
type LogListItem = ListLogsResponse['data'][number]
8+
type LogDetail = GetLogResponse['data']
9+
type ExecutionDetail = GetExecutionResponse['data']
2310

2411
function level(value: string): string {
2512
return value === 'error' ? chalk.red(value) : value
@@ -128,17 +115,9 @@ export function logsCommand(): Command {
128115
.description('Show the workflow state snapshot for an execution')
129116
.action(async (executionId: string, _options: unknown, command: Command) => {
130117
const { client, profile } = clientFrom(command)
131-
const execution = await client.getData<{
132-
executionId: string
133-
workflowId: string | null
134-
executionMetadata: {
135-
trigger: string
136-
startedAt: string
137-
endedAt: string | null
138-
totalDurationMs: number | null
139-
cost: { total: number } | null
140-
}
141-
}>(`/api/v2/logs/executions/${executionId}`)
118+
const execution = await client.getData<ExecutionDetail>(
119+
`/api/v2/logs/executions/${executionId}`
120+
)
142121

143122
printRecord(
144123
profile.output,

0 commit comments

Comments
 (0)