@bli-cockpit/cli 0.1.27 → 0.1.28

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 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
- `npm install -g @bli-cockpit/cli@latest`, then run
50
- `cockpit onboard` from anywhere to refresh pairing, agent rules, autostart, and
51
- an initial sync against saved roots. `--repo <path>` remains supported for older
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
@@ -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 parseOnboardArgs(args) {
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, "onboard");
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: [
@@ -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,6 +85,8 @@ 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]",
@@ -88,6 +98,7 @@ export function localCommandHelp(command) {
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
  [
@@ -222,6 +252,17 @@ function localSubcommandHelp(command) {
222
252
  "Action defaults to `install`.",
223
253
  ],
224
254
  ],
255
+ [
256
+ "release",
257
+ [
258
+ "Usage: cockpit release [--dry-run] [--skip-checks] [--tag <tag>] [--access <public|restricted>] [--otp <code>]",
259
+ "",
260
+ "Maintainer-only helper. Run from inside the bli-cockpit repo checkout.",
261
+ "Requires a clean `main` branch and runs `git pull --ff-only` before publishing.",
262
+ "Delegates to `npm run publish:public -- ...` so public packages are",
263
+ "built, checked, and published in the safe telemetry-core then CLI order.",
264
+ ],
265
+ ],
225
266
  ]);
226
267
  return (helpByCommand.get(command) ?? [localCommandHelp()]).join("\n");
227
268
  }
@@ -244,6 +285,186 @@ async function runInstall(command, io) {
244
285
  writeLine(io.stdout, "Next: run `cockpit login`, then `cockpit start` inside the repo; add `--ticket <id>` only when ticket work begins.");
245
286
  return 0;
246
287
  }
288
+ async function runUpdate(command, io) {
289
+ const exec = io.exec ?? defaultExec();
290
+ const installArgs = [
291
+ "install",
292
+ "-g",
293
+ "@bli-cockpit/cli@latest",
294
+ "--prefer-online",
295
+ ];
296
+ if (!command.json) {
297
+ writeLine(io.stdout, "Updating Cockpit CLI from npm...");
298
+ }
299
+ const install = await exec("npm", installArgs);
300
+ writeExecOutput(io, install, { stdout: !command.json, stderr: true });
301
+ if (install.code !== 0) {
302
+ if (command.json) {
303
+ writeLine(io.stdout, JSON.stringify({
304
+ status: "blocked",
305
+ step: "npm_install",
306
+ command: `npm ${installArgs.join(" ")}`,
307
+ exit_code: install.code,
308
+ }, null, 2));
309
+ }
310
+ else {
311
+ writeLine(io.stderr, "BLOCKED: npm install failed; Cockpit CLI was not refreshed.");
312
+ }
313
+ return install.code || 1;
314
+ }
315
+ if (!command.json) {
316
+ writeLine(io.stdout, "Cockpit CLI updated. Rechecking onboarding...");
317
+ }
318
+ const onboard = await exec("cockpit", [
319
+ "onboard",
320
+ ...updateOnboardArgs(command),
321
+ ]);
322
+ writeExecOutput(io, onboard, { stdout: true, stderr: true });
323
+ return onboard.code;
324
+ }
325
+ async function runRelease(command, io) {
326
+ const releaseRoot = await findPublicReleaseRoot(process.cwd());
327
+ if (!releaseRoot) {
328
+ writeLine(io.stderr, "cockpit release must be run inside the bli-cockpit repo checkout (missing publish:public script).");
329
+ return 1;
330
+ }
331
+ const exec = io.exec ?? defaultExec();
332
+ const gitReady = await prepareReleaseMainBranch(releaseRoot, exec, io);
333
+ if (!gitReady)
334
+ return 1;
335
+ writeLine(io.stdout, "Running Cockpit public package release...");
336
+ const npmArgs = ["--prefix", releaseRoot, "run", "publish:public"];
337
+ if (command.args.length > 0)
338
+ npmArgs.push("--", ...command.args);
339
+ const releaseExec = io.interactiveExec ?? defaultInteractiveExec();
340
+ const result = await releaseExec("npm", npmArgs);
341
+ writeExecOutput(io, result, { stdout: true, stderr: true });
342
+ return result.code;
343
+ }
344
+ async function prepareReleaseMainBranch(releaseRoot, exec, io) {
345
+ const branch = await exec("git", [
346
+ "-C",
347
+ releaseRoot,
348
+ "rev-parse",
349
+ "--abbrev-ref",
350
+ "HEAD",
351
+ ]);
352
+ writeExecOutput(io, branch, { stdout: false, stderr: true });
353
+ if (branch.code !== 0) {
354
+ writeLine(io.stderr, "BLOCKED: cockpit release could not read the current git branch.");
355
+ return false;
356
+ }
357
+ const currentBranch = branch.stdout.trim();
358
+ if (currentBranch !== "main") {
359
+ writeLine(io.stderr, `BLOCKED: cockpit release only publishes from main. Current branch is ${currentBranch || "unknown"}.`);
360
+ writeLine(io.stderr, "Merge the release changes, switch to main, then rerun `cockpit release`.");
361
+ return false;
362
+ }
363
+ const status = await exec("git", [
364
+ "-C",
365
+ releaseRoot,
366
+ "status",
367
+ "--porcelain",
368
+ ]);
369
+ writeExecOutput(io, status, { stdout: false, stderr: true });
370
+ if (status.code !== 0) {
371
+ writeLine(io.stderr, "BLOCKED: cockpit release could not inspect git status.");
372
+ return false;
373
+ }
374
+ if (status.stdout.trim()) {
375
+ writeLine(io.stderr, "BLOCKED: cockpit release requires a clean main checkout.");
376
+ writeLine(io.stderr, "Commit or discard local changes, then rerun `cockpit release`.");
377
+ return false;
378
+ }
379
+ writeLine(io.stdout, "Syncing main with git pull --ff-only...");
380
+ const pull = await exec("git", ["-C", releaseRoot, "pull", "--ff-only"]);
381
+ writeExecOutput(io, pull, { stdout: true, stderr: true });
382
+ if (pull.code !== 0) {
383
+ writeLine(io.stderr, "BLOCKED: git pull --ff-only failed; main is not safely current.");
384
+ return false;
385
+ }
386
+ return true;
387
+ }
388
+ async function findPublicReleaseRoot(startDir) {
389
+ let current = path.resolve(startDir);
390
+ while (true) {
391
+ const packageJsonPath = path.join(current, "package.json");
392
+ try {
393
+ const packageJson = JSON.parse(await readFile(packageJsonPath, "utf8"));
394
+ if (packageJson.scripts?.["publish:public"] !== undefined)
395
+ return current;
396
+ }
397
+ catch {
398
+ // Keep walking: nested packages may be missing package.json or have one
399
+ // without the release script.
400
+ }
401
+ const parent = path.dirname(current);
402
+ if (parent === current)
403
+ return null;
404
+ current = parent;
405
+ }
406
+ }
407
+ function updateOnboardArgs(command) {
408
+ const args = [];
409
+ if (command.homeDir)
410
+ args.push("--home", command.homeDir);
411
+ for (const root of updateCollectionRoots(command)) {
412
+ args.push("--workspace", root);
413
+ }
414
+ if (command.dashboardUrlExplicit) {
415
+ args.push("--dashboard-url", command.dashboardUrl);
416
+ }
417
+ if (command.claimedOwnerEmail)
418
+ args.push("--email", command.claimedOwnerEmail);
419
+ if (command.deviceName)
420
+ args.push("--device-name", command.deviceName);
421
+ if (command.activeTicketId)
422
+ args.push("--ticket", command.activeTicketId);
423
+ if (command.branch)
424
+ args.push("--branch", command.branch);
425
+ if (command.pollIntervalMs !== undefined) {
426
+ args.push("--poll-interval-ms", String(command.pollIntervalMs));
427
+ }
428
+ if (command.timeoutMs !== undefined) {
429
+ args.push("--timeout-ms", String(command.timeoutMs));
430
+ }
431
+ if (command.maxDepth !== undefined)
432
+ args.push("--max-depth", String(command.maxDepth));
433
+ if (command.maxRepos !== undefined)
434
+ args.push("--max-repos", String(command.maxRepos));
435
+ if (command.json)
436
+ args.push("--json");
437
+ return args;
438
+ }
439
+ function updateCollectionRoots(command) {
440
+ const roots = command.collectionRoots?.length
441
+ ? command.collectionRoots
442
+ : command.repoRoot
443
+ ? [command.repoRoot]
444
+ : [];
445
+ const seen = new Set();
446
+ const deduped = [];
447
+ for (const root of roots) {
448
+ if (seen.has(root))
449
+ continue;
450
+ seen.add(root);
451
+ deduped.push(root);
452
+ }
453
+ return deduped;
454
+ }
455
+ function writeExecOutput(io, result, options) {
456
+ if (options.stdout)
457
+ writeRaw(io.stdout, result.stdout);
458
+ if (options.stderr)
459
+ writeRaw(io.stderr, result.stderr);
460
+ }
461
+ function writeRaw(stream, text) {
462
+ if (!text)
463
+ return;
464
+ stream.write(text);
465
+ if (!text.endsWith("\n"))
466
+ stream.write("\n");
467
+ }
247
468
  function isInteractiveStdin(io) {
248
469
  return Boolean(io.stdin.isTTY);
249
470
  }
@@ -1343,6 +1564,17 @@ function defaultExec() {
1343
1564
  });
1344
1565
  });
1345
1566
  }
1567
+ function defaultInteractiveExec() {
1568
+ return (cmd, args) => new Promise((resolve) => {
1569
+ const child = spawn(cmd, args, { stdio: "inherit" });
1570
+ child.on("error", (error) => {
1571
+ resolve({ code: 1, stdout: "", stderr: errorMessage(error) });
1572
+ });
1573
+ child.on("close", (code) => {
1574
+ resolve({ code: code ?? 1, stdout: "", stderr: "" });
1575
+ });
1576
+ });
1577
+ }
1346
1578
  function defaultIo() {
1347
1579
  if (!globalThis.fetch) {
1348
1580
  throw new Error("global fetch is unavailable; use Node.js 20 or newer.");
@@ -1354,6 +1586,7 @@ function defaultIo() {
1354
1586
  env: process.env,
1355
1587
  fetch: globalThis.fetch.bind(globalThis),
1356
1588
  exec: defaultExec(),
1589
+ interactiveExec: defaultInteractiveExec(),
1357
1590
  };
1358
1591
  }
1359
1592
  function writeLine(stream, text) {
@@ -22,13 +22,15 @@ function cockpitHelp() {
22
22
  "Usage:",
23
23
  localCommandHelp(),
24
24
  "",
25
- "Install/update: `npm install -g @bli-cockpit/cli@latest`.",
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: rerun `cockpit onboard` from anywhere to refresh pairing, roots, agent rules, autostart, and sync.",
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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bli-cockpit/cli",
3
- "version": "0.1.27",
3
+ "version": "0.1.28",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "bin": {