@bli-cockpit/cli 0.2.237 → 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 (139) 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 +73 -30
  29. package/dist/commands/cal.js +36 -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/heartbeat-post.js +116 -0
  35. package/dist/commands/heartbeat-token-lock.js +46 -33
  36. package/dist/commands/heartbeat.js +43 -125
  37. package/dist/commands/install-update.js +17 -15
  38. package/dist/commands/issue-show.js +27 -28
  39. package/dist/commands/issue-write.js +70 -23
  40. package/dist/commands/issue.js +68 -37
  41. package/dist/commands/jarvis-live-answer.js +99 -0
  42. package/dist/commands/jarvis-render.js +1 -94
  43. package/dist/commands/jarvis-trace.js +30 -22
  44. package/dist/commands/jarvis-turn-report.js +124 -0
  45. package/dist/commands/jarvis-turn.js +108 -182
  46. package/dist/commands/jarvis.js +26 -17
  47. package/dist/commands/local-args-collector-setup.js +82 -84
  48. package/dist/commands/local-args-collector-work.js +37 -38
  49. package/dist/commands/local-args-tower-admin.js +155 -142
  50. package/dist/commands/local-args-tower-cal.js +52 -50
  51. package/dist/commands/local-args-tower-careers.js +73 -54
  52. package/dist/commands/local-args-tower-docs-msg.js +55 -45
  53. package/dist/commands/local-args-tower-github.js +26 -20
  54. package/dist/commands/local-args-tower-hits.js +32 -20
  55. package/dist/commands/local-args-tower-manager.js +85 -4
  56. package/dist/commands/local-args-tower-memory.js +139 -117
  57. package/dist/commands/local-args-tower-notes.js +346 -0
  58. package/dist/commands/local-args-tower-pages.js +100 -378
  59. package/dist/commands/local-args-tower-settings.js +28 -29
  60. package/dist/commands/local-args-tower-work.js +52 -33
  61. package/dist/commands/local-args.js +80 -129
  62. package/dist/commands/local-help-commands-tower-ops.js +15 -4
  63. package/dist/commands/local-help-commands-tower.js +6 -2
  64. package/dist/commands/local-help-commands.js +342 -341
  65. package/dist/commands/local-help.js +87 -83
  66. package/dist/commands/local-run-guards.js +100 -0
  67. package/dist/commands/local.js +137 -206
  68. package/dist/commands/login.js +13 -9
  69. package/dist/commands/logs.js +55 -38
  70. package/dist/commands/mail-attachments.js +44 -26
  71. package/dist/commands/manager-render-home.js +208 -0
  72. package/dist/commands/manager-render.js +7 -3
  73. package/dist/commands/manager-requests.js +33 -0
  74. package/dist/commands/manager.js +39 -5
  75. package/dist/commands/memory-consolidation.js +18 -9
  76. package/dist/commands/memory-daemon-probe.js +37 -28
  77. package/dist/commands/memory-install-config.js +64 -60
  78. package/dist/commands/memory-install.js +21 -41
  79. package/dist/commands/memory-requeue.js +26 -19
  80. package/dist/commands/memory-rules.js +26 -18
  81. package/dist/commands/memory-search-print.js +26 -0
  82. package/dist/commands/memory-store.js +14 -30
  83. package/dist/commands/memory-verb-list.js +49 -48
  84. package/dist/commands/notes-accounts.js +49 -32
  85. package/dist/commands/notes-sync-loop.js +115 -89
  86. package/dist/commands/notes-wisprflow-login.js +44 -23
  87. package/dist/commands/notes-writes.js +115 -100
  88. package/dist/commands/notes.js +33 -55
  89. package/dist/commands/onboard-flows.js +69 -51
  90. package/dist/commands/onboard.js +11 -9
  91. package/dist/commands/ops-render-memory.js +10 -7
  92. package/dist/commands/ops-render.js +33 -30
  93. package/dist/commands/ops.js +3 -0
  94. package/dist/commands/public-root.js +1 -1
  95. package/dist/commands/session-sync-outside-roots.js +40 -36
  96. package/dist/commands/session-sync-plan.js +14 -10
  97. package/dist/commands/session-sync-upload.js +24 -22
  98. package/dist/commands/session-sync.js +19 -8
  99. package/dist/commands/slack.js +25 -16
  100. package/dist/commands/sources-check.js +13 -7
  101. package/dist/commands/sources-discovery.js +25 -17
  102. package/dist/commands/sources-verdict.js +19 -18
  103. package/dist/commands/speed.js +23 -19
  104. package/dist/commands/start.js +20 -16
  105. package/dist/commands/sync-doctor.js +52 -48
  106. package/dist/commands/sync.js +22 -16
  107. package/dist/commands/usage-telemetry-toml.js +71 -45
  108. package/dist/commands/usage.js +91 -67
  109. package/dist/commands/workbook-render.js +10 -7
  110. package/dist/evidence-reconcile-client.js +75 -64
  111. package/dist/evidence-redelivery-plan.js +37 -34
  112. package/dist/evidence-redelivery.js +32 -29
  113. package/dist/evidence-transcripts-client.js +20 -20
  114. package/dist/evidence-upload-object.js +86 -89
  115. package/dist/evidence-upload-plan.js +94 -83
  116. package/dist/evidence-upload-terminal.js +45 -50
  117. package/dist/image-description-session.js +156 -159
  118. package/dist/local-state-pairing-code.js +39 -31
  119. package/dist/local-state-status.js +9 -2
  120. package/dist/onboarding-roots.js +5 -8
  121. package/dist/process-runner.js +8 -8
  122. package/dist/raw-evidence-gc.js +33 -38
  123. package/dist/raw-evidence-stuck-summary.js +7 -14
  124. package/dist/repo-identity.js +26 -23
  125. package/dist/scheduled-self-update.js +11 -15
  126. package/dist/second-install.js +3 -0
  127. package/dist/slim/codex-rules.js +23 -36
  128. package/dist/sync-lock.js +5 -15
  129. package/dist/tower-client.js +36 -53
  130. package/dist/tower-stream.js +7 -24
  131. package/dist/upload-agent-artifacts.js +58 -64
  132. package/dist/upload-envelope-build.js +60 -59
  133. package/dist/upload-evidence-delivery-reconcile.js +58 -56
  134. package/dist/upload-evidence-delivery-summary.js +32 -34
  135. package/dist/upload-ingest-receipt.js +20 -23
  136. package/dist/upload-session-reports-queue.js +4 -1
  137. package/dist/upload-session-reports-wire.js +64 -68
  138. package/dist/upload-sync.js +171 -157
  139. package/package.json +2 -2
@@ -13,46 +13,45 @@
13
13
  */
14
14
  import { DEFAULT_DASHBOARD_URL } from "../local-state.js";
15
15
  import { assertNoPositionals, normalizeUrl, optionalEmail, optionalNonEmpty, optionalNonEmptyList, optionalPositiveInteger, optionalUrl, parseNamedArgs, workRootFlagValue, workRootFlagValues, } from "./local-arg-values.js";
16
+ const ONBOARD_ALLOWED_FLAGS = [
17
+ "--home",
18
+ "--repo",
19
+ "--workspace",
20
+ "--dashboard-url",
21
+ "--email",
22
+ "--device-name",
23
+ "--ticket",
24
+ "--branch",
25
+ "--json",
26
+ "--no-auth",
27
+ "--poll-interval-ms",
28
+ "--timeout-ms",
29
+ "--max-depth",
30
+ "--max-repos",
31
+ "--allow-home-root",
32
+ // BLI-3731, the one-sign-in ceremony. Same three flags as `login`, so a
33
+ // person who learned them on one door does not relearn them on another.
34
+ "--pair",
35
+ "--no-browser",
36
+ "--legacy-pair",
37
+ ];
38
+ const ONBOARD_VALUE_FLAGS = [
39
+ "--home",
40
+ "--repo",
41
+ "--workspace",
42
+ "--dashboard-url",
43
+ "--email",
44
+ "--device-name",
45
+ "--ticket",
46
+ "--branch",
47
+ "--poll-interval-ms",
48
+ "--timeout-ms",
49
+ "--max-depth",
50
+ "--max-repos",
51
+ "--pair",
52
+ ];
16
53
  function parseOnboardLikeArgs(args, command) {
17
- const values = parseNamedArgs(args, {
18
- allowedFlags: [
19
- "--home",
20
- "--repo",
21
- "--workspace",
22
- "--dashboard-url",
23
- "--email",
24
- "--device-name",
25
- "--ticket",
26
- "--branch",
27
- "--json",
28
- "--no-auth",
29
- "--poll-interval-ms",
30
- "--timeout-ms",
31
- "--max-depth",
32
- "--max-repos",
33
- "--allow-home-root",
34
- // BLI-3731, the one-sign-in ceremony. Same three flags as `login`, so a
35
- // person who learned them on one door does not relearn them on another.
36
- "--pair",
37
- "--no-browser",
38
- "--legacy-pair",
39
- ],
40
- valueFlags: [
41
- "--home",
42
- "--repo",
43
- "--workspace",
44
- "--dashboard-url",
45
- "--email",
46
- "--device-name",
47
- "--ticket",
48
- "--branch",
49
- "--poll-interval-ms",
50
- "--timeout-ms",
51
- "--max-depth",
52
- "--max-repos",
53
- "--pair",
54
- ],
55
- });
54
+ const values = parseNamedArgs(args, { allowedFlags: ONBOARD_ALLOWED_FLAGS, valueFlags: ONBOARD_VALUE_FLAGS });
56
55
  assertNoPositionals(values.positionals, command);
57
56
  return {
58
57
  homeDir: optionalNonEmpty(values.flags.get("--home")),
@@ -86,55 +85,54 @@ export function parseUpdateArgs(alias, args) {
86
85
  ...parseOnboardLikeArgs(args, alias),
87
86
  };
88
87
  }
88
+ const DOCTOR_ALLOWED_FLAGS = [
89
+ "--home",
90
+ "--repo",
91
+ "--workspace",
92
+ "--dashboard-url",
93
+ "--update-tag",
94
+ "--dry-run",
95
+ "--check",
96
+ "--no-repair",
97
+ "--no-usage-telemetry",
98
+ "--lock-wait",
99
+ "--backfill-budget",
100
+ "--json",
101
+ "--allow-home-root",
102
+ "--max-depth",
103
+ "--max-repos",
104
+ // Accepted and ignored: the convergence run works these out itself.
105
+ // Rejecting them would break existing DMs and runbooks for no gain.
106
+ "--email",
107
+ "--device-name",
108
+ "--ticket",
109
+ "--branch",
110
+ "--no-auth",
111
+ "--poll-interval-ms",
112
+ "--timeout-ms",
113
+ ];
114
+ const DOCTOR_VALUE_FLAGS = [
115
+ "--lock-wait",
116
+ "--backfill-budget",
117
+ "--home",
118
+ "--repo",
119
+ "--workspace",
120
+ "--dashboard-url",
121
+ "--update-tag",
122
+ "--max-depth",
123
+ "--max-repos",
124
+ "--email",
125
+ "--device-name",
126
+ "--ticket",
127
+ "--branch",
128
+ "--poll-interval-ms",
129
+ "--timeout-ms",
130
+ ];
89
131
  export function parseDoctorArgs(alias, args) {
90
132
  // The allowed set is the UNION of what the six setup doors used to accept.
91
133
  // An alias that silently rejected a flag its own docs told people to pass
92
134
  // would be a worse dead end than the one we are removing.
93
- const values = parseNamedArgs(args, {
94
- allowedFlags: [
95
- "--home",
96
- "--repo",
97
- "--workspace",
98
- "--dashboard-url",
99
- "--update-tag",
100
- "--dry-run",
101
- "--check",
102
- "--no-repair",
103
- "--no-usage-telemetry",
104
- "--lock-wait",
105
- "--backfill-budget",
106
- "--json",
107
- "--allow-home-root",
108
- "--max-depth",
109
- "--max-repos",
110
- // Accepted and ignored: the convergence run works these out itself.
111
- // Rejecting them would break existing DMs and runbooks for no gain.
112
- "--email",
113
- "--device-name",
114
- "--ticket",
115
- "--branch",
116
- "--no-auth",
117
- "--poll-interval-ms",
118
- "--timeout-ms",
119
- ],
120
- valueFlags: [
121
- "--lock-wait",
122
- "--backfill-budget",
123
- "--home",
124
- "--repo",
125
- "--workspace",
126
- "--dashboard-url",
127
- "--update-tag",
128
- "--max-depth",
129
- "--max-repos",
130
- "--email",
131
- "--device-name",
132
- "--ticket",
133
- "--branch",
134
- "--poll-interval-ms",
135
- "--timeout-ms",
136
- ],
137
- });
135
+ const values = parseNamedArgs(args, { allowedFlags: DOCTOR_ALLOWED_FLAGS, valueFlags: DOCTOR_VALUE_FLAGS });
138
136
  assertNoPositionals(values.positionals, alias);
139
137
  const updateTag = optionalNonEmpty(values.flags.get("--update-tag"));
140
138
  if (updateTag && !/^[a-z0-9][a-z0-9._-]*$/u.test(updateTag)) {
@@ -9,45 +9,44 @@
9
9
  */
10
10
  import { IntentSourceSchema, WorkIntentSchema, WorkPhaseSchema, } from "@bli-cockpit/telemetry-core";
11
11
  import { assertNoPositionals, optionalConfidence, optionalNonEmpty, optionalNonNegativeInteger, optionalPositiveInteger, optionalSchemaValue, optionalUrl, parseNamedArgs, workRootFlagValue, } from "./local-arg-values.js";
12
+ const START_ALLOWED_FLAGS = [
13
+ "--home",
14
+ "--repo",
15
+ "--workspace",
16
+ "--branch",
17
+ "--ticket",
18
+ "--clear-ticket",
19
+ "--topic",
20
+ "--topic-summary",
21
+ "--intent",
22
+ "--phase",
23
+ "--intent-source",
24
+ "--intent-confidence",
25
+ "--operator-id",
26
+ "--session-id",
27
+ "--json",
28
+ "--max-depth",
29
+ "--max-repos",
30
+ ];
31
+ const START_VALUE_FLAGS = [
32
+ "--home",
33
+ "--repo",
34
+ "--workspace",
35
+ "--branch",
36
+ "--ticket",
37
+ "--topic",
38
+ "--topic-summary",
39
+ "--intent",
40
+ "--phase",
41
+ "--intent-source",
42
+ "--intent-confidence",
43
+ "--operator-id",
44
+ "--session-id",
45
+ "--max-depth",
46
+ "--max-repos",
47
+ ];
12
48
  export function parseStartArgs(args) {
13
- const values = parseNamedArgs(args, {
14
- allowedFlags: [
15
- "--home",
16
- "--repo",
17
- "--workspace",
18
- "--branch",
19
- "--ticket",
20
- "--clear-ticket",
21
- "--topic",
22
- "--topic-summary",
23
- "--intent",
24
- "--phase",
25
- "--intent-source",
26
- "--intent-confidence",
27
- "--operator-id",
28
- "--session-id",
29
- "--json",
30
- "--max-depth",
31
- "--max-repos",
32
- ],
33
- valueFlags: [
34
- "--home",
35
- "--repo",
36
- "--workspace",
37
- "--branch",
38
- "--ticket",
39
- "--topic",
40
- "--topic-summary",
41
- "--intent",
42
- "--phase",
43
- "--intent-source",
44
- "--intent-confidence",
45
- "--operator-id",
46
- "--session-id",
47
- "--max-depth",
48
- "--max-repos",
49
- ],
50
- });
49
+ const values = parseNamedArgs(args, { allowedFlags: START_ALLOWED_FLAGS, valueFlags: START_VALUE_FLAGS });
51
50
  assertNoPositionals(values.positionals, "start");
52
51
  const activeTicketId = optionalNonEmpty(values.flags.get("--ticket"));
53
52
  const clearTicket = values.booleans.has("--clear-ticket");
@@ -65,6 +65,27 @@ export function parseScoutArgs(args) {
65
65
  }
66
66
  return { kind: "scout", action: rawAction, experimentRef, ...base };
67
67
  }
68
+ const OPS_ALLOWED_FLAGS = [
69
+ "--home",
70
+ "--dashboard-url",
71
+ "--job",
72
+ "--skips",
73
+ "--memory",
74
+ "--memory-days",
75
+ // BLI-3798: the coverage half on its own, with the bucket table.
76
+ "--coverage",
77
+ // BLI-3912: one line per model — latency, tokens/s, today's spend, last
78
+ // probe verdict. A SECOND door like --memory, asked for only when named.
79
+ "--models",
80
+ "--turns",
81
+ "--tool-router",
82
+ // BLI-12507: the MCP's `people: true`. Per-device and per-person rows in --json.
83
+ "--people",
84
+ "--person",
85
+ "--dry-run",
86
+ "--json",
87
+ ];
88
+ const OPS_VALUE_FLAGS = ["--home", "--dashboard-url", "--job", "--person", "--memory-days"];
68
89
  /**
69
90
  * `cockpit ops status [--job <id>] [--skips] [--memory [--memory-days N]]` and
70
91
  * `cockpit ops recompile --person <p> [--dry-run]` (BLI-3462).
@@ -76,29 +97,7 @@ export function parseScoutArgs(args) {
76
97
  * weekly or a monthly with the compile script, which does support it.
77
98
  */
78
99
  export function parseOpsArgs(args) {
79
- const values = parseNamedArgs(args, {
80
- allowedFlags: [
81
- "--home",
82
- "--dashboard-url",
83
- "--job",
84
- "--skips",
85
- "--memory",
86
- "--memory-days",
87
- // BLI-3798: the coverage half on its own, with the bucket table.
88
- "--coverage",
89
- // BLI-3912: one line per model — latency, tokens/s, today's spend, last
90
- // probe verdict. A SECOND door like --memory, asked for only when named.
91
- "--models",
92
- "--turns",
93
- "--tool-router",
94
- // BLI-12507: the MCP's `people: true`. Per-device and per-person rows in --json.
95
- "--people",
96
- "--person",
97
- "--dry-run",
98
- "--json",
99
- ],
100
- valueFlags: ["--home", "--dashboard-url", "--job", "--person", "--memory-days"],
101
- });
100
+ const values = parseNamedArgs(args, { allowedFlags: OPS_ALLOWED_FLAGS, valueFlags: OPS_VALUE_FLAGS });
102
101
  if (values.positionals.length > 1) {
103
102
  throw new Error("ops accepts one action: status or recompile.");
104
103
  }
@@ -111,36 +110,39 @@ export function parseOpsArgs(args) {
111
110
  dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")),
112
111
  json: values.booleans.has("--json"),
113
112
  };
114
- if (rawAction === "status") {
115
- // BLI-3729: `--memory-days N` implies `--memory`, because asking for a
116
- // window and getting nothing back is the silent breakage BLI-2490 forbids.
117
- const memoryDays = optionalNonEmpty(values.flags.get("--memory-days"));
118
- if (memoryDays !== undefined && !/^[0-9]{1,2}$/u.test(memoryDays)) {
119
- throw new Error("ops --memory-days takes a whole number of days, 1 to 30.");
120
- }
121
- const coverageOnly = values.booleans.has("--coverage");
122
- const job = optionalNonEmpty(values.flags.get("--job"));
123
- if (coverageOnly && job !== undefined && job !== "collection-coverage") {
124
- // Refused rather than silently preferring one: `--coverage --job
125
- // daily-brief` is somebody asking two different questions in one
126
- // command, and answering the wrong one is how a board loses trust.
127
- throw new Error("ops --coverage IS --job collection-coverage. Pass one or the other, not a --job naming a different pipeline.");
128
- }
129
- return {
130
- kind: "ops",
131
- action: "status",
132
- job: coverageOnly ? "collection-coverage" : job,
133
- coverage: coverageOnly,
134
- skips: values.booleans.has("--skips"),
135
- memory: values.booleans.has("--memory") || memoryDays !== undefined,
136
- models: values.booleans.has("--models"),
137
- turns: values.booleans.has("--turns"),
138
- toolRouter: values.booleans.has("--tool-router"),
139
- people: values.booleans.has("--people"),
140
- ...(memoryDays === undefined ? {} : { memoryDays: Number(memoryDays) }),
141
- ...base,
142
- };
113
+ return rawAction === "status" ? parseOpsStatus(values, base) : parseOpsRecompile(values, base);
114
+ }
115
+ function parseOpsStatus(values, base) {
116
+ // BLI-3729: `--memory-days N` implies `--memory`, because asking for a
117
+ // window and getting nothing back is the silent breakage BLI-2490 forbids.
118
+ const memoryDays = optionalNonEmpty(values.flags.get("--memory-days"));
119
+ if (memoryDays !== undefined && !/^[0-9]{1,2}$/u.test(memoryDays)) {
120
+ throw new Error("ops --memory-days takes a whole number of days, 1 to 30.");
121
+ }
122
+ const coverageOnly = values.booleans.has("--coverage");
123
+ const job = optionalNonEmpty(values.flags.get("--job"));
124
+ if (coverageOnly && job !== undefined && job !== "collection-coverage") {
125
+ // Refused rather than silently preferring one: `--coverage --job
126
+ // daily-brief` is somebody asking two different questions in one
127
+ // command, and answering the wrong one is how a board loses trust.
128
+ throw new Error("ops --coverage IS --job collection-coverage. Pass one or the other, not a --job naming a different pipeline.");
143
129
  }
130
+ return {
131
+ kind: "ops",
132
+ action: "status",
133
+ job: coverageOnly ? "collection-coverage" : job,
134
+ coverage: coverageOnly,
135
+ skips: values.booleans.has("--skips"),
136
+ memory: values.booleans.has("--memory") || memoryDays !== undefined,
137
+ models: values.booleans.has("--models"),
138
+ turns: values.booleans.has("--turns"),
139
+ toolRouter: values.booleans.has("--tool-router"),
140
+ people: values.booleans.has("--people"),
141
+ ...(memoryDays === undefined ? {} : { memoryDays: Number(memoryDays) }),
142
+ ...base,
143
+ };
144
+ }
145
+ function parseOpsRecompile(values, base) {
144
146
  const person = optionalNonEmpty(values.flags.get("--person"));
145
147
  if (!person) {
146
148
  throw new Error("ops recompile needs --person <email|name|id>. Nothing is recompiled by default, on purpose.");
@@ -161,37 +163,36 @@ export const SLACK_WORKSPACE_KEYS = ["bli", "blue_pearl"];
161
163
  * (BLI-3462).
162
164
  */
163
165
  const SLACK_READ_FORMATS = ["compact", "full"];
166
+ const SLACK_ALLOWED_FLAGS = [
167
+ "--home",
168
+ "--dashboard-url",
169
+ "--workspace",
170
+ "--stale-only",
171
+ "--list",
172
+ "--person",
173
+ "--channel",
174
+ "--query",
175
+ "--word",
176
+ "--since",
177
+ "--until",
178
+ "--limit",
179
+ "--format",
180
+ "--json",
181
+ ];
182
+ const SLACK_VALUE_FLAGS = [
183
+ "--home",
184
+ "--dashboard-url",
185
+ "--workspace",
186
+ "--person",
187
+ "--channel",
188
+ "--query",
189
+ "--since",
190
+ "--until",
191
+ "--limit",
192
+ "--format",
193
+ ];
164
194
  export function parseSlackArgs(args) {
165
- const values = parseNamedArgs(args, {
166
- allowedFlags: [
167
- "--home",
168
- "--dashboard-url",
169
- "--workspace",
170
- "--stale-only",
171
- "--list",
172
- "--person",
173
- "--channel",
174
- "--query",
175
- "--word",
176
- "--since",
177
- "--until",
178
- "--limit",
179
- "--format",
180
- "--json",
181
- ],
182
- valueFlags: [
183
- "--home",
184
- "--dashboard-url",
185
- "--workspace",
186
- "--person",
187
- "--channel",
188
- "--query",
189
- "--since",
190
- "--until",
191
- "--limit",
192
- "--format",
193
- ],
194
- });
195
+ const values = parseNamedArgs(args, { allowedFlags: SLACK_ALLOWED_FLAGS, valueFlags: SLACK_VALUE_FLAGS });
195
196
  if (values.positionals.length > 1) {
196
197
  throw new Error("slack accepts one action: coverage, read, connect or disconnect.");
197
198
  }
@@ -210,32 +211,39 @@ export function parseSlackArgs(args) {
210
211
  if (values.booleans.has("--list") && rawAction !== "coverage") {
211
212
  throw new Error("--list belongs to `cockpit slack coverage`.");
212
213
  }
213
- if (rawAction === "connect" || rawAction === "disconnect") {
214
- const workspace = optionalNonEmpty(values.flags.get("--workspace"));
215
- if (workspace && !SLACK_WORKSPACE_KEYS.includes(workspace))
216
- throw new Error("Unknown Slack workspace.");
217
- if (rawAction === "disconnect" && !workspace)
218
- throw new Error("slack disconnect needs --workspace bli|blue_pearl.");
219
- return { kind: "slack", action: rawAction, workspace, ...base };
220
- }
221
- if (rawAction === "coverage") {
222
- // `--workspace` here is a Slack workspace key, NOT the collection-root
223
- // `--workspace <path>` every setup command takes. Named at the point of use
224
- // so the two cannot be confused silently.
225
- const workspace = optionalNonEmpty(values.flags.get("--workspace"));
226
- if (workspace && !SLACK_WORKSPACE_KEYS.includes(workspace)) {
227
- throw new Error(`slack --workspace must be one of ${SLACK_WORKSPACE_KEYS.join(", ")}; got ${workspace}.`);
228
- }
229
- return {
230
- kind: "slack",
231
- action: "coverage",
232
- workspace,
233
- staleOnly: values.booleans.has("--stale-only"),
234
- // BLI-10450: counts alone could not answer "is there a Kaiben channel?".
235
- list: values.booleans.has("--list"),
236
- ...base,
237
- };
214
+ if (rawAction === "connect" || rawAction === "disconnect")
215
+ return parseSlackConnection(values, base, rawAction);
216
+ if (rawAction === "coverage")
217
+ return parseSlackCoverage(values, base);
218
+ return parseSlackRead(values, base);
219
+ }
220
+ function parseSlackConnection(values, base, action) {
221
+ const workspace = optionalNonEmpty(values.flags.get("--workspace"));
222
+ if (workspace && !SLACK_WORKSPACE_KEYS.includes(workspace))
223
+ throw new Error("Unknown Slack workspace.");
224
+ if (action === "disconnect" && !workspace)
225
+ throw new Error("slack disconnect needs --workspace bli|blue_pearl.");
226
+ return { kind: "slack", action, workspace, ...base };
227
+ }
228
+ function parseSlackCoverage(values, base) {
229
+ // `--workspace` here is a Slack workspace key, NOT the collection-root
230
+ // `--workspace <path>` every setup command takes. Named at the point of use
231
+ // so the two cannot be confused silently.
232
+ const workspace = optionalNonEmpty(values.flags.get("--workspace"));
233
+ if (workspace && !SLACK_WORKSPACE_KEYS.includes(workspace)) {
234
+ throw new Error(`slack --workspace must be one of ${SLACK_WORKSPACE_KEYS.join(", ")}; got ${workspace}.`);
238
235
  }
236
+ return {
237
+ kind: "slack",
238
+ action: "coverage",
239
+ workspace,
240
+ staleOnly: values.booleans.has("--stale-only"),
241
+ // BLI-10450: counts alone could not answer "is there a Kaiben channel?".
242
+ list: values.booleans.has("--list"),
243
+ ...base,
244
+ };
245
+ }
246
+ function parseSlackRead(values, base) {
239
247
  const person = optionalNonEmpty(values.flags.get("--person"));
240
248
  const channel = optionalNonEmpty(values.flags.get("--channel"));
241
249
  const query = optionalNonEmpty(values.flags.get("--query"));
@@ -271,20 +279,19 @@ export function parseSlackArgs(args) {
271
279
  * `cockpit team [members|invite <email>|role <userId>|device list|device
272
280
  * revoke <id|name>]` (BLI-3461; `device` added BLI-3559).
273
281
  */
282
+ const TEAM_ALLOWED_FLAGS = [
283
+ "--home",
284
+ "--dashboard-url",
285
+ "--json",
286
+ "--role",
287
+ "--team-id",
288
+ "--yes",
289
+ "--reason",
290
+ "--note",
291
+ ];
292
+ const TEAM_VALUE_FLAGS = ["--home", "--dashboard-url", "--role", "--team-id", "--reason", "--note"];
274
293
  export function parseTeamArgs(args) {
275
- const values = parseNamedArgs(args, {
276
- allowedFlags: [
277
- "--home",
278
- "--dashboard-url",
279
- "--json",
280
- "--role",
281
- "--team-id",
282
- "--yes",
283
- "--reason",
284
- "--note",
285
- ],
286
- valueFlags: ["--home", "--dashboard-url", "--role", "--team-id", "--reason", "--note"],
287
- });
294
+ const values = parseNamedArgs(args, { allowedFlags: TEAM_ALLOWED_FLAGS, valueFlags: TEAM_VALUE_FLAGS });
288
295
  const common = {
289
296
  homeDir: optionalNonEmpty(values.flags.get("--home")),
290
297
  dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")),
@@ -293,31 +300,15 @@ export function parseTeamArgs(args) {
293
300
  json: values.booleans.has("--json"),
294
301
  };
295
302
  const [verb, ...rest] = values.positionals;
296
- if (verb === "github-login") {
297
- const [operation, personRef, githubLogin, ...extra] = rest;
298
- if (!["add", "remove", "list"].includes(operation ?? "") || !personRef || extra.length ||
299
- (operation === "list" ? githubLogin !== undefined : !githubLogin)) {
300
- throw new Error("team github-login takes add|remove <person> <login>, or list <person>.");
301
- }
302
- return { kind: "team", action: `github-login-${operation}`, personRef, githubLogin, ...common };
303
- }
304
- if (verb === "timezone") {
305
- const [operation, personRef, zone, ...extra] = rest;
306
- if (!["set", "list"].includes(operation ?? "") || extra.length ||
307
- (operation === "list" ? personRef !== undefined : !personRef || !zone)) {
308
- throw new Error("team timezone takes set <person> <iana-zone> (or the word unknown), or list.");
309
- }
310
- return { kind: "team", action: operation === "set" ? "timezone-set" : "timezone-list", ...(personRef ? { personRef } : {}), ...(zone ? { zone } : {}), ...common };
311
- }
303
+ if (verb === "github-login")
304
+ return parseTeamGithubLoginArgs(rest, common);
305
+ if (verb === "timezone")
306
+ return parseTeamTimezoneArgs(rest, common);
312
307
  if (verb === "device") {
313
308
  return parseTeamDeviceArgs(rest, values.flags, common);
314
309
  }
315
- if (verb === "grant" || verb === "revoke") {
316
- const [person, section, level = "see", ...extra] = rest;
317
- if (!person || section !== "careers" || !["see", "act"].includes(level) || extra.length)
318
- throw new Error("team grant|revoke <person uuid> careers [see|act]");
319
- return { kind: "team", action: verb, targetUserId: person, section: "careers", level: level, ...common };
320
- }
310
+ if (verb === "grant" || verb === "revoke")
311
+ return parseTeamGrantArgs(verb, rest, common);
321
312
  const [subject, ...extra] = rest;
322
313
  if (extra.length > 0)
323
314
  throw new Error("team does not accept that many arguments.");
@@ -345,6 +336,28 @@ export function parseTeamArgs(args) {
345
336
  }
346
337
  throw new Error("team takes one verb: members, invite, role, grant, revoke, or device.");
347
338
  }
339
+ function parseTeamGithubLoginArgs(rest, common) {
340
+ const [operation, personRef, githubLogin, ...extra] = rest;
341
+ if (!["add", "remove", "list"].includes(operation ?? "") || !personRef || extra.length ||
342
+ (operation === "list" ? githubLogin !== undefined : !githubLogin)) {
343
+ throw new Error("team github-login takes add|remove <person> <login>, or list <person>.");
344
+ }
345
+ return { kind: "team", action: `github-login-${operation}`, personRef, githubLogin, ...common };
346
+ }
347
+ function parseTeamTimezoneArgs(rest, common) {
348
+ const [operation, personRef, zone, ...extra] = rest;
349
+ if (!["set", "list"].includes(operation ?? "") || extra.length ||
350
+ (operation === "list" ? personRef !== undefined : !personRef || !zone)) {
351
+ throw new Error("team timezone takes set <person> <iana-zone> (or the word unknown), or list.");
352
+ }
353
+ return { kind: "team", action: operation === "set" ? "timezone-set" : "timezone-list", ...(personRef ? { personRef } : {}), ...(zone ? { zone } : {}), ...common };
354
+ }
355
+ function parseTeamGrantArgs(verb, rest, common) {
356
+ const [person, section, level = "see", ...extra] = rest;
357
+ if (!person || section !== "careers" || !["see", "act"].includes(level) || extra.length)
358
+ throw new Error("team grant|revoke <person uuid> careers [see|act]");
359
+ return { kind: "team", action: verb, targetUserId: person, section: "careers", level: level, ...common };
360
+ }
348
361
  /**
349
362
  * `cockpit team device [list] [--json]` or
350
363
  * `cockpit team device revoke <id|name> --reason <label> [--note "<text>"]`