@bli-cockpit/cli 0.2.53 → 0.2.55
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/dist/adapters/attribution-core-fallbacks.js +247 -0
- package/dist/adapters/attribution-core-paths.js +182 -0
- package/dist/adapters/attribution-core-score.js +159 -0
- package/dist/adapters/attribution-core-types.js +13 -0
- package/dist/adapters/attribution-core.js +13 -565
- package/dist/adapters/claude-attribution-discovery.js +186 -0
- package/dist/adapters/claude-attribution-score.js +204 -0
- package/dist/adapters/claude-attribution-signals.js +180 -0
- package/dist/adapters/claude-attribution-types.js +25 -0
- package/dist/adapters/claude-attribution.js +14 -569
- package/dist/commands/doctor-access.js +129 -0
- package/dist/commands/doctor-pipeline.js +326 -0
- package/dist/commands/doctor-registration.js +105 -0
- package/dist/commands/doctor-report.js +111 -0
- package/dist/commands/doctor-update.js +120 -0
- package/dist/commands/doctor.js +8 -753
- package/dist/commands/heartbeat.js +8 -0
- package/dist/commands/jarvis-contracts.js +8 -0
- package/dist/commands/jarvis-render.js +413 -0
- package/dist/commands/jarvis-turn.js +305 -0
- package/dist/commands/jarvis.js +23 -698
- package/dist/commands/local-args-collector-setup.js +250 -0
- package/dist/commands/local-args-collector-status.js +227 -0
- package/dist/commands/local-args-collector-work.js +175 -0
- package/dist/commands/local-args-collector.js +19 -624
- package/dist/commands/local-args-tower-admin.js +456 -0
- package/dist/commands/local-args-tower-chat.js +194 -0
- package/dist/commands/local-args-tower-pages.js +314 -0
- package/dist/commands/local-args-tower.js +13 -880
- package/dist/commands/local-help.js +10 -2
- package/dist/commands/onboard-completion.js +136 -0
- package/dist/commands/onboard-flows.js +165 -0
- package/dist/commands/onboard-setup.js +102 -0
- package/dist/commands/onboard.js +5 -392
- package/dist/commands/public-root.js +1 -1
- package/dist/commands/session-sync-counters.js +55 -0
- package/dist/commands/session-sync-health.js +8 -1
- package/dist/commands/session-sync-plan.js +47 -7
- package/dist/commands/session-sync-scan.js +4 -4
- package/dist/commands/session-sync.js +6 -0
- package/dist/commands/settings-render.js +27 -0
- package/dist/commands/sync-followups.js +5 -1
- package/dist/commands/sync.js +5 -1
- package/dist/commands/team-device-reasons.js +16 -0
- package/dist/commands/team.js +87 -7
- package/dist/evidence-upload-client.js +14 -763
- package/dist/evidence-upload-object.js +181 -0
- package/dist/evidence-upload-plan.js +233 -0
- package/dist/evidence-upload-terminal.js +309 -0
- package/dist/evidence-upload-transport.js +104 -0
- package/dist/spool/local-spool-io.js +122 -0
- package/dist/spool/local-spool-mutations.js +174 -0
- package/dist/spool/local-spool-parse.js +143 -0
- package/dist/spool/local-spool-types.js +22 -0
- package/dist/spool/local-spool.js +20 -426
- package/dist/upload-evidence-delivery-offer.js +144 -0
- package/dist/upload-evidence-delivery-reconcile.js +134 -0
- package/dist/upload-evidence-delivery-summary.js +205 -0
- package/dist/upload-evidence-delivery.js +12 -482
- package/package.json +3 -3
|
@@ -0,0 +1,456 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The commands that manage this machine's account or watch the pipeline
|
|
3
|
+
* rather than carry on a conversation: `cockpit scout`, `cockpit ops`,
|
|
4
|
+
* `cockpit slack`, `cockpit settings`, `cockpit team`, and `cockpit model`.
|
|
5
|
+
* Split out of local-args-tower.ts (BLI-3636).
|
|
6
|
+
*
|
|
7
|
+
* Every parser moved verbatim: same flags, same defaults, same refusal
|
|
8
|
+
* sentences. The two constants this family publishes
|
|
9
|
+
* (`SCOUT_MIN_PREFIX_LENGTH`, `SLACK_WORKSPACE_KEYS`) are re-exported from
|
|
10
|
+
* `./local-args-tower.js`, the address they have always had.
|
|
11
|
+
*/
|
|
12
|
+
import { optionalEmail, optionalNonEmpty, optionalPositiveInteger, optionalUrl, parseNamedArgs, } from "./local-arg-values.js";
|
|
13
|
+
import { isTeamDeviceRevokeReasonLabel, TEAM_DEVICE_REVOKE_REASON_LABELS, } from "./team-device-reasons.js";
|
|
14
|
+
/** The shortest prefix `cockpit scout start` will resolve. Below this, ids collide. */
|
|
15
|
+
export const SCOUT_MIN_PREFIX_LENGTH = 6;
|
|
16
|
+
/**
|
|
17
|
+
* `cockpit scout [--days <n>]` reads the board; `cockpit scout start|dismiss|undo
|
|
18
|
+
* <id>` moves one card. Positional shape modelled on `autostart`: an optional
|
|
19
|
+
* action word first, then what it acts on.
|
|
20
|
+
*/
|
|
21
|
+
export function parseScoutArgs(args) {
|
|
22
|
+
const values = parseNamedArgs(args, {
|
|
23
|
+
allowedFlags: ["--home", "--dashboard-url", "--days", "--json"],
|
|
24
|
+
valueFlags: ["--home", "--dashboard-url", "--days"],
|
|
25
|
+
});
|
|
26
|
+
if (values.positionals.length > 2) {
|
|
27
|
+
throw new Error("scout accepts at most an action (start|dismiss|undo) and one experiment id.");
|
|
28
|
+
}
|
|
29
|
+
const [rawAction, rawRef] = values.positionals;
|
|
30
|
+
const base = {
|
|
31
|
+
homeDir: optionalNonEmpty(values.flags.get("--home")),
|
|
32
|
+
dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")),
|
|
33
|
+
days: optionalPositiveInteger(values.flags.get("--days"), "--days"),
|
|
34
|
+
json: values.booleans.has("--json"),
|
|
35
|
+
};
|
|
36
|
+
if (!rawAction)
|
|
37
|
+
return { kind: "scout", action: "board", ...base };
|
|
38
|
+
if (rawAction !== "start" && rawAction !== "dismiss" && rawAction !== "undo") {
|
|
39
|
+
throw new Error("scout action must be start, dismiss, or undo.");
|
|
40
|
+
}
|
|
41
|
+
const experimentRef = optionalNonEmpty(rawRef);
|
|
42
|
+
if (!experimentRef) {
|
|
43
|
+
throw new Error(`scout ${rawAction} needs an experiment id from the board.`);
|
|
44
|
+
}
|
|
45
|
+
if (experimentRef.length < SCOUT_MIN_PREFIX_LENGTH) {
|
|
46
|
+
throw new Error(`scout ${rawAction} needs a full experiment id or a prefix of at least ${SCOUT_MIN_PREFIX_LENGTH} characters.`);
|
|
47
|
+
}
|
|
48
|
+
return { kind: "scout", action: rawAction, experimentRef, ...base };
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* `cockpit ops status [--job <id>] [--skips]` and
|
|
52
|
+
* `cockpit ops recompile --person <p> [--dry-run]` (BLI-3462).
|
|
53
|
+
*
|
|
54
|
+
* There is deliberately **no `--cadence`** on `recompile`. The compile path
|
|
55
|
+
* behind it (`writePageAgain` → `publishPage`) takes no cadence and writes a
|
|
56
|
+
* daily; accepting the flag and dropping it is the silent breakage BLI-2490
|
|
57
|
+
* forbids, and threading cadence through that path is its own change. Ask for a
|
|
58
|
+
* weekly or a monthly with the compile script, which does support it.
|
|
59
|
+
*/
|
|
60
|
+
export function parseOpsArgs(args) {
|
|
61
|
+
const values = parseNamedArgs(args, {
|
|
62
|
+
allowedFlags: [
|
|
63
|
+
"--home",
|
|
64
|
+
"--dashboard-url",
|
|
65
|
+
"--job",
|
|
66
|
+
"--skips",
|
|
67
|
+
"--person",
|
|
68
|
+
"--dry-run",
|
|
69
|
+
"--json",
|
|
70
|
+
],
|
|
71
|
+
valueFlags: ["--home", "--dashboard-url", "--job", "--person"],
|
|
72
|
+
});
|
|
73
|
+
if (values.positionals.length > 1) {
|
|
74
|
+
throw new Error("ops accepts one action: status or recompile.");
|
|
75
|
+
}
|
|
76
|
+
const rawAction = values.positionals[0] ?? "status";
|
|
77
|
+
if (rawAction !== "status" && rawAction !== "recompile") {
|
|
78
|
+
throw new Error("ops action must be status or recompile.");
|
|
79
|
+
}
|
|
80
|
+
const base = {
|
|
81
|
+
homeDir: optionalNonEmpty(values.flags.get("--home")),
|
|
82
|
+
dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")),
|
|
83
|
+
json: values.booleans.has("--json"),
|
|
84
|
+
};
|
|
85
|
+
if (rawAction === "status") {
|
|
86
|
+
return {
|
|
87
|
+
kind: "ops",
|
|
88
|
+
action: "status",
|
|
89
|
+
job: optionalNonEmpty(values.flags.get("--job")),
|
|
90
|
+
skips: values.booleans.has("--skips"),
|
|
91
|
+
...base,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
const person = optionalNonEmpty(values.flags.get("--person"));
|
|
95
|
+
if (!person) {
|
|
96
|
+
throw new Error("ops recompile needs --person <email|name|id>. Nothing is recompiled by default, on purpose.");
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
kind: "ops",
|
|
100
|
+
action: "recompile",
|
|
101
|
+
person,
|
|
102
|
+
dryRun: values.booleans.has("--dry-run"),
|
|
103
|
+
...base,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
/** The workspaces Cockpit collects. A typo is refused here rather than searched for. */
|
|
107
|
+
export const SLACK_WORKSPACE_KEYS = ["bli", "blue_pearl"];
|
|
108
|
+
/**
|
|
109
|
+
* `cockpit slack coverage [--workspace <key>] [--stale-only]` and
|
|
110
|
+
* `cockpit slack read [--person|--channel|--query|--since|--until|--limit]`
|
|
111
|
+
* (BLI-3462).
|
|
112
|
+
*/
|
|
113
|
+
export function parseSlackArgs(args) {
|
|
114
|
+
const values = parseNamedArgs(args, {
|
|
115
|
+
allowedFlags: [
|
|
116
|
+
"--home",
|
|
117
|
+
"--dashboard-url",
|
|
118
|
+
"--workspace",
|
|
119
|
+
"--stale-only",
|
|
120
|
+
"--person",
|
|
121
|
+
"--channel",
|
|
122
|
+
"--query",
|
|
123
|
+
"--since",
|
|
124
|
+
"--until",
|
|
125
|
+
"--limit",
|
|
126
|
+
"--json",
|
|
127
|
+
],
|
|
128
|
+
valueFlags: [
|
|
129
|
+
"--home",
|
|
130
|
+
"--dashboard-url",
|
|
131
|
+
"--workspace",
|
|
132
|
+
"--person",
|
|
133
|
+
"--channel",
|
|
134
|
+
"--query",
|
|
135
|
+
"--since",
|
|
136
|
+
"--until",
|
|
137
|
+
"--limit",
|
|
138
|
+
],
|
|
139
|
+
});
|
|
140
|
+
if (values.positionals.length > 1) {
|
|
141
|
+
throw new Error("slack accepts one action: coverage or read.");
|
|
142
|
+
}
|
|
143
|
+
const rawAction = values.positionals[0] ?? "coverage";
|
|
144
|
+
if (rawAction !== "coverage" && rawAction !== "read") {
|
|
145
|
+
throw new Error("slack action must be coverage or read.");
|
|
146
|
+
}
|
|
147
|
+
const base = {
|
|
148
|
+
homeDir: optionalNonEmpty(values.flags.get("--home")),
|
|
149
|
+
dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")),
|
|
150
|
+
json: values.booleans.has("--json"),
|
|
151
|
+
};
|
|
152
|
+
if (rawAction === "coverage") {
|
|
153
|
+
// `--workspace` here is a Slack workspace key, NOT the collection-root
|
|
154
|
+
// `--workspace <path>` every setup command takes. Named at the point of use
|
|
155
|
+
// so the two cannot be confused silently.
|
|
156
|
+
const workspace = optionalNonEmpty(values.flags.get("--workspace"));
|
|
157
|
+
if (workspace && !SLACK_WORKSPACE_KEYS.includes(workspace)) {
|
|
158
|
+
throw new Error(`slack --workspace must be one of ${SLACK_WORKSPACE_KEYS.join(", ")}; got ${workspace}.`);
|
|
159
|
+
}
|
|
160
|
+
return {
|
|
161
|
+
kind: "slack",
|
|
162
|
+
action: "coverage",
|
|
163
|
+
workspace,
|
|
164
|
+
staleOnly: values.booleans.has("--stale-only"),
|
|
165
|
+
...base,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
const person = optionalNonEmpty(values.flags.get("--person"));
|
|
169
|
+
const channel = optionalNonEmpty(values.flags.get("--channel"));
|
|
170
|
+
const query = optionalNonEmpty(values.flags.get("--query"));
|
|
171
|
+
if (!person && !channel && !query) {
|
|
172
|
+
throw new Error("slack read needs at least one of --person, --channel or --query. An unfiltered dump of every " +
|
|
173
|
+
"message is not something this command offers.");
|
|
174
|
+
}
|
|
175
|
+
return {
|
|
176
|
+
kind: "slack",
|
|
177
|
+
action: "read",
|
|
178
|
+
person,
|
|
179
|
+
channel,
|
|
180
|
+
query,
|
|
181
|
+
since: optionalNonEmpty(values.flags.get("--since")),
|
|
182
|
+
until: optionalNonEmpty(values.flags.get("--until")),
|
|
183
|
+
limit: optionalPositiveInteger(values.flags.get("--limit"), "--limit"),
|
|
184
|
+
...base,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
const SETTINGS_SECTIONS = ["personal", "switches", "models", "env", "cli-floor"];
|
|
188
|
+
/**
|
|
189
|
+
* `cockpit settings [section] [verb] [args]` (BLI-3461).
|
|
190
|
+
*
|
|
191
|
+
* The flag set is the UNION across sections and the combinations are checked
|
|
192
|
+
* afterwards, per section — the same shape `parseDoctorArgs` uses. A flag that
|
|
193
|
+
* belongs to another section is refused by name rather than silently ignored,
|
|
194
|
+
* because a dropped `--project` on an env write is a change a person believes
|
|
195
|
+
* they made.
|
|
196
|
+
*/
|
|
197
|
+
export function parseSettingsArgs(args) {
|
|
198
|
+
const values = parseNamedArgs(args, {
|
|
199
|
+
allowedFlags: [
|
|
200
|
+
"--home",
|
|
201
|
+
"--dashboard-url",
|
|
202
|
+
"--json",
|
|
203
|
+
"--chat-model",
|
|
204
|
+
"--brief-model",
|
|
205
|
+
"--chat",
|
|
206
|
+
"--memory",
|
|
207
|
+
"--project",
|
|
208
|
+
"--file",
|
|
209
|
+
"--id",
|
|
210
|
+
"--content-stdin",
|
|
211
|
+
"--yes",
|
|
212
|
+
],
|
|
213
|
+
valueFlags: [
|
|
214
|
+
"--home",
|
|
215
|
+
"--dashboard-url",
|
|
216
|
+
"--chat-model",
|
|
217
|
+
"--brief-model",
|
|
218
|
+
"--chat",
|
|
219
|
+
"--memory",
|
|
220
|
+
"--project",
|
|
221
|
+
"--file",
|
|
222
|
+
"--id",
|
|
223
|
+
],
|
|
224
|
+
});
|
|
225
|
+
const [rawSection, ...rest] = values.positionals;
|
|
226
|
+
const common = {
|
|
227
|
+
homeDir: optionalNonEmpty(values.flags.get("--home")),
|
|
228
|
+
dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")),
|
|
229
|
+
yes: values.booleans.has("--yes"),
|
|
230
|
+
json: values.booleans.has("--json"),
|
|
231
|
+
};
|
|
232
|
+
if (!rawSection) {
|
|
233
|
+
if (rest.length > 0)
|
|
234
|
+
throw new Error("settings does not accept that argument.");
|
|
235
|
+
return { kind: "settings", section: "overview", action: "show", ...common };
|
|
236
|
+
}
|
|
237
|
+
if (!SETTINGS_SECTIONS.includes(rawSection)) {
|
|
238
|
+
throw new Error(`settings section must be one of ${SETTINGS_SECTIONS.join(", ")} — or nothing, for all of them.`);
|
|
239
|
+
}
|
|
240
|
+
const section = rawSection;
|
|
241
|
+
if (section === "personal") {
|
|
242
|
+
if (rest.length > 0)
|
|
243
|
+
throw new Error("settings personal does not accept positional arguments.");
|
|
244
|
+
const chatModel = optionalNonEmpty(values.flags.get("--chat-model"));
|
|
245
|
+
const briefModel = optionalNonEmpty(values.flags.get("--brief-model"));
|
|
246
|
+
return {
|
|
247
|
+
kind: "settings",
|
|
248
|
+
section,
|
|
249
|
+
action: chatModel || briefModel ? "set" : "show",
|
|
250
|
+
chatModel,
|
|
251
|
+
briefModel,
|
|
252
|
+
...common,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
if (section === "switches") {
|
|
256
|
+
if (rest.length === 0) {
|
|
257
|
+
return { kind: "settings", section, action: "show", ...common };
|
|
258
|
+
}
|
|
259
|
+
if (rest[0] !== "set") {
|
|
260
|
+
throw new Error("settings switches takes no verb, or `set <key> <value>`.");
|
|
261
|
+
}
|
|
262
|
+
const [, key, value, ...extra] = rest;
|
|
263
|
+
if (!key || !value || extra.length > 0) {
|
|
264
|
+
throw new Error("settings switches set needs exactly a key and a value.");
|
|
265
|
+
}
|
|
266
|
+
return { kind: "settings", section, action: "set", switchKey: key, switchValue: value, ...common };
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* BLI-3557: `settings cli-floor` shows the fleet forced-update floor,
|
|
270
|
+
* `settings cli-floor <version>` raises it. There is no `set` verb because
|
|
271
|
+
* there is nothing else to do to a floor, and no flag for the version because
|
|
272
|
+
* a bare positional is how a release says it: `cockpit settings cli-floor
|
|
273
|
+
* 0.2.48`. The server refuses a LOWER version; the terminal does not
|
|
274
|
+
* second-guess that decision locally.
|
|
275
|
+
*/
|
|
276
|
+
if (section === "cli-floor") {
|
|
277
|
+
if (rest.length === 0) {
|
|
278
|
+
return { kind: "settings", section, action: "show", ...common };
|
|
279
|
+
}
|
|
280
|
+
const [version, ...extra] = rest;
|
|
281
|
+
if (!version || extra.length > 0) {
|
|
282
|
+
throw new Error("settings cli-floor takes no argument, or one version: `cli-floor 0.2.48`.");
|
|
283
|
+
}
|
|
284
|
+
return { kind: "settings", section, action: "set", floorVersion: version, ...common };
|
|
285
|
+
}
|
|
286
|
+
if (section === "models") {
|
|
287
|
+
if (rest.length === 0) {
|
|
288
|
+
return { kind: "settings", section, action: "show", ...common };
|
|
289
|
+
}
|
|
290
|
+
if (rest[0] !== "set" || rest.length > 1) {
|
|
291
|
+
throw new Error("settings models takes no verb, or `set --chat <key>` / `set --memory <id>`.");
|
|
292
|
+
}
|
|
293
|
+
const orgChatModel = optionalNonEmpty(values.flags.get("--chat"));
|
|
294
|
+
const orgMemoryModel = optionalNonEmpty(values.flags.get("--memory"));
|
|
295
|
+
if (!orgChatModel && !orgMemoryModel) {
|
|
296
|
+
throw new Error("settings models set needs --chat <key>, --memory <id>, or both.");
|
|
297
|
+
}
|
|
298
|
+
return { kind: "settings", section, action: "set", orgChatModel, orgMemoryModel, ...common };
|
|
299
|
+
}
|
|
300
|
+
// env
|
|
301
|
+
const verb = rest[0] ?? "list";
|
|
302
|
+
if (rest.length > 1)
|
|
303
|
+
throw new Error("settings env takes one verb: list, set, or delete.");
|
|
304
|
+
if (verb === "list") {
|
|
305
|
+
return { kind: "settings", section, action: "list", ...common };
|
|
306
|
+
}
|
|
307
|
+
if (verb === "set") {
|
|
308
|
+
const envProject = optionalNonEmpty(values.flags.get("--project"));
|
|
309
|
+
const envFile = optionalNonEmpty(values.flags.get("--file"));
|
|
310
|
+
if (!envProject || !envFile) {
|
|
311
|
+
throw new Error("settings env set needs --project <project> and --file <file name>.");
|
|
312
|
+
}
|
|
313
|
+
if (!values.booleans.has("--content-stdin")) {
|
|
314
|
+
// Deliberate: there is no `--content <value>` flag and never will be. A
|
|
315
|
+
// secret on a command line lands in shell history and in every process
|
|
316
|
+
// listing on the machine.
|
|
317
|
+
throw new Error("settings env set reads the file contents from stdin: add --content-stdin and pipe the file in.");
|
|
318
|
+
}
|
|
319
|
+
return {
|
|
320
|
+
kind: "settings",
|
|
321
|
+
section,
|
|
322
|
+
action: "set",
|
|
323
|
+
envProject,
|
|
324
|
+
envFile,
|
|
325
|
+
contentStdin: true,
|
|
326
|
+
...common,
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
if (verb === "delete") {
|
|
330
|
+
const envId = optionalNonEmpty(values.flags.get("--id"));
|
|
331
|
+
if (!envId)
|
|
332
|
+
throw new Error("settings env delete needs --id <uuid>.");
|
|
333
|
+
return { kind: "settings", section, action: "delete", envId, ...common };
|
|
334
|
+
}
|
|
335
|
+
throw new Error("settings env takes one verb: list, set, or delete.");
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* `cockpit team [members|invite <email>|role <userId>|device list|device
|
|
339
|
+
* revoke <id|name>]` (BLI-3461; `device` added BLI-3559).
|
|
340
|
+
*/
|
|
341
|
+
export function parseTeamArgs(args) {
|
|
342
|
+
const values = parseNamedArgs(args, {
|
|
343
|
+
allowedFlags: [
|
|
344
|
+
"--home",
|
|
345
|
+
"--dashboard-url",
|
|
346
|
+
"--json",
|
|
347
|
+
"--role",
|
|
348
|
+
"--team-id",
|
|
349
|
+
"--yes",
|
|
350
|
+
"--reason",
|
|
351
|
+
"--note",
|
|
352
|
+
],
|
|
353
|
+
valueFlags: ["--home", "--dashboard-url", "--role", "--team-id", "--reason", "--note"],
|
|
354
|
+
});
|
|
355
|
+
const common = {
|
|
356
|
+
homeDir: optionalNonEmpty(values.flags.get("--home")),
|
|
357
|
+
dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")),
|
|
358
|
+
teamId: optionalNonEmpty(values.flags.get("--team-id")),
|
|
359
|
+
yes: values.booleans.has("--yes"),
|
|
360
|
+
json: values.booleans.has("--json"),
|
|
361
|
+
};
|
|
362
|
+
const [verb, ...rest] = values.positionals;
|
|
363
|
+
if (verb === "device") {
|
|
364
|
+
return parseTeamDeviceArgs(rest, values.flags, common);
|
|
365
|
+
}
|
|
366
|
+
const [subject, ...extra] = rest;
|
|
367
|
+
if (extra.length > 0)
|
|
368
|
+
throw new Error("team does not accept that many arguments.");
|
|
369
|
+
if (!verb || verb === "members") {
|
|
370
|
+
if (subject)
|
|
371
|
+
throw new Error("team members does not accept positional arguments.");
|
|
372
|
+
return { kind: "team", action: "members", ...common };
|
|
373
|
+
}
|
|
374
|
+
if (verb === "invite") {
|
|
375
|
+
if (!subject)
|
|
376
|
+
throw new Error("team invite needs an email address.");
|
|
377
|
+
const email = optionalEmail(subject);
|
|
378
|
+
const role = optionalNonEmpty(values.flags.get("--role"));
|
|
379
|
+
if (!role)
|
|
380
|
+
throw new Error("team invite needs --role <role>.");
|
|
381
|
+
return { kind: "team", action: "invite", email, role, ...common };
|
|
382
|
+
}
|
|
383
|
+
if (verb === "role") {
|
|
384
|
+
if (!subject)
|
|
385
|
+
throw new Error("team role needs the user id whose role is changing.");
|
|
386
|
+
const role = optionalNonEmpty(values.flags.get("--role"));
|
|
387
|
+
if (!role)
|
|
388
|
+
throw new Error("team role needs --role <role>.");
|
|
389
|
+
return { kind: "team", action: "role", targetUserId: subject, role, ...common };
|
|
390
|
+
}
|
|
391
|
+
throw new Error("team takes one verb: members, invite, role, or device.");
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* `cockpit team device [list] [--json]` or
|
|
395
|
+
* `cockpit team device revoke <id|name> --reason <label> [--note "<text>"]`
|
|
396
|
+
* (BLI-3559). The reason label is validated here (a closed vocabulary is
|
|
397
|
+
* worth refusing a typo locally) but the note's 120-char cap is left to the
|
|
398
|
+
* server — this file has no business re-deciding a limit that lives in the
|
|
399
|
+
* route's schema.
|
|
400
|
+
*/
|
|
401
|
+
function parseTeamDeviceArgs(rest, flags, common) {
|
|
402
|
+
const [deviceVerb, deviceRef, ...extra] = rest;
|
|
403
|
+
if (extra.length > 0)
|
|
404
|
+
throw new Error("team device does not accept that many arguments.");
|
|
405
|
+
if (!deviceVerb || deviceVerb === "list") {
|
|
406
|
+
if (deviceRef)
|
|
407
|
+
throw new Error("team device list does not accept positional arguments.");
|
|
408
|
+
return { kind: "team", action: "device-list", ...common };
|
|
409
|
+
}
|
|
410
|
+
if (deviceVerb === "revoke") {
|
|
411
|
+
if (!deviceRef)
|
|
412
|
+
throw new Error("team device revoke needs a device id or name.");
|
|
413
|
+
const reasonLabel = optionalNonEmpty(flags.get("--reason"));
|
|
414
|
+
if (!reasonLabel) {
|
|
415
|
+
throw new Error(`team device revoke needs --reason <label> (${TEAM_DEVICE_REVOKE_REASON_LABELS.join(", ")}).`);
|
|
416
|
+
}
|
|
417
|
+
if (!isTeamDeviceRevokeReasonLabel(reasonLabel)) {
|
|
418
|
+
throw new Error(`team device revoke --reason must be one of ${TEAM_DEVICE_REVOKE_REASON_LABELS.join(", ")}; got ${reasonLabel}.`);
|
|
419
|
+
}
|
|
420
|
+
return {
|
|
421
|
+
kind: "team",
|
|
422
|
+
action: "device-revoke",
|
|
423
|
+
deviceRef,
|
|
424
|
+
reasonLabel,
|
|
425
|
+
reasonNote: optionalNonEmpty(flags.get("--note")),
|
|
426
|
+
...common,
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
throw new Error("team device takes one verb: list or revoke <id|name>.");
|
|
430
|
+
}
|
|
431
|
+
/** `cockpit model [show|set <key>]` — the alias surface for the personal chat model. */
|
|
432
|
+
export function parseModelArgs(args) {
|
|
433
|
+
const values = parseNamedArgs(args, {
|
|
434
|
+
allowedFlags: ["--home", "--dashboard-url", "--json"],
|
|
435
|
+
valueFlags: ["--home", "--dashboard-url"],
|
|
436
|
+
});
|
|
437
|
+
const [verb, key, ...extra] = values.positionals;
|
|
438
|
+
if (extra.length > 0)
|
|
439
|
+
throw new Error("model does not accept that many arguments.");
|
|
440
|
+
const common = {
|
|
441
|
+
homeDir: optionalNonEmpty(values.flags.get("--home")),
|
|
442
|
+
dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")),
|
|
443
|
+
json: values.booleans.has("--json"),
|
|
444
|
+
};
|
|
445
|
+
if (!verb || verb === "show") {
|
|
446
|
+
if (key)
|
|
447
|
+
throw new Error("model show does not accept positional arguments.");
|
|
448
|
+
return { kind: "model", action: "show", ...common };
|
|
449
|
+
}
|
|
450
|
+
if (verb === "set") {
|
|
451
|
+
if (!key)
|
|
452
|
+
throw new Error("model set needs a model key, e.g. openai:gpt-5.6-terra.");
|
|
453
|
+
return { kind: "model", action: "set", modelKey: key, ...common };
|
|
454
|
+
}
|
|
455
|
+
throw new Error("model takes one verb: show or set <key>.");
|
|
456
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The two commands that carry a conversation forward with JARVIS: asking it
|
|
3
|
+
* something (`cockpit jarvis`) and telling it a line on the page was wrong
|
|
4
|
+
* (`cockpit correct`). Split out of local-args-tower.ts (BLI-3636) — `correct`
|
|
5
|
+
* moved here rather than beside `brief` because it is the same act as
|
|
6
|
+
* `jarvis`: a person addressing JARVIS directly, not reading a page.
|
|
7
|
+
*
|
|
8
|
+
* Every parser moved verbatim: same flags, same defaults, same refusal
|
|
9
|
+
* sentences.
|
|
10
|
+
*/
|
|
11
|
+
import { optionalNonEmpty, optionalPositiveInteger, optionalUrl, parseNamedArgs, } from "./local-arg-values.js";
|
|
12
|
+
export function parseJarvisArgs(args) {
|
|
13
|
+
const values = parseNamedArgs(args, {
|
|
14
|
+
allowedFlags: [
|
|
15
|
+
"--home",
|
|
16
|
+
"--dashboard-url",
|
|
17
|
+
"--prompt",
|
|
18
|
+
"--as",
|
|
19
|
+
"--thread",
|
|
20
|
+
"--model",
|
|
21
|
+
"--image",
|
|
22
|
+
"--file",
|
|
23
|
+
"--no-stream",
|
|
24
|
+
// BLI-3484: which day's page this turn is about.
|
|
25
|
+
"--date",
|
|
26
|
+
// BLI-3458: reading back what was already said, rather than saying
|
|
27
|
+
// something new. Neither takes a turn or reaches the model.
|
|
28
|
+
"--threads",
|
|
29
|
+
"--history",
|
|
30
|
+
// BLI-3560: reading back what a turn DID — the step tree of one turn.
|
|
31
|
+
"--trace",
|
|
32
|
+
"--limit",
|
|
33
|
+
"--json",
|
|
34
|
+
],
|
|
35
|
+
valueFlags: [
|
|
36
|
+
"--home",
|
|
37
|
+
"--dashboard-url",
|
|
38
|
+
"--prompt",
|
|
39
|
+
"--as",
|
|
40
|
+
"--thread",
|
|
41
|
+
"--model",
|
|
42
|
+
"--trace",
|
|
43
|
+
"--image",
|
|
44
|
+
"--file",
|
|
45
|
+
"--date",
|
|
46
|
+
"--limit",
|
|
47
|
+
],
|
|
48
|
+
});
|
|
49
|
+
const flaggedPrompt = optionalNonEmpty(values.flags.get("--prompt"));
|
|
50
|
+
const positionalPrompt = optionalNonEmpty(values.positionals.join(" "));
|
|
51
|
+
if (flaggedPrompt && positionalPrompt) {
|
|
52
|
+
throw new Error("jarvis accepts either --prompt or positional text, not both.");
|
|
53
|
+
}
|
|
54
|
+
const thread = optionalNonEmpty(values.flags.get("--thread")) ?? "main";
|
|
55
|
+
if (!/^[A-Za-z0-9_-]{1,40}$/.test(thread)) {
|
|
56
|
+
throw new Error("jarvis --thread must use 1 to 40 letters, numbers, underscores, or hyphens.");
|
|
57
|
+
}
|
|
58
|
+
// BLI-3414: `--file` is a plain alias for `--image` — same flag, whichever
|
|
59
|
+
// word a person reaches for first.
|
|
60
|
+
const image = optionalNonEmpty(values.flags.get("--image"));
|
|
61
|
+
const file = optionalNonEmpty(values.flags.get("--file"));
|
|
62
|
+
if (image && file) {
|
|
63
|
+
throw new Error("jarvis accepts either --image or --file, not both — they are the same flag.");
|
|
64
|
+
}
|
|
65
|
+
// BLI-3458. Reading history and asking a question are different acts, and a
|
|
66
|
+
// command that quietly did one while you asked for the other would be worse
|
|
67
|
+
// than a refusal — `--threads` with a question would silently drop the
|
|
68
|
+
// question.
|
|
69
|
+
const threads = values.booleans.has("--threads");
|
|
70
|
+
const history = values.booleans.has("--history");
|
|
71
|
+
if (threads && history) {
|
|
72
|
+
throw new Error("jarvis --threads lists every thread; --history replays one. Pass one, not both.");
|
|
73
|
+
}
|
|
74
|
+
if ((threads || history) && (flaggedPrompt || positionalPrompt)) {
|
|
75
|
+
throw new Error("jarvis --threads and --history read back what was already said; they do not take a question.");
|
|
76
|
+
}
|
|
77
|
+
if ((threads || history) && image) {
|
|
78
|
+
throw new Error("jarvis --threads and --history do not take an attachment.");
|
|
79
|
+
}
|
|
80
|
+
// BLI-3484. `--date` binds the page that was live on one of the subject's
|
|
81
|
+
// days, so a turn can be about Sunday's page. Refused on the two reading
|
|
82
|
+
// commands for the same reason an attachment is: they replay what was said
|
|
83
|
+
// and bind no page at all.
|
|
84
|
+
const date = optionalNonEmpty(values.flags.get("--date"));
|
|
85
|
+
if ((threads || history) && date) {
|
|
86
|
+
throw new Error("jarvis --threads and --history replay what was said; they bind no page.");
|
|
87
|
+
}
|
|
88
|
+
// BLI-3560. `--trace` reads back what a turn DID, the same class of act as
|
|
89
|
+
// `--threads` and `--history`, and refused alongside them for the same
|
|
90
|
+
// reason: a command that quietly asked a question while you asked to see one
|
|
91
|
+
// is worse than a refusal.
|
|
92
|
+
const trace = optionalNonEmpty(values.flags.get("--trace"));
|
|
93
|
+
if (trace && (threads || history)) {
|
|
94
|
+
throw new Error("jarvis --trace shows one turn's steps; --threads and --history replay what was said. Pass one.");
|
|
95
|
+
}
|
|
96
|
+
if (trace && (flaggedPrompt || positionalPrompt)) {
|
|
97
|
+
throw new Error("jarvis --trace shows a turn that already ran; it does not take a question.");
|
|
98
|
+
}
|
|
99
|
+
if (trace && image) {
|
|
100
|
+
throw new Error("jarvis --trace does not take an attachment.");
|
|
101
|
+
}
|
|
102
|
+
if (trace !== undefined && !/^(last|[A-Za-z0-9_-]{1,64})$/.test(trace)) {
|
|
103
|
+
throw new Error("jarvis --trace takes `last` or a trace id (up to 64 letters, numbers, underscores or hyphens).");
|
|
104
|
+
}
|
|
105
|
+
return {
|
|
106
|
+
kind: "jarvis",
|
|
107
|
+
homeDir: optionalNonEmpty(values.flags.get("--home")),
|
|
108
|
+
dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")),
|
|
109
|
+
prompt: flaggedPrompt ?? positionalPrompt,
|
|
110
|
+
subject: optionalNonEmpty(values.flags.get("--as")),
|
|
111
|
+
thread,
|
|
112
|
+
threads,
|
|
113
|
+
history,
|
|
114
|
+
...(trace ? { trace } : {}),
|
|
115
|
+
limit: optionalPositiveInteger(values.flags.get("--limit"), "--limit"),
|
|
116
|
+
// BLI-3381: no client-side allowlist — the dashboard forwards this key
|
|
117
|
+
// to the inference server's own allowlist and relays its refusal.
|
|
118
|
+
model: optionalNonEmpty(values.flags.get("--model")),
|
|
119
|
+
...(date ? { date } : {}),
|
|
120
|
+
imagePath: image ?? file,
|
|
121
|
+
// BLI-3457: streaming is on unless a caller opts out. A dashboard that
|
|
122
|
+
// does not stream yet still answers plain JSON, so this flag is for
|
|
123
|
+
// callers that want the single-body shape on purpose, not a compat knob.
|
|
124
|
+
stream: !values.booleans.has("--no-stream"),
|
|
125
|
+
json: values.booleans.has("--json"),
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* `cockpit correct` (BLI-3458) — say that one line on the page is wrong.
|
|
130
|
+
*
|
|
131
|
+
* `--claim` is required and comes from `cockpit brief --claims`, which prints
|
|
132
|
+
* the id beside every line it names. A correction with no claim would be a note
|
|
133
|
+
* about the page in general, and the panel has a sentinel for that; the
|
|
134
|
+
* terminal does not offer it, because a person who has just read
|
|
135
|
+
* `[claimId] the sentence` has the id in front of them.
|
|
136
|
+
*
|
|
137
|
+
* `--text` may be omitted when something is piped in — `git log | cockpit
|
|
138
|
+
* correct --claim x` — and the command reads stdin instead. Absent both, the
|
|
139
|
+
* command refuses rather than filing an empty correction.
|
|
140
|
+
*/
|
|
141
|
+
export function parseCorrectArgs(args) {
|
|
142
|
+
const values = parseNamedArgs(args, {
|
|
143
|
+
allowedFlags: [
|
|
144
|
+
"--home",
|
|
145
|
+
"--dashboard-url",
|
|
146
|
+
"--claim",
|
|
147
|
+
"--text",
|
|
148
|
+
"--for",
|
|
149
|
+
"--as",
|
|
150
|
+
"--version",
|
|
151
|
+
"--supersedes",
|
|
152
|
+
"--json",
|
|
153
|
+
],
|
|
154
|
+
valueFlags: [
|
|
155
|
+
"--home",
|
|
156
|
+
"--dashboard-url",
|
|
157
|
+
"--claim",
|
|
158
|
+
"--text",
|
|
159
|
+
"--for",
|
|
160
|
+
"--as",
|
|
161
|
+
"--version",
|
|
162
|
+
"--supersedes",
|
|
163
|
+
],
|
|
164
|
+
});
|
|
165
|
+
const claimId = optionalNonEmpty(values.flags.get("--claim"));
|
|
166
|
+
if (!claimId) {
|
|
167
|
+
throw new Error("correct needs --claim <claimId>. Run `cockpit brief --claims` to see the id beside every line.");
|
|
168
|
+
}
|
|
169
|
+
const flaggedText = optionalNonEmpty(values.flags.get("--text"));
|
|
170
|
+
const positionalText = optionalNonEmpty(values.positionals.join(" "));
|
|
171
|
+
if (flaggedText && positionalText) {
|
|
172
|
+
throw new Error("correct accepts either --text or positional text, not both.");
|
|
173
|
+
}
|
|
174
|
+
const text = flaggedText ?? positionalText;
|
|
175
|
+
if (text && text.length > 4000) {
|
|
176
|
+
throw new Error("A correction is limited to 4000 characters.");
|
|
177
|
+
}
|
|
178
|
+
const forPerson = optionalNonEmpty(values.flags.get("--for"));
|
|
179
|
+
const asPerson = optionalNonEmpty(values.flags.get("--as"));
|
|
180
|
+
if (forPerson && asPerson && forPerson !== asPerson) {
|
|
181
|
+
throw new Error("correct --for and --as must name the same person.");
|
|
182
|
+
}
|
|
183
|
+
return {
|
|
184
|
+
kind: "correct",
|
|
185
|
+
homeDir: optionalNonEmpty(values.flags.get("--home")),
|
|
186
|
+
dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")),
|
|
187
|
+
claimId,
|
|
188
|
+
text,
|
|
189
|
+
subject: forPerson ?? asPerson,
|
|
190
|
+
version: optionalNonEmpty(values.flags.get("--version")),
|
|
191
|
+
supersedes: optionalNonEmpty(values.flags.get("--supersedes")),
|
|
192
|
+
json: values.booleans.has("--json"),
|
|
193
|
+
};
|
|
194
|
+
}
|