@bli-cockpit/cli 0.1.33 → 0.2.0
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 +112 -202
- package/dist/commands/local-args.js +4 -0
- package/dist/commands/local.js +154 -16
- package/dist/local-state.js +6 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,251 +1,161 @@
|
|
|
1
1
|
# Cockpit CLI
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Cockpit watches how you and your AI agents work so Edward can coach from evidence instead of screenshots and status pings.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
CLI pairs a local laptop with the private Cockpit dashboard, records safe
|
|
7
|
-
work context, uploads consented raw Codex/Claude/diff evidence and explicit
|
|
8
|
-
images attached into agent sessions to durable private storage, and uploads
|
|
9
|
-
metadata refs after dashboard-approved
|
|
10
|
-
device pairing.
|
|
5
|
+
## 90-second speedrun
|
|
11
6
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
Run this in a terminal. If you are inside `~/BLI`, Cockpit suggests that
|
|
15
|
-
workspace root; otherwise it looks for likely roots such as `~/BLI` and asks
|
|
16
|
-
before collecting. Later reruns remember the approved roots:
|
|
7
|
+
Run these three commands on the Mac that should be collected:
|
|
17
8
|
|
|
18
9
|
```bash
|
|
19
|
-
npm install -g @bli-cockpit/cli@latest
|
|
20
|
-
|
|
10
|
+
$ npm install -g @bli-cockpit/cli@latest
|
|
11
|
+
added <packages> in <seconds>s
|
|
21
12
|
```
|
|
22
13
|
|
|
23
|
-
Interactive onboarding prompts for the dashboard email. Enter the approved
|
|
24
|
-
intern/operator email. For reused laptops, VMs, or headless/agent-run setup,
|
|
25
|
-
pass the email and root explicitly:
|
|
26
|
-
|
|
27
14
|
```bash
|
|
28
|
-
cockpit onboard
|
|
15
|
+
$ cockpit onboard
|
|
16
|
+
Cockpit harvest onboarding
|
|
17
|
+
Dashboard: https://bli-cockpit-dashboard.vercel.app
|
|
18
|
+
Ticket: general ambient
|
|
19
|
+
What's your @buildlaunchiterate.ca email? (press enter to skip): ian@buildlaunchiterate.ca
|
|
20
|
+
Signing in as ian@buildlaunchiterate.ca.
|
|
21
|
+
Code sent; valid 1h, resend in 60s by rerunning this command.
|
|
22
|
+
Enter the 6-digit code: 482913
|
|
23
|
+
Signed in as ian@buildlaunchiterate.ca.
|
|
24
|
+
2/5 Device paired.
|
|
25
|
+
PASS: Cockpit collector is ready for harvest.
|
|
26
|
+
You're live.
|
|
27
|
+
Dashboard: https://bli-cockpit-dashboard.vercel.app/my-work
|
|
28
|
+
Next: cockpit status
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
-
If a work folder contains multiple repos, confirm the parent folder as the
|
|
32
|
-
collection root. In a headless setup, pass it explicitly:
|
|
33
|
-
|
|
34
31
|
```bash
|
|
35
|
-
|
|
36
|
-
|
|
32
|
+
$ cockpit status
|
|
33
|
+
Install: ready
|
|
34
|
+
Auth: paired
|
|
35
|
+
Upload state: ready
|
|
37
36
|
```
|
|
38
37
|
|
|
39
|
-
|
|
38
|
+
The OTP proves you own an approved BLI mailbox. The JWT is used once to register this device and is never saved; the durable local credential remains the existing Cockpit device token.
|
|
39
|
+
|
|
40
|
+
## Every command, what it does, and why it's called that
|
|
41
|
+
|
|
42
|
+
You only need three commands (`onboard`, `status`, and `backfill` once). The rest exist for
|
|
43
|
+
recovery and maintenance.
|
|
44
|
+
|
|
45
|
+
| Command | What it does | Why it exists / why this name |
|
|
46
|
+
|---|---|---|
|
|
47
|
+
| `cockpit onboard` | The everything-command: signs you in (email code), registers this Mac, starts capture, uploads once, installs the 15-min background sync, and prints proof you're live. | You are boarding the crew. Run it once per machine; rerunning is always safe. |
|
|
48
|
+
| `cockpit status` | Prints install / sign-in / capture / upload health in one screen. | The "is it working?" command. Run it whenever you're unsure. |
|
|
49
|
+
| `cockpit backfill --all` | Uploads your HISTORICAL Codex + Claude sessions (from before Cockpit existed on this Mac). | One-time catch-up so your past work counts too. "Backfill" = fill in the back-catalog. |
|
|
50
|
+
| `cockpit sync` | Captures and uploads once, right now. This is what the background agent runs every 15 min — you almost never type it yourself. | Named for what it does: synchronize local session files up to the dashboard. |
|
|
51
|
+
| `cockpit start --ticket <id>` | Tags your CURRENT work with a Linear ticket so sessions attribute to it. `--clear-ticket` returns to general capture. | "Start (working on) X." Agents usually run this for you per the agent rules. |
|
|
52
|
+
| `cockpit login` / `cockpit pair` | Just the sign-in + device-registration step, standalone (onboard already includes it). Two names, one command: `login` is what humans guess, `pair` is what the dashboard's device screen calls it. | Recovery path when a session expires or you switch accounts. |
|
|
53
|
+
| `cockpit logout` | Deletes this machine's session. | The undo of login. |
|
|
54
|
+
| `cockpit update` / `cockpit upgrade` | Updates the CLI itself from npm, then reruns onboard. Two names, one command — aliases because half of people guess each. | Self-update; run when Edward announces a new version. |
|
|
55
|
+
| `cockpit install` | Writes local config only (step 1 of onboard, standalone). | Plumbing; exists so onboard's pieces are individually runnable. You'll likely never type it. |
|
|
56
|
+
| `cockpit sessions` | Read-only: lists your observed sessions and how each attributed. | Debugging "why isn't my session showing up?" without uploading anything. |
|
|
57
|
+
| `cockpit autostart install\|uninstall\|status` | Manages the macOS launchd agent behind the 15-min background sync. | Direct control of the background piece when you need to stop/inspect it. |
|
|
58
|
+
| `cockpit agent-rules install\|uninstall\|status` | Manages the Cockpit ticket-binding text block inside `~/.codex/AGENTS.md` / `~/.claude/CLAUDE.md`. | It's literally "the rules your agents read." Passive markdown — never an executable hook. |
|
|
59
|
+
| `cockpit serve` | Runs a tiny local HTTP status server. | Niche: lets other tools ask the collector how it's doing. |
|
|
60
|
+
| `cockpit logout`, `cockpit release`, `cockpit admin *` | `release` publishes the CLI (maintainer, repo-gated); `admin` (bootstrap/invite) is absent from the public build entirely. | Maintainer surface — interns never touch these. |
|
|
61
|
+
|
|
62
|
+
Honest naming notes: `update`/`upgrade` and `login`/`pair` are deliberate synonym-aliases (cheap
|
|
63
|
+
forgiveness beats a "command not found"); `install` is a misleading name for "write config" but
|
|
64
|
+
is kept for onboard-step symmetry.
|
|
65
|
+
|
|
66
|
+
## Flags
|
|
67
|
+
|
|
68
|
+
Normal setup needs no flags. Use flags only when the default guess is wrong:
|
|
40
69
|
|
|
41
70
|
```bash
|
|
42
|
-
cockpit onboard --email <
|
|
71
|
+
cockpit onboard --email <you@buildlaunchiterate.ca> --workspace ~/BLI
|
|
72
|
+
cockpit onboard --workspace ~/BLI --workspace ~/side-projects
|
|
73
|
+
cockpit onboard --device-name "Ian MacBook"
|
|
74
|
+
cockpit onboard --no-auth
|
|
43
75
|
```
|
|
44
76
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
For intern machines, `cockpit onboard` refreshes the launchd autostart agent so
|
|
64
|
-
Cockpit syncs confirmed roots at login and every 15 minutes. For a manual
|
|
65
|
-
repair path, run `cockpit autostart install --workspace <root>` later.
|
|
77
|
+
- `--email` skips the email prompt.
|
|
78
|
+
- `--workspace` pins one collection root; repeat it for multiple unrelated roots.
|
|
79
|
+
- `--device-name` changes only the human label shown in Cockpit.
|
|
80
|
+
- `--dashboard-url` is for staging/custom dashboards only. Production is the default.
|
|
81
|
+
- `--no-auth` forces the old manual approval queue.
|
|
82
|
+
- `--repo` still works as a legacy alias for `--workspace`.
|
|
83
|
+
|
|
84
|
+
`cockpit update` installs the latest public CLI and reruns onboarding checks against saved roots. `cockpit upgrade` is the same command.
|
|
85
|
+
|
|
86
|
+
## Parent Mode
|
|
87
|
+
|
|
88
|
+
Use a parent folder such as `~/BLI` when it contains multiple repos. Cockpit scans child git repos/worktrees, creates one stable work context per worktree, and rolls them up under repo rows in the dashboard.
|
|
89
|
+
|
|
90
|
+
Defaults: 3 folder levels deep, up to 50 repos. Tune with `--max-depth` and `--max-repos`.
|
|
91
|
+
|
|
92
|
+
## Pairing Recovery
|
|
93
|
+
|
|
94
|
+
The v2 happy path auto-approves pairing after the email OTP. Manual approval still exists for recovery:
|
|
66
95
|
|
|
67
96
|
```bash
|
|
68
|
-
|
|
69
|
-
cockpit agent-rules install --workspace <root>
|
|
97
|
+
cockpit onboard --no-auth --email <you@buildlaunchiterate.ca> --workspace ~/BLI
|
|
70
98
|
```
|
|
71
99
|
|
|
72
|
-
|
|
73
|
-
the Cockpit rule to bind known Linear tickets before edits, or ask once when
|
|
74
|
-
the ticket ID is missing inside that workspace. That binding starts attributing
|
|
75
|
-
the session's work to the specific ticket in Cockpit. It checks for managed or
|
|
76
|
-
equivalent guidance first, so current files are left unchanged and stale Cockpit
|
|
77
|
-
ticket-binding sections are replaced.
|
|
78
|
-
|
|
79
|
-
Parent mode scans child git repos/worktrees (3 folder levels deep, up to 50
|
|
80
|
-
repos by default — tune with `--max-depth` / `--max-repos`; a warning prints
|
|
81
|
-
if the repo cap truncates discovery), creates one stable work context per
|
|
82
|
-
worktree, and the dashboard rolls them up under one repo row with worktree
|
|
83
|
-
drilldown. Repos cloned later are picked up automatically: `cockpit sync`
|
|
84
|
-
starts a general ambient work context for newly discovered repos on its own. Device approval is still one time per laptop/dashboard/email. Codex
|
|
85
|
-
JSONL transcripts are attributed to a repo/worktree deterministically (session
|
|
86
|
-
cwd, workspace roots, git origin/branch/commit signals) and upload only into
|
|
87
|
-
that repo's evidence lane; transcripts that cannot be attributed to exactly one
|
|
88
|
-
worktree are reported as ambiguous/unattributed with reason labels instead of
|
|
89
|
-
being duplicated across repos or dropped.
|
|
90
|
-
|
|
91
|
-
What happens:
|
|
92
|
-
|
|
93
|
-
1. npm installs or updates the public `@bli-cockpit/cli` package.
|
|
94
|
-
2. `cockpit onboard` writes local user config.
|
|
95
|
-
3. Cockpit prints a dashboard pairing URL and code.
|
|
96
|
-
4. Admin approves the persistent email access request from Ambient -> Collector
|
|
97
|
-
approvals. Pair codes still expire, but the access request remains. If the
|
|
98
|
-
terminal is still polling, approval pairs it immediately; if not, the intern
|
|
99
|
-
reruns `cockpit onboard --email <APPROVED_EMAIL> --workspace ~/BLI` or
|
|
100
|
-
simply `cockpit onboard` after roots are saved, and the stored approval
|
|
101
|
-
auto-pairs without another Admin action. Admin can still paste the printed
|
|
102
|
-
code there as a rescue path. The signed-in intern can still use the printed
|
|
103
|
-
URL. If the claimed email is not an app user yet, a super admin approval
|
|
104
|
-
creates the user, adds team membership, and sends the Supabase invite flow
|
|
105
|
-
automatically.
|
|
106
|
-
5. The CLI starts general ambient capture, uploads private raw evidence objects
|
|
107
|
-
when present (chunked and resumable, with identical content acknowledged
|
|
108
|
-
instead of re-uploaded), then uploads one safe metadata/ref envelope. Text
|
|
109
|
-
evidence with secret-shaped values is sanitized before storage and marked as
|
|
110
|
-
partial evidence with redaction metadata; env files are still never read.
|
|
111
|
-
6. The CLI prints `PASS: Cockpit collector is ready for harvest.` and a final
|
|
112
|
-
`You're live.` block with collection roots, pairing state, background sync
|
|
113
|
-
state, dashboard link, and `cockpit status` as the next command.
|
|
114
|
-
|
|
115
|
-
`--device-name` is optional and only a readable label in Cockpit. Pass it only
|
|
116
|
-
when you want a specific label such as `"Savina MacBook"` or `"Box VM 42"`.
|
|
117
|
-
On reused laptops or VMs, keep `--email <APPROVED_EMAIL>` in the command.
|
|
118
|
-
`cockpit onboard` skips pairing only when the existing valid session belongs to
|
|
119
|
-
that same email and dashboard URL (the production default unless overridden); a
|
|
120
|
-
different email or dashboard forces a new approval.
|
|
121
|
-
|
|
122
|
-
Before the command runs, an Admin does not need to open Supabase or pre-create
|
|
123
|
-
ordinary interns in Settings -> Team. Approval of the claimed email in Ambient
|
|
124
|
-
creates the missing app user and stores the durable access approval. Settings
|
|
125
|
-
-> Team remains available for manual pre-provisioning, including
|
|
126
|
-
temporary-password launch. For temporary-password accounts, the intern signs in
|
|
127
|
-
directly; there is no separate invite acceptance step.
|
|
128
|
-
|
|
129
|
-
When ticket work starts later:
|
|
100
|
+
Use that when the mailbox is unavailable, auth routes are down, or Edward has added an exact non-domain email exception. Cockpit prints a URL and code; Admin approves from Ambient -> Collector approvals. If the code expires, rerun `cockpit onboard` after approval and the durable access row will auto-pair.
|
|
130
101
|
|
|
131
|
-
|
|
132
|
-
cockpit start \
|
|
133
|
-
--ticket <ticket-id> \
|
|
134
|
-
--workspace "$PWD"
|
|
102
|
+
If this is a reused laptop or VM, keep `--email` set. Cockpit reuses an existing session only when it belongs to that same email and dashboard URL.
|
|
135
103
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
```
|
|
104
|
+
## Ticket Semantics
|
|
105
|
+
|
|
106
|
+
Setup is general ambient capture. Do not invent a ticket for onboarding.
|
|
140
107
|
|
|
141
|
-
|
|
142
|
-
Only pass `--ticket` when the work really belongs to a visible ticket.
|
|
143
|
-
Omitting `--ticket` preserves any existing ticket binding; run
|
|
144
|
-
`cockpit start --clear-ticket --workspace "$PWD"` when you intentionally want
|
|
145
|
-
to return a previously bound work context to general ambient capture.
|
|
108
|
+
When real ticket work starts:
|
|
146
109
|
|
|
147
|
-
|
|
148
|
-
|
|
110
|
+
```bash
|
|
111
|
+
cockpit start --ticket <ticket-id> --workspace "$PWD"
|
|
112
|
+
cockpit sync --workspace "$PWD" --json
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
For important ticketless work:
|
|
149
116
|
|
|
150
117
|
```bash
|
|
151
118
|
cockpit start \
|
|
152
119
|
--workspace "$PWD" \
|
|
153
|
-
--topic "lead ingestion
|
|
120
|
+
--topic "lead ingestion planning" \
|
|
154
121
|
--intent planning \
|
|
155
122
|
--phase discovery \
|
|
156
123
|
--intent-confidence 0.9
|
|
157
|
-
|
|
158
|
-
cockpit sync \
|
|
159
|
-
--workspace "$PWD" \
|
|
160
|
-
--json
|
|
161
124
|
```
|
|
162
125
|
|
|
163
|
-
|
|
164
|
-
`root_cause_analysis`, `planning`, `discovery`, `review`, `testing`,
|
|
165
|
-
`documentation`, `release`, `learning`, `coordination`, `maintenance`,
|
|
166
|
-
`analysis`, `unknown`, and `other`. Supported `--phase` values are `planning`,
|
|
167
|
-
`discovery`, `implementation`, `debugging`, `review`, `testing`,
|
|
168
|
-
`documentation`, `release`, `handoff`, `analysis`, `unknown`, and `other`.
|
|
169
|
-
Use `--topic-summary` only for short redacted summaries, not transcript text.
|
|
126
|
+
`cockpit onboard` also refreshes managed ticket-binding guidance in `~/.codex/AGENTS.md` and `~/.claude/CLAUDE.md` so agents bind visible Linear tickets before edits inside the confirmed workspace.
|
|
170
127
|
|
|
171
|
-
##
|
|
128
|
+
## Storage Layout
|
|
172
129
|
|
|
173
130
|
Local files:
|
|
174
131
|
|
|
175
|
-
- `~/.config/bli-cockpit/config.json`: dashboard URL
|
|
176
|
-
|
|
177
|
-
- `~/.
|
|
178
|
-
- `~/.local/state/bli-cockpit/
|
|
179
|
-
-
|
|
180
|
-
already-durable evidence so repeated syncs skip re-uploading the same
|
|
181
|
-
content (no raw content is stored in the cursor).
|
|
182
|
-
- `~/.local/state/bli-cockpit/cursors/backfill.json`: high-water progress for
|
|
183
|
-
the explicit `cockpit backfill` command.
|
|
184
|
-
- `.codex-autorunner/contextspace/active_context.md` in the work repo when a
|
|
185
|
-
work context is active.
|
|
186
|
-
|
|
187
|
-
Remote dashboard:
|
|
188
|
-
|
|
189
|
-
- approved user and device identity;
|
|
190
|
-
- repo/worktree fingerprints, branch, head SHA, optional ticket, source
|
|
191
|
-
availability, risk flags, and upload timestamps;
|
|
192
|
-
- raw evidence refs accepted by `/api/ambient/ingest`;
|
|
193
|
-
- durable private Storage objects accepted by the chunked
|
|
194
|
-
`/api/ambient/evidence/upload/begin|chunk|commit` endpoints, tracked in a
|
|
195
|
-
durable per-object upload ledger. When deterministic sanitization was
|
|
196
|
-
applied, the evidence ref stores the redaction metadata and sanitized object
|
|
197
|
-
hash/size;
|
|
198
|
-
- attached image artifact metadata accepted by `/api/ambient/agent-artifacts`
|
|
199
|
-
for screenshots/images explicitly attached into Codex or Claude sessions;
|
|
200
|
-
- Codex session attribution records (session id, file hash, attribution state
|
|
201
|
-
and reason labels, scores) accepted by `/api/ambient/codex-sessions`.
|
|
202
|
-
The local collector scans both active `~/.codex/sessions` files and archived
|
|
203
|
-
`~/.codex/archived_sessions` files, using only `session_meta` and
|
|
204
|
-
`turn_context` metadata for attribution. Transcript bytes are sanitized or
|
|
205
|
-
blocked later by raw evidence collection before upload.
|
|
206
|
-
|
|
207
|
-
New raw evidence object keys are readable from the Storage browser:
|
|
208
|
-
|
|
209
|
-
```text
|
|
210
|
-
operators/<operator-slug>-<operator-short>/repos/<repo-slug>/worktrees/<worktree-slug>/tickets/<ticket-id-or-unbound>/dates/<YYYY-MM-DD>/sessions/<session-slug>/ids/<operator_user_id>/<work_context_id>/<source>/<source-path>
|
|
211
|
-
```
|
|
132
|
+
- `~/.config/bli-cockpit/config.json`: dashboard URL, device label, collection roots.
|
|
133
|
+
- `~/.config/bli-cockpit/session.json`: paired device token and owner metadata.
|
|
134
|
+
- `~/.local/state/bli-cockpit/spool/`: safe retry records.
|
|
135
|
+
- `~/.local/state/bli-cockpit/cursors/`: upload/backfill cursors, no raw content.
|
|
136
|
+
- `.codex-autorunner/contextspace/active_context.md`: current work context inside a repo.
|
|
212
137
|
|
|
213
|
-
|
|
214
|
-
`ambient_evidence_refs.raw_evidence_pointer_id`, `storage_bucket`, and
|
|
215
|
-
`object_key`. For SQL recipes that reconstruct a day of work by operator, date,
|
|
216
|
-
repo, ticket, and raw JSONL object, see
|
|
217
|
-
[`docs/runbooks/cockpit-data-traceability.md`](../../docs/runbooks/cockpit-data-traceability.md).
|
|
138
|
+
Remote data:
|
|
218
139
|
|
|
219
|
-
|
|
140
|
+
- approved user/device identity;
|
|
141
|
+
- repo/worktree fingerprints, branch, head SHA, optional ticket, source counts;
|
|
142
|
+
- private raw evidence objects and `ambient_evidence_refs` pointers;
|
|
143
|
+
- Codex/Claude session attribution metadata.
|
|
220
144
|
|
|
221
|
-
|
|
145
|
+
Never provide service-role keys, raw DB URLs, cookies, root env files, or deployment tokens to this CLI. The collector never reads env files and does not collect random desktop screenshots or screen recordings.
|
|
146
|
+
|
|
147
|
+
## Debug
|
|
222
148
|
|
|
223
149
|
```bash
|
|
224
150
|
cockpit sessions --workspace "$PWD" --json
|
|
151
|
+
cockpit status --workspace "$PWD" --json
|
|
152
|
+
cockpit sync --workspace "$PWD" --json
|
|
225
153
|
```
|
|
226
154
|
|
|
227
|
-
|
|
228
|
-
scan window, plus Claude Code sessions when Claude collection is enabled. Use
|
|
229
|
-
`--since-days N` or `--all` when debugging older local history.
|
|
230
|
-
|
|
231
|
-
Remote metadata path:
|
|
155
|
+
If `cockpit update` fails with npm `EACCES`, fix Homebrew global-package ownership once:
|
|
232
156
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
branch, and collector version.
|
|
237
|
-
3. Open the `ambient-raw-evidence` object only after SQL identifies the exact
|
|
238
|
-
row and `object_key`.
|
|
239
|
-
|
|
240
|
-
Never provide Supabase service-role keys, raw DB URLs, root env files, cookies,
|
|
241
|
-
or deployment tokens to this CLI. The collector must never read env files and
|
|
242
|
-
does not collect random desktop screenshots or screen recordings.
|
|
243
|
-
|
|
244
|
-
## Public package boundary
|
|
157
|
+
```bash
|
|
158
|
+
sudo chown -R $(whoami) /opt/homebrew/lib/node_modules/@bli-cockpit /opt/homebrew/bin/cockpit
|
|
159
|
+
```
|
|
245
160
|
|
|
246
|
-
|
|
247
|
-
service-role credential handling, source maps, tests, and internal runbooks.
|
|
248
|
-
Clean `npm pack` and `npm publish` run the public CLI build before packaging so
|
|
249
|
-
`dist/cli.js` is present in emergency releases.
|
|
250
|
-
When collector changes depend on new telemetry-core exports, publish
|
|
251
|
-
`@bli-cockpit/telemetry-core` first, then publish `@bli-cockpit/cli`.
|
|
161
|
+
Do not use `sudo npm i -g`; it recreates the ownership problem.
|
|
@@ -56,6 +56,7 @@ function parseOnboardLikeArgs(args, command) {
|
|
|
56
56
|
"--ticket",
|
|
57
57
|
"--branch",
|
|
58
58
|
"--json",
|
|
59
|
+
"--no-auth",
|
|
59
60
|
"--poll-interval-ms",
|
|
60
61
|
"--timeout-ms",
|
|
61
62
|
"--max-depth",
|
|
@@ -87,6 +88,7 @@ function parseOnboardLikeArgs(args, command) {
|
|
|
87
88
|
deviceName: optionalNonEmpty(values.flags.get("--device-name")),
|
|
88
89
|
activeTicketId: optionalNonEmpty(values.flags.get("--ticket")),
|
|
89
90
|
branch: optionalNonEmpty(values.flags.get("--branch")),
|
|
91
|
+
noAuth: values.booleans.has("--no-auth"),
|
|
90
92
|
json: values.booleans.has("--json"),
|
|
91
93
|
pollIntervalMs: optionalPositiveInteger(values.flags.get("--poll-interval-ms"), "--poll-interval-ms"),
|
|
92
94
|
timeoutMs: optionalPositiveInteger(values.flags.get("--timeout-ms"), "--timeout-ms"),
|
|
@@ -164,6 +166,7 @@ function parseLoginArgs(args) {
|
|
|
164
166
|
"--email",
|
|
165
167
|
"--device-name",
|
|
166
168
|
"--json",
|
|
169
|
+
"--no-auth",
|
|
167
170
|
"--poll-interval-ms",
|
|
168
171
|
"--timeout-ms",
|
|
169
172
|
],
|
|
@@ -184,6 +187,7 @@ function parseLoginArgs(args) {
|
|
|
184
187
|
claimedOwnerEmail: optionalEmail(values.flags.get("--email")),
|
|
185
188
|
deviceName: optionalNonEmpty(values.flags.get("--device-name")),
|
|
186
189
|
json: values.booleans.has("--json"),
|
|
190
|
+
noAuth: values.booleans.has("--no-auth"),
|
|
187
191
|
pollIntervalMs: optionalPositiveInteger(values.flags.get("--poll-interval-ms"), "--poll-interval-ms"),
|
|
188
192
|
timeoutMs: optionalPositiveInteger(values.flags.get("--timeout-ms"), "--timeout-ms"),
|
|
189
193
|
};
|
package/dist/commands/local.js
CHANGED
|
@@ -90,12 +90,12 @@ export function localCommandHelp(command) {
|
|
|
90
90
|
if (command)
|
|
91
91
|
return localSubcommandHelp(command);
|
|
92
92
|
return [
|
|
93
|
-
" cockpit onboard [--ticket <id>] [--email <owner@email>] [--device-name <name>] [--dashboard-url <url>] [--workspace <path>] [--branch <name>] [--max-depth <n>] [--max-repos <n>] [--json]",
|
|
94
|
-
" cockpit update [--email <owner@email>] [--device-name <name>] [--dashboard-url <url>] [--workspace <path>] [--json]",
|
|
93
|
+
" cockpit onboard [--ticket <id>] [--email <owner@email>] [--device-name <name>] [--dashboard-url <url>] [--workspace <path>] [--branch <name>] [--no-auth] [--max-depth <n>] [--max-repos <n>] [--json]",
|
|
94
|
+
" cockpit update [--email <owner@email>] [--device-name <name>] [--dashboard-url <url>] [--workspace <path>] [--no-auth] [--json]",
|
|
95
95
|
" cockpit upgrade [same flags as update]",
|
|
96
96
|
" cockpit install [--dashboard-url <url>] [--workspace <path>] [--json]",
|
|
97
|
-
" cockpit login [--email <owner@email>] [--device-name <name>] [--dashboard-url <url>] [--json]",
|
|
98
|
-
" cockpit pair [--email <owner@email>] [--device-name <name>] [--dashboard-url <url>] [--json]",
|
|
97
|
+
" cockpit login [--email <owner@email>] [--device-name <name>] [--dashboard-url <url>] [--no-auth] [--json]",
|
|
98
|
+
" cockpit pair [--email <owner@email>] [--device-name <name>] [--dashboard-url <url>] [--no-auth] [--json]",
|
|
99
99
|
" cockpit logout",
|
|
100
100
|
" cockpit start [--ticket <id>|--clear-ticket] [--topic <label>] [--intent <intent>] [--phase <phase>] [--workspace <path>] [--branch <name>] [--max-depth <n>] [--max-repos <n>] [--json]",
|
|
101
101
|
" cockpit sync [--workspace <path>] [--dashboard-url <url>] [--max-depth <n>] [--max-repos <n>] [--json]",
|
|
@@ -115,14 +115,14 @@ function localSubcommandHelp(command) {
|
|
|
115
115
|
[
|
|
116
116
|
"onboard",
|
|
117
117
|
[
|
|
118
|
-
"Usage: cockpit onboard [--ticket <id>] [--email <owner@email>] [--device-name <name>] [--dashboard-url <url>] [--workspace <path>] [--branch <name>] [--json]",
|
|
118
|
+
"Usage: cockpit onboard [--ticket <id>] [--email <owner@email>] [--device-name <name>] [--dashboard-url <url>] [--workspace <path>] [--branch <name>] [--no-auth] [--json]",
|
|
119
119
|
"",
|
|
120
120
|
"Installs, pairs, starts work context(s), syncs once, and prints readiness proof.",
|
|
121
121
|
"If --workspace is a parent folder, scans child git repos/worktrees and rolls them up by repo.",
|
|
122
122
|
"`--repo <path>` remains supported as a backward-compatible alias.",
|
|
123
123
|
`Omit --dashboard-url for normal production setup (${DEFAULT_DASHBOARD_URL}).`,
|
|
124
124
|
"Pass --dashboard-url only for staging/custom dashboards or to force a different pairing.",
|
|
125
|
-
"Run with no flags in a terminal and it prompts for the dashboard email; pass --email to skip the prompt
|
|
125
|
+
"Run with no flags in a terminal and it prompts for the dashboard email and OTP code; pass --email to skip the email prompt. Use --no-auth to force the manual approval fallback.",
|
|
126
126
|
"Interactive runs also offer to add Cockpit ticket-binding rules to AGENTS.md and CLAUDE.md after readiness proof.",
|
|
127
127
|
],
|
|
128
128
|
],
|
|
@@ -139,7 +139,7 @@ function localSubcommandHelp(command) {
|
|
|
139
139
|
[
|
|
140
140
|
"update",
|
|
141
141
|
[
|
|
142
|
-
"Usage: cockpit update [--email <owner@email>] [--device-name <name>] [--dashboard-url <url>] [--workspace <path>] [--json]",
|
|
142
|
+
"Usage: cockpit update [--email <owner@email>] [--device-name <name>] [--dashboard-url <url>] [--workspace <path>] [--no-auth] [--json]",
|
|
143
143
|
"",
|
|
144
144
|
"Updates the global public CLI from npm, then reruns `cockpit onboard`",
|
|
145
145
|
"with the same setup flags so pairing, saved roots, agent rules,",
|
|
@@ -158,16 +158,16 @@ function localSubcommandHelp(command) {
|
|
|
158
158
|
[
|
|
159
159
|
"login",
|
|
160
160
|
[
|
|
161
|
-
"Usage: cockpit login [--email <owner@email>] [--device-name <name>] [--dashboard-url <url>] [--json]",
|
|
161
|
+
"Usage: cockpit login [--email <owner@email>] [--device-name <name>] [--dashboard-url <url>] [--no-auth] [--json]",
|
|
162
162
|
"",
|
|
163
|
-
"
|
|
163
|
+
"Signs in with an email OTP when interactive, starts dashboard device pairing, and stores the approved local session.",
|
|
164
164
|
"Omit --dashboard-url for the production dashboard; pass it only for staging/custom dashboards.",
|
|
165
165
|
],
|
|
166
166
|
],
|
|
167
167
|
[
|
|
168
168
|
"pair",
|
|
169
169
|
[
|
|
170
|
-
"Usage: cockpit pair [--email <owner@email>] [--device-name <name>] [--dashboard-url <url>] [--json]",
|
|
170
|
+
"Usage: cockpit pair [--email <owner@email>] [--device-name <name>] [--dashboard-url <url>] [--no-auth] [--json]",
|
|
171
171
|
"",
|
|
172
172
|
"Alias for `cockpit login`.",
|
|
173
173
|
],
|
|
@@ -330,6 +330,10 @@ async function runUpdate(command, io) {
|
|
|
330
330
|
}
|
|
331
331
|
else {
|
|
332
332
|
writeLine(io.stderr, "BLOCKED: npm install failed; Cockpit CLI was not refreshed.");
|
|
333
|
+
if (isNpmEaccesFailure(install.stderr)) {
|
|
334
|
+
writeLine(io.stderr, "Fix Homebrew npm ownership once: sudo chown -R $(whoami) /opt/homebrew/lib/node_modules/@bli-cockpit /opt/homebrew/bin/cockpit");
|
|
335
|
+
writeLine(io.stderr, "Do not use `sudo npm i -g`; it makes the ownership problem come back.");
|
|
336
|
+
}
|
|
333
337
|
}
|
|
334
338
|
return install.code || 1;
|
|
335
339
|
}
|
|
@@ -437,6 +441,8 @@ function updateOnboardArgs(command) {
|
|
|
437
441
|
}
|
|
438
442
|
if (command.claimedOwnerEmail)
|
|
439
443
|
args.push("--email", command.claimedOwnerEmail);
|
|
444
|
+
if (command.noAuth)
|
|
445
|
+
args.push("--no-auth");
|
|
440
446
|
if (command.deviceName)
|
|
441
447
|
args.push("--device-name", command.deviceName);
|
|
442
448
|
if (command.activeTicketId)
|
|
@@ -457,6 +463,9 @@ function updateOnboardArgs(command) {
|
|
|
457
463
|
args.push("--json");
|
|
458
464
|
return args;
|
|
459
465
|
}
|
|
466
|
+
function isNpmEaccesFailure(stderr) {
|
|
467
|
+
return /EACCES|permission denied/i.test(stderr);
|
|
468
|
+
}
|
|
460
469
|
function updateCollectionRoots(command) {
|
|
461
470
|
const roots = command.collectionRoots?.length
|
|
462
471
|
? command.collectionRoots
|
|
@@ -514,7 +523,7 @@ async function readLine(io, prompt) {
|
|
|
514
523
|
* leniency (the approving admin's account then owns the device).
|
|
515
524
|
*/
|
|
516
525
|
async function promptOnboardEmail(io) {
|
|
517
|
-
const raw = await readLine(io, "
|
|
526
|
+
const raw = await readLine(io, "What's your @buildlaunchiterate.ca email? (press enter to skip): ");
|
|
518
527
|
const answer = raw.trim().toLowerCase();
|
|
519
528
|
if (!answer)
|
|
520
529
|
return undefined;
|
|
@@ -558,6 +567,112 @@ async function resolveOnboardEmail(command, roots, config, io) {
|
|
|
558
567
|
}
|
|
559
568
|
return promptOnboardEmail(io);
|
|
560
569
|
}
|
|
570
|
+
async function resolveInteractiveLoginEmail(command, io) {
|
|
571
|
+
if (command.claimedOwnerEmail)
|
|
572
|
+
return command.claimedOwnerEmail;
|
|
573
|
+
if (command.noAuth || command.json || !isInteractiveStdin(io)) {
|
|
574
|
+
return undefined;
|
|
575
|
+
}
|
|
576
|
+
return promptOnboardEmail(io);
|
|
577
|
+
}
|
|
578
|
+
async function requestPairingAccessToken(input, io) {
|
|
579
|
+
if (!input.email ||
|
|
580
|
+
input.noAuth ||
|
|
581
|
+
input.json ||
|
|
582
|
+
!isInteractiveStdin(io)) {
|
|
583
|
+
return undefined;
|
|
584
|
+
}
|
|
585
|
+
try {
|
|
586
|
+
const fetchImpl = io.fetch;
|
|
587
|
+
writeLine(io.stdout, `Signing in as ${input.email}.`);
|
|
588
|
+
const start = await postOtpStart(fetchImpl, input.dashboardUrl, input.email);
|
|
589
|
+
const resendAfter = typeof start.resend_after_seconds === "number"
|
|
590
|
+
? start.resend_after_seconds
|
|
591
|
+
: 60;
|
|
592
|
+
writeLine(io.stdout, `Code sent; valid 1h, resend in ${resendAfter}s by rerunning this command.`);
|
|
593
|
+
const code = (await readLine(io, "Enter the 6-digit code: ")).trim();
|
|
594
|
+
if (!/^\d{6}$/.test(code)) {
|
|
595
|
+
throw new Error("OTP code must be 6 digits.");
|
|
596
|
+
}
|
|
597
|
+
const verified = await postOtpVerify(fetchImpl, input.dashboardUrl, input.email, code);
|
|
598
|
+
if (typeof verified.access_token !== "string" || !verified.access_token) {
|
|
599
|
+
throw new Error("OTP verified but dashboard returned no access token.");
|
|
600
|
+
}
|
|
601
|
+
writeLine(io.stdout, `Signed in as ${input.email}.`);
|
|
602
|
+
return verified.access_token;
|
|
603
|
+
}
|
|
604
|
+
catch (error) {
|
|
605
|
+
writeLine(io.stderr, `Auth step skipped: ${errorMessage(error)}`);
|
|
606
|
+
writeLine(io.stderr, "Continuing with manual dashboard approval.");
|
|
607
|
+
return undefined;
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
async function pairLocalCollectorWithAuthFallback(options, io) {
|
|
611
|
+
try {
|
|
612
|
+
return await pairLocalCollector(options);
|
|
613
|
+
}
|
|
614
|
+
catch (error) {
|
|
615
|
+
if (!options.pairingAccessToken || !isPairingAuthFailure(error)) {
|
|
616
|
+
throw error;
|
|
617
|
+
}
|
|
618
|
+
writeLine(io.stderr, `Authenticated pairing failed: ${errorMessage(error)}`);
|
|
619
|
+
writeLine(io.stderr, "Continuing with manual dashboard approval.");
|
|
620
|
+
return pairLocalCollector({
|
|
621
|
+
...options,
|
|
622
|
+
pairingAccessToken: undefined,
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
function isPairingAuthFailure(error) {
|
|
627
|
+
return /\b(auth|authorization|bearer|token|jwt|otp)\b/i.test(errorMessage(error));
|
|
628
|
+
}
|
|
629
|
+
async function postOtpStart(fetchImpl, dashboardUrl, email) {
|
|
630
|
+
const response = await fetchImpl(`${dashboardUrl}/api/auth/otp/start`, {
|
|
631
|
+
method: "POST",
|
|
632
|
+
headers: { "Content-Type": "application/json" },
|
|
633
|
+
body: JSON.stringify({ email }),
|
|
634
|
+
});
|
|
635
|
+
const parsed = await readJsonResponse(response);
|
|
636
|
+
if (!response.ok) {
|
|
637
|
+
throw new Error(responseErrorMessage(parsed, "OTP start failed"));
|
|
638
|
+
}
|
|
639
|
+
return parsed;
|
|
640
|
+
}
|
|
641
|
+
async function postOtpVerify(fetchImpl, dashboardUrl, email, code) {
|
|
642
|
+
const response = await fetchImpl(`${dashboardUrl}/api/auth/otp/verify`, {
|
|
643
|
+
method: "POST",
|
|
644
|
+
headers: { "Content-Type": "application/json" },
|
|
645
|
+
body: JSON.stringify({ email, code }),
|
|
646
|
+
});
|
|
647
|
+
const parsed = await readJsonResponse(response);
|
|
648
|
+
if (!response.ok) {
|
|
649
|
+
throw new Error(responseErrorMessage(parsed, "OTP verify failed"));
|
|
650
|
+
}
|
|
651
|
+
return parsed;
|
|
652
|
+
}
|
|
653
|
+
async function readJsonResponse(response) {
|
|
654
|
+
const text = await response.text();
|
|
655
|
+
if (!text)
|
|
656
|
+
return null;
|
|
657
|
+
try {
|
|
658
|
+
return JSON.parse(text);
|
|
659
|
+
}
|
|
660
|
+
catch {
|
|
661
|
+
return text;
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
function responseErrorMessage(parsed, fallback) {
|
|
665
|
+
if (parsed && typeof parsed === "object") {
|
|
666
|
+
const record = parsed;
|
|
667
|
+
if (typeof record["message"] === "string" && record["message"].trim()) {
|
|
668
|
+
return record["message"];
|
|
669
|
+
}
|
|
670
|
+
if (typeof record["error"] === "string" && record["error"].trim()) {
|
|
671
|
+
return record["error"];
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
return fallback;
|
|
675
|
+
}
|
|
561
676
|
async function inferOnboardEmail(homeDir, roots, config) {
|
|
562
677
|
const session = await readOnboardSessionReuseCandidate(homeDir).catch(() => null);
|
|
563
678
|
const sessionEmail = normalizeEmailForComparison(session?.email);
|
|
@@ -748,18 +863,25 @@ async function runOnboard(command, io) {
|
|
|
748
863
|
writeLine(io.stdout, "2/5 Existing valid device session does not match requested owner or dashboard; pairing again.");
|
|
749
864
|
}
|
|
750
865
|
}
|
|
751
|
-
|
|
866
|
+
const pairingAccessToken = await requestPairingAccessToken({
|
|
867
|
+
dashboardUrl: command.dashboardUrl,
|
|
868
|
+
email: claimedOwnerEmail,
|
|
869
|
+
noAuth: command.noAuth,
|
|
870
|
+
json: command.json,
|
|
871
|
+
}, io);
|
|
872
|
+
pair = await pairLocalCollectorWithAuthFallback({
|
|
752
873
|
homeDir: command.homeDir,
|
|
753
874
|
dashboardUrl: command.dashboardUrl,
|
|
754
875
|
claimedOwnerEmail,
|
|
755
876
|
deviceName: command.deviceName,
|
|
756
877
|
pollIntervalMs: command.pollIntervalMs,
|
|
757
878
|
timeoutMs: command.timeoutMs,
|
|
879
|
+
pairingAccessToken,
|
|
758
880
|
fetch: io.fetch,
|
|
759
881
|
onPairStarted: command.json
|
|
760
882
|
? undefined
|
|
761
883
|
: (request) => writePairingInstructions(io, request),
|
|
762
|
-
});
|
|
884
|
+
}, io);
|
|
763
885
|
if (!command.json) {
|
|
764
886
|
writeLine(io.stdout, "2/5 Device paired.");
|
|
765
887
|
writeLine(io.stdout, `User: ${pair.session.email ?? pair.session.auth_subject_id}`);
|
|
@@ -1107,13 +1229,22 @@ function worktreeSyncRow(outcome, run) {
|
|
|
1107
1229
|
};
|
|
1108
1230
|
}
|
|
1109
1231
|
async function runLogin(command, io) {
|
|
1110
|
-
const
|
|
1232
|
+
const claimedOwnerEmail = await resolveInteractiveLoginEmail(command, io);
|
|
1233
|
+
const dashboardUrl = await resolveLoginDashboardUrl(command);
|
|
1234
|
+
const pairingAccessToken = await requestPairingAccessToken({
|
|
1235
|
+
dashboardUrl,
|
|
1236
|
+
email: claimedOwnerEmail,
|
|
1237
|
+
noAuth: command.noAuth,
|
|
1238
|
+
json: command.json,
|
|
1239
|
+
}, io);
|
|
1240
|
+
const result = await pairLocalCollectorWithAuthFallback({
|
|
1111
1241
|
homeDir: command.homeDir,
|
|
1112
1242
|
dashboardUrl: command.dashboardUrl,
|
|
1113
|
-
claimedOwnerEmail
|
|
1243
|
+
claimedOwnerEmail,
|
|
1114
1244
|
deviceName: command.deviceName,
|
|
1115
1245
|
pollIntervalMs: command.pollIntervalMs,
|
|
1116
1246
|
timeoutMs: command.timeoutMs,
|
|
1247
|
+
pairingAccessToken,
|
|
1117
1248
|
fetch: io.fetch,
|
|
1118
1249
|
onPairStarted: command.json
|
|
1119
1250
|
? undefined
|
|
@@ -1123,7 +1254,7 @@ async function runLogin(command, io) {
|
|
|
1123
1254
|
writeLine(io.stdout, `Code: ${request.user_code}`);
|
|
1124
1255
|
writeLine(io.stdout, "Waiting for dashboard approval...");
|
|
1125
1256
|
},
|
|
1126
|
-
});
|
|
1257
|
+
}, io);
|
|
1127
1258
|
if (command.json) {
|
|
1128
1259
|
writeLine(io.stdout, JSON.stringify(result, null, 2));
|
|
1129
1260
|
return 0;
|
|
@@ -1135,6 +1266,13 @@ async function runLogin(command, io) {
|
|
|
1135
1266
|
writeLine(io.stdout, "Next: run `cockpit start` inside the repo.");
|
|
1136
1267
|
return 0;
|
|
1137
1268
|
}
|
|
1269
|
+
async function resolveLoginDashboardUrl(command) {
|
|
1270
|
+
if (command.dashboardUrl)
|
|
1271
|
+
return command.dashboardUrl;
|
|
1272
|
+
const paths = getCollectorRuntimePaths(command.homeDir);
|
|
1273
|
+
const config = await readLocalCollectorConfig(paths);
|
|
1274
|
+
return config.dashboard_url;
|
|
1275
|
+
}
|
|
1138
1276
|
function writePairingInstructions(io, request) {
|
|
1139
1277
|
writeLine(io.stdout, "2/5 Device pairing started.");
|
|
1140
1278
|
writeLine(io.stdout, `Open: ${request.approve_url}`);
|
package/dist/local-state.js
CHANGED
|
@@ -106,7 +106,7 @@ export async function pairLocalCollector(options = {}) {
|
|
|
106
106
|
device_name: deviceName,
|
|
107
107
|
claimed_owner_email: claimedOwnerEmail,
|
|
108
108
|
collector_version: LOCAL_COLLECTOR_VERSION,
|
|
109
|
-
});
|
|
109
|
+
}, options.pairingAccessToken);
|
|
110
110
|
options.onPairStarted?.(startResponse);
|
|
111
111
|
const sessionFile = await pollPairRequest(fetchImpl, dashboardUrl, {
|
|
112
112
|
paths,
|
|
@@ -459,10 +459,13 @@ function normalizeDashboardUrl(value) {
|
|
|
459
459
|
throw new Error("Dashboard URL cannot be empty.");
|
|
460
460
|
return normalized;
|
|
461
461
|
}
|
|
462
|
-
async function postPairStart(fetchImpl, dashboardUrl, body) {
|
|
462
|
+
async function postPairStart(fetchImpl, dashboardUrl, body, accessToken) {
|
|
463
463
|
const response = await fetchImpl(`${dashboardUrl}/api/ambient/pair/start`, {
|
|
464
464
|
method: "POST",
|
|
465
|
-
headers: {
|
|
465
|
+
headers: {
|
|
466
|
+
"Content-Type": "application/json",
|
|
467
|
+
...(accessToken ? { "Authorization": `Bearer ${accessToken}` } : {}),
|
|
468
|
+
},
|
|
466
469
|
body: JSON.stringify(body),
|
|
467
470
|
});
|
|
468
471
|
const parsed = await readResponseJson(response);
|