@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 +11 -1
- package/dist/commands/local.js +133 -12
- package/dist/local-state.js +1 -1
- package/dist/upload.js +5 -6
- package/package.json +2 -1
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.
|
|
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.
|
package/dist/commands/local.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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 "
|
|
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":
|
package/dist/local-state.js
CHANGED
|
@@ -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.
|
|
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:
|
|
221
|
-
? "remote_durable_raw_evidence"
|
|
222
|
-
: "metadata",
|
|
223
|
+
privacy_classification: eventPrivacyClassification,
|
|
223
224
|
redaction: {
|
|
224
|
-
privacy_classification:
|
|
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.
|
|
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",
|