@bli-cockpit/cli 0.2.237 → 0.2.239
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/attribution-core-fallbacks.js +15 -11
- package/dist/adapters/attribution-core-score.js +111 -87
- package/dist/adapters/car-state.js +38 -35
- package/dist/adapters/claude-attribution-discovery.js +81 -56
- package/dist/adapters/claude-attribution-score.js +108 -94
- package/dist/adapters/claude-attribution-signals.js +52 -42
- package/dist/adapters/codex-attribution.js +42 -33
- package/dist/adapters/codex-session-signals.js +11 -9
- package/dist/adapters/git-state.js +56 -50
- package/dist/adapters/local-sources.js +52 -43
- package/dist/adapters/raw-evidence-git-diff.js +4 -2
- package/dist/adapters/raw-evidence-sanitize.js +15 -12
- package/dist/adapters/raw-evidence-transcript-file.js +32 -31
- package/dist/autostart-self-heal.js +18 -2
- package/dist/commands/agent-session-report.js +51 -59
- package/dist/commands/agent-setup-observed.js +30 -23
- package/dist/commands/agent-setup.js +96 -45
- package/dist/commands/autostart-heal.js +8 -2
- package/dist/commands/autostart-reading.js +3 -0
- package/dist/commands/backfill-evidence-outcomes.js +9 -6
- package/dist/commands/backfill-issues.js +3 -0
- package/dist/commands/backfill-scan.js +27 -22
- package/dist/commands/backfill-upload.js +69 -60
- package/dist/commands/backfill.js +26 -21
- package/dist/commands/brief-done.js +4 -1
- package/dist/commands/brief-edit.js +71 -46
- package/dist/commands/cal-detail.js +35 -0
- package/dist/commands/cal-direct.js +119 -0
- package/dist/commands/cal-overlay.js +73 -30
- package/dist/commands/cal.js +62 -25
- package/dist/commands/clean.js +4 -1
- package/dist/commands/docs-read.js +5 -2
- package/dist/commands/docs.js +38 -6
- package/dist/commands/doctor-backfill.js +88 -71
- package/dist/commands/doctor-pipeline.js +59 -46
- package/dist/commands/heartbeat-post.js +116 -0
- package/dist/commands/heartbeat-token-lock.js +46 -33
- package/dist/commands/heartbeat.js +43 -125
- package/dist/commands/install-update.js +17 -15
- package/dist/commands/issue-show.js +27 -28
- package/dist/commands/issue-versions.js +93 -0
- package/dist/commands/issue-write.js +70 -23
- package/dist/commands/issue.js +73 -37
- package/dist/commands/jarvis-live-answer.js +99 -0
- package/dist/commands/jarvis-render.js +1 -94
- package/dist/commands/jarvis-trace.js +30 -22
- package/dist/commands/jarvis-turn-report.js +124 -0
- package/dist/commands/jarvis-turn.js +108 -182
- package/dist/commands/jarvis.js +26 -17
- package/dist/commands/local-args-collector-setup.js +82 -84
- package/dist/commands/local-args-collector-work.js +37 -38
- package/dist/commands/local-args-tower-admin.js +155 -142
- package/dist/commands/local-args-tower-cal.js +69 -54
- package/dist/commands/local-args-tower-careers.js +73 -54
- package/dist/commands/local-args-tower-docs-msg.js +79 -51
- package/dist/commands/local-args-tower-github.js +26 -20
- package/dist/commands/local-args-tower-hits.js +32 -20
- package/dist/commands/local-args-tower-manager.js +85 -4
- package/dist/commands/local-args-tower-memory.js +139 -117
- package/dist/commands/local-args-tower-msg.js +24 -6
- package/dist/commands/local-args-tower-notes.js +346 -0
- package/dist/commands/local-args-tower-pages.js +100 -378
- package/dist/commands/local-args-tower-settings.js +28 -29
- package/dist/commands/local-args-tower-setup.js +11 -4
- package/dist/commands/local-args-tower-work.js +74 -37
- package/dist/commands/local-args.js +80 -129
- package/dist/commands/local-help-commands-tower-ops.js +15 -4
- package/dist/commands/local-help-commands-tower.js +20 -7
- package/dist/commands/local-help-commands.js +342 -341
- package/dist/commands/local-help.js +87 -83
- package/dist/commands/local-run-guards.js +100 -0
- package/dist/commands/local.js +137 -206
- package/dist/commands/login.js +13 -9
- package/dist/commands/logs.js +55 -38
- package/dist/commands/mail-attachments.js +44 -26
- package/dist/commands/manager-render-home.js +208 -0
- package/dist/commands/manager-render.js +7 -3
- package/dist/commands/manager-requests.js +33 -0
- package/dist/commands/manager.js +39 -5
- package/dist/commands/memory-consolidation.js +18 -9
- package/dist/commands/memory-daemon-probe.js +37 -28
- package/dist/commands/memory-install-config.js +64 -60
- package/dist/commands/memory-install.js +21 -41
- package/dist/commands/memory-requeue.js +26 -19
- package/dist/commands/memory-rules.js +26 -18
- package/dist/commands/memory-search-print.js +26 -0
- package/dist/commands/memory-store.js +14 -30
- package/dist/commands/memory-verb-list.js +49 -48
- package/dist/commands/msg-read-line.js +32 -0
- package/dist/commands/msg.js +45 -3
- package/dist/commands/notes-accounts.js +49 -32
- package/dist/commands/notes-sync-loop.js +115 -89
- package/dist/commands/notes-wisprflow-login.js +44 -23
- package/dist/commands/notes-writes.js +115 -100
- package/dist/commands/notes.js +33 -55
- package/dist/commands/onboard-flows.js +69 -51
- package/dist/commands/onboard.js +11 -9
- package/dist/commands/ops-render-memory.js +10 -7
- package/dist/commands/ops-render.js +33 -30
- package/dist/commands/ops.js +3 -0
- package/dist/commands/public-root.js +1 -1
- package/dist/commands/session-sync-outside-roots.js +40 -36
- package/dist/commands/session-sync-plan.js +14 -10
- package/dist/commands/session-sync-upload.js +24 -22
- package/dist/commands/session-sync.js +19 -8
- package/dist/commands/setup.js +13 -0
- package/dist/commands/slack.js +25 -16
- package/dist/commands/sources-check.js +13 -7
- package/dist/commands/sources-discovery.js +25 -17
- package/dist/commands/sources-verdict.js +19 -18
- package/dist/commands/speed.js +23 -19
- package/dist/commands/start.js +20 -16
- package/dist/commands/sync-doctor.js +52 -48
- package/dist/commands/sync.js +22 -16
- package/dist/commands/usage-telemetry-toml.js +71 -45
- package/dist/commands/usage.js +94 -68
- package/dist/commands/workbook-render.js +10 -7
- package/dist/evidence-reconcile-client.js +75 -64
- package/dist/evidence-redelivery-plan.js +37 -34
- package/dist/evidence-redelivery.js +32 -29
- package/dist/evidence-transcripts-client.js +20 -20
- package/dist/evidence-upload-object.js +86 -89
- package/dist/evidence-upload-plan.js +94 -83
- package/dist/evidence-upload-terminal.js +45 -50
- package/dist/image-description-session.js +156 -159
- package/dist/local-state-pairing-code.js +39 -31
- package/dist/local-state-status.js +9 -2
- package/dist/onboarding-roots.js +5 -8
- package/dist/process-runner.js +8 -8
- package/dist/raw-evidence-gc.js +33 -38
- package/dist/raw-evidence-stuck-summary.js +7 -14
- package/dist/repo-identity.js +26 -23
- package/dist/scheduled-self-update.js +11 -15
- package/dist/second-install.js +3 -0
- package/dist/slim/codex-rules.js +23 -36
- package/dist/spool/local-spool-parse.js +13 -12
- package/dist/sync-lock.js +5 -15
- package/dist/tower-client.js +36 -53
- package/dist/tower-stream.js +7 -24
- package/dist/upload-agent-artifacts.js +58 -64
- package/dist/upload-envelope-build.js +60 -59
- package/dist/upload-evidence-delivery-reconcile.js +58 -56
- package/dist/upload-evidence-delivery-summary.js +32 -34
- package/dist/upload-ingest-receipt.js +20 -23
- package/dist/upload-session-reports-queue.js +4 -1
- package/dist/upload-session-reports-wire.js +64 -68
- package/dist/upload-sync.js +171 -157
- package/package.json +4 -4
|
@@ -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
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
60
|
-
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
|
@@ -8,63 +8,70 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { optionalNonEmpty, optionalPositiveInteger, optionalUrl, parseNamedArgs, } from "./local-arg-values.js";
|
|
10
10
|
import { parseDocsExportFlags } from "./local-args-docs-export.js";
|
|
11
|
-
const DOCS_ACTIONS = new Set(["list", "tree", "read", "create", "update", "delete", "link", "sync-status", "export"]);
|
|
11
|
+
const DOCS_ACTIONS = new Set(["list", "tree", "read", "create", "update", "delete", "link", "sync-status", "export", "mutation-status"]);
|
|
12
|
+
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
12
13
|
// BLI-10450: `read` moved to its own branch in `parseDocsDocumentReference` —
|
|
13
14
|
// it takes SEVERAL references now, so it no longer shares the "exactly one"
|
|
14
15
|
// rule `update`/`link` still enforce. BLI‑10555: `delete` takes several too
|
|
15
16
|
// (the same door, called once per ref), so it joined `read`'s branch rather
|
|
16
17
|
// than this set.
|
|
17
|
-
const DOCS_ACTIONS_NEEDING_A_DOC = new Set(["update", "link", "export"]);
|
|
18
|
+
const DOCS_ACTIONS_NEEDING_A_DOC = new Set(["update", "link", "export", "mutation-status"]);
|
|
19
|
+
const DOCS_ALLOWED_FLAGS = [
|
|
20
|
+
"--home",
|
|
21
|
+
"--dashboard-url",
|
|
22
|
+
"--title",
|
|
23
|
+
"--parent",
|
|
24
|
+
"--clear-parent",
|
|
25
|
+
"--allow-empty",
|
|
26
|
+
"--expected-revision",
|
|
27
|
+
"--request-id",
|
|
28
|
+
"--visibility",
|
|
29
|
+
"--file",
|
|
30
|
+
"--body-stdin",
|
|
31
|
+
"--query",
|
|
32
|
+
"--word",
|
|
33
|
+
"--limit",
|
|
34
|
+
"--clear",
|
|
35
|
+
"--bodies",
|
|
36
|
+
"--section",
|
|
37
|
+
"--offset",
|
|
38
|
+
"--format",
|
|
39
|
+
"--out",
|
|
40
|
+
"--json",
|
|
41
|
+
];
|
|
42
|
+
const DOCS_VALUE_FLAGS = [
|
|
43
|
+
"--home",
|
|
44
|
+
"--dashboard-url",
|
|
45
|
+
"--title",
|
|
46
|
+
"--parent",
|
|
47
|
+
"--expected-revision",
|
|
48
|
+
"--request-id",
|
|
49
|
+
"--visibility",
|
|
50
|
+
"--file",
|
|
51
|
+
"--query",
|
|
52
|
+
"--limit",
|
|
53
|
+
"--section",
|
|
54
|
+
"--offset",
|
|
55
|
+
"--format",
|
|
56
|
+
"--out",
|
|
57
|
+
];
|
|
18
58
|
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
|
-
});
|
|
59
|
+
const values = parseNamedArgs(args, { allowedFlags: DOCS_ALLOWED_FLAGS, valueFlags: DOCS_VALUE_FLAGS });
|
|
56
60
|
const { action, rest } = parseDocsAction(values.positionals);
|
|
57
61
|
const link = parseDocsLink(action, rest, values.booleans.has("--clear"));
|
|
58
62
|
const { docRef, docRefs } = parseDocsDocumentReference(action, link.rest);
|
|
59
|
-
const { visibility, clearParent, parentId, allowEmpty, title, query, wholeWord, limit, bodies, section, offset } = parseDocsFlags(action, values);
|
|
63
|
+
const { visibility, clearParent, parentId, allowEmpty, title, requestId, expectedRevision, query, wholeWord, limit, bodies, section, offset } = parseDocsFlags(action, values);
|
|
60
64
|
if (action === "read")
|
|
61
65
|
validateDocsReadTargets(docRefs, parentId, bodies);
|
|
66
|
+
if (action === "mutation-status" && !UUID_PATTERN.test(docRef ?? "")) {
|
|
67
|
+
throw new Error("docs mutation-status needs a request UUID (the --request-id you sent, or the one `docs update` printed).");
|
|
68
|
+
}
|
|
62
69
|
// BLI‑10555: `delete` needs no `--parent`/`--bodies` shape, only at least
|
|
63
70
|
// one reference — `read`'s emptier validation does not apply.
|
|
64
71
|
if (action === "delete" && (!docRefs || docRefs.length === 0)) {
|
|
65
72
|
throw new Error("docs delete needs one or more document ids/slugs.");
|
|
66
73
|
}
|
|
67
|
-
return buildDocsCommand(values, action, docRef, docRefs, visibility, clearParent, parentId, allowEmpty, title, query, wholeWord, limit, bodies, section, offset, link);
|
|
74
|
+
return buildDocsCommand(values, action, docRef, docRefs, visibility, clearParent, parentId, allowEmpty, title, query, wholeWord, limit, bodies, section, offset, link, { requestId, expectedRevision });
|
|
68
75
|
}
|
|
69
76
|
/**
|
|
70
77
|
* BLI-10450: `read` takes either several document references OR
|
|
@@ -116,7 +123,7 @@ function parseDocsAction(positionals) {
|
|
|
116
123
|
const first = positionals[0];
|
|
117
124
|
const action = (first === undefined ? "list" : first);
|
|
118
125
|
if (!DOCS_ACTIONS.has(action)) {
|
|
119
|
-
throw new Error(`Unknown docs command: ${first}. Try list, tree, read, create, update, delete, link, sync-status, or
|
|
126
|
+
throw new Error(`Unknown docs command: ${first}. Try list, tree, read, create, update, delete, link, sync-status, export, or mutation-status.`);
|
|
120
127
|
}
|
|
121
128
|
return { action, rest: positionals.slice(first === undefined ? 0 : 1) };
|
|
122
129
|
}
|
|
@@ -150,6 +157,10 @@ function parseDocsDocumentReference(action, rest) {
|
|
|
150
157
|
return { docRef: undefined, docRefs: undefined };
|
|
151
158
|
}
|
|
152
159
|
function parseDocsFlags(action, values) {
|
|
160
|
+
return { ...parseDocsWriteFlags(action, values), ...parseDocsReadFlags(action, values) };
|
|
161
|
+
}
|
|
162
|
+
/** The flags that decide where a document lives and what it is called. */
|
|
163
|
+
function parseDocsWriteFlags(action, values) {
|
|
153
164
|
const visibility = optionalNonEmpty(values.flags.get("--visibility"));
|
|
154
165
|
if (visibility && visibility !== "org" && visibility !== "private") {
|
|
155
166
|
throw new Error('docs --visibility must be "org" or "private".');
|
|
@@ -170,10 +181,22 @@ function parseDocsFlags(action, values) {
|
|
|
170
181
|
if (allowEmpty && action !== "update") {
|
|
171
182
|
throw new Error("--allow-empty belongs to `cockpit docs update`.");
|
|
172
183
|
}
|
|
184
|
+
// BLI-21298: the write-safety pair belongs to `update`, like the other save-shaping flags.
|
|
185
|
+
if ((values.flags.has("--request-id") || values.flags.has("--expected-revision")) && action !== "update") {
|
|
186
|
+
throw new Error("--request-id and --expected-revision belong to `cockpit docs update`.");
|
|
187
|
+
}
|
|
188
|
+
const requestId = optionalNonEmpty(values.flags.get("--request-id"));
|
|
189
|
+
if (requestId && !UUID_PATTERN.test(requestId))
|
|
190
|
+
throw new Error("--request-id must be a UUID.");
|
|
191
|
+
const expectedRevision = optionalPositiveInteger(values.flags.get("--expected-revision"), "--expected-revision");
|
|
173
192
|
const title = optionalNonEmpty(values.flags.get("--title"));
|
|
174
193
|
if (action === "create" && !title) {
|
|
175
194
|
throw new Error("docs create needs --title.");
|
|
176
195
|
}
|
|
196
|
+
return { visibility, clearParent, parentId, allowEmpty, title, requestId, expectedRevision };
|
|
197
|
+
}
|
|
198
|
+
/** The flags that narrow or slice what a list or read returns. */
|
|
199
|
+
function parseDocsReadFlags(action, values) {
|
|
177
200
|
// BLI-3737: narrowing belongs to `list` only. `--parent` keeps its create/
|
|
178
201
|
// update meaning elsewhere (which parent to file the document under); on
|
|
179
202
|
// `list` the same word names which parent to list UNDER, plus the literal
|
|
@@ -205,18 +228,21 @@ function parseDocsFlags(action, values) {
|
|
|
205
228
|
const section = optionalNonEmpty(values.flags.get("--section"));
|
|
206
229
|
if (section && action !== "read")
|
|
207
230
|
throw new Error("--section belongs to `cockpit docs read`.");
|
|
208
|
-
const
|
|
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
|
-
}
|
|
231
|
+
const offset = parseDocsOffset(values.flags.get("--offset"));
|
|
215
232
|
if (offset !== undefined && action !== "read")
|
|
216
233
|
throw new Error("--offset belongs to `cockpit docs read`.");
|
|
217
|
-
return {
|
|
234
|
+
return { query, wholeWord, limit, bodies, section, offset };
|
|
235
|
+
}
|
|
236
|
+
function parseDocsOffset(raw) {
|
|
237
|
+
const offsetRaw = optionalNonEmpty(raw);
|
|
238
|
+
if (offsetRaw === undefined)
|
|
239
|
+
return undefined;
|
|
240
|
+
const offset = Number(offsetRaw);
|
|
241
|
+
if (!Number.isInteger(offset) || offset < 0)
|
|
242
|
+
throw new Error("--offset must be a whole number 0 or greater.");
|
|
243
|
+
return offset;
|
|
218
244
|
}
|
|
219
|
-
function buildDocsCommand(values, action, docRef, docRefs, visibility, clearParent, parentId, allowEmpty, title, query, wholeWord, limit, bodies, section, offset, link) {
|
|
245
|
+
function buildDocsCommand(values, action, docRef, docRefs, visibility, clearParent, parentId, allowEmpty, title, query, wholeWord, limit, bodies, section, offset, link, safety) {
|
|
220
246
|
return {
|
|
221
247
|
kind: "docs",
|
|
222
248
|
action,
|
|
@@ -228,6 +254,8 @@ function buildDocsCommand(values, action, docRef, docRefs, visibility, clearPare
|
|
|
228
254
|
...(parentId ? { parentId } : {}),
|
|
229
255
|
clearParent,
|
|
230
256
|
...(allowEmpty ? { allowEmpty } : {}),
|
|
257
|
+
...(safety.requestId ? { requestId: safety.requestId } : {}),
|
|
258
|
+
...(safety.expectedRevision === undefined ? {} : { expectedRevision: safety.expectedRevision }),
|
|
231
259
|
visibility: visibility,
|
|
232
260
|
filePath: optionalNonEmpty(values.flags.get("--file")),
|
|
233
261
|
bodyStdin: values.booleans.has("--body-stdin"),
|
|
@@ -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
|
-
|
|
37
|
-
|
|
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
|
}
|
|
@@ -37,27 +37,39 @@ export function parseHitsArgs(args) {
|
|
|
37
37
|
}
|
|
38
38
|
if (given("--weeks") && action !== "timeline")
|
|
39
39
|
throw new Error("--weeks belongs to hits timeline.");
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
return
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
throw new Error("--scope and --version belong to hits approve.");
|
|
51
|
-
return { ...common, action };
|
|
52
|
-
}
|
|
53
|
-
if (action === "approve") {
|
|
54
|
-
const scope = optionalNonEmpty(values.flags.get("--scope"));
|
|
55
|
-
if (!scope || !SCOPE.test(scope))
|
|
56
|
-
throw new Error("hits approve wants --scope team, agents or project:<id> (for example project:pearl-portal).");
|
|
57
|
-
return { ...common, action, scope, ...versionOf(values.flags.get("--version")) };
|
|
58
|
-
}
|
|
40
|
+
const parsed = { values, given, common };
|
|
41
|
+
if (action === "timeline")
|
|
42
|
+
return parseHitsTimeline(parsed);
|
|
43
|
+
if (action === "coach")
|
|
44
|
+
return parseHitsCoach(parsed);
|
|
45
|
+
if (action === "approve")
|
|
46
|
+
return parseHitsApprove(parsed);
|
|
47
|
+
return parseHitsRead(parsed);
|
|
48
|
+
}
|
|
49
|
+
function refuseApproveFlags({ given }) {
|
|
59
50
|
if (given("--scope") || given("--version"))
|
|
60
51
|
throw new Error("--scope and --version belong to hits approve.");
|
|
52
|
+
}
|
|
53
|
+
function parseHitsTimeline(parsed) {
|
|
54
|
+
refuseApproveFlags(parsed);
|
|
55
|
+
const weeks = optionalPositiveInteger(parsed.values.flags.get("--weeks"), "--weeks");
|
|
56
|
+
if (weeks !== undefined && weeks > 26)
|
|
57
|
+
throw new Error("--weeks must be 26 or fewer.");
|
|
58
|
+
return { ...parsed.common, action: "timeline", ...(weeks !== undefined ? { weeks } : {}) };
|
|
59
|
+
}
|
|
60
|
+
function parseHitsCoach(parsed) {
|
|
61
|
+
refuseApproveFlags(parsed);
|
|
62
|
+
return { ...parsed.common, action: "coach" };
|
|
63
|
+
}
|
|
64
|
+
function parseHitsApprove({ values, common }) {
|
|
65
|
+
const scope = optionalNonEmpty(values.flags.get("--scope"));
|
|
66
|
+
if (!scope || !SCOPE.test(scope))
|
|
67
|
+
throw new Error("hits approve wants --scope team, agents or project:<id> (for example project:pearl-portal).");
|
|
68
|
+
return { ...common, action: "approve", scope, ...versionOf(values.flags.get("--version")) };
|
|
69
|
+
}
|
|
70
|
+
function parseHitsRead(parsed) {
|
|
71
|
+
const { values, given, common } = parsed;
|
|
72
|
+
refuseApproveFlags(parsed);
|
|
61
73
|
const whoFlags = ["--mine", "--person", "--agent", "--project"].filter(given);
|
|
62
74
|
if (whoFlags.length > 1)
|
|
63
75
|
throw new Error(`Pick one of --mine, --person, --agent or --project (got ${whoFlags.join(" and ")}).`);
|
|
@@ -77,7 +89,7 @@ export function parseHitsArgs(args) {
|
|
|
77
89
|
throw new Error("--learning is read for one person: use --mine or --person <name>.");
|
|
78
90
|
return {
|
|
79
91
|
...common,
|
|
80
|
-
action,
|
|
92
|
+
action: "read",
|
|
81
93
|
...(who ? { who } : {}),
|
|
82
94
|
...(person ? { person } : {}),
|
|
83
95
|
...(project ? { project } : {}),
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* cockpit manager clients
|
|
5
5
|
* cockpit manager client <client>
|
|
6
|
+
* cockpit manager home <client>
|
|
7
|
+
* cockpit manager summary <client> --version <v>|none [--health on_track|at_risk|blocked] [--headline <t>] [--bullet "YYYY-MM-DD: t"|"t"]... [--next <t>|none] [--blocker <t>|none]
|
|
6
8
|
* cockpit manager run <run-id>
|
|
7
9
|
* cockpit manager list <client> [--timeline] [--needs-review] [--status open|done|dropped|closed|all] [--owner <name> | --no-owner] [--overdue] [--kind <kind>] [--search <text>] [--archived] [--review dismissed|all]
|
|
8
10
|
* cockpit manager queue <client>
|
|
@@ -22,27 +24,36 @@
|
|
|
22
24
|
*/
|
|
23
25
|
import { optionalNonEmpty, optionalUrl, parseNamedArgs } from "./local-arg-values.js";
|
|
24
26
|
export const MANAGER_VERBS = [
|
|
25
|
-
"clients", "client", "run", "list", "queue", "show", "add", "edit", "done", "reopen", "link",
|
|
27
|
+
"clients", "client", "home", "summary", "run", "list", "queue", "show", "add", "edit", "done", "reopen", "link",
|
|
26
28
|
"approve", "dismiss", "proposal", "archive", "unarchive",
|
|
27
29
|
];
|
|
28
30
|
export const MANAGER_ACTION_STATUSES = ["open", "done", "dropped", "closed", "all"];
|
|
29
31
|
export const MANAGER_TIMELINE_KINDS = ["meeting", "document", "milestone", "internal"];
|
|
32
|
+
export const MANAGER_SUMMARY_HEALTHS = ["on_track", "at_risk", "blocked"];
|
|
33
|
+
/** The verbs whose one positional word is a client (its id or its exact name). */
|
|
34
|
+
const CLIENT_VERBS = new Set(["client", "home", "summary", "list", "queue", "add"]);
|
|
30
35
|
const ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
31
36
|
const VERSION = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}/;
|
|
32
37
|
const ONE_VERB = `manager takes one verb: ${MANAGER_VERBS.join(", ")}. Example: \`cockpit manager clients\` or \`cockpit manager list <client>\`.`;
|
|
33
38
|
const OWN_FLAGS = [
|
|
34
39
|
"--timeline", "--needs-review", "--status", "--owner", "--owners", "--no-owner", "--kind", "--search", "--archived", "--review",
|
|
35
40
|
"--title", "--detail", "--due", "--date", "--meeting", "--none", "--version", "--request-id", "--restore", "--overdue", "--source-url", "--source-note",
|
|
41
|
+
"--health", "--headline", "--bullet", "--next", "--blocker",
|
|
36
42
|
];
|
|
37
43
|
const COMMON_FLAGS = ["--home", "--dashboard-url", "--json", "--help"];
|
|
38
44
|
const VALUE_FLAGS = [
|
|
39
45
|
"--status", "--owner", "--owners", "--kind", "--search", "--review", "--title", "--detail", "--due", "--date", "--meeting",
|
|
40
46
|
"--version", "--request-id", "--source-url", "--source-note", "--home", "--dashboard-url",
|
|
47
|
+
"--health", "--headline", "--bullet", "--next", "--blocker",
|
|
41
48
|
];
|
|
49
|
+
/** `summary --bullet` is given once per bullet: the one value flag that may repeat. */
|
|
50
|
+
const REPEATABLE_FLAGS = new Set(["--bullet"]);
|
|
42
51
|
/** Which flags each verb owns, so a flag on the wrong verb is refused by name rather than ignored. */
|
|
43
52
|
export const FLAGS_BY_VERB = {
|
|
44
53
|
clients: [],
|
|
45
54
|
client: [],
|
|
55
|
+
home: [],
|
|
56
|
+
summary: ["--version", "--health", "--headline", "--bullet", "--next", "--blocker"],
|
|
46
57
|
run: [],
|
|
47
58
|
list: ["--timeline", "--needs-review", "--status", "--owner", "--no-owner", "--overdue", "--kind", "--search", "--archived", "--review"],
|
|
48
59
|
queue: [],
|
|
@@ -62,6 +73,8 @@ export const FLAGS_BY_VERB = {
|
|
|
62
73
|
const POSITIONALS = {
|
|
63
74
|
clients: [],
|
|
64
75
|
client: ["client"],
|
|
76
|
+
home: ["client"],
|
|
77
|
+
summary: ["client"],
|
|
65
78
|
run: ["run id"],
|
|
66
79
|
list: ["client"],
|
|
67
80
|
queue: ["client"],
|
|
@@ -80,6 +93,8 @@ const POSITIONALS = {
|
|
|
80
93
|
/** A value flag given twice would silently keep only the last; say so instead. */
|
|
81
94
|
function refuseRepeatedFlags(args) {
|
|
82
95
|
for (const flag of VALUE_FLAGS) {
|
|
96
|
+
if (REPEATABLE_FLAGS.has(flag))
|
|
97
|
+
continue;
|
|
83
98
|
const times = args.filter((arg) => arg === flag || arg.startsWith(`${flag}=`)).length;
|
|
84
99
|
if (times > 1) {
|
|
85
100
|
throw new Error(flag === "--owners"
|
|
@@ -113,12 +128,12 @@ export function parseManagerArgs(args) {
|
|
|
113
128
|
if (rest.length !== wanted.length) {
|
|
114
129
|
throw new Error(wanted.length === 0
|
|
115
130
|
? `\`cockpit manager ${verb}\` takes no positional arguments.`
|
|
116
|
-
: `\`cockpit manager ${verb}\` takes exactly: ${wanted.map((word) => `<${word}>`).join(" ")}${verb
|
|
131
|
+
: `\`cockpit manager ${verb}\` takes exactly: ${wanted.map((word) => `<${word}>`).join(" ")}${CLIENT_VERBS.has(verb) ? " (quote a client's name that has spaces)" : ""}.`);
|
|
117
132
|
}
|
|
118
133
|
const timeline = values.booleans.has("--timeline");
|
|
119
134
|
const shape = { ...base, action: verb, timeline };
|
|
120
135
|
const flag = (name) => values.flags.get(name);
|
|
121
|
-
if (verb
|
|
136
|
+
if (CLIENT_VERBS.has(verb))
|
|
122
137
|
shape.client = rest[0];
|
|
123
138
|
else if (verb === "run")
|
|
124
139
|
shape.runId = readId(rest[0], "A run");
|
|
@@ -128,9 +143,11 @@ export function parseManagerArgs(args) {
|
|
|
128
143
|
shape.proposalId = rest[1];
|
|
129
144
|
shape.decision = readDecision(rest[2]);
|
|
130
145
|
}
|
|
131
|
-
const needsVersion = !["clients", "client", "run", "list", "queue", "show", "add"].includes(verb);
|
|
146
|
+
const needsVersion = !["clients", "client", "home", "summary", "run", "list", "queue", "show", "add"].includes(verb);
|
|
132
147
|
if (needsVersion)
|
|
133
148
|
shape.version = readVersion(flag("--version"), verb);
|
|
149
|
+
if (verb === "summary")
|
|
150
|
+
readSummaryFields(values, shape);
|
|
134
151
|
if (verb === "list")
|
|
135
152
|
readListFilters(values, shape);
|
|
136
153
|
if (verb === "add")
|
|
@@ -368,4 +385,68 @@ function readLink(values, shape) {
|
|
|
368
385
|
if (!meeting && !none)
|
|
369
386
|
throw new Error("`cockpit manager link` needs --meeting <timeline-id> to link, or --none to unlink.");
|
|
370
387
|
shape.timelineItemId = meeting ? readId(meeting, "A timeline item") : null;
|
|
388
|
+
}
|
|
389
|
+
/** `summary`'s version: the card's `summary.updatedAt` as `home --json` gives it, or `none` when the client has no card yet. */
|
|
390
|
+
function readSummaryVersion(raw) {
|
|
391
|
+
if (raw === undefined) {
|
|
392
|
+
throw new Error("`cockpit manager summary` needs --version: the card's summary.updatedAt from `cockpit manager home <client> --json`, or none when the client has no card yet. It keeps you from writing over a newer card.");
|
|
393
|
+
}
|
|
394
|
+
if (isNone(raw))
|
|
395
|
+
return null;
|
|
396
|
+
if (!VERSION.test(raw))
|
|
397
|
+
throw new Error("--version is the card's summary.updatedAt exactly as `cockpit manager home <client> --json` gives it, like 2026-10-09T17:03:11.123456+00:00, or none.");
|
|
398
|
+
return raw;
|
|
399
|
+
}
|
|
400
|
+
/** `--bullet "2026-10-14: Signed the SOW"` is a dated bullet; without a leading `YYYY-MM-DD: ` it is undated. */
|
|
401
|
+
export function readBullet(raw) {
|
|
402
|
+
// A bullet pasted from a list keeps no "- " or "* " of its own.
|
|
403
|
+
const value = raw.trim().replace(/^[-*•]\s+/u, "");
|
|
404
|
+
const dated = /^(\d{4}-\d{2}-\d{2}):\s*(.*)$/s.exec(value);
|
|
405
|
+
const dateLike = dated ? null : /^(\d{1,4}[-/.]\d{1,2}[-/.]\d{1,4})\s*:/.exec(value);
|
|
406
|
+
if (dateLike)
|
|
407
|
+
throw new Error(`--bullet "${raw}" starts with ${dateLike[1]}, which is not a YYYY-MM-DD day. Write it like 2026-10-14, or leave the date out for an undated bullet.`);
|
|
408
|
+
const date = dated ? dated[1] : null;
|
|
409
|
+
const text = (dated ? dated[2] : value).trim();
|
|
410
|
+
if (date !== null && !isCalendarDate(date))
|
|
411
|
+
throw new Error(`--bullet "${raw}" starts with ${date}, which is not a real day. Write the date YYYY-MM-DD, like 2026-10-14, or leave it out for an undated bullet.`);
|
|
412
|
+
if (text === "")
|
|
413
|
+
throw new Error("--bullet needs words after its date. Example: --bullet \"2026-10-14: Signed the SOW\".");
|
|
414
|
+
return { date, text };
|
|
415
|
+
}
|
|
416
|
+
/** The card's fields. Counts, lengths and "blocked needs a blocker" are the server's rules, refused there by name. */
|
|
417
|
+
function readSummaryFields(values, shape) {
|
|
418
|
+
shape.version = readSummaryVersion(values.flags.get("--version"));
|
|
419
|
+
let changes = 0;
|
|
420
|
+
const health = values.flags.get("--health");
|
|
421
|
+
if (health !== undefined) {
|
|
422
|
+
if (!MANAGER_SUMMARY_HEALTHS.includes(health))
|
|
423
|
+
throw new Error(`--health wants one of ${MANAGER_SUMMARY_HEALTHS.join(", ")}.`);
|
|
424
|
+
shape.health = health;
|
|
425
|
+
changes += 1;
|
|
426
|
+
}
|
|
427
|
+
const headline = values.flags.get("--headline");
|
|
428
|
+
if (headline !== undefined) {
|
|
429
|
+
if (headline.trim() === "")
|
|
430
|
+
throw new Error("--headline cannot be blank.");
|
|
431
|
+
shape.headline = headline.trim();
|
|
432
|
+
changes += 1;
|
|
433
|
+
}
|
|
434
|
+
const bullets = values.flagValues.get("--bullet");
|
|
435
|
+
if (bullets !== undefined && bullets.length > 0) {
|
|
436
|
+
shape.bullets = bullets.map(readBullet);
|
|
437
|
+
changes += 1;
|
|
438
|
+
}
|
|
439
|
+
const next = values.flags.get("--next");
|
|
440
|
+
if (next !== undefined) {
|
|
441
|
+
shape.next = isNone(next) ? null : next.trim();
|
|
442
|
+
changes += 1;
|
|
443
|
+
}
|
|
444
|
+
const blocker = values.flags.get("--blocker");
|
|
445
|
+
if (blocker !== undefined) {
|
|
446
|
+
shape.blocker = isNone(blocker) ? null : blocker.trim();
|
|
447
|
+
changes += 1;
|
|
448
|
+
}
|
|
449
|
+
if (changes === 0) {
|
|
450
|
+
throw new Error("`cockpit manager summary` has nothing to change. Give at least one of --health, --headline, --bullet (1 to 5 of them, replacing the list), --next, --blocker. A flag you leave out keeps its value; none clears --next or --blocker.");
|
|
451
|
+
}
|
|
371
452
|
}
|