@bli-cockpit/cli 0.2.38 → 0.2.40
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/commands/brief.js +133 -0
- package/dist/commands/cli-io.js +33 -1
- package/dist/commands/correct.js +149 -0
- package/dist/commands/jarvis.js +98 -15
- package/dist/commands/local-args.js +565 -3
- package/dist/commands/local-help.js +169 -4
- package/dist/commands/local.js +91 -3
- package/dist/commands/notes-file.js +129 -0
- package/dist/commands/notes.js +488 -0
- package/dist/commands/public-root.js +1 -1
- package/dist/commands/scout-render.js +172 -0
- package/dist/commands/scout.js +158 -0
- package/dist/commands/settings-render.js +137 -0
- package/dist/commands/settings.js +378 -0
- package/dist/commands/team.js +111 -0
- package/dist/commands/tower-command.js +111 -0
- package/dist/commands/workbook-render.js +196 -0
- package/dist/commands/workbook.js +180 -0
- package/package.json +1 -1
|
@@ -49,6 +49,22 @@ export function parseLocalArgs(argv) {
|
|
|
49
49
|
return parseAnalyzeArgs(argv.slice(1));
|
|
50
50
|
case "jarvis":
|
|
51
51
|
return parseJarvisArgs(argv.slice(1));
|
|
52
|
+
case "model":
|
|
53
|
+
return parseModelArgs(argv.slice(1));
|
|
54
|
+
case "scout":
|
|
55
|
+
return parseScoutArgs(argv.slice(1));
|
|
56
|
+
case "settings":
|
|
57
|
+
return parseSettingsArgs(argv.slice(1));
|
|
58
|
+
case "team":
|
|
59
|
+
return parseTeamArgs(argv.slice(1));
|
|
60
|
+
case "workbook":
|
|
61
|
+
return parseWorkbookArgs(argv.slice(1));
|
|
62
|
+
case "brief":
|
|
63
|
+
return parseBriefArgs(argv.slice(1));
|
|
64
|
+
case "correct":
|
|
65
|
+
return parseCorrectArgs(argv.slice(1));
|
|
66
|
+
case "notes":
|
|
67
|
+
return parseNotesArgs(argv.slice(1));
|
|
52
68
|
case "backfill":
|
|
53
69
|
return parseBackfillArgs(argv.slice(1));
|
|
54
70
|
case "status":
|
|
@@ -283,15 +299,216 @@ function parseLoginArgs(args) {
|
|
|
283
299
|
}
|
|
284
300
|
function parseLogoutArgs(args) {
|
|
285
301
|
const values = parseNamedArgs(args, {
|
|
286
|
-
allowedFlags: ["--home", "--json"],
|
|
287
|
-
valueFlags: ["--home"],
|
|
302
|
+
allowedFlags: ["--home", "--json", "--revoke", "--dashboard-url", "--yes"],
|
|
303
|
+
valueFlags: ["--home", "--dashboard-url"],
|
|
288
304
|
});
|
|
289
305
|
assertNoPositionals(values.positionals, "logout");
|
|
290
306
|
return {
|
|
291
307
|
kind: "logout",
|
|
292
308
|
homeDir: optionalNonEmpty(values.flags.get("--home")),
|
|
293
309
|
json: values.booleans.has("--json"),
|
|
310
|
+
revoke: values.booleans.has("--revoke"),
|
|
311
|
+
dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")),
|
|
312
|
+
yes: values.booleans.has("--yes"),
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
const SETTINGS_SECTIONS = ["personal", "switches", "models", "env"];
|
|
316
|
+
/**
|
|
317
|
+
* `cockpit settings [section] [verb] [args]` (BLI-3461).
|
|
318
|
+
*
|
|
319
|
+
* The flag set is the UNION across sections and the combinations are checked
|
|
320
|
+
* afterwards, per section — the same shape `parseDoctorArgs` uses. A flag that
|
|
321
|
+
* belongs to another section is refused by name rather than silently ignored,
|
|
322
|
+
* because a dropped `--project` on an env write is a change a person believes
|
|
323
|
+
* they made.
|
|
324
|
+
*/
|
|
325
|
+
function parseSettingsArgs(args) {
|
|
326
|
+
const values = parseNamedArgs(args, {
|
|
327
|
+
allowedFlags: [
|
|
328
|
+
"--home",
|
|
329
|
+
"--dashboard-url",
|
|
330
|
+
"--json",
|
|
331
|
+
"--chat-model",
|
|
332
|
+
"--brief-model",
|
|
333
|
+
"--chat",
|
|
334
|
+
"--memory",
|
|
335
|
+
"--project",
|
|
336
|
+
"--file",
|
|
337
|
+
"--id",
|
|
338
|
+
"--content-stdin",
|
|
339
|
+
"--yes",
|
|
340
|
+
],
|
|
341
|
+
valueFlags: [
|
|
342
|
+
"--home",
|
|
343
|
+
"--dashboard-url",
|
|
344
|
+
"--chat-model",
|
|
345
|
+
"--brief-model",
|
|
346
|
+
"--chat",
|
|
347
|
+
"--memory",
|
|
348
|
+
"--project",
|
|
349
|
+
"--file",
|
|
350
|
+
"--id",
|
|
351
|
+
],
|
|
352
|
+
});
|
|
353
|
+
const [rawSection, ...rest] = values.positionals;
|
|
354
|
+
const common = {
|
|
355
|
+
homeDir: optionalNonEmpty(values.flags.get("--home")),
|
|
356
|
+
dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")),
|
|
357
|
+
yes: values.booleans.has("--yes"),
|
|
358
|
+
json: values.booleans.has("--json"),
|
|
294
359
|
};
|
|
360
|
+
if (!rawSection) {
|
|
361
|
+
if (rest.length > 0)
|
|
362
|
+
throw new Error("settings does not accept that argument.");
|
|
363
|
+
return { kind: "settings", section: "overview", action: "show", ...common };
|
|
364
|
+
}
|
|
365
|
+
if (!SETTINGS_SECTIONS.includes(rawSection)) {
|
|
366
|
+
throw new Error(`settings section must be one of ${SETTINGS_SECTIONS.join(", ")} — or nothing, for all of them.`);
|
|
367
|
+
}
|
|
368
|
+
const section = rawSection;
|
|
369
|
+
if (section === "personal") {
|
|
370
|
+
if (rest.length > 0)
|
|
371
|
+
throw new Error("settings personal does not accept positional arguments.");
|
|
372
|
+
const chatModel = optionalNonEmpty(values.flags.get("--chat-model"));
|
|
373
|
+
const briefModel = optionalNonEmpty(values.flags.get("--brief-model"));
|
|
374
|
+
return {
|
|
375
|
+
kind: "settings",
|
|
376
|
+
section,
|
|
377
|
+
action: chatModel || briefModel ? "set" : "show",
|
|
378
|
+
chatModel,
|
|
379
|
+
briefModel,
|
|
380
|
+
...common,
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
if (section === "switches") {
|
|
384
|
+
if (rest.length === 0) {
|
|
385
|
+
return { kind: "settings", section, action: "show", ...common };
|
|
386
|
+
}
|
|
387
|
+
if (rest[0] !== "set") {
|
|
388
|
+
throw new Error("settings switches takes no verb, or `set <key> <value>`.");
|
|
389
|
+
}
|
|
390
|
+
const [, key, value, ...extra] = rest;
|
|
391
|
+
if (!key || !value || extra.length > 0) {
|
|
392
|
+
throw new Error("settings switches set needs exactly a key and a value.");
|
|
393
|
+
}
|
|
394
|
+
return { kind: "settings", section, action: "set", switchKey: key, switchValue: value, ...common };
|
|
395
|
+
}
|
|
396
|
+
if (section === "models") {
|
|
397
|
+
if (rest.length === 0) {
|
|
398
|
+
return { kind: "settings", section, action: "show", ...common };
|
|
399
|
+
}
|
|
400
|
+
if (rest[0] !== "set" || rest.length > 1) {
|
|
401
|
+
throw new Error("settings models takes no verb, or `set --chat <key>` / `set --memory <id>`.");
|
|
402
|
+
}
|
|
403
|
+
const orgChatModel = optionalNonEmpty(values.flags.get("--chat"));
|
|
404
|
+
const orgMemoryModel = optionalNonEmpty(values.flags.get("--memory"));
|
|
405
|
+
if (!orgChatModel && !orgMemoryModel) {
|
|
406
|
+
throw new Error("settings models set needs --chat <key>, --memory <id>, or both.");
|
|
407
|
+
}
|
|
408
|
+
return { kind: "settings", section, action: "set", orgChatModel, orgMemoryModel, ...common };
|
|
409
|
+
}
|
|
410
|
+
// env
|
|
411
|
+
const verb = rest[0] ?? "list";
|
|
412
|
+
if (rest.length > 1)
|
|
413
|
+
throw new Error("settings env takes one verb: list, set, or delete.");
|
|
414
|
+
if (verb === "list") {
|
|
415
|
+
return { kind: "settings", section, action: "list", ...common };
|
|
416
|
+
}
|
|
417
|
+
if (verb === "set") {
|
|
418
|
+
const envProject = optionalNonEmpty(values.flags.get("--project"));
|
|
419
|
+
const envFile = optionalNonEmpty(values.flags.get("--file"));
|
|
420
|
+
if (!envProject || !envFile) {
|
|
421
|
+
throw new Error("settings env set needs --project <project> and --file <file name>.");
|
|
422
|
+
}
|
|
423
|
+
if (!values.booleans.has("--content-stdin")) {
|
|
424
|
+
// Deliberate: there is no `--content <value>` flag and never will be. A
|
|
425
|
+
// secret on a command line lands in shell history and in every process
|
|
426
|
+
// listing on the machine.
|
|
427
|
+
throw new Error("settings env set reads the file contents from stdin: add --content-stdin and pipe the file in.");
|
|
428
|
+
}
|
|
429
|
+
return {
|
|
430
|
+
kind: "settings",
|
|
431
|
+
section,
|
|
432
|
+
action: "set",
|
|
433
|
+
envProject,
|
|
434
|
+
envFile,
|
|
435
|
+
contentStdin: true,
|
|
436
|
+
...common,
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
if (verb === "delete") {
|
|
440
|
+
const envId = optionalNonEmpty(values.flags.get("--id"));
|
|
441
|
+
if (!envId)
|
|
442
|
+
throw new Error("settings env delete needs --id <uuid>.");
|
|
443
|
+
return { kind: "settings", section, action: "delete", envId, ...common };
|
|
444
|
+
}
|
|
445
|
+
throw new Error("settings env takes one verb: list, set, or delete.");
|
|
446
|
+
}
|
|
447
|
+
/** `cockpit team [members|invite <email>|role <userId>]` (BLI-3461). */
|
|
448
|
+
function parseTeamArgs(args) {
|
|
449
|
+
const values = parseNamedArgs(args, {
|
|
450
|
+
allowedFlags: ["--home", "--dashboard-url", "--json", "--role", "--team-id", "--yes"],
|
|
451
|
+
valueFlags: ["--home", "--dashboard-url", "--role", "--team-id"],
|
|
452
|
+
});
|
|
453
|
+
const common = {
|
|
454
|
+
homeDir: optionalNonEmpty(values.flags.get("--home")),
|
|
455
|
+
dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")),
|
|
456
|
+
teamId: optionalNonEmpty(values.flags.get("--team-id")),
|
|
457
|
+
yes: values.booleans.has("--yes"),
|
|
458
|
+
json: values.booleans.has("--json"),
|
|
459
|
+
};
|
|
460
|
+
const [verb, subject, ...extra] = values.positionals;
|
|
461
|
+
if (extra.length > 0)
|
|
462
|
+
throw new Error("team does not accept that many arguments.");
|
|
463
|
+
if (!verb || verb === "members") {
|
|
464
|
+
if (subject)
|
|
465
|
+
throw new Error("team members does not accept positional arguments.");
|
|
466
|
+
return { kind: "team", action: "members", ...common };
|
|
467
|
+
}
|
|
468
|
+
if (verb === "invite") {
|
|
469
|
+
if (!subject)
|
|
470
|
+
throw new Error("team invite needs an email address.");
|
|
471
|
+
const email = optionalEmail(subject);
|
|
472
|
+
const role = optionalNonEmpty(values.flags.get("--role"));
|
|
473
|
+
if (!role)
|
|
474
|
+
throw new Error("team invite needs --role <role>.");
|
|
475
|
+
return { kind: "team", action: "invite", email, role, ...common };
|
|
476
|
+
}
|
|
477
|
+
if (verb === "role") {
|
|
478
|
+
if (!subject)
|
|
479
|
+
throw new Error("team role needs the user id whose role is changing.");
|
|
480
|
+
const role = optionalNonEmpty(values.flags.get("--role"));
|
|
481
|
+
if (!role)
|
|
482
|
+
throw new Error("team role needs --role <role>.");
|
|
483
|
+
return { kind: "team", action: "role", targetUserId: subject, role, ...common };
|
|
484
|
+
}
|
|
485
|
+
throw new Error("team takes one verb: members, invite, or role.");
|
|
486
|
+
}
|
|
487
|
+
/** `cockpit model [show|set <key>]` — the alias surface for the personal chat model. */
|
|
488
|
+
function parseModelArgs(args) {
|
|
489
|
+
const values = parseNamedArgs(args, {
|
|
490
|
+
allowedFlags: ["--home", "--dashboard-url", "--json"],
|
|
491
|
+
valueFlags: ["--home", "--dashboard-url"],
|
|
492
|
+
});
|
|
493
|
+
const [verb, key, ...extra] = values.positionals;
|
|
494
|
+
if (extra.length > 0)
|
|
495
|
+
throw new Error("model does not accept that many arguments.");
|
|
496
|
+
const common = {
|
|
497
|
+
homeDir: optionalNonEmpty(values.flags.get("--home")),
|
|
498
|
+
dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")),
|
|
499
|
+
json: values.booleans.has("--json"),
|
|
500
|
+
};
|
|
501
|
+
if (!verb || verb === "show") {
|
|
502
|
+
if (key)
|
|
503
|
+
throw new Error("model show does not accept positional arguments.");
|
|
504
|
+
return { kind: "model", action: "show", ...common };
|
|
505
|
+
}
|
|
506
|
+
if (verb === "set") {
|
|
507
|
+
if (!key)
|
|
508
|
+
throw new Error("model set needs a model key, e.g. openai:gpt-5.6-terra.");
|
|
509
|
+
return { kind: "model", action: "set", modelKey: key, ...common };
|
|
510
|
+
}
|
|
511
|
+
throw new Error("model takes one verb: show or set <key>.");
|
|
295
512
|
}
|
|
296
513
|
function parseStartArgs(args) {
|
|
297
514
|
const values = parseNamedArgs(args, {
|
|
@@ -621,9 +838,24 @@ function parseJarvisArgs(args) {
|
|
|
621
838
|
"--image",
|
|
622
839
|
"--file",
|
|
623
840
|
"--no-stream",
|
|
841
|
+
// BLI-3458: reading back what was already said, rather than saying
|
|
842
|
+
// something new. Neither takes a turn or reaches the model.
|
|
843
|
+
"--threads",
|
|
844
|
+
"--history",
|
|
845
|
+
"--limit",
|
|
624
846
|
"--json",
|
|
625
847
|
],
|
|
626
|
-
valueFlags: [
|
|
848
|
+
valueFlags: [
|
|
849
|
+
"--home",
|
|
850
|
+
"--dashboard-url",
|
|
851
|
+
"--prompt",
|
|
852
|
+
"--as",
|
|
853
|
+
"--thread",
|
|
854
|
+
"--model",
|
|
855
|
+
"--image",
|
|
856
|
+
"--file",
|
|
857
|
+
"--limit",
|
|
858
|
+
],
|
|
627
859
|
});
|
|
628
860
|
const flaggedPrompt = optionalNonEmpty(values.flags.get("--prompt"));
|
|
629
861
|
const positionalPrompt = optionalNonEmpty(values.positionals.join(" "));
|
|
@@ -641,6 +873,21 @@ function parseJarvisArgs(args) {
|
|
|
641
873
|
if (image && file) {
|
|
642
874
|
throw new Error("jarvis accepts either --image or --file, not both — they are the same flag.");
|
|
643
875
|
}
|
|
876
|
+
// BLI-3458. Reading history and asking a question are different acts, and a
|
|
877
|
+
// command that quietly did one while you asked for the other would be worse
|
|
878
|
+
// than a refusal — `--threads` with a question would silently drop the
|
|
879
|
+
// question.
|
|
880
|
+
const threads = values.booleans.has("--threads");
|
|
881
|
+
const history = values.booleans.has("--history");
|
|
882
|
+
if (threads && history) {
|
|
883
|
+
throw new Error("jarvis --threads lists every thread; --history replays one. Pass one, not both.");
|
|
884
|
+
}
|
|
885
|
+
if ((threads || history) && (flaggedPrompt || positionalPrompt)) {
|
|
886
|
+
throw new Error("jarvis --threads and --history read back what was already said; they do not take a question.");
|
|
887
|
+
}
|
|
888
|
+
if ((threads || history) && image) {
|
|
889
|
+
throw new Error("jarvis --threads and --history do not take an attachment.");
|
|
890
|
+
}
|
|
644
891
|
return {
|
|
645
892
|
kind: "jarvis",
|
|
646
893
|
homeDir: optionalNonEmpty(values.flags.get("--home")),
|
|
@@ -648,6 +895,9 @@ function parseJarvisArgs(args) {
|
|
|
648
895
|
prompt: flaggedPrompt ?? positionalPrompt,
|
|
649
896
|
subject: optionalNonEmpty(values.flags.get("--as")),
|
|
650
897
|
thread,
|
|
898
|
+
threads,
|
|
899
|
+
history,
|
|
900
|
+
limit: optionalPositiveInteger(values.flags.get("--limit"), "--limit"),
|
|
651
901
|
// BLI-3381: no client-side allowlist — the dashboard forwards this key
|
|
652
902
|
// to the inference server's own allowlist and relays its refusal.
|
|
653
903
|
model: optionalNonEmpty(values.flags.get("--model")),
|
|
@@ -659,6 +909,318 @@ function parseJarvisArgs(args) {
|
|
|
659
909
|
json: values.booleans.has("--json"),
|
|
660
910
|
};
|
|
661
911
|
}
|
|
912
|
+
/** The shortest prefix `cockpit scout start` will resolve. Below this, ids collide. */
|
|
913
|
+
export const SCOUT_MIN_PREFIX_LENGTH = 6;
|
|
914
|
+
/**
|
|
915
|
+
* `cockpit scout [--days <n>]` reads the board; `cockpit scout start|dismiss|undo
|
|
916
|
+
* <id>` moves one card. Positional shape modelled on `autostart`: an optional
|
|
917
|
+
* action word first, then what it acts on.
|
|
918
|
+
*/
|
|
919
|
+
function parseScoutArgs(args) {
|
|
920
|
+
const values = parseNamedArgs(args, {
|
|
921
|
+
allowedFlags: ["--home", "--dashboard-url", "--days", "--json"],
|
|
922
|
+
valueFlags: ["--home", "--dashboard-url", "--days"],
|
|
923
|
+
});
|
|
924
|
+
if (values.positionals.length > 2) {
|
|
925
|
+
throw new Error("scout accepts at most an action (start|dismiss|undo) and one experiment id.");
|
|
926
|
+
}
|
|
927
|
+
const [rawAction, rawRef] = values.positionals;
|
|
928
|
+
const base = {
|
|
929
|
+
homeDir: optionalNonEmpty(values.flags.get("--home")),
|
|
930
|
+
dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")),
|
|
931
|
+
days: optionalPositiveInteger(values.flags.get("--days"), "--days"),
|
|
932
|
+
json: values.booleans.has("--json"),
|
|
933
|
+
};
|
|
934
|
+
if (!rawAction)
|
|
935
|
+
return { kind: "scout", action: "board", ...base };
|
|
936
|
+
if (rawAction !== "start" && rawAction !== "dismiss" && rawAction !== "undo") {
|
|
937
|
+
throw new Error("scout action must be start, dismiss, or undo.");
|
|
938
|
+
}
|
|
939
|
+
const experimentRef = optionalNonEmpty(rawRef);
|
|
940
|
+
if (!experimentRef) {
|
|
941
|
+
throw new Error(`scout ${rawAction} needs an experiment id from the board.`);
|
|
942
|
+
}
|
|
943
|
+
if (experimentRef.length < SCOUT_MIN_PREFIX_LENGTH) {
|
|
944
|
+
throw new Error(`scout ${rawAction} needs a full experiment id or a prefix of at least ${SCOUT_MIN_PREFIX_LENGTH} characters.`);
|
|
945
|
+
}
|
|
946
|
+
return { kind: "scout", action: rawAction, experimentRef, ...base };
|
|
947
|
+
}
|
|
948
|
+
/** The narrowest width worth wrapping to; below it every line is one word. */
|
|
949
|
+
export const WORKBOOK_MIN_WIDTH = 20;
|
|
950
|
+
/**
|
|
951
|
+
* `cockpit workbook` with nothing lists the library, one positional lists a
|
|
952
|
+
* project's shelf, two read a document.
|
|
953
|
+
*/
|
|
954
|
+
function parseWorkbookArgs(args) {
|
|
955
|
+
const values = parseNamedArgs(args, {
|
|
956
|
+
allowedFlags: [
|
|
957
|
+
"--home",
|
|
958
|
+
"--dashboard-url",
|
|
959
|
+
"--section",
|
|
960
|
+
"--width",
|
|
961
|
+
"--markdown",
|
|
962
|
+
"--json",
|
|
963
|
+
],
|
|
964
|
+
valueFlags: ["--home", "--dashboard-url", "--section", "--width"],
|
|
965
|
+
});
|
|
966
|
+
if (values.positionals.length > 2) {
|
|
967
|
+
throw new Error("workbook accepts at most a project and a document.");
|
|
968
|
+
}
|
|
969
|
+
const project = optionalNonEmpty(values.positionals[0]);
|
|
970
|
+
const doc = optionalNonEmpty(values.positionals[1]);
|
|
971
|
+
const section = optionalNonEmpty(values.flags.get("--section"));
|
|
972
|
+
const markdown = values.booleans.has("--markdown");
|
|
973
|
+
if (!doc && (section || markdown)) {
|
|
974
|
+
throw new Error("workbook --section and --markdown need a project and a document, e.g. `cockpit workbook tower workbook`.");
|
|
975
|
+
}
|
|
976
|
+
const width = optionalPositiveInteger(values.flags.get("--width"), "--width");
|
|
977
|
+
if (width !== undefined && width < WORKBOOK_MIN_WIDTH) {
|
|
978
|
+
throw new Error(`workbook --width must be at least ${WORKBOOK_MIN_WIDTH}.`);
|
|
979
|
+
}
|
|
980
|
+
return {
|
|
981
|
+
kind: "workbook",
|
|
982
|
+
project,
|
|
983
|
+
doc,
|
|
984
|
+
section,
|
|
985
|
+
markdown,
|
|
986
|
+
width,
|
|
987
|
+
homeDir: optionalNonEmpty(values.flags.get("--home")),
|
|
988
|
+
dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")),
|
|
989
|
+
json: values.booleans.has("--json"),
|
|
990
|
+
};
|
|
991
|
+
}
|
|
992
|
+
/**
|
|
993
|
+
* `cockpit brief` (BLI-3458) — the TODAY page, in the terminal.
|
|
994
|
+
*
|
|
995
|
+
* `--for` is the terminal's `?p=`, and it follows the same rule the website
|
|
996
|
+
* does: the dashboard resolves it against the roster and the database (or the
|
|
997
|
+
* route's own mirror of the database's rule) decides whether the page opens.
|
|
998
|
+
* Nothing is decided here.
|
|
999
|
+
*/
|
|
1000
|
+
function parseBriefArgs(args) {
|
|
1001
|
+
const values = parseNamedArgs(args, {
|
|
1002
|
+
allowedFlags: [
|
|
1003
|
+
"--home",
|
|
1004
|
+
"--dashboard-url",
|
|
1005
|
+
"--for",
|
|
1006
|
+
"--as",
|
|
1007
|
+
"--version",
|
|
1008
|
+
"--tldr",
|
|
1009
|
+
"--full",
|
|
1010
|
+
"--versions",
|
|
1011
|
+
"--claims",
|
|
1012
|
+
"--json",
|
|
1013
|
+
],
|
|
1014
|
+
valueFlags: ["--home", "--dashboard-url", "--for", "--as", "--version"],
|
|
1015
|
+
});
|
|
1016
|
+
assertNoPositionals(values.positionals, "brief");
|
|
1017
|
+
const tldr = values.booleans.has("--tldr");
|
|
1018
|
+
const full = values.booleans.has("--full");
|
|
1019
|
+
if (tldr && full) {
|
|
1020
|
+
throw new Error("brief accepts either --tldr or --full, not both.");
|
|
1021
|
+
}
|
|
1022
|
+
// `--as` is accepted as an alias so the two conversational commands read the
|
|
1023
|
+
// same way; `cockpit jarvis --as <person>` has meant this since BLI-3380.
|
|
1024
|
+
const forPerson = optionalNonEmpty(values.flags.get("--for"));
|
|
1025
|
+
const asPerson = optionalNonEmpty(values.flags.get("--as"));
|
|
1026
|
+
if (forPerson && asPerson && forPerson !== asPerson) {
|
|
1027
|
+
throw new Error("brief --for and --as must name the same person.");
|
|
1028
|
+
}
|
|
1029
|
+
return {
|
|
1030
|
+
kind: "brief",
|
|
1031
|
+
homeDir: optionalNonEmpty(values.flags.get("--home")),
|
|
1032
|
+
dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")),
|
|
1033
|
+
subject: forPerson ?? asPerson,
|
|
1034
|
+
version: optionalNonEmpty(values.flags.get("--version")),
|
|
1035
|
+
tldr,
|
|
1036
|
+
versions: values.booleans.has("--versions"),
|
|
1037
|
+
claims: values.booleans.has("--claims"),
|
|
1038
|
+
json: values.booleans.has("--json"),
|
|
1039
|
+
};
|
|
1040
|
+
}
|
|
1041
|
+
/**
|
|
1042
|
+
* `cockpit correct` (BLI-3458) — say that one line on the page is wrong.
|
|
1043
|
+
*
|
|
1044
|
+
* `--claim` is required and comes from `cockpit brief --claims`, which prints
|
|
1045
|
+
* the id beside every line it names. A correction with no claim would be a note
|
|
1046
|
+
* about the page in general, and the panel has a sentinel for that; the
|
|
1047
|
+
* terminal does not offer it, because a person who has just read
|
|
1048
|
+
* `[claimId] the sentence` has the id in front of them.
|
|
1049
|
+
*
|
|
1050
|
+
* `--text` may be omitted when something is piped in — `git log | cockpit
|
|
1051
|
+
* correct --claim x` — and the command reads stdin instead. Absent both, the
|
|
1052
|
+
* command refuses rather than filing an empty correction.
|
|
1053
|
+
*/
|
|
1054
|
+
function parseCorrectArgs(args) {
|
|
1055
|
+
const values = parseNamedArgs(args, {
|
|
1056
|
+
allowedFlags: [
|
|
1057
|
+
"--home",
|
|
1058
|
+
"--dashboard-url",
|
|
1059
|
+
"--claim",
|
|
1060
|
+
"--text",
|
|
1061
|
+
"--for",
|
|
1062
|
+
"--as",
|
|
1063
|
+
"--version",
|
|
1064
|
+
"--supersedes",
|
|
1065
|
+
"--json",
|
|
1066
|
+
],
|
|
1067
|
+
valueFlags: [
|
|
1068
|
+
"--home",
|
|
1069
|
+
"--dashboard-url",
|
|
1070
|
+
"--claim",
|
|
1071
|
+
"--text",
|
|
1072
|
+
"--for",
|
|
1073
|
+
"--as",
|
|
1074
|
+
"--version",
|
|
1075
|
+
"--supersedes",
|
|
1076
|
+
],
|
|
1077
|
+
});
|
|
1078
|
+
const claimId = optionalNonEmpty(values.flags.get("--claim"));
|
|
1079
|
+
if (!claimId) {
|
|
1080
|
+
throw new Error("correct needs --claim <claimId>. Run `cockpit brief --claims` to see the id beside every line.");
|
|
1081
|
+
}
|
|
1082
|
+
const flaggedText = optionalNonEmpty(values.flags.get("--text"));
|
|
1083
|
+
const positionalText = optionalNonEmpty(values.positionals.join(" "));
|
|
1084
|
+
if (flaggedText && positionalText) {
|
|
1085
|
+
throw new Error("correct accepts either --text or positional text, not both.");
|
|
1086
|
+
}
|
|
1087
|
+
const text = flaggedText ?? positionalText;
|
|
1088
|
+
if (text && text.length > 4000) {
|
|
1089
|
+
throw new Error("A correction is limited to 4000 characters.");
|
|
1090
|
+
}
|
|
1091
|
+
const forPerson = optionalNonEmpty(values.flags.get("--for"));
|
|
1092
|
+
const asPerson = optionalNonEmpty(values.flags.get("--as"));
|
|
1093
|
+
if (forPerson && asPerson && forPerson !== asPerson) {
|
|
1094
|
+
throw new Error("correct --for and --as must name the same person.");
|
|
1095
|
+
}
|
|
1096
|
+
return {
|
|
1097
|
+
kind: "correct",
|
|
1098
|
+
homeDir: optionalNonEmpty(values.flags.get("--home")),
|
|
1099
|
+
dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")),
|
|
1100
|
+
claimId,
|
|
1101
|
+
text,
|
|
1102
|
+
subject: forPerson ?? asPerson,
|
|
1103
|
+
version: optionalNonEmpty(values.flags.get("--version")),
|
|
1104
|
+
supersedes: optionalNonEmpty(values.flags.get("--supersedes")),
|
|
1105
|
+
json: values.booleans.has("--json"),
|
|
1106
|
+
};
|
|
1107
|
+
}
|
|
1108
|
+
const NOTES_ACTIONS = new Set([
|
|
1109
|
+
"list",
|
|
1110
|
+
"show",
|
|
1111
|
+
"shelf",
|
|
1112
|
+
"shelves",
|
|
1113
|
+
"upload",
|
|
1114
|
+
"paste",
|
|
1115
|
+
"share",
|
|
1116
|
+
"unshare",
|
|
1117
|
+
"move",
|
|
1118
|
+
]);
|
|
1119
|
+
/** Actions whose first positional is the note it acts on. */
|
|
1120
|
+
const NOTES_ACTIONS_NEEDING_A_NOTE = new Set([
|
|
1121
|
+
"show",
|
|
1122
|
+
"share",
|
|
1123
|
+
"unshare",
|
|
1124
|
+
"move",
|
|
1125
|
+
]);
|
|
1126
|
+
function parseNotesArgs(args) {
|
|
1127
|
+
const values = parseNamedArgs(args, {
|
|
1128
|
+
allowedFlags: [
|
|
1129
|
+
"--home",
|
|
1130
|
+
"--dashboard-url",
|
|
1131
|
+
"--file",
|
|
1132
|
+
"--name",
|
|
1133
|
+
"--exclude",
|
|
1134
|
+
"--to",
|
|
1135
|
+
"--clear-shelf",
|
|
1136
|
+
"--series",
|
|
1137
|
+
"--kind",
|
|
1138
|
+
"--since",
|
|
1139
|
+
"--until",
|
|
1140
|
+
"--limit",
|
|
1141
|
+
"--yes",
|
|
1142
|
+
"--json",
|
|
1143
|
+
],
|
|
1144
|
+
valueFlags: [
|
|
1145
|
+
"--home",
|
|
1146
|
+
"--dashboard-url",
|
|
1147
|
+
"--file",
|
|
1148
|
+
"--name",
|
|
1149
|
+
"--exclude",
|
|
1150
|
+
"--to",
|
|
1151
|
+
"--series",
|
|
1152
|
+
"--kind",
|
|
1153
|
+
"--since",
|
|
1154
|
+
"--until",
|
|
1155
|
+
"--limit",
|
|
1156
|
+
],
|
|
1157
|
+
});
|
|
1158
|
+
// Bare `cockpit notes` is the list, which is the thing a person typing it
|
|
1159
|
+
// almost always wants — same reasoning as bare `cockpit` running convergence.
|
|
1160
|
+
const first = values.positionals[0];
|
|
1161
|
+
const action = (first === undefined ? "list" : first);
|
|
1162
|
+
if (!NOTES_ACTIONS.has(action)) {
|
|
1163
|
+
throw new Error(`Unknown notes command: ${first}. Try list, show, shelf, shelves, upload, paste, share, unshare, or move.`);
|
|
1164
|
+
}
|
|
1165
|
+
const rest = values.positionals.slice(first === undefined ? 0 : 1);
|
|
1166
|
+
const json = values.booleans.has("--json");
|
|
1167
|
+
// `--json` implies `--yes`: a machine-readable run has nobody to prompt, and
|
|
1168
|
+
// failing it for want of a confirmation flag it cannot see is a worse
|
|
1169
|
+
// surprise than an explicit share it already asked for by name.
|
|
1170
|
+
const yes = values.booleans.has("--yes") || json;
|
|
1171
|
+
let noteId;
|
|
1172
|
+
if (NOTES_ACTIONS_NEEDING_A_NOTE.has(action)) {
|
|
1173
|
+
noteId = optionalNonEmpty(rest[0]);
|
|
1174
|
+
if (!noteId)
|
|
1175
|
+
throw new Error(`notes ${action} needs a note id.`);
|
|
1176
|
+
if (rest.length > 1) {
|
|
1177
|
+
throw new Error(`notes ${action} takes one note id, not ${rest.length}.`);
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
let paths;
|
|
1181
|
+
if (action === "upload") {
|
|
1182
|
+
// Explicit paths only. No globbing happens here — a shell that expands
|
|
1183
|
+
// `*.md` hands us the names it found, and a shell that does not would have
|
|
1184
|
+
// this command silently uploading a file literally called `*.md`.
|
|
1185
|
+
paths = rest.filter((value) => value.trim() !== "");
|
|
1186
|
+
if (paths.length === 0)
|
|
1187
|
+
throw new Error("notes upload needs at least one file path.");
|
|
1188
|
+
}
|
|
1189
|
+
else if (action !== "paste" && !NOTES_ACTIONS_NEEDING_A_NOTE.has(action) && rest.length > 0) {
|
|
1190
|
+
throw new Error(`notes ${action} does not take "${rest[0]}".`);
|
|
1191
|
+
}
|
|
1192
|
+
const to = optionalNonEmpty(values.flags.get("--to"));
|
|
1193
|
+
const clearShelf = values.booleans.has("--clear-shelf");
|
|
1194
|
+
if (action === "move") {
|
|
1195
|
+
if (to && clearShelf) {
|
|
1196
|
+
throw new Error("notes move accepts either --to or --clear-shelf, not both.");
|
|
1197
|
+
}
|
|
1198
|
+
if (!to && !clearShelf) {
|
|
1199
|
+
throw new Error('notes move needs --to "<shelf>" or --clear-shelf.');
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
const limit = optionalPositiveInteger(values.flags.get("--limit"), "--limit");
|
|
1203
|
+
return {
|
|
1204
|
+
kind: "notes",
|
|
1205
|
+
action,
|
|
1206
|
+
homeDir: optionalNonEmpty(values.flags.get("--home")),
|
|
1207
|
+
dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")),
|
|
1208
|
+
...(noteId ? { noteId } : {}),
|
|
1209
|
+
...(paths ? { paths } : {}),
|
|
1210
|
+
filePath: optionalNonEmpty(values.flags.get("--file")),
|
|
1211
|
+
name: optionalNonEmpty(values.flags.get("--name")),
|
|
1212
|
+
exclude: optionalNonEmpty(values.flags.get("--exclude")),
|
|
1213
|
+
...(to ? { to } : {}),
|
|
1214
|
+
...(clearShelf ? { clearShelf } : {}),
|
|
1215
|
+
series: optionalNonEmpty(values.flags.get("--series")),
|
|
1216
|
+
meetingKind: optionalNonEmpty(values.flags.get("--kind")),
|
|
1217
|
+
since: optionalNonEmpty(values.flags.get("--since")),
|
|
1218
|
+
until: optionalNonEmpty(values.flags.get("--until")),
|
|
1219
|
+
...(limit === undefined ? {} : { limit }),
|
|
1220
|
+
yes,
|
|
1221
|
+
json,
|
|
1222
|
+
};
|
|
1223
|
+
}
|
|
662
1224
|
function parseNamedArgs(args, options) {
|
|
663
1225
|
const allowed = new Set(options.allowedFlags);
|
|
664
1226
|
const valueFlags = new Set(options.valueFlags);
|