@bli-cockpit/cli 0.1.7 → 0.1.9
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/dist/autostart.js +154 -0
- package/dist/commands/local.js +193 -3
- package/dist/local-state.js +2 -2
- package/package.json +1 -1
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { mkdir, rm, stat, writeFile } from "node:fs/promises";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { DEFAULT_DASHBOARD_URL, getCollectorRuntimePaths } from "./local-state.js";
|
|
5
|
+
/** launchd LaunchAgent label; matches docs/runbooks/cockpit-launchd-sync.md. */
|
|
6
|
+
export const AUTOSTART_LABEL = "com.bli.cockpit.sync";
|
|
7
|
+
const DEFAULT_INTERVAL_SECONDS = 1800;
|
|
8
|
+
const UNSUPPORTED_MESSAGE = "macOS-only for now; see docs/runbooks/cockpit-launchd-sync.md";
|
|
9
|
+
function plistPathFor(homeDir) {
|
|
10
|
+
return path.join(homeDir, "Library", "LaunchAgents", `${AUTOSTART_LABEL}.plist`);
|
|
11
|
+
}
|
|
12
|
+
function unsupportedResult(homeDir) {
|
|
13
|
+
return {
|
|
14
|
+
status: "unsupported",
|
|
15
|
+
label: AUTOSTART_LABEL,
|
|
16
|
+
plist_path: plistPathFor(homeDir ?? os.homedir()),
|
|
17
|
+
message: UNSUPPORTED_MESSAGE,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Installs (or refreshes) the launchd LaunchAgent that keeps `cockpit sync`
|
|
22
|
+
* running at login and every `intervalSeconds`. Mirrors the plist in
|
|
23
|
+
* docs/runbooks/cockpit-launchd-sync.md exactly, but writes resolved absolute
|
|
24
|
+
* log paths (launchd does not expand `$HOME`). The unload before load makes the
|
|
25
|
+
* install idempotent — re-running picks up a changed repo/url/interval.
|
|
26
|
+
*/
|
|
27
|
+
export async function installAutostartAgent(options) {
|
|
28
|
+
const platform = options.platform ?? process.platform;
|
|
29
|
+
if (platform !== "darwin")
|
|
30
|
+
return unsupportedResult(options.homeDir);
|
|
31
|
+
const homeDir = options.homeDir ?? os.homedir();
|
|
32
|
+
const workDir = path.resolve(options.repoRoot ?? process.cwd());
|
|
33
|
+
const dashboardUrl = options.dashboardUrl ?? DEFAULT_DASHBOARD_URL;
|
|
34
|
+
const intervalSeconds = options.intervalSeconds ?? DEFAULT_INTERVAL_SECONDS;
|
|
35
|
+
const paths = getCollectorRuntimePaths(homeDir);
|
|
36
|
+
const plistPath = plistPathFor(homeDir);
|
|
37
|
+
const stdoutPath = path.join(paths.state_dir, "sync.log");
|
|
38
|
+
const stderrPath = path.join(paths.state_dir, "sync.err.log");
|
|
39
|
+
await mkdir(path.dirname(plistPath), { recursive: true });
|
|
40
|
+
await mkdir(paths.state_dir, { recursive: true });
|
|
41
|
+
await writeFile(plistPath, renderPlist({
|
|
42
|
+
workDir,
|
|
43
|
+
dashboardUrl,
|
|
44
|
+
intervalSeconds,
|
|
45
|
+
stdoutPath,
|
|
46
|
+
stderrPath,
|
|
47
|
+
}), "utf8");
|
|
48
|
+
// Unload first so a changed plist is actually picked up; a not-yet-loaded
|
|
49
|
+
// agent makes unload fail harmlessly, so the error is ignored.
|
|
50
|
+
await options.exec("launchctl", ["unload", plistPath]).catch(() => undefined);
|
|
51
|
+
const load = await options.exec("launchctl", ["load", plistPath]);
|
|
52
|
+
const loaded = load.code === 0;
|
|
53
|
+
return {
|
|
54
|
+
status: "installed",
|
|
55
|
+
label: AUTOSTART_LABEL,
|
|
56
|
+
plist_path: plistPath,
|
|
57
|
+
loaded,
|
|
58
|
+
interval_seconds: intervalSeconds,
|
|
59
|
+
work_dir: workDir,
|
|
60
|
+
dashboard_url: dashboardUrl,
|
|
61
|
+
...(loaded
|
|
62
|
+
? {}
|
|
63
|
+
: {
|
|
64
|
+
message: `launchctl load exited ${load.code}: ${load.stderr.trim() || "unknown error"}`,
|
|
65
|
+
}),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Removes the LaunchAgent. Reports `absent` when there was nothing to remove so
|
|
70
|
+
* the command is safe to run repeatedly.
|
|
71
|
+
*/
|
|
72
|
+
export async function uninstallAutostartAgent(options) {
|
|
73
|
+
const platform = options.platform ?? process.platform;
|
|
74
|
+
if (platform !== "darwin")
|
|
75
|
+
return unsupportedResult(options.homeDir);
|
|
76
|
+
const homeDir = options.homeDir ?? os.homedir();
|
|
77
|
+
const plistPath = plistPathFor(homeDir);
|
|
78
|
+
if (!(await fileExists(plistPath))) {
|
|
79
|
+
return { status: "absent", label: AUTOSTART_LABEL, plist_path: plistPath };
|
|
80
|
+
}
|
|
81
|
+
await options.exec("launchctl", ["unload", plistPath]).catch(() => undefined);
|
|
82
|
+
await rm(plistPath, { force: true });
|
|
83
|
+
return { status: "uninstalled", label: AUTOSTART_LABEL, plist_path: plistPath };
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Reports whether the agent is installed and loaded. `absent` when no plist
|
|
87
|
+
* exists; otherwise `launchctl list <label>` exit code distinguishes `loaded`
|
|
88
|
+
* (0) from `not_loaded` (non-zero).
|
|
89
|
+
*/
|
|
90
|
+
export async function autostartStatus(options) {
|
|
91
|
+
const platform = options.platform ?? process.platform;
|
|
92
|
+
if (platform !== "darwin")
|
|
93
|
+
return unsupportedResult(options.homeDir);
|
|
94
|
+
const homeDir = options.homeDir ?? os.homedir();
|
|
95
|
+
const plistPath = plistPathFor(homeDir);
|
|
96
|
+
if (!(await fileExists(plistPath))) {
|
|
97
|
+
return { status: "absent", label: AUTOSTART_LABEL, plist_path: plistPath };
|
|
98
|
+
}
|
|
99
|
+
const list = await options.exec("launchctl", ["list", AUTOSTART_LABEL]);
|
|
100
|
+
return {
|
|
101
|
+
status: list.code === 0 ? "loaded" : "not_loaded",
|
|
102
|
+
label: AUTOSTART_LABEL,
|
|
103
|
+
plist_path: plistPath,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
function renderPlist(options) {
|
|
107
|
+
// The repo path is shell-quoted because it lands inside a `/bin/zsh -lc "…"`
|
|
108
|
+
// command string; the whole command is then XML-escaped for the <string>.
|
|
109
|
+
const command = `npm exec --yes --package=@bli-cockpit/cli@latest -- cockpit sync --repo ${shellQuote(options.workDir)} --dashboard-url ${shellQuote(options.dashboardUrl)} --json`;
|
|
110
|
+
return [
|
|
111
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
112
|
+
'<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
|
|
113
|
+
'<plist version="1.0">',
|
|
114
|
+
"<dict>",
|
|
115
|
+
" <key>Label</key>",
|
|
116
|
+
` <string>${xmlEscape(AUTOSTART_LABEL)}</string>`,
|
|
117
|
+
" <key>ProgramArguments</key>",
|
|
118
|
+
" <array>",
|
|
119
|
+
" <string>/bin/zsh</string>",
|
|
120
|
+
" <string>-lc</string>",
|
|
121
|
+
` <string>${xmlEscape(command)}</string>`,
|
|
122
|
+
" </array>",
|
|
123
|
+
" <key>StartInterval</key>",
|
|
124
|
+
` <integer>${options.intervalSeconds}</integer>`,
|
|
125
|
+
" <key>RunAtLoad</key>",
|
|
126
|
+
" <true/>",
|
|
127
|
+
" <key>EnvironmentVariables</key>",
|
|
128
|
+
" <dict>",
|
|
129
|
+
" <key>PATH</key>",
|
|
130
|
+
" <string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin</string>",
|
|
131
|
+
" </dict>",
|
|
132
|
+
" <key>StandardOutPath</key>",
|
|
133
|
+
` <string>${xmlEscape(options.stdoutPath)}</string>`,
|
|
134
|
+
" <key>StandardErrorPath</key>",
|
|
135
|
+
` <string>${xmlEscape(options.stderrPath)}</string>`,
|
|
136
|
+
"</dict>",
|
|
137
|
+
"</plist>",
|
|
138
|
+
"",
|
|
139
|
+
].join("\n");
|
|
140
|
+
}
|
|
141
|
+
function shellQuote(value) {
|
|
142
|
+
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
143
|
+
}
|
|
144
|
+
function xmlEscape(value) {
|
|
145
|
+
return value
|
|
146
|
+
.replace(/&/g, "&")
|
|
147
|
+
.replace(/</g, "<")
|
|
148
|
+
.replace(/>/g, ">")
|
|
149
|
+
.replace(/"/g, """)
|
|
150
|
+
.replace(/'/g, "'");
|
|
151
|
+
}
|
|
152
|
+
async function fileExists(filePath) {
|
|
153
|
+
return stat(filePath).then(() => true, () => false);
|
|
154
|
+
}
|
package/dist/commands/local.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
1
2
|
import os from "node:os";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
import { createCollectorServer } from "../server.js";
|
|
5
|
+
import { autostartStatus, installAutostartAgent, uninstallAutostartAgent, } from "../autostart.js";
|
|
4
6
|
import { DEFAULT_DASHBOARD_URL, getCollectorRuntimePaths, inspectLocalCollectorStatus, installLocalCollector, logoutLocalCollector, pairLocalCollector, readLocalCollectorSessionFile, readLocalSessionReference, startLocalWorkContext, } from "../local-state.js";
|
|
5
7
|
import { LocalUploadBlockedError, postCodexSessionReport, syncLocalAmbientEnvelope, } from "../upload.js";
|
|
6
8
|
import { scanAndAttributeCodexSessions, } from "../adapters/codex-attribution.js";
|
|
@@ -21,6 +23,7 @@ export const rootCommandNames = new Set([
|
|
|
21
23
|
"status",
|
|
22
24
|
"sessions",
|
|
23
25
|
"serve",
|
|
26
|
+
"autostart",
|
|
24
27
|
]);
|
|
25
28
|
export async function runLocalCockpitCli(argv, io = defaultIo()) {
|
|
26
29
|
if (isLocalHelpRequest(argv)) {
|
|
@@ -57,6 +60,8 @@ export async function runLocalCockpitCli(argv, io = defaultIo()) {
|
|
|
57
60
|
return await runSessions(command, io);
|
|
58
61
|
case "serve":
|
|
59
62
|
return await runServe(command, io);
|
|
63
|
+
case "autostart":
|
|
64
|
+
return await runAutostart(command, io);
|
|
60
65
|
}
|
|
61
66
|
}
|
|
62
67
|
catch (error) {
|
|
@@ -78,6 +83,7 @@ export function localCommandHelp(command) {
|
|
|
78
83
|
" cockpit status [--repo <path>] [--max-depth <n>] [--max-repos <n>] [--json]",
|
|
79
84
|
" cockpit sessions [--source codex|claude] [--repo <path>] [--max-depth <n>] [--max-repos <n>] [--json]",
|
|
80
85
|
" cockpit serve [--port <port>] [--repo <path>]",
|
|
86
|
+
" cockpit autostart [install|uninstall|status] [--repo <path>] [--dashboard-url <url>] [--interval-seconds <n>] [--json]",
|
|
81
87
|
].join("\n");
|
|
82
88
|
}
|
|
83
89
|
function localSubcommandHelp(command) {
|
|
@@ -89,7 +95,7 @@ function localSubcommandHelp(command) {
|
|
|
89
95
|
"",
|
|
90
96
|
"Installs, pairs, starts work context(s), syncs once, and prints readiness proof.",
|
|
91
97
|
"If --repo is a parent folder, scans child git repos/worktrees and rolls them up by repo.",
|
|
92
|
-
"
|
|
98
|
+
"Run with no flags in a terminal and it prompts for the dashboard email; pass --email to skip the prompt (and on shared/reused machines, where mismatched sessions are re-paired).",
|
|
93
99
|
],
|
|
94
100
|
],
|
|
95
101
|
[
|
|
@@ -169,6 +175,17 @@ function localSubcommandHelp(command) {
|
|
|
169
175
|
"Starts the local collector HTTP status server.",
|
|
170
176
|
],
|
|
171
177
|
],
|
|
178
|
+
[
|
|
179
|
+
"autostart",
|
|
180
|
+
[
|
|
181
|
+
"Usage: cockpit autostart [install|uninstall|status] [--repo <path>] [--dashboard-url <url>] [--interval-seconds <n>] [--json]",
|
|
182
|
+
"",
|
|
183
|
+
"Installs a macOS launchd LaunchAgent that runs `cockpit sync` at login and",
|
|
184
|
+
"every 30 min (default), surviving reboots — so machines never drift to Stale.",
|
|
185
|
+
"Action defaults to `install`. `--repo` is the parent work folder to sync.",
|
|
186
|
+
"macOS-only for now; see docs/runbooks/cockpit-launchd-sync.md.",
|
|
187
|
+
],
|
|
188
|
+
],
|
|
172
189
|
]);
|
|
173
190
|
return (helpByCommand.get(command) ?? [localCommandHelp()]).join("\n");
|
|
174
191
|
}
|
|
@@ -200,6 +217,8 @@ function parseLocalArgs(argv) {
|
|
|
200
217
|
return parseSessionsArgs(argv.slice(1));
|
|
201
218
|
case "serve":
|
|
202
219
|
return parseServeArgs(argv.slice(1));
|
|
220
|
+
case "autostart":
|
|
221
|
+
return parseAutostartArgs(argv.slice(1));
|
|
203
222
|
default:
|
|
204
223
|
throw new Error(`Unknown local command: ${command ?? ""}`);
|
|
205
224
|
}
|
|
@@ -422,6 +441,34 @@ function parseServeArgs(args) {
|
|
|
422
441
|
port,
|
|
423
442
|
};
|
|
424
443
|
}
|
|
444
|
+
function parseAutostartArgs(args) {
|
|
445
|
+
const values = parseNamedArgs(args, {
|
|
446
|
+
allowedFlags: [
|
|
447
|
+
"--home",
|
|
448
|
+
"--repo",
|
|
449
|
+
"--dashboard-url",
|
|
450
|
+
"--interval-seconds",
|
|
451
|
+
"--json",
|
|
452
|
+
],
|
|
453
|
+
valueFlags: ["--home", "--repo", "--dashboard-url", "--interval-seconds"],
|
|
454
|
+
});
|
|
455
|
+
if (values.positionals.length > 1) {
|
|
456
|
+
throw new Error("autostart accepts at most one action (install|uninstall|status).");
|
|
457
|
+
}
|
|
458
|
+
const action = values.positionals[0] ?? "install";
|
|
459
|
+
if (action !== "install" && action !== "uninstall" && action !== "status") {
|
|
460
|
+
throw new Error("autostart action must be install, uninstall, or status.");
|
|
461
|
+
}
|
|
462
|
+
return {
|
|
463
|
+
kind: "autostart",
|
|
464
|
+
action,
|
|
465
|
+
homeDir: optionalNonEmpty(values.flags.get("--home")),
|
|
466
|
+
repoRoot: optionalNonEmpty(values.flags.get("--repo")),
|
|
467
|
+
dashboardUrl: normalizeUrl(values.flags.get("--dashboard-url") ?? DEFAULT_DASHBOARD_URL),
|
|
468
|
+
intervalSeconds: optionalPositiveInteger(values.flags.get("--interval-seconds"), "--interval-seconds") ?? 1800,
|
|
469
|
+
json: values.booleans.has("--json"),
|
|
470
|
+
};
|
|
471
|
+
}
|
|
425
472
|
function parseNamedArgs(args, options) {
|
|
426
473
|
const allowed = new Set(options.allowedFlags);
|
|
427
474
|
const valueFlags = new Set(options.valueFlags);
|
|
@@ -469,6 +516,76 @@ async function runInstall(command, io) {
|
|
|
469
516
|
writeLine(io.stdout, "Next: run `cockpit login`, then `cockpit start` inside the repo; add `--ticket <id>` only when ticket work begins.");
|
|
470
517
|
return 0;
|
|
471
518
|
}
|
|
519
|
+
function isInteractiveStdin(io) {
|
|
520
|
+
return Boolean(io.stdin.isTTY);
|
|
521
|
+
}
|
|
522
|
+
/**
|
|
523
|
+
* Reads one line from stdin after writing a prompt. Shared by the onboard email
|
|
524
|
+
* prompt and the autostart prompt; callers gate on `isInteractiveStdin` first so
|
|
525
|
+
* headless / piped / spawned runs never block on input.
|
|
526
|
+
*/
|
|
527
|
+
async function readLine(io, prompt) {
|
|
528
|
+
io.stdout.write(prompt);
|
|
529
|
+
io.stdin.setEncoding("utf8");
|
|
530
|
+
return new Promise((resolve) => {
|
|
531
|
+
const onData = (chunk) => {
|
|
532
|
+
io.stdin.removeListener("data", onData);
|
|
533
|
+
io.stdin.pause();
|
|
534
|
+
resolve(chunk);
|
|
535
|
+
};
|
|
536
|
+
io.stdin.resume();
|
|
537
|
+
io.stdin.on("data", onData);
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
/**
|
|
541
|
+
* Asks for the dashboard email so `cockpit onboard` (no flags) need not force a
|
|
542
|
+
* `--email`. Only called when stdin is a TTY and not in --json mode. An empty
|
|
543
|
+
* answer or a non-email skips rather than failing, matching `optionalEmail`'s
|
|
544
|
+
* leniency (the approving admin's account then owns the device).
|
|
545
|
+
*/
|
|
546
|
+
async function promptOnboardEmail(io) {
|
|
547
|
+
const raw = await readLine(io, "Dashboard email (press enter to skip): ");
|
|
548
|
+
const answer = raw.trim().toLowerCase();
|
|
549
|
+
if (!answer)
|
|
550
|
+
return undefined;
|
|
551
|
+
if (!answer.includes("@")) {
|
|
552
|
+
writeLine(io.stderr, `"${answer}" is not an email; continuing without one — the approving admin's account will own this device.`);
|
|
553
|
+
return undefined;
|
|
554
|
+
}
|
|
555
|
+
return answer;
|
|
556
|
+
}
|
|
557
|
+
/**
|
|
558
|
+
* After a successful onboard, offers to install the launchd autostart agent so a
|
|
559
|
+
* Mac mini keeps syncing without anyone re-running cockpit — this is the fix for
|
|
560
|
+
* interns drifting to Stale. Only in an interactive, non-JSON run with a real
|
|
561
|
+
* exec runner (`io.exec`): headless / piped / spawned onboards and tests that
|
|
562
|
+
* pass no exec skip it entirely. Declining leaves onboarding's success untouched.
|
|
563
|
+
*/
|
|
564
|
+
async function maybeOfferAutostart(command, io) {
|
|
565
|
+
if (command.json || !isInteractiveStdin(io) || !io.exec)
|
|
566
|
+
return;
|
|
567
|
+
const answer = (await readLine(io, "Keep Cockpit syncing in the background, even after restart? [Y/n] "))
|
|
568
|
+
.trim()
|
|
569
|
+
.toLowerCase();
|
|
570
|
+
if (answer === "n" || answer === "no") {
|
|
571
|
+
writeLine(io.stdout, "Skipped background autostart. Run `cockpit autostart install` anytime.");
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
const result = await installAutostartAgent({
|
|
575
|
+
homeDir: command.homeDir,
|
|
576
|
+
repoRoot: command.repoRoot,
|
|
577
|
+
dashboardUrl: command.dashboardUrl,
|
|
578
|
+
exec: io.exec,
|
|
579
|
+
});
|
|
580
|
+
if (result.status === "unsupported") {
|
|
581
|
+
writeLine(io.stdout, `Background autostart unsupported: ${result.message}`);
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
writeLine(io.stdout, result.loaded
|
|
585
|
+
? "Background autostart installed; Cockpit syncs at login and every 30 min."
|
|
586
|
+
: "Background autostart installed, but launchctl load reported a problem; check `cockpit autostart status`.");
|
|
587
|
+
writeLine(io.stdout, `Plist: ${result.plist_path}`);
|
|
588
|
+
}
|
|
472
589
|
async function runOnboard(command, io) {
|
|
473
590
|
let install = null;
|
|
474
591
|
let pair = null;
|
|
@@ -480,6 +597,10 @@ async function runOnboard(command, io) {
|
|
|
480
597
|
writeLine(io.stdout, `Dashboard: ${command.dashboardUrl}`);
|
|
481
598
|
writeLine(io.stdout, `Ticket: ${command.activeTicketId ?? "general ambient"}`);
|
|
482
599
|
}
|
|
600
|
+
let claimedOwnerEmail = command.claimedOwnerEmail;
|
|
601
|
+
if (!claimedOwnerEmail && !command.json && isInteractiveStdin(io)) {
|
|
602
|
+
claimedOwnerEmail = await promptOnboardEmail(io);
|
|
603
|
+
}
|
|
483
604
|
install = await installLocalCollector({
|
|
484
605
|
homeDir: command.homeDir,
|
|
485
606
|
repoRoot: command.repoRoot,
|
|
@@ -496,7 +617,7 @@ async function runOnboard(command, io) {
|
|
|
496
617
|
branch: command.branch,
|
|
497
618
|
});
|
|
498
619
|
const installedSession = await readOnboardSessionReuseCandidate(command.homeDir);
|
|
499
|
-
const canReuseInstalledSession = canReuseOnboardSession(installedSession,
|
|
620
|
+
const canReuseInstalledSession = canReuseOnboardSession(installedSession, claimedOwnerEmail, command.dashboardUrl);
|
|
500
621
|
if (installedStatus.session_state === "valid" && canReuseInstalledSession) {
|
|
501
622
|
if (!command.json) {
|
|
502
623
|
writeLine(io.stdout, "2/5 Existing valid device session found; pairing skipped.");
|
|
@@ -512,7 +633,7 @@ async function runOnboard(command, io) {
|
|
|
512
633
|
pair = await pairLocalCollector({
|
|
513
634
|
homeDir: command.homeDir,
|
|
514
635
|
dashboardUrl: command.dashboardUrl,
|
|
515
|
-
claimedOwnerEmail
|
|
636
|
+
claimedOwnerEmail,
|
|
516
637
|
deviceName: command.deviceName,
|
|
517
638
|
pollIntervalMs: command.pollIntervalMs,
|
|
518
639
|
timeoutMs: command.timeoutMs,
|
|
@@ -538,6 +659,8 @@ async function runOnboard(command, io) {
|
|
|
538
659
|
codex_sessions: multi.codex_sessions,
|
|
539
660
|
}, null, 2));
|
|
540
661
|
}
|
|
662
|
+
if (multi.ok)
|
|
663
|
+
await maybeOfferAutostart(command, io);
|
|
541
664
|
return multi.ok ? 0 : 1;
|
|
542
665
|
}
|
|
543
666
|
const context = await startLocalWorkContext({
|
|
@@ -602,6 +725,7 @@ async function runOnboard(command, io) {
|
|
|
602
725
|
writeLine(io.stdout, `Upload state: ${status.upload_state}`);
|
|
603
726
|
writeLine(io.stdout, "PASS: Cockpit collector is ready for harvest.");
|
|
604
727
|
writeLine(io.stdout, `Open: ${command.dashboardUrl}/my-work`);
|
|
728
|
+
await maybeOfferAutostart(command, io);
|
|
605
729
|
return 0;
|
|
606
730
|
}
|
|
607
731
|
catch (error) {
|
|
@@ -1649,6 +1773,61 @@ async function runServe(command, io) {
|
|
|
1649
1773
|
});
|
|
1650
1774
|
return 0;
|
|
1651
1775
|
}
|
|
1776
|
+
async function runAutostart(command, io) {
|
|
1777
|
+
const exec = io.exec ?? defaultExec();
|
|
1778
|
+
const result = command.action === "install"
|
|
1779
|
+
? await installAutostartAgent({
|
|
1780
|
+
homeDir: command.homeDir,
|
|
1781
|
+
repoRoot: command.repoRoot,
|
|
1782
|
+
dashboardUrl: command.dashboardUrl,
|
|
1783
|
+
intervalSeconds: command.intervalSeconds,
|
|
1784
|
+
exec,
|
|
1785
|
+
})
|
|
1786
|
+
: command.action === "uninstall"
|
|
1787
|
+
? await uninstallAutostartAgent({ homeDir: command.homeDir, exec })
|
|
1788
|
+
: await autostartStatus({ homeDir: command.homeDir, exec });
|
|
1789
|
+
if (command.json) {
|
|
1790
|
+
writeLine(io.stdout, JSON.stringify(result, null, 2));
|
|
1791
|
+
return result.status === "unsupported" ? 1 : 0;
|
|
1792
|
+
}
|
|
1793
|
+
writeAutostartResult(io, result);
|
|
1794
|
+
return result.status === "unsupported" ? 1 : 0;
|
|
1795
|
+
}
|
|
1796
|
+
function writeAutostartResult(io, result) {
|
|
1797
|
+
switch (result.status) {
|
|
1798
|
+
case "installed":
|
|
1799
|
+
writeLine(io.stdout, result.loaded
|
|
1800
|
+
? "Cockpit autostart installed and loaded."
|
|
1801
|
+
: "Cockpit autostart installed (launchctl load reported a problem).");
|
|
1802
|
+
writeLine(io.stdout, `Label: ${result.label}`);
|
|
1803
|
+
writeLine(io.stdout, `Plist: ${result.plist_path}`);
|
|
1804
|
+
writeLine(io.stdout, `Interval: every ${result.interval_seconds}s`);
|
|
1805
|
+
writeLine(io.stdout, `Repo: ${result.work_dir}`);
|
|
1806
|
+
writeLine(io.stdout, `Dashboard: ${result.dashboard_url}`);
|
|
1807
|
+
if (!result.loaded && result.message)
|
|
1808
|
+
writeLine(io.stderr, result.message);
|
|
1809
|
+
return;
|
|
1810
|
+
case "uninstalled":
|
|
1811
|
+
writeLine(io.stdout, "Cockpit autostart removed.");
|
|
1812
|
+
writeLine(io.stdout, `Plist: ${result.plist_path}`);
|
|
1813
|
+
return;
|
|
1814
|
+
case "absent":
|
|
1815
|
+
writeLine(io.stdout, "Cockpit autostart is not installed.");
|
|
1816
|
+
writeLine(io.stdout, `Plist: ${result.plist_path}`);
|
|
1817
|
+
return;
|
|
1818
|
+
case "loaded":
|
|
1819
|
+
writeLine(io.stdout, "Cockpit autostart is installed and loaded.");
|
|
1820
|
+
writeLine(io.stdout, `Plist: ${result.plist_path}`);
|
|
1821
|
+
return;
|
|
1822
|
+
case "not_loaded":
|
|
1823
|
+
writeLine(io.stdout, "Cockpit autostart plist exists but is not loaded; run `cockpit autostart install` to reload.");
|
|
1824
|
+
writeLine(io.stdout, `Plist: ${result.plist_path}`);
|
|
1825
|
+
return;
|
|
1826
|
+
case "unsupported":
|
|
1827
|
+
writeLine(io.stderr, `Autostart unsupported: ${result.message}`);
|
|
1828
|
+
return;
|
|
1829
|
+
}
|
|
1830
|
+
}
|
|
1652
1831
|
function assertNoPositionals(positionals, command) {
|
|
1653
1832
|
if (positionals.length > 0) {
|
|
1654
1833
|
throw new Error(`${command} does not accept positional arguments.`);
|
|
@@ -1718,6 +1897,16 @@ function base64UrlToBase64(value) {
|
|
|
1718
1897
|
const normalized = value.replace(/-/g, "+").replace(/_/g, "/");
|
|
1719
1898
|
return `${normalized}${"=".repeat((4 - (normalized.length % 4)) % 4)}`;
|
|
1720
1899
|
}
|
|
1900
|
+
function defaultExec() {
|
|
1901
|
+
return (cmd, args) => new Promise((resolve) => {
|
|
1902
|
+
execFile(cmd, args, { encoding: "utf8" }, (err, stdout, stderr) => {
|
|
1903
|
+
// execFile's error `.code` is the exit code when numeric, but a string
|
|
1904
|
+
// (e.g. "ENOENT") for spawn failures — treat those as a generic 1.
|
|
1905
|
+
const code = err == null ? 0 : typeof err.code === "number" ? err.code : 1;
|
|
1906
|
+
resolve({ code, stdout: stdout ?? "", stderr: stderr ?? "" });
|
|
1907
|
+
});
|
|
1908
|
+
});
|
|
1909
|
+
}
|
|
1721
1910
|
function defaultIo() {
|
|
1722
1911
|
if (!globalThis.fetch) {
|
|
1723
1912
|
throw new Error("global fetch is unavailable; use Node.js 20 or newer.");
|
|
@@ -1728,6 +1917,7 @@ function defaultIo() {
|
|
|
1728
1917
|
stderr: process.stderr,
|
|
1729
1918
|
env: process.env,
|
|
1730
1919
|
fetch: globalThis.fetch.bind(globalThis),
|
|
1920
|
+
exec: defaultExec(),
|
|
1731
1921
|
};
|
|
1732
1922
|
}
|
|
1733
1923
|
function writeLine(stream, text) {
|
package/dist/local-state.js
CHANGED
|
@@ -5,8 +5,8 @@ import os from "node:os";
|
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { resolveRepoWorktreeIdentity, } from "./repo-identity.js";
|
|
7
7
|
import { summarizeLocalUploadSpool } from "./spool/local-spool.js";
|
|
8
|
-
export const LOCAL_COLLECTOR_VERSION = "0.1.
|
|
9
|
-
export const DEFAULT_DASHBOARD_URL = "
|
|
8
|
+
export const LOCAL_COLLECTOR_VERSION = "0.1.9";
|
|
9
|
+
export const DEFAULT_DASHBOARD_URL = "https://bli-cockpit-dashboard.vercel.app";
|
|
10
10
|
export function getCollectorRuntimePaths(homeDir = os.homedir()) {
|
|
11
11
|
const paths = getUserLocalCockpitPaths(homeDir);
|
|
12
12
|
return {
|