@bli-cockpit/cli 0.2.236 → 0.2.238

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.
Files changed (143) hide show
  1. package/dist/adapters/attribution-core-fallbacks.js +15 -11
  2. package/dist/adapters/attribution-core-score.js +111 -87
  3. package/dist/adapters/car-state.js +38 -35
  4. package/dist/adapters/claude-attribution-discovery.js +81 -56
  5. package/dist/adapters/claude-attribution-score.js +108 -94
  6. package/dist/adapters/claude-attribution-signals.js +52 -42
  7. package/dist/adapters/codex-attribution.js +42 -33
  8. package/dist/adapters/codex-session-signals.js +11 -9
  9. package/dist/adapters/git-state.js +56 -50
  10. package/dist/adapters/local-sources.js +52 -43
  11. package/dist/adapters/raw-evidence-git-diff.js +4 -2
  12. package/dist/adapters/raw-evidence-sanitize.js +15 -12
  13. package/dist/adapters/raw-evidence-transcript-file.js +32 -31
  14. package/dist/autostart-self-heal.js +18 -2
  15. package/dist/commands/agent-session-report.js +51 -59
  16. package/dist/commands/agent-setup-observed.js +30 -23
  17. package/dist/commands/agent-setup.js +96 -45
  18. package/dist/commands/autostart-heal.js +8 -2
  19. package/dist/commands/autostart-reading.js +3 -0
  20. package/dist/commands/backfill-evidence-outcomes.js +9 -6
  21. package/dist/commands/backfill-issues.js +3 -0
  22. package/dist/commands/backfill-scan.js +27 -22
  23. package/dist/commands/backfill-upload.js +69 -60
  24. package/dist/commands/backfill.js +26 -21
  25. package/dist/commands/brief-done.js +4 -1
  26. package/dist/commands/brief-edit.js +71 -46
  27. package/dist/commands/cal-detail.js +35 -0
  28. package/dist/commands/cal-overlay.js +106 -0
  29. package/dist/commands/cal.js +40 -23
  30. package/dist/commands/clean.js +4 -1
  31. package/dist/commands/docs-read.js +5 -2
  32. package/dist/commands/doctor-backfill.js +88 -71
  33. package/dist/commands/doctor-pipeline.js +59 -46
  34. package/dist/commands/doctor-usage-telemetry.js +2 -1
  35. package/dist/commands/heartbeat-post.js +116 -0
  36. package/dist/commands/heartbeat-token-lock.js +46 -33
  37. package/dist/commands/heartbeat.js +43 -125
  38. package/dist/commands/install-update.js +17 -15
  39. package/dist/commands/issue-show.js +27 -28
  40. package/dist/commands/issue-write.js +70 -23
  41. package/dist/commands/issue.js +68 -37
  42. package/dist/commands/jarvis-live-answer.js +99 -0
  43. package/dist/commands/jarvis-render.js +1 -94
  44. package/dist/commands/jarvis-trace.js +30 -22
  45. package/dist/commands/jarvis-turn-report.js +124 -0
  46. package/dist/commands/jarvis-turn.js +108 -182
  47. package/dist/commands/jarvis.js +26 -17
  48. package/dist/commands/local-args-collector-setup.js +82 -84
  49. package/dist/commands/local-args-collector-work.js +37 -38
  50. package/dist/commands/local-args-tower-admin.js +155 -142
  51. package/dist/commands/local-args-tower-cal.js +54 -48
  52. package/dist/commands/local-args-tower-careers.js +73 -54
  53. package/dist/commands/local-args-tower-docs-msg.js +56 -188
  54. package/dist/commands/local-args-tower-github.js +26 -20
  55. package/dist/commands/local-args-tower-hits.js +32 -20
  56. package/dist/commands/local-args-tower-manager.js +85 -4
  57. package/dist/commands/local-args-tower-memory.js +139 -117
  58. package/dist/commands/local-args-tower-msg.js +149 -0
  59. package/dist/commands/local-args-tower-notes.js +346 -0
  60. package/dist/commands/local-args-tower-pages.js +100 -378
  61. package/dist/commands/local-args-tower-settings.js +28 -29
  62. package/dist/commands/local-args-tower-work.js +52 -33
  63. package/dist/commands/local-args.js +80 -129
  64. package/dist/commands/local-help-commands-tower-ops.js +15 -4
  65. package/dist/commands/local-help-commands-tower.js +11 -4
  66. package/dist/commands/local-help-commands.js +342 -341
  67. package/dist/commands/local-help.js +87 -83
  68. package/dist/commands/local-run-guards.js +100 -0
  69. package/dist/commands/local.js +137 -206
  70. package/dist/commands/login.js +13 -9
  71. package/dist/commands/logs.js +55 -38
  72. package/dist/commands/mail-attachments.js +44 -26
  73. package/dist/commands/mail.js +6 -4
  74. package/dist/commands/manager-render-home.js +208 -0
  75. package/dist/commands/manager-render.js +7 -3
  76. package/dist/commands/manager-requests.js +33 -0
  77. package/dist/commands/manager.js +39 -5
  78. package/dist/commands/memory-consolidation.js +18 -9
  79. package/dist/commands/memory-daemon-probe.js +37 -28
  80. package/dist/commands/memory-install-config.js +64 -60
  81. package/dist/commands/memory-install.js +21 -41
  82. package/dist/commands/memory-requeue.js +26 -19
  83. package/dist/commands/memory-rules.js +26 -18
  84. package/dist/commands/memory-search-print.js +26 -0
  85. package/dist/commands/memory-store.js +14 -30
  86. package/dist/commands/memory-verb-list.js +49 -48
  87. package/dist/commands/msg.js +51 -9
  88. package/dist/commands/notes-accounts.js +49 -32
  89. package/dist/commands/notes-sync-loop.js +115 -89
  90. package/dist/commands/notes-wisprflow-login.js +44 -23
  91. package/dist/commands/notes-writes.js +115 -100
  92. package/dist/commands/notes.js +33 -55
  93. package/dist/commands/onboard-flows.js +69 -51
  94. package/dist/commands/onboard.js +11 -9
  95. package/dist/commands/ops-render-memory.js +10 -7
  96. package/dist/commands/ops-render.js +33 -30
  97. package/dist/commands/ops.js +3 -0
  98. package/dist/commands/public-root.js +1 -1
  99. package/dist/commands/session-sync-outside-roots.js +40 -36
  100. package/dist/commands/session-sync-plan.js +14 -10
  101. package/dist/commands/session-sync-upload.js +24 -22
  102. package/dist/commands/session-sync.js +19 -8
  103. package/dist/commands/slack.js +25 -16
  104. package/dist/commands/sources-check.js +13 -7
  105. package/dist/commands/sources-discovery.js +25 -17
  106. package/dist/commands/sources-verdict.js +19 -18
  107. package/dist/commands/speed.js +23 -19
  108. package/dist/commands/start.js +20 -16
  109. package/dist/commands/sync-doctor.js +52 -48
  110. package/dist/commands/sync.js +22 -16
  111. package/dist/commands/usage-telemetry-toml.js +71 -45
  112. package/dist/commands/usage.js +91 -67
  113. package/dist/commands/workbook-render.js +10 -7
  114. package/dist/evidence-reconcile-client.js +75 -64
  115. package/dist/evidence-redelivery-plan.js +37 -34
  116. package/dist/evidence-redelivery.js +32 -29
  117. package/dist/evidence-transcripts-client.js +20 -20
  118. package/dist/evidence-upload-object.js +86 -89
  119. package/dist/evidence-upload-plan.js +94 -83
  120. package/dist/evidence-upload-terminal.js +45 -50
  121. package/dist/image-description-session.js +156 -159
  122. package/dist/local-state-pairing-code.js +39 -31
  123. package/dist/local-state-status.js +9 -2
  124. package/dist/onboarding-roots.js +5 -8
  125. package/dist/process-runner.js +8 -8
  126. package/dist/raw-evidence-gc.js +33 -38
  127. package/dist/raw-evidence-stuck-summary.js +7 -14
  128. package/dist/repo-identity.js +26 -23
  129. package/dist/scheduled-self-update.js +11 -15
  130. package/dist/second-install.js +3 -0
  131. package/dist/slim/codex-rules.js +23 -36
  132. package/dist/sync-lock.js +5 -15
  133. package/dist/tower-client.js +36 -53
  134. package/dist/tower-stream.js +7 -24
  135. package/dist/upload-agent-artifacts.js +58 -64
  136. package/dist/upload-envelope-build.js +60 -59
  137. package/dist/upload-evidence-delivery-reconcile.js +58 -56
  138. package/dist/upload-evidence-delivery-summary.js +32 -34
  139. package/dist/upload-ingest-receipt.js +20 -23
  140. package/dist/upload-session-reports-queue.js +4 -1
  141. package/dist/upload-session-reports-wire.js +64 -68
  142. package/dist/upload-sync.js +171 -157
  143. package/package.json +2 -2
@@ -20,6 +20,7 @@
20
20
  */
21
21
  import { optionalNonEmpty, optionalPositiveInteger, optionalUrl, parseNamedArgs, } from "./local-arg-values.js";
22
22
  const CAL_ACTIONS = new Set([
23
+ "event", "export", "conflicts", "calendar-change",
23
24
  "today",
24
25
  "week",
25
26
  "next",
@@ -35,56 +36,59 @@ const CAL_ACTIONS = new Set([
35
36
  "delete",
36
37
  ]);
37
38
  /** Every verb that names ONE thing first — a calendar id, or (delete) an event id. */
38
- const CAL_ACTIONS_NEEDING_A_SUBJECT = new Set(["sync", "detach", "share", "delete", "change", "cancel"]);
39
+ const CAL_ACTIONS_NEEDING_A_SUBJECT = new Set(["event", "export", "calendar-change", "sync", "detach", "share", "delete", "change", "cancel"]);
39
40
  /** `sync-status [<calendar id>]` — every calendar, or one's. Ticket 10730. */
40
41
  const CAL_ACTIONS_WITH_OPTIONAL_SUBJECT = new Set(["sync-status"]);
42
+ const CAL_ALLOWED_FLAGS = [
43
+ "--home",
44
+ "--dashboard-url",
45
+ "--color",
46
+ "--calendar",
47
+ "--account",
48
+ "--tz",
49
+ "--occurrence-start",
50
+ "--offset",
51
+ "--hours",
52
+ "--from",
53
+ "--to",
54
+ "--limit",
55
+ "--all",
56
+ "--name",
57
+ "--title",
58
+ "--at",
59
+ "--until",
60
+ "--location",
61
+ "--attendee",
62
+ "--all-day",
63
+ "--meet",
64
+ "--org-visible",
65
+ "--private",
66
+ "--full",
67
+ "--yes",
68
+ "--json",
69
+ ];
70
+ const CAL_VALUE_FLAGS = [
71
+ "--home",
72
+ "--dashboard-url",
73
+ "--color",
74
+ "--calendar",
75
+ "--account",
76
+ "--tz",
77
+ "--occurrence-start",
78
+ "--offset",
79
+ "--hours",
80
+ "--from",
81
+ "--to",
82
+ "--limit",
83
+ "--name",
84
+ "--title",
85
+ "--at",
86
+ "--until",
87
+ "--location",
88
+ "--attendee",
89
+ ];
41
90
  export function parseCalArgs(args) {
42
- const values = parseNamedArgs(args, {
43
- allowedFlags: [
44
- "--home",
45
- "--dashboard-url",
46
- "--calendar",
47
- "--account",
48
- "--tz",
49
- "--offset",
50
- "--hours",
51
- "--from",
52
- "--to",
53
- "--limit",
54
- "--all",
55
- "--name",
56
- "--title",
57
- "--at",
58
- "--until",
59
- "--location",
60
- "--attendee",
61
- "--all-day",
62
- "--meet",
63
- "--org-visible",
64
- "--private",
65
- "--full",
66
- "--yes",
67
- "--json",
68
- ],
69
- valueFlags: [
70
- "--home",
71
- "--dashboard-url",
72
- "--calendar",
73
- "--account",
74
- "--tz",
75
- "--offset",
76
- "--hours",
77
- "--from",
78
- "--to",
79
- "--limit",
80
- "--name",
81
- "--title",
82
- "--at",
83
- "--until",
84
- "--location",
85
- "--attendee",
86
- ],
87
- });
91
+ const values = parseNamedArgs(args, { allowedFlags: CAL_ALLOWED_FLAGS, valueFlags: CAL_VALUE_FLAGS });
88
92
  const { action, rest } = readCalAction(values);
89
93
  const { subject, query } = readCalSubjectOrQuery(action, rest);
90
94
  const { title, startsAt, endsAt } = readCalCreateDetails(action, values);
@@ -100,7 +104,7 @@ function readCalAction(values) {
100
104
  const first = values.positionals[0];
101
105
  const action = (first === undefined ? "today" : first);
102
106
  if (!CAL_ACTIONS.has(action)) {
103
- throw new Error(`Unknown cal command: ${first}. Try today, week, next, find, calendars, add-ical, create, share, detach, sync, sync-status, or delete.`);
107
+ throw new Error(`Unknown cal command: ${first}. Try today, week, conflicts, calendar-change, next, find, calendars, add-ical, create, share, detach, sync, sync-status, or delete.`);
104
108
  }
105
109
  return { action, rest: values.positionals.slice(first === undefined ? 0 : 1) };
106
110
  }
@@ -170,6 +174,8 @@ function readCalOffset(values) {
170
174
  function buildCalCommand(values, action, subject, query, title, startsAt, endsAt, offset, limit) {
171
175
  return {
172
176
  kind: "cal",
177
+ occurrenceStart: optionalNonEmpty(values.flags.get("--occurrence-start")),
178
+ color: optionalNonEmpty(values.flags.get("--color")),
173
179
  action,
174
180
  homeDir: optionalNonEmpty(values.flags.get("--home")),
175
181
  dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")),
@@ -9,11 +9,56 @@ const NEEDS_ROLE = ['takehome-show', 'takehome-set'];
9
9
  export function parseCareersArgs(args) {
10
10
  const valueFlags = ['--role', '--min-score', '--since', '--home', '--dashboard-url', '--decision', '--stage', '--subject', '--body-file', '--link', '--pass-score', '--account', '--auto-invite-min-score', '--attention-min-score', '--enabled', '--booking-link', '--grader-model', '--out', '--record-sent-at', '--at', '--followup-enabled', '--followup-after-days', '--takehome-window-hours'];
11
11
  const values = parseNamedArgs(args, { allowedFlags: [...valueFlags, '--json', '--now', '--force', '--clear'], valueFlags });
12
- const positionals = [...values.positionals];
13
- // `takehome show` and `settings set` are two words a person types and one
14
- // action everything downstream reads.
15
- // A bare `careers takehome` or `careers settings` reads as its `show`, the way
16
- // `team device` reads as `team device list`: the noun alone is a question.
12
+ const { verb, target } = readCareersVerb([...values.positionals]);
13
+ const rawScore = values.flags.get('--min-score');
14
+ const minScore = rawScore === undefined ? undefined : Number(rawScore);
15
+ if (minScore !== undefined && (!Number.isFinite(minScore) || minScore < 0 || minScore > 100))
16
+ throw new Error('--min-score must be 0 to 100.');
17
+ const since = optionalNonEmpty(values.flags.get('--since'));
18
+ if (since && !Number.isFinite(Date.parse(since)))
19
+ throw new Error('--since must be a date.');
20
+ const decision = optionalNonEmpty(values.flags.get('--decision'));
21
+ if (verb === 'decide' && (!decision || !DECISIONS.includes(decision)))
22
+ throw new Error(`careers decide needs --decision ${DECISIONS.join('|')}.`);
23
+ const passScore = numberFlag(values.flags.get('--pass-score'), '--pass-score', 0, 10);
24
+ const autoInviteMinScore = numberFlag(values.flags.get('--auto-invite-min-score'), '--auto-invite-min-score', 0, 100);
25
+ const attentionMinScore = numberFlag(values.flags.get('--attention-min-score'), '--attention-min-score', 0, 100);
26
+ const enabledRaw = trueOrFalseFlag(values.flags.get('--enabled'), '--enabled');
27
+ const recordSentAt = readRecordSentAt(values, verb);
28
+ const { interviewAt, clear } = readInterviewFlags(values, verb);
29
+ const followupEnabledRaw = trueOrFalseFlag(values.flags.get('--followup-enabled'), '--followup-enabled');
30
+ const followupAfterDays = wholeFlag(values.flags.get('--followup-after-days'), '--followup-after-days', 1, 60);
31
+ const takehomeWindowHours = wholeFlag(values.flags.get('--takehome-window-hours'), '--takehome-window-hours', 1, 720);
32
+ const { outPath, force } = readResumeFileFlags(values, verb);
33
+ return {
34
+ kind: 'careers', action: verb,
35
+ id: NEEDS_ID.includes(verb) ? target : undefined,
36
+ takehomeRole: NEEDS_ROLE.includes(verb) ? target : undefined,
37
+ role: optionalNonEmpty(values.flags.get('--role')), minScore, since: since ? new Date(since).toISOString() : undefined,
38
+ stage: optionalNonEmpty(values.flags.get('--stage')), decision,
39
+ subject: optionalNonEmpty(values.flags.get('--subject')), bodyFile: optionalNonEmpty(values.flags.get('--body-file')),
40
+ link: optionalNonEmpty(values.flags.get('--link')), passScore,
41
+ inviteAccountId: optionalNonEmpty(values.flags.get('--account')),
42
+ bookingLink: httpsFlag(values.flags.get('--booking-link')),
43
+ graderModel: optionalNonEmpty(values.flags.get('--grader-model')),
44
+ now: values.booleans.has('--now'),
45
+ outPath, force,
46
+ interviewAt: interviewAt ? new Date(interviewAt).toISOString() : undefined, clear: clear || undefined,
47
+ recordSentAt: recordSentAt ? new Date(recordSentAt).toISOString() : undefined,
48
+ followupEnabled: followupEnabledRaw === undefined ? undefined : followupEnabledRaw === 'true',
49
+ followupAfterDays, takehomeWindowHours,
50
+ autoInviteMinScore, attentionMinScore,
51
+ autoInviteEnabled: enabledRaw === undefined ? undefined : enabledRaw === 'true',
52
+ homeDir: optionalNonEmpty(values.flags.get('--home')), dashboardUrl: optionalUrl(values.flags.get('--dashboard-url')), json: values.booleans.has('--json'),
53
+ };
54
+ }
55
+ /**
56
+ * `takehome show` and `settings set` are two words a person types and one
57
+ * action everything downstream reads.
58
+ * A bare `careers takehome` or `careers settings` reads as its `show`, the way
59
+ * `team device` reads as `team device list`: the noun alone is a question.
60
+ */
61
+ function readCareersVerb(positionals) {
17
62
  const first = positionals[0] ?? 'list';
18
63
  const paired = first === 'takehome' || first === 'settings';
19
64
  const typed = paired
@@ -32,32 +77,28 @@ export function parseCareersArgs(args) {
32
77
  throw new Error(`careers ${verb.replace('-', ' ')} needs a role slug.`);
33
78
  if (positionals.length > 0)
34
79
  throw new Error('Unexpected careers argument.');
35
- const rawScore = values.flags.get('--min-score');
36
- const minScore = rawScore === undefined ? undefined : Number(rawScore);
37
- if (minScore !== undefined && (!Number.isFinite(minScore) || minScore < 0 || minScore > 100))
38
- throw new Error('--min-score must be 0 to 100.');
39
- const since = optionalNonEmpty(values.flags.get('--since'));
40
- if (since && !Number.isFinite(Date.parse(since)))
41
- throw new Error('--since must be a date.');
42
- const decision = optionalNonEmpty(values.flags.get('--decision'));
43
- if (verb === 'decide' && (!decision || !DECISIONS.includes(decision)))
44
- throw new Error(`careers decide needs --decision ${DECISIONS.join('|')}.`);
45
- const passScore = numberFlag(values.flags.get('--pass-score'), '--pass-score', 0, 10);
46
- const autoInviteMinScore = numberFlag(values.flags.get('--auto-invite-min-score'), '--auto-invite-min-score', 0, 100);
47
- const attentionMinScore = numberFlag(values.flags.get('--attention-min-score'), '--attention-min-score', 0, 100);
48
- const enabledRaw = optionalNonEmpty(values.flags.get('--enabled'));
49
- if (enabledRaw !== undefined && enabledRaw !== 'true' && enabledRaw !== 'false')
50
- throw new Error('--enabled must be true or false.');
51
- // BLI-12183: the second-chance follow-up. `--record-sent-at` records one sent
52
- // outside Tower and sends nothing; the three settings flags are whole numbers
53
- // and a boolean, refused here as well as on the door.
80
+ return { typed, verb, target };
81
+ }
82
+ function trueOrFalseFlag(raw, flag) {
83
+ const value = optionalNonEmpty(raw);
84
+ if (value !== undefined && value !== 'true' && value !== 'false')
85
+ throw new Error(`${flag} must be true or false.`);
86
+ return value;
87
+ }
88
+ // BLI-12183: the second-chance follow-up. `--record-sent-at` records one sent
89
+ // outside Tower and sends nothing; the three settings flags are whole numbers
90
+ // and a boolean, refused here as well as on the door.
91
+ function readRecordSentAt(values, verb) {
54
92
  const recordSentAt = optionalNonEmpty(values.flags.get('--record-sent-at'));
55
93
  if (recordSentAt !== undefined && verb !== 'followup')
56
94
  throw new Error('--record-sent-at belongs to `cockpit careers followup <id>`.');
57
95
  if (recordSentAt !== undefined && !Number.isFinite(Date.parse(recordSentAt)))
58
96
  throw new Error('--record-sent-at must be an ISO date-time, e.g. 2026-10-03T08:00:00Z.');
59
- // BLI-12198: an interview Edward booked himself. Exactly one of `--at <iso>`
60
- // (book or reschedule) or `--clear`, refused here as well as on the door.
97
+ return recordSentAt;
98
+ }
99
+ // BLI-12198: an interview Edward booked himself. Exactly one of `--at <iso>`
100
+ // (book or reschedule) or `--clear`, refused here as well as on the door.
101
+ function readInterviewFlags(values, verb) {
61
102
  const interviewAt = optionalNonEmpty(values.flags.get('--at'));
62
103
  const clear = values.booleans.has('--clear');
63
104
  if (interviewAt !== undefined && verb !== 'interview')
@@ -71,13 +112,11 @@ export function parseCareersArgs(args) {
71
112
  throw new Error('--at must be an ISO date-time with an offset, e.g. 2026-10-05T10:00:00-07:00.');
72
113
  }
73
114
  }
74
- const followupEnabledRaw = optionalNonEmpty(values.flags.get('--followup-enabled'));
75
- if (followupEnabledRaw !== undefined && followupEnabledRaw !== 'true' && followupEnabledRaw !== 'false')
76
- throw new Error('--followup-enabled must be true or false.');
77
- const followupAfterDays = wholeFlag(values.flags.get('--followup-after-days'), '--followup-after-days', 1, 60);
78
- const takehomeWindowHours = wholeFlag(values.flags.get('--takehome-window-hours'), '--takehome-window-hours', 1, 720);
79
- // The resume is the only thing this noun writes to disk, so both file flags
80
- // belong to it alone and say so rather than being accepted and ignored.
115
+ return { interviewAt, clear };
116
+ }
117
+ // The resume is the only thing this noun writes to disk, so both file flags
118
+ // belong to it alone and say so rather than being accepted and ignored.
119
+ function readResumeFileFlags(values, verb) {
81
120
  const outPath = optionalNonEmpty(values.flags.get('--out'));
82
121
  const force = values.booleans.has('--force');
83
122
  if (outPath !== undefined && verb !== 'resume')
@@ -86,27 +125,7 @@ export function parseCareersArgs(args) {
86
125
  throw new Error('--force belongs to `cockpit careers resume <id> --out <path>`.');
87
126
  if (force && outPath === undefined)
88
127
  throw new Error('--force only means something beside --out <path>: with no file to write there is nothing to overwrite.');
89
- return {
90
- kind: 'careers', action: verb,
91
- id: NEEDS_ID.includes(verb) ? target : undefined,
92
- takehomeRole: NEEDS_ROLE.includes(verb) ? target : undefined,
93
- role: optionalNonEmpty(values.flags.get('--role')), minScore, since: since ? new Date(since).toISOString() : undefined,
94
- stage: optionalNonEmpty(values.flags.get('--stage')), decision,
95
- subject: optionalNonEmpty(values.flags.get('--subject')), bodyFile: optionalNonEmpty(values.flags.get('--body-file')),
96
- link: optionalNonEmpty(values.flags.get('--link')), passScore,
97
- inviteAccountId: optionalNonEmpty(values.flags.get('--account')),
98
- bookingLink: httpsFlag(values.flags.get('--booking-link')),
99
- graderModel: optionalNonEmpty(values.flags.get('--grader-model')),
100
- now: values.booleans.has('--now'),
101
- outPath, force,
102
- interviewAt: interviewAt ? new Date(interviewAt).toISOString() : undefined, clear: clear || undefined,
103
- recordSentAt: recordSentAt ? new Date(recordSentAt).toISOString() : undefined,
104
- followupEnabled: followupEnabledRaw === undefined ? undefined : followupEnabledRaw === 'true',
105
- followupAfterDays, takehomeWindowHours,
106
- autoInviteMinScore, attentionMinScore,
107
- autoInviteEnabled: enabledRaw === undefined ? undefined : enabledRaw === 'true',
108
- homeDir: optionalNonEmpty(values.flags.get('--home')), dashboardUrl: optionalUrl(values.flags.get('--dashboard-url')), json: values.booleans.has('--json'),
109
- };
128
+ return { outPath, force };
110
129
  }
111
130
  /**
112
131
  * BLI-4461: a booking link goes into an email a stranger clicks, so the
@@ -15,44 +15,43 @@ const DOCS_ACTIONS = new Set(["list", "tree", "read", "create", "update", "delet
15
15
  // (the same door, called once per ref), so it joined `read`'s branch rather
16
16
  // than this set.
17
17
  const DOCS_ACTIONS_NEEDING_A_DOC = new Set(["update", "link", "export"]);
18
+ const DOCS_ALLOWED_FLAGS = [
19
+ "--home",
20
+ "--dashboard-url",
21
+ "--title",
22
+ "--parent",
23
+ "--clear-parent",
24
+ "--allow-empty",
25
+ "--visibility",
26
+ "--file",
27
+ "--body-stdin",
28
+ "--query",
29
+ "--word",
30
+ "--limit",
31
+ "--clear",
32
+ "--bodies",
33
+ "--section",
34
+ "--offset",
35
+ "--format",
36
+ "--out",
37
+ "--json",
38
+ ];
39
+ const DOCS_VALUE_FLAGS = [
40
+ "--home",
41
+ "--dashboard-url",
42
+ "--title",
43
+ "--parent",
44
+ "--visibility",
45
+ "--file",
46
+ "--query",
47
+ "--limit",
48
+ "--section",
49
+ "--offset",
50
+ "--format",
51
+ "--out",
52
+ ];
18
53
  export function parseDocsArgs(args) {
19
- const values = parseNamedArgs(args, {
20
- allowedFlags: [
21
- "--home",
22
- "--dashboard-url",
23
- "--title",
24
- "--parent",
25
- "--clear-parent",
26
- "--allow-empty",
27
- "--visibility",
28
- "--file",
29
- "--body-stdin",
30
- "--query",
31
- "--word",
32
- "--limit",
33
- "--clear",
34
- "--bodies",
35
- "--section",
36
- "--offset",
37
- "--format",
38
- "--out",
39
- "--json",
40
- ],
41
- valueFlags: [
42
- "--home",
43
- "--dashboard-url",
44
- "--title",
45
- "--parent",
46
- "--visibility",
47
- "--file",
48
- "--query",
49
- "--limit",
50
- "--section",
51
- "--offset",
52
- "--format",
53
- "--out",
54
- ],
55
- });
54
+ const values = parseNamedArgs(args, { allowedFlags: DOCS_ALLOWED_FLAGS, valueFlags: DOCS_VALUE_FLAGS });
56
55
  const { action, rest } = parseDocsAction(values.positionals);
57
56
  const link = parseDocsLink(action, rest, values.booleans.has("--clear"));
58
57
  const { docRef, docRefs } = parseDocsDocumentReference(action, link.rest);
@@ -150,6 +149,10 @@ function parseDocsDocumentReference(action, rest) {
150
149
  return { docRef: undefined, docRefs: undefined };
151
150
  }
152
151
  function parseDocsFlags(action, values) {
152
+ return { ...parseDocsWriteFlags(action, values), ...parseDocsReadFlags(action, values) };
153
+ }
154
+ /** The flags that decide where a document lives and what it is called. */
155
+ function parseDocsWriteFlags(action, values) {
153
156
  const visibility = optionalNonEmpty(values.flags.get("--visibility"));
154
157
  if (visibility && visibility !== "org" && visibility !== "private") {
155
158
  throw new Error('docs --visibility must be "org" or "private".');
@@ -174,6 +177,10 @@ function parseDocsFlags(action, values) {
174
177
  if (action === "create" && !title) {
175
178
  throw new Error("docs create needs --title.");
176
179
  }
180
+ return { visibility, clearParent, parentId, allowEmpty, title };
181
+ }
182
+ /** The flags that narrow or slice what a list or read returns. */
183
+ function parseDocsReadFlags(action, values) {
177
184
  // BLI-3737: narrowing belongs to `list` only. `--parent` keeps its create/
178
185
  // update meaning elsewhere (which parent to file the document under); on
179
186
  // `list` the same word names which parent to list UNDER, plus the literal
@@ -205,16 +212,19 @@ function parseDocsFlags(action, values) {
205
212
  const section = optionalNonEmpty(values.flags.get("--section"));
206
213
  if (section && action !== "read")
207
214
  throw new Error("--section belongs to `cockpit docs read`.");
208
- const offsetRaw = optionalNonEmpty(values.flags.get("--offset"));
209
- let offset;
210
- if (offsetRaw !== undefined) {
211
- offset = Number(offsetRaw);
212
- if (!Number.isInteger(offset) || offset < 0)
213
- throw new Error("--offset must be a whole number 0 or greater.");
214
- }
215
+ const offset = parseDocsOffset(values.flags.get("--offset"));
215
216
  if (offset !== undefined && action !== "read")
216
217
  throw new Error("--offset belongs to `cockpit docs read`.");
217
- return { visibility, clearParent, parentId, allowEmpty, title, query, wholeWord, limit, bodies, section, offset };
218
+ return { query, wholeWord, limit, bodies, section, offset };
219
+ }
220
+ function parseDocsOffset(raw) {
221
+ const offsetRaw = optionalNonEmpty(raw);
222
+ if (offsetRaw === undefined)
223
+ return undefined;
224
+ const offset = Number(offsetRaw);
225
+ if (!Number.isInteger(offset) || offset < 0)
226
+ throw new Error("--offset must be a whole number 0 or greater.");
227
+ return offset;
218
228
  }
219
229
  function buildDocsCommand(values, action, docRef, docRefs, visibility, clearParent, parentId, allowEmpty, title, query, wholeWord, limit, bodies, section, offset, link) {
220
230
  return {
@@ -243,146 +253,4 @@ function buildDocsCommand(values, action, docRef, docRefs, visibility, clearPare
243
253
  json: values.booleans.has("--json"),
244
254
  };
245
255
  }
246
- const MSG_ACTIONS = new Set(["channels", "read", "send", "thread", "create", "dm"]);
247
- const MSG_ACTIONS_NEEDING_A_CHANNEL = new Set(["read", "send"]);
248
- /**
249
- * `--members a@x.test,b@y.test` — addresses only, split on commas, never
250
- * fuzzy (BLI-3749). The door resolves each one exactly, so a typo comes back
251
- * as `person_not_found` naming the address rather than as a stranger added
252
- * to a private channel.
253
- */
254
- function parseMemberEmails(raw) {
255
- const value = optionalNonEmpty(raw);
256
- if (!value)
257
- return undefined;
258
- const emails = value
259
- .split(",")
260
- .map((part) => part.trim())
261
- .filter((part) => part !== "");
262
- if (emails.length === 0)
263
- throw new Error("msg create --members needs at least one email address.");
264
- const notAnAddress = emails.find((email) => !email.includes("@"));
265
- if (notAnAddress) {
266
- throw new Error(`msg create --members takes email addresses; "${notAnAddress}" is not one.`);
267
- }
268
- return emails;
269
- }
270
- export function parseMsgArgs(args) {
271
- const values = parseNamedArgs(args, {
272
- allowedFlags: [
273
- "--home",
274
- "--dashboard-url",
275
- "--channel",
276
- "--thread",
277
- "--limit",
278
- "--private",
279
- "--members",
280
- "--description",
281
- "--json",
282
- ],
283
- valueFlags: ["--home", "--dashboard-url", "--channel", "--thread", "--limit", "--members", "--description"],
284
- });
285
- const { action, rest } = parseMsgAction(values.positionals);
286
- const { channelName, dmEmail } = parseMsgCreateTarget(action, rest);
287
- const { isPrivate, memberEmails, description } = parseMsgCreateFlags(action, values);
288
- const { channelRef, threadId, limit } = parseMsgReadTarget(action, rest, values);
289
- return buildMsgCommand(values, action, channelRef, channelName, dmEmail, memberEmails, description, isPrivate, threadId, limit);
290
- }
291
- function parseMsgAction(positionals) {
292
- const first = positionals[0];
293
- const action = (first === undefined ? "channels" : first);
294
- if (!MSG_ACTIONS.has(action)) {
295
- throw new Error(`Unknown msg command: ${first}. Try channels, read, send, thread, create, or dm.`);
296
- }
297
- return { action, rest: positionals.slice(first === undefined ? 0 : 1) };
298
- }
299
- function parseMsgCreateTarget(action, rest) {
300
- let channelName;
301
- let dmEmail;
302
- if (action === "create") {
303
- channelName = optionalNonEmpty(rest[0]);
304
- if (!channelName)
305
- throw new Error("msg create needs a channel name, e.g. `cockpit msg create general`.");
306
- if (rest.length > 1)
307
- throw new Error(`msg create takes one channel name, not ${rest.length}.`);
308
- // `#general` is how a person says it and how `read`/`send` accept it, so
309
- // the leading # is dropped here rather than becoming part of the name.
310
- if (channelName.startsWith("#"))
311
- channelName = channelName.slice(1);
312
- if (channelName === "")
313
- throw new Error("msg create needs a channel name, e.g. `cockpit msg create general`.");
314
- }
315
- else if (action === "dm") {
316
- dmEmail = optionalNonEmpty(rest[0]);
317
- if (!dmEmail)
318
- throw new Error("msg dm needs the person's email address, e.g. `cockpit msg dm ada@example.com`.");
319
- if (rest.length > 1)
320
- throw new Error(`msg dm takes one email address, not ${rest.length}.`);
321
- if (!dmEmail.includes("@"))
322
- throw new Error(`msg dm takes an email address; "${dmEmail}" is not one.`);
323
- }
324
- return { channelName, dmEmail };
325
- }
326
- function parseMsgCreateFlags(action, values) {
327
- const isPrivate = values.booleans.has("--private");
328
- if (isPrivate && action !== "create") {
329
- throw new Error("--private belongs to `cockpit msg create`.");
330
- }
331
- const memberEmails = parseMemberEmails(values.flags.get("--members"));
332
- if (memberEmails && action !== "create") {
333
- throw new Error("--members belongs to `cockpit msg create`.");
334
- }
335
- const description = optionalNonEmpty(values.flags.get("--description"));
336
- if (description && action !== "create") {
337
- throw new Error("--description belongs to `cockpit msg create`.");
338
- }
339
- return { isPrivate, memberEmails, description };
340
- }
341
- function parseMsgReadTarget(action, rest, values) {
342
- let channelRef;
343
- if (MSG_ACTIONS_NEEDING_A_CHANNEL.has(action)) {
344
- channelRef = optionalNonEmpty(rest[0]);
345
- if (!channelRef)
346
- throw new Error(`msg ${action} needs a channel id or name.`);
347
- if (rest.length > 1)
348
- throw new Error(`msg ${action} takes one channel, not ${rest.length}.`);
349
- }
350
- else if (action === "thread") {
351
- // `thread <id> --channel <channel>` — the channel is a flag here because
352
- // the positional names the THREAD, not the channel (`--channel` below).
353
- if (rest.length > 1)
354
- throw new Error(`msg thread takes one thread id, not ${rest.length}.`);
355
- }
356
- else if (action !== "create" && action !== "dm" && rest.length > 0) {
357
- throw new Error(`msg ${action} does not take "${rest[0]}".`);
358
- }
359
- const threadFlag = optionalNonEmpty(values.flags.get("--thread"));
360
- const threadId = action === "thread" ? optionalNonEmpty(rest[0]) : threadFlag;
361
- if (action === "thread" && !threadId)
362
- throw new Error("msg thread needs a thread id.");
363
- const channelFlag = optionalNonEmpty(values.flags.get("--channel"));
364
- if (action === "thread")
365
- channelRef = channelFlag;
366
- const limit = optionalPositiveInteger(values.flags.get("--limit"), "--limit");
367
- if (limit !== undefined && action !== "read" && action !== "thread") {
368
- throw new Error("--limit belongs to `cockpit msg read` or `cockpit msg thread`.");
369
- }
370
- return { channelRef, threadId, limit };
371
- }
372
- function buildMsgCommand(values, action, channelRef, channelName, dmEmail, memberEmails, description, isPrivate, threadId, limit) {
373
- return {
374
- kind: "msg",
375
- action,
376
- homeDir: optionalNonEmpty(values.flags.get("--home")),
377
- dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")),
378
- ...(channelRef ? { channelRef } : {}),
379
- ...(channelName ? { channelName } : {}),
380
- ...(dmEmail ? { dmEmail } : {}),
381
- ...(memberEmails ? { memberEmails } : {}),
382
- ...(description ? { description } : {}),
383
- isPrivate,
384
- ...(threadId ? { threadId } : {}),
385
- ...(limit === undefined ? {} : { limit }),
386
- json: values.booleans.has("--json"),
387
- };
388
- }
256
+ export { parseMsgArgs } from "./local-args-tower-msg.js";
@@ -33,26 +33,8 @@ export function parseGithubArgs(args) {
33
33
  throw new Error(`github ${action} takes a repo and a pull request number only.`);
34
34
  const pullNumber = numberText === undefined ? undefined : optionalPositiveInteger(numberText, "pull request number");
35
35
  const runId = optionalPositiveInteger(values.flags.get("--run"), "--run");
36
- if (action === "rerun") {
37
- if ((pullNumber === undefined) === (runId === undefined)) {
38
- throw new Error("github rerun needs exactly one of a pull request number or --run <id>.");
39
- }
40
- }
41
- else if (pullNumber === undefined) {
42
- throw new Error(`github ${action} needs a pull request number.`);
43
- }
44
- const only = (flag, allowed) => {
45
- if (values.flags.has(flag) && !allowed.includes(action)) {
46
- throw new Error(`${flag} belongs to \`cockpit github ${allowed.join("|")}\`.`);
47
- }
48
- };
49
- only("--claim", ["pr"]);
50
- only("--method", ["merge"]);
51
- only("--title", ["retitle"]);
52
- only("--run", ["rerun"]);
53
- if (values.flags.has("--confirm") && action === "pr") {
54
- throw new Error("--confirm belongs to merge, retitle and rerun; pr only reads.");
55
- }
36
+ requireTargetFor(action, pullNumber, runId);
37
+ refuseFlagsFromOtherActions(values, action);
56
38
  const title = optionalNonEmpty(values.flags.get("--title"));
57
39
  if (action === "retitle" && !title)
58
40
  throw new Error("github retitle needs --title \"<new title>\".");
@@ -78,4 +60,28 @@ export function parseGithubArgs(args) {
78
60
  dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")),
79
61
  json: values.booleans.has("--json"),
80
62
  };
63
+ }
64
+ function requireTargetFor(action, pullNumber, runId) {
65
+ if (action === "rerun") {
66
+ if ((pullNumber === undefined) === (runId === undefined)) {
67
+ throw new Error("github rerun needs exactly one of a pull request number or --run <id>.");
68
+ }
69
+ }
70
+ else if (pullNumber === undefined) {
71
+ throw new Error(`github ${action} needs a pull request number.`);
72
+ }
73
+ }
74
+ function refuseFlagsFromOtherActions(values, action) {
75
+ const only = (flag, allowed) => {
76
+ if (values.flags.has(flag) && !allowed.includes(action)) {
77
+ throw new Error(`${flag} belongs to \`cockpit github ${allowed.join("|")}\`.`);
78
+ }
79
+ };
80
+ only("--claim", ["pr"]);
81
+ only("--method", ["merge"]);
82
+ only("--title", ["retitle"]);
83
+ only("--run", ["rerun"]);
84
+ if (values.flags.has("--confirm") && action === "pr") {
85
+ throw new Error("--confirm belongs to merge, retitle and rerun; pr only reads.");
86
+ }
81
87
  }