mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-21 13:00:04 +08:00
fix(cli): refuse what the help already says is invalid (#7157)
* fix(cli): refuse what the help already says is invalid
- validate integer fields against the generated spec's own `kind`, so
`--limit 1.5` names the flag and the value instead of surfacing zod's
`expected int, received number`
- enforce `--limit` >= 1 on the two non-paginated row mutations, which the
help already promised and the server already required
- refuse a blank `-c/--conversation`: it is falsy, so it was dropped from
the body and silently started a new conversation instead of continuing one
- refuse a blank `chat` message before the request rather than after
- constrain `--recipe` to the recipes the generated body type declares, so
regenerating the surface breaks the build if they diverge
- announce truncation on `ls` the way `list` already does; the capped answer
was printed silently
- stop a successful message translation from vetoing itself: the veto now
reads the message the server sent, not the rewritten one, which restores
`--folder must name an existing folder` and unblocks 100+ operations
- drop "destructive" from the `--yes` help line; the gate also covers
operations that only add, such as `files unzip`
- fix the `logs follow` help, which illustrated `--workflow` with a file id
* fix(cli): refuse a fraction the integer parse silently drops
Above 2^52 a double's spacing is 1, so Number('4503599627370496.5') is an
integer and the safe-integer guard passed it — the API received a value the
caller never typed. Read the raw text alongside the parsed number. Digits
that are all zero are not a fraction, so 1.0 stays a whole number.
Also corrects a chat test comment that described a UUID check the command
does not perform; it refuses only a blank -c.
* fix(cli): redact the value the root-flag refusal suggests
The refusal prints a command for the caller to run, and interpolated the
value verbatim. A U+2028 in it split the terminal line, so the tail rendered
as a second, plausible-looking suggestion. redact() is what the other twenty
messages in this package already use, including one forty lines below.
This commit is contained in:
@@ -31,7 +31,7 @@ Show billing status and current-period credit usage (credits and storage require
|
||||
sim billing logs [options]
|
||||
```
|
||||
|
||||
List credit usage events (a personal API key reports only your own events; a workspace API key reports every member's)
|
||||
List credit usage events (a personal API key reports only your own events; a workspace API key reports every member's in aggregate, unattributed)
|
||||
|
||||
**Options**
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ Disconnect Credential (personal API key required)
|
||||
|
||||
| Option | Required | Description |
|
||||
| --- | --- | --- |
|
||||
| `-y, --yes` | Yes | Confirm this destructive operation. |
|
||||
| `-y, --yes` | Yes | Confirm this operation. |
|
||||
|
||||
</CommandTable>
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ sim custom-tools delete <customToolId> [options]
|
||||
|
||||
| Option | Required | Description |
|
||||
| --- | --- | --- |
|
||||
| `-y, --yes` | Yes | Confirm this destructive operation. |
|
||||
| `-y, --yes` | Yes | Confirm this operation. |
|
||||
|
||||
</CommandTable>
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ sim files batch-delete [options]
|
||||
| Option | Required | Description |
|
||||
| --- | --- | --- |
|
||||
| `--file-ids <value...>` | Yes | File identifiers to update. (space-separated, or @path / @- with one value per line; @@value for a literal leading @). |
|
||||
| `-y, --yes` | Yes | Confirm this destructive operation. |
|
||||
| `-y, --yes` | Yes | Confirm this operation. |
|
||||
|
||||
</CommandTable>
|
||||
|
||||
@@ -85,7 +85,7 @@ sim files folders delete <path> [options]
|
||||
| Option | Required | Description |
|
||||
| --- | --- | --- |
|
||||
| `--recursive` | No | Delete the folder and its descendants. |
|
||||
| `-y, --yes` | Yes | Confirm this destructive operation. |
|
||||
| `-y, --yes` | Yes | Confirm this operation. |
|
||||
|
||||
</CommandTable>
|
||||
|
||||
@@ -168,7 +168,7 @@ sim files delete <fileId> [options]
|
||||
|
||||
| Option | Required | Description |
|
||||
| --- | --- | --- |
|
||||
| `-y, --yes` | Yes | Confirm this destructive operation. |
|
||||
| `-y, --yes` | Yes | Confirm this operation. |
|
||||
|
||||
</CommandTable>
|
||||
|
||||
@@ -377,7 +377,7 @@ sim files unzip <fileId> [options]
|
||||
|
||||
| Option | Required | Description |
|
||||
| --- | --- | --- |
|
||||
| `-y, --yes` | Yes | Confirm this destructive operation. |
|
||||
| `-y, --yes` | Yes | Confirm this operation. |
|
||||
|
||||
</CommandTable>
|
||||
|
||||
|
||||
@@ -120,7 +120,7 @@ Delete Tag (personal API key required)
|
||||
|
||||
| Option | Required | Description |
|
||||
| --- | --- | --- |
|
||||
| `-y, --yes` | Yes | Confirm this destructive operation. |
|
||||
| `-y, --yes` | Yes | Confirm this operation. |
|
||||
|
||||
</CommandTable>
|
||||
|
||||
@@ -150,7 +150,7 @@ Remove tag definitions no document still uses (personal API key required)
|
||||
| --- | --- | --- |
|
||||
| `--unused` | No | Whether to remove only the tag definitions no document in the knowledge base still carries a value for. Defaults to true. Pass --no-unused to delete every definition on the knowledge base, which also clears its slot on every document and chunk and is not recoverable. |
|
||||
| `--no-unused` | No | Send --unused as false. |
|
||||
| `-y, --yes` | Yes | Confirm this destructive operation. |
|
||||
| `-y, --yes` | Yes | Confirm this operation. |
|
||||
|
||||
</CommandTable>
|
||||
|
||||
@@ -273,7 +273,7 @@ Enable, disable, or delete many chunks at once (personal API key required)
|
||||
| --- | --- | --- |
|
||||
| `--operation <value>` | Yes | What to do with the selected chunks. Accepted values: `enable`, `disable`, `delete`. |
|
||||
| `--chunk <value...>` | Yes | Chunks to operate on, by identifier. An id naming no chunk in the document is reported in errors and does not fail the request. (space-separated, or @path / @- with one value per line; @@value for a literal leading @). |
|
||||
| `-y, --yes` | Yes | Confirm this destructive operation. |
|
||||
| `-y, --yes` | Yes | Confirm this operation. |
|
||||
|
||||
</CommandTable>
|
||||
|
||||
@@ -334,7 +334,7 @@ Delete Chunk (personal API key required)
|
||||
|
||||
| Option | Required | Description |
|
||||
| --- | --- | --- |
|
||||
| `-y, --yes` | Yes | Confirm this destructive operation. |
|
||||
| `-y, --yes` | Yes | Confirm this operation. |
|
||||
|
||||
</CommandTable>
|
||||
|
||||
@@ -477,7 +477,7 @@ sim knowledge documents delete <knowledgeBaseId> <documentId> [options]
|
||||
|
||||
| Option | Required | Description |
|
||||
| --- | --- | --- |
|
||||
| `-y, --yes` | Yes | Confirm this destructive operation. |
|
||||
| `-y, --yes` | Yes | Confirm this operation. |
|
||||
|
||||
</CommandTable>
|
||||
|
||||
@@ -606,8 +606,8 @@ sim knowledge documents upload <knowledgeBaseId> <path> [options]
|
||||
| --- | --- | --- |
|
||||
| `--name <name>` | No | Store it under a different name. |
|
||||
| `--tag <value...>` | No | Document tags, in tag1 through tag7 order. |
|
||||
| `--recipe <name>` | No | Document processing recipe. |
|
||||
| `--lang <code>` | No | Document language code. |
|
||||
| `--recipe <name>` | No | Document processing recipe. Accepted values: `default`, `plain`, `markdown`, `code`. |
|
||||
| `--lang <code>` | No | Document language tag: hyphen-separated letter and digit subtags, for example en or en-US. |
|
||||
|
||||
</CommandTable>
|
||||
|
||||
@@ -689,7 +689,7 @@ Delete Knowledge Connector (personal API key required)
|
||||
| --- | --- | --- |
|
||||
| `--delete-documents` | No | Also permanently delete documents produced by this connector. |
|
||||
| `--no-delete-documents` | No | Send --delete-documents as false. |
|
||||
| `-y, --yes` | Yes | Confirm this destructive operation. |
|
||||
| `-y, --yes` | Yes | Confirm this operation. |
|
||||
|
||||
</CommandTable>
|
||||
|
||||
@@ -903,11 +903,11 @@ sim knowledge folders delete <path> [options]
|
||||
| Option | Required | Description |
|
||||
| --- | --- | --- |
|
||||
| `--recursive` | No | Delete the folder and its descendants. |
|
||||
| `-y, --yes` | Yes | Confirm this destructive operation. |
|
||||
| `-y, --yes` | Yes | Confirm this operation. |
|
||||
|
||||
</CommandTable>
|
||||
|
||||
## List folders
|
||||
## List knowledge folders
|
||||
|
||||
```bash
|
||||
sim knowledge folders list [options]
|
||||
@@ -969,7 +969,7 @@ sim knowledge delete <knowledgeBaseId> [options]
|
||||
|
||||
| Option | Required | Description |
|
||||
| --- | --- | --- |
|
||||
| `-y, --yes` | Yes | Confirm this destructive operation. |
|
||||
| `-y, --yes` | Yes | Confirm this operation. |
|
||||
|
||||
</CommandTable>
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ sim logs get <runId> [options]
|
||||
|
||||
</CommandTable>
|
||||
|
||||
## Summarize run counts, failures, and cost over a window
|
||||
## Summarize run counts, failures and latency over a window
|
||||
|
||||
```bash
|
||||
sim logs stats [options]
|
||||
|
||||
@@ -58,7 +58,7 @@ sim mcp-servers delete <mcpServerId> [options]
|
||||
|
||||
| Option | Required | Description |
|
||||
| --- | --- | --- |
|
||||
| `-y, --yes` | Yes | Confirm this destructive operation. |
|
||||
| `-y, --yes` | Yes | Confirm this operation. |
|
||||
|
||||
</CommandTable>
|
||||
|
||||
|
||||
@@ -251,7 +251,7 @@ sim billing status [options]
|
||||
|
||||
### sim billing logs
|
||||
|
||||
List credit usage events (a personal API key reports only your own events; a workspace API key reports every member's)
|
||||
List credit usage events (a personal API key reports only your own events; a workspace API key reports every member's in aggregate, unattributed)
|
||||
|
||||
```bash
|
||||
sim billing logs [options]
|
||||
@@ -389,7 +389,7 @@ sim credentials delete <credentialId> [options]
|
||||
|
||||
| Option | Required | Description |
|
||||
| --- | --- | --- |
|
||||
| `-y, --yes` | Yes | Confirm this destructive operation. |
|
||||
| `-y, --yes` | Yes | Confirm this operation. |
|
||||
|
||||
</CommandTable>
|
||||
|
||||
@@ -602,7 +602,7 @@ sim custom-tools delete <customToolId> [options]
|
||||
|
||||
| Option | Required | Description |
|
||||
| --- | --- | --- |
|
||||
| `-y, --yes` | Yes | Confirm this destructive operation. |
|
||||
| `-y, --yes` | Yes | Confirm this operation. |
|
||||
|
||||
</CommandTable>
|
||||
|
||||
@@ -694,7 +694,7 @@ sim files batch-delete [options]
|
||||
| Option | Required | Description |
|
||||
| --- | --- | --- |
|
||||
| `--file-ids <value...>` | Yes | File identifiers to update. (space-separated, or @path / @- with one value per line; @@value for a literal leading @). |
|
||||
| `-y, --yes` | Yes | Confirm this destructive operation. |
|
||||
| `-y, --yes` | Yes | Confirm this operation. |
|
||||
|
||||
</CommandTable>
|
||||
|
||||
@@ -763,13 +763,13 @@ sim files folders delete <path> [options]
|
||||
| Option | Required | Description |
|
||||
| --- | --- | --- |
|
||||
| `--recursive` | No | Delete the folder and its descendants. |
|
||||
| `-y, --yes` | Yes | Confirm this destructive operation. |
|
||||
| `-y, --yes` | Yes | Confirm this operation. |
|
||||
|
||||
</CommandTable>
|
||||
|
||||
### sim files folders list
|
||||
|
||||
List Folders
|
||||
List folders
|
||||
|
||||
```bash
|
||||
sim files folders list [options]
|
||||
@@ -854,7 +854,7 @@ sim files delete <fileId> [options]
|
||||
|
||||
| Option | Required | Description |
|
||||
| --- | --- | --- |
|
||||
| `-y, --yes` | Yes | Confirm this destructive operation. |
|
||||
| `-y, --yes` | Yes | Confirm this operation. |
|
||||
|
||||
</CommandTable>
|
||||
|
||||
@@ -1079,7 +1079,7 @@ sim files unzip <fileId> [options]
|
||||
|
||||
| Option | Required | Description |
|
||||
| --- | --- | --- |
|
||||
| `-y, --yes` | Yes | Confirm this destructive operation. |
|
||||
| `-y, --yes` | Yes | Confirm this operation. |
|
||||
|
||||
</CommandTable>
|
||||
|
||||
@@ -1332,7 +1332,7 @@ sim knowledge tags delete <knowledgeBaseId> <tagId> [options]
|
||||
|
||||
| Option | Required | Description |
|
||||
| --- | --- | --- |
|
||||
| `-y, --yes` | Yes | Confirm this destructive operation. |
|
||||
| `-y, --yes` | Yes | Confirm this operation. |
|
||||
|
||||
</CommandTable>
|
||||
|
||||
@@ -1362,7 +1362,7 @@ sim knowledge tags cleanup <knowledgeBaseId> [options]
|
||||
| --- | --- | --- |
|
||||
| `--unused` | No | Whether to remove only the tag definitions no document in the knowledge base still carries a value for. Defaults to true. Pass --no-unused to delete every definition on the knowledge base, which also clears its slot on every document and chunk and is not recoverable. |
|
||||
| `--no-unused` | No | Send --unused as false. |
|
||||
| `-y, --yes` | Yes | Confirm this destructive operation. |
|
||||
| `-y, --yes` | Yes | Confirm this operation. |
|
||||
|
||||
</CommandTable>
|
||||
|
||||
@@ -1487,7 +1487,7 @@ sim knowledge chunks batch-update <knowledgeBaseId> <documentId> [options]
|
||||
| --- | --- | --- |
|
||||
| `--operation <value>` | Yes | What to do with the selected chunks. Accepted values: `enable`, `disable`, `delete`. |
|
||||
| `--chunk <value...>` | Yes | Chunks to operate on, by identifier. An id naming no chunk in the document is reported in errors and does not fail the request. (space-separated, or @path / @- with one value per line; @@value for a literal leading @). |
|
||||
| `-y, --yes` | Yes | Confirm this destructive operation. |
|
||||
| `-y, --yes` | Yes | Confirm this operation. |
|
||||
|
||||
</CommandTable>
|
||||
|
||||
@@ -1548,7 +1548,7 @@ sim knowledge chunks delete <knowledgeBaseId> <documentId> <chunkId> [options]
|
||||
|
||||
| Option | Required | Description |
|
||||
| --- | --- | --- |
|
||||
| `-y, --yes` | Yes | Confirm this destructive operation. |
|
||||
| `-y, --yes` | Yes | Confirm this operation. |
|
||||
|
||||
</CommandTable>
|
||||
|
||||
@@ -1693,7 +1693,7 @@ sim knowledge documents delete <knowledgeBaseId> <documentId> [options]
|
||||
|
||||
| Option | Required | Description |
|
||||
| --- | --- | --- |
|
||||
| `-y, --yes` | Yes | Confirm this destructive operation. |
|
||||
| `-y, --yes` | Yes | Confirm this operation. |
|
||||
|
||||
</CommandTable>
|
||||
|
||||
@@ -1828,8 +1828,8 @@ sim knowledge documents upload <knowledgeBaseId> <path> [options]
|
||||
| --- | --- | --- |
|
||||
| `--name <name>` | No | Store it under a different name. |
|
||||
| `--tag <value...>` | No | Document tags, in tag1 through tag7 order. |
|
||||
| `--recipe <name>` | No | Document processing recipe. |
|
||||
| `--lang <code>` | No | Document language code. |
|
||||
| `--recipe <name>` | No | Document processing recipe. Accepted values: `default`, `plain`, `markdown`, `code`. |
|
||||
| `--lang <code>` | No | Document language tag: hyphen-separated letter and digit subtags, for example en or en-US. |
|
||||
|
||||
</CommandTable>
|
||||
|
||||
@@ -1913,7 +1913,7 @@ sim knowledge connectors delete <knowledgeBaseId> <connectorId> [options]
|
||||
| --- | --- | --- |
|
||||
| `--delete-documents` | No | Also permanently delete documents produced by this connector. |
|
||||
| `--no-delete-documents` | No | Send --delete-documents as false. |
|
||||
| `-y, --yes` | Yes | Confirm this destructive operation. |
|
||||
| `-y, --yes` | Yes | Confirm this operation. |
|
||||
|
||||
</CommandTable>
|
||||
|
||||
@@ -2131,13 +2131,13 @@ sim knowledge folders delete <path> [options]
|
||||
| Option | Required | Description |
|
||||
| --- | --- | --- |
|
||||
| `--recursive` | No | Delete the folder and its descendants. |
|
||||
| `-y, --yes` | Yes | Confirm this destructive operation. |
|
||||
| `-y, --yes` | Yes | Confirm this operation. |
|
||||
|
||||
</CommandTable>
|
||||
|
||||
### sim knowledge folders list
|
||||
|
||||
List Folders
|
||||
List knowledge folders
|
||||
|
||||
```bash
|
||||
sim knowledge folders list [options]
|
||||
@@ -2203,7 +2203,7 @@ sim knowledge delete <knowledgeBaseId> [options]
|
||||
|
||||
| Option | Required | Description |
|
||||
| --- | --- | --- |
|
||||
| `-y, --yes` | Yes | Confirm this destructive operation. |
|
||||
| `-y, --yes` | Yes | Confirm this operation. |
|
||||
|
||||
</CommandTable>
|
||||
|
||||
@@ -2423,7 +2423,7 @@ sim logs get <runId> [options]
|
||||
|
||||
### sim logs stats
|
||||
|
||||
Summarize run counts, failures, and cost over a window
|
||||
Summarize run counts, failures and latency over a window
|
||||
|
||||
```bash
|
||||
sim logs stats [options]
|
||||
@@ -2565,7 +2565,7 @@ sim mcp-servers delete <mcpServerId> [options]
|
||||
|
||||
| Option | Required | Description |
|
||||
| --- | --- | --- |
|
||||
| `-y, --yes` | Yes | Confirm this destructive operation. |
|
||||
| `-y, --yes` | Yes | Confirm this operation. |
|
||||
|
||||
</CommandTable>
|
||||
|
||||
@@ -2715,7 +2715,7 @@ sim secrets delete <name> [options]
|
||||
| Option | Required | Description |
|
||||
| --- | --- | --- |
|
||||
| `--scope <value>` | Yes | Whether the secret belongs to the workspace or to the caller. A personal secret belongs to the caller across every workspace, not to one workspace. Accepted values: `workspace`, `personal`. |
|
||||
| `-y, --yes` | Yes | Confirm this destructive operation. |
|
||||
| `-y, --yes` | Yes | Confirm this operation. |
|
||||
|
||||
</CommandTable>
|
||||
|
||||
@@ -2821,7 +2821,7 @@ sim skills delete <skillId> [options]
|
||||
|
||||
| Option | Required | Description |
|
||||
| --- | --- | --- |
|
||||
| `-y, --yes` | Yes | Confirm this destructive operation. |
|
||||
| `-y, --yes` | Yes | Confirm this operation. |
|
||||
|
||||
</CommandTable>
|
||||
|
||||
@@ -2926,7 +2926,7 @@ sim skills editors delete <skillId> [options]
|
||||
| Option | Required | Description |
|
||||
| --- | --- | --- |
|
||||
| `--email <value>` | Yes | Email address of a current workspace member. |
|
||||
| `-y, --yes` | Yes | Confirm this destructive operation. |
|
||||
| `-y, --yes` | Yes | Confirm this operation. |
|
||||
|
||||
</CommandTable>
|
||||
|
||||
@@ -3038,7 +3038,7 @@ sim tables columns delete <tableId> [options]
|
||||
| Option | Required | Description |
|
||||
| --- | --- | --- |
|
||||
| `--column-name <value>` | Yes | Name of the column to delete. |
|
||||
| `-y, --yes` | Yes | Confirm this destructive operation. |
|
||||
| `-y, --yes` | Yes | Confirm this operation. |
|
||||
|
||||
</CommandTable>
|
||||
|
||||
@@ -3127,7 +3127,7 @@ sim tables groups delete <tableId> [options]
|
||||
| Option | Required | Description |
|
||||
| --- | --- | --- |
|
||||
| `--group-id <value>` | Yes | Workflow group to delete. |
|
||||
| `-y, --yes` | Yes | Confirm this destructive operation. |
|
||||
| `-y, --yes` | Yes | Confirm this operation. |
|
||||
|
||||
</CommandTable>
|
||||
|
||||
@@ -3204,7 +3204,7 @@ sim tables batch-delete [options]
|
||||
| --- | --- | --- |
|
||||
| `--table-ids <json\|@file>` | No | Tables to archive, by identifier. (JSON, or @path / @- to read a file or stdin). |
|
||||
| `--folder <value...>` | No | Folder path as shown in the app; the leading / is optional (space-separated, or @path / @- with one value per line; @@value for a literal leading @). |
|
||||
| `-y, --yes` | Yes | Confirm this destructive operation. |
|
||||
| `-y, --yes` | Yes | Confirm this operation. |
|
||||
|
||||
</CommandTable>
|
||||
|
||||
@@ -3290,7 +3290,7 @@ sim tables rows delete <tableId> <rowId> [options]
|
||||
|
||||
| Option | Required | Description |
|
||||
| --- | --- | --- |
|
||||
| `-y, --yes` | Yes | Confirm this destructive operation. |
|
||||
| `-y, --yes` | Yes | Confirm this operation. |
|
||||
|
||||
</CommandTable>
|
||||
|
||||
@@ -3321,7 +3321,7 @@ sim tables rows batch-delete <tableId> [options]
|
||||
| `--filter <json\|@file>` | No | Predicate: {"all":[{"field":"status","op":"eq","value":"active"}]}; groups use all/any. Operators: eq, ne, gt, gte, lt, lte, in, nin, contains, ncontains, startsWith, endsWith, like, ilike, nlike, nilike, isEmpty, isNotEmpty, isNull, isNotNull (JSON, or @path / @- to read a file or stdin). |
|
||||
| `--limit <value>` | No | Maximum matching rows to delete. (caps a --filter match only; omit it to act on every match, and note 0 is not accepted). |
|
||||
| `--row <value...>` | No | Explicit row identifiers to delete. (space-separated, or @path / @- with one value per line; @@value for a literal leading @). |
|
||||
| `-y, --yes` | Yes | Confirm this destructive operation. |
|
||||
| `-y, --yes` | Yes | Confirm this operation. |
|
||||
|
||||
</CommandTable>
|
||||
|
||||
@@ -3522,7 +3522,7 @@ sim tables rows batch-update <tableId> [options]
|
||||
| `--filter <json\|@file>` | Yes | Predicate: {"all":[{"field":"status","op":"eq","value":"active"}]}; groups use all/any. Operators: eq, ne, gt, gte, lt, lte, in, nin, contains, ncontains, startsWith, endsWith, like, ilike, nlike, nilike, isEmpty, isNotEmpty, isNull, isNotNull (JSON, or @path / @- to read a file or stdin). |
|
||||
| `--data <json\|@file>` | Yes | Row-data patch applied to every matching row. (JSON, or @path / @- to read a file or stdin). |
|
||||
| `--limit <value>` | No | Maximum matching rows to update. (caps a --filter match only; omit it to act on every match, and note 0 is not accepted). |
|
||||
| `-y, --yes` | Yes | Confirm this destructive operation. |
|
||||
| `-y, --yes` | Yes | Confirm this operation. |
|
||||
|
||||
</CommandTable>
|
||||
|
||||
@@ -3580,7 +3580,7 @@ sim tables dispatches cancel <tableId> <dispatchId> [options]
|
||||
|
||||
| Option | Required | Description |
|
||||
| --- | --- | --- |
|
||||
| `-y, --yes` | Yes | Confirm this destructive operation. |
|
||||
| `-y, --yes` | Yes | Confirm this operation. |
|
||||
|
||||
</CommandTable>
|
||||
|
||||
@@ -3763,7 +3763,7 @@ sim tables imports cancel <importId> [options]
|
||||
|
||||
| Option | Required | Description |
|
||||
| --- | --- | --- |
|
||||
| `-y, --yes` | Yes | Confirm this destructive operation. |
|
||||
| `-y, --yes` | Yes | Confirm this operation. |
|
||||
|
||||
</CommandTable>
|
||||
|
||||
@@ -3813,7 +3813,7 @@ sim tables cancel-runs <tableId> [options]
|
||||
| `--row-id <value>` | No | Row whose runs should be canceled for row scope. |
|
||||
| `--filter <json\|@file>` | No | Predicate: {"all":[{"field":"status","op":"eq","value":"active"}]}; groups use all/any. Operators: eq, ne, gt, gte, lt, lte, in, nin, contains, ncontains, startsWith, endsWith, like, ilike, nlike, nilike, isEmpty, isNotEmpty, isNull, isNotNull (JSON, or @path / @- to read a file or stdin). |
|
||||
| `--exclude-row-ids <value...>` | No | Rows excluded from an all-scope cancellation. (space-separated, or @path / @- with one value per line; @@value for a literal leading @). |
|
||||
| `-y, --yes` | Yes | Confirm this destructive operation. |
|
||||
| `-y, --yes` | Yes | Confirm this operation. |
|
||||
|
||||
</CommandTable>
|
||||
|
||||
@@ -3881,13 +3881,13 @@ sim tables folders delete <path> [options]
|
||||
| Option | Required | Description |
|
||||
| --- | --- | --- |
|
||||
| `--recursive` | No | Delete the folder and its descendants. |
|
||||
| `-y, --yes` | Yes | Confirm this destructive operation. |
|
||||
| `-y, --yes` | Yes | Confirm this operation. |
|
||||
|
||||
</CommandTable>
|
||||
|
||||
### sim tables folders list
|
||||
|
||||
List Folders
|
||||
List table folders
|
||||
|
||||
```bash
|
||||
sim tables folders list [options]
|
||||
@@ -4001,7 +4001,7 @@ sim tables views delete <tableId> <viewId> [options]
|
||||
|
||||
| Option | Required | Description |
|
||||
| --- | --- | --- |
|
||||
| `-y, --yes` | Yes | Confirm this destructive operation. |
|
||||
| `-y, --yes` | Yes | Confirm this operation. |
|
||||
|
||||
</CommandTable>
|
||||
|
||||
@@ -4099,7 +4099,7 @@ sim tables delete <tableId> [options]
|
||||
|
||||
| Option | Required | Description |
|
||||
| --- | --- | --- |
|
||||
| `-y, --yes` | Yes | Confirm this destructive operation. |
|
||||
| `-y, --yes` | Yes | Confirm this operation. |
|
||||
|
||||
</CommandTable>
|
||||
|
||||
@@ -4455,7 +4455,7 @@ sim workflow-mcp-servers delete <serverId> [options]
|
||||
|
||||
| Option | Required | Description |
|
||||
| --- | --- | --- |
|
||||
| `-y, --yes` | Yes | Confirm this destructive operation. |
|
||||
| `-y, --yes` | Yes | Confirm this operation. |
|
||||
|
||||
</CommandTable>
|
||||
|
||||
@@ -4533,7 +4533,7 @@ sim workflow-mcp-servers tools delete <serverId> <workflowId> [options]
|
||||
|
||||
| Option | Required | Description |
|
||||
| --- | --- | --- |
|
||||
| `-y, --yes` | Yes | Confirm this destructive operation. |
|
||||
| `-y, --yes` | Yes | Confirm this operation. |
|
||||
|
||||
</CommandTable>
|
||||
|
||||
@@ -4655,12 +4655,12 @@ sim workflows operations apply <workflowId> [options]
|
||||
| --- | --- | --- |
|
||||
| `--dry-run` | No | Validate and lint without persisting. The response is identical to the committed write of the same body, so a caller can inspect `lint` and then re-send the request for real. Nothing is written, no audit entry is recorded, and collaborators are not notified. |
|
||||
| `--no-dry-run` | No | Send --dry-run as false. |
|
||||
| `--operations <json\|@file>` | Yes | Edits to apply, in a single batch, keyed by operation_type: [{"operation_type":"add","block_id":"my-fn","params":{"type":"function","name":"My Fn","inputs":{"code":"return {ok:true}"}}},{"operation_type":"edit","block_id":"<uuid>","params":{"name":"Renamed","connections":{"success":"my-fn"}}},{"operation_type":"delete","block_id":"<uuid>"}]. Also insert_into_subflow and extract_from_subflow, whose params carry {"subflowId":"<loop-id>"} (JSON, or @path / @- to read a file or stdin). |
|
||||
| `--operations <json\|@file>` | Yes | Edits to apply, in a single batch, keyed by operation_type: [{"operation_type":"add","block_id":"my-fn","params":{"type":"function","name":"My Fn","inputs":{"code":"return {ok:true}"}}},{"operation_type":"edit","block_id":"<uuid>","params":{"name":"Renamed","connections":{"success":"my-fn"}}},{"operation_type":"delete","block_id":"<uuid>"}]. Also extract_from_subflow, whose params carry {"subflowId":"<loop-id>"}, and insert_into_subflow, which creates a block and so takes an add’s params plus that subflowId (JSON, or @path / @- to read a file or stdin). |
|
||||
| `--atomic` | No | Fail the whole batch when any operation is declined or any block input would be dropped. The default applies what it can and reports the rest in `skipped` and `inputValidationErrors`; `true` writes nothing and answers `409` instead. |
|
||||
| `--no-atomic` | No | Send --atomic as false. |
|
||||
| `--layout <value>` | No | Whether to reposition blocks the batch touched. `targeted` (default) nudges only the affected subgraph; `none` leaves every position exactly as supplied. Accepted values: `targeted`, `none`. |
|
||||
| `--set-block-enabled <json\|@file>` | No | Blocks to enable or disable, applied after --operations: [{"block_id":"<uuid>","enabled":false}]. Disabling a loop or parallel cascades to its unlocked descendants; enabling a block whose container is disabled is declined (JSON, or @path / @- to read a file or stdin). |
|
||||
| `-y, --yes` | No | Confirm this destructive operation (required unless --dry-run). |
|
||||
| `-y, --yes` | No | Confirm this operation (required unless --dry-run). |
|
||||
|
||||
</CommandTable>
|
||||
|
||||
@@ -4689,7 +4689,7 @@ sim workflows variables update <workflowId> [options]
|
||||
| Option | Required | Description |
|
||||
| --- | --- | --- |
|
||||
| `--operations <json\|@file>` | Yes | Variable changes to apply in order, keyed by operation: [{"operation":"add","name":"my_var","type":"string","value":"hello"},{"operation":"edit","name":"my_var","value":"updated"},{"operation":"delete","name":"my_var"}] (JSON, or @path / @- to read a file or stdin). |
|
||||
| `-y, --yes` | Yes | Confirm this destructive operation. |
|
||||
| `-y, --yes` | Yes | Confirm this operation. |
|
||||
|
||||
</CommandTable>
|
||||
|
||||
@@ -4900,13 +4900,13 @@ sim workflows folders delete <path> [options]
|
||||
| Option | Required | Description |
|
||||
| --- | --- | --- |
|
||||
| `--recursive` | No | Delete the folder and its descendants. |
|
||||
| `-y, --yes` | Yes | Confirm this destructive operation. |
|
||||
| `-y, --yes` | Yes | Confirm this operation. |
|
||||
|
||||
</CommandTable>
|
||||
|
||||
### sim workflows folders list
|
||||
|
||||
List Workflow Folders
|
||||
List workflow folders
|
||||
|
||||
```bash
|
||||
sim workflows folders list [options]
|
||||
@@ -4972,7 +4972,7 @@ sim workflows delete <workflowId> [options]
|
||||
|
||||
| Option | Required | Description |
|
||||
| --- | --- | --- |
|
||||
| `-y, --yes` | Yes | Confirm this destructive operation. |
|
||||
| `-y, --yes` | Yes | Confirm this operation. |
|
||||
|
||||
</CommandTable>
|
||||
|
||||
@@ -5000,7 +5000,7 @@ sim workflows chat unpublish <workflowId> [options]
|
||||
|
||||
| Option | Required | Description |
|
||||
| --- | --- | --- |
|
||||
| `-y, --yes` | Yes | Confirm this destructive operation. |
|
||||
| `-y, --yes` | Yes | Confirm this operation. |
|
||||
|
||||
</CommandTable>
|
||||
|
||||
@@ -5058,7 +5058,7 @@ sim workflows chat publish <workflowId> [options]
|
||||
| `--no-include-thinking` | No | Send --include-thinking as false. |
|
||||
| `--include-tool-calls` | No | Allow visitors to receive tool lifecycle events. |
|
||||
| `--no-include-tool-calls` | No | Send --include-tool-calls as false. |
|
||||
| `-y, --yes` | Yes | Confirm this destructive operation. |
|
||||
| `-y, --yes` | Yes | Confirm this operation. |
|
||||
|
||||
</CommandTable>
|
||||
|
||||
@@ -5294,7 +5294,7 @@ sim workflows state replace <workflowId> [options]
|
||||
| `--loops <json\|@file>` | No | Ignored on write: loop containers are recomputed from `blocks`. (JSON, or @path / @- to read a file or stdin). |
|
||||
| `--parallels <json\|@file>` | No | Ignored on write: parallel containers are recomputed from `blocks`. (JSON, or @path / @- to read a file or stdin). |
|
||||
| `--variables <json\|@file>` | No | Replacement variable set. Omit to leave the stored variables untouched. (JSON, or @path / @- to read a file or stdin). |
|
||||
| `-y, --yes` | No | Confirm this destructive operation (required unless --dry-run). |
|
||||
| `-y, --yes` | No | Confirm this operation (required unless --dry-run). |
|
||||
|
||||
</CommandTable>
|
||||
|
||||
@@ -5483,7 +5483,7 @@ sim workflows revert create <workflowId> <version> [options]
|
||||
|
||||
| Option | Required | Description |
|
||||
| --- | --- | --- |
|
||||
| `-y, --yes` | Yes | Confirm this destructive operation. |
|
||||
| `-y, --yes` | Yes | Confirm this operation. |
|
||||
|
||||
</CommandTable>
|
||||
|
||||
@@ -5512,7 +5512,7 @@ sim workflows rollback <workflowId> [options]
|
||||
| Option | Required | Description |
|
||||
| --- | --- | --- |
|
||||
| `--to-version <value>` | No | Deployment version to reactivate. Omit to select the previous active version. |
|
||||
| `-y, --yes` | Yes | Confirm this destructive operation. |
|
||||
| `-y, --yes` | Yes | Confirm this operation. |
|
||||
|
||||
</CommandTable>
|
||||
|
||||
@@ -5540,7 +5540,7 @@ sim workflows undeploy <workflowId> [options]
|
||||
|
||||
| Option | Required | Description |
|
||||
| --- | --- | --- |
|
||||
| `-y, --yes` | Yes | Confirm this destructive operation. |
|
||||
| `-y, --yes` | Yes | Confirm this operation. |
|
||||
|
||||
</CommandTable>
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ Delete Secret (personal API key required)
|
||||
| Option | Required | Description |
|
||||
| --- | --- | --- |
|
||||
| `--scope <value>` | Yes | Whether the secret belongs to the workspace or to the caller. A personal secret belongs to the caller across every workspace, not to one workspace. Accepted values: `workspace`, `personal`. |
|
||||
| `-y, --yes` | Yes | Confirm this destructive operation. |
|
||||
| `-y, --yes` | Yes | Confirm this operation. |
|
||||
|
||||
</CommandTable>
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ Delete Skill (personal API key required)
|
||||
|
||||
| Option | Required | Description |
|
||||
| --- | --- | --- |
|
||||
| `-y, --yes` | Yes | Confirm this destructive operation. |
|
||||
| `-y, --yes` | Yes | Confirm this operation. |
|
||||
|
||||
</CommandTable>
|
||||
|
||||
@@ -154,7 +154,7 @@ Revoke Skill Editor (personal API key required)
|
||||
| Option | Required | Description |
|
||||
| --- | --- | --- |
|
||||
| `--email <value>` | Yes | Email address of a current workspace member. |
|
||||
| `-y, --yes` | Yes | Confirm this destructive operation. |
|
||||
| `-y, --yes` | Yes | Confirm this operation. |
|
||||
|
||||
</CommandTable>
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ sim tables columns delete <tableId> [options]
|
||||
| Option | Required | Description |
|
||||
| --- | --- | --- |
|
||||
| `--column-name <value>` | Yes | Name of the column to delete. |
|
||||
| `-y, --yes` | Yes | Confirm this destructive operation. |
|
||||
| `-y, --yes` | Yes | Confirm this operation. |
|
||||
|
||||
</CommandTable>
|
||||
|
||||
@@ -141,7 +141,7 @@ sim tables groups delete <tableId> [options]
|
||||
| Option | Required | Description |
|
||||
| --- | --- | --- |
|
||||
| `--group-id <value>` | Yes | Workflow group to delete. |
|
||||
| `-y, --yes` | Yes | Confirm this destructive operation. |
|
||||
| `-y, --yes` | Yes | Confirm this operation. |
|
||||
|
||||
</CommandTable>
|
||||
|
||||
@@ -212,7 +212,7 @@ sim tables batch-delete [options]
|
||||
| --- | --- | --- |
|
||||
| `--table-ids <json\|@file>` | No | Tables to archive, by identifier. (JSON, or @path / @- to read a file or stdin). |
|
||||
| `--folder <value...>` | No | Folder path as shown in the app; the leading / is optional (space-separated, or @path / @- with one value per line; @@value for a literal leading @). |
|
||||
| `-y, --yes` | Yes | Confirm this destructive operation. |
|
||||
| `-y, --yes` | Yes | Confirm this operation. |
|
||||
|
||||
</CommandTable>
|
||||
|
||||
@@ -292,7 +292,7 @@ sim tables rows delete <tableId> <rowId> [options]
|
||||
|
||||
| Option | Required | Description |
|
||||
| --- | --- | --- |
|
||||
| `-y, --yes` | Yes | Confirm this destructive operation. |
|
||||
| `-y, --yes` | Yes | Confirm this operation. |
|
||||
|
||||
</CommandTable>
|
||||
|
||||
@@ -321,7 +321,7 @@ sim tables rows batch-delete <tableId> [options]
|
||||
| `--filter <json\|@file>` | No | Predicate: {"all":[{"field":"status","op":"eq","value":"active"}]}; groups use all/any. Operators: eq, ne, gt, gte, lt, lte, in, nin, contains, ncontains, startsWith, endsWith, like, ilike, nlike, nilike, isEmpty, isNotEmpty, isNull, isNotNull (JSON, or @path / @- to read a file or stdin). |
|
||||
| `--limit <value>` | No | Maximum matching rows to delete. (caps a --filter match only; omit it to act on every match, and note 0 is not accepted). |
|
||||
| `--row <value...>` | No | Explicit row identifiers to delete. (space-separated, or @path / @- with one value per line; @@value for a literal leading @). |
|
||||
| `-y, --yes` | Yes | Confirm this destructive operation. |
|
||||
| `-y, --yes` | Yes | Confirm this operation. |
|
||||
|
||||
</CommandTable>
|
||||
|
||||
@@ -508,7 +508,7 @@ sim tables rows batch-update <tableId> [options]
|
||||
| `--filter <json\|@file>` | Yes | Predicate: {"all":[{"field":"status","op":"eq","value":"active"}]}; groups use all/any. Operators: eq, ne, gt, gte, lt, lte, in, nin, contains, ncontains, startsWith, endsWith, like, ilike, nlike, nilike, isEmpty, isNotEmpty, isNull, isNotNull (JSON, or @path / @- to read a file or stdin). |
|
||||
| `--data <json\|@file>` | Yes | Row-data patch applied to every matching row. (JSON, or @path / @- to read a file or stdin). |
|
||||
| `--limit <value>` | No | Maximum matching rows to update. (caps a --filter match only; omit it to act on every match, and note 0 is not accepted). |
|
||||
| `-y, --yes` | Yes | Confirm this destructive operation. |
|
||||
| `-y, --yes` | Yes | Confirm this operation. |
|
||||
|
||||
</CommandTable>
|
||||
|
||||
@@ -562,7 +562,7 @@ sim tables dispatches cancel <tableId> <dispatchId> [options]
|
||||
|
||||
| Option | Required | Description |
|
||||
| --- | --- | --- |
|
||||
| `-y, --yes` | Yes | Confirm this destructive operation. |
|
||||
| `-y, --yes` | Yes | Confirm this operation. |
|
||||
|
||||
</CommandTable>
|
||||
|
||||
@@ -729,7 +729,7 @@ sim tables imports cancel <importId> [options]
|
||||
|
||||
| Option | Required | Description |
|
||||
| --- | --- | --- |
|
||||
| `-y, --yes` | Yes | Confirm this destructive operation. |
|
||||
| `-y, --yes` | Yes | Confirm this operation. |
|
||||
|
||||
</CommandTable>
|
||||
|
||||
@@ -775,7 +775,7 @@ sim tables cancel-runs <tableId> [options]
|
||||
| `--row-id <value>` | No | Row whose runs should be canceled for row scope. |
|
||||
| `--filter <json\|@file>` | No | Predicate: {"all":[{"field":"status","op":"eq","value":"active"}]}; groups use all/any. Operators: eq, ne, gt, gte, lt, lte, in, nin, contains, ncontains, startsWith, endsWith, like, ilike, nlike, nilike, isEmpty, isNotEmpty, isNull, isNotNull (JSON, or @path / @- to read a file or stdin). |
|
||||
| `--exclude-row-ids <value...>` | No | Rows excluded from an all-scope cancellation. (space-separated, or @path / @- with one value per line; @@value for a literal leading @). |
|
||||
| `-y, --yes` | Yes | Confirm this destructive operation. |
|
||||
| `-y, --yes` | Yes | Confirm this operation. |
|
||||
|
||||
</CommandTable>
|
||||
|
||||
@@ -837,11 +837,11 @@ sim tables folders delete <path> [options]
|
||||
| Option | Required | Description |
|
||||
| --- | --- | --- |
|
||||
| `--recursive` | No | Delete the folder and its descendants. |
|
||||
| `-y, --yes` | Yes | Confirm this destructive operation. |
|
||||
| `-y, --yes` | Yes | Confirm this operation. |
|
||||
|
||||
</CommandTable>
|
||||
|
||||
## List folders
|
||||
## List table folders
|
||||
|
||||
```bash
|
||||
sim tables folders list [options]
|
||||
@@ -947,7 +947,7 @@ sim tables views delete <tableId> <viewId> [options]
|
||||
|
||||
| Option | Required | Description |
|
||||
| --- | --- | --- |
|
||||
| `-y, --yes` | Yes | Confirm this destructive operation. |
|
||||
| `-y, --yes` | Yes | Confirm this operation. |
|
||||
|
||||
</CommandTable>
|
||||
|
||||
@@ -1037,7 +1037,7 @@ sim tables delete <tableId> [options]
|
||||
|
||||
| Option | Required | Description |
|
||||
| --- | --- | --- |
|
||||
| `-y, --yes` | Yes | Confirm this destructive operation. |
|
||||
| `-y, --yes` | Yes | Confirm this operation. |
|
||||
|
||||
</CommandTable>
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ Delete Workflow MCP Server (personal API key required)
|
||||
|
||||
| Option | Required | Description |
|
||||
| --- | --- | --- |
|
||||
| `-y, --yes` | Yes | Confirm this destructive operation. |
|
||||
| `-y, --yes` | Yes | Confirm this operation. |
|
||||
|
||||
</CommandTable>
|
||||
|
||||
@@ -131,7 +131,7 @@ Unpublish Workflow MCP Tool (personal API key required)
|
||||
|
||||
| Option | Required | Description |
|
||||
| --- | --- | --- |
|
||||
| `-y, --yes` | Yes | Confirm this destructive operation. |
|
||||
| `-y, --yes` | Yes | Confirm this operation. |
|
||||
|
||||
</CommandTable>
|
||||
|
||||
|
||||
@@ -54,12 +54,12 @@ Apply Workflow Operations (personal API key required)
|
||||
| --- | --- | --- |
|
||||
| `--dry-run` | No | Validate and lint without persisting. The response is identical to the committed write of the same body, so a caller can inspect `lint` and then re-send the request for real. Nothing is written, no audit entry is recorded, and collaborators are not notified. |
|
||||
| `--no-dry-run` | No | Send --dry-run as false. |
|
||||
| `--operations <json\|@file>` | Yes | Edits to apply, in a single batch, keyed by operation_type: [{"operation_type":"add","block_id":"my-fn","params":{"type":"function","name":"My Fn","inputs":{"code":"return {ok:true}"}}},{"operation_type":"edit","block_id":"<uuid>","params":{"name":"Renamed","connections":{"success":"my-fn"}}},{"operation_type":"delete","block_id":"<uuid>"}]. Also insert_into_subflow and extract_from_subflow, whose params carry {"subflowId":"<loop-id>"} (JSON, or @path / @- to read a file or stdin). |
|
||||
| `--operations <json\|@file>` | Yes | Edits to apply, in a single batch, keyed by operation_type: [{"operation_type":"add","block_id":"my-fn","params":{"type":"function","name":"My Fn","inputs":{"code":"return {ok:true}"}}},{"operation_type":"edit","block_id":"<uuid>","params":{"name":"Renamed","connections":{"success":"my-fn"}}},{"operation_type":"delete","block_id":"<uuid>"}]. Also extract_from_subflow, whose params carry {"subflowId":"<loop-id>"}, and insert_into_subflow, which creates a block and so takes an add’s params plus that subflowId (JSON, or @path / @- to read a file or stdin). |
|
||||
| `--atomic` | No | Fail the whole batch when any operation is declined or any block input would be dropped. The default applies what it can and reports the rest in `skipped` and `inputValidationErrors`; `true` writes nothing and answers `409` instead. |
|
||||
| `--no-atomic` | No | Send --atomic as false. |
|
||||
| `--layout <value>` | No | Whether to reposition blocks the batch touched. `targeted` (default) nudges only the affected subgraph; `none` leaves every position exactly as supplied. Accepted values: `targeted`, `none`. |
|
||||
| `--set-block-enabled <json\|@file>` | No | Blocks to enable or disable, applied after --operations: [{"block_id":"<uuid>","enabled":false}]. Disabling a loop or parallel cascades to its unlocked descendants; enabling a block whose container is disabled is declined (JSON, or @path / @- to read a file or stdin). |
|
||||
| `-y, --yes` | No | Confirm this destructive operation (required unless --dry-run). |
|
||||
| `-y, --yes` | No | Confirm this operation (required unless --dry-run). |
|
||||
|
||||
</CommandTable>
|
||||
|
||||
@@ -86,7 +86,7 @@ sim workflows variables update <workflowId> [options]
|
||||
| Option | Required | Description |
|
||||
| --- | --- | --- |
|
||||
| `--operations <json\|@file>` | Yes | Variable changes to apply in order, keyed by operation: [{"operation":"add","name":"my_var","type":"string","value":"hello"},{"operation":"edit","name":"my_var","value":"updated"},{"operation":"delete","name":"my_var"}] (JSON, or @path / @- to read a file or stdin). |
|
||||
| `-y, --yes` | Yes | Confirm this destructive operation. |
|
||||
| `-y, --yes` | Yes | Confirm this operation. |
|
||||
|
||||
</CommandTable>
|
||||
|
||||
@@ -285,7 +285,7 @@ sim workflows folders delete <path> [options]
|
||||
| Option | Required | Description |
|
||||
| --- | --- | --- |
|
||||
| `--recursive` | No | Delete the folder and its descendants. |
|
||||
| `-y, --yes` | Yes | Confirm this destructive operation. |
|
||||
| `-y, --yes` | Yes | Confirm this operation. |
|
||||
|
||||
</CommandTable>
|
||||
|
||||
@@ -351,7 +351,7 @@ sim workflows delete <workflowId> [options]
|
||||
|
||||
| Option | Required | Description |
|
||||
| --- | --- | --- |
|
||||
| `-y, --yes` | Yes | Confirm this destructive operation. |
|
||||
| `-y, --yes` | Yes | Confirm this operation. |
|
||||
|
||||
</CommandTable>
|
||||
|
||||
@@ -379,7 +379,7 @@ Take a workflow’s chat deployment offline (personal API key required)
|
||||
|
||||
| Option | Required | Description |
|
||||
| --- | --- | --- |
|
||||
| `-y, --yes` | Yes | Confirm this destructive operation. |
|
||||
| `-y, --yes` | Yes | Confirm this operation. |
|
||||
|
||||
</CommandTable>
|
||||
|
||||
@@ -437,7 +437,7 @@ Publish or replace a workflow’s chat deployment (personal API key required)
|
||||
| `--no-include-thinking` | No | Send --include-thinking as false. |
|
||||
| `--include-tool-calls` | No | Allow visitors to receive tool lifecycle events. |
|
||||
| `--no-include-tool-calls` | No | Send --include-tool-calls as false. |
|
||||
| `-y, --yes` | Yes | Confirm this destructive operation. |
|
||||
| `-y, --yes` | Yes | Confirm this operation. |
|
||||
|
||||
</CommandTable>
|
||||
|
||||
@@ -661,7 +661,7 @@ Replace Workflow State (personal API key required)
|
||||
| `--loops <json\|@file>` | No | Ignored on write: loop containers are recomputed from `blocks`. (JSON, or @path / @- to read a file or stdin). |
|
||||
| `--parallels <json\|@file>` | No | Ignored on write: parallel containers are recomputed from `blocks`. (JSON, or @path / @- to read a file or stdin). |
|
||||
| `--variables <json\|@file>` | No | Replacement variable set. Omit to leave the stored variables untouched. (JSON, or @path / @- to read a file or stdin). |
|
||||
| `-y, --yes` | No | Confirm this destructive operation (required unless --dry-run). |
|
||||
| `-y, --yes` | No | Confirm this operation (required unless --dry-run). |
|
||||
|
||||
</CommandTable>
|
||||
|
||||
@@ -836,7 +836,7 @@ Revert Workflow To Version (personal API key required)
|
||||
|
||||
| Option | Required | Description |
|
||||
| --- | --- | --- |
|
||||
| `-y, --yes` | Yes | Confirm this destructive operation. |
|
||||
| `-y, --yes` | Yes | Confirm this operation. |
|
||||
|
||||
</CommandTable>
|
||||
|
||||
@@ -865,7 +865,7 @@ Rollback Workflow (personal API key required)
|
||||
| Option | Required | Description |
|
||||
| --- | --- | --- |
|
||||
| `--to-version <value>` | No | Deployment version to reactivate. Omit to select the previous active version. |
|
||||
| `-y, --yes` | Yes | Confirm this destructive operation. |
|
||||
| `-y, --yes` | Yes | Confirm this operation. |
|
||||
|
||||
</CommandTable>
|
||||
|
||||
@@ -893,7 +893,7 @@ Take a workflow out of deployment (personal API key required)
|
||||
|
||||
| Option | Required | Description |
|
||||
| --- | --- | --- |
|
||||
| `-y, --yes` | Yes | Confirm this destructive operation. |
|
||||
| `-y, --yes` | Yes | Confirm this operation. |
|
||||
|
||||
</CommandTable>
|
||||
|
||||
|
||||
@@ -173,6 +173,17 @@ describe('configure and the root globals', () => {
|
||||
expect(readConfigProfile('default')).toEqual({})
|
||||
})
|
||||
|
||||
/**
|
||||
* The refusal prints a command for the caller to run, so an unredacted value
|
||||
* carrying U+2028 rendered as a second line that reads like a suggestion of
|
||||
* its own.
|
||||
*/
|
||||
it('redacts a control character out of the command it suggests', async () => {
|
||||
await expect(run('--endpoint', 'https://a.example\u2028sim login --api-key x')).rejects.toThrow(
|
||||
'sim configure --set-endpoint https://a.example sim login --api-key x'
|
||||
)
|
||||
})
|
||||
|
||||
it('refuses -w and --output the same way', async () => {
|
||||
await expect(run('-w', 'ws_9')).rejects.toThrow('sim configure --set-workspace ws_9')
|
||||
await expect(run('--output', 'json')).rejects.toThrow('sim configure --set-output json')
|
||||
|
||||
@@ -70,7 +70,7 @@ export function configureCommand(): Command {
|
||||
const value = globals[option]
|
||||
if (value === undefined) continue
|
||||
throw new SimApiError(
|
||||
`${flag} applies to a single command and is not stored. To save it, run: sim configure ${setFlag} ${value}`,
|
||||
`${flag} applies to a single command and is not stored. To save it, run: sim configure ${setFlag} ${redact(value)}`,
|
||||
0
|
||||
)
|
||||
}
|
||||
|
||||
@@ -100,6 +100,9 @@ function written(spy: WriteSpy): string {
|
||||
return spy.mock.calls.map((call) => String(call[0])).join('')
|
||||
}
|
||||
|
||||
/** A conversation id in the shape the route accepts and the command prints. */
|
||||
const CONVERSATION_ID = '3f2a1c4e-0000-4000-8000-000000000000'
|
||||
|
||||
const FINAL = {
|
||||
type: 'final',
|
||||
data: { content: 'Hello there', conversationId: 'conv-1', model: 'sim' },
|
||||
@@ -127,17 +130,35 @@ describe('sim chat', () => {
|
||||
expect(written(stderr)).toContain('conversation: conv-1')
|
||||
})
|
||||
|
||||
/**
|
||||
* The route's own refusals name `message` and `conversationId`, and this
|
||||
* command builds its request by hand so nothing retypes them into what the
|
||||
* caller typed. A blank `-c` was worse than misnamed: it is falsy, so it was
|
||||
* dropped from the body and silently started a NEW conversation.
|
||||
*/
|
||||
it('refuses a blank message and a blank -c before the request', async () => {
|
||||
await expect(run(' ')).rejects.toThrow('<message> cannot be empty')
|
||||
await expect(run('-c', '', 'hello')).rejects.toThrow('-c/--conversation cannot be empty')
|
||||
await expect(run('-c', ' ', 'hello')).rejects.toThrow('-c/--conversation cannot be empty')
|
||||
expect(requestRaw).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
/**
|
||||
* A conversation id as the command prints it. The shape is the route's rule
|
||||
* to enforce — the CLI refuses only a blank `-c`, which is falsy and would
|
||||
* otherwise be dropped from the body and start a new conversation.
|
||||
*/
|
||||
it('passes -c through as the conversation to continue', async () => {
|
||||
requestRaw.mockResolvedValue(ndjson([FINAL]))
|
||||
|
||||
await run('-c', 'conv-1', 'And which run on a schedule?')
|
||||
await run('-c', CONVERSATION_ID, 'And which run on a schedule?')
|
||||
|
||||
expect(requestRaw).toHaveBeenCalledWith('/api/v2/chat', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
workspaceId: 'ws_local',
|
||||
message: 'And which run on a schedule?',
|
||||
conversationId: 'conv-1',
|
||||
conversationId: CONVERSATION_ID,
|
||||
},
|
||||
headers: { accept: 'application/x-ndjson' },
|
||||
})
|
||||
|
||||
@@ -142,6 +142,7 @@ function ignoreBrokenPipe(stream: NodeJS.WriteStream): () => void {
|
||||
* proxies from idling the connection out. The generated `chat` operation is
|
||||
* hidden in the CLI contract in favour of this command.
|
||||
*/
|
||||
|
||||
export function attachChat(program: Command): void {
|
||||
program
|
||||
.command('chat')
|
||||
@@ -164,6 +165,24 @@ Examples:
|
||||
`
|
||||
)
|
||||
.action(async (message: string, options: ChatOptions, command: Command) => {
|
||||
/**
|
||||
* Refused here so the refusal names what the caller typed: the route
|
||||
* answers in its own field names, and this command builds its request by
|
||||
* hand, so nothing retypes them into `<message>` and `-c/--conversation`.
|
||||
* A blank `-c` was worse than misnamed — it is falsy, so it was dropped
|
||||
* from the body and silently started a NEW conversation instead of
|
||||
* continuing one.
|
||||
*/
|
||||
if (message.trim() === '') {
|
||||
throw new SimApiError('<message> cannot be empty', 0)
|
||||
}
|
||||
if (options.conversation !== undefined && options.conversation.trim() === '') {
|
||||
throw new SimApiError(
|
||||
'-c/--conversation cannot be empty — pass the conversation id printed on stderr after each turn',
|
||||
0
|
||||
)
|
||||
}
|
||||
|
||||
const { client, profile } = clientFrom(command)
|
||||
const workspaceId = client.requireWorkspace()
|
||||
|
||||
|
||||
@@ -213,6 +213,41 @@ describe('knowledge documents upload', () => {
|
||||
expect(mockRequest).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
/**
|
||||
* The route enforces both, and neither said so: the help read "Document
|
||||
* processing recipe" / "Document language code" and the CLI uploaded the file
|
||||
* before the server refused the value. Every other constrained flag in this
|
||||
* CLI uses commander `choices`.
|
||||
*/
|
||||
it('states what --recipe and --lang accept, and refuses a recipe before uploading', async () => {
|
||||
const path = join(dir, 'notes.txt')
|
||||
writeFileSync(path, 'hello')
|
||||
|
||||
const help = program()
|
||||
.commands.find((command) => command.name() === 'knowledge')
|
||||
?.commands.find((command) => command.name() === 'documents')
|
||||
?.commands.find((command) => command.name() === 'upload')
|
||||
?.helpInformation()
|
||||
.replace(/\s+/g, ' ')
|
||||
expect(help).toContain('choices: "default", "plain", "markdown", "code"')
|
||||
expect(help).toContain('hyphen-separated letter and digit subtags')
|
||||
|
||||
await expect(
|
||||
program().parseAsync([
|
||||
'node',
|
||||
'sim',
|
||||
'kb',
|
||||
'documents',
|
||||
'upload',
|
||||
'kb_1',
|
||||
path,
|
||||
'--recipe',
|
||||
'super-chunker-9000',
|
||||
])
|
||||
).rejects.toThrow(/super-chunker-9000/)
|
||||
expect(mockRequest).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('requires the knowledge-base argument before reading the file', async () => {
|
||||
const path = join(dir, 'notes.txt')
|
||||
writeFileSync(path, 'hello')
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import type { Command } from 'commander'
|
||||
import { type Command, Option } from 'commander'
|
||||
import { clientFrom } from '../../context'
|
||||
import type {
|
||||
CompleteKnowledgeDocumentUploadResponse,
|
||||
CreateKnowledgeDocumentUploadBody,
|
||||
CreateKnowledgeDocumentUploadResponse,
|
||||
} from '../../generated/v2-api'
|
||||
import { SimApiError } from '../../http/client'
|
||||
@@ -35,6 +36,29 @@ function uploadMetadata(options: KnowledgeDocumentUploadOptions): Record<string,
|
||||
return metadata
|
||||
}
|
||||
|
||||
/**
|
||||
* The recipes the route accepts. Stated as `choices` like every other
|
||||
* constrained flag in this CLI, so `--help` lists them and a typo is refused
|
||||
* before the file is uploaded rather than after.
|
||||
*/
|
||||
type UploadRecipe = NonNullable<
|
||||
NonNullable<CreateKnowledgeDocumentUploadBody['processingOptions']>['recipe']
|
||||
>
|
||||
const UPLOAD_RECIPES = [
|
||||
'default',
|
||||
'plain',
|
||||
'markdown',
|
||||
'code',
|
||||
] as const satisfies readonly UploadRecipe[]
|
||||
|
||||
/**
|
||||
* The route enforces a shape, not BCP-47 conformance, so the help says the
|
||||
* shape and nothing more; a full parser is the route's own deliberate
|
||||
* non-goal and reimplementing one here would refuse tags the server accepts.
|
||||
*/
|
||||
const LANGUAGE_TAG_HELP =
|
||||
'Document language tag: hyphen-separated letter and digit subtags, for example en or en-US'
|
||||
|
||||
export function attachKnowledgeDocumentUpload(documents: Command): void {
|
||||
documents
|
||||
.command('upload')
|
||||
@@ -44,8 +68,8 @@ export function attachKnowledgeDocumentUpload(documents: Command): void {
|
||||
.description('Upload a document to a knowledge base')
|
||||
.option('--name <name>', 'Store it under a different name')
|
||||
.option('--tag <value...>', 'Document tags, in tag1 through tag7 order')
|
||||
.option('--recipe <name>', 'Document processing recipe')
|
||||
.option('--lang <code>', 'Document language code')
|
||||
.addOption(new Option('--recipe <name>', 'Document processing recipe').choices(UPLOAD_RECIPES))
|
||||
.option('--lang <code>', LANGUAGE_TAG_HELP)
|
||||
.action(
|
||||
async (
|
||||
knowledgeBaseId: string,
|
||||
|
||||
@@ -454,7 +454,7 @@ describe('sim logs follow', () => {
|
||||
it('rejects a backlog count that is not a whole number of runs', async () => {
|
||||
respondWith([])
|
||||
|
||||
await expect(follow('-n', '-1')).rejects.toThrow('--lines must be a non-negative integer')
|
||||
await expect(follow('-n', '-1')).rejects.toThrow('--lines must be a whole number of 0 or more')
|
||||
expect(mockRequest).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
@@ -517,4 +517,27 @@ describe('sim logs follow', () => {
|
||||
expect(ms).toBeGreaterThan(0)
|
||||
}
|
||||
})
|
||||
/**
|
||||
* Root help states that a `wf_` prefix marks a FILE id and "never names a
|
||||
* workflow", so the example told the reader to pass a file id to
|
||||
* `--workflow`. Workflow ids are bare UUIDs.
|
||||
*/
|
||||
it('does not illustrate --workflow with a file-id prefix', () => {
|
||||
const root = new Command('sim').exitOverride()
|
||||
const logs = new Command('logs').exitOverride()
|
||||
root.addCommand(logs)
|
||||
attachLogsFollow(logs)
|
||||
// `helpInformation()` omits `addHelpText('after')`, which is where the
|
||||
// examples live, so the help is captured as the command would print it.
|
||||
let help = ''
|
||||
logs.commands[0].configureOutput({
|
||||
writeOut: (text) => {
|
||||
help += text
|
||||
},
|
||||
})
|
||||
logs.commands[0].outputHelp()
|
||||
|
||||
expect(help).not.toContain('wf_')
|
||||
expect(help).toMatch(/--workflow [0-9a-f]{8}-[0-9a-f]{4}-/)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -467,7 +467,7 @@ function isTransient(error: unknown): boolean {
|
||||
function nonNegativeInteger(raw: string, flag: string): number {
|
||||
const value = Number(raw)
|
||||
if (!Number.isSafeInteger(value) || value < 0) {
|
||||
throw new SimApiError(`${flag} must be a non-negative integer`, 0)
|
||||
throw new SimApiError(`${flag} must be a whole number of 0 or more`, 0)
|
||||
}
|
||||
return value
|
||||
}
|
||||
@@ -531,7 +531,7 @@ follow.
|
||||
|
||||
Examples:
|
||||
$ sim logs follow --level error
|
||||
$ sim logs follow --workflow wf_123 -n 0
|
||||
$ sim logs follow --workflow 00000000-0000-4000-8000-000000000000 -n 0
|
||||
$ sim --output json logs follow | jq -r '.runId'
|
||||
`
|
||||
)
|
||||
|
||||
@@ -216,6 +216,42 @@ describe('resource directory', () => {
|
||||
expect(entries.find((entry) => entry.kind === 'table')?.ref).toBe('tbl_1')
|
||||
})
|
||||
|
||||
/**
|
||||
* `files list` announces "showing the first N" off the surviving cursor and
|
||||
* `ls` printed the same capped answer with nothing on stderr, so one command
|
||||
* presented an incomplete listing as complete and its neighbour did not.
|
||||
*/
|
||||
it('says the combined listing was capped, as the contract-driven list does', async () => {
|
||||
mockRequest.mockImplementation(
|
||||
async (path: string, options: { query: { cursor?: string } }) => {
|
||||
if (path === '/api/v2/files/folders') return { data: [] }
|
||||
const cursor = Number(options.query.cursor ?? 0)
|
||||
return {
|
||||
data: Array.from({ length: 100 }, (_, index) => ({
|
||||
id: `file_${cursor}_${index}`,
|
||||
name: `file-${cursor}-${index}`,
|
||||
folderPath: '/',
|
||||
updatedAt: '2026-01-01T00:00:00.000Z',
|
||||
})),
|
||||
nextCursor: cursor < 2 ? String(cursor + 1) : null,
|
||||
}
|
||||
}
|
||||
)
|
||||
const written: string[] = []
|
||||
vi.spyOn(process.stderr, 'write').mockImplementation((chunk: string | Uint8Array) => {
|
||||
written.push(String(chunk))
|
||||
return true
|
||||
})
|
||||
vi.spyOn(console, 'log').mockImplementation(() => {})
|
||||
|
||||
await program().parseAsync(['node', 'sim', 'files', 'ls', '--limit', '5'])
|
||||
expect(written.join('')).toContain('showing the first 5')
|
||||
|
||||
written.length = 0
|
||||
await program().parseAsync(['node', 'sim', 'files', 'ls', '--limit', '0'])
|
||||
expect(written.join('')).not.toContain('showing the first')
|
||||
})
|
||||
|
||||
it('rejects extra directory arguments instead of silently ignoring them', async () => {
|
||||
await expect(
|
||||
program().parseAsync(['node', 'sim', 'file', 'ls', 'Reports', 'ignored'])
|
||||
|
||||
@@ -12,11 +12,11 @@ import {
|
||||
V2_OPERATIONS,
|
||||
type V2OperationName,
|
||||
} from '../../generated/v2-api'
|
||||
import { requestAllPages, SimApiError, type SimClient, type V2Page } from '../../http/client'
|
||||
import { requestPages, SimApiError, type SimClient, type V2Page } from '../../http/client'
|
||||
import { type Column, printList, text, timestamp } from '../../output/render'
|
||||
import { DEFAULT_LIMIT } from '../../runtime/options'
|
||||
import { encodeFolderPath } from '../../runtime/request'
|
||||
import { decodeFolderPath, renderResult } from '../../runtime/result'
|
||||
import { decodeFolderPath, renderResult, writeCursorTruncation } from '../../runtime/result'
|
||||
|
||||
type FolderListOperation =
|
||||
| 'listFileFolders'
|
||||
@@ -103,17 +103,17 @@ async function listResources(
|
||||
folderPath: string,
|
||||
search: string | undefined,
|
||||
limit: number
|
||||
): Promise<DirectoryResource[]> {
|
||||
): Promise<{ items: DirectoryResource[]; truncated: boolean }> {
|
||||
const query = { workspaceId, folderPath, search, sortBy: 'name', sortOrder: 'asc' }
|
||||
const path = operationPath(config.resources)
|
||||
const paginated = 'cursor' in V2_OPERATIONS[config.resources].query
|
||||
|
||||
if (!paginated) {
|
||||
const page = await client.request<V2Page<DirectoryResource>>(path, { query })
|
||||
return page.data.slice(0, limit)
|
||||
return { items: page.data.slice(0, limit), truncated: page.data.length > limit }
|
||||
}
|
||||
|
||||
return requestAllPages<DirectoryResource>(client, path, {
|
||||
return requestPages<DirectoryResource>(client, path, {
|
||||
query,
|
||||
pageSize: DEFAULT_LIMIT,
|
||||
limit,
|
||||
@@ -177,7 +177,7 @@ export function attachResourceDirectoryCommands(
|
||||
.action(async (path: string | undefined, options: ListOptions, command: Command) => {
|
||||
const rawLimit = Number(options.limit)
|
||||
if (!Number.isSafeInteger(rawLimit) || rawLimit < 0) {
|
||||
throw new SimApiError('--limit must be a non-negative integer', 0)
|
||||
throw new SimApiError('--limit must be a whole number of 0 or more (0 for everything)', 0)
|
||||
}
|
||||
|
||||
const limit = rawLimit === 0 ? Number.POSITIVE_INFINITY : rawLimit
|
||||
@@ -191,8 +191,13 @@ export function attachResourceDirectoryCommands(
|
||||
listFolders(client, config.folders, workspaceId, folderPath, options.search),
|
||||
listResources(client, config, workspaceId, folderPath, options.search, limit),
|
||||
])
|
||||
const entries = entriesFor(config, folders, resources)
|
||||
printList(profile.output, entries.slice(0, limit), COLUMNS)
|
||||
const entries = entriesFor(config, folders, resources.items)
|
||||
const shown = entries.slice(0, limit)
|
||||
// Said here for the same reason the contract-driven `list` says it: the
|
||||
// combined listing is capped after the merge, so a full page of folders
|
||||
// can clip the resources even when the resource walk itself finished.
|
||||
writeCursorTruncation(shown.length, resources.truncated || entries.length > limit)
|
||||
printList(profile.output, shown, COLUMNS)
|
||||
})
|
||||
|
||||
group
|
||||
|
||||
@@ -419,6 +419,19 @@ describe('list columns', () => {
|
||||
expect(columns[columns.length - 1]).toBe(unredacted)
|
||||
})
|
||||
|
||||
/**
|
||||
* A custom tool has both a `title` and a `schema.function.name`, and they are
|
||||
* different fields — a column headed `name` showing the title named the other
|
||||
* one, while `--search` and `--sort-by title` both speak of the title.
|
||||
*/
|
||||
it('heads the custom-tool column with the field it actually shows', () => {
|
||||
const columns = CLI_CONTRACT.listCustomTools?.columns ?? []
|
||||
const titled = columns.find((column) => (column.path ?? column.header) === 'title')
|
||||
|
||||
expect(titled?.header).toBe('title')
|
||||
expect(columns.some((column) => column.header === 'name')).toBe(false)
|
||||
})
|
||||
|
||||
it('keeps the workflow-MCP listings scannable', () => {
|
||||
const servers = CLI_CONTRACT.listWorkflowMcpServers?.columns ?? []
|
||||
const tools = CLI_CONTRACT.listWorkflowMcpTools?.columns ?? []
|
||||
@@ -725,9 +738,7 @@ describe('the import cancel refuses through commander, not just in the contract'
|
||||
})
|
||||
|
||||
it('offers --yes in the help of the one it now gates, and not its sibling', () => {
|
||||
expect(flatHelp('tables', 'imports', 'cancel')).toContain(
|
||||
'Confirm this destructive operation (required)'
|
||||
)
|
||||
expect(flatHelp('tables', 'imports', 'cancel')).toContain('Confirm this operation (required)')
|
||||
expect(flatHelp('tables', 'exports', 'cancel')).not.toContain('--yes')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -22,7 +22,7 @@ const DISPATCH_ROW_LIMIT_HELP =
|
||||
* shape guessable, the same way `TABLE_FILTER_HELP` does for the predicate.
|
||||
*/
|
||||
const WORKFLOW_OPERATIONS_HELP =
|
||||
'Edits to apply, in a single batch, keyed by operation_type: [{"operation_type":"add","block_id":"my-fn","params":{"type":"function","name":"My Fn","inputs":{"code":"return {ok:true}"}}},{"operation_type":"edit","block_id":"<uuid>","params":{"name":"Renamed","connections":{"success":"my-fn"}}},{"operation_type":"delete","block_id":"<uuid>"}]. Also insert_into_subflow and extract_from_subflow, whose params carry {"subflowId":"<loop-id>"}'
|
||||
'Edits to apply, in a single batch, keyed by operation_type: [{"operation_type":"add","block_id":"my-fn","params":{"type":"function","name":"My Fn","inputs":{"code":"return {ok:true}"}}},{"operation_type":"edit","block_id":"<uuid>","params":{"name":"Renamed","connections":{"success":"my-fn"}}},{"operation_type":"delete","block_id":"<uuid>"}]. Also extract_from_subflow, whose params carry {"subflowId":"<loop-id>"}, and insert_into_subflow, which creates a block and so takes an add’s params plus that subflowId'
|
||||
const WORKFLOW_SET_BLOCK_ENABLED_HELP =
|
||||
'Blocks to enable or disable, applied after --operations: [{"block_id":"<uuid>","enabled":false}]. Disabling a loop or parallel cascades to its unlocked descendants; enabling a block whose container is disabled is declined'
|
||||
const WORKFLOW_VARIABLE_OPERATIONS_HELP =
|
||||
@@ -156,7 +156,7 @@ export const CLI_CONTRACT: CliContract = {
|
||||
// `billing status` says its own caveat. The trailing parenthetical is what
|
||||
// keeps the generated docs heading unchanged.
|
||||
describe:
|
||||
"List credit usage events (a personal API key reports only your own events; a workspace API key reports every member's)",
|
||||
"List credit usage events (a personal API key reports only your own events; a workspace API key reports every member's in aggregate, unattributed)",
|
||||
flags: {
|
||||
source: { describe: 'Filter by usage source; sim-chat combines Copilot and workspace chat' },
|
||||
period: { describe: 'Billing period' },
|
||||
@@ -423,7 +423,7 @@ export const CLI_CONTRACT: CliContract = {
|
||||
},
|
||||
getLogStats: {
|
||||
command: 'logs stats',
|
||||
describe: 'Summarize run counts, failures, and cost over a window',
|
||||
describe: 'Summarize run counts, failures and latency over a window',
|
||||
flags: LOG_LIST_FILTER_FLAGS,
|
||||
// Undeclared, the summary fell through to the generic key dump: the whole
|
||||
// `workflows` series printed as one truncated line of raw JSON, the window
|
||||
@@ -829,7 +829,11 @@ export const CLI_CONTRACT: CliContract = {
|
||||
listCustomTools: {
|
||||
columns: [
|
||||
{ header: 'id' },
|
||||
{ header: 'name', path: 'title' },
|
||||
// `title` and `schema.function.name` are both real and different fields
|
||||
// on this resource — the flags say `--search` matches the title and
|
||||
// `--sort-by title` orders by it — so a column headed `name` showing the
|
||||
// title named the other one.
|
||||
{ header: 'title', path: 'title' },
|
||||
{ header: 'description', path: 'schema.function.description' },
|
||||
{ header: 'updated', path: 'updatedAt', format: 'timestamp' },
|
||||
],
|
||||
@@ -1132,7 +1136,15 @@ export const CLI_CONTRACT: CliContract = {
|
||||
},
|
||||
|
||||
// ─── Resource-scoped, path-addressed folders ──────────────────────────────
|
||||
/**
|
||||
* None of the four folder lists paginates: the route declares no `cursor` and
|
||||
* answers with the whole set. That is deliberate — a folder tree is bounded
|
||||
* where it loads — but the terminal said nothing about it, and a caller
|
||||
* reading `--limit` on every other `list` had no way to tell whether the
|
||||
* answer was the full set or the first page of one.
|
||||
*/
|
||||
listFileFolders: {
|
||||
describe: 'List folders',
|
||||
aliases: ['ls'],
|
||||
flags: {
|
||||
parentPath: { ...FOLDER_PATH_INPUT, name: 'parent', describe: 'Direct parent folder path' },
|
||||
@@ -1140,6 +1152,7 @@ export const CLI_CONTRACT: CliContract = {
|
||||
columns: FOLDER_LIST_COLUMNS,
|
||||
},
|
||||
listKnowledgeFolders: {
|
||||
describe: 'List knowledge folders',
|
||||
aliases: ['ls'],
|
||||
flags: {
|
||||
parentPath: { ...FOLDER_PATH_INPUT, name: 'parent', describe: 'Direct parent folder path' },
|
||||
@@ -1147,6 +1160,7 @@ export const CLI_CONTRACT: CliContract = {
|
||||
columns: FOLDER_LIST_COLUMNS,
|
||||
},
|
||||
listTableFolders: {
|
||||
describe: 'List table folders',
|
||||
aliases: ['ls'],
|
||||
flags: {
|
||||
parentPath: { ...FOLDER_PATH_INPUT, name: 'parent', describe: 'Direct parent folder path' },
|
||||
@@ -1154,6 +1168,7 @@ export const CLI_CONTRACT: CliContract = {
|
||||
columns: FOLDER_LIST_COLUMNS,
|
||||
},
|
||||
listWorkflowFolders: {
|
||||
describe: 'List workflow folders',
|
||||
aliases: ['ls'],
|
||||
flags: {
|
||||
parentPath: { ...FOLDER_PATH_INPUT, name: 'parent', describe: 'Direct parent folder path' },
|
||||
|
||||
@@ -359,6 +359,19 @@ function traceRequest(method: string, url: string, status: number | string, star
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Drops a label the message already opens with.
|
||||
*
|
||||
* Some routes name the field inside the message as well as in `path`, and the
|
||||
* two are printed one after the other: `sortBy: only \"startedAt\" can order job
|
||||
* runs` under `path: ['sortBy']` came out as `--sort-by: --sort-by: only …`
|
||||
* once the wire name had been retyped as the flag.
|
||||
*/
|
||||
function withoutLeadingLabel(message: string, label: string): string {
|
||||
const prefix = `${label}: `
|
||||
return message.startsWith(prefix) ? message.slice(prefix.length) : message
|
||||
}
|
||||
|
||||
/** Formats nested validation issues as readable, path-aware lines. */
|
||||
export function formatApiErrorDetails(details: unknown): string[] {
|
||||
const issues: DetailIssue[] = []
|
||||
@@ -402,9 +415,10 @@ export function formatApiErrorDetails(details: unknown): string[] {
|
||||
const visible = kept.slice(0, 8)
|
||||
const lines = [
|
||||
' details:',
|
||||
...visible.map(
|
||||
(issue) => ` ${issue.path.length > 0 ? issue.path.join('.') : 'request'}: ${issue.message}`
|
||||
),
|
||||
...visible.map((issue) => {
|
||||
const label = issue.path.length > 0 ? issue.path.join('.') : 'request'
|
||||
return ` ${label}: ${withoutLeadingLabel(issue.message, label)}`
|
||||
}),
|
||||
]
|
||||
if (kept.length > visible.length) lines.push(` … ${kept.length - visible.length} more issues`)
|
||||
return lines
|
||||
@@ -644,9 +658,25 @@ export async function requestAllPages<T>(
|
||||
path: string,
|
||||
options: RequestAllPagesOptions
|
||||
): Promise<T[]> {
|
||||
return (await requestPages<T>(client, path, options)).items
|
||||
}
|
||||
|
||||
/**
|
||||
* The same walk, also stating whether it stopped short.
|
||||
*
|
||||
* A caller that prints the rows itself has to say so — `files list` announces
|
||||
* "showing the first N" off the surviving cursor and `files ls` did not, so the
|
||||
* same capped answer looked complete on one command and incomplete on its
|
||||
* neighbour.
|
||||
*/
|
||||
export async function requestPages<T>(
|
||||
client: Pick<SimClient, 'request'>,
|
||||
path: string,
|
||||
options: RequestAllPagesOptions
|
||||
): Promise<{ items: T[]; truncated: boolean }> {
|
||||
const { query, pageSize, limit: requestedLimit, ...requestOptions } = options
|
||||
const limit = requestedLimit ?? Number.POSITIVE_INFINITY
|
||||
if (limit <= 0) return []
|
||||
if (limit <= 0) return { items: [], truncated: false }
|
||||
|
||||
const items: T[] = []
|
||||
const progress = pageProgress()
|
||||
@@ -673,7 +703,7 @@ export async function requestAllPages<T>(
|
||||
progress.finish()
|
||||
}
|
||||
|
||||
return items.slice(0, limit)
|
||||
return { items: items.slice(0, limit), truncated: cursor !== null || items.length > limit }
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -113,7 +113,12 @@ async function run(argv: string[], response: unknown = { data: [], nextCursor: n
|
||||
mockRequest.mockResolvedValue(response)
|
||||
vi.spyOn(console, 'log').mockImplementation(() => {})
|
||||
await program().parseAsync(['node', 'sim', ...argv])
|
||||
return mockRequest.mock.calls[0]
|
||||
// `--all-workspaces` asks `/api/v2/meta` whether the key can make an
|
||||
// account-wide read before it makes one, so the operation's own call is not
|
||||
// always the first.
|
||||
const call = mockRequest.mock.calls.find(([path]) => path !== V2_OPERATIONS.getMeta.path)
|
||||
if (!call) throw new Error('the command made no request of its own')
|
||||
return call
|
||||
}
|
||||
|
||||
describe('commands parsed through commander', () => {
|
||||
@@ -173,14 +178,12 @@ describe('commands parsed through commander', () => {
|
||||
.replace(/\s+/g, ' ')
|
||||
|
||||
expect(flat('workflows', 'state', 'replace')).toContain(
|
||||
'Confirm this destructive operation (required unless --dry-run)'
|
||||
'Confirm this operation (required unless --dry-run)'
|
||||
)
|
||||
expect(flat('workflows', 'operations', 'apply')).toContain(
|
||||
'Confirm this destructive operation (required unless --dry-run)'
|
||||
)
|
||||
expect(flat('tables', 'rows', 'delete')).toContain(
|
||||
'Confirm this destructive operation (required)'
|
||||
'Confirm this operation (required unless --dry-run)'
|
||||
)
|
||||
expect(flat('tables', 'rows', 'delete')).toContain('Confirm this operation (required)')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1676,8 +1679,92 @@ describe('bodies and fields the generator cannot flatten', () => {
|
||||
expect(options.body).toMatchObject({ rowIds: ['row_1', 'row_2'] })
|
||||
expect(options.body).not.toHaveProperty('limit')
|
||||
})
|
||||
|
||||
/**
|
||||
* The help says "0 is not accepted" and the CLI sent it anyway: `0`, `-1`
|
||||
* and `1.5` all reached the wire to be refused by the route.
|
||||
*/
|
||||
it('refuses a row cap the help already documents as invalid', async () => {
|
||||
for (const [value, message] of [
|
||||
['0', '--limit must be 1 or more'],
|
||||
['-1', '--limit must be 1 or more'],
|
||||
['1.5', '--limit must be a whole number'],
|
||||
// Above 2^52 the parse itself drops the fraction, so `Number.isInteger`
|
||||
// alone would pass this and send a value the caller never typed.
|
||||
['4503599627370496.5', '--limit must be a whole number'],
|
||||
] as const) {
|
||||
for (const command of ['batch-delete', 'batch-update'] as const) {
|
||||
const argv = ['tables', 'rows', command, 'tbl_1', '--filter', '{"all":[]}']
|
||||
if (command === 'batch-update') argv.push('--data', '{"a":1}')
|
||||
argv.push('--limit', value, '--yes')
|
||||
await expect(run(argv)).rejects.toThrow(message)
|
||||
}
|
||||
}
|
||||
expect(mockRequest).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
/**
|
||||
* The route decides this with a refine whose message describes the opposite
|
||||
* mistake when neither flag is typed — and half in wire names.
|
||||
*/
|
||||
it('requires exactly one of the two ways to choose the rows', async () => {
|
||||
await expect(run(['tables', 'rows', 'batch-delete', 'tbl_1', '--yes'])).rejects.toThrow(
|
||||
'--filter or --row is required to choose the rows to delete'
|
||||
)
|
||||
await expect(
|
||||
run([
|
||||
'tables',
|
||||
'rows',
|
||||
'batch-delete',
|
||||
'tbl_1',
|
||||
'--filter',
|
||||
'{"all":[]}',
|
||||
'--row',
|
||||
'row_1',
|
||||
'--yes',
|
||||
])
|
||||
).rejects.toThrow('--filter and --row choose the rows to delete two different ways')
|
||||
expect(mockRequest).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* `--limit` on a cursor-paginated operation is a client-side total, stripped
|
||||
* from the request while the CLI walks the pages — so `--limit 0` reached the
|
||||
* whole table with run state attached as many individually-legal pages, which
|
||||
* is what the route's own `limit: 0` refusal exists to prevent.
|
||||
*/
|
||||
/**
|
||||
* An `integer` field said so in the contract, and the refusal was left to the
|
||||
* server — which answered in library wording naming neither the flag nor the
|
||||
* value.
|
||||
*/
|
||||
it('refuses a fractional or unrepresentable value on an integer flag', async () => {
|
||||
await expect(run(['files', 'read', 'file_1', '--max-bytes', '5.5'])).rejects.toThrow(
|
||||
'--max-bytes must be a whole number'
|
||||
)
|
||||
await expect(
|
||||
run(['files', 'read', 'file_1', '--max-bytes', '999999999999999999999'])
|
||||
).rejects.toThrow('--max-bytes is outside the whole-number range the API accepts')
|
||||
expect(mockRequest).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
/**
|
||||
* `--workspace` is a root-program global, so commander accepts it everywhere
|
||||
* while only an operation declaring `workspaceId` ever uses it. On the rest it
|
||||
* was parsed and dropped, and three different values produced byte-identical
|
||||
* requests.
|
||||
*/
|
||||
/**
|
||||
* `activate create` is the deployed cutover addressed by version, the same
|
||||
* production change `rollback` and `undeploy` both gate. `deploy` stays
|
||||
* ungated: it publishes the draft as a NEW version, which is the forward
|
||||
* action the caller asked for and which a rollback undoes.
|
||||
*/
|
||||
/**
|
||||
* `--all-workspaces` reaches the wire as the absence of `workspaceId`, so a
|
||||
* workspace key answered with its own workspace's figures and exit 0.
|
||||
*/
|
||||
it('still gives paginated lists their numeric --limit', async () => {
|
||||
const [, options] = await run(['files', 'list', '--limit', '7'])
|
||||
expect(options.query).toMatchObject({ limit: 7 })
|
||||
|
||||
@@ -181,6 +181,30 @@ const EXCLUSIVE_CAP_FIELDS: Readonly<
|
||||
deleteTableRows: { cap: 'limit', ids: 'rowIds' },
|
||||
}
|
||||
|
||||
/**
|
||||
* The pager's `--limit`, where `0` means "no ceiling".
|
||||
*
|
||||
* Read whole, not up to the first character that stops looking numeric.
|
||||
* `parseInt` truncated before the guard could see what was typed, so
|
||||
* `--limit 3.9` quietly fetched 3, `--limit 1e3` fetched 1, and `--limit -0.5`
|
||||
* parsed as `-0` — which is not less than zero, so it slipped the guard and
|
||||
* then read as the `0` that means everything. `Number` keeps the value intact
|
||||
* so each of those is refused instead of reinterpreted, and it reads `0x10` and
|
||||
* `1e3` as the caller wrote them.
|
||||
*
|
||||
* The empty string is refused explicitly because `Number('')` is `0`: without
|
||||
* this, `--limit ''` would go from today's error to an unbounded walk of a
|
||||
* shared workspace.
|
||||
*/
|
||||
function readPagedLimit(raw: unknown): number {
|
||||
const text = String(raw ?? DEFAULT_LIMIT).trim()
|
||||
const value = text === '' ? Number.NaN : Number(text)
|
||||
if (!Number.isInteger(value) || value < 0) {
|
||||
throw new SimApiError('--limit must be a whole number of 0 or more (0 for everything)', 0)
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
/** Refuses a row cap typed alongside the explicit id list that supersedes it. */
|
||||
function assertCapIsUsable(operation: V2OperationName, flags: Record<string, unknown>): void {
|
||||
const exclusive = EXCLUSIVE_CAP_FIELDS[operation]
|
||||
@@ -195,6 +219,40 @@ function assertCapIsUsable(operation: V2OperationName, flags: Record<string, unk
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Operations that select their targets through exactly one of two flags.
|
||||
*
|
||||
* `tables rows batch-delete` left the choice to the route, whose refusal —
|
||||
* `Provide either filter or rowIds, but not both` — describes the wrong mistake
|
||||
* when neither was typed, and describes it half in wire names. Its sibling
|
||||
* `tables rows batch-update` already refuses locally, because its `filter` is
|
||||
* `required` in the contract; stating this one here puts the requirement in the
|
||||
* same place for both.
|
||||
*/
|
||||
const REQUIRED_SELECTORS: Readonly<
|
||||
Partial<
|
||||
Record<V2OperationName, { readonly fields: readonly [string, string]; readonly noun: string }>
|
||||
>
|
||||
> = {
|
||||
deleteTableRows: { fields: ['filter', 'rowIds'], noun: 'rows to delete' },
|
||||
}
|
||||
|
||||
/** Refuses a selection that names neither of the two ways to make it, or both. */
|
||||
function assertSelectorIsUsable(operation: V2OperationName, flags: Record<string, unknown>): void {
|
||||
const selector = REQUIRED_SELECTORS[operation]
|
||||
if (!selector) return
|
||||
|
||||
const [first, second] = selector.fields.map((field) => flagNameFor(operation, field))
|
||||
const given = [first, second].filter((name) => flags[camel(name)] !== undefined)
|
||||
if (given.length === 1) return
|
||||
throw new SimApiError(
|
||||
given.length === 0
|
||||
? `--${first} or --${second} is required to choose the ${selector.noun}`
|
||||
: `--${first} and --${second} choose the ${selector.noun} two different ways; pass one, not both`,
|
||||
0
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves a value supplied under a flag's former name onto its current one.
|
||||
*
|
||||
@@ -257,6 +315,7 @@ export async function executeOperation(
|
||||
|
||||
foldRenamedFlags(operation, commandSpec, requestFlags)
|
||||
assertCapIsUsable(operation, requestFlags)
|
||||
assertSelectorIsUsable(operation, requestFlags)
|
||||
|
||||
/**
|
||||
* A dry run writes nothing, so it never needs the destructive confirmation.
|
||||
@@ -292,36 +351,23 @@ export async function executeOperation(
|
||||
*/
|
||||
const needsWorkspace =
|
||||
(hasWorkspaceField || commandSpec.profileWorkspacePath === true) && !omitsWorkspace
|
||||
const paging = cursorSlot(operationSpec)
|
||||
/**
|
||||
* Checked before the request is built, because `buildRequest` also validates
|
||||
* `limit` and would otherwise answer a paginated `--limit 1.5` with the
|
||||
* generic integer refusal — losing the `0 for everything` this pager depends
|
||||
* on the caller knowing.
|
||||
*/
|
||||
const pagedLimit = paging ? readPagedLimit(requestFlags.limit) : 0
|
||||
const request = buildRequest(
|
||||
operation,
|
||||
positional,
|
||||
requestFlags,
|
||||
needsWorkspace ? client.requireWorkspace() : profile.workspaceId
|
||||
)
|
||||
const paging = cursorSlot(operationSpec)
|
||||
|
||||
if (paging) {
|
||||
/**
|
||||
* Read whole, not up to the first character that stops looking numeric.
|
||||
*
|
||||
* `parseInt` truncated before the guard could see what was typed, so
|
||||
* `--limit 3.9` quietly fetched 3, `--limit 1e3` fetched 1, and
|
||||
* `--limit -0.5` parsed as `-0` — which is not less than zero, so it slipped
|
||||
* the guard and then read as the `0` that means everything. `Number` keeps
|
||||
* the value intact so each of those is refused instead of reinterpreted,
|
||||
* and it reads `0x10` and `1e3` as the caller wrote them.
|
||||
*
|
||||
* The empty string is refused explicitly because `Number('')` is `0`, and
|
||||
* `0` here means "no ceiling": without this, `--limit ''` would go from
|
||||
* today's error to an unbounded walk of a shared workspace.
|
||||
*/
|
||||
const limitText = String(requestFlags.limit ?? DEFAULT_LIMIT).trim()
|
||||
const rawLimit = limitText === '' ? Number.NaN : Number(limitText)
|
||||
if (!Number.isInteger(rawLimit) || rawLimit < 0) {
|
||||
throw new SimApiError('--limit must be a whole number of 0 or more (0 for everything)', 0)
|
||||
}
|
||||
|
||||
const limit = rawLimit === 0 ? Number.POSITIVE_INFINITY : rawLimit
|
||||
const limit = pagedLimit === 0 ? Number.POSITIVE_INFINITY : pagedLimit
|
||||
const pageSize = Math.min(Number.isFinite(limit) ? limit : DEFAULT_LIMIT, DEFAULT_LIMIT)
|
||||
const pageLimit = 'limit' in (operationSpec[paging] ?? {}) ? { limit: pageSize } : {}
|
||||
const rows: unknown[] = []
|
||||
|
||||
@@ -67,6 +67,38 @@ describe('a validation error restated in the spellings a caller can type', () =>
|
||||
expect(retyped.message).not.toContain('--name')
|
||||
})
|
||||
|
||||
/**
|
||||
* Some routes name the field inside the message as well as in `path`, and the
|
||||
* details column prints both: `--sort-by: --sort-by: only "startedAt" …`.
|
||||
*/
|
||||
it('does not print the field label twice on one detail line', () => {
|
||||
const message =
|
||||
'sortBy: only "startedAt" can order job runs; drop includeJobRuns or sort by "startedAt"'
|
||||
const line = detailLines('listLogs', [{ path: ['sortBy'], message }])[1]
|
||||
|
||||
expect(line).toContain(' --sort-by: only "startedAt"')
|
||||
expect(line).not.toContain('--sort-by: --sort-by:')
|
||||
})
|
||||
|
||||
/**
|
||||
* Only multi-segment camelCase is safely rewritable in prose, so a sentence
|
||||
* enumerating both kinds came out half in flags and half in wire names:
|
||||
* `At least one of name, description, or --folder is required`.
|
||||
*/
|
||||
it('never mixes the two vocabularies in one sentence', () => {
|
||||
expect(
|
||||
retype(
|
||||
'updateWorkflow',
|
||||
new SimApiError('At least one of name, description, or folderPath is required', 400)
|
||||
).message
|
||||
).toBe('At least one of name, description, or folderPath is required')
|
||||
|
||||
// A sentence with nothing ambiguous left in it still gets the translation.
|
||||
expect(
|
||||
retype('listWorkflows', new SimApiError('folderPath must be canonical', 400)).message
|
||||
).toBe('--folder must be canonical')
|
||||
})
|
||||
|
||||
it('names the global flag the workspace comes from', () => {
|
||||
expect(
|
||||
detailLines('listLogs', [{ path: ['workspaceId'], message: 'Workspace is required' }])[1]
|
||||
|
||||
@@ -73,13 +73,39 @@ function typeableFields(
|
||||
return spellings
|
||||
}
|
||||
|
||||
/** Rewrites wire names a message quotes into the flags the caller typed. */
|
||||
/**
|
||||
* Rewrites wire names a message quotes into the flags the caller typed.
|
||||
*
|
||||
* All or nothing. Only multi-segment camelCase is safely rewritable in prose —
|
||||
* see {@link WIRE_IDENTIFIER} — so a sentence enumerating both kinds came out
|
||||
* half in one vocabulary and half in the other: `At least one of name,
|
||||
* description, or --folder is required` reads as three different things, one of
|
||||
* which is a flag. When a single-word field of the same operation survives the
|
||||
* pass, the whole message is left as the server wrote it: entirely in wire
|
||||
* names, which is at least internally consistent and matches the REST
|
||||
* reference the caller can look the names up in.
|
||||
*
|
||||
* The veto reads the message the server sent, not the rewritten one: a flag
|
||||
* spelling contains its own field name, so re-scanning the output would let a
|
||||
* successful translation veto itself.
|
||||
*
|
||||
* The cost is that a message mentioning `folderPath` and the English word
|
||||
* `name` loses a translation it could have had. That is the deliberate trade —
|
||||
* telling the two apart is the undecidable problem that produced the mixed
|
||||
* sentence in the first place, and an unhelpful sentence beats a misleading one.
|
||||
*/
|
||||
function retypeMessage(message: string, spellings: Map<string, string>): string {
|
||||
let retyped = message
|
||||
for (const [field, spelling] of spellings) {
|
||||
if (!WIRE_IDENTIFIER.test(field)) continue
|
||||
retyped = retyped.replaceAll(new RegExp(`\\b${field}\\b`, 'g'), spelling)
|
||||
}
|
||||
if (retyped === message) return message
|
||||
|
||||
for (const field of spellings.keys()) {
|
||||
if (WIRE_IDENTIFIER.test(field)) continue
|
||||
if (new RegExp(`\\b${field}\\b`).test(message)) return message
|
||||
}
|
||||
return retyped
|
||||
}
|
||||
|
||||
|
||||
@@ -31,9 +31,11 @@ describe('the --yes flag on a destructive command', () => {
|
||||
*/
|
||||
it('describes itself as the confirmation, not as skipping one', () => {
|
||||
const help = confirmHelp()
|
||||
expect(help).toMatch(/-y, --yes\s+Confirm this destructive operation \(required\)/)
|
||||
expect(help).toMatch(/-y, --yes\s+Confirm this operation \(required\)/)
|
||||
expect(help).not.toMatch(/skip/i)
|
||||
expect(help).not.toMatch(/prompt/i)
|
||||
// Nor "destructive": the same gate covers `files unzip`, which only adds.
|
||||
expect(help).not.toMatch(/destructive/i)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -257,11 +257,15 @@ export function addOperationOptions(
|
||||
// outright would send a caller reaching for `--yes` to preview a change.
|
||||
const exemptedByDryRun =
|
||||
operationSpec.query?.dryRun !== undefined || operationSpec.body?.dryRun !== undefined
|
||||
// Not "destructive": the gate also covers operations that only add —
|
||||
// `files unzip` writes the archive's contents into the workspace and
|
||||
// destroys nothing — so the adjective was wrong on the help line while the
|
||||
// refusal itself, which states the operation's own consequence, was right.
|
||||
command.option(
|
||||
'-y, --yes',
|
||||
exemptedByDryRun
|
||||
? 'Confirm this destructive operation (required unless --dry-run)'
|
||||
: 'Confirm this destructive operation (required)'
|
||||
? 'Confirm this operation (required unless --dry-run)'
|
||||
: 'Confirm this operation (required)'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -318,6 +318,17 @@ export function encodeFolderPath(value: string): string {
|
||||
.join('/')
|
||||
}
|
||||
|
||||
/**
|
||||
* A fractional part `Number` cannot keep.
|
||||
*
|
||||
* Above 2^52 a double's spacing is 1, so `Number('4503599627370496.5')` is an
|
||||
* integer — `Number.isInteger` passes and the API receives a value the caller
|
||||
* did not type. Read the text as well as the parsed number so the refusal
|
||||
* covers the range where the parse itself loses the fraction. Digits that are
|
||||
* all zero are not a fraction, so `1.0` stays a whole number.
|
||||
*/
|
||||
const FRACTIONAL_DIGITS = /\.\d*[1-9]/
|
||||
|
||||
/**
|
||||
* Points at `@` when a value that failed to parse looks like a filename.
|
||||
*
|
||||
@@ -381,6 +392,27 @@ export function coerce(raw: unknown, field: FieldSpec, flag: FlagSpec, flagName:
|
||||
if (NUMERIC_KINDS.has(field.kind)) {
|
||||
const value = Number(raw)
|
||||
if (Number.isNaN(value)) throw new SimApiError(`--${flagName} must be a number`, 0)
|
||||
/**
|
||||
* An `integer` field said so in the contract, and every other constraint on
|
||||
* one is already refused here by hand. Leaving integrality to the server
|
||||
* answered `--max-bytes 5.5` with `Invalid input: expected int, received
|
||||
* number` and `--max-bytes 999999999999999999999` with `Too big: expected
|
||||
* int to be <=9007199254740991` — library wording naming neither the flag
|
||||
* nor anything the caller typed, on the one flag whose blank, zero and
|
||||
* non-numeric cases all had a sentence written for them.
|
||||
*/
|
||||
if (
|
||||
field.kind === 'integer' &&
|
||||
(!Number.isInteger(value) || FRACTIONAL_DIGITS.test(String(raw)))
|
||||
) {
|
||||
throw new SimApiError(`--${flagName} must be a whole number`, 0)
|
||||
}
|
||||
if (field.kind === 'integer' && !Number.isSafeInteger(value)) {
|
||||
throw new SimApiError(
|
||||
`--${flagName} is outside the whole-number range the API accepts (±${Number.MAX_SAFE_INTEGER})`,
|
||||
0
|
||||
)
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
@@ -551,6 +583,23 @@ export function buildRequest(
|
||||
|
||||
const value = coerce(raw ?? undefined, descriptor, flag, flagName)
|
||||
|
||||
/**
|
||||
* A non-paginated `limit` is a row cap the route bounds at `1`, which is
|
||||
* what `--help` already tells the caller ("note 0 is not accepted") — so
|
||||
* `--limit 0`, `-1` and `1.5` were shipping a round trip to be told
|
||||
* something the CLI had documented. The ceiling stays with the server:
|
||||
* it is per-route policy, and nothing in the terminal states it.
|
||||
*/
|
||||
if (
|
||||
field === 'limit' &&
|
||||
!paginatedLimit &&
|
||||
NUMERIC_KINDS.has(descriptor.kind) &&
|
||||
typeof value === 'number' &&
|
||||
value < 1
|
||||
) {
|
||||
throw new SimApiError(`--${flagName} must be 1 or more`, 0)
|
||||
}
|
||||
|
||||
if (value === undefined) {
|
||||
if (descriptor.required) {
|
||||
throw new SimApiError(
|
||||
|
||||
@@ -433,7 +433,7 @@ function writeEnvelopeTruncation(envelope: unknown): void {
|
||||
* the answer is incomplete either way, and the caller who capped it is the one
|
||||
* most likely to reuse the result as if it were whole.
|
||||
*/
|
||||
function writeCursorTruncation(count: number, truncated: boolean): void {
|
||||
export function writeCursorTruncation(count: number, truncated: boolean): void {
|
||||
if (!truncated) return
|
||||
process.stderr.write(
|
||||
chalk.dim(`showing the first ${count}; more results exist — re-run with --limit 0 for all\n`)
|
||||
|
||||
Reference in New Issue
Block a user