@usenaive-sdk/cli 0.12.2 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +76 -5
- package/dist/client.d.ts +74 -1
- package/dist/client.js +351 -17
- package/dist/client.js.map +1 -1
- package/dist/commands/aeo.js +69 -27
- package/dist/commands/aeo.js.map +1 -1
- package/dist/commands/app-data.js +29 -11
- package/dist/commands/app-data.js.map +1 -1
- package/dist/commands/apps.d.ts +7 -0
- package/dist/commands/apps.js +255 -18
- package/dist/commands/apps.js.map +1 -1
- package/dist/commands/audio.d.ts +9 -0
- package/dist/commands/audio.js +391 -0
- package/dist/commands/audio.js.map +1 -0
- package/dist/commands/auth.d.ts +31 -0
- package/dist/commands/auth.js +403 -0
- package/dist/commands/auth.js.map +1 -0
- package/dist/commands/billing.js +12 -12
- package/dist/commands/billing.js.map +1 -1
- package/dist/commands/brain.d.ts +2 -0
- package/dist/commands/brain.js +1255 -0
- package/dist/commands/brain.js.map +1 -0
- package/dist/commands/browser.js +35 -8
- package/dist/commands/browser.js.map +1 -1
- package/dist/commands/business.js +117 -16
- package/dist/commands/business.js.map +1 -1
- package/dist/commands/clone.d.ts +2 -0
- package/dist/commands/clone.js +203 -0
- package/dist/commands/clone.js.map +1 -0
- package/dist/commands/companies.d.ts +8 -0
- package/dist/commands/companies.js +137 -47
- package/dist/commands/companies.js.map +1 -1
- package/dist/commands/company-data.d.ts +2 -0
- package/dist/commands/company-data.js +168 -0
- package/dist/commands/company-data.js.map +1 -0
- package/dist/commands/compute.js +10 -3
- package/dist/commands/compute.js.map +1 -1
- package/dist/commands/config.d.ts +2 -0
- package/dist/commands/config.js +36 -0
- package/dist/commands/config.js.map +1 -0
- package/dist/commands/connections.d.ts +6 -0
- package/dist/commands/connections.js +29 -1
- package/dist/commands/connections.js.map +1 -1
- package/dist/commands/domains.js +1 -1
- package/dist/commands/ecommerce.js +19 -9
- package/dist/commands/ecommerce.js.map +1 -1
- package/dist/commands/email.js +56 -9
- package/dist/commands/email.js.map +1 -1
- package/dist/commands/env.d.ts +3 -3
- package/dist/commands/env.js +68 -4
- package/dist/commands/env.js.map +1 -1
- package/dist/commands/formation.js +3 -3
- package/dist/commands/identity.js +4 -3
- package/dist/commands/identity.js.map +1 -1
- package/dist/commands/images.js +87 -13
- package/dist/commands/images.js.map +1 -1
- package/dist/commands/init.d.ts +3 -0
- package/dist/commands/init.js +39 -36
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/jobs.js +1 -0
- package/dist/commands/jobs.js.map +1 -1
- package/dist/commands/keys.js +26 -3
- package/dist/commands/keys.js.map +1 -1
- package/dist/commands/legacy-auth.d.ts +27 -0
- package/dist/commands/legacy-auth.js +64 -0
- package/dist/commands/legacy-auth.js.map +1 -0
- package/dist/commands/llm.js +13 -0
- package/dist/commands/llm.js.map +1 -1
- package/dist/commands/login.js +2 -0
- package/dist/commands/login.js.map +1 -1
- package/dist/commands/loops.d.ts +2 -0
- package/dist/commands/loops.js +260 -0
- package/dist/commands/loops.js.map +1 -0
- package/dist/commands/media.js +3 -3
- package/dist/commands/media.js.map +1 -1
- package/dist/commands/mobile.d.ts +4 -0
- package/dist/commands/mobile.js +382 -0
- package/dist/commands/mobile.js.map +1 -0
- package/dist/commands/payments.d.ts +2 -0
- package/dist/commands/payments.js +131 -0
- package/dist/commands/payments.js.map +1 -0
- package/dist/commands/people.d.ts +2 -0
- package/dist/commands/people.js +193 -0
- package/dist/commands/people.js.map +1 -0
- package/dist/commands/phone.js +3 -3
- package/dist/commands/phone.js.map +1 -1
- package/dist/commands/projects.d.ts +13 -0
- package/dist/commands/projects.js +220 -0
- package/dist/commands/projects.js.map +1 -0
- package/dist/commands/register.js +16 -8
- package/dist/commands/register.js.map +1 -1
- package/dist/commands/sandbox.d.ts +2 -0
- package/dist/commands/sandbox.js +245 -0
- package/dist/commands/sandbox.js.map +1 -0
- package/dist/commands/search.js +26 -12
- package/dist/commands/search.js.map +1 -1
- package/dist/commands/seo.js +16 -8
- package/dist/commands/seo.js.map +1 -1
- package/dist/commands/sessions.d.ts +6 -0
- package/dist/commands/sessions.js +109 -0
- package/dist/commands/sessions.js.map +1 -0
- package/dist/commands/social-data.d.ts +2 -0
- package/dist/commands/social-data.js +244 -0
- package/dist/commands/social-data.js.map +1 -0
- package/dist/commands/social.js +8 -6
- package/dist/commands/social.js.map +1 -1
- package/dist/commands/teams.d.ts +69 -0
- package/dist/commands/teams.js +503 -0
- package/dist/commands/teams.js.map +1 -0
- package/dist/commands/up.d.ts +150 -0
- package/dist/commands/up.js +415 -15
- package/dist/commands/up.js.map +1 -1
- package/dist/commands/usage.js +15 -4
- package/dist/commands/usage.js.map +1 -1
- package/dist/commands/use.d.ts +9 -0
- package/dist/commands/use.js +76 -11
- package/dist/commands/use.js.map +1 -1
- package/dist/commands/users.d.ts +8 -0
- package/dist/commands/users.js +28 -9
- package/dist/commands/users.js.map +1 -1
- package/dist/commands/vault.js +28 -1
- package/dist/commands/vault.js.map +1 -1
- package/dist/commands/video.js +190 -270
- package/dist/commands/video.js.map +1 -1
- package/dist/commands/voice.d.ts +2 -0
- package/dist/commands/voice.js +516 -0
- package/dist/commands/voice.js.map +1 -0
- package/dist/commands/wallet.d.ts +2 -0
- package/dist/commands/wallet.js +244 -0
- package/dist/commands/wallet.js.map +1 -0
- package/dist/commands/whoami.js +17 -1
- package/dist/commands/whoami.js.map +1 -1
- package/dist/config.d.ts +24 -1
- package/dist/config.js +36 -7
- package/dist/config.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +290 -20
- package/dist/index.js.map +1 -1
- package/dist/lib/brain-select.d.ts +193 -0
- package/dist/lib/brain-select.js +403 -0
- package/dist/lib/brain-select.js.map +1 -0
- package/dist/lib/cli-validation.d.ts +12 -0
- package/dist/lib/cli-validation.js +46 -0
- package/dist/lib/cli-validation.js.map +1 -0
- package/dist/lib/deprecation.d.ts +158 -0
- package/dist/lib/deprecation.js +140 -0
- package/dist/lib/deprecation.js.map +1 -0
- package/dist/lib/duration.d.ts +8 -0
- package/dist/lib/duration.js +11 -0
- package/dist/lib/duration.js.map +1 -0
- package/dist/lib/http-response.d.ts +11 -0
- package/dist/lib/http-response.js +96 -0
- package/dist/lib/http-response.js.map +1 -0
- package/dist/lib/pricing.d.ts +132 -0
- package/dist/lib/pricing.js +148 -0
- package/dist/lib/pricing.js.map +1 -0
- package/dist/lib/stream-frames.d.ts +114 -0
- package/dist/lib/stream-frames.js +169 -0
- package/dist/lib/stream-frames.js.map +1 -0
- package/dist/lib/teams-catalog.d.ts +141 -0
- package/dist/lib/teams-catalog.js +765 -0
- package/dist/lib/teams-catalog.js.map +1 -0
- package/dist/lib/unknown-command.d.ts +57 -0
- package/dist/lib/unknown-command.js +93 -0
- package/dist/lib/unknown-command.js.map +1 -0
- package/dist/lib/version-check.d.ts +26 -0
- package/dist/lib/version-check.js +21 -0
- package/dist/lib/version-check.js.map +1 -0
- package/dist/output.d.ts +119 -10
- package/dist/output.js +309 -15
- package/dist/output.js.map +1 -1
- package/dist/prompt.d.ts +17 -0
- package/dist/prompt.js +70 -0
- package/dist/prompt.js.map +1 -0
- package/package.json +11 -6
- package/dist/commands/billing-client.d.ts +0 -2
- package/dist/commands/billing-client.js +0 -271
- package/dist/commands/billing-client.js.map +0 -1
- package/dist/commands/crm-app.d.ts +0 -2
- package/dist/commands/crm-app.js +0 -306
- package/dist/commands/crm-app.js.map +0 -1
- package/dist/commands/crm.d.ts +0 -2
- package/dist/commands/crm.js +0 -319
- package/dist/commands/crm.js.map +0 -1
- package/dist/commands/integrations.d.ts +0 -2
- package/dist/commands/integrations.js +0 -199
- package/dist/commands/integrations.js.map +0 -1
- package/dist/commands/link.d.ts +0 -3
- package/dist/commands/link.js +0 -91
- package/dist/commands/link.js.map +0 -1
- package/dist/commands/operators.d.ts +0 -11
- package/dist/commands/operators.js +0 -120
- package/dist/commands/operators.js.map +0 -1
|
@@ -0,0 +1,765 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE ONE DECLARATION SITE for the `naive teams` surface (06 §1.4).
|
|
3
|
+
*
|
|
4
|
+
* Two design notes shipped two `teams` command sets and neither was a superset of
|
|
5
|
+
* the other. Two hand-written lists is exactly the drift a one-declaration rule
|
|
6
|
+
* exists to catch, so both are deleted and the union is declared once, here, as
|
|
7
|
+
* DATA. `commands/teams.ts` builds the commander tree by walking this array; it
|
|
8
|
+
* writes no subcommand of its own. A row added here is a working command, and a
|
|
9
|
+
* command that is not a row here does not exist.
|
|
10
|
+
*
|
|
11
|
+
* ── WHY THIS IS A .ts FILE IN THIS PACKAGE AND NOT A GENERATED ARTIFACT ─────
|
|
12
|
+
*
|
|
13
|
+
* 06 §1.4 puts the declaration in `@usenaive/agent-contract/capabilities` and has
|
|
14
|
+
* a generator emit `packages/cli/src/generated/teams-commands.ts` from it, so the
|
|
15
|
+
* CLI subcommand, the MCP tool, the REST path and the openapi operation are all
|
|
16
|
+
* one statement. MEASURED: there is no `packages/agent-contract` directory in
|
|
17
|
+
* this tree and no generator to run, and this task owns `packages/cli` only. A
|
|
18
|
+
* hand-written file that IMITATES a generated one — committed under `generated/`,
|
|
19
|
+
* with nothing that regenerates it — is worse than an honest source file: the
|
|
20
|
+
* next person edits it and their edit is silently the truth.
|
|
21
|
+
*
|
|
22
|
+
* So this is the honest intermediate: ONE declaration, inside the package that
|
|
23
|
+
* consumes it, shaped so a future generator emits exactly this array. The fields
|
|
24
|
+
* a generator would need and the CLI does not (`action`, `rest`, `mcp`,
|
|
25
|
+
* `because`, `fenced`, `write`) are carried anyway, so the day the contract
|
|
26
|
+
* package exists this file is deleted rather than reconciled.
|
|
27
|
+
*
|
|
28
|
+
* ── WHY IT MUST BE ONE LIST AND NOT 28 `.command()` CALLS ───────────────────
|
|
29
|
+
*
|
|
30
|
+
* `src/index.ts` calls `findUnknownSubcommand` BEFORE `parseAsync` and hard-fails
|
|
31
|
+
* on an unrecognised subcommand even with `--help` appended. A subcommand list
|
|
32
|
+
* that can drift from the thing that answers it therefore makes VALID commands
|
|
33
|
+
* fail with `cli_outdated`, rather than degrading. One array, walked once, cannot
|
|
34
|
+
* drift from itself.
|
|
35
|
+
*/
|
|
36
|
+
/* -------------------------------------------------------------------------- */
|
|
37
|
+
/* shared flag groups — declared once so two ops cannot spell the same filter */
|
|
38
|
+
/* two ways */
|
|
39
|
+
/* -------------------------------------------------------------------------- */
|
|
40
|
+
const CURSOR = {
|
|
41
|
+
flag: "--cursor <seq>",
|
|
42
|
+
key: "cursor",
|
|
43
|
+
describe: "Resume from this position (a cursor is a POSITION, not a time)",
|
|
44
|
+
in: "query",
|
|
45
|
+
};
|
|
46
|
+
const LIMIT = { flag: "--limit <n>", key: "limit", describe: "Maximum rows", in: "query", int: true };
|
|
47
|
+
const STATUS = { flag: "--status <status>", key: "status", describe: "Filter by status", in: "query" };
|
|
48
|
+
const ROLE = { flag: "--role <role>", key: "role", describe: "Filter by the declared role of the agent", in: "query" };
|
|
49
|
+
const TASK = { flag: "--task <id>", key: "task", describe: "Filter to one task", in: "query" };
|
|
50
|
+
/**
|
|
51
|
+
* `--because` is a `requiredOption`, never a positional, on every op that widens
|
|
52
|
+
* authority or destroys state. A positional reason is unquotable in practice and
|
|
53
|
+
* invisible in `--help`; and because commander validates required options before
|
|
54
|
+
* any action hook runs, a missing one already produces the CLI's structured
|
|
55
|
+
* `invalid_input` envelope for free (index.ts's CommanderError catch).
|
|
56
|
+
*/
|
|
57
|
+
const BECAUSE = {
|
|
58
|
+
flag: "--because <why>",
|
|
59
|
+
key: "because",
|
|
60
|
+
describe: "Why you are doing this. Recorded on the decision. REQUIRED",
|
|
61
|
+
in: "body",
|
|
62
|
+
required: true,
|
|
63
|
+
};
|
|
64
|
+
/**
|
|
65
|
+
* 🔴 THIS TEXT SAID "HERMES ONLY" AFTER THAT STOPPED BEING TRUE, AND THE HELP IS
|
|
66
|
+
* THE ONLY ACCOUNT MOST OPERATORS EVER READ.
|
|
67
|
+
*
|
|
68
|
+
* `unverified` is written by naive's own completion adjudicator onto
|
|
69
|
+
* `tasks_mirror`. A durable tenant's board used to come straight from the runtime,
|
|
70
|
+
* whose six states (`todo ready doing review done blocked`) carry no such value,
|
|
71
|
+
* so exit 3 really was unreachable there — and `naive teams tasks` announced that
|
|
72
|
+
* on stderr rather than let a CI job read exit 0 as "attested".
|
|
73
|
+
*
|
|
74
|
+
* The verification join removed the cause: `durableCard` joins `tasks_mirror` and
|
|
75
|
+
* the terminal column is naive's verdict. `commands/teams.ts` deleted the banner
|
|
76
|
+
* on exactly that ground and `legacy-commands-still-work.test.ts` asserts exit 3
|
|
77
|
+
* on a durable board. This string was left behind, so one binary shipped two
|
|
78
|
+
* contradictory accounts of one flag — and the one in `--help` said the gate
|
|
79
|
+
* cannot fire, which is the reading that makes an operator stop trusting it.
|
|
80
|
+
*
|
|
81
|
+
* WHAT IT SAYS NOW IS THE FLAG'S OWN CONTRACT AND NOTHING ABOUT THE LANE. The
|
|
82
|
+
* gate asks one question of one field, deliberately (see `teams.ts`: a CLI that
|
|
83
|
+
* branched on which runtime answered would be a second place the two lanes could
|
|
84
|
+
* diverge). So the caveat that belongs here is the one that is true on BOTH
|
|
85
|
+
* lanes: exit 0 means no card on this board reads `unverified`, which is not the
|
|
86
|
+
* same claim as "every card was adjudicated" — a board naive never scored reports
|
|
87
|
+
* no `unverified` for want of a verdict, not for want of a problem.
|
|
88
|
+
*/
|
|
89
|
+
const FAIL_ON_UNVERIFIED = {
|
|
90
|
+
flag: "--fail-on-unverified",
|
|
91
|
+
key: "failOnUnverified",
|
|
92
|
+
describe: "Exit 3 when a task's terminal state is `unverified` (a THIRD terminal state, not a success). " +
|
|
93
|
+
"Both lanes. Exit 0 means no card on this board reads `unverified` — NOT that every card was adjudicated",
|
|
94
|
+
in: "client",
|
|
95
|
+
};
|
|
96
|
+
const BOARD_FILTERS = [STATUS, ROLE, LIMIT, CURSOR];
|
|
97
|
+
const TEAM_ARG = { arg: "<team>", name: "team", describe: "The declared team", in: "path" };
|
|
98
|
+
/** The base every tenant-scoped path is built on. R-4: the addressing tuple is
|
|
99
|
+
* `(company, tenant, team)`, and the tuple is IN THE PATH rather than inferred. */
|
|
100
|
+
const BASE = "/v1/teams/{team}/tenants/{tenant}";
|
|
101
|
+
export const TEAM_OPS = [
|
|
102
|
+
{
|
|
103
|
+
cli: "list",
|
|
104
|
+
describe: "List the declared teams (the only subcommand that does not take --tenant)",
|
|
105
|
+
action: "teams.list",
|
|
106
|
+
method: "GET",
|
|
107
|
+
path: "/v1/teams",
|
|
108
|
+
tenant: false,
|
|
109
|
+
fenced: false,
|
|
110
|
+
write: false,
|
|
111
|
+
mcp: false,
|
|
112
|
+
because: "GET /v1/teams answers 501, so a tool over it could only ever refuse",
|
|
113
|
+
args: [],
|
|
114
|
+
flags: [{ flag: "--runtime <kind>", key: "runtime", describe: "durable | hermes", in: "query" }],
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
cli: "show",
|
|
118
|
+
describe: "Show one team's status for one tenant",
|
|
119
|
+
action: "teams.status",
|
|
120
|
+
method: "GET",
|
|
121
|
+
path: BASE,
|
|
122
|
+
tenant: true,
|
|
123
|
+
fenced: false,
|
|
124
|
+
write: false,
|
|
125
|
+
mcp: false,
|
|
126
|
+
because: "the MCP surface declares no such tool; a model reads the board, not the team header",
|
|
127
|
+
args: [TEAM_ARG],
|
|
128
|
+
flags: [],
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
cli: "plan",
|
|
132
|
+
describe: "The honesty report for one (team, tenant): digests, fence, caps, attestation parity",
|
|
133
|
+
action: "teams.plan",
|
|
134
|
+
method: "GET",
|
|
135
|
+
path: `${BASE}/plan`,
|
|
136
|
+
tenant: true,
|
|
137
|
+
fenced: false,
|
|
138
|
+
write: false,
|
|
139
|
+
mcp: false,
|
|
140
|
+
because: "the MCP surface declares no such tool; the honesty report is an operator artifact, and its central field (the manifest digest) has no source",
|
|
141
|
+
args: [TEAM_ARG],
|
|
142
|
+
flags: [
|
|
143
|
+
{
|
|
144
|
+
flag: "--detailed-exitcode",
|
|
145
|
+
key: "detailedExitcode",
|
|
146
|
+
describe: "Exit 2 when a diff exists (terraform's convention; exit 1 stays a real error)",
|
|
147
|
+
in: "client",
|
|
148
|
+
},
|
|
149
|
+
],
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
cli: "roster",
|
|
153
|
+
describe: "The declared agents of a team, and optionally the edges between them",
|
|
154
|
+
action: "teams.roster",
|
|
155
|
+
method: "GET",
|
|
156
|
+
path: `${BASE}/roster`,
|
|
157
|
+
tenant: true,
|
|
158
|
+
fenced: false,
|
|
159
|
+
write: false,
|
|
160
|
+
mcp: false,
|
|
161
|
+
because: "the MCP surface declares no such tool; can/approve/view are all null on this build, so it would report an authority it cannot see",
|
|
162
|
+
args: [TEAM_ARG],
|
|
163
|
+
flags: [{ flag: "--edges", key: "edges", describe: "Include the declared edges", in: "query" }],
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
cli: "board",
|
|
167
|
+
describe: "The team's board for one tenant",
|
|
168
|
+
action: "teams.board",
|
|
169
|
+
method: "GET",
|
|
170
|
+
path: `${BASE}/board`,
|
|
171
|
+
tenant: true,
|
|
172
|
+
fenced: false,
|
|
173
|
+
write: false,
|
|
174
|
+
mcp: "naive_teams_board",
|
|
175
|
+
args: [TEAM_ARG],
|
|
176
|
+
flags: BOARD_FILTERS,
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
cli: "submit",
|
|
180
|
+
describe: "Send work to a team. THE verb — one ActionId, one fence, every surface",
|
|
181
|
+
action: "teams.submit",
|
|
182
|
+
method: "POST",
|
|
183
|
+
path: `${BASE}/submit`,
|
|
184
|
+
tenant: true,
|
|
185
|
+
fenced: true,
|
|
186
|
+
write: true,
|
|
187
|
+
mcp: false,
|
|
188
|
+
because: "the MCP surface declares no such tool; POST …/submit answers 501, and a submit tool is the one verb that must not exist before the route does",
|
|
189
|
+
args: [TEAM_ARG, { arg: "<goal>", name: "goal", describe: "What you want done", in: "body" }],
|
|
190
|
+
flags: [
|
|
191
|
+
{ flag: "--task <id>", key: "task", describe: "Attach this goal to an existing task", in: "body" },
|
|
192
|
+
{
|
|
193
|
+
flag: "--brief <file>",
|
|
194
|
+
key: "brief",
|
|
195
|
+
describe: "Read the prose brief from a file, or `-` for stdin (prose on argv is mangled by every shell)",
|
|
196
|
+
in: "body",
|
|
197
|
+
},
|
|
198
|
+
{ flag: "--attest <claim>", key: "attest", describe: "The claim the result must attest to", in: "body" },
|
|
199
|
+
{ flag: "--parent <id>", key: "parent", describe: "Parent task id", in: "body" },
|
|
200
|
+
{
|
|
201
|
+
flag: "--idempotency-key <k>",
|
|
202
|
+
key: "idempotencyKey",
|
|
203
|
+
describe: "Forwarded as Idempotency-Key. Best-effort today — see `naive teams plan`",
|
|
204
|
+
in: "client",
|
|
205
|
+
},
|
|
206
|
+
],
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
cli: "say",
|
|
210
|
+
describe: "Say something into a team session",
|
|
211
|
+
action: "teams.say",
|
|
212
|
+
method: "POST",
|
|
213
|
+
path: `${BASE}/sessions/{channel}/messages`,
|
|
214
|
+
tenant: true,
|
|
215
|
+
fenced: true,
|
|
216
|
+
write: true,
|
|
217
|
+
mcp: false,
|
|
218
|
+
because: "the CLI `say` posts a SESSION message (…/sessions/{channel}/messages, 501 today); the MCP tool of the nearest name, naive_teams_comment, posts a TASK comment — a different operation, not this one",
|
|
219
|
+
args: [TEAM_ARG, { arg: "<text>", name: "text", describe: "What to say", in: "body" }],
|
|
220
|
+
flags: [
|
|
221
|
+
{ flag: "--channel <channel>", key: "channel", describe: "The session channel (default: main)", in: "path" },
|
|
222
|
+
{ flag: "--as <role>", key: "as", describe: "Speak as a declared role", in: "body" },
|
|
223
|
+
],
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
cli: "tasks",
|
|
227
|
+
describe: "The board as a task list",
|
|
228
|
+
action: "teams.board",
|
|
229
|
+
method: "GET",
|
|
230
|
+
path: `${BASE}/board`,
|
|
231
|
+
tenant: true,
|
|
232
|
+
fenced: false,
|
|
233
|
+
write: false,
|
|
234
|
+
mcp: false,
|
|
235
|
+
because: "an alias of `board`; naive_teams_board is the tool",
|
|
236
|
+
args: [TEAM_ARG],
|
|
237
|
+
flags: [...BOARD_FILTERS, FAIL_ON_UNVERIFIED],
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
cli: "task",
|
|
241
|
+
describe: "One card on the board",
|
|
242
|
+
action: "teams.board",
|
|
243
|
+
method: "GET",
|
|
244
|
+
path: `${BASE}/board/{card}`,
|
|
245
|
+
tenant: true,
|
|
246
|
+
fenced: false,
|
|
247
|
+
write: false,
|
|
248
|
+
mcp: false,
|
|
249
|
+
because: "the MCP surface declares no such tool; a model reads one card through naive_teams_board",
|
|
250
|
+
args: [TEAM_ARG, { arg: "<id>", name: "card", describe: "The card id", in: "path" }],
|
|
251
|
+
flags: [FAIL_ON_UNVERIFIED],
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
cli: "unblock",
|
|
255
|
+
describe: "Unblock a card",
|
|
256
|
+
action: "teams.unblock",
|
|
257
|
+
method: "POST",
|
|
258
|
+
path: `${BASE}/board/{card}/unblock`,
|
|
259
|
+
tenant: true,
|
|
260
|
+
fenced: true,
|
|
261
|
+
write: true,
|
|
262
|
+
mcp: "naive_teams_unblock",
|
|
263
|
+
args: [TEAM_ARG, { arg: "<id>", name: "card", describe: "The card id", in: "path" }],
|
|
264
|
+
flags: [BECAUSE],
|
|
265
|
+
},
|
|
266
|
+
{
|
|
267
|
+
cli: "runs",
|
|
268
|
+
describe: "The team's runs",
|
|
269
|
+
action: "teams.runs",
|
|
270
|
+
method: "GET",
|
|
271
|
+
path: `${BASE}/runs`,
|
|
272
|
+
tenant: true,
|
|
273
|
+
fenced: false,
|
|
274
|
+
write: false,
|
|
275
|
+
mcp: false,
|
|
276
|
+
because: "the MCP surface declares no such tool; naive_teams_run reads ONE run by id, it does not list them",
|
|
277
|
+
args: [TEAM_ARG],
|
|
278
|
+
// NOT [TASK, STATUS, …]. The runs handler reads exactly `limit` and
|
|
279
|
+
// `cursor`; `?task=` and `?status=` were declared as query flags, sent, and
|
|
280
|
+
// silently ignored, so filtering a busy tenant's runs returned the
|
|
281
|
+
// unfiltered list with no signal. openapi declares no such parameters
|
|
282
|
+
// either. They come back when the route supports them.
|
|
283
|
+
flags: [LIMIT, CURSOR],
|
|
284
|
+
},
|
|
285
|
+
{
|
|
286
|
+
cli: "watch",
|
|
287
|
+
describe: "Watch a run's transcript as NDJSON — one object per line, resumable",
|
|
288
|
+
action: "teams.runs",
|
|
289
|
+
method: "GET",
|
|
290
|
+
path: `${BASE}/runs/{run}/stream`,
|
|
291
|
+
tenant: true,
|
|
292
|
+
fenced: false,
|
|
293
|
+
write: false,
|
|
294
|
+
mcp: false,
|
|
295
|
+
because: "the MCP surface declares no such tool; …/runs/{id}/stream answers 501 and MCP has no streaming response",
|
|
296
|
+
streams: true,
|
|
297
|
+
args: [TEAM_ARG],
|
|
298
|
+
flags: [
|
|
299
|
+
{ flag: "--run <id>", key: "run", describe: "The run to watch (required)", in: "path", required: true },
|
|
300
|
+
{
|
|
301
|
+
flag: "--follow",
|
|
302
|
+
// What it decides is what happens at a TERMINAL frame, not how much
|
|
303
|
+
// of the stream is read: without it the command stops at the first
|
|
304
|
+
// `stream.end`, with it the connection stays open past it. Both
|
|
305
|
+
// modes drain the body — the old behaviour stopped after one
|
|
306
|
+
// network chunk, which truncated the transcript at an arbitrary
|
|
307
|
+
// point and still exited 0.
|
|
308
|
+
key: "follow",
|
|
309
|
+
describe: "Keep watching past a terminal frame (default: stop at the first stream.end)",
|
|
310
|
+
in: "client",
|
|
311
|
+
},
|
|
312
|
+
{ ...CURSOR, in: "client", describe: "Resume from this seq — sent as Last-Event-ID" },
|
|
313
|
+
{
|
|
314
|
+
flag: "--fail-on-terminal",
|
|
315
|
+
key: "failOnTerminal",
|
|
316
|
+
// Of the three, only `failed` is reachable on the durable runtime: a
|
|
317
|
+
// durable run's status vocabulary is `running | completed | failed`
|
|
318
|
+
// (`runState` in the runtime's naive.ts), so `unverified` and `blocked`
|
|
319
|
+
// are hermes states. The flag is not hermes-only the way
|
|
320
|
+
// `--fail-on-unverified` is — it keeps a real reach on both — so it is
|
|
321
|
+
// narrowed in the text rather than announced at runtime.
|
|
322
|
+
describe: "Exit 3 when the final status is unverified, failed or blocked (on the durable runtime only `failed` occurs)",
|
|
323
|
+
in: "client",
|
|
324
|
+
},
|
|
325
|
+
{
|
|
326
|
+
flag: "--show-args",
|
|
327
|
+
key: "showArgs",
|
|
328
|
+
describe: "Print tool arguments instead of args_digest. Requires an interactive terminal",
|
|
329
|
+
in: "client",
|
|
330
|
+
},
|
|
331
|
+
],
|
|
332
|
+
},
|
|
333
|
+
{
|
|
334
|
+
cli: "events",
|
|
335
|
+
describe: "The team's event log",
|
|
336
|
+
action: "teams.events",
|
|
337
|
+
method: "GET",
|
|
338
|
+
path: `${BASE}/events`,
|
|
339
|
+
tenant: true,
|
|
340
|
+
fenced: false,
|
|
341
|
+
write: false,
|
|
342
|
+
mcp: false,
|
|
343
|
+
because: "the MCP surface declares no such tool",
|
|
344
|
+
args: [TEAM_ARG],
|
|
345
|
+
flags: [TASK, CURSOR, LIMIT],
|
|
346
|
+
},
|
|
347
|
+
{
|
|
348
|
+
cli: "cost",
|
|
349
|
+
describe: "What the team spent",
|
|
350
|
+
action: "teams.cost",
|
|
351
|
+
method: "GET",
|
|
352
|
+
path: `${BASE}/cost`,
|
|
353
|
+
tenant: true,
|
|
354
|
+
fenced: false,
|
|
355
|
+
write: false,
|
|
356
|
+
mcp: false,
|
|
357
|
+
because: "the MCP surface declares no such tool; spend is an operator surface",
|
|
358
|
+
args: [TEAM_ARG],
|
|
359
|
+
flags: [
|
|
360
|
+
// `action` IS THE ONLY ACCEPTED VALUE. The route throws
|
|
361
|
+
// `invalid_input: by must be "action" (the only bucketing this build can
|
|
362
|
+
// compute)` for anything else (routes/teams.ts), so the three the describe
|
|
363
|
+
// string used to advertise — task | day | role — all 400.
|
|
364
|
+
{
|
|
365
|
+
flag: "--by <dimension>",
|
|
366
|
+
key: "by",
|
|
367
|
+
describe: 'action (the only value this build accepts)',
|
|
368
|
+
in: "query",
|
|
369
|
+
},
|
|
370
|
+
// `--period` IS NOT SENT. The route never reads `req.query.period`; the
|
|
371
|
+
// window comes from `spendWindow(accountKit.budget)`. A query flag the
|
|
372
|
+
// server ignores is the worst shape available — the response looks right
|
|
373
|
+
// and answers a different question — so it is `client` and the CLI prints
|
|
374
|
+
// the window the server actually used.
|
|
375
|
+
{
|
|
376
|
+
flag: "--period <period>",
|
|
377
|
+
key: "period",
|
|
378
|
+
describe: "IGNORED — the window is fixed by the account kit's budget period",
|
|
379
|
+
in: "client",
|
|
380
|
+
},
|
|
381
|
+
],
|
|
382
|
+
},
|
|
383
|
+
{
|
|
384
|
+
cli: "diagnose",
|
|
385
|
+
describe: "Diagnostics for a team that is not behaving",
|
|
386
|
+
action: "teams.diagnostics",
|
|
387
|
+
method: "GET",
|
|
388
|
+
path: `${BASE}/diagnostics`,
|
|
389
|
+
tenant: true,
|
|
390
|
+
fenced: false,
|
|
391
|
+
write: false,
|
|
392
|
+
mcp: false,
|
|
393
|
+
because: "the MCP surface declares no such tool",
|
|
394
|
+
args: [TEAM_ARG],
|
|
395
|
+
flags: [{ flag: "--kind <kind>", key: "kind", describe: "Restrict to one diagnostic kind", in: "query" }],
|
|
396
|
+
},
|
|
397
|
+
{
|
|
398
|
+
cli: "sessions",
|
|
399
|
+
describe: "The team's sessions",
|
|
400
|
+
action: "teams.sessions",
|
|
401
|
+
method: "GET",
|
|
402
|
+
path: `${BASE}/sessions`,
|
|
403
|
+
tenant: true,
|
|
404
|
+
fenced: false,
|
|
405
|
+
write: false,
|
|
406
|
+
mcp: false,
|
|
407
|
+
because: "the MCP surface declares no such tool; both session reads answer 501",
|
|
408
|
+
args: [TEAM_ARG],
|
|
409
|
+
flags: [CURSOR, LIMIT],
|
|
410
|
+
},
|
|
411
|
+
{
|
|
412
|
+
cli: "read",
|
|
413
|
+
describe: "Read one session channel",
|
|
414
|
+
action: "teams.sessions",
|
|
415
|
+
method: "GET",
|
|
416
|
+
path: `${BASE}/sessions/{channel}`,
|
|
417
|
+
tenant: true,
|
|
418
|
+
fenced: false,
|
|
419
|
+
write: false,
|
|
420
|
+
mcp: false,
|
|
421
|
+
because: "the MCP surface declares no such tool; both session reads answer 501",
|
|
422
|
+
args: [TEAM_ARG, { arg: "<channel>", name: "channel", describe: "The channel to read", in: "path" }],
|
|
423
|
+
flags: [CURSOR, LIMIT],
|
|
424
|
+
},
|
|
425
|
+
{
|
|
426
|
+
cli: "approvals",
|
|
427
|
+
describe: "Approvals this team is waiting on",
|
|
428
|
+
action: "teams.approvals",
|
|
429
|
+
method: "GET",
|
|
430
|
+
path: `${BASE}/approvals`,
|
|
431
|
+
tenant: true,
|
|
432
|
+
fenced: false,
|
|
433
|
+
write: false,
|
|
434
|
+
mcp: false,
|
|
435
|
+
because: "naive_approvals_list is the tool, and it is not a teams tool",
|
|
436
|
+
args: [TEAM_ARG],
|
|
437
|
+
flags: [STATUS],
|
|
438
|
+
},
|
|
439
|
+
{
|
|
440
|
+
cli: "decide",
|
|
441
|
+
describe: "Allow or deny one approval",
|
|
442
|
+
action: "teams.approval_decide",
|
|
443
|
+
method: "POST",
|
|
444
|
+
path: `${BASE}/approvals/{approval}/decide`,
|
|
445
|
+
tenant: true,
|
|
446
|
+
fenced: true,
|
|
447
|
+
write: true,
|
|
448
|
+
mcp: false,
|
|
449
|
+
because: "an agent may not resolve the gate meant to constrain it — assertHumanResolver refuses an agent key",
|
|
450
|
+
args: [TEAM_ARG, { arg: "<id>", name: "approval", describe: "The approval id", in: "path" }],
|
|
451
|
+
flags: [
|
|
452
|
+
{ flag: "--allow", key: "allow", describe: "Allow it", in: "body", field: "decision", value: "approve" },
|
|
453
|
+
{ flag: "--deny", key: "deny", describe: "Deny it", in: "body", field: "decision", value: "deny" },
|
|
454
|
+
BECAUSE,
|
|
455
|
+
],
|
|
456
|
+
},
|
|
457
|
+
{
|
|
458
|
+
cli: "effects",
|
|
459
|
+
describe: "Outstanding effects",
|
|
460
|
+
action: "teams.effects",
|
|
461
|
+
method: "GET",
|
|
462
|
+
path: `${BASE}/effects`,
|
|
463
|
+
tenant: true,
|
|
464
|
+
fenced: false,
|
|
465
|
+
write: false,
|
|
466
|
+
mcp: false,
|
|
467
|
+
because: "the MCP surface declares no such tool; the effects routes answer 501",
|
|
468
|
+
args: [TEAM_ARG],
|
|
469
|
+
flags: [{ flag: "--state <state>", key: "state", describe: "Filter by effect state", in: "query" }],
|
|
470
|
+
},
|
|
471
|
+
{
|
|
472
|
+
cli: "settle",
|
|
473
|
+
describe: "Settle one effect — done, compensated or rejected",
|
|
474
|
+
action: "teams.effect_settle",
|
|
475
|
+
method: "POST",
|
|
476
|
+
path: `${BASE}/effects/{effect}/settle`,
|
|
477
|
+
tenant: true,
|
|
478
|
+
fenced: true,
|
|
479
|
+
write: true,
|
|
480
|
+
mcp: false,
|
|
481
|
+
because: "the MCP surface declares no such tool; the effects routes answer 501",
|
|
482
|
+
args: [TEAM_ARG, { arg: "<id>", name: "effect", describe: "The effect id", in: "path" }],
|
|
483
|
+
flags: [
|
|
484
|
+
// Same selector shape as `decide`. openapi declares `{decision, because,
|
|
485
|
+
// receipt}` here too, with `decision` an unconstrained string — the route
|
|
486
|
+
// is a 501 refusal, so nothing narrows the value set yet. The values are
|
|
487
|
+
// therefore spelled exactly like their flags, so `--help` and the wire
|
|
488
|
+
// cannot drift into two names for one choice.
|
|
489
|
+
{ flag: "--done", key: "done", describe: "It completed", in: "body", field: "decision", value: "done" },
|
|
490
|
+
{ flag: "--compensate", key: "compensate", describe: "It was compensated", in: "body", field: "decision", value: "compensate" },
|
|
491
|
+
{ flag: "--reject", key: "reject", describe: "It was rejected", in: "body", field: "decision", value: "reject" },
|
|
492
|
+
{ flag: "--receipt <file>", key: "receipt", describe: "Read the receipt from a file, or `-` for stdin", in: "body" },
|
|
493
|
+
BECAUSE,
|
|
494
|
+
],
|
|
495
|
+
},
|
|
496
|
+
{
|
|
497
|
+
cli: "schedule",
|
|
498
|
+
describe: "Schedule recurring work for a team",
|
|
499
|
+
action: "teams.schedule",
|
|
500
|
+
method: "POST",
|
|
501
|
+
path: `${BASE}/schedule`,
|
|
502
|
+
tenant: true,
|
|
503
|
+
fenced: true,
|
|
504
|
+
write: true,
|
|
505
|
+
mcp: false,
|
|
506
|
+
because: "the MCP surface declares no such tool; the schedule routes answer 501",
|
|
507
|
+
args: [
|
|
508
|
+
TEAM_ARG,
|
|
509
|
+
{ arg: "<cron>", name: "cron", describe: "Cron expression", in: "body" },
|
|
510
|
+
{ arg: "<goal>", name: "goal", describe: "What to submit on each tick", in: "body" },
|
|
511
|
+
],
|
|
512
|
+
flags: [
|
|
513
|
+
{ flag: "--brief <file>", key: "brief", describe: "Read the prose brief from a file, or `-` for stdin", in: "body" },
|
|
514
|
+
{ flag: "--timezone <tz>", key: "timezone", describe: "IANA timezone for the cron expression", in: "body" },
|
|
515
|
+
],
|
|
516
|
+
},
|
|
517
|
+
{
|
|
518
|
+
cli: "unschedule",
|
|
519
|
+
// 06 §1.4 gives `schedule / unschedule` ONE row, one arg list and the path
|
|
520
|
+
// glob `…/schedule*`. A DELETE carrying a body to identify its own target is
|
|
521
|
+
// the shape that makes a proxy drop the request, so the id is in the path.
|
|
522
|
+
describe: "Remove a schedule",
|
|
523
|
+
action: "teams.schedule",
|
|
524
|
+
method: "DELETE",
|
|
525
|
+
path: `${BASE}/schedule/{schedule}`,
|
|
526
|
+
tenant: true,
|
|
527
|
+
fenced: true,
|
|
528
|
+
write: true,
|
|
529
|
+
mcp: false,
|
|
530
|
+
because: "the MCP surface declares no such tool; the schedule routes answer 501",
|
|
531
|
+
args: [TEAM_ARG, { arg: "<id>", name: "schedule", describe: "The schedule id", in: "path" }],
|
|
532
|
+
flags: [],
|
|
533
|
+
},
|
|
534
|
+
{
|
|
535
|
+
// 🔴 THIS ROW WAS MISSING, AND IT IS A STEP OF THE CORE LOOP.
|
|
536
|
+
//
|
|
537
|
+
// `POST /v1/teams/{team}/tenants/{tenant}/apply` has been mounted the whole
|
|
538
|
+
// time and no CLI verb reached it — so "declare a team, apply it, submit
|
|
539
|
+
// work" had a hole in the middle that only a hand-written curl could cross.
|
|
540
|
+
// The other 26 rows were derived from the route table; this one was not,
|
|
541
|
+
// because a route with no CLI verb is invisible to a reader checking the CLI
|
|
542
|
+
// against itself.
|
|
543
|
+
//
|
|
544
|
+
// `--expected-digest` is REQUIRED and has no default. Apply exists to raise
|
|
545
|
+
// `409 stale_expected_digest` — to refuse when the compiled company has
|
|
546
|
+
// moved since you last looked — and an apply that sends whatever digest is
|
|
547
|
+
// current cannot ever raise it. A flag defaulted to "whatever you have"
|
|
548
|
+
// turns a concurrency check into a rubber stamp. `naive teams plan` prints
|
|
549
|
+
// the digest to pass.
|
|
550
|
+
cli: "apply",
|
|
551
|
+
describe: "Record that an operator applied this exact manifest digest. Refuses a stale one",
|
|
552
|
+
action: "teams.apply",
|
|
553
|
+
method: "POST",
|
|
554
|
+
path: `${BASE}/apply`,
|
|
555
|
+
tenant: true,
|
|
556
|
+
fenced: true,
|
|
557
|
+
write: true,
|
|
558
|
+
mcp: false,
|
|
559
|
+
because: "applying a manifest is the act that makes a capability change something a human reviewed; an agent applying its own manifest reviews nothing",
|
|
560
|
+
args: [TEAM_ARG],
|
|
561
|
+
flags: [
|
|
562
|
+
{
|
|
563
|
+
flag: "--expected-digest <digest>",
|
|
564
|
+
key: "expectedDigest",
|
|
565
|
+
describe: "REQUIRED. The digest you are applying, from `naive teams plan`. A stale one is refused",
|
|
566
|
+
in: "body",
|
|
567
|
+
field: "expected_digest",
|
|
568
|
+
required: true,
|
|
569
|
+
},
|
|
570
|
+
// NO `--because`, deliberately, and it is the one fenced op without one.
|
|
571
|
+
// The published contract for this operation declares exactly one body
|
|
572
|
+
// property — `expected_digest` — and the CLI may not send a key the
|
|
573
|
+
// contract does not declare (the gate below enforces that, and it caught
|
|
574
|
+
// this row). More importantly the reason would be redundant: the artefact
|
|
575
|
+
// that records what an operator approved is the DIGEST, which names the
|
|
576
|
+
// exact compiled manifest, and a free-text sentence beside it adds a
|
|
577
|
+
// second, weaker account of the same decision.
|
|
578
|
+
],
|
|
579
|
+
},
|
|
580
|
+
{
|
|
581
|
+
cli: "model",
|
|
582
|
+
describe: "Change the model a team runs on",
|
|
583
|
+
action: "teams.model",
|
|
584
|
+
method: "POST",
|
|
585
|
+
path: `${BASE}/model`,
|
|
586
|
+
tenant: true,
|
|
587
|
+
fenced: true,
|
|
588
|
+
write: true,
|
|
589
|
+
mcp: false,
|
|
590
|
+
because: "the MCP surface declares no such tool; a model must not choose its own model",
|
|
591
|
+
args: [TEAM_ARG, { arg: "<modelId>", name: "modelId", describe: "The model id", in: "body", field: "model_id" }],
|
|
592
|
+
flags: [BECAUSE],
|
|
593
|
+
},
|
|
594
|
+
{
|
|
595
|
+
cli: "stop",
|
|
596
|
+
describe: "Stop a team",
|
|
597
|
+
action: "teams.stop",
|
|
598
|
+
method: "POST",
|
|
599
|
+
path: `${BASE}/stop`,
|
|
600
|
+
tenant: true,
|
|
601
|
+
fenced: true,
|
|
602
|
+
write: true,
|
|
603
|
+
mcp: false,
|
|
604
|
+
because: "the MCP surface declares no such tool; naive_teams_run reads a run, it does not stop one",
|
|
605
|
+
args: [TEAM_ARG],
|
|
606
|
+
flags: [BECAUSE],
|
|
607
|
+
},
|
|
608
|
+
{
|
|
609
|
+
cli: "migrate",
|
|
610
|
+
describe: "Move this tenant onto the durable runtime",
|
|
611
|
+
action: "teams.migrate",
|
|
612
|
+
method: "POST",
|
|
613
|
+
path: `${BASE}/migrate`,
|
|
614
|
+
tenant: true,
|
|
615
|
+
fenced: true,
|
|
616
|
+
write: true,
|
|
617
|
+
// An agent must not move its own tenant between runtimes: the cutover
|
|
618
|
+
// destroys the legacy coordinates irreversibly. Declared, not omitted, so a
|
|
619
|
+
// four-way parity check sees an exemption rather than a hole.
|
|
620
|
+
mcp: false,
|
|
621
|
+
because: "an agent must not move its own tenant between runtimes; the cutover is irreversible",
|
|
622
|
+
args: [TEAM_ARG],
|
|
623
|
+
flags: [{ flag: "--dry-run", key: "dryRun", describe: "Report what would move; change nothing", in: "body" }, BECAUSE],
|
|
624
|
+
},
|
|
625
|
+
{
|
|
626
|
+
cli: "rollback",
|
|
627
|
+
describe: "Move this tenant back off the durable runtime. DESTROYS the workspace",
|
|
628
|
+
action: "teams.rollback",
|
|
629
|
+
method: "POST",
|
|
630
|
+
path: `${BASE}/rollback`,
|
|
631
|
+
tenant: true,
|
|
632
|
+
fenced: true,
|
|
633
|
+
write: true,
|
|
634
|
+
mcp: false,
|
|
635
|
+
because: "an agent must not move its own tenant between runtimes; the rollback destroys /data",
|
|
636
|
+
args: [TEAM_ARG],
|
|
637
|
+
flags: [
|
|
638
|
+
{
|
|
639
|
+
// A flag a script cannot accidentally satisfy is the honest interlock:
|
|
640
|
+
// the rollback re-claims the slot, so /data is gone with it.
|
|
641
|
+
flag: "--i-understand-data-loss",
|
|
642
|
+
key: "iUnderstandDataLoss",
|
|
643
|
+
describe: "Required. The workspace is destroyed by this operation and is not recoverable",
|
|
644
|
+
in: "body",
|
|
645
|
+
},
|
|
646
|
+
BECAUSE,
|
|
647
|
+
],
|
|
648
|
+
},
|
|
649
|
+
];
|
|
650
|
+
/* -------------------------------------------------------------------------- */
|
|
651
|
+
/* pure builders — everything the runner needs, testable without a network */
|
|
652
|
+
/* -------------------------------------------------------------------------- */
|
|
653
|
+
export class TeamOpError extends Error {
|
|
654
|
+
}
|
|
655
|
+
/** Every `{placeholder}` this op's path expects. */
|
|
656
|
+
export function pathPlaceholders(op) {
|
|
657
|
+
return [...op.path.matchAll(/\{([a-zA-Z]+)\}/g)].map((m) => m[1]);
|
|
658
|
+
}
|
|
659
|
+
/**
|
|
660
|
+
* Fill the path. Every value is `encodeURIComponent`'d — a team name or a card id
|
|
661
|
+
* that contains a slash must not be able to reach a different route, and an
|
|
662
|
+
* absent value is an error rather than the literal string "undefined" in a URL.
|
|
663
|
+
*/
|
|
664
|
+
export function buildPath(op, values) {
|
|
665
|
+
return op.path.replace(/\{([a-zA-Z]+)\}/g, (_m, name) => {
|
|
666
|
+
const v = values[name];
|
|
667
|
+
if (v === undefined || v === "") {
|
|
668
|
+
throw new TeamOpError(`naive teams ${op.cli}: missing value for path segment {${name}}`);
|
|
669
|
+
}
|
|
670
|
+
return encodeURIComponent(v);
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
/** The query string, including its leading `?`, or `""`. */
|
|
674
|
+
export function buildQuery(op, opts) {
|
|
675
|
+
const params = new URLSearchParams();
|
|
676
|
+
for (const f of op.flags) {
|
|
677
|
+
if (f.in !== "query")
|
|
678
|
+
continue;
|
|
679
|
+
const raw = opts[f.key];
|
|
680
|
+
if (raw === undefined || raw === false || raw === null)
|
|
681
|
+
continue;
|
|
682
|
+
params.set(f.field ?? f.key, raw === true ? "true" : String(raw));
|
|
683
|
+
}
|
|
684
|
+
const s = params.toString();
|
|
685
|
+
return s ? `?${s}` : "";
|
|
686
|
+
}
|
|
687
|
+
/**
|
|
688
|
+
* The request body: positional args declared `in: "body"`, then flags declared
|
|
689
|
+
* `in: "body"`. A GET has no body at all — a GET that carries one is a request
|
|
690
|
+
* some proxy will strip and some server will ignore, which is a silent wrong
|
|
691
|
+
* answer rather than an error.
|
|
692
|
+
*/
|
|
693
|
+
export function buildBody(op, args, opts) {
|
|
694
|
+
if (op.method === "GET")
|
|
695
|
+
return undefined;
|
|
696
|
+
const body = {};
|
|
697
|
+
for (const a of op.args) {
|
|
698
|
+
if (a.in !== "body")
|
|
699
|
+
continue;
|
|
700
|
+
const v = args[a.name];
|
|
701
|
+
if (v !== undefined)
|
|
702
|
+
body[a.field ?? a.name] = v;
|
|
703
|
+
}
|
|
704
|
+
for (const f of op.flags) {
|
|
705
|
+
if (f.in !== "body")
|
|
706
|
+
continue;
|
|
707
|
+
const v = opts[f.key];
|
|
708
|
+
if (v === undefined || v === false || v === null)
|
|
709
|
+
continue;
|
|
710
|
+
// A selector flag sends its declared VALUE under a shared field, not `true`
|
|
711
|
+
// under its own name. See `FlagSpec.value`.
|
|
712
|
+
body[f.field ?? snake(f.key)] = f.value !== undefined ? f.value : v;
|
|
713
|
+
}
|
|
714
|
+
return body;
|
|
715
|
+
}
|
|
716
|
+
/**
|
|
717
|
+
* The mutually exclusive selector groups on this op, keyed by the wire field
|
|
718
|
+
* they share. DERIVED from the flags rather than listed, so a third member added
|
|
719
|
+
* to a group joins the exclusion without anyone remembering to widen a list.
|
|
720
|
+
*/
|
|
721
|
+
export function selectorGroups(op) {
|
|
722
|
+
const groups = new Map();
|
|
723
|
+
for (const f of op.flags) {
|
|
724
|
+
if (f.value === undefined)
|
|
725
|
+
continue;
|
|
726
|
+
if (f.field === undefined) {
|
|
727
|
+
throw new TeamOpError(`teams-catalog: ${op.cli} --${f.key} declares a value with no field`);
|
|
728
|
+
}
|
|
729
|
+
const g = groups.get(f.field) ?? [];
|
|
730
|
+
g.push(f);
|
|
731
|
+
groups.set(f.field, g);
|
|
732
|
+
}
|
|
733
|
+
return groups;
|
|
734
|
+
}
|
|
735
|
+
/**
|
|
736
|
+
* Refuse an ambiguous or absent decision BEFORE the request leaves the process.
|
|
737
|
+
*
|
|
738
|
+
* A selector group is a CHOICE: exactly one member must be given. Neither branch
|
|
739
|
+
* can be left to the server. `--allow --deny` would otherwise resolve by flag
|
|
740
|
+
* order in this file — an approval decided by declaration order — and giving
|
|
741
|
+
* neither would send a body with no `decision`, which `settle` (whose openapi
|
|
742
|
+
* marks nothing required) would accept as a well-formed request that settles an
|
|
743
|
+
* effect no particular way.
|
|
744
|
+
*/
|
|
745
|
+
export function assertSelectors(op, opts) {
|
|
746
|
+
for (const [field, flags] of selectorGroups(op)) {
|
|
747
|
+
const chosen = flags.filter((f) => opts[f.key] === true);
|
|
748
|
+
const spellings = flags.map((f) => `--${f.key}`).join(" or ");
|
|
749
|
+
if (chosen.length === 0) {
|
|
750
|
+
throw new TeamOpError(`naive teams ${op.cli}: ${spellings} is required (it is the \`${field}\` this sends).`);
|
|
751
|
+
}
|
|
752
|
+
if (chosen.length > 1) {
|
|
753
|
+
throw new TeamOpError(`naive teams ${op.cli}: ${chosen.map((f) => `--${f.key}`).join(" and ")} are mutually exclusive — pass exactly one.`);
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
/** `failOnTerminal` -> `fail_on_terminal`. Flags are camelCase in commander and
|
|
758
|
+
* snake_case on this API's wire; converting in one place keeps the two apart. */
|
|
759
|
+
export function snake(key) {
|
|
760
|
+
return key.replace(/[A-Z]/g, (c) => `_${c.toLowerCase()}`);
|
|
761
|
+
}
|
|
762
|
+
export function findOp(cli) {
|
|
763
|
+
return TEAM_OPS.find((o) => o.cli === cli);
|
|
764
|
+
}
|
|
765
|
+
//# sourceMappingURL=teams-catalog.js.map
|