Skip to content

Commit 342dd14

Browse files
TheodoreSpeaksclaude
authored andcommitted
feat(cli): CLI contract for the v2 surface, incl. execution
Adds `packages/sim-cli/src/contract` — the declarative definition of how the terminal maps onto the API — and folds in the v2 execution endpoints that just landed on improvement/v2-endpoints. Read it as a diff against what is already derivable, not a listing. Method, path, path params, field types, enum values, defaults and required-ness all come from the generated operation table (which comes from the Zod contracts), and the command name derives from `<resource> [sub-resource] <verb>`. 23 of 47 operations therefore need no entry at all. The 24 that do carry only what a schema cannot express: - names, where REST overloads one path — `DELETE /rows` vs `DELETE /rows/[rowId]` becomes `batch-delete` vs `delete`, and `DELETE /deploy` becomes `undeploy` - flags, where a field's type misdescribes its meaning — `workflowIds` is `z.string()` that the route splits on commas; no generator can infer that - columns, which are editorial - confirm, for the 8 destructive operations `executeWorkflow` / `getWorkflowExecution` / `cancelWorkflowExecution` derive badly (`/execute` and `/cancel` are verbs the deriver reads as nouns), so all three are named explicitly: `workflows run`, `workflows executions get|cancel`. `stream` is marked `omit`: it switches the response to SSE, which the JSON client would try to parse. Advertising a flag that breaks the response is worse than not offering it — a `--follow` command that renders the stream is separate and hand-written, like `files download`. - Drops `check:openapi-drift`. The branch landed `check:openapi`, which does the same path/method reconciliation plus a recursive field diff and validates doc examples against the real Zod schemas — mine was a strict subset. - Surfaces the new v2 rollout gate in the CLI: it answers 404 for callers outside the cohort, indistinguishable from a missing resource, so a 404 now carries that as a possibility rather than a diagnosis. - `executor/utils/errors.ts` widens instead of casting through `unknown`, which is both more honest (the value is an Error) and keeps the double-cast ratchet at 8. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EsThUPqZXjwuuyRjBbmVkj
1 parent 52468f5 commit 342dd14

7 files changed

Lines changed: 413 additions & 75 deletions

File tree

apps/sim/executor/utils/errors.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,10 @@ function readAttachedBlockContext(error: unknown): {
169169
blockType?: string
170170
} {
171171
if (!(error instanceof Error)) return {}
172-
const attached = error as unknown as AttachedBlockContext
172+
// Widen rather than erase: the value is an Error, it just may carry extra
173+
// fields attached at throw time. Casting through `unknown` would discard
174+
// that, and trips the double-cast ratchet for no benefit.
175+
const attached = error as Error & Partial<AttachedBlockContext>
173176
return {
174177
blockId: typeof attached.blockId === 'string' ? attached.blockId : undefined,
175178
blockName: typeof attached.blockName === 'string' ? attached.blockName : undefined,

package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,6 @@
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",
5353
"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",
5554
"generate:cli-api": "bun run scripts/generate-v2-cli-api.ts",
5655
"desktop-bridge-contract:update": "bun run scripts/check-desktop-bridge-contract.ts --update",
5756
"mship-contracts:generate": "bun run scripts/sync-mothership-stream-contract.ts",
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
import type { CliContract } from './types.js'
2+
3+
/**
4+
* The CLI contract for the v2 surface.
5+
*
6+
* Read this as a diff against what is already derivable — an operation absent
7+
* from this table still gets a command, built entirely from the generated
8+
* operation table. Only the entries below needed a human.
9+
*
10+
* Derived by default:
11+
* listTables → sim tables list
12+
* getKnowledgeDocument → sim knowledge documents get <id> <documentId>
13+
* upsertTableRow → sim tables upsert <tableId>
14+
*/
15+
export const CLI_CONTRACT: CliContract = {
16+
// ─── Name collisions: REST overloads one path for single and bulk ─────────
17+
// The derived name is identical for both, so the bulk form is renamed. AWS's
18+
// `batch-` prefix rather than a `--all` flag: the plural is a different and
19+
// more dangerous operation, and it should be a different word.
20+
deleteTableRows: {
21+
command: 'tables rows batch-delete',
22+
describe: 'Delete rows matching a filter, or an explicit list of ids',
23+
flags: { rowIds: { name: 'row', list: true }, filter: { json: true } },
24+
confirm: 'This deletes every matching row and cannot be undone.',
25+
},
26+
updateRowsByFilter: {
27+
command: 'tables rows batch-update',
28+
describe: 'Update every row matching a filter',
29+
flags: { filter: { json: true }, data: { json: true } },
30+
confirm: 'This updates every matching row and cannot be undone.',
31+
},
32+
// `DELETE /workflows/[id]/deploy` is an undeploy, not a delete.
33+
undeployWorkflow: {
34+
command: 'workflows undeploy',
35+
describe: 'Take a workflow out of deployment',
36+
},
37+
38+
// ─── Destructive single-resource operations ───────────────────────────────
39+
deleteTable: { confirm: 'This deletes the table and all of its rows.' },
40+
deleteTableRow: { confirm: 'This deletes the row.' },
41+
deleteTableColumn: { confirm: 'This deletes the column and its values in every row.' },
42+
deleteKnowledgeBase: { confirm: 'This deletes the knowledge base and every document in it.' },
43+
deleteKnowledgeDocument: { confirm: 'This deletes the document and its embeddings.' },
44+
deleteFile: { confirm: 'This archives the file.' },
45+
46+
// ─── Fields whose type misdescribes their meaning ─────────────────────────
47+
// `z.string()` that the route splits on commas. No generator can infer this.
48+
listLogs: {
49+
flags: {
50+
workflowIds: { name: 'workflow', list: true },
51+
folderIds: { name: 'folder', list: true },
52+
triggers: { name: 'trigger', list: true },
53+
},
54+
columns: [
55+
{ header: 'started', path: 'startedAt', format: 'timestamp' },
56+
{ header: 'level' },
57+
{ header: 'trigger' },
58+
{ header: 'workflow', path: 'workflow.name' },
59+
{ header: 'duration', path: 'totalDurationMs', format: 'duration' },
60+
{ header: 'cost', path: 'cost.total', format: 'cost' },
61+
{ header: 'execution', path: 'executionId' },
62+
],
63+
},
64+
searchKnowledge: {
65+
// Accepts a string or an array on the wire; the CLI always sends the array.
66+
flags: { knowledgeBaseIds: { name: 'kb', list: true }, tagFilters: { json: true } },
67+
columns: [
68+
{ header: 'score', path: 'similarity' },
69+
{ header: 'document', path: 'documentName' },
70+
{ header: 'chunk', path: 'chunkIndex' },
71+
{ header: 'content' },
72+
],
73+
},
74+
75+
// ─── Friendlier flag names ────────────────────────────────────────────────
76+
upsertTableRow: {
77+
describe: 'Insert a row, or update the one that conflicts on a unique column',
78+
flags: {
79+
data: { json: true },
80+
conflictTarget: { name: 'on', describe: 'Unique column to resolve the conflict against' },
81+
},
82+
columns: [{ header: 'id' }, { header: 'operation' }],
83+
},
84+
queryRows: {
85+
command: 'tables rows query',
86+
flags: { predicate: { name: 'filter', json: true }, sort: { json: true } },
87+
},
88+
89+
// ─── Output columns for list commands ─────────────────────────────────────
90+
listTables: {
91+
columns: [
92+
{ header: 'id' },
93+
{ header: 'name' },
94+
{ header: 'rows', path: 'rowCount' },
95+
{ header: 'updated', path: 'updatedAt', format: 'timestamp' },
96+
],
97+
},
98+
listWorkflows: {
99+
columns: [
100+
{ header: 'id' },
101+
{ header: 'name' },
102+
{ header: 'deployed', path: 'isDeployed', format: 'bool' },
103+
{ header: 'runs', path: 'runCount' },
104+
{ header: 'last run', path: 'lastRunAt', format: 'timestamp' },
105+
],
106+
},
107+
listFiles: {
108+
columns: [
109+
{ header: 'id' },
110+
{ header: 'name' },
111+
{ header: 'size', format: 'bytes' },
112+
{ header: 'type' },
113+
{ header: 'uploaded', path: 'uploadedAt', format: 'timestamp' },
114+
],
115+
},
116+
listKnowledgeBases: {
117+
columns: [
118+
{ header: 'id' },
119+
{ header: 'name' },
120+
{ header: 'docs', path: 'docCount' },
121+
{ header: 'tokens', path: 'tokenCount' },
122+
{ header: 'model', path: 'embeddingModel' },
123+
],
124+
},
125+
listKnowledgeDocuments: {
126+
columns: [
127+
{ header: 'id' },
128+
{ header: 'filename' },
129+
{ header: 'size', path: 'fileSize', format: 'bytes' },
130+
{ header: 'status', path: 'processingStatus' },
131+
{ header: 'chunks', path: 'chunkCount' },
132+
],
133+
},
134+
listAuditLogs: {
135+
columns: [
136+
{ header: 'at', path: 'createdAt', format: 'timestamp' },
137+
{ header: 'actor', path: 'actorEmail' },
138+
{ header: 'action' },
139+
{ header: 'resource', path: 'resourceName' },
140+
],
141+
},
142+
143+
// ─── Execution ────────────────────────────────────────────────────────────
144+
// The derived names land badly here: `/execute` and `/cancel` are verbs in
145+
// the path, but neither is in the action list, so POST would derive
146+
// `workflows execute create` and `workflows cancel create`.
147+
executeWorkflow: {
148+
command: 'workflows run',
149+
describe: 'Run a deployed workflow and wait for the result',
150+
flags: {
151+
input: { json: true, describe: 'Trigger input as JSON' },
152+
selectedOutputs: { name: 'output', list: true },
153+
// SSE, not JSON — the generic client cannot consume it. A `sim workflows
154+
// run --follow` that renders the stream is a separate, hand-written
155+
// command; advertising a flag that breaks the response is worse than
156+
// not offering it yet.
157+
stream: { omit: true },
158+
},
159+
},
160+
getWorkflowExecution: {
161+
command: 'workflows executions get',
162+
describe: 'Show the status of one execution',
163+
},
164+
cancelWorkflowExecution: {
165+
command: 'workflows executions cancel',
166+
describe: 'Cancel a running execution',
167+
// Not `confirm`-gated: cancelling is recoverable (re-run it), and the
168+
// whole point is to stop something that is already going wrong.
169+
},
170+
171+
// ─── Not a terminal-shaped operation ──────────────────────────────────────
172+
// Multipart upload; `sim files upload <path>` needs its own file-reading
173+
// command rather than a generated flag surface.
174+
uploadFile: { hidden: true },
175+
uploadKnowledgeDocument: { hidden: true },
176+
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import type { V2OperationName } from '../generated/v2-api.js'
2+
3+
/**
4+
* The CLI contract: how the terminal surface maps onto the v2 API.
5+
*
6+
* Most of a command is derivable and is NOT stated here. Method, path, path
7+
* params, field types, enum values, defaults, and required-ness all come from
8+
* the generated operation table, which comes from the Zod route contracts. The
9+
* command name itself derives from `<resource> <sub-resource> <verb>` for 41 of
10+
* the 44 operations.
11+
*
12+
* This file carries only what a schema cannot say:
13+
*
14+
* - `command` — when the derived name collides or reads badly. REST overloads
15+
* one path for single and bulk (`DELETE /rows` vs `DELETE /rows/[rowId]`), so
16+
* those need a human to pick `delete` vs `batch-delete`.
17+
* - `flags` — when a field's *type* misdescribes its *meaning*. `workflowIds`
18+
* is `z.string()` that the route splits on commas; nothing in the schema says
19+
* "list". Also friendlier aliases (`conflictTarget` → `--on`).
20+
* - `columns` — which of a response's fields belong in a table. Editorial.
21+
* - `confirm` — which operations are destructive enough to demand `--yes`.
22+
*
23+
* An operation with nothing unusual needs no entry at all.
24+
*/
25+
26+
/** How one request field is exposed as a flag. */
27+
export interface FlagSpec {
28+
/** Flag name, kebab-case, without `--`. Defaults to the kebab-cased field name. */
29+
name?: string
30+
/** Short alias, e.g. `w` for `--workspace`. */
31+
short?: string
32+
/**
33+
* Accept a repeated flag and send it comma-joined. For fields the schema
34+
* types as `string` but the route splits — invisible to any type-driven
35+
* generator, so it has to be stated.
36+
*/
37+
list?: boolean
38+
/** Take a JSON string. Implied for object/array/unknown fields. */
39+
json?: boolean
40+
/** Overrides the help text otherwise taken from the OpenAPI description. */
41+
describe?: string
42+
/**
43+
* Never expose this field as a flag, and never send it.
44+
*
45+
* For request fields the terminal cannot honor — `stream: true` switches the
46+
* response to SSE, which the JSON client would try to `JSON.parse`. Offering
47+
* the flag would advertise a mode that breaks; a bespoke streaming command
48+
* owns that instead.
49+
*/
50+
omit?: boolean
51+
}
52+
53+
/** A column in table-mode output. */
54+
export interface ColumnSpec {
55+
/** Header, and the default path into the row when `value` is omitted. */
56+
header: string
57+
/** Dot path into the row. Defaults to `header`. */
58+
path?: string
59+
/** Rendering hint; `auto` inspects the value. */
60+
format?: 'auto' | 'timestamp' | 'bytes' | 'duration' | 'bool' | 'cost'
61+
}
62+
63+
export interface CommandSpec {
64+
/**
65+
* Command path, space-separated. Omit to accept the derived
66+
* `<resource> [sub-resource] <verb>` name.
67+
*/
68+
command?: string
69+
/** One-line help. Falls back to the OpenAPI summary for the operation. */
70+
describe?: string
71+
/** Per-field flag overrides, keyed by the contract's field name. */
72+
flags?: Record<string, FlagSpec>
73+
/** Columns for table output. Omit on non-list commands to print a record. */
74+
columns?: ColumnSpec[]
75+
/**
76+
* Require `--yes`. The message should say what is about to be destroyed —
77+
* the point is that the caller can tell whether they meant it.
78+
*/
79+
confirm?: string
80+
/** Keep the operation out of the CLI surface entirely. */
81+
hidden?: boolean
82+
}
83+
84+
/** The contract: operation name → how it appears in the terminal. */
85+
export type CliContract = Partial<Record<V2OperationName, CommandSpec>>

0 commit comments

Comments
 (0)