@bli-cockpit/cli 0.1.8 → 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 +163 -9
- package/dist/local-state.js +1 -1
- 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) {
|
|
@@ -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);
|
|
@@ -473,17 +520,14 @@ function isInteractiveStdin(io) {
|
|
|
473
520
|
return Boolean(io.stdin.isTTY);
|
|
474
521
|
}
|
|
475
522
|
/**
|
|
476
|
-
* Reads one line from stdin
|
|
477
|
-
*
|
|
478
|
-
*
|
|
479
|
-
* never block on input — they keep the existing behaviour (email optional; the
|
|
480
|
-
* approving admin's account owns the device). An empty answer or a non-email
|
|
481
|
-
* skips rather than failing, matching `optionalEmail`'s leniency.
|
|
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.
|
|
482
526
|
*/
|
|
483
|
-
async function
|
|
484
|
-
io.stdout.write(
|
|
527
|
+
async function readLine(io, prompt) {
|
|
528
|
+
io.stdout.write(prompt);
|
|
485
529
|
io.stdin.setEncoding("utf8");
|
|
486
|
-
|
|
530
|
+
return new Promise((resolve) => {
|
|
487
531
|
const onData = (chunk) => {
|
|
488
532
|
io.stdin.removeListener("data", onData);
|
|
489
533
|
io.stdin.pause();
|
|
@@ -492,6 +536,15 @@ async function promptOnboardEmail(io) {
|
|
|
492
536
|
io.stdin.resume();
|
|
493
537
|
io.stdin.on("data", onData);
|
|
494
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): ");
|
|
495
548
|
const answer = raw.trim().toLowerCase();
|
|
496
549
|
if (!answer)
|
|
497
550
|
return undefined;
|
|
@@ -501,6 +554,38 @@ async function promptOnboardEmail(io) {
|
|
|
501
554
|
}
|
|
502
555
|
return answer;
|
|
503
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
|
+
}
|
|
504
589
|
async function runOnboard(command, io) {
|
|
505
590
|
let install = null;
|
|
506
591
|
let pair = null;
|
|
@@ -574,6 +659,8 @@ async function runOnboard(command, io) {
|
|
|
574
659
|
codex_sessions: multi.codex_sessions,
|
|
575
660
|
}, null, 2));
|
|
576
661
|
}
|
|
662
|
+
if (multi.ok)
|
|
663
|
+
await maybeOfferAutostart(command, io);
|
|
577
664
|
return multi.ok ? 0 : 1;
|
|
578
665
|
}
|
|
579
666
|
const context = await startLocalWorkContext({
|
|
@@ -638,6 +725,7 @@ async function runOnboard(command, io) {
|
|
|
638
725
|
writeLine(io.stdout, `Upload state: ${status.upload_state}`);
|
|
639
726
|
writeLine(io.stdout, "PASS: Cockpit collector is ready for harvest.");
|
|
640
727
|
writeLine(io.stdout, `Open: ${command.dashboardUrl}/my-work`);
|
|
728
|
+
await maybeOfferAutostart(command, io);
|
|
641
729
|
return 0;
|
|
642
730
|
}
|
|
643
731
|
catch (error) {
|
|
@@ -1685,6 +1773,61 @@ async function runServe(command, io) {
|
|
|
1685
1773
|
});
|
|
1686
1774
|
return 0;
|
|
1687
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
|
+
}
|
|
1688
1831
|
function assertNoPositionals(positionals, command) {
|
|
1689
1832
|
if (positionals.length > 0) {
|
|
1690
1833
|
throw new Error(`${command} does not accept positional arguments.`);
|
|
@@ -1754,6 +1897,16 @@ function base64UrlToBase64(value) {
|
|
|
1754
1897
|
const normalized = value.replace(/-/g, "+").replace(/_/g, "/");
|
|
1755
1898
|
return `${normalized}${"=".repeat((4 - (normalized.length % 4)) % 4)}`;
|
|
1756
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
|
+
}
|
|
1757
1910
|
function defaultIo() {
|
|
1758
1911
|
if (!globalThis.fetch) {
|
|
1759
1912
|
throw new Error("global fetch is unavailable; use Node.js 20 or newer.");
|
|
@@ -1764,6 +1917,7 @@ function defaultIo() {
|
|
|
1764
1917
|
stderr: process.stderr,
|
|
1765
1918
|
env: process.env,
|
|
1766
1919
|
fetch: globalThis.fetch.bind(globalThis),
|
|
1920
|
+
exec: defaultExec(),
|
|
1767
1921
|
};
|
|
1768
1922
|
}
|
|
1769
1923
|
function writeLine(stream, text) {
|
package/dist/local-state.js
CHANGED
|
@@ -5,7 +5,7 @@ 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.
|
|
8
|
+
export const LOCAL_COLLECTOR_VERSION = "0.1.9";
|
|
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);
|