@bli-cockpit/cli 0.2.222 → 0.2.236
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/agent-plugins-converge.js +182 -0
- package/dist/agent-plugins.js +116 -0
- package/dist/autostart-darwin-plist.js +2 -2
- package/dist/autostart-windows-scripts.js +2 -2
- package/dist/commands/agent-door.js +35 -0
- package/dist/commands/approvals.js +3 -1
- package/dist/commands/brain.js +23 -0
- package/dist/commands/ci.js +23 -0
- package/dist/commands/docs-export.js +62 -0
- package/dist/commands/docs.js +3 -0
- package/dist/commands/doctor-access.js +2 -2
- package/dist/commands/doctor-agent-plugins.js +82 -0
- package/dist/commands/doctor-pipeline.js +2 -2
- package/dist/commands/doctor-report.js +1 -0
- package/dist/commands/doctor-update.js +2 -2
- package/dist/commands/doctor.js +6 -0
- package/dist/commands/drills-render.js +63 -0
- package/dist/commands/drills.js +152 -0
- package/dist/commands/hits.js +83 -0
- package/dist/commands/intentions.js +49 -0
- package/dist/commands/local-args-docs-export.js +23 -0
- package/dist/commands/local-args-tower-admin.js +8 -0
- package/dist/commands/local-args-tower-brain-ladder.js +90 -0
- package/dist/commands/local-args-tower-brain.js +11 -1
- package/dist/commands/local-args-tower-docs-msg.js +9 -3
- package/dist/commands/local-args-tower-drills.js +87 -0
- package/dist/commands/local-args-tower-hits.js +93 -0
- package/dist/commands/local-args-tower-intentions.js +70 -0
- package/dist/commands/local-args-tower-logs-search.js +51 -0
- package/dist/commands/local-args-tower-logs.js +3 -0
- package/dist/commands/local-args-tower-memory.js +67 -1
- package/dist/commands/local-args-tower-pages.js +3 -1
- package/dist/commands/local-args-tower-people.js +25 -0
- package/dist/commands/local-args-tower-usage.js +4 -4
- package/dist/commands/local-args-tower-who.js +36 -0
- package/dist/commands/local-args-tower.js +8 -1
- package/dist/commands/local-args.js +15 -1
- package/dist/commands/local-auth.js +5 -2
- package/dist/commands/local-command-shapes-auth.js +1 -0
- package/dist/commands/local-command-shapes-drills.js +7 -0
- package/dist/commands/local-command-shapes-hits.js +5 -0
- package/dist/commands/local-help-commands-tower-ops.js +123 -1
- package/dist/commands/local-help-commands-tower.js +19 -7
- package/dist/commands/local-help-commands.js +3 -0
- package/dist/commands/local-help.js +24 -4
- package/dist/commands/local-run-ops.js +11 -0
- package/dist/commands/local.js +16 -8
- package/dist/commands/logs-search.js +45 -0
- package/dist/commands/logs.js +3 -0
- package/dist/commands/memory-exam.js +47 -0
- package/dist/commands/memory-repeats.js +44 -0
- package/dist/commands/memory-search-print.js +95 -0
- package/dist/commands/memory-source.js +39 -0
- package/dist/commands/memory-store.js +18 -30
- package/dist/commands/memory-verb-list.js +7 -2
- package/dist/commands/notes-reads.js +20 -0
- package/dist/commands/notes-writes.js +24 -0
- package/dist/commands/notes.js +4 -2
- package/dist/commands/people.js +29 -0
- package/dist/commands/public-root.js +1 -1
- package/dist/commands/spend.js +79 -0
- package/dist/commands/sync-heal-agent-plugins.js +46 -0
- package/dist/commands/sync-heal.js +6 -0
- package/dist/commands/team.js +35 -0
- package/dist/commands/usage.js +9 -0
- package/dist/commands/who.js +36 -0
- package/dist/evidence-upload-direct.js +44 -0
- package/dist/evidence-upload-object.js +18 -4
- package/dist/evidence-upload-plan.js +1 -0
- package/dist/evidence-upload-transport.js +2 -0
- package/dist/local-state-config.js +21 -1
- package/dist/local-state.js +1 -1
- package/dist/process-runner.js +7 -3
- package/dist/state-dir-audit.js +1 -0
- package/package.json +4 -4
|
@@ -5,7 +5,7 @@ import { retentionOptionsFromEnv } from "../disk-retention.js";
|
|
|
5
5
|
import { readStagingGate, stagingGateLimitsFromEnv } from "../staging-gate.js";
|
|
6
6
|
import { readDiskFootprint } from "../disk-usage.js";
|
|
7
7
|
import { runEvidenceRedelivery } from "../evidence-redelivery.js";
|
|
8
|
-
import {
|
|
8
|
+
import { isDefaultDashboardUrl, getCollectorRuntimePaths } from "../local-state.js";
|
|
9
9
|
import { runRawEvidenceLocalGc, rawEvidenceGcSummary } from "../raw-evidence-gc.js";
|
|
10
10
|
import { FOOTPRINT_NEEDS_FIX_BYTES, stagingPausedWords, diskRowMessage, footprintHolder, footprintTotalBytes, mib, redeliveryLine, } from "./doctor-disk-words.js";
|
|
11
11
|
import { doctorRoots } from "./doctor-access.js";
|
|
@@ -185,7 +185,7 @@ async function runSyncRepair(context) {
|
|
|
185
185
|
const args = ["sync", "--json", "--workspace", repoRoot, ...discoveryArgs];
|
|
186
186
|
if (context.command.homeDir)
|
|
187
187
|
args.push("--home", context.command.homeDir);
|
|
188
|
-
if (context.command.dashboardUrl
|
|
188
|
+
if (!isDefaultDashboardUrl(context.command.dashboardUrl)) {
|
|
189
189
|
args.push("--dashboard-url", context.command.dashboardUrl);
|
|
190
190
|
}
|
|
191
191
|
// One line every 30 s while doctor's own sync runs; the exec has nothing
|
|
@@ -174,6 +174,7 @@ export function doctorNeedsPerson(rows, receipt) {
|
|
|
174
174
|
"usage-telemetry": "cockpit doctor",
|
|
175
175
|
"memory-registered": "cockpit doctor",
|
|
176
176
|
"agent-rules": "cockpit doctor",
|
|
177
|
+
"agent-plugins": "cockpit doctor",
|
|
177
178
|
"mcp-answers": "cockpit doctor",
|
|
178
179
|
"memory-daemon": "cockpit doctor",
|
|
179
180
|
"backfill-complete": "cockpit doctor",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { isSemverBelow } from "../scheduled-self-update.js";
|
|
2
|
-
import {
|
|
2
|
+
import { isDefaultDashboardUrl, LOCAL_COLLECTOR_VERSION } from "../local-state.js";
|
|
3
3
|
import { createInteractiveExecRunner } from "../process-runner.js";
|
|
4
4
|
import { asRecord, fail, needsFix, ok } from "./doctor-report.js";
|
|
5
5
|
/**
|
|
@@ -89,7 +89,7 @@ export function reexecDoctor(command, io) {
|
|
|
89
89
|
args.push("--max-repos", String(command.maxRepos));
|
|
90
90
|
for (const root of command.collectionRoots ?? (command.repoRoot ? [command.repoRoot] : []))
|
|
91
91
|
args.push("--workspace", root);
|
|
92
|
-
if (command.dashboardUrl
|
|
92
|
+
if (!isDefaultDashboardUrl(command.dashboardUrl)) {
|
|
93
93
|
args.push("--dashboard-url", command.dashboardUrl);
|
|
94
94
|
}
|
|
95
95
|
if (command.updateTag)
|
package/dist/commands/doctor.js
CHANGED
|
@@ -10,6 +10,7 @@ import { gateRow, readCollectionGate } from "./doctor-collection-lock.js";
|
|
|
10
10
|
import { checkMcpAnswersState, fixMcpAnswersState } from "./doctor-mcp.js";
|
|
11
11
|
import { budgetSpentRow, doctorRunDeadline, DOCTOR_JSON_BUDGET_SECONDS } from "./doctor-budget.js";
|
|
12
12
|
import { checkMemoryDaemonState } from "./doctor-memory-daemon.js";
|
|
13
|
+
import { checkAgentPluginsState, fixAgentPluginsState } from "./doctor-agent-plugins.js";
|
|
13
14
|
import { checkDoctorAgentRules, fixDoctorAgentRules, checkAutostartState, checkMemoryState, fixAutostartState, fixMemoryState, } from "./doctor-registration.js";
|
|
14
15
|
import { dryRunPreview, isInteractiveDoctorFix, maybeReportDoctorEvents, writeDoctorOutput, doctorNeedsPerson, finalizeDoctorRepairs, } from "./doctor-report.js";
|
|
15
16
|
import { buildSetupReceipt, refreshSetupReceipt } from "./setup-receipt.js";
|
|
@@ -189,6 +190,9 @@ function doctorInvariants() {
|
|
|
189
190
|
},
|
|
190
191
|
{ id: "usage-telemetry", check: context => context.deps.checkUsageTelemetry(context), fix: context => context.deps.fixUsageTelemetry(context) },
|
|
191
192
|
{ id: "agent-rules", check: (context) => context.deps.checkAgentRules(context), fix: (context) => context.deps.fixAgentRules(context) },
|
|
193
|
+
// BLI-18200. Right after the rules row: same family, what the two agent
|
|
194
|
+
// hosts carry. The check reads the hosts' files; only the fix starts their CLIs.
|
|
195
|
+
{ id: "agent-plugins", check: (context) => context.deps.checkAgentPlugins(context), fix: (context) => context.deps.fixAgentPlugins(context) },
|
|
192
196
|
// BLI-3804. Right after the registration row, because it asks the second
|
|
193
197
|
// half of the same question: `memory-registered` proves the entry exists
|
|
194
198
|
// and names a bin, this one proves the server behind it starts, speaks the
|
|
@@ -240,6 +244,8 @@ function defaultDoctorDeps(hooks) {
|
|
|
240
244
|
onReceipt: hooks.onReceipt,
|
|
241
245
|
checkAgentRules: checkDoctorAgentRules,
|
|
242
246
|
fixAgentRules: fixDoctorAgentRules,
|
|
247
|
+
checkAgentPlugins: (context) => checkAgentPluginsState(context),
|
|
248
|
+
fixAgentPlugins: (context) => fixAgentPluginsState(context),
|
|
243
249
|
latestCliVersion: latestCliVersionFromNpm,
|
|
244
250
|
selfUpdate: hooks.selfUpdate,
|
|
245
251
|
reexecDoctor: reexecDoctor,
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/** Plain-words rendering of the drills doors for the terminal (BLI-19259). No system nouns, no dashes used as punctuation. */
|
|
2
|
+
const indent = (text, prefix = " ") => text.split("\n").map((line) => `${prefix}${line}`);
|
|
3
|
+
export function drillLines(current) {
|
|
4
|
+
const lines = [`${current.framing.bossName}: ${current.framing.roundLine}${current.redrillDay ? ` (back after ${current.redrillDay} day${current.redrillDay === 1 ? "" : "s"})` : ""}`, "", current.scenario.title, current.scenario.context, ""];
|
|
5
|
+
lines.push(`The ${current.scenario.artifactKind}:`, ...indent(current.scenario.artifact));
|
|
6
|
+
if (current.scenario.logExcerpt)
|
|
7
|
+
lines.push("", "What it printed, up to the moment before it broke:", ...indent(current.scenario.logExcerpt));
|
|
8
|
+
lines.push("", current.scenario.question);
|
|
9
|
+
if (current.scenario.reconstructed)
|
|
10
|
+
lines.push("(Rebuilt from the incident write-up, so the details are close, not word for word.)");
|
|
11
|
+
return lines;
|
|
12
|
+
}
|
|
13
|
+
const minutes = (value) => (value === null ? "none yet" : value >= 120 ? `${(value / 60).toFixed(1)} hours` : `${Math.round(value)} minutes`);
|
|
14
|
+
export function overviewLines(body) {
|
|
15
|
+
const lines = [];
|
|
16
|
+
if (body.current)
|
|
17
|
+
lines.push("You have a drill open. Answer it before you see the cause:", "", ...drillLines(body.current), "", "Answer with: echo \"what you think\" | cockpit drills answer --confidence <1-5>", "");
|
|
18
|
+
else if (body.queue.redrillsDue > 0)
|
|
19
|
+
lines.push(`${body.queue.redrillsDue} drill${body.queue.redrillsDue === 1 ? "" : "s"} you were sure about ${body.queue.redrillsDue === 1 ? "is" : "are"} due for another look. Run: cockpit drills start`, "");
|
|
20
|
+
else if (body.queue.newLeft > 0)
|
|
21
|
+
lines.push(`${body.queue.newLeft} new drill${body.queue.newLeft === 1 ? "" : "s"} waiting. Run: cockpit drills start`, "");
|
|
22
|
+
else
|
|
23
|
+
lines.push("You are all caught up. Nothing new is waiting.", "");
|
|
24
|
+
lines.push(`Theme: ${body.theme.label}${body.theme.chosen ? "" : " (the default; change it with `cockpit drills theme set <theme>`)"}`, "");
|
|
25
|
+
if (body.bosses.length > 0) {
|
|
26
|
+
lines.push("Bosses");
|
|
27
|
+
for (const boss of body.bosses)
|
|
28
|
+
lines.push(` ${boss.bossName}: ${boss.progressLine}`);
|
|
29
|
+
lines.push("");
|
|
30
|
+
}
|
|
31
|
+
const { stats } = body;
|
|
32
|
+
lines.push(`You have answered ${stats.answered} and passed ${stats.passed}. ${stats.confidentMisses} wrong answer${stats.confidentMisses === 1 ? "" : "s"} you were sure about.`);
|
|
33
|
+
lines.push(stats.firstRunGreenStreak.known ? `PRs green on the first CI run, in a row: at least ${stats.firstRunGreenStreak.value}.` : (stats.firstRunGreenStreak.note ?? ""));
|
|
34
|
+
if (stats.recovery.known && stats.recovery.value)
|
|
35
|
+
lines.push(`Recovering from a hit: last ${minutes(stats.recovery.value.lastMinutes)}, your best ${minutes(stats.recovery.value.bestMinutes)}.`);
|
|
36
|
+
if (stats.teamBeaten.known && stats.teamBeaten.value)
|
|
37
|
+
lines.push(`Across the team, ${stats.teamBeaten.value.fightsBeaten} boss fight${stats.teamBeaten.value.fightsBeaten === 1 ? "" : "s"} won, ${stats.teamBeaten.value.patternsBeaten} pattern${stats.teamBeaten.value.patternsBeaten === 1 ? "" : "s"} beaten at least once.`);
|
|
38
|
+
if (body.history.length > 0) {
|
|
39
|
+
lines.push("", "Recently");
|
|
40
|
+
for (const item of body.history.slice(0, 5))
|
|
41
|
+
lines.push(` ${item.verdict === "pass" ? "passed" : item.verdict === "miss" ? "missed" : "unmarked"} ${item.title} (sure: ${item.confidence} of 5${item.hypercorrection ? ", comes back" : ""})`);
|
|
42
|
+
}
|
|
43
|
+
for (const note of body.notes)
|
|
44
|
+
lines.push("", note);
|
|
45
|
+
return lines.filter((line, index, all) => !(line === "" && all[index - 1] === ""));
|
|
46
|
+
}
|
|
47
|
+
export function reviewLines(body) {
|
|
48
|
+
const lines = [body.headline];
|
|
49
|
+
const section = (title, items) => {
|
|
50
|
+
if (items.length > 0)
|
|
51
|
+
lines.push("", title, ...items.map((item) => ` ${item}`));
|
|
52
|
+
};
|
|
53
|
+
if (body.hits.known && body.hits.value)
|
|
54
|
+
section("Your hits this week", body.hits.value.length === 0 ? ["None."] : body.hits.value.map((hit) => `A ${hit.weight} one: ${hit.patternLabel.toLowerCase()}${hit.recoveredMinutes === null ? "" : `, back to green in ${minutes(hit.recoveredMinutes)}`}.`));
|
|
55
|
+
section("Wins", body.wins);
|
|
56
|
+
section("Highs", body.highs);
|
|
57
|
+
section("Lows", body.lows);
|
|
58
|
+
section("Where you have stopped improving", body.plateaus);
|
|
59
|
+
section("What to practise", body.practise.map((item) => `${item.patternLabel}: ${item.why}`));
|
|
60
|
+
for (const note of body.notes)
|
|
61
|
+
lines.push("", note);
|
|
62
|
+
return lines;
|
|
63
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { askAgentDoor, emitAgentDoor, failAgentDoor, openAgentDoor } from "./agent-door.js";
|
|
2
|
+
import { drillLines, overviewLines, reviewLines } from "./drills-render.js";
|
|
3
|
+
import { isInteractiveStdin, readPipedText, writeLine } from "./cli-io.js";
|
|
4
|
+
const TAG = "[drills cli]";
|
|
5
|
+
const READ_DEADLINE_MS = 30_000;
|
|
6
|
+
const WRITE_DEADLINE_MS = 55_000;
|
|
7
|
+
const ANSWER_MAX_CHARS = 4_000;
|
|
8
|
+
export async function runDrills(command, io) {
|
|
9
|
+
let door;
|
|
10
|
+
try {
|
|
11
|
+
door = await openAgentDoor("drills", command, io);
|
|
12
|
+
}
|
|
13
|
+
catch (error) {
|
|
14
|
+
const detail = error instanceof Error ? error.message : "this machine is not paired with Tower";
|
|
15
|
+
writeLine(io.stderr, `${TAG} refused ${JSON.stringify({ reason: "not_paired" })}`);
|
|
16
|
+
if (command.json)
|
|
17
|
+
writeLine(io.stdout, JSON.stringify({ ok: false, error: "not_paired", detail }));
|
|
18
|
+
else
|
|
19
|
+
writeLine(io.stderr, detail);
|
|
20
|
+
return 1;
|
|
21
|
+
}
|
|
22
|
+
switch (command.action) {
|
|
23
|
+
case "list": return list(door);
|
|
24
|
+
case "start": return start(door);
|
|
25
|
+
case "answer": return answer(command, door);
|
|
26
|
+
case "theme": return themeShow(door);
|
|
27
|
+
case "theme-set": return themeSet(command, door);
|
|
28
|
+
case "review": return review(door);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
async function list(door) {
|
|
32
|
+
const response = await askAgentDoor(door, { path: "/api/drills", method: "GET", label: "drills list", timeoutMs: READ_DEADLINE_MS });
|
|
33
|
+
if (!response.ok)
|
|
34
|
+
return failAgentDoor(door, TAG, response.reason, response.detail);
|
|
35
|
+
const body = response.body;
|
|
36
|
+
writeLine(door.io.stderr, `${TAG} list answered ${JSON.stringify({ open: body.current !== null, bosses: body.bosses.length, history: body.history.length, new_left: body.queue.newLeft })}`);
|
|
37
|
+
if (door.json)
|
|
38
|
+
return emitAgentDoor(door, response.body);
|
|
39
|
+
for (const line of overviewLines(body))
|
|
40
|
+
writeLine(door.io.stdout, line);
|
|
41
|
+
return 0;
|
|
42
|
+
}
|
|
43
|
+
async function start(door) {
|
|
44
|
+
const response = await askAgentDoor(door, { path: "/api/drills/start", method: "POST", label: "drills start", timeoutMs: READ_DEADLINE_MS, body: {} });
|
|
45
|
+
if (!response.ok)
|
|
46
|
+
return failAgentDoor(door, TAG, response.reason, response.detail);
|
|
47
|
+
const body = response.body;
|
|
48
|
+
writeLine(door.io.stderr, `${TAG} start answered ${JSON.stringify({ started: body.started, reason: body.reason })}`);
|
|
49
|
+
if (door.json)
|
|
50
|
+
return emitAgentDoor(door, response.body);
|
|
51
|
+
if (!body.started || !body.current) {
|
|
52
|
+
writeLine(door.io.stdout, body.message ?? "Nothing new is waiting for you right now.");
|
|
53
|
+
return 0;
|
|
54
|
+
}
|
|
55
|
+
for (const line of drillLines(body.current))
|
|
56
|
+
writeLine(door.io.stdout, line);
|
|
57
|
+
writeLine(door.io.stdout, "");
|
|
58
|
+
writeLine(door.io.stdout, "Answer first, then see the cause: echo \"what you think\" | cockpit drills answer --confidence <1-5>");
|
|
59
|
+
return 0;
|
|
60
|
+
}
|
|
61
|
+
async function openAttemptId(door) {
|
|
62
|
+
const response = await askAgentDoor(door, { path: "/api/drills", method: "GET", label: "drills list", timeoutMs: READ_DEADLINE_MS });
|
|
63
|
+
if (!response.ok)
|
|
64
|
+
return { ok: false, reason: response.reason, detail: response.detail };
|
|
65
|
+
const current = response.body.current;
|
|
66
|
+
if (!current)
|
|
67
|
+
return { ok: false, reason: "no_open_drill", detail: "No drill is open. Run `cockpit drills start` first." };
|
|
68
|
+
return { ok: true, id: current.attemptId };
|
|
69
|
+
}
|
|
70
|
+
async function answer(command, door) {
|
|
71
|
+
let text = command.text;
|
|
72
|
+
if (!text) {
|
|
73
|
+
if (isInteractiveStdin(door.io))
|
|
74
|
+
return failAgentDoor(door, TAG, "nothing_piped", "Pipe your answer in, e.g. `echo \"it will stop at 1,000 rows\" | cockpit drills answer --confidence 4`, or use --text. Nothing was sent.");
|
|
75
|
+
try {
|
|
76
|
+
text = (await readPipedText(door.io.stdin, { maxChars: ANSWER_MAX_CHARS, overflowMessage: `Keep your answer under ${ANSWER_MAX_CHARS} characters.` })).trim();
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
return failAgentDoor(door, TAG, "answer_too_long", error instanceof Error ? error.message : String(error));
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
let id = command.attemptId;
|
|
83
|
+
if (!id) {
|
|
84
|
+
const open = await openAttemptId(door);
|
|
85
|
+
if (!open.ok)
|
|
86
|
+
return failAgentDoor(door, TAG, open.reason, open.detail);
|
|
87
|
+
id = open.id;
|
|
88
|
+
}
|
|
89
|
+
const response = await askAgentDoor(door, { path: "/api/drills/answer", method: "POST", label: "drills answer", timeoutMs: WRITE_DEADLINE_MS, body: { attemptId: id, answer: text, confidence: command.confidence } });
|
|
90
|
+
if (!response.ok)
|
|
91
|
+
return failAgentDoor(door, TAG, response.reason, response.detail);
|
|
92
|
+
const body = response.body;
|
|
93
|
+
writeLine(door.io.stderr, `${TAG} answered ${JSON.stringify({ verdict: body.verdict, hypercorrection: body.hypercorrection, redrills: body.redrills.length, new_win: body.newWin })}`);
|
|
94
|
+
if (door.json)
|
|
95
|
+
return emitAgentDoor(door, response.body);
|
|
96
|
+
const out = door.io.stdout;
|
|
97
|
+
writeLine(out, body.message);
|
|
98
|
+
writeLine(out, "");
|
|
99
|
+
writeLine(out, "The real cause:");
|
|
100
|
+
writeLine(out, ` ${body.reveal.realCause}`);
|
|
101
|
+
writeLine(out, "The guard that would have caught it:");
|
|
102
|
+
writeLine(out, ` ${body.reveal.guard}`);
|
|
103
|
+
if (body.reveal.aftermath)
|
|
104
|
+
writeLine(out, `What it cost: ${body.reveal.aftermath}`);
|
|
105
|
+
if (body.redrills.length > 0)
|
|
106
|
+
writeLine(out, `It comes back in ${body.redrills.map((plan) => plan.day).join(", ")} days.`);
|
|
107
|
+
return 0;
|
|
108
|
+
}
|
|
109
|
+
async function themeShow(door) {
|
|
110
|
+
const response = await askAgentDoor(door, { path: "/api/drills/theme", method: "GET", label: "drills theme", timeoutMs: READ_DEADLINE_MS });
|
|
111
|
+
if (!response.ok)
|
|
112
|
+
return failAgentDoor(door, TAG, response.reason, response.detail);
|
|
113
|
+
const body = response.body;
|
|
114
|
+
writeLine(door.io.stderr, `${TAG} theme answered ${JSON.stringify({ key: body.theme.key, chosen: body.theme.chosen, ready: body.ready })}`);
|
|
115
|
+
if (door.json)
|
|
116
|
+
return emitAgentDoor(door, response.body);
|
|
117
|
+
writeLine(door.io.stdout, `Your theme: ${body.theme.label}${body.theme.chosen ? "" : " (the default)"}`);
|
|
118
|
+
writeLine(door.io.stdout, "Choices:");
|
|
119
|
+
for (const choice of body.themes)
|
|
120
|
+
writeLine(door.io.stdout, ` ${choice.key}: ${choice.blurb}${choice.key === body.suggestedTheme && !body.theme.chosen ? " (suggested)" : ""}`);
|
|
121
|
+
writeLine(door.io.stdout, "Or use your own words. Change it with: cockpit drills theme set <theme> [--play <what you play>] [--watch <what you watch>] [--train <what you train>]");
|
|
122
|
+
return 0;
|
|
123
|
+
}
|
|
124
|
+
async function themeSet(command, door) {
|
|
125
|
+
const response = await askAgentDoor(door, {
|
|
126
|
+
path: "/api/drills/theme",
|
|
127
|
+
method: "POST",
|
|
128
|
+
label: "drills theme set",
|
|
129
|
+
timeoutMs: WRITE_DEADLINE_MS,
|
|
130
|
+
body: { theme: command.theme, ...(command.play ? { play: command.play } : {}), ...(command.watch ? { watch: command.watch } : {}), ...(command.train ? { train: command.train } : {}) },
|
|
131
|
+
});
|
|
132
|
+
if (!response.ok)
|
|
133
|
+
return failAgentDoor(door, TAG, response.reason, response.detail);
|
|
134
|
+
const body = response.body;
|
|
135
|
+
writeLine(door.io.stderr, `${TAG} theme saved ${JSON.stringify({ key: body.theme.key, closed: body.theme.closed, preparing: body.preparing ?? false })}`);
|
|
136
|
+
if (door.json)
|
|
137
|
+
return emitAgentDoor(door, response.body);
|
|
138
|
+
writeLine(door.io.stdout, body.message ?? "Theme saved.");
|
|
139
|
+
return 0;
|
|
140
|
+
}
|
|
141
|
+
async function review(door) {
|
|
142
|
+
const response = await askAgentDoor(door, { path: "/api/drills/review", method: "GET", label: "drills review", timeoutMs: READ_DEADLINE_MS });
|
|
143
|
+
if (!response.ok)
|
|
144
|
+
return failAgentDoor(door, TAG, response.reason, response.detail);
|
|
145
|
+
const body = response.body;
|
|
146
|
+
writeLine(door.io.stderr, `${TAG} review answered ${JSON.stringify({ practise: body.practise.length, plateaus: body.plateaus.length })}`);
|
|
147
|
+
if (door.json)
|
|
148
|
+
return emitAgentDoor(door, response.body);
|
|
149
|
+
for (const line of reviewLines(body))
|
|
150
|
+
writeLine(door.io.stdout, line);
|
|
151
|
+
return 0;
|
|
152
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { askAgentDoor, emitAgentDoor, failAgentDoor, openAgentDoor } from "./agent-door.js";
|
|
2
|
+
import { writeLine } from "./cli-io.js";
|
|
3
|
+
const TAG = "[hits cli]";
|
|
4
|
+
const READ_DEADLINE_MS = 55_000;
|
|
5
|
+
/** The door's own query for a read: only what the person asked for, so the door's defaults stay the door's. */
|
|
6
|
+
export function hitsReadQuery(command) {
|
|
7
|
+
const query = new URLSearchParams();
|
|
8
|
+
if (command.who)
|
|
9
|
+
query.set("who", command.who);
|
|
10
|
+
if (command.person)
|
|
11
|
+
query.set("person", command.person);
|
|
12
|
+
if (command.project)
|
|
13
|
+
query.set("project", command.project);
|
|
14
|
+
if (command.days !== undefined)
|
|
15
|
+
query.set("days", String(command.days));
|
|
16
|
+
if (command.top !== undefined)
|
|
17
|
+
query.set("top", String(command.top));
|
|
18
|
+
if (command.learning)
|
|
19
|
+
query.set("learning", "1");
|
|
20
|
+
if (command.patterns)
|
|
21
|
+
query.set("view", "patterns");
|
|
22
|
+
return query.toString();
|
|
23
|
+
}
|
|
24
|
+
export async function runHits(command, io) {
|
|
25
|
+
let door;
|
|
26
|
+
try {
|
|
27
|
+
door = await openAgentDoor("hits", command, io);
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
const detail = error instanceof Error ? error.message : "this machine is not paired with Tower";
|
|
31
|
+
writeLine(io.stderr, `${TAG} refused ${JSON.stringify({ reason: "not_paired" })}`);
|
|
32
|
+
if (command.json)
|
|
33
|
+
writeLine(io.stdout, JSON.stringify({ ok: false, error: "not_paired", detail }));
|
|
34
|
+
else
|
|
35
|
+
writeLine(io.stderr, detail);
|
|
36
|
+
return 1;
|
|
37
|
+
}
|
|
38
|
+
if (command.action === "approve") {
|
|
39
|
+
const answer = await askAgentDoor(door, { path: "/api/hits/patterns/approve", method: "POST", label: "hits approve", timeoutMs: READ_DEADLINE_MS,
|
|
40
|
+
body: { scope: command.scope, ...(command.version !== undefined ? { version: command.version } : {}) } });
|
|
41
|
+
if (!answer.ok)
|
|
42
|
+
return failAgentDoor(door, TAG, answer.reason, answer.detail);
|
|
43
|
+
const body = answer.body;
|
|
44
|
+
writeLine(io.stderr, `${TAG} approve answered ${JSON.stringify({ reason: body.reason, approved: body.approved ?? 0, version: body.version ?? null })}`);
|
|
45
|
+
if (command.json)
|
|
46
|
+
return emitAgentDoor(door, body, body.ok ? 0 : 1);
|
|
47
|
+
writeLine(io.stdout, body.text);
|
|
48
|
+
return body.ok ? 0 : 1;
|
|
49
|
+
}
|
|
50
|
+
if (command.action === "timeline") {
|
|
51
|
+
const path = command.weeks !== undefined ? `/api/hits/timeline?weeks=${command.weeks}` : "/api/hits/timeline";
|
|
52
|
+
const answer = await askAgentDoor(door, { path, method: "GET", label: "hits timeline", timeoutMs: READ_DEADLINE_MS });
|
|
53
|
+
if (!answer.ok)
|
|
54
|
+
return failAgentDoor(door, TAG, answer.reason, answer.detail);
|
|
55
|
+
const body = answer.body;
|
|
56
|
+
writeLine(io.stderr, `${TAG} timeline read ${JSON.stringify({ weeks: body.weeks.length, hits: body.coverage.hits, labels: body.coverage.labels, end: body.coverage.end })}`);
|
|
57
|
+
if (command.json)
|
|
58
|
+
return emitAgentDoor(door, body);
|
|
59
|
+
writeLine(io.stdout, body.text);
|
|
60
|
+
return 0;
|
|
61
|
+
}
|
|
62
|
+
if (command.action === "coach") {
|
|
63
|
+
const answer = await askAgentDoor(door, { path: "/api/hits/coach", method: "GET", label: "hits coach", timeoutMs: READ_DEADLINE_MS });
|
|
64
|
+
if (!answer.ok)
|
|
65
|
+
return failAgentDoor(door, TAG, answer.reason, answer.detail);
|
|
66
|
+
const body = answer.body;
|
|
67
|
+
writeLine(io.stderr, `${TAG} coach read ${JSON.stringify({ alerts: body.alerts.length, rows: body.coverage.rows, pages: body.coverage.pages, end: body.coverage.end })}`);
|
|
68
|
+
if (command.json)
|
|
69
|
+
return emitAgentDoor(door, body);
|
|
70
|
+
writeLine(io.stdout, body.text);
|
|
71
|
+
return 0;
|
|
72
|
+
}
|
|
73
|
+
const asked = hitsReadQuery(command);
|
|
74
|
+
const answer = await askAgentDoor(door, { path: asked ? `/api/hits?${asked}` : "/api/hits", method: "GET", label: "hits read", timeoutMs: READ_DEADLINE_MS });
|
|
75
|
+
if (!answer.ok)
|
|
76
|
+
return failAgentDoor(door, TAG, answer.reason, answer.detail);
|
|
77
|
+
const body = answer.body;
|
|
78
|
+
writeLine(io.stderr, `${TAG} read answered ${JSON.stringify({ who: body.who, hits: body.totals.hits, rows: body.coverage.rows, pages: body.coverage.pages, end: body.coverage.end })}`);
|
|
79
|
+
if (command.json)
|
|
80
|
+
return emitAgentDoor(door, body);
|
|
81
|
+
writeLine(io.stdout, body.text);
|
|
82
|
+
return 0;
|
|
83
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { askAgentDoor, emitAgentDoor, failAgentDoor, openAgentDoor } from "./agent-door.js";
|
|
2
|
+
import { writeLine } from "./cli-io.js";
|
|
3
|
+
const TAG = "[intentions cli]";
|
|
4
|
+
const DEADLINE_MS = 55_000;
|
|
5
|
+
export async function runIntentions(command, io) {
|
|
6
|
+
let door;
|
|
7
|
+
try {
|
|
8
|
+
door = await openAgentDoor("intentions", command, io);
|
|
9
|
+
}
|
|
10
|
+
catch (error) {
|
|
11
|
+
const detail = error instanceof Error ? error.message : "this machine is not paired with Tower";
|
|
12
|
+
writeLine(io.stderr, `${TAG} refused ${JSON.stringify({ reason: "not_paired" })}`);
|
|
13
|
+
if (command.json)
|
|
14
|
+
writeLine(io.stdout, JSON.stringify({ ok: false, error: "not_paired", detail }));
|
|
15
|
+
else
|
|
16
|
+
writeLine(io.stderr, detail);
|
|
17
|
+
return 1;
|
|
18
|
+
}
|
|
19
|
+
let request;
|
|
20
|
+
if (command.action === "list") {
|
|
21
|
+
const query = new URLSearchParams();
|
|
22
|
+
if (command.status)
|
|
23
|
+
query.set("status", command.status);
|
|
24
|
+
if (command.project)
|
|
25
|
+
query.set("project", command.project);
|
|
26
|
+
if (command.limit !== undefined)
|
|
27
|
+
query.set("limit", String(command.limit));
|
|
28
|
+
const asked = query.toString();
|
|
29
|
+
request = { path: asked ? `/api/intentions?${asked}` : "/api/intentions", method: "GET", label: "intentions list" };
|
|
30
|
+
}
|
|
31
|
+
else if (command.action === "show") {
|
|
32
|
+
request = { path: `/api/intentions/${encodeURIComponent(command.id)}`, method: "GET", label: "intentions show" };
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
request = {
|
|
36
|
+
path: `/api/intentions/${encodeURIComponent(command.id)}`, method: "POST", label: `intentions ${command.action}`,
|
|
37
|
+
body: { action: command.action, ...(command.evidence ? { evidence: command.evidence } : {}), ...(command.by ? { by: command.by } : {}) },
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
const answer = await askAgentDoor(door, { ...request, timeoutMs: DEADLINE_MS });
|
|
41
|
+
if (!answer.ok)
|
|
42
|
+
return failAgentDoor(door, TAG, answer.reason, answer.detail);
|
|
43
|
+
const body = answer.body;
|
|
44
|
+
writeLine(io.stderr, `${TAG} ${command.action} answered ${JSON.stringify({ rows: body.intentions?.length ?? 1, ...(body.coverage ? { requested: body.coverage.requested, returned: body.coverage.returned, unique: body.coverage.unique, end: body.coverage.end } : {}) })}`);
|
|
45
|
+
if (command.json)
|
|
46
|
+
return emitAgentDoor(door, body);
|
|
47
|
+
writeLine(io.stdout, body.text ?? "");
|
|
48
|
+
return 0;
|
|
49
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `cockpit docs export` flag parsing (ticket 20694), split out of `local-args-tower-docs-msg.ts`,
|
|
3
|
+
* which sits at the file-size ratchet.
|
|
4
|
+
*/
|
|
5
|
+
import { optionalNonEmpty } from "./local-arg-values.js";
|
|
6
|
+
const DOCS_EXPORT_FORMATS = ["md", "pdf", "docx", "txt"];
|
|
7
|
+
/** Ticket 20694: `--format`/`--out` mean something on `export` alone, and an unknown format is refused by name. */
|
|
8
|
+
export function parseDocsExportFlags(action, values) {
|
|
9
|
+
const format = optionalNonEmpty(values.flags.get("--format"))?.toLowerCase();
|
|
10
|
+
const outPath = optionalNonEmpty(values.flags.get("--out"));
|
|
11
|
+
if (action !== "export") {
|
|
12
|
+
if (format)
|
|
13
|
+
throw new Error("--format belongs to `cockpit docs export`.");
|
|
14
|
+
if (outPath)
|
|
15
|
+
throw new Error("--out belongs to `cockpit docs export`.");
|
|
16
|
+
return {};
|
|
17
|
+
}
|
|
18
|
+
const chosen = format === "markdown" ? "md" : format === "word" ? "docx" : format === "text" ? "txt" : (format ?? "md");
|
|
19
|
+
if (!DOCS_EXPORT_FORMATS.includes(chosen)) {
|
|
20
|
+
throw new Error(`docs export --format must be one of ${DOCS_EXPORT_FORMATS.join(", ")} (Word is docx), not "${format}".`);
|
|
21
|
+
}
|
|
22
|
+
return { exportFormat: chosen, ...(outPath ? { outPath } : {}) };
|
|
23
|
+
}
|
|
@@ -301,6 +301,14 @@ export function parseTeamArgs(args) {
|
|
|
301
301
|
}
|
|
302
302
|
return { kind: "team", action: `github-login-${operation}`, personRef, githubLogin, ...common };
|
|
303
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
|
+
}
|
|
304
312
|
if (verb === "device") {
|
|
305
313
|
return parseTeamDeviceArgs(rest, values.flags, common);
|
|
306
314
|
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `cockpit brain ladder` argument parsing (BLI-19250): Edward's call ladder.
|
|
3
|
+
*
|
|
4
|
+
* cockpit brain ladder show [--json]
|
|
5
|
+
* cockpit brain ladder promote <class> [--anyway] [--note <text>] [--json]
|
|
6
|
+
* cockpit brain ladder demote <class> [--note <text>] [--json]
|
|
7
|
+
* cockpit brain ladder predict "<question>" [--context <text>] [--asker <label>] [--source-kind <k>] [--source-ref <ref>] [--json]
|
|
8
|
+
* cockpit brain ladder answer "<what he answered>" (--prediction <uuid> | --source-kind <k> --source-ref <ref>) [--json]
|
|
9
|
+
*
|
|
10
|
+
* The class names are checked by the door against the closed list (`lib/edward-predictor/classes.ts`), not copied here.
|
|
11
|
+
*/
|
|
12
|
+
import { optionalNonEmpty, optionalUrl, parseNamedArgs } from "./local-arg-values.js";
|
|
13
|
+
const VERBS = ["show", "promote", "demote", "predict", "answer"];
|
|
14
|
+
const SOURCE_KIND = /^[a-z][a-z0-9_]{0,31}$/;
|
|
15
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
16
|
+
export function parseBrainLadderArgs(args) {
|
|
17
|
+
const values = parseNamedArgs(args, {
|
|
18
|
+
allowedFlags: ["--home", "--dashboard-url", "--json", "--anyway", "--note", "--context", "--asker", "--source-kind", "--source-ref", "--prediction"],
|
|
19
|
+
valueFlags: ["--home", "--dashboard-url", "--note", "--context", "--asker", "--source-kind", "--source-ref", "--prediction"],
|
|
20
|
+
});
|
|
21
|
+
const [ladderWord, verb, ...rest] = values.positionals;
|
|
22
|
+
if (ladderWord !== "ladder" || !VERBS.includes(verb ?? "")) {
|
|
23
|
+
throw new Error(`brain ladder takes one verb: ${VERBS.join(", ")}. Example: \`cockpit brain ladder show\`.`);
|
|
24
|
+
}
|
|
25
|
+
const shape = { verb: verb };
|
|
26
|
+
const flag = (name) => optionalNonEmpty(values.flags.get(name))?.trim();
|
|
27
|
+
const note = flag("--note");
|
|
28
|
+
if (note && note.length > 300)
|
|
29
|
+
throw new Error("--note is at most 300 characters.");
|
|
30
|
+
const sourceKind = flag("--source-kind");
|
|
31
|
+
if (sourceKind && !SOURCE_KIND.test(sourceKind))
|
|
32
|
+
throw new Error("--source-kind wants a lowercase label like approval_card.");
|
|
33
|
+
const prediction = flag("--prediction");
|
|
34
|
+
if (prediction && !UUID.test(prediction))
|
|
35
|
+
throw new Error("--prediction wants a prediction id (a UUID).");
|
|
36
|
+
const only = (allowed) => {
|
|
37
|
+
for (const name of ["--anyway", "--note", "--context", "--asker", "--source-kind", "--source-ref", "--prediction"]) {
|
|
38
|
+
const given = values.booleans.has(name) || values.flags.has(name);
|
|
39
|
+
if (given && !allowed.includes(name))
|
|
40
|
+
throw new Error(`${name} does not belong to brain ladder ${shape.verb}.`);
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
switch (shape.verb) {
|
|
44
|
+
case "show":
|
|
45
|
+
if (rest.length)
|
|
46
|
+
throw new Error("brain ladder show takes no arguments.");
|
|
47
|
+
only([]);
|
|
48
|
+
break;
|
|
49
|
+
case "promote":
|
|
50
|
+
case "demote":
|
|
51
|
+
if (rest.length !== 1)
|
|
52
|
+
throw new Error(`brain ladder ${shape.verb} takes one class name. Example: \`cockpit brain ladder ${shape.verb} merge\`.`);
|
|
53
|
+
only(shape.verb === "promote" ? ["--anyway", "--note"] : ["--note"]);
|
|
54
|
+
shape.className = rest[0];
|
|
55
|
+
if (values.booleans.has("--anyway"))
|
|
56
|
+
shape.anyway = true;
|
|
57
|
+
if (note)
|
|
58
|
+
shape.note = note;
|
|
59
|
+
break;
|
|
60
|
+
case "predict":
|
|
61
|
+
if (rest.length !== 1)
|
|
62
|
+
throw new Error('brain ladder predict takes the question in quotes. Example: `cockpit brain ladder predict "Merge #1820 now?"`.');
|
|
63
|
+
only(["--context", "--asker", "--source-kind", "--source-ref"]);
|
|
64
|
+
shape.text = rest[0];
|
|
65
|
+
if (flag("--context"))
|
|
66
|
+
shape.context = flag("--context");
|
|
67
|
+
if (flag("--asker"))
|
|
68
|
+
shape.asker = flag("--asker");
|
|
69
|
+
if (sourceKind)
|
|
70
|
+
shape.sourceKind = sourceKind;
|
|
71
|
+
if (flag("--source-ref"))
|
|
72
|
+
shape.sourceRef = flag("--source-ref");
|
|
73
|
+
break;
|
|
74
|
+
case "answer":
|
|
75
|
+
if (rest.length !== 1)
|
|
76
|
+
throw new Error('brain ladder answer takes what he answered, in quotes. Example: `cockpit brain ladder answer "Yes, merge it" --prediction <id>`.');
|
|
77
|
+
only(["--prediction", "--source-kind", "--source-ref"]);
|
|
78
|
+
shape.text = rest[0];
|
|
79
|
+
if (prediction)
|
|
80
|
+
shape.predictionId = prediction;
|
|
81
|
+
if (sourceKind)
|
|
82
|
+
shape.sourceKind = sourceKind;
|
|
83
|
+
if (flag("--source-ref"))
|
|
84
|
+
shape.sourceRef = flag("--source-ref");
|
|
85
|
+
if (!shape.predictionId && !(shape.sourceKind && shape.sourceRef))
|
|
86
|
+
throw new Error("brain ladder answer needs --prediction <id>, or --source-kind and --source-ref.");
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
return { ladder: shape, dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")), homeDir: flag("--home"), json: values.booleans.has("--json") };
|
|
90
|
+
}
|
|
@@ -9,16 +9,26 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { LOG_PROJECT_PATTERN } from "@bli-cockpit/telemetry-core/log-digest";
|
|
11
11
|
import { optionalNonEmpty, optionalUrl, parseNamedArgs } from "./local-arg-values.js";
|
|
12
|
+
import { parseBrainLadderArgs } from "./local-args-tower-brain-ladder.js";
|
|
12
13
|
/** The statuses the queue's door accepts for `--show`; the migration's CHECK constraint is the other copy. */
|
|
13
14
|
export const BRAIN_SHOW_STATUSES = ["open", "running", "needs_human", "fixed", "not_actionable", "cleared"];
|
|
14
15
|
export function parseBrainArgs(args) {
|
|
16
|
+
// BLI-19250: `brain ladder ...` is Edward's call ladder; it has its own verbs and flags.
|
|
17
|
+
// The census spells these `brain ladder-show` (one action word), so that spelling is accepted beside `brain ladder show`.
|
|
18
|
+
const first = args.find((arg) => !arg.startsWith("--"));
|
|
19
|
+
if (first === "ladder" || /^ladder-[a-z]+$/.test(first ?? "")) {
|
|
20
|
+
const hyphen = first !== "ladder";
|
|
21
|
+
const split = hyphen ? args.flatMap((arg) => (arg === first ? ["ladder", first.slice("ladder-".length)] : [arg])) : args;
|
|
22
|
+
const parsed = parseBrainLadderArgs(split);
|
|
23
|
+
return { kind: "brain", action: `ladder-${parsed.ladder.verb}`, ladder: parsed.ladder, dashboardUrl: parsed.dashboardUrl, homeDir: parsed.homeDir, json: parsed.json };
|
|
24
|
+
}
|
|
15
25
|
const values = parseNamedArgs(args, {
|
|
16
26
|
allowedFlags: ["--project", "--show", "--home", "--dashboard-url", "--json", "--all-needs-human", "--reason"],
|
|
17
27
|
valueFlags: ["--project", "--show", "--home", "--dashboard-url", "--reason"],
|
|
18
28
|
});
|
|
19
29
|
const action = values.positionals[0];
|
|
20
30
|
if ((action !== "status" && action !== "retry") || (action === "status" && values.positionals.length > 1)) {
|
|
21
|
-
throw new Error("brain takes one verb: `status` or `
|
|
31
|
+
throw new Error("brain takes one verb: `status`, `retry` or `ladder`. Example: `cockpit brain status` or `cockpit brain status --project pearl-portal`.");
|
|
22
32
|
}
|
|
23
33
|
const project = optionalNonEmpty(values.flags.get("--project"));
|
|
24
34
|
if (project && !LOG_PROJECT_PATTERN.test(project))
|
|
@@ -7,13 +7,14 @@
|
|
|
7
7
|
* added both without stretching either existing family's own doc comment.
|
|
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
12
|
// BLI-10450: `read` moved to its own branch in `parseDocsDocumentReference` —
|
|
12
13
|
// it takes SEVERAL references now, so it no longer shares the "exactly one"
|
|
13
14
|
// rule `update`/`link` still enforce. BLI‑10555: `delete` takes several too
|
|
14
15
|
// (the same door, called once per ref), so it joined `read`'s branch rather
|
|
15
16
|
// than this set.
|
|
16
|
-
const DOCS_ACTIONS_NEEDING_A_DOC = new Set(["update", "link"]);
|
|
17
|
+
const DOCS_ACTIONS_NEEDING_A_DOC = new Set(["update", "link", "export"]);
|
|
17
18
|
export function parseDocsArgs(args) {
|
|
18
19
|
const values = parseNamedArgs(args, {
|
|
19
20
|
allowedFlags: [
|
|
@@ -33,6 +34,8 @@ export function parseDocsArgs(args) {
|
|
|
33
34
|
"--bodies",
|
|
34
35
|
"--section",
|
|
35
36
|
"--offset",
|
|
37
|
+
"--format",
|
|
38
|
+
"--out",
|
|
36
39
|
"--json",
|
|
37
40
|
],
|
|
38
41
|
valueFlags: [
|
|
@@ -46,6 +49,8 @@ export function parseDocsArgs(args) {
|
|
|
46
49
|
"--limit",
|
|
47
50
|
"--section",
|
|
48
51
|
"--offset",
|
|
52
|
+
"--format",
|
|
53
|
+
"--out",
|
|
49
54
|
],
|
|
50
55
|
});
|
|
51
56
|
const { action, rest } = parseDocsAction(values.positionals);
|
|
@@ -111,7 +116,7 @@ function parseDocsAction(positionals) {
|
|
|
111
116
|
const first = positionals[0];
|
|
112
117
|
const action = (first === undefined ? "list" : first);
|
|
113
118
|
if (!DOCS_ACTIONS.has(action)) {
|
|
114
|
-
throw new Error(`Unknown docs command: ${first}. Try list, tree, read, create, update, delete, link,
|
|
119
|
+
throw new Error(`Unknown docs command: ${first}. Try list, tree, read, create, update, delete, link, sync-status, or export.`);
|
|
115
120
|
}
|
|
116
121
|
return { action, rest: positionals.slice(first === undefined ? 0 : 1) };
|
|
117
122
|
}
|
|
@@ -234,6 +239,7 @@ function buildDocsCommand(values, action, docRef, docRefs, visibility, clearPare
|
|
|
234
239
|
...(offset === undefined ? {} : { offset }),
|
|
235
240
|
...(link.notionPage ? { notionPage: link.notionPage } : {}),
|
|
236
241
|
...(link.clearLink ? { clearLink: true } : {}),
|
|
242
|
+
...parseDocsExportFlags(action, values),
|
|
237
243
|
json: values.booleans.has("--json"),
|
|
238
244
|
};
|
|
239
245
|
}
|