@bli-cockpit/cli 0.2.57 → 0.2.59
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/browser-open.js +88 -0
- package/dist/commands/docs.js +27 -6
- package/dist/commands/doctor-report.js +17 -1
- package/dist/commands/doctor.js +12 -2
- package/dist/commands/heartbeat.js +65 -1
- package/dist/commands/issue-contracts.js +99 -0
- package/dist/commands/issue-write.js +129 -0
- package/dist/commands/issue.js +189 -0
- package/dist/commands/jarvis-answer-envelope.js +80 -0
- package/dist/commands/jarvis-turn.js +16 -1
- package/dist/commands/jarvis.js +3 -0
- package/dist/commands/local-args-collector-setup.js +17 -0
- package/dist/commands/local-args-tower-admin.js +12 -2
- package/dist/commands/local-args-tower-docs-msg.js +95 -6
- package/dist/commands/local-args-tower-search.js +50 -0
- package/dist/commands/local-args-tower-work.js +178 -0
- package/dist/commands/local-args-tower.js +7 -1
- package/dist/commands/local-args.js +29 -14
- package/dist/commands/local-command-shapes.js +12 -0
- package/dist/commands/local-help-commands.js +643 -0
- package/dist/commands/local-help.js +12 -551
- package/dist/commands/local.js +9 -0
- package/dist/commands/login.js +91 -8
- package/dist/commands/memory-install-claude.js +35 -15
- package/dist/commands/memory-install-codex-hooks.js +200 -0
- package/dist/commands/memory-install-codex.js +12 -2
- package/dist/commands/memory-install-config.js +140 -0
- package/dist/commands/memory-install-receipt.js +222 -0
- package/dist/commands/memory-install-report.js +113 -0
- package/dist/commands/memory-install.js +99 -276
- package/dist/commands/msg.js +85 -2
- package/dist/commands/notes-door.js +120 -0
- package/dist/commands/notes-reads.js +134 -0
- package/dist/commands/notes-writes.js +208 -0
- package/dist/commands/notes.js +16 -442
- package/dist/commands/onboard-completion.js +47 -0
- package/dist/commands/onboard-setup.js +82 -2
- package/dist/commands/ops-render-memory.js +76 -0
- package/dist/commands/ops-render.js +13 -1
- package/dist/commands/ops.js +65 -3
- package/dist/commands/project.js +38 -0
- package/dist/commands/public-root.js +1 -1
- package/dist/commands/search.js +122 -0
- package/dist/commands/setup-receipt-lines.js +71 -0
- package/dist/commands/setup-receipt.js +241 -0
- package/dist/commands/status.js +20 -1
- package/dist/local-state-pairing-code.js +200 -0
- package/dist/local-state.js +6 -0
- package/dist/repo-identity-fingerprint.js +88 -0
- package/dist/repo-identity-git.js +76 -0
- package/dist/repo-identity-linked-worktrees.js +81 -0
- package/dist/repo-identity.js +5 -222
- package/package.json +7 -7
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `cockpit issue` — Tower's issue tracker, typed (BLI-3716).
|
|
3
|
+
*
|
|
4
|
+
* Seven verbs over `/api/work/**`, the same doors the browser uses. Every
|
|
5
|
+
* one of those routes already accepted the collector device token
|
|
6
|
+
* (`resolveCaller({ allowDeviceToken: true })`, BLI-3703), so this command
|
|
7
|
+
* is a terminal in front of an existing door, not a new one.
|
|
8
|
+
*
|
|
9
|
+
* Three references a person actually types, resolved here or by the door:
|
|
10
|
+
* - an ISSUE is `BLI-3654` or a uuid. The SERVER resolves it
|
|
11
|
+
* (`lib/work/issue-ref.ts`), so `show`/`move`/`comment`/`history` cost one
|
|
12
|
+
* request, not two.
|
|
13
|
+
* - a PROJECT is its name or a uuid, resolved by `issue-contracts.ts`
|
|
14
|
+
* against the same `GET /api/work/projects` list `cockpit project list`
|
|
15
|
+
* prints — exact after case-folding, never fuzzy.
|
|
16
|
+
* - a PARENT is another issue reference, resolved with one `GET` so the
|
|
17
|
+
* create/patch body carries the uuid its schema demands.
|
|
18
|
+
*
|
|
19
|
+
* This file is the router plus the READ verbs (`list`, `show`, `history`);
|
|
20
|
+
* `issue-write.ts` holds `create`, `update`, `move` and `comment`, and
|
|
21
|
+
* `issue-contracts.ts` the shapes and resolvers both halves share.
|
|
22
|
+
*
|
|
23
|
+
* Bodies never travel on argv: a description or a comment comes from stdin
|
|
24
|
+
* or `--file`. A refusal keeps the door's own `reason` label verbatim
|
|
25
|
+
* (`agent-door.ts`) — `issue_not_found_or_unreadable`, `invalid_state`,
|
|
26
|
+
* `needs_rls_client`, `comment_too_long`, and so on.
|
|
27
|
+
*/
|
|
28
|
+
import { askAgentDoor, emitAgentDoor, failAgentDoor, openAgentDoor, } from "./agent-door.js";
|
|
29
|
+
import { writeLine } from "./cli-io.js";
|
|
30
|
+
import { READ_DEADLINE_MS, TAG, resolveProjectId, } from "./issue-contracts.js";
|
|
31
|
+
import { commentIssue, createIssue, moveIssue, updateIssue } from "./issue-write.js";
|
|
32
|
+
export async function runIssue(command, io) {
|
|
33
|
+
const door = await openAgentDoor("issue", command, io);
|
|
34
|
+
switch (command.action) {
|
|
35
|
+
case "list":
|
|
36
|
+
return listIssues(command, door);
|
|
37
|
+
case "show":
|
|
38
|
+
return showIssue(command, door);
|
|
39
|
+
case "create":
|
|
40
|
+
return createIssue(command, door);
|
|
41
|
+
case "update":
|
|
42
|
+
return updateIssue(command, door);
|
|
43
|
+
case "move":
|
|
44
|
+
return moveIssue(command, door);
|
|
45
|
+
case "comment":
|
|
46
|
+
return commentIssue(command, door);
|
|
47
|
+
case "history":
|
|
48
|
+
return issueHistory(command, door);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function issueLine(issue) {
|
|
52
|
+
return `${issue.identifier.padEnd(9)} ${issue.state.padEnd(11)} ${issue.title}`;
|
|
53
|
+
}
|
|
54
|
+
async function listIssues(command, door) {
|
|
55
|
+
const query = new URLSearchParams();
|
|
56
|
+
if (command.stateFilter)
|
|
57
|
+
query.set("state", command.stateFilter);
|
|
58
|
+
if (command.assignee)
|
|
59
|
+
query.set("assignee_id", command.assignee);
|
|
60
|
+
if (command.limit !== undefined)
|
|
61
|
+
query.set("limit", String(command.limit));
|
|
62
|
+
if (command.project) {
|
|
63
|
+
const resolved = await resolveProjectId(door, command.project);
|
|
64
|
+
if (resolved.status === "list_failed")
|
|
65
|
+
return failAgentDoor(door, TAG, resolved.reason, resolved.detail);
|
|
66
|
+
if (resolved.status === "not_found") {
|
|
67
|
+
return failAgentDoor(door, TAG, "project_not_found_or_unreadable", `No project here is called "${command.project}" — run \`cockpit project list\` for the names.`);
|
|
68
|
+
}
|
|
69
|
+
query.set("project_id", resolved.id);
|
|
70
|
+
}
|
|
71
|
+
const suffix = query.toString();
|
|
72
|
+
const answer = await askAgentDoor(door, {
|
|
73
|
+
path: `/api/work/issues${suffix ? `?${suffix}` : ""}`,
|
|
74
|
+
method: "GET",
|
|
75
|
+
label: "issue list",
|
|
76
|
+
timeoutMs: READ_DEADLINE_MS,
|
|
77
|
+
});
|
|
78
|
+
if (!answer.ok)
|
|
79
|
+
return failAgentDoor(door, TAG, answer.reason, answer.detail);
|
|
80
|
+
const issues = answer.body.issues ?? [];
|
|
81
|
+
if (door.json)
|
|
82
|
+
return emitAgentDoor(door, { ok: true, issues });
|
|
83
|
+
if (issues.length === 0) {
|
|
84
|
+
writeLine(door.io.stdout, "No issues match that.");
|
|
85
|
+
return 0;
|
|
86
|
+
}
|
|
87
|
+
for (const issue of issues)
|
|
88
|
+
writeLine(door.io.stdout, issueLine(issue));
|
|
89
|
+
writeLine(door.io.stdout, "");
|
|
90
|
+
writeLine(door.io.stdout, `${issues.length} issue(s).`);
|
|
91
|
+
return 0;
|
|
92
|
+
}
|
|
93
|
+
async function showIssue(command, door) {
|
|
94
|
+
const ref = command.issueRef ?? "";
|
|
95
|
+
const answer = await askAgentDoor(door, {
|
|
96
|
+
path: `/api/work/issues/${encodeURIComponent(ref)}`,
|
|
97
|
+
method: "GET",
|
|
98
|
+
label: "issue show",
|
|
99
|
+
timeoutMs: READ_DEADLINE_MS,
|
|
100
|
+
});
|
|
101
|
+
if (!answer.ok)
|
|
102
|
+
return failAgentDoor(door, TAG, answer.reason, answer.detail);
|
|
103
|
+
const issue = answer.body.issue;
|
|
104
|
+
if (!issue) {
|
|
105
|
+
return failAgentDoor(door, TAG, "issue_not_found_or_unreadable", `That issue does not exist, or you cannot read it: ${ref}`);
|
|
106
|
+
}
|
|
107
|
+
// A comment read that fails does NOT fail the whole `show` — the issue is
|
|
108
|
+
// already in hand, and a named gap beats losing what was read.
|
|
109
|
+
const commentsAnswer = await askAgentDoor(door, {
|
|
110
|
+
path: `/api/work/issues/${encodeURIComponent(issue.id)}/comments`,
|
|
111
|
+
method: "GET",
|
|
112
|
+
label: "issue show comments",
|
|
113
|
+
timeoutMs: READ_DEADLINE_MS,
|
|
114
|
+
});
|
|
115
|
+
const comments = commentsAnswer.ok
|
|
116
|
+
? (commentsAnswer.body.comments ?? [])
|
|
117
|
+
: [];
|
|
118
|
+
if (!commentsAnswer.ok) {
|
|
119
|
+
writeLine(door.io.stderr, `${TAG} comments unread ${JSON.stringify({ reason: commentsAnswer.reason, issue_id: issue.id })}`);
|
|
120
|
+
}
|
|
121
|
+
if (door.json) {
|
|
122
|
+
return emitAgentDoor(door, {
|
|
123
|
+
ok: true,
|
|
124
|
+
issue,
|
|
125
|
+
comments,
|
|
126
|
+
...(commentsAnswer.ok ? {} : { comments_unread_reason: commentsAnswer.reason }),
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
writeLine(door.io.stdout, `${issue.identifier} ${issue.title}`);
|
|
130
|
+
writeLine(door.io.stdout, `${issue.state} · priority ${issue.priority ?? 0} · assignee ${issue.assignee_id ?? "(nobody)"} · updated ${issue.updated_at}`);
|
|
131
|
+
writeLine(door.io.stdout, `id ${issue.id}${issue.parent_id ? ` · parent ${issue.parent_id}` : ""}`);
|
|
132
|
+
writeLine(door.io.stdout, "");
|
|
133
|
+
writeLine(door.io.stdout, issue.description ?? "(no description)");
|
|
134
|
+
if (comments.length > 0) {
|
|
135
|
+
writeLine(door.io.stdout, "");
|
|
136
|
+
writeLine(door.io.stdout, `--- ${comments.length} comment(s) ---`);
|
|
137
|
+
for (const comment of comments) {
|
|
138
|
+
writeLine(door.io.stdout, "");
|
|
139
|
+
writeLine(door.io.stdout, `[${comment.created_at}] ${comment.author_name ?? comment.author_id ?? "unknown"}`);
|
|
140
|
+
writeLine(door.io.stdout, comment.body_markdown);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
else if (commentsAnswer.ok) {
|
|
144
|
+
writeLine(door.io.stdout, "");
|
|
145
|
+
writeLine(door.io.stdout, "No comments.");
|
|
146
|
+
}
|
|
147
|
+
return 0;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* What a history row actually says. A row imported from Linear's issue
|
|
151
|
+
* TIMESTAMPS (created/started/completed/canceled) carries no state names at
|
|
152
|
+
* all: the reader refuses to name a workflow state it cannot prove
|
|
153
|
+
* (`linear-reader.ts` `buildSyntheticTransitions` — "naming a stale workflow
|
|
154
|
+
* state would be a small invented fact"), and only the state CATEGORY it did
|
|
155
|
+
* know is dropped on the way into `work_issue_history`. Printing
|
|
156
|
+
* "(none) -> (none)" is honest and useless; this says which it is, so nobody
|
|
157
|
+
* reads a real gap as a broken command.
|
|
158
|
+
*/
|
|
159
|
+
function historyChange(row) {
|
|
160
|
+
if (row.from_value === null && row.to_value === null) {
|
|
161
|
+
return `(no state names recorded — imported from ${row.source})`;
|
|
162
|
+
}
|
|
163
|
+
return `${row.from_value ?? "(none)"} -> ${row.to_value ?? "(none)"}`;
|
|
164
|
+
}
|
|
165
|
+
async function issueHistory(command, door) {
|
|
166
|
+
const ref = command.issueRef ?? "";
|
|
167
|
+
const query = command.limit === undefined ? "" : `?limit=${command.limit}`;
|
|
168
|
+
const answer = await askAgentDoor(door, {
|
|
169
|
+
path: `/api/work/issues/${encodeURIComponent(ref)}/history${query}`,
|
|
170
|
+
method: "GET",
|
|
171
|
+
label: "issue history",
|
|
172
|
+
timeoutMs: READ_DEADLINE_MS,
|
|
173
|
+
});
|
|
174
|
+
if (!answer.ok)
|
|
175
|
+
return failAgentDoor(door, TAG, answer.reason, answer.detail);
|
|
176
|
+
const history = answer.body.history ?? [];
|
|
177
|
+
if (door.json)
|
|
178
|
+
return emitAgentDoor(door, { ok: true, history });
|
|
179
|
+
if (history.length === 0) {
|
|
180
|
+
writeLine(door.io.stdout, `No recorded changes for ${ref}.`);
|
|
181
|
+
return 0;
|
|
182
|
+
}
|
|
183
|
+
for (const row of history) {
|
|
184
|
+
writeLine(door.io.stdout, `${row.occurred_at} ${row.change_type.padEnd(9)} ${historyChange(row)} ${row.actor_name ?? ""}`.trimEnd());
|
|
185
|
+
}
|
|
186
|
+
writeLine(door.io.stdout, "");
|
|
187
|
+
writeLine(door.io.stdout, `${history.length} change(s).`);
|
|
188
|
+
return 0;
|
|
189
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ONE shape a JARVIS answer wears when nobody is reading it with their
|
|
3
|
+
* eyes (BLI-3732).
|
|
4
|
+
*
|
|
5
|
+
* `cockpit jarvis --json` and the `bli-tower` MCP server's `jarvis_ask` reach
|
|
6
|
+
* the same door (`POST /api/jarvis/cli`) with the same device token and get
|
|
7
|
+
* the same reply body back. Before this file they described that body two
|
|
8
|
+
* different ways, so a script and an agent read two contracts for one turn.
|
|
9
|
+
* Now both build this envelope, and the five fields a consumer actually asks
|
|
10
|
+
* for — the answer, where it came from, which turn it was, which conversation
|
|
11
|
+
* it belongs to, and whether anything about it was second-rate — have one
|
|
12
|
+
* spelling each.
|
|
13
|
+
*
|
|
14
|
+
* ## Where the second copy is, and why
|
|
15
|
+
*
|
|
16
|
+
* `packages/bli-cockpit-mcp/src/jarvis-answer-envelope.ts` is a deliberate
|
|
17
|
+
* copy of this file. That package declares no dependency on this one — see
|
|
18
|
+
* `packages/bli-cockpit-mcp/src/agent-door-session.ts`, whose own header makes
|
|
19
|
+
* the same call for the same reason — so the shape is duplicated rather than
|
|
20
|
+
* imported. Both copies are pinned by a test on the same literal key list
|
|
21
|
+
* (`JARVIS_ANSWER_ENVELOPE_KEYS` below), so a field added on one side and not
|
|
22
|
+
* the other fails a suite instead of drifting quietly.
|
|
23
|
+
*
|
|
24
|
+
* ## What `--json` still prints
|
|
25
|
+
*
|
|
26
|
+
* These keys are ADDED to what `cockpit jarvis --json` already printed; the
|
|
27
|
+
* older `reply` / `thread` / `traceId` / `model` / `trace` / `latency` /
|
|
28
|
+
* `clientLatency` keys are untouched, because scripts already read them and a
|
|
29
|
+
* one-contract ticket that broke the contract would be a joke.
|
|
30
|
+
*/
|
|
31
|
+
/**
|
|
32
|
+
* Every key of the envelope, in the order it is written. The literal list IS
|
|
33
|
+
* the contract: both copies assert against it, so a drift is a red suite.
|
|
34
|
+
*/
|
|
35
|
+
export const JARVIS_ANSWER_ENVELOPE_KEYS = [
|
|
36
|
+
"ok",
|
|
37
|
+
"answer",
|
|
38
|
+
"sources",
|
|
39
|
+
"turn_id",
|
|
40
|
+
"thread_id",
|
|
41
|
+
"trace_thread_id",
|
|
42
|
+
"degraded",
|
|
43
|
+
"degraded_reasons",
|
|
44
|
+
];
|
|
45
|
+
/** A `Source:` line, as the grounding gate renders it into the answer. */
|
|
46
|
+
const SOURCE_LINE = /^\s*Source:\s*\S/;
|
|
47
|
+
/**
|
|
48
|
+
* The `Source:` lines inside an answer. Pure string work on what the server
|
|
49
|
+
* already sent — this side never decides what a source IS, it only finds the
|
|
50
|
+
* lines the server wrote, so a new citation kind needs no change here.
|
|
51
|
+
*/
|
|
52
|
+
export function extractSourceLines(answer) {
|
|
53
|
+
return answer
|
|
54
|
+
.split("\n")
|
|
55
|
+
.map((line) => line.trim())
|
|
56
|
+
.filter((line) => SOURCE_LINE.test(line));
|
|
57
|
+
}
|
|
58
|
+
export function buildJarvisAnswerEnvelope(input) {
|
|
59
|
+
const reasons = [];
|
|
60
|
+
if (input.modelFallback === true)
|
|
61
|
+
reasons.push("model_fallback");
|
|
62
|
+
if (input.revised === true)
|
|
63
|
+
reasons.push("answer_revised");
|
|
64
|
+
if ((input.trace ?? []).some((step) => step?.status === "failed")) {
|
|
65
|
+
reasons.push("tool_step_failed");
|
|
66
|
+
}
|
|
67
|
+
const turnId = input.traceId ?? null;
|
|
68
|
+
if (!turnId)
|
|
69
|
+
reasons.push("no_turn_id");
|
|
70
|
+
return {
|
|
71
|
+
ok: true,
|
|
72
|
+
answer: input.reply,
|
|
73
|
+
sources: extractSourceLines(input.reply),
|
|
74
|
+
turn_id: turnId,
|
|
75
|
+
thread_id: input.thread ?? null,
|
|
76
|
+
trace_thread_id: input.traceThread ?? null,
|
|
77
|
+
degraded: reasons.length > 0,
|
|
78
|
+
degraded_reasons: reasons,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* `jarvis-render.ts` — imported one direction only.
|
|
8
8
|
*/
|
|
9
9
|
import { colorEnabled, dim, writeLine } from "./cli-io.js";
|
|
10
|
+
import { buildJarvisAnswerEnvelope } from "./jarvis-answer-envelope.js";
|
|
10
11
|
import { readAttachedImage } from "./jarvis-attachment.js";
|
|
11
12
|
import { rememberTurnTrace } from "./jarvis-trace.js";
|
|
12
13
|
import { towerFailureDetail, towerJsonRequest, towerRequest } from "../tower-client.js";
|
|
@@ -131,7 +132,21 @@ export async function sendOneTurn(context, prompt, io) {
|
|
|
131
132
|
await rememberTurnTrace({ traceId: body.traceId ?? null, threadId: body.traceThread ?? null }, io, context.command.homeDir);
|
|
132
133
|
if (context.command.json) {
|
|
133
134
|
writeLine(io.stdout, JSON.stringify({
|
|
134
|
-
|
|
135
|
+
// BLI-3732: the shared envelope FIRST — `answer`, `sources`,
|
|
136
|
+
// `turn_id`, `thread_id`, `trace_thread_id`, `degraded`,
|
|
137
|
+
// `degraded_reasons` — so a script here and an agent on the
|
|
138
|
+
// `jarvis_ask` MCP tool read one contract for one turn. The keys
|
|
139
|
+
// below it are the ones this command has always printed and are
|
|
140
|
+
// untouched; `reply` and `answer` are the same string.
|
|
141
|
+
...buildJarvisAnswerEnvelope({
|
|
142
|
+
reply: body.reply,
|
|
143
|
+
thread: body.thread ?? context.command.thread,
|
|
144
|
+
traceId: body.traceId,
|
|
145
|
+
traceThread: body.traceThread,
|
|
146
|
+
modelFallback: body.model?.fallback,
|
|
147
|
+
revised: body.revised,
|
|
148
|
+
trace,
|
|
149
|
+
}),
|
|
135
150
|
reply: body.reply,
|
|
136
151
|
thread: body.thread ?? context.command.thread,
|
|
137
152
|
model: body.model ?? null,
|
package/dist/commands/jarvis.js
CHANGED
|
@@ -12,6 +12,9 @@
|
|
|
12
12
|
* - `jarvis-contracts.ts` — the parsed `cockpit jarvis` command type, and the
|
|
13
13
|
* loose reply shapes read off `/api/jarvis/cli`, `--threads` and
|
|
14
14
|
* `--history`.
|
|
15
|
+
* - `jarvis-answer-envelope.ts` — the shape `--json` prints (BLI-3732), which
|
|
16
|
+
* is the same shape the `bli-tower` MCP server's `jarvis_ask` returns, so a
|
|
17
|
+
* script and an agent read one contract for one turn.
|
|
15
18
|
* - `jarvis-turn.ts` — the turn engine: `sendOneTurn` (attach, request,
|
|
16
19
|
* stream, settle, print, log — the one send for both a one-shot invocation
|
|
17
20
|
* and the interactive loop below) and `readHistory` (`--threads` /
|
|
@@ -31,6 +31,11 @@ function parseOnboardLikeArgs(args, command) {
|
|
|
31
31
|
"--max-depth",
|
|
32
32
|
"--max-repos",
|
|
33
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",
|
|
34
39
|
],
|
|
35
40
|
valueFlags: [
|
|
36
41
|
"--home",
|
|
@@ -45,6 +50,7 @@ function parseOnboardLikeArgs(args, command) {
|
|
|
45
50
|
"--timeout-ms",
|
|
46
51
|
"--max-depth",
|
|
47
52
|
"--max-repos",
|
|
53
|
+
"--pair",
|
|
48
54
|
],
|
|
49
55
|
});
|
|
50
56
|
assertNoPositionals(values.positionals, command);
|
|
@@ -65,6 +71,9 @@ function parseOnboardLikeArgs(args, command) {
|
|
|
65
71
|
maxDepth: optionalPositiveInteger(values.flags.get("--max-depth"), "--max-depth"),
|
|
66
72
|
maxRepos: optionalPositiveInteger(values.flags.get("--max-repos"), "--max-repos"),
|
|
67
73
|
allowHomeRoot: values.booleans.has("--allow-home-root"),
|
|
74
|
+
noBrowser: values.booleans.has("--no-browser"),
|
|
75
|
+
legacyPair: values.booleans.has("--legacy-pair"),
|
|
76
|
+
pairCode: optionalNonEmpty(values.flags.get("--pair")),
|
|
68
77
|
};
|
|
69
78
|
}
|
|
70
79
|
export function parseOnboardArgs(args) {
|
|
@@ -210,6 +219,10 @@ export function parseLoginArgs(args) {
|
|
|
210
219
|
"--no-auth",
|
|
211
220
|
"--poll-interval-ms",
|
|
212
221
|
"--timeout-ms",
|
|
222
|
+
// BLI-3731, the one-sign-in ceremony.
|
|
223
|
+
"--pair",
|
|
224
|
+
"--no-browser",
|
|
225
|
+
"--legacy-pair",
|
|
213
226
|
],
|
|
214
227
|
valueFlags: [
|
|
215
228
|
"--home",
|
|
@@ -218,6 +231,7 @@ export function parseLoginArgs(args) {
|
|
|
218
231
|
"--device-name",
|
|
219
232
|
"--poll-interval-ms",
|
|
220
233
|
"--timeout-ms",
|
|
234
|
+
"--pair",
|
|
221
235
|
],
|
|
222
236
|
});
|
|
223
237
|
assertNoPositionals(values.positionals, "login");
|
|
@@ -231,6 +245,9 @@ export function parseLoginArgs(args) {
|
|
|
231
245
|
noAuth: values.booleans.has("--no-auth"),
|
|
232
246
|
pollIntervalMs: optionalPositiveInteger(values.flags.get("--poll-interval-ms"), "--poll-interval-ms"),
|
|
233
247
|
timeoutMs: optionalPositiveInteger(values.flags.get("--timeout-ms"), "--timeout-ms"),
|
|
248
|
+
pairCode: optionalNonEmpty(values.flags.get("--pair")),
|
|
249
|
+
noBrowser: values.booleans.has("--no-browser"),
|
|
250
|
+
legacyPair: values.booleans.has("--legacy-pair"),
|
|
234
251
|
};
|
|
235
252
|
}
|
|
236
253
|
export function parseLogoutArgs(args) {
|
|
@@ -48,7 +48,7 @@ export function parseScoutArgs(args) {
|
|
|
48
48
|
return { kind: "scout", action: rawAction, experimentRef, ...base };
|
|
49
49
|
}
|
|
50
50
|
/**
|
|
51
|
-
* `cockpit ops status [--job <id>] [--skips]` and
|
|
51
|
+
* `cockpit ops status [--job <id>] [--skips] [--memory [--memory-days N]]` and
|
|
52
52
|
* `cockpit ops recompile --person <p> [--dry-run]` (BLI-3462).
|
|
53
53
|
*
|
|
54
54
|
* There is deliberately **no `--cadence`** on `recompile`. The compile path
|
|
@@ -64,11 +64,13 @@ export function parseOpsArgs(args) {
|
|
|
64
64
|
"--dashboard-url",
|
|
65
65
|
"--job",
|
|
66
66
|
"--skips",
|
|
67
|
+
"--memory",
|
|
68
|
+
"--memory-days",
|
|
67
69
|
"--person",
|
|
68
70
|
"--dry-run",
|
|
69
71
|
"--json",
|
|
70
72
|
],
|
|
71
|
-
valueFlags: ["--home", "--dashboard-url", "--job", "--person"],
|
|
73
|
+
valueFlags: ["--home", "--dashboard-url", "--job", "--person", "--memory-days"],
|
|
72
74
|
});
|
|
73
75
|
if (values.positionals.length > 1) {
|
|
74
76
|
throw new Error("ops accepts one action: status or recompile.");
|
|
@@ -83,11 +85,19 @@ export function parseOpsArgs(args) {
|
|
|
83
85
|
json: values.booleans.has("--json"),
|
|
84
86
|
};
|
|
85
87
|
if (rawAction === "status") {
|
|
88
|
+
// BLI-3729: `--memory-days N` implies `--memory`, because asking for a
|
|
89
|
+
// window and getting nothing back is the silent breakage BLI-2490 forbids.
|
|
90
|
+
const memoryDays = optionalNonEmpty(values.flags.get("--memory-days"));
|
|
91
|
+
if (memoryDays !== undefined && !/^[0-9]{1,2}$/u.test(memoryDays)) {
|
|
92
|
+
throw new Error("ops --memory-days takes a whole number of days, 1 to 30.");
|
|
93
|
+
}
|
|
86
94
|
return {
|
|
87
95
|
kind: "ops",
|
|
88
96
|
action: "status",
|
|
89
97
|
job: optionalNonEmpty(values.flags.get("--job")),
|
|
90
98
|
skips: values.booleans.has("--skips"),
|
|
99
|
+
memory: values.booleans.has("--memory") || memoryDays !== undefined,
|
|
100
|
+
...(memoryDays === undefined ? {} : { memoryDays: Number(memoryDays) }),
|
|
91
101
|
...base,
|
|
92
102
|
};
|
|
93
103
|
}
|
|
@@ -20,9 +20,11 @@ export function parseDocsArgs(args) {
|
|
|
20
20
|
"--visibility",
|
|
21
21
|
"--file",
|
|
22
22
|
"--body-stdin",
|
|
23
|
+
"--query",
|
|
24
|
+
"--limit",
|
|
23
25
|
"--json",
|
|
24
26
|
],
|
|
25
|
-
valueFlags: ["--home", "--dashboard-url", "--title", "--parent", "--visibility", "--file"],
|
|
27
|
+
valueFlags: ["--home", "--dashboard-url", "--title", "--parent", "--visibility", "--file", "--query", "--limit"],
|
|
26
28
|
});
|
|
27
29
|
const first = values.positionals[0];
|
|
28
30
|
const action = (first === undefined ? "list" : first);
|
|
@@ -57,6 +59,18 @@ export function parseDocsArgs(args) {
|
|
|
57
59
|
if (action === "create" && !title) {
|
|
58
60
|
throw new Error("docs create needs --title.");
|
|
59
61
|
}
|
|
62
|
+
// BLI-3737: narrowing belongs to `list` only. `--parent` keeps its create/
|
|
63
|
+
// update meaning elsewhere (which parent to file the document under); on
|
|
64
|
+
// `list` the same word names which parent to list UNDER, plus the literal
|
|
65
|
+
// `root` for the top of the tree.
|
|
66
|
+
const query = optionalNonEmpty(values.flags.get("--query"));
|
|
67
|
+
if (query && action !== "list") {
|
|
68
|
+
throw new Error("--query belongs to `cockpit docs list`.");
|
|
69
|
+
}
|
|
70
|
+
const limit = optionalPositiveInteger(values.flags.get("--limit"), "--limit");
|
|
71
|
+
if (limit !== undefined && action !== "list") {
|
|
72
|
+
throw new Error("--limit belongs to `cockpit docs list`.");
|
|
73
|
+
}
|
|
60
74
|
return {
|
|
61
75
|
kind: "docs",
|
|
62
76
|
action,
|
|
@@ -69,22 +83,92 @@ export function parseDocsArgs(args) {
|
|
|
69
83
|
visibility: visibility,
|
|
70
84
|
filePath: optionalNonEmpty(values.flags.get("--file")),
|
|
71
85
|
bodyStdin: values.booleans.has("--body-stdin"),
|
|
86
|
+
...(query ? { query } : {}),
|
|
87
|
+
...(limit === undefined ? {} : { limit }),
|
|
72
88
|
json: values.booleans.has("--json"),
|
|
73
89
|
};
|
|
74
90
|
}
|
|
75
|
-
const MSG_ACTIONS = new Set(["channels", "read", "send", "thread"]);
|
|
91
|
+
const MSG_ACTIONS = new Set(["channels", "read", "send", "thread", "create", "dm"]);
|
|
76
92
|
const MSG_ACTIONS_NEEDING_A_CHANNEL = new Set(["read", "send"]);
|
|
93
|
+
/**
|
|
94
|
+
* `--members a@x.test,b@y.test` — addresses only, split on commas, never
|
|
95
|
+
* fuzzy (BLI-3749). The door resolves each one exactly, so a typo comes back
|
|
96
|
+
* as `person_not_found` naming the address rather than as a stranger added
|
|
97
|
+
* to a private channel.
|
|
98
|
+
*/
|
|
99
|
+
function parseMemberEmails(raw) {
|
|
100
|
+
const value = optionalNonEmpty(raw);
|
|
101
|
+
if (!value)
|
|
102
|
+
return undefined;
|
|
103
|
+
const emails = value
|
|
104
|
+
.split(",")
|
|
105
|
+
.map((part) => part.trim())
|
|
106
|
+
.filter((part) => part !== "");
|
|
107
|
+
if (emails.length === 0)
|
|
108
|
+
throw new Error("msg create --members needs at least one email address.");
|
|
109
|
+
const notAnAddress = emails.find((email) => !email.includes("@"));
|
|
110
|
+
if (notAnAddress) {
|
|
111
|
+
throw new Error(`msg create --members takes email addresses; "${notAnAddress}" is not one.`);
|
|
112
|
+
}
|
|
113
|
+
return emails;
|
|
114
|
+
}
|
|
77
115
|
export function parseMsgArgs(args) {
|
|
78
116
|
const values = parseNamedArgs(args, {
|
|
79
|
-
allowedFlags: [
|
|
80
|
-
|
|
117
|
+
allowedFlags: [
|
|
118
|
+
"--home",
|
|
119
|
+
"--dashboard-url",
|
|
120
|
+
"--channel",
|
|
121
|
+
"--thread",
|
|
122
|
+
"--limit",
|
|
123
|
+
"--private",
|
|
124
|
+
"--members",
|
|
125
|
+
"--description",
|
|
126
|
+
"--json",
|
|
127
|
+
],
|
|
128
|
+
valueFlags: ["--home", "--dashboard-url", "--channel", "--thread", "--limit", "--members", "--description"],
|
|
81
129
|
});
|
|
82
130
|
const first = values.positionals[0];
|
|
83
131
|
const action = (first === undefined ? "channels" : first);
|
|
84
132
|
if (!MSG_ACTIONS.has(action)) {
|
|
85
|
-
throw new Error(`Unknown msg command: ${first}. Try channels, read, send, or
|
|
133
|
+
throw new Error(`Unknown msg command: ${first}. Try channels, read, send, thread, create, or dm.`);
|
|
86
134
|
}
|
|
87
135
|
const rest = values.positionals.slice(first === undefined ? 0 : 1);
|
|
136
|
+
let channelName;
|
|
137
|
+
let dmEmail;
|
|
138
|
+
if (action === "create") {
|
|
139
|
+
channelName = optionalNonEmpty(rest[0]);
|
|
140
|
+
if (!channelName)
|
|
141
|
+
throw new Error("msg create needs a channel name, e.g. `cockpit msg create general`.");
|
|
142
|
+
if (rest.length > 1)
|
|
143
|
+
throw new Error(`msg create takes one channel name, not ${rest.length}.`);
|
|
144
|
+
// `#general` is how a person says it and how `read`/`send` accept it, so
|
|
145
|
+
// the leading # is dropped here rather than becoming part of the name.
|
|
146
|
+
if (channelName.startsWith("#"))
|
|
147
|
+
channelName = channelName.slice(1);
|
|
148
|
+
if (channelName === "")
|
|
149
|
+
throw new Error("msg create needs a channel name, e.g. `cockpit msg create general`.");
|
|
150
|
+
}
|
|
151
|
+
else if (action === "dm") {
|
|
152
|
+
dmEmail = optionalNonEmpty(rest[0]);
|
|
153
|
+
if (!dmEmail)
|
|
154
|
+
throw new Error("msg dm needs the person's email address, e.g. `cockpit msg dm ada@example.com`.");
|
|
155
|
+
if (rest.length > 1)
|
|
156
|
+
throw new Error(`msg dm takes one email address, not ${rest.length}.`);
|
|
157
|
+
if (!dmEmail.includes("@"))
|
|
158
|
+
throw new Error(`msg dm takes an email address; "${dmEmail}" is not one.`);
|
|
159
|
+
}
|
|
160
|
+
const isPrivate = values.booleans.has("--private");
|
|
161
|
+
if (isPrivate && action !== "create") {
|
|
162
|
+
throw new Error("--private belongs to `cockpit msg create`.");
|
|
163
|
+
}
|
|
164
|
+
const memberEmails = parseMemberEmails(values.flags.get("--members"));
|
|
165
|
+
if (memberEmails && action !== "create") {
|
|
166
|
+
throw new Error("--members belongs to `cockpit msg create`.");
|
|
167
|
+
}
|
|
168
|
+
const description = optionalNonEmpty(values.flags.get("--description"));
|
|
169
|
+
if (description && action !== "create") {
|
|
170
|
+
throw new Error("--description belongs to `cockpit msg create`.");
|
|
171
|
+
}
|
|
88
172
|
let channelRef;
|
|
89
173
|
if (MSG_ACTIONS_NEEDING_A_CHANNEL.has(action)) {
|
|
90
174
|
channelRef = optionalNonEmpty(rest[0]);
|
|
@@ -99,7 +183,7 @@ export function parseMsgArgs(args) {
|
|
|
99
183
|
if (rest.length > 1)
|
|
100
184
|
throw new Error(`msg thread takes one thread id, not ${rest.length}.`);
|
|
101
185
|
}
|
|
102
|
-
else if (rest.length > 0) {
|
|
186
|
+
else if (action !== "create" && action !== "dm" && rest.length > 0) {
|
|
103
187
|
throw new Error(`msg ${action} does not take "${rest[0]}".`);
|
|
104
188
|
}
|
|
105
189
|
const threadFlag = optionalNonEmpty(values.flags.get("--thread"));
|
|
@@ -119,6 +203,11 @@ export function parseMsgArgs(args) {
|
|
|
119
203
|
homeDir: optionalNonEmpty(values.flags.get("--home")),
|
|
120
204
|
dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")),
|
|
121
205
|
...(channelRef ? { channelRef } : {}),
|
|
206
|
+
...(channelName ? { channelName } : {}),
|
|
207
|
+
...(dmEmail ? { dmEmail } : {}),
|
|
208
|
+
...(memberEmails ? { memberEmails } : {}),
|
|
209
|
+
...(description ? { description } : {}),
|
|
210
|
+
isPrivate,
|
|
122
211
|
...(threadId ? { threadId } : {}),
|
|
123
212
|
...(limit === undefined ? {} : { limit }),
|
|
124
213
|
json: values.booleans.has("--json"),
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `cockpit search` argument parsing (BLI-3728).
|
|
3
|
+
*
|
|
4
|
+
* Its own sibling of `local-args-tower.ts` rather than folded into
|
|
5
|
+
* `local-args-tower-pages.ts` or `-docs-msg.ts`: search is not one surface's
|
|
6
|
+
* verb, it is the door OVER all of them — documents, messages, issues, meeting
|
|
7
|
+
* notes and memory — and putting it under any one family's doc comment would
|
|
8
|
+
* say something untrue about what it reads.
|
|
9
|
+
*
|
|
10
|
+
* The query is a POSITIONAL, not a flag, because that is how every search
|
|
11
|
+
* command a person has ever typed works (`grep`, `rg`, `gh search`). Several
|
|
12
|
+
* positionals are joined with a space so `cockpit search storage ceiling`
|
|
13
|
+
* behaves the way it looks, without demanding quotes.
|
|
14
|
+
*/
|
|
15
|
+
import { optionalNonEmpty, optionalPositiveInteger, optionalUrl, parseNamedArgs, } from "./local-arg-values.js";
|
|
16
|
+
/** The five corpora. Kept here so an unknown kind is refused BEFORE a round trip. */
|
|
17
|
+
export const SEARCH_KINDS = ["doc", "msg", "issue", "note", "memory"];
|
|
18
|
+
export function parseSearchArgs(args) {
|
|
19
|
+
const values = parseNamedArgs(args, {
|
|
20
|
+
allowedFlags: ["--home", "--dashboard-url", "--kind", "--limit", "--json"],
|
|
21
|
+
valueFlags: ["--home", "--dashboard-url", "--kind", "--limit"],
|
|
22
|
+
});
|
|
23
|
+
const query = values.positionals.join(" ").trim();
|
|
24
|
+
if (query.length === 0) {
|
|
25
|
+
throw new Error('search needs something to search for, e.g. `cockpit search "storage ceiling"`.');
|
|
26
|
+
}
|
|
27
|
+
const kindFlag = optionalNonEmpty(values.flags.get("--kind"));
|
|
28
|
+
let kinds;
|
|
29
|
+
if (kindFlag) {
|
|
30
|
+
kinds = [];
|
|
31
|
+
for (const part of kindFlag.split(",").map((entry) => entry.trim()).filter(Boolean)) {
|
|
32
|
+
if (!SEARCH_KINDS.includes(part)) {
|
|
33
|
+
throw new Error(`search --kind must be one or more of ${SEARCH_KINDS.join(", ")} — not "${part}".`);
|
|
34
|
+
}
|
|
35
|
+
if (!kinds.includes(part))
|
|
36
|
+
kinds.push(part);
|
|
37
|
+
}
|
|
38
|
+
if (kinds.length === 0)
|
|
39
|
+
kinds = undefined;
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
kind: "search",
|
|
43
|
+
query,
|
|
44
|
+
...(kinds ? { kinds } : {}),
|
|
45
|
+
limit: optionalPositiveInteger(values.flags.get("--limit"), "--limit"),
|
|
46
|
+
homeDir: optionalNonEmpty(values.flags.get("--home")),
|
|
47
|
+
dashboardUrl: optionalUrl(values.flags.get("--dashboard-url")),
|
|
48
|
+
json: values.booleans.has("--json"),
|
|
49
|
+
};
|
|
50
|
+
}
|