@bli-cockpit/cli 0.1.1 → 0.1.3

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
@@ -26,17 +26,25 @@ What happens:
26
26
  1. npm downloads the public `@bli-cockpit/cli` package.
27
27
  2. `cockpit onboard` writes local user config.
28
28
  3. Cockpit prints a dashboard pairing URL and code.
29
- 4. An Admin approves that exact code.
29
+ 4. Admin approves the pending request from Ambient -> Collector approvals, or
30
+ pastes the printed code there. The signed-in intern can still use the
31
+ printed URL.
30
32
  5. The CLI starts general ambient capture, uploads private raw evidence objects
31
33
  when present, then uploads one safe metadata/ref envelope.
32
34
  6. The CLI prints `PASS: Cockpit collector is ready for harvest.`
33
35
 
34
36
  `--device-name` is just a readable label in Cockpit. It can be
35
37
  `"Savina MacBook"`, `"Box VM 42"`, or the auto-filled macOS computer name.
38
+ On reused laptops or VMs, keep `--email <APPROVED_EMAIL>` in the command.
39
+ `cockpit onboard` skips pairing only when the existing valid session belongs to
40
+ that same email and dashboard URL; a different email or dashboard forces a new
41
+ approval.
36
42
 
37
43
  Before the command runs, an Admin must create or approve the email in the
38
44
  private dashboard. Default launch path is email + temporary password, handed
39
45
  over out of band. Magic link remains available as fallback.
46
+ For temporary-password accounts, the intern signs in directly; there is no
47
+ separate invite acceptance step.
40
48
 
41
49
  When ticket work starts later:
42
50
 
@@ -80,3 +88,5 @@ or deployment tokens to this CLI. The collector must never read env files.
80
88
 
81
89
  This public package intentionally excludes Cockpit admin bootstrap commands,
82
90
  service-role credential handling, source maps, tests, and internal runbooks.
91
+ Clean `npm pack` and `npm publish` run the public CLI build before packaging so
92
+ `dist/cli.js` is present in emergency releases.
@@ -1,5 +1,5 @@
1
1
  import { createCollectorServer } from "../server.js";
2
- import { DEFAULT_DASHBOARD_URL, inspectLocalCollectorStatus, installLocalCollector, logoutLocalCollector, pairLocalCollector, startLocalWorkContext, } from "../local-state.js";
2
+ import { DEFAULT_DASHBOARD_URL, getCollectorRuntimePaths, inspectLocalCollectorStatus, installLocalCollector, logoutLocalCollector, pairLocalCollector, readLocalCollectorSessionFile, readLocalSessionReference, startLocalWorkContext, } from "../local-state.js";
3
3
  import { syncLocalAmbientEnvelope } from "../upload.js";
4
4
  export const rootCommandNames = new Set([
5
5
  "onboard",
@@ -13,6 +13,10 @@ export const rootCommandNames = new Set([
13
13
  "serve",
14
14
  ]);
15
15
  export async function runLocalCockpitCli(argv, io = defaultIo()) {
16
+ if (isLocalHelpRequest(argv)) {
17
+ writeLine(io.stdout, localCommandHelp(argv[0]));
18
+ return 0;
19
+ }
16
20
  let command;
17
21
  try {
18
22
  command = parseLocalArgs(argv);
@@ -48,19 +52,98 @@ export async function runLocalCockpitCli(argv, io = defaultIo()) {
48
52
  return 1;
49
53
  }
50
54
  }
51
- export function localCommandHelp() {
55
+ export function localCommandHelp(command) {
56
+ if (command)
57
+ return localSubcommandHelp(command);
52
58
  return [
53
- " cockpit onboard [--ticket <id>] [--email <owner@email>] [--device-name <name>] [--dashboard-url <url>] [--repo <path>]",
54
- " cockpit install [--dashboard-url <url>] [--repo <path>]",
55
- " cockpit login [--email <owner@email>] [--device-name <name>] [--dashboard-url <url>]",
56
- " cockpit pair [--email <owner@email>] [--device-name <name>] [--dashboard-url <url>]",
59
+ " cockpit onboard [--ticket <id>] [--email <owner@email>] [--device-name <name>] [--dashboard-url <url>] [--repo <path>] [--json]",
60
+ " cockpit install [--dashboard-url <url>] [--repo <path>] [--json]",
61
+ " cockpit login [--email <owner@email>] [--device-name <name>] [--dashboard-url <url>] [--json]",
62
+ " cockpit pair [--email <owner@email>] [--device-name <name>] [--dashboard-url <url>] [--json]",
57
63
  " cockpit logout",
58
- " cockpit start [--ticket <id>] [--repo <path>] [--branch <name>]",
59
- " cockpit sync [--repo <path>] [--dashboard-url <url>]",
60
- " cockpit status [--repo <path>]",
64
+ " cockpit start [--ticket <id>] [--repo <path>] [--branch <name>] [--json]",
65
+ " cockpit sync [--repo <path>] [--dashboard-url <url>] [--json]",
66
+ " cockpit status [--repo <path>] [--json]",
61
67
  " cockpit serve [--port <port>] [--repo <path>]",
62
68
  ].join("\n");
63
69
  }
70
+ function localSubcommandHelp(command) {
71
+ const helpByCommand = new Map([
72
+ [
73
+ "onboard",
74
+ [
75
+ "Usage: cockpit onboard [--ticket <id>] [--email <owner@email>] [--device-name <name>] [--dashboard-url <url>] [--repo <path>] [--json]",
76
+ "",
77
+ "Installs, pairs, starts a work context, syncs once, and prints readiness proof.",
78
+ "Use --email on shared or reused machines; mismatched existing sessions are re-paired.",
79
+ ],
80
+ ],
81
+ [
82
+ "install",
83
+ [
84
+ "Usage: cockpit install [--dashboard-url <url>] [--repo <path>] [--json]",
85
+ "",
86
+ "Writes local collector config. Pair with `cockpit login`, then run `cockpit start` when work begins.",
87
+ ],
88
+ ],
89
+ [
90
+ "login",
91
+ [
92
+ "Usage: cockpit login [--email <owner@email>] [--device-name <name>] [--dashboard-url <url>] [--json]",
93
+ "",
94
+ "Starts dashboard device pairing and stores the approved local session.",
95
+ ],
96
+ ],
97
+ [
98
+ "pair",
99
+ [
100
+ "Usage: cockpit pair [--email <owner@email>] [--device-name <name>] [--dashboard-url <url>] [--json]",
101
+ "",
102
+ "Alias for `cockpit login`.",
103
+ ],
104
+ ],
105
+ ["logout", ["Usage: cockpit logout [--json]", "", "Removes the local device session."]],
106
+ [
107
+ "start",
108
+ [
109
+ "Usage: cockpit start [--ticket <id>] [--repo <path>] [--branch <name>] [--json]",
110
+ "",
111
+ "Starts local ambient capture. Add --ticket only when the work already has a visible ticket.",
112
+ ],
113
+ ],
114
+ [
115
+ "sync",
116
+ [
117
+ "Usage: cockpit sync [--repo <path>] [--dashboard-url <url>] [--json]",
118
+ "",
119
+ "Uploads the latest local ambient envelope or spools a safe retry if blocked.",
120
+ ],
121
+ ],
122
+ [
123
+ "status",
124
+ [
125
+ "Usage: cockpit status [--repo <path>] [--json]",
126
+ "",
127
+ "Prints install, pairing, active work, upload, and retry state.",
128
+ ],
129
+ ],
130
+ [
131
+ "serve",
132
+ [
133
+ "Usage: cockpit serve [--port <port>] [--repo <path>]",
134
+ "",
135
+ "Starts the local collector HTTP status server.",
136
+ ],
137
+ ],
138
+ ]);
139
+ return (helpByCommand.get(command) ?? [localCommandHelp()]).join("\n");
140
+ }
141
+ function isLocalHelpRequest(argv) {
142
+ const command = argv[0];
143
+ if (!command || !rootCommandNames.has(command))
144
+ return false;
145
+ return argv.length === 2 && (argv[1] === "--help" || argv[1] === "-h");
146
+ }
64
147
  function parseLocalArgs(argv) {
65
148
  const command = argv[0];
66
149
  switch (command) {
@@ -311,7 +394,7 @@ async function runInstall(command, io) {
311
394
  writeLine(io.stdout, `Config: ${result.paths.config_file}`);
312
395
  writeLine(io.stdout, `Session: ${result.paths.session_file}`);
313
396
  writeLine(io.stdout, "Auth: missing; upload stays local-only until pairing/login.");
314
- writeLine(io.stdout, "Next: run `cockpit login`, then `cockpit start --ticket <id>`.");
397
+ writeLine(io.stdout, "Next: run `cockpit login`, then `cockpit start` inside the repo; add `--ticket <id>` only when ticket work begins.");
315
398
  return 0;
316
399
  }
317
400
  async function runOnboard(command, io) {
@@ -340,12 +423,20 @@ async function runOnboard(command, io) {
340
423
  repoRoot: command.repoRoot,
341
424
  branch: command.branch,
342
425
  });
343
- if (installedStatus.session_state === "valid") {
426
+ const installedSession = await readOnboardSessionReuseCandidate(command.homeDir);
427
+ const canReuseInstalledSession = canReuseOnboardSession(installedSession, command.claimedOwnerEmail, command.dashboardUrl);
428
+ if (installedStatus.session_state === "valid" && canReuseInstalledSession) {
344
429
  if (!command.json) {
345
430
  writeLine(io.stdout, "2/5 Existing valid device session found; pairing skipped.");
346
431
  }
347
432
  }
348
433
  else {
434
+ if (installedStatus.session_state === "valid" && !canReuseInstalledSession) {
435
+ await logoutLocalCollector({ homeDir: command.homeDir });
436
+ if (!command.json) {
437
+ writeLine(io.stdout, "2/5 Existing valid device session does not match requested owner or dashboard; pairing again.");
438
+ }
439
+ }
349
440
  pair = await pairLocalCollector({
350
441
  homeDir: command.homeDir,
351
442
  dashboardUrl: command.dashboardUrl,
@@ -439,6 +530,36 @@ async function runOnboard(command, io) {
439
530
  return 1;
440
531
  }
441
532
  }
533
+ function canReuseOnboardSession(session, claimedOwnerEmail, dashboardUrl) {
534
+ if (session.session_state !== "valid")
535
+ return false;
536
+ const expectedEmail = normalizeEmailForComparison(claimedOwnerEmail);
537
+ if (expectedEmail && normalizeEmailForComparison(session.email) !== expectedEmail) {
538
+ return false;
539
+ }
540
+ const expectedDashboardUrl = normalizeUrlForComparison(dashboardUrl);
541
+ if (expectedDashboardUrl &&
542
+ normalizeUrlForComparison(session.dashboard_url) !== expectedDashboardUrl) {
543
+ return false;
544
+ }
545
+ return true;
546
+ }
547
+ function normalizeEmailForComparison(value) {
548
+ const normalized = value?.trim().toLowerCase();
549
+ return normalized ? normalized : null;
550
+ }
551
+ async function readOnboardSessionReuseCandidate(homeDir) {
552
+ const paths = getCollectorRuntimePaths(homeDir);
553
+ try {
554
+ return await readLocalCollectorSessionFile(paths);
555
+ }
556
+ catch {
557
+ return readLocalSessionReference(paths);
558
+ }
559
+ }
560
+ function normalizeUrlForComparison(value) {
561
+ return value ? normalizeUrl(value) : null;
562
+ }
442
563
  async function runLogin(command, io) {
443
564
  const result = await pairLocalCollector({
444
565
  homeDir: command.homeDir,
@@ -515,7 +636,7 @@ function nextStepForOnboardBlocker(blocker) {
515
636
  case "ticket_binding":
516
637
  return "Run `cockpit start --ticket <id>` when actual ticket work begins, then run `cockpit sync`.";
517
638
  case "device_pairing":
518
- return "Open the pairing URL, approve the exact code in Cockpit, then rerun `cockpit onboard`.";
639
+ return "Approve from Ambient -> Collector approvals, or paste the pairing code there, then rerun `cockpit onboard`.";
519
640
  case "network_or_ingest":
520
641
  return "Check dashboard URL/network, then run `cockpit sync --json` or rerun `cockpit onboard`.";
521
642
  case "install":
@@ -4,7 +4,7 @@ import fs from "node:fs/promises";
4
4
  import os from "node:os";
5
5
  import path from "node:path";
6
6
  import { summarizeLocalUploadSpool } from "./spool/local-spool.js";
7
- export const LOCAL_COLLECTOR_VERSION = "0.1.0";
7
+ export const LOCAL_COLLECTOR_VERSION = "0.1.3";
8
8
  export const DEFAULT_DASHBOARD_URL = "http://127.0.0.1:3100";
9
9
  export function getCollectorRuntimePaths(homeDir = os.homedir()) {
10
10
  const paths = getUserLocalCockpitPaths(homeDir);
package/dist/upload.js CHANGED
@@ -212,18 +212,17 @@ function makeSourceScanCompletedEvent(options) {
212
212
  }
213
213
  const rawEvidencePointers = options.rawEvidenceFacts?.pointers ?? [];
214
214
  const hasRawEvidence = rawEvidencePointers.length > 0;
215
+ const eventPrivacyClassification = hasRawEvidence
216
+ ? "redacted_summary"
217
+ : "metadata";
215
218
  return TelemetryIngestEventDtoSchema.parse({
216
219
  event_id: `ambient-sync:${options.context.work_context_id}:${options.generatedAt}`,
217
220
  event_type: "source_scan_completed",
218
221
  occurred_at: options.generatedAt,
219
222
  provenance: options.context.provenance,
220
- privacy_classification: hasRawEvidence
221
- ? "remote_durable_raw_evidence"
222
- : "metadata",
223
+ privacy_classification: eventPrivacyClassification,
223
224
  redaction: {
224
- privacy_classification: hasRawEvidence
225
- ? "remote_durable_raw_evidence"
226
- : "metadata",
225
+ privacy_classification: eventPrivacyClassification,
227
226
  redaction_status: hasRawEvidence
228
227
  ? "raw_remote_durable"
229
228
  : "metadata_only",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bli-cockpit/cli",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "bin": {
@@ -19,6 +19,7 @@
19
19
  "scripts": {
20
20
  "prebuild": "npm run build --workspace=@bli-cockpit/local-collector",
21
21
  "build": "node ../../scripts/build-public-cli.mjs",
22
+ "prepack": "npm run build",
22
23
  "pretypecheck": "npm run build",
23
24
  "typecheck": "node -e \"await import('./dist/commands/public-root.js')\"",
24
25
  "pretest": "npm run build",