@bli-cockpit/cli 0.1.27 → 0.1.29
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/README.md +8 -5
- package/dist/commands/local-args.js +50 -4
- package/dist/commands/local.js +239 -4
- package/dist/commands/public-root.js +4 -2
- package/dist/commands/session-sync.js +18 -0
- package/dist/local-state.js +29 -10
- package/dist/repo-identity.js +15 -10
- package/dist/upload.js +1 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -45,11 +45,11 @@ cockpit onboard --email <APPROVED_EMAIL> --workspace ~/BLI --workspace ~/side-pr
|
|
|
45
45
|
The CLI defaults to the production dashboard. Normal intern/operator setup,
|
|
46
46
|
updates, and syncs omit `--dashboard-url`. Pass `--dashboard-url` only for
|
|
47
47
|
staging, a custom dashboard, or deliberately forcing a different dashboard
|
|
48
|
-
pairing. Already-onboarded users update with
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
prompts and the agent ticket-binding guardrail.
|
|
48
|
+
pairing. Already-onboarded users update with `cockpit update` from anywhere. It
|
|
49
|
+
installs the latest public CLI from npm, then reruns onboarding checks to
|
|
50
|
+
refresh pairing, agent rules, autostart, and an initial sync against saved
|
|
51
|
+
roots. `cockpit upgrade` is a compatibility alias. `--repo <path>` remains
|
|
52
|
+
supported for older prompts and the agent ticket-binding guardrail.
|
|
53
53
|
|
|
54
54
|
On machines where Codex or Claude agents will do ticketed work, `cockpit
|
|
55
55
|
onboard` refreshes `~/.codex/AGENTS.md` and `~/.claude/CLAUDE.md` after harvest
|
|
@@ -137,6 +137,9 @@ cockpit sync \
|
|
|
137
137
|
|
|
138
138
|
No ticket is required for setup, chatting, planning, or general ambient capture.
|
|
139
139
|
Only pass `--ticket` when the work really belongs to a visible ticket.
|
|
140
|
+
Omitting `--ticket` preserves any existing ticket binding; run
|
|
141
|
+
`cockpit start --clear-ticket --workspace "$PWD"` when you intentionally want
|
|
142
|
+
to return a previously bound work context to general ambient capture.
|
|
140
143
|
|
|
141
144
|
When the work is important but ticketless, label it explicitly before syncing so
|
|
142
145
|
later analysis does not have to guess the topic:
|
|
@@ -12,6 +12,9 @@ export function parseLocalArgs(argv) {
|
|
|
12
12
|
switch (command) {
|
|
13
13
|
case "onboard":
|
|
14
14
|
return parseOnboardArgs(argv.slice(1));
|
|
15
|
+
case "update":
|
|
16
|
+
case "upgrade":
|
|
17
|
+
return parseUpdateArgs(command, argv.slice(1));
|
|
15
18
|
case "install":
|
|
16
19
|
return parseInstallArgs(argv.slice(1));
|
|
17
20
|
case "login":
|
|
@@ -33,11 +36,13 @@ export function parseLocalArgs(argv) {
|
|
|
33
36
|
return parseAutostartArgs(argv.slice(1));
|
|
34
37
|
case "agent-rules":
|
|
35
38
|
return parseAgentRulesArgs(argv.slice(1));
|
|
39
|
+
case "release":
|
|
40
|
+
return parseReleaseArgs(argv.slice(1));
|
|
36
41
|
default:
|
|
37
42
|
throw new Error(`Unknown local command: ${command ?? ""}`);
|
|
38
43
|
}
|
|
39
44
|
}
|
|
40
|
-
function
|
|
45
|
+
function parseOnboardLikeArgs(args, command) {
|
|
41
46
|
const values = parseNamedArgs(args, {
|
|
42
47
|
allowedFlags: [
|
|
43
48
|
"--home",
|
|
@@ -69,13 +74,13 @@ function parseOnboardArgs(args) {
|
|
|
69
74
|
"--max-repos",
|
|
70
75
|
],
|
|
71
76
|
});
|
|
72
|
-
assertNoPositionals(values.positionals,
|
|
77
|
+
assertNoPositionals(values.positionals, command);
|
|
73
78
|
return {
|
|
74
|
-
kind: "onboard",
|
|
75
79
|
homeDir: optionalNonEmpty(values.flags.get("--home")),
|
|
76
80
|
repoRoot: optionalNonEmpty(workRootFlagValue(values)),
|
|
77
81
|
collectionRoots: optionalNonEmptyList(workRootFlagValues(values)),
|
|
78
82
|
dashboardUrl: normalizeUrl(values.flags.get("--dashboard-url") ?? DEFAULT_DASHBOARD_URL),
|
|
83
|
+
dashboardUrlExplicit: values.flags.has("--dashboard-url"),
|
|
79
84
|
claimedOwnerEmail: optionalEmail(values.flags.get("--email")),
|
|
80
85
|
deviceName: optionalNonEmpty(values.flags.get("--device-name")),
|
|
81
86
|
activeTicketId: optionalNonEmpty(values.flags.get("--ticket")),
|
|
@@ -87,6 +92,16 @@ function parseOnboardArgs(args) {
|
|
|
87
92
|
maxRepos: optionalPositiveInteger(values.flags.get("--max-repos"), "--max-repos"),
|
|
88
93
|
};
|
|
89
94
|
}
|
|
95
|
+
function parseOnboardArgs(args) {
|
|
96
|
+
return { kind: "onboard", ...parseOnboardLikeArgs(args, "onboard") };
|
|
97
|
+
}
|
|
98
|
+
function parseUpdateArgs(alias, args) {
|
|
99
|
+
return {
|
|
100
|
+
kind: "update",
|
|
101
|
+
alias,
|
|
102
|
+
...parseOnboardLikeArgs(args, alias),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
90
105
|
function parseInstallArgs(args) {
|
|
91
106
|
const values = parseNamedArgs(args, {
|
|
92
107
|
allowedFlags: [
|
|
@@ -115,6 +130,30 @@ function parseInstallArgs(args) {
|
|
|
115
130
|
json: values.booleans.has("--json"),
|
|
116
131
|
};
|
|
117
132
|
}
|
|
133
|
+
function parseReleaseArgs(args) {
|
|
134
|
+
const values = parseNamedArgs(args, {
|
|
135
|
+
allowedFlags: [
|
|
136
|
+
"--dry-run",
|
|
137
|
+
"--tag",
|
|
138
|
+
"--access",
|
|
139
|
+
"--otp",
|
|
140
|
+
"--skip-checks",
|
|
141
|
+
],
|
|
142
|
+
valueFlags: ["--tag", "--access", "--otp"],
|
|
143
|
+
});
|
|
144
|
+
assertNoPositionals(values.positionals, "release");
|
|
145
|
+
const releaseArgs = [];
|
|
146
|
+
if (values.booleans.has("--dry-run"))
|
|
147
|
+
releaseArgs.push("--dry-run");
|
|
148
|
+
if (values.booleans.has("--skip-checks"))
|
|
149
|
+
releaseArgs.push("--skip-checks");
|
|
150
|
+
for (const flag of ["--tag", "--access", "--otp"]) {
|
|
151
|
+
const value = values.flags.get(flag);
|
|
152
|
+
if (value !== undefined)
|
|
153
|
+
releaseArgs.push(flag, value);
|
|
154
|
+
}
|
|
155
|
+
return { kind: "release", args: releaseArgs };
|
|
156
|
+
}
|
|
118
157
|
function parseLoginArgs(args) {
|
|
119
158
|
const values = parseNamedArgs(args, {
|
|
120
159
|
allowedFlags: [
|
|
@@ -167,6 +206,7 @@ function parseStartArgs(args) {
|
|
|
167
206
|
"--workspace",
|
|
168
207
|
"--branch",
|
|
169
208
|
"--ticket",
|
|
209
|
+
"--clear-ticket",
|
|
170
210
|
"--topic",
|
|
171
211
|
"--topic-summary",
|
|
172
212
|
"--intent",
|
|
@@ -198,6 +238,11 @@ function parseStartArgs(args) {
|
|
|
198
238
|
],
|
|
199
239
|
});
|
|
200
240
|
assertNoPositionals(values.positionals, "start");
|
|
241
|
+
const activeTicketId = optionalNonEmpty(values.flags.get("--ticket"));
|
|
242
|
+
const clearTicket = values.booleans.has("--clear-ticket");
|
|
243
|
+
if (activeTicketId && clearTicket) {
|
|
244
|
+
throw new Error("--ticket and --clear-ticket cannot be combined.");
|
|
245
|
+
}
|
|
201
246
|
const topicLabel = optionalNonEmpty(values.flags.get("--topic"));
|
|
202
247
|
const topicSummaryRedacted = optionalNonEmpty(values.flags.get("--topic-summary"));
|
|
203
248
|
const workIntent = optionalSchemaValue(WorkIntentSchema, values.flags.get("--intent"), "--intent");
|
|
@@ -213,7 +258,8 @@ function parseStartArgs(args) {
|
|
|
213
258
|
homeDir: optionalNonEmpty(values.flags.get("--home")),
|
|
214
259
|
repoRoot: optionalNonEmpty(workRootFlagValue(values)),
|
|
215
260
|
branch: optionalNonEmpty(values.flags.get("--branch")),
|
|
216
|
-
activeTicketId
|
|
261
|
+
activeTicketId,
|
|
262
|
+
clearTicket,
|
|
217
263
|
topicLabel,
|
|
218
264
|
topicSummaryRedacted,
|
|
219
265
|
workIntent,
|
package/dist/commands/local.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { execFile } from "node:child_process";
|
|
1
|
+
import { execFile, spawn } from "node:child_process";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
2
3
|
import os from "node:os";
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
import { createCollectorServer } from "../server.js";
|
|
@@ -14,6 +15,8 @@ import { runAttributedWorktreeSync, } from "./session-sync.js";
|
|
|
14
15
|
import { COLLECTION_ROOT_REQUIRED, resolveOnboardingRoots, } from "../onboarding-roots.js";
|
|
15
16
|
export const rootCommandNames = new Set([
|
|
16
17
|
"onboard",
|
|
18
|
+
"update",
|
|
19
|
+
"upgrade",
|
|
17
20
|
"install",
|
|
18
21
|
"login",
|
|
19
22
|
"pair",
|
|
@@ -25,6 +28,7 @@ export const rootCommandNames = new Set([
|
|
|
25
28
|
"serve",
|
|
26
29
|
"autostart",
|
|
27
30
|
"agent-rules",
|
|
31
|
+
"release",
|
|
28
32
|
]);
|
|
29
33
|
export async function runLocalCockpitCli(argv, io = defaultIo()) {
|
|
30
34
|
if (isLocalHelpRequest(argv)) {
|
|
@@ -47,6 +51,8 @@ export async function runLocalCockpitCli(argv, io = defaultIo()) {
|
|
|
47
51
|
return await runInstall(command, io);
|
|
48
52
|
case "onboard":
|
|
49
53
|
return await runOnboard(command, io);
|
|
54
|
+
case "update":
|
|
55
|
+
return await runUpdate(command, io);
|
|
50
56
|
case "login":
|
|
51
57
|
return await runLogin(command, io);
|
|
52
58
|
case "logout":
|
|
@@ -65,6 +71,8 @@ export async function runLocalCockpitCli(argv, io = defaultIo()) {
|
|
|
65
71
|
return await runAutostart(command, io);
|
|
66
72
|
case "agent-rules":
|
|
67
73
|
return await runAgentRules(command, io);
|
|
74
|
+
case "release":
|
|
75
|
+
return await runRelease(command, io);
|
|
68
76
|
}
|
|
69
77
|
}
|
|
70
78
|
catch (error) {
|
|
@@ -77,17 +85,20 @@ export function localCommandHelp(command) {
|
|
|
77
85
|
return localSubcommandHelp(command);
|
|
78
86
|
return [
|
|
79
87
|
" cockpit onboard [--ticket <id>] [--email <owner@email>] [--device-name <name>] [--dashboard-url <url>] [--workspace <path>] [--branch <name>] [--max-depth <n>] [--max-repos <n>] [--json]",
|
|
88
|
+
" cockpit update [--email <owner@email>] [--device-name <name>] [--dashboard-url <url>] [--workspace <path>] [--json]",
|
|
89
|
+
" cockpit upgrade [same flags as update]",
|
|
80
90
|
" cockpit install [--dashboard-url <url>] [--workspace <path>] [--json]",
|
|
81
91
|
" cockpit login [--email <owner@email>] [--device-name <name>] [--dashboard-url <url>] [--json]",
|
|
82
92
|
" cockpit pair [--email <owner@email>] [--device-name <name>] [--dashboard-url <url>] [--json]",
|
|
83
93
|
" cockpit logout",
|
|
84
|
-
" cockpit start [--ticket <id
|
|
94
|
+
" cockpit start [--ticket <id>|--clear-ticket] [--topic <label>] [--intent <intent>] [--phase <phase>] [--workspace <path>] [--branch <name>] [--max-depth <n>] [--max-repos <n>] [--json]",
|
|
85
95
|
" cockpit sync [--workspace <path>] [--dashboard-url <url>] [--max-depth <n>] [--max-repos <n>] [--json]",
|
|
86
96
|
" cockpit status [--workspace <path>] [--max-depth <n>] [--max-repos <n>] [--json]",
|
|
87
97
|
" cockpit sessions [--source codex|claude] [--workspace <path>] [--max-depth <n>] [--max-repos <n>] [--json]",
|
|
88
98
|
" cockpit serve [--port <port>] [--workspace <path>]",
|
|
89
99
|
" cockpit autostart [install|uninstall|status] [--workspace <path>] [--dashboard-url <url>] [--interval-seconds <n>] [--json]",
|
|
90
100
|
" cockpit agent-rules [install|uninstall|status] [--host codex|claude|all] [--workspace <path>] [--json]",
|
|
101
|
+
" cockpit release [--dry-run] [--skip-checks] [--tag <tag>] [--access <public|restricted>] [--otp <code>]",
|
|
91
102
|
"",
|
|
92
103
|
`Default dashboard: ${DEFAULT_DASHBOARD_URL}. Omit --dashboard-url for normal production use; pass it only for staging/custom dashboards or to force a different pairing.`,
|
|
93
104
|
].join("\n");
|
|
@@ -118,6 +129,25 @@ function localSubcommandHelp(command) {
|
|
|
118
129
|
"Omit --dashboard-url for the production dashboard; pass it only for staging/custom dashboards.",
|
|
119
130
|
],
|
|
120
131
|
],
|
|
132
|
+
[
|
|
133
|
+
"update",
|
|
134
|
+
[
|
|
135
|
+
"Usage: cockpit update [--email <owner@email>] [--device-name <name>] [--dashboard-url <url>] [--workspace <path>] [--json]",
|
|
136
|
+
"",
|
|
137
|
+
"Updates the global public CLI from npm, then reruns `cockpit onboard`",
|
|
138
|
+
"with the same setup flags so pairing, saved roots, agent rules,",
|
|
139
|
+
"autostart, and the initial sync are refreshed in one command.",
|
|
140
|
+
"`cockpit upgrade` is an alias.",
|
|
141
|
+
],
|
|
142
|
+
],
|
|
143
|
+
[
|
|
144
|
+
"upgrade",
|
|
145
|
+
[
|
|
146
|
+
"Usage: cockpit upgrade [same flags as cockpit update]",
|
|
147
|
+
"",
|
|
148
|
+
"Alias for `cockpit update`.",
|
|
149
|
+
],
|
|
150
|
+
],
|
|
121
151
|
[
|
|
122
152
|
"login",
|
|
123
153
|
[
|
|
@@ -139,10 +169,11 @@ function localSubcommandHelp(command) {
|
|
|
139
169
|
[
|
|
140
170
|
"start",
|
|
141
171
|
[
|
|
142
|
-
"Usage: cockpit start [--ticket <id
|
|
172
|
+
"Usage: cockpit start [--ticket <id>|--clear-ticket] [--topic <label>] [--topic-summary <summary>] [--intent <intent>] [--phase <phase>] [--intent-confidence <0..1>] [--workspace <path>] [--branch <name>] [--json]",
|
|
143
173
|
"",
|
|
144
174
|
"Starts local ambient capture. Parent folders start each child git worktree.",
|
|
145
|
-
"Add --ticket only when the work already has a visible ticket.",
|
|
175
|
+
"Add --ticket only when the work already has a visible ticket; omit it to preserve an existing binding.",
|
|
176
|
+
"Use --clear-ticket to intentionally return the context to general ambient capture.",
|
|
146
177
|
"Use --topic/--intent/--phase for planning, discovery, and learning work that has no ticket yet.",
|
|
147
178
|
"Supported intents: implementation, bug_fix, root_cause_analysis, planning, discovery, review, testing, documentation, release, learning, coordination, maintenance, analysis, unknown, other.",
|
|
148
179
|
"Supported phases: planning, discovery, implementation, debugging, review, testing, documentation, release, handoff, analysis, unknown, other.",
|
|
@@ -222,6 +253,17 @@ function localSubcommandHelp(command) {
|
|
|
222
253
|
"Action defaults to `install`.",
|
|
223
254
|
],
|
|
224
255
|
],
|
|
256
|
+
[
|
|
257
|
+
"release",
|
|
258
|
+
[
|
|
259
|
+
"Usage: cockpit release [--dry-run] [--skip-checks] [--tag <tag>] [--access <public|restricted>] [--otp <code>]",
|
|
260
|
+
"",
|
|
261
|
+
"Maintainer-only helper. Run from inside the bli-cockpit repo checkout.",
|
|
262
|
+
"Requires a clean `main` branch and runs `git pull --ff-only` before publishing.",
|
|
263
|
+
"Delegates to `npm run publish:public -- ...` so public packages are",
|
|
264
|
+
"built, checked, and published in the safe telemetry-core then CLI order.",
|
|
265
|
+
],
|
|
266
|
+
],
|
|
225
267
|
]);
|
|
226
268
|
return (helpByCommand.get(command) ?? [localCommandHelp()]).join("\n");
|
|
227
269
|
}
|
|
@@ -244,6 +286,186 @@ async function runInstall(command, io) {
|
|
|
244
286
|
writeLine(io.stdout, "Next: run `cockpit login`, then `cockpit start` inside the repo; add `--ticket <id>` only when ticket work begins.");
|
|
245
287
|
return 0;
|
|
246
288
|
}
|
|
289
|
+
async function runUpdate(command, io) {
|
|
290
|
+
const exec = io.exec ?? defaultExec();
|
|
291
|
+
const installArgs = [
|
|
292
|
+
"install",
|
|
293
|
+
"-g",
|
|
294
|
+
"@bli-cockpit/cli@latest",
|
|
295
|
+
"--prefer-online",
|
|
296
|
+
];
|
|
297
|
+
if (!command.json) {
|
|
298
|
+
writeLine(io.stdout, "Updating Cockpit CLI from npm...");
|
|
299
|
+
}
|
|
300
|
+
const install = await exec("npm", installArgs);
|
|
301
|
+
writeExecOutput(io, install, { stdout: !command.json, stderr: true });
|
|
302
|
+
if (install.code !== 0) {
|
|
303
|
+
if (command.json) {
|
|
304
|
+
writeLine(io.stdout, JSON.stringify({
|
|
305
|
+
status: "blocked",
|
|
306
|
+
step: "npm_install",
|
|
307
|
+
command: `npm ${installArgs.join(" ")}`,
|
|
308
|
+
exit_code: install.code,
|
|
309
|
+
}, null, 2));
|
|
310
|
+
}
|
|
311
|
+
else {
|
|
312
|
+
writeLine(io.stderr, "BLOCKED: npm install failed; Cockpit CLI was not refreshed.");
|
|
313
|
+
}
|
|
314
|
+
return install.code || 1;
|
|
315
|
+
}
|
|
316
|
+
if (!command.json) {
|
|
317
|
+
writeLine(io.stdout, "Cockpit CLI updated. Rechecking onboarding...");
|
|
318
|
+
}
|
|
319
|
+
const onboard = await exec("cockpit", [
|
|
320
|
+
"onboard",
|
|
321
|
+
...updateOnboardArgs(command),
|
|
322
|
+
]);
|
|
323
|
+
writeExecOutput(io, onboard, { stdout: true, stderr: true });
|
|
324
|
+
return onboard.code;
|
|
325
|
+
}
|
|
326
|
+
async function runRelease(command, io) {
|
|
327
|
+
const releaseRoot = await findPublicReleaseRoot(process.cwd());
|
|
328
|
+
if (!releaseRoot) {
|
|
329
|
+
writeLine(io.stderr, "cockpit release must be run inside the bli-cockpit repo checkout (missing publish:public script).");
|
|
330
|
+
return 1;
|
|
331
|
+
}
|
|
332
|
+
const exec = io.exec ?? defaultExec();
|
|
333
|
+
const gitReady = await prepareReleaseMainBranch(releaseRoot, exec, io);
|
|
334
|
+
if (!gitReady)
|
|
335
|
+
return 1;
|
|
336
|
+
writeLine(io.stdout, "Running Cockpit public package release...");
|
|
337
|
+
const npmArgs = ["--prefix", releaseRoot, "run", "publish:public"];
|
|
338
|
+
if (command.args.length > 0)
|
|
339
|
+
npmArgs.push("--", ...command.args);
|
|
340
|
+
const releaseExec = io.interactiveExec ?? defaultInteractiveExec();
|
|
341
|
+
const result = await releaseExec("npm", npmArgs);
|
|
342
|
+
writeExecOutput(io, result, { stdout: true, stderr: true });
|
|
343
|
+
return result.code;
|
|
344
|
+
}
|
|
345
|
+
async function prepareReleaseMainBranch(releaseRoot, exec, io) {
|
|
346
|
+
const branch = await exec("git", [
|
|
347
|
+
"-C",
|
|
348
|
+
releaseRoot,
|
|
349
|
+
"rev-parse",
|
|
350
|
+
"--abbrev-ref",
|
|
351
|
+
"HEAD",
|
|
352
|
+
]);
|
|
353
|
+
writeExecOutput(io, branch, { stdout: false, stderr: true });
|
|
354
|
+
if (branch.code !== 0) {
|
|
355
|
+
writeLine(io.stderr, "BLOCKED: cockpit release could not read the current git branch.");
|
|
356
|
+
return false;
|
|
357
|
+
}
|
|
358
|
+
const currentBranch = branch.stdout.trim();
|
|
359
|
+
if (currentBranch !== "main") {
|
|
360
|
+
writeLine(io.stderr, `BLOCKED: cockpit release only publishes from main. Current branch is ${currentBranch || "unknown"}.`);
|
|
361
|
+
writeLine(io.stderr, "Merge the release changes, switch to main, then rerun `cockpit release`.");
|
|
362
|
+
return false;
|
|
363
|
+
}
|
|
364
|
+
const status = await exec("git", [
|
|
365
|
+
"-C",
|
|
366
|
+
releaseRoot,
|
|
367
|
+
"status",
|
|
368
|
+
"--porcelain",
|
|
369
|
+
]);
|
|
370
|
+
writeExecOutput(io, status, { stdout: false, stderr: true });
|
|
371
|
+
if (status.code !== 0) {
|
|
372
|
+
writeLine(io.stderr, "BLOCKED: cockpit release could not inspect git status.");
|
|
373
|
+
return false;
|
|
374
|
+
}
|
|
375
|
+
if (status.stdout.trim()) {
|
|
376
|
+
writeLine(io.stderr, "BLOCKED: cockpit release requires a clean main checkout.");
|
|
377
|
+
writeLine(io.stderr, "Commit or discard local changes, then rerun `cockpit release`.");
|
|
378
|
+
return false;
|
|
379
|
+
}
|
|
380
|
+
writeLine(io.stdout, "Syncing main with git pull --ff-only...");
|
|
381
|
+
const pull = await exec("git", ["-C", releaseRoot, "pull", "--ff-only"]);
|
|
382
|
+
writeExecOutput(io, pull, { stdout: true, stderr: true });
|
|
383
|
+
if (pull.code !== 0) {
|
|
384
|
+
writeLine(io.stderr, "BLOCKED: git pull --ff-only failed; main is not safely current.");
|
|
385
|
+
return false;
|
|
386
|
+
}
|
|
387
|
+
return true;
|
|
388
|
+
}
|
|
389
|
+
async function findPublicReleaseRoot(startDir) {
|
|
390
|
+
let current = path.resolve(startDir);
|
|
391
|
+
while (true) {
|
|
392
|
+
const packageJsonPath = path.join(current, "package.json");
|
|
393
|
+
try {
|
|
394
|
+
const packageJson = JSON.parse(await readFile(packageJsonPath, "utf8"));
|
|
395
|
+
if (packageJson.scripts?.["publish:public"] !== undefined)
|
|
396
|
+
return current;
|
|
397
|
+
}
|
|
398
|
+
catch {
|
|
399
|
+
// Keep walking: nested packages may be missing package.json or have one
|
|
400
|
+
// without the release script.
|
|
401
|
+
}
|
|
402
|
+
const parent = path.dirname(current);
|
|
403
|
+
if (parent === current)
|
|
404
|
+
return null;
|
|
405
|
+
current = parent;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
function updateOnboardArgs(command) {
|
|
409
|
+
const args = [];
|
|
410
|
+
if (command.homeDir)
|
|
411
|
+
args.push("--home", command.homeDir);
|
|
412
|
+
for (const root of updateCollectionRoots(command)) {
|
|
413
|
+
args.push("--workspace", root);
|
|
414
|
+
}
|
|
415
|
+
if (command.dashboardUrlExplicit) {
|
|
416
|
+
args.push("--dashboard-url", command.dashboardUrl);
|
|
417
|
+
}
|
|
418
|
+
if (command.claimedOwnerEmail)
|
|
419
|
+
args.push("--email", command.claimedOwnerEmail);
|
|
420
|
+
if (command.deviceName)
|
|
421
|
+
args.push("--device-name", command.deviceName);
|
|
422
|
+
if (command.activeTicketId)
|
|
423
|
+
args.push("--ticket", command.activeTicketId);
|
|
424
|
+
if (command.branch)
|
|
425
|
+
args.push("--branch", command.branch);
|
|
426
|
+
if (command.pollIntervalMs !== undefined) {
|
|
427
|
+
args.push("--poll-interval-ms", String(command.pollIntervalMs));
|
|
428
|
+
}
|
|
429
|
+
if (command.timeoutMs !== undefined) {
|
|
430
|
+
args.push("--timeout-ms", String(command.timeoutMs));
|
|
431
|
+
}
|
|
432
|
+
if (command.maxDepth !== undefined)
|
|
433
|
+
args.push("--max-depth", String(command.maxDepth));
|
|
434
|
+
if (command.maxRepos !== undefined)
|
|
435
|
+
args.push("--max-repos", String(command.maxRepos));
|
|
436
|
+
if (command.json)
|
|
437
|
+
args.push("--json");
|
|
438
|
+
return args;
|
|
439
|
+
}
|
|
440
|
+
function updateCollectionRoots(command) {
|
|
441
|
+
const roots = command.collectionRoots?.length
|
|
442
|
+
? command.collectionRoots
|
|
443
|
+
: command.repoRoot
|
|
444
|
+
? [command.repoRoot]
|
|
445
|
+
: [];
|
|
446
|
+
const seen = new Set();
|
|
447
|
+
const deduped = [];
|
|
448
|
+
for (const root of roots) {
|
|
449
|
+
if (seen.has(root))
|
|
450
|
+
continue;
|
|
451
|
+
seen.add(root);
|
|
452
|
+
deduped.push(root);
|
|
453
|
+
}
|
|
454
|
+
return deduped;
|
|
455
|
+
}
|
|
456
|
+
function writeExecOutput(io, result, options) {
|
|
457
|
+
if (options.stdout)
|
|
458
|
+
writeRaw(io.stdout, result.stdout);
|
|
459
|
+
if (options.stderr)
|
|
460
|
+
writeRaw(io.stderr, result.stderr);
|
|
461
|
+
}
|
|
462
|
+
function writeRaw(stream, text) {
|
|
463
|
+
if (!text)
|
|
464
|
+
return;
|
|
465
|
+
stream.write(text);
|
|
466
|
+
if (!text.endsWith("\n"))
|
|
467
|
+
stream.write("\n");
|
|
468
|
+
}
|
|
247
469
|
function isInteractiveStdin(io) {
|
|
248
470
|
return Boolean(io.stdin.isTTY);
|
|
249
471
|
}
|
|
@@ -965,6 +1187,7 @@ async function runStart(command, io) {
|
|
|
965
1187
|
repoRoot: worktree.repo_root,
|
|
966
1188
|
branch: command.branch,
|
|
967
1189
|
activeTicketId: command.activeTicketId,
|
|
1190
|
+
clearTicket: command.clearTicket,
|
|
968
1191
|
topicLabel: command.topicLabel,
|
|
969
1192
|
topicSummaryRedacted: command.topicSummaryRedacted,
|
|
970
1193
|
workIntent: command.workIntent,
|
|
@@ -1343,6 +1566,17 @@ function defaultExec() {
|
|
|
1343
1566
|
});
|
|
1344
1567
|
});
|
|
1345
1568
|
}
|
|
1569
|
+
function defaultInteractiveExec() {
|
|
1570
|
+
return (cmd, args) => new Promise((resolve) => {
|
|
1571
|
+
const child = spawn(cmd, args, { stdio: "inherit" });
|
|
1572
|
+
child.on("error", (error) => {
|
|
1573
|
+
resolve({ code: 1, stdout: "", stderr: errorMessage(error) });
|
|
1574
|
+
});
|
|
1575
|
+
child.on("close", (code) => {
|
|
1576
|
+
resolve({ code: code ?? 1, stdout: "", stderr: "" });
|
|
1577
|
+
});
|
|
1578
|
+
});
|
|
1579
|
+
}
|
|
1346
1580
|
function defaultIo() {
|
|
1347
1581
|
if (!globalThis.fetch) {
|
|
1348
1582
|
throw new Error("global fetch is unavailable; use Node.js 20 or newer.");
|
|
@@ -1354,6 +1588,7 @@ function defaultIo() {
|
|
|
1354
1588
|
env: process.env,
|
|
1355
1589
|
fetch: globalThis.fetch.bind(globalThis),
|
|
1356
1590
|
exec: defaultExec(),
|
|
1591
|
+
interactiveExec: defaultInteractiveExec(),
|
|
1357
1592
|
};
|
|
1358
1593
|
}
|
|
1359
1594
|
function writeLine(stream, text) {
|
|
@@ -22,13 +22,15 @@ function cockpitHelp() {
|
|
|
22
22
|
"Usage:",
|
|
23
23
|
localCommandHelp(),
|
|
24
24
|
"",
|
|
25
|
-
"Install
|
|
25
|
+
"Install: `npm install -g @bli-cockpit/cli@latest`.",
|
|
26
|
+
"Update: run `cockpit update` to refresh the global CLI and rerun onboarding checks.",
|
|
26
27
|
"Intern path: run `cockpit onboard`; it confirms a `/BLI` collection root before syncing.",
|
|
27
28
|
"Headless/reused laptop path: `cockpit onboard --email <email> --workspace ~/BLI`.",
|
|
28
|
-
"Already onboarded:
|
|
29
|
+
"Already onboarded: run `cockpit update` from anywhere to refresh pairing, roots, agent rules, autostart, and sync.",
|
|
29
30
|
"Agent setup: `cockpit onboard` refreshes AGENTS.md/CLAUDE.md rules; use `cockpit agent-rules install --workspace ~/BLI` for repair.",
|
|
30
31
|
"Dashboard URL is optional for normal production use; pass `--dashboard-url` only for staging/custom dashboards or forced re-pairing.",
|
|
31
32
|
"Manual collector path: `install`, `login`, `start [--ticket <id>] [--topic <label>] [--intent <intent>] [--phase <phase>]`, `sync`, `status`, `agent-rules`.",
|
|
33
|
+
"Maintainer release path: merge to main first, then run `cockpit release --dry-run` and `cockpit release` from a clean main checkout.",
|
|
32
34
|
].join("\n");
|
|
33
35
|
}
|
|
34
36
|
|
|
@@ -114,6 +114,7 @@ export async function runAttributedWorktreeSync(options) {
|
|
|
114
114
|
homeDir: options.homeDir,
|
|
115
115
|
repoRoot: worktree.repo_root,
|
|
116
116
|
dashboardUrl: options.dashboardUrl,
|
|
117
|
+
worktreeInventory: worktreeInventoryForRepo(worktree, options.worktrees),
|
|
117
118
|
codexSessionFiles: codexAttribution.results
|
|
118
119
|
.filter((result) => result.state === "attributed" &&
|
|
119
120
|
result.worktree?.worktree_fingerprint ===
|
|
@@ -510,6 +511,23 @@ function emptyClaudeScan() {
|
|
|
510
511
|
},
|
|
511
512
|
};
|
|
512
513
|
}
|
|
514
|
+
function worktreeInventoryForRepo(current, worktrees) {
|
|
515
|
+
return worktrees
|
|
516
|
+
.filter((worktree) => worktree.repo_fingerprint
|
|
517
|
+
? worktree.repo_fingerprint === current.repo_fingerprint
|
|
518
|
+
: worktree.repo_label === current.repo_label)
|
|
519
|
+
.map((worktree) => ({
|
|
520
|
+
repo: worktree.repo_root,
|
|
521
|
+
repo_label: worktree.repo_label,
|
|
522
|
+
repo_fingerprint: worktree.repo_fingerprint,
|
|
523
|
+
repo_origin_url: worktree.repo_origin_url ?? undefined,
|
|
524
|
+
head_sha: worktree.head_sha ?? undefined,
|
|
525
|
+
worktree_label: worktree.worktree_label,
|
|
526
|
+
worktree_fingerprint: worktree.worktree_fingerprint,
|
|
527
|
+
worktree_is_primary: worktree.worktree_is_primary,
|
|
528
|
+
branch: worktree.branch,
|
|
529
|
+
}));
|
|
530
|
+
}
|
|
513
531
|
async function isClaudeCollectionEnabled(paths) {
|
|
514
532
|
const config = await readLocalCollectorConfig(paths).catch(() => null);
|
|
515
533
|
return config?.collect_claude_jsonl !== false;
|
package/dist/local-state.js
CHANGED
|
@@ -4,7 +4,7 @@ import { readFileSync } from "node:fs";
|
|
|
4
4
|
import fs from "node:fs/promises";
|
|
5
5
|
import os from "node:os";
|
|
6
6
|
import path from "node:path";
|
|
7
|
-
import { resolveRepoWorktreeIdentity, } from "./repo-identity.js";
|
|
7
|
+
import { resolveRepoWorktreeIdentity, stableWorktreeFingerprint, stableWorktreeRoot, } from "./repo-identity.js";
|
|
8
8
|
import { normalizeCollectionRoots } from "./root-normalization.js";
|
|
9
9
|
import { summarizeLocalUploadSpool } from "./spool/local-spool.js";
|
|
10
10
|
const localCollectorPackage = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
@@ -138,6 +138,9 @@ export async function logoutLocalCollector(options = {}) {
|
|
|
138
138
|
return { removed, session_file: paths.session_file };
|
|
139
139
|
}
|
|
140
140
|
export async function startLocalWorkContext(options = {}) {
|
|
141
|
+
if (options.activeTicketId && options.clearTicket) {
|
|
142
|
+
throw new Error("--ticket and --clear-ticket cannot be combined.");
|
|
143
|
+
}
|
|
141
144
|
const now = options.now ?? new Date();
|
|
142
145
|
const homeDir = options.homeDir ?? os.homedir();
|
|
143
146
|
const repoRoot = path.resolve(options.repoRoot ?? process.cwd());
|
|
@@ -161,6 +164,9 @@ export async function startLocalWorkContext(options = {}) {
|
|
|
161
164
|
worktreeFingerprint: identity.worktree_fingerprint,
|
|
162
165
|
});
|
|
163
166
|
const existingContext = await readLocalWorkContextByFingerprint(paths, identity.worktree_fingerprint).catch(() => null);
|
|
167
|
+
const activeTicketId = options.clearTicket
|
|
168
|
+
? undefined
|
|
169
|
+
: (options.activeTicketId ?? existingContext?.active_ticket_id);
|
|
164
170
|
const ticketBindingCandidates = options.activeTicketId
|
|
165
171
|
? [
|
|
166
172
|
{
|
|
@@ -170,7 +176,9 @@ export async function startLocalWorkContext(options = {}) {
|
|
|
170
176
|
evidence_labels: ["cockpit_start_ticket"],
|
|
171
177
|
},
|
|
172
178
|
]
|
|
173
|
-
:
|
|
179
|
+
: options.clearTicket
|
|
180
|
+
? []
|
|
181
|
+
: (existingContext?.ticket_binding_candidates ?? []);
|
|
174
182
|
const context = LocalWorkContextSchema.parse({
|
|
175
183
|
work_context_id: workContextId,
|
|
176
184
|
repo: identity.repo_root,
|
|
@@ -186,7 +194,7 @@ export async function startLocalWorkContext(options = {}) {
|
|
|
186
194
|
session_id: sessionId,
|
|
187
195
|
started_at: existingContext?.started_at ?? now.toISOString(),
|
|
188
196
|
updated_at: now.toISOString(),
|
|
189
|
-
active_ticket_id:
|
|
197
|
+
active_ticket_id: activeTicketId,
|
|
190
198
|
ticket_binding_candidates: ticketBindingCandidates,
|
|
191
199
|
topic_label: options.topicLabel,
|
|
192
200
|
topic_summary_redacted: options.topicSummaryRedacted,
|
|
@@ -229,8 +237,8 @@ export async function inspectLocalCollectorStatus(options = {}) {
|
|
|
229
237
|
const identity = await resolveIdentityOrFallback(repoRoot, options.branch);
|
|
230
238
|
const context = await readLocalWorkContextForRepo(paths, repoRoot).catch(() => null);
|
|
231
239
|
const branch = options.branch ?? identity.branch;
|
|
232
|
-
const freshness = classifyCollectorFreshness(context, now);
|
|
233
240
|
const uploadSpool = await summarizeLocalUploadSpool(paths);
|
|
241
|
+
const freshness = classifyCollectorFreshness(context, uploadSpool.last_upload_success_at, now);
|
|
234
242
|
const uploadState = !config
|
|
235
243
|
? "not_installed"
|
|
236
244
|
: uploadSpool.pending_upload_count > 0
|
|
@@ -381,14 +389,14 @@ function workContextFile(paths, worktreeFingerprint) {
|
|
|
381
389
|
return path.join(paths.work_contexts_dir, `${worktreeFingerprint}.json`);
|
|
382
390
|
}
|
|
383
391
|
async function resolveIdentityOrFallback(repoRoot, branchOverride) {
|
|
384
|
-
const resolvedRoot =
|
|
392
|
+
const resolvedRoot = await stableWorktreeRoot(repoRoot);
|
|
385
393
|
const identity = await resolveRepoWorktreeIdentity(resolvedRoot).catch(() => null);
|
|
386
394
|
if (identity) {
|
|
387
395
|
return branchOverride ? { ...identity, branch: branchOverride } : identity;
|
|
388
396
|
}
|
|
389
397
|
const repoLabel = path.basename(resolvedRoot) || "workspace";
|
|
390
398
|
const repoFingerprint = `repo-${sha256(`local:${resolvedRoot}`).slice(0, 24)}`;
|
|
391
|
-
const worktreeFingerprint =
|
|
399
|
+
const worktreeFingerprint = stableWorktreeFingerprint(resolvedRoot);
|
|
392
400
|
const branch = branchOverride ?? (await resolveGitBranch(resolvedRoot));
|
|
393
401
|
return {
|
|
394
402
|
requested_path: resolvedRoot,
|
|
@@ -426,13 +434,24 @@ async function writeJsonFile(filePath, value) {
|
|
|
426
434
|
await fs.chmod(filePath, 0o600).catch(() => undefined);
|
|
427
435
|
}
|
|
428
436
|
}
|
|
429
|
-
function classifyCollectorFreshness(context, now) {
|
|
437
|
+
export function classifyCollectorFreshness(context, lastUploadSuccessAt, now) {
|
|
430
438
|
if (!context)
|
|
431
439
|
return "missing";
|
|
432
|
-
const
|
|
433
|
-
|
|
440
|
+
const latestActivityAt = latestTimestamp([
|
|
441
|
+
context.updated_at ?? context.started_at,
|
|
442
|
+
lastUploadSuccessAt,
|
|
443
|
+
]);
|
|
444
|
+
if (latestActivityAt === null)
|
|
434
445
|
return "stale";
|
|
435
|
-
return now.getTime() -
|
|
446
|
+
return now.getTime() - latestActivityAt <= 5 * 60 * 1000 ? "fresh" : "stale";
|
|
447
|
+
}
|
|
448
|
+
function latestTimestamp(values) {
|
|
449
|
+
const timestamps = values
|
|
450
|
+
.map((value) => Date.parse(value ?? ""))
|
|
451
|
+
.filter((value) => Number.isFinite(value));
|
|
452
|
+
if (timestamps.length === 0)
|
|
453
|
+
return null;
|
|
454
|
+
return Math.max(...timestamps);
|
|
436
455
|
}
|
|
437
456
|
function normalizeDashboardUrl(value) {
|
|
438
457
|
const normalized = value.trim().replace(/\/+$/, "");
|
package/dist/repo-identity.js
CHANGED
|
@@ -18,7 +18,7 @@ const SKIPPED_DIR_NAMES = new Set([
|
|
|
18
18
|
export async function resolveRepoWorktreeIdentity(repoRoot) {
|
|
19
19
|
const requestedPath = path.resolve(repoRoot);
|
|
20
20
|
const gitRoot = await runGit(["rev-parse", "--show-toplevel"], requestedPath);
|
|
21
|
-
const resolvedRoot =
|
|
21
|
+
const resolvedRoot = await stableWorktreeRoot(gitRoot.trim() || requestedPath);
|
|
22
22
|
const branch = await resolveGitBranchWithGit(resolvedRoot);
|
|
23
23
|
const headSha = await runGit(["rev-parse", "HEAD"], resolvedRoot).then((value) => value.trim() || null, () => null);
|
|
24
24
|
const absoluteGitDir = await runGit(["rev-parse", "--absolute-git-dir"], resolvedRoot)
|
|
@@ -36,11 +36,6 @@ export async function resolveRepoWorktreeIdentity(repoRoot) {
|
|
|
36
36
|
? `origin:${repoOriginUrl}`
|
|
37
37
|
: `local:${sha256(commonGitDir ?? resolvedRoot)}`;
|
|
38
38
|
const repoFingerprint = `repo-${sha256(repoMaterial).slice(0, 24)}`;
|
|
39
|
-
const worktreeMaterial = [
|
|
40
|
-
repoFingerprint,
|
|
41
|
-
resolvedRoot,
|
|
42
|
-
commonGitDir ?? "",
|
|
43
|
-
].join("\n");
|
|
44
39
|
const gitFilePath = path.join(resolvedRoot, ".git");
|
|
45
40
|
const worktreeIsPrimary = await fs.stat(gitFilePath).then((stat) => stat.isDirectory(), () => false);
|
|
46
41
|
return {
|
|
@@ -52,7 +47,7 @@ export async function resolveRepoWorktreeIdentity(repoRoot) {
|
|
|
52
47
|
branch,
|
|
53
48
|
head_sha: headSha,
|
|
54
49
|
worktree_label: path.basename(resolvedRoot),
|
|
55
|
-
worktree_fingerprint:
|
|
50
|
+
worktree_fingerprint: stableWorktreeFingerprint(resolvedRoot),
|
|
56
51
|
worktree_is_primary: worktreeIsPrimary,
|
|
57
52
|
};
|
|
58
53
|
}
|
|
@@ -144,10 +139,9 @@ async function hasGitMarker(dir) {
|
|
|
144
139
|
return fs.stat(path.join(dir, ".git")).then((stat) => stat.isDirectory() || stat.isFile(), () => false);
|
|
145
140
|
}
|
|
146
141
|
async function fallbackFilesystemIdentity(repoRoot) {
|
|
147
|
-
const resolvedRoot =
|
|
142
|
+
const resolvedRoot = await stableWorktreeRoot(repoRoot);
|
|
148
143
|
const repoLabel = path.basename(resolvedRoot) || "repo";
|
|
149
144
|
const repoFingerprint = `repo-${sha256(`local:${resolvedRoot}`).slice(0, 24)}`;
|
|
150
|
-
const worktreeFingerprint = `wt-${sha256(`${repoFingerprint}:${resolvedRoot}`).slice(0, 24)}`;
|
|
151
145
|
return {
|
|
152
146
|
requested_path: resolvedRoot,
|
|
153
147
|
repo_root: resolvedRoot,
|
|
@@ -157,10 +151,17 @@ async function fallbackFilesystemIdentity(repoRoot) {
|
|
|
157
151
|
branch: await resolveBranchFromHead(resolvedRoot),
|
|
158
152
|
head_sha: null,
|
|
159
153
|
worktree_label: repoLabel,
|
|
160
|
-
worktree_fingerprint:
|
|
154
|
+
worktree_fingerprint: stableWorktreeFingerprint(resolvedRoot),
|
|
161
155
|
worktree_is_primary: true,
|
|
162
156
|
};
|
|
163
157
|
}
|
|
158
|
+
export async function stableWorktreeRoot(repoRoot) {
|
|
159
|
+
const resolvedRoot = path.resolve(repoRoot);
|
|
160
|
+
return fs.realpath(resolvedRoot).catch(() => resolvedRoot);
|
|
161
|
+
}
|
|
162
|
+
export function stableWorktreeFingerprint(repoRoot) {
|
|
163
|
+
return `wt-${sha256(`worktree:${normalizeFingerprintPath(repoRoot)}`).slice(0, 24)}`;
|
|
164
|
+
}
|
|
164
165
|
async function resolveBranchFromHead(repoRoot) {
|
|
165
166
|
try {
|
|
166
167
|
const gitPath = path.join(repoRoot, ".git");
|
|
@@ -244,6 +245,10 @@ async function runGit(args, cwd) {
|
|
|
244
245
|
});
|
|
245
246
|
return stdout;
|
|
246
247
|
}
|
|
248
|
+
function normalizeFingerprintPath(repoRoot) {
|
|
249
|
+
const resolved = path.resolve(repoRoot);
|
|
250
|
+
return process.platform === "win32" ? resolved.toLowerCase() : resolved;
|
|
251
|
+
}
|
|
247
252
|
function sha256(value) {
|
|
248
253
|
return crypto.createHash("sha256").update(value, "utf8").digest("hex");
|
|
249
254
|
}
|
package/dist/upload.js
CHANGED
|
@@ -101,6 +101,7 @@ export async function buildLocalAmbientEnvelope(options = {}) {
|
|
|
101
101
|
collector_version: LOCAL_COLLECTOR_VERSION,
|
|
102
102
|
session_reference: sanitizeSessionReference(session),
|
|
103
103
|
work_context: uploadWorkContext,
|
|
104
|
+
worktree_inventory: options.worktreeInventory ?? [],
|
|
104
105
|
source_scan_results: sanitizeSourceScanResults(sourceCollection.scans, repoLabel),
|
|
105
106
|
events,
|
|
106
107
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bli-cockpit/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.29",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -26,6 +26,6 @@
|
|
|
26
26
|
"test": "node dist/cli.js --help && node ../../scripts/assert-public-package-pack.mjs --workspace=@bli-cockpit/cli"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@bli-cockpit/telemetry-core": "0.1.
|
|
29
|
+
"@bli-cockpit/telemetry-core": "0.1.9"
|
|
30
30
|
}
|
|
31
31
|
}
|