@cabane/companion 0.6.239 → 0.6.241
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 +2 -2
- package/dist/cli.js +273 -223
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -38,7 +38,7 @@ Enter the code in Cabane, signed in as yourself: from Your settings → Devices
|
|
|
38
38
|
|
|
39
39
|
Without a terminal, setup prints the same lines once and waits, so a container's logs carry the code. An expired code is replaced by a fresh one, up to three; Ctrl-C stops cleanly. The link is on the Cabane the device pairs with. There is no default: setup pairs with the Cabane named by `--server <url>`, by `CABANE_BASE_URL`, or by the device's config, and stops if none names one. Once a Cabane has issued a code, setup keeps its address, so after a cancelled or expired pairing a bare `cabane setup` pairs with the same one.
|
|
40
40
|
|
|
41
|
-
The address must be https, or plain http to this machine (`localhost`, `127.0.0.1`). A Companion that reaches its Cabane across a private network where the server has no certificate, such as a container on a Docker network it shares only with the server, can be allowed plain http to that one host with `CABANE_ALLOW_PLAIN_HTTP_HOST`: for example `CABANE_ALLOW_PLAIN_HTTP_HOST=app` with `CABANE_BASE_URL=http://app:8788`. The value must be a name with no dot, which only the local network's resolver answers, or a private address (`10.x.x.x`, `172.16.x.x`–`172.31.x.x`, `192.168.x.x`, or IPv6 `fc00::/7`); anything else is ignored. A Companion that pairs over plain http to that host keeps that address, instead of the public one Cabane names, since that is the only way it reaches Cabane. Over plain http, anything else on that network can read the device token and the agents' work, so use it only on a network nothing else joins.
|
|
41
|
+
The address must be https, or plain http to this machine (`localhost`, `127.0.0.1`). A Companion that reaches its Cabane across a private network where the server has no certificate, such as a container on a Docker network it shares only with the server, can be allowed plain http to that one host with `CABANE_ALLOW_PLAIN_HTTP_HOST`: for example `CABANE_ALLOW_PLAIN_HTTP_HOST=app` with `CABANE_BASE_URL=http://app:8788`. The value must be a name with no dot, which only the local network's resolver answers, or a private address (`10.x.x.x`, `172.16.x.x`–`172.31.x.x`, `192.168.x.x`, or IPv6 `fc00::/7`); anything else is ignored. A Companion that pairs over plain http to that host keeps that address, instead of the public one Cabane names, since that is the only way it reaches Cabane. A browser can't open that address, so the Companion doesn't print it: where it would name the device's page in Cabane, it says Your settings → Devices instead. Over plain http, anything else on that network can read the device token and the agents' work, so use it only on a network nothing else joins.
|
|
42
42
|
|
|
43
43
|
From an interactive terminal, setup then starts the Companion in the background, waits for its first check-in and reports the device's state. Without an interactive terminal, setup stays attached to the running Companion.
|
|
44
44
|
|
|
@@ -59,7 +59,7 @@ If no harness is usable, interactive setup asks which one you want to set up. Fo
|
|
|
59
59
|
In Cabane: https://cabane.example.com/settings/devices
|
|
60
60
|
```
|
|
61
61
|
|
|
62
|
-
Setup never installs the service. On macOS or Linux, run `cabane service install` when you want the Companion to start at login and keep itself updated; the closing block names it. Native Windows uses the background process and has no service installer.
|
|
62
|
+
Setup never installs the service. On macOS or Linux, run `cabane service install` when you want the Companion to start at login and keep itself updated; the closing block names it. Native Windows uses the background process and has no service installer. Inside a container the Companion names neither the service nor automatic updates, in the closing block, `cabane status` or `cabane update`: whatever runs the container keeps it running, and you update it by replacing the container with a newer image.
|
|
63
63
|
|
|
64
64
|
If `cabane` isn't found afterwards, open a new terminal, or run the `source` line setup printed.
|
|
65
65
|
|
package/dist/cli.js
CHANGED
|
@@ -325,15 +325,21 @@ function privateIpv6(host) {
|
|
|
325
325
|
return (first & 65024) === 64512 ? bracketed : null;
|
|
326
326
|
}
|
|
327
327
|
function addressToKeep(pairedOver, cabaneNamed, env = process.env) {
|
|
328
|
+
return onPlainHttpHost(pairedOver, env) ? pairedOver.replace(/\/+$/, "") : cabaneNamed;
|
|
329
|
+
}
|
|
330
|
+
function browserAddress(baseUrl, env = process.env) {
|
|
331
|
+
return onPlainHttpHost(baseUrl, env) ? null : baseUrl.replace(/\/+$/, "");
|
|
332
|
+
}
|
|
333
|
+
function onPlainHttpHost(raw, env) {
|
|
328
334
|
const allowed = plainHttpHost(env);
|
|
329
|
-
if (allowed === null) return
|
|
335
|
+
if (allowed === null) return false;
|
|
330
336
|
let url;
|
|
331
337
|
try {
|
|
332
|
-
url = new URL(
|
|
338
|
+
url = new URL(raw);
|
|
333
339
|
} catch {
|
|
334
|
-
return
|
|
340
|
+
return false;
|
|
335
341
|
}
|
|
336
|
-
return url.protocol === "http:" && url.hostname.toLowerCase() === allowed
|
|
342
|
+
return url.protocol === "http:" && url.hostname.toLowerCase() === allowed;
|
|
337
343
|
}
|
|
338
344
|
var ALLOW_PLAIN_HTTP_HOST_ENV;
|
|
339
345
|
var init_pairing = __esm({
|
|
@@ -10109,7 +10115,12 @@ async function awaitEnrollment(baseUrl, code, opts = {}) {
|
|
|
10109
10115
|
init_cabane_command();
|
|
10110
10116
|
init_device_state();
|
|
10111
10117
|
init_harness_copy();
|
|
10118
|
+
init_pairing();
|
|
10112
10119
|
var UNPAIRED = `This device isn't paired yet. Run ${cabane("setup")} on it.`;
|
|
10120
|
+
function devicesUrl(baseUrl, env = process.env) {
|
|
10121
|
+
const base = browserAddress(baseUrl, env);
|
|
10122
|
+
return base === null ? null : `${base}/settings/devices`;
|
|
10123
|
+
}
|
|
10113
10124
|
var lease = DEFAULT_DEVICE_OFFLINE_THRESHOLD_MS;
|
|
10114
10125
|
function age(at, now) {
|
|
10115
10126
|
const seconds = Math.max(0, Math.floor((now - Date.parse(at)) / 1e3));
|
|
@@ -10118,7 +10129,7 @@ function age(at, now) {
|
|
|
10118
10129
|
function deviceLine(f, now = Date.now()) {
|
|
10119
10130
|
if (!f.running) return `not running \u2014 start it with ${cabane("start")}`;
|
|
10120
10131
|
if (f.lastHeartbeatRefused)
|
|
10121
|
-
return `refused by the server \u2014 it may be deactivated; see ${f.devicesUrl}
|
|
10132
|
+
return f.devicesUrl ? `refused by the server \u2014 it may be deactivated; see ${f.devicesUrl}` : "refused by the server \u2014 it may be deactivated; see Your settings \u2192 Devices in Cabane";
|
|
10122
10133
|
if (!f.lastHeartbeatAckAt) return "starting \u2014 hasn't checked in yet";
|
|
10123
10134
|
const since = age(f.lastHeartbeatAckAt, now);
|
|
10124
10135
|
if (now - Date.parse(f.lastHeartbeatAckAt) > lease)
|
|
@@ -10167,7 +10178,8 @@ function closingBlock(input, now = Date.now()) {
|
|
|
10167
10178
|
"",
|
|
10168
10179
|
` Status ${cabane("status")}${mode === "attached" ? "" : ` Stop ${cabane("stop")}`} Log ${cabane("logs")}`
|
|
10169
10180
|
);
|
|
10170
|
-
if (
|
|
10181
|
+
if (input.container) {
|
|
10182
|
+
} else if (!input.service && input.servicePlatform) {
|
|
10171
10183
|
lines2.push(
|
|
10172
10184
|
` Start at login and keep Cabane updating itself: ${cabane("service install")}${input.servicePlatform === "launchd" ? " (macOS will ask you to allow a background item)" : ""}`
|
|
10173
10185
|
);
|
|
@@ -11632,14 +11644,15 @@ function osUser() {
|
|
|
11632
11644
|
return typeof process.getuid === "function" ? `uid ${process.getuid()}` : "unknown";
|
|
11633
11645
|
}
|
|
11634
11646
|
}
|
|
11635
|
-
function
|
|
11636
|
-
let container;
|
|
11647
|
+
function containerHere() {
|
|
11637
11648
|
try {
|
|
11638
|
-
|
|
11649
|
+
return inContainer(readContainerSignals());
|
|
11639
11650
|
} catch {
|
|
11640
|
-
|
|
11651
|
+
return null;
|
|
11641
11652
|
}
|
|
11642
|
-
|
|
11653
|
+
}
|
|
11654
|
+
function hostReport() {
|
|
11655
|
+
return { user: osUser(), container: containerHere() };
|
|
11643
11656
|
}
|
|
11644
11657
|
|
|
11645
11658
|
// src/providers/local-process.ts
|
|
@@ -12325,7 +12338,7 @@ async function verifyInstall(root, version, harnesses, deps) {
|
|
|
12325
12338
|
env: { ...process.env, CABANE_LAUNCH_SELF: "1" }
|
|
12326
12339
|
});
|
|
12327
12340
|
if (answered.code !== 0 || answered.stdout.trim() !== version) {
|
|
12328
|
-
return `it doesn't start: ${
|
|
12341
|
+
return `it doesn't start: ${errorLine(answered.stderr) || "no version printed"}`;
|
|
12329
12342
|
}
|
|
12330
12343
|
if (harnesses.length === 0) return null;
|
|
12331
12344
|
const probed = await deps.run(deps.execPath, [join24(root, "dist", "postinstall.js"), "--json"], {
|
|
@@ -12347,8 +12360,21 @@ function harnessName(h) {
|
|
|
12347
12360
|
return h === "claude-code" ? "Claude Code" : "Codex";
|
|
12348
12361
|
}
|
|
12349
12362
|
function lastLine(text) {
|
|
12350
|
-
|
|
12351
|
-
|
|
12363
|
+
return nonEmptyLines(text).at(-1)?.slice(0, 300) ?? "";
|
|
12364
|
+
}
|
|
12365
|
+
function errorLine(stderr) {
|
|
12366
|
+
const lines2 = nonEmptyLines(stderr).filter(
|
|
12367
|
+
(l) => !/^Node\.js v\d|^\(Use `node |^\(node:\d+\) /.test(l)
|
|
12368
|
+
);
|
|
12369
|
+
const caret = lines2.findIndex((l) => /^\^+$/.test(l));
|
|
12370
|
+
const rest = caret >= 0 && caret <= 2 ? lines2.slice(caret + 1) : lines2;
|
|
12371
|
+
const named = rest.find(
|
|
12372
|
+
(l) => /^(?:[A-Z][\w$]*)?(?:Error|Exception)(?: \[[A-Z0-9_]+\])?(?::|$)/.test(l)
|
|
12373
|
+
);
|
|
12374
|
+
return (named ?? rest[0])?.slice(0, 300) ?? "";
|
|
12375
|
+
}
|
|
12376
|
+
function nonEmptyLines(text) {
|
|
12377
|
+
return text.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
12352
12378
|
}
|
|
12353
12379
|
var CompanionUpdater = class {
|
|
12354
12380
|
constructor(deps) {
|
|
@@ -14832,6 +14858,217 @@ function defaultSpawnDetached(args) {
|
|
|
14832
14858
|
}
|
|
14833
14859
|
}
|
|
14834
14860
|
|
|
14861
|
+
// src/commands/service.ts
|
|
14862
|
+
init_cabane_command();
|
|
14863
|
+
init_config();
|
|
14864
|
+
init_term();
|
|
14865
|
+
|
|
14866
|
+
// src/commands/stop.ts
|
|
14867
|
+
var TERM_GRACE_MS = 6e3;
|
|
14868
|
+
var POLL_INTERVAL_MS = 150;
|
|
14869
|
+
async function stop(deps = {}) {
|
|
14870
|
+
if ((deps.serviceInstalled ?? serviceInstalled)()) {
|
|
14871
|
+
const outcome = await (deps.stopService ?? stopService)();
|
|
14872
|
+
for (const line of outcome.lines) process.stdout.write(`${line}
|
|
14873
|
+
`);
|
|
14874
|
+
if (!outcome.ok) {
|
|
14875
|
+
process.exitCode = 1;
|
|
14876
|
+
return;
|
|
14877
|
+
}
|
|
14878
|
+
process.stdout.write(
|
|
14879
|
+
"companion: stopped the service. It starts again at boot, or with `cabane start`.\n"
|
|
14880
|
+
);
|
|
14881
|
+
return;
|
|
14882
|
+
}
|
|
14883
|
+
const readState = deps.readState ?? readLiveRuntimeState;
|
|
14884
|
+
const verify = deps.verify ?? ((s) => verifyRuntime(s));
|
|
14885
|
+
const kill = deps.kill ?? ((pid2, signal) => process.kill(pid2, signal));
|
|
14886
|
+
const sleep4 = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
14887
|
+
const now = deps.now ?? (() => Date.now());
|
|
14888
|
+
const state = readState();
|
|
14889
|
+
if (!state) {
|
|
14890
|
+
process.stdout.write("companion: not running (nothing to stop).\n");
|
|
14891
|
+
return;
|
|
14892
|
+
}
|
|
14893
|
+
if (await verify(state) === "stale") {
|
|
14894
|
+
clearRuntimeState();
|
|
14895
|
+
process.stdout.write("companion: not running (stale marker swept).\n");
|
|
14896
|
+
return;
|
|
14897
|
+
}
|
|
14898
|
+
const { pid } = state;
|
|
14899
|
+
process.stdout.write(`companion: stopping (pid ${pid})\u2026
|
|
14900
|
+
`);
|
|
14901
|
+
try {
|
|
14902
|
+
kill(pid, "SIGTERM");
|
|
14903
|
+
} catch {
|
|
14904
|
+
clearRuntimeState();
|
|
14905
|
+
process.stdout.write("companion: already stopped.\n");
|
|
14906
|
+
return;
|
|
14907
|
+
}
|
|
14908
|
+
const deadline = now() + TERM_GRACE_MS;
|
|
14909
|
+
while (now() < deadline) {
|
|
14910
|
+
if (!isAlive(kill, pid)) {
|
|
14911
|
+
process.stdout.write("companion: stopped.\n");
|
|
14912
|
+
return;
|
|
14913
|
+
}
|
|
14914
|
+
await sleep4(POLL_INTERVAL_MS);
|
|
14915
|
+
}
|
|
14916
|
+
process.stdout.write(
|
|
14917
|
+
`companion: didn't exit within ${TERM_GRACE_MS / 1e3}s, sending SIGKILL.
|
|
14918
|
+
`
|
|
14919
|
+
);
|
|
14920
|
+
try {
|
|
14921
|
+
kill(pid, "SIGKILL");
|
|
14922
|
+
} catch {
|
|
14923
|
+
}
|
|
14924
|
+
clearRuntimeState();
|
|
14925
|
+
process.stdout.write("companion: force-stopped.\n");
|
|
14926
|
+
}
|
|
14927
|
+
function isAlive(kill, pid) {
|
|
14928
|
+
try {
|
|
14929
|
+
kill(pid, 0);
|
|
14930
|
+
return true;
|
|
14931
|
+
} catch {
|
|
14932
|
+
return false;
|
|
14933
|
+
}
|
|
14934
|
+
}
|
|
14935
|
+
|
|
14936
|
+
// src/commands/service.ts
|
|
14937
|
+
var STARTUP_WAIT_MS = 2e4;
|
|
14938
|
+
async function installAndStartService(deps = {}) {
|
|
14939
|
+
const readState = deps.readState ?? readLiveRuntimeState;
|
|
14940
|
+
const sleep4 = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
14941
|
+
const now = deps.now ?? Date.now;
|
|
14942
|
+
const before = await serviceState(deps);
|
|
14943
|
+
const running = readState();
|
|
14944
|
+
if (running && running.pid !== before.pid) await (deps.stopRunning ?? (() => stop()))();
|
|
14945
|
+
setRunsAsService(true);
|
|
14946
|
+
const outcome = await installService(deps);
|
|
14947
|
+
for (const line of outcome.lines) write(`${INDENT}${line}`);
|
|
14948
|
+
if (!outcome.ok) {
|
|
14949
|
+
setRunsAsService(false);
|
|
14950
|
+
return false;
|
|
14951
|
+
}
|
|
14952
|
+
const deadline = now() + STARTUP_WAIT_MS;
|
|
14953
|
+
while (now() < deadline) {
|
|
14954
|
+
const state = readState();
|
|
14955
|
+
if (state?.socket) return true;
|
|
14956
|
+
await sleep4(250);
|
|
14957
|
+
}
|
|
14958
|
+
write(
|
|
14959
|
+
`${INDENT}The service is installed but Cabane hasn't come up yet. ${cabane("service status")} and ${cabane("logs")} say why.`
|
|
14960
|
+
);
|
|
14961
|
+
return false;
|
|
14962
|
+
}
|
|
14963
|
+
async function serviceInstall(deps = {}) {
|
|
14964
|
+
if (!await installAndStartService(deps)) {
|
|
14965
|
+
process.exitCode = 1;
|
|
14966
|
+
return;
|
|
14967
|
+
}
|
|
14968
|
+
write(
|
|
14969
|
+
INDENT + tick(
|
|
14970
|
+
"Cabane runs as a service. It starts at boot, restarts if it stops, and updates itself, usually between turns."
|
|
14971
|
+
)
|
|
14972
|
+
);
|
|
14973
|
+
write(`${INDENT} Status: ${cabane("service status")} Logs: ${cabane("logs")}`);
|
|
14974
|
+
}
|
|
14975
|
+
async function serviceUninstall(deps = {}) {
|
|
14976
|
+
const outcome = await uninstallService(deps);
|
|
14977
|
+
for (const line of outcome.lines) write(`${INDENT}${line}`);
|
|
14978
|
+
if (outcome.lines.length > 0) return;
|
|
14979
|
+
setRunsAsService(false);
|
|
14980
|
+
write(
|
|
14981
|
+
INDENT + tick("Removed the service. Cabane won't start at boot, and it isn't running now.")
|
|
14982
|
+
);
|
|
14983
|
+
write(`${INDENT} Start it again with ${cabane("start")}`);
|
|
14984
|
+
}
|
|
14985
|
+
async function serviceStatus(deps = {}) {
|
|
14986
|
+
const state = await serviceState(deps);
|
|
14987
|
+
const container = containerWords(containerHere() === true, state);
|
|
14988
|
+
for (const line of serviceRows(state, { container })) write(line);
|
|
14989
|
+
for (const line of updateRows(readUpdateState(), safeConfig()?.update?.auto, {
|
|
14990
|
+
service: state.installed,
|
|
14991
|
+
container
|
|
14992
|
+
}))
|
|
14993
|
+
write(line);
|
|
14994
|
+
if (state.installed && state.state !== "running") process.exitCode = 1;
|
|
14995
|
+
}
|
|
14996
|
+
var LABEL_WIDTH = 13;
|
|
14997
|
+
var row = (label, value) => ` ${label.padEnd(LABEL_WIDTH)}${value}`;
|
|
14998
|
+
function containerWords(container, state, opts = {}) {
|
|
14999
|
+
if (!container || opts.supervisedHere) return false;
|
|
15000
|
+
const serviceRuns = state.installed && state.state === "running";
|
|
15001
|
+
if (!serviceRuns) return true;
|
|
15002
|
+
return opts.companion !== void 0 && state.pid !== opts.companion;
|
|
15003
|
+
}
|
|
15004
|
+
function serviceRows(state, opts = {}) {
|
|
15005
|
+
if (opts.container) return [row("Service", "not used in a container")];
|
|
15006
|
+
if (!state.installed) {
|
|
15007
|
+
return [
|
|
15008
|
+
row(
|
|
15009
|
+
"Service",
|
|
15010
|
+
state.platform ? `not installed \u2014 ${cabane("service install")} starts Cabane at boot` : "not available on this system"
|
|
15011
|
+
)
|
|
15012
|
+
];
|
|
15013
|
+
}
|
|
15014
|
+
const via = state.platform === "systemd" ? "systemd" : "launchd";
|
|
15015
|
+
const rows = [
|
|
15016
|
+
row(
|
|
15017
|
+
"Service",
|
|
15018
|
+
state.state === "running" ? `running (${via}${state.pid ? `, pid ${state.pid}` : ""})` : `installed, not running (${via}: ${state.state})`
|
|
15019
|
+
)
|
|
15020
|
+
];
|
|
15021
|
+
if (!state.nodeOk) {
|
|
15022
|
+
rows.push(
|
|
15023
|
+
` ! The service's Node at ${tildePath(state.node ?? "")} is gone. Run ${cabane("service install")} again.`
|
|
15024
|
+
);
|
|
15025
|
+
}
|
|
15026
|
+
return rows;
|
|
15027
|
+
}
|
|
15028
|
+
function updateRows(state, auto, opts = {}) {
|
|
15029
|
+
const rows = [];
|
|
15030
|
+
if (opts.version !== false) rows.push(row("Version", COMPANION_VERSION));
|
|
15031
|
+
if (state.server) {
|
|
15032
|
+
rows.push(
|
|
15033
|
+
row(
|
|
15034
|
+
"Cabane wants",
|
|
15035
|
+
`${state.server.latest} (the latest), ${state.server.minimum} or newer to run turns`
|
|
15036
|
+
)
|
|
15037
|
+
);
|
|
15038
|
+
}
|
|
15039
|
+
rows.push(
|
|
15040
|
+
row(
|
|
15041
|
+
"Auto-update",
|
|
15042
|
+
opts.container ? "off \u2014 to update, replace the container with a newer image" : !opts.service ? `off \u2014 runs with the service: ${cabane("service install")}` : auto === true ? "on \u2014 updates install between turns when they can, and roll back if they fail" : `off \u2014 ${cabane("update --auto on")} turns it on`
|
|
15043
|
+
)
|
|
15044
|
+
);
|
|
15045
|
+
const last = state.lastResult;
|
|
15046
|
+
if (last) {
|
|
15047
|
+
const when = new Date(last.at).toLocaleDateString("en-GB", { day: "numeric", month: "short" });
|
|
15048
|
+
const why = last.reason ? `: ${last.reason}` : "";
|
|
15049
|
+
rows.push(
|
|
15050
|
+
row(
|
|
15051
|
+
"Last update",
|
|
15052
|
+
last.kind === "updated" ? `${last.from} \u2192 ${last.to} on ${when}` : last.kind === "rolled_back" ? `${last.to} rolled back to ${last.from} on ${when}${why}` : `${last.to} failed on ${when}${why}`
|
|
15053
|
+
)
|
|
15054
|
+
);
|
|
15055
|
+
}
|
|
15056
|
+
return rows;
|
|
15057
|
+
}
|
|
15058
|
+
function safeConfig() {
|
|
15059
|
+
try {
|
|
15060
|
+
return loadConfig();
|
|
15061
|
+
} catch {
|
|
15062
|
+
return null;
|
|
15063
|
+
}
|
|
15064
|
+
}
|
|
15065
|
+
function setRunsAsService(on) {
|
|
15066
|
+
const cfg = safeConfig();
|
|
15067
|
+
if (!cfg) return;
|
|
15068
|
+
const { serviceDeclined: _declined, ...rest } = cfg;
|
|
15069
|
+
saveConfig({ ...rest, update: { auto: on } });
|
|
15070
|
+
}
|
|
15071
|
+
|
|
14835
15072
|
// src/terminal-status.ts
|
|
14836
15073
|
async function readTerminalStatus(socket, timeoutMs = 1e3) {
|
|
14837
15074
|
if (!socket) return emptyHeartbeatStatus();
|
|
@@ -14857,6 +15094,14 @@ async function waitForAcknowledgement(socket) {
|
|
|
14857
15094
|
}
|
|
14858
15095
|
|
|
14859
15096
|
// src/commands/start.ts
|
|
15097
|
+
var NO_SERVICE = {
|
|
15098
|
+
installed: false,
|
|
15099
|
+
platform: null,
|
|
15100
|
+
state: "not installed",
|
|
15101
|
+
pid: null,
|
|
15102
|
+
node: null,
|
|
15103
|
+
nodeOk: true
|
|
15104
|
+
};
|
|
14860
15105
|
async function start(opts = {}, deps = {}) {
|
|
14861
15106
|
setLogOverrides(opts);
|
|
14862
15107
|
setConsoleLogging(false);
|
|
@@ -14970,7 +15215,11 @@ async function startScript(opts, deps) {
|
|
|
14970
15215
|
if (service2.state !== "running" || service2.pid !== current.pid) mode = "existing";
|
|
14971
15216
|
}
|
|
14972
15217
|
}
|
|
14973
|
-
const address =
|
|
15218
|
+
const address = devicesUrl(cfg.baseUrl);
|
|
15219
|
+
const container = containerHere() === true && containerWords(true, installed ? await serviceState() : NO_SERVICE, {
|
|
15220
|
+
companion: runtime ? process.pid : running?.pid ?? null,
|
|
15221
|
+
supervisedHere: runtime !== null && process.env.CABANE_COMPANION_SUPERVISED === "1"
|
|
15222
|
+
});
|
|
14974
15223
|
if (!daemon)
|
|
14975
15224
|
for (const line of closingBlock({
|
|
14976
15225
|
mode,
|
|
@@ -14980,6 +15229,7 @@ async function startScript(opts, deps) {
|
|
|
14980
15229
|
service: installed,
|
|
14981
15230
|
servicePlatform: servicePlatform(),
|
|
14982
15231
|
autoUpdate: cfg.update?.auto !== false,
|
|
15232
|
+
container,
|
|
14983
15233
|
address
|
|
14984
15234
|
}))
|
|
14985
15235
|
write(line);
|
|
@@ -15371,210 +15621,6 @@ init_harness_copy();
|
|
|
15371
15621
|
init_secrets();
|
|
15372
15622
|
init_term();
|
|
15373
15623
|
init_transcript_writer();
|
|
15374
|
-
|
|
15375
|
-
// src/commands/service.ts
|
|
15376
|
-
init_cabane_command();
|
|
15377
|
-
init_config();
|
|
15378
|
-
init_term();
|
|
15379
|
-
|
|
15380
|
-
// src/commands/stop.ts
|
|
15381
|
-
var TERM_GRACE_MS = 6e3;
|
|
15382
|
-
var POLL_INTERVAL_MS = 150;
|
|
15383
|
-
async function stop(deps = {}) {
|
|
15384
|
-
if ((deps.serviceInstalled ?? serviceInstalled)()) {
|
|
15385
|
-
const outcome = await (deps.stopService ?? stopService)();
|
|
15386
|
-
for (const line of outcome.lines) process.stdout.write(`${line}
|
|
15387
|
-
`);
|
|
15388
|
-
if (!outcome.ok) {
|
|
15389
|
-
process.exitCode = 1;
|
|
15390
|
-
return;
|
|
15391
|
-
}
|
|
15392
|
-
process.stdout.write(
|
|
15393
|
-
"companion: stopped the service. It starts again at boot, or with `cabane start`.\n"
|
|
15394
|
-
);
|
|
15395
|
-
return;
|
|
15396
|
-
}
|
|
15397
|
-
const readState = deps.readState ?? readLiveRuntimeState;
|
|
15398
|
-
const verify = deps.verify ?? ((s) => verifyRuntime(s));
|
|
15399
|
-
const kill = deps.kill ?? ((pid2, signal) => process.kill(pid2, signal));
|
|
15400
|
-
const sleep4 = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
15401
|
-
const now = deps.now ?? (() => Date.now());
|
|
15402
|
-
const state = readState();
|
|
15403
|
-
if (!state) {
|
|
15404
|
-
process.stdout.write("companion: not running (nothing to stop).\n");
|
|
15405
|
-
return;
|
|
15406
|
-
}
|
|
15407
|
-
if (await verify(state) === "stale") {
|
|
15408
|
-
clearRuntimeState();
|
|
15409
|
-
process.stdout.write("companion: not running (stale marker swept).\n");
|
|
15410
|
-
return;
|
|
15411
|
-
}
|
|
15412
|
-
const { pid } = state;
|
|
15413
|
-
process.stdout.write(`companion: stopping (pid ${pid})\u2026
|
|
15414
|
-
`);
|
|
15415
|
-
try {
|
|
15416
|
-
kill(pid, "SIGTERM");
|
|
15417
|
-
} catch {
|
|
15418
|
-
clearRuntimeState();
|
|
15419
|
-
process.stdout.write("companion: already stopped.\n");
|
|
15420
|
-
return;
|
|
15421
|
-
}
|
|
15422
|
-
const deadline = now() + TERM_GRACE_MS;
|
|
15423
|
-
while (now() < deadline) {
|
|
15424
|
-
if (!isAlive(kill, pid)) {
|
|
15425
|
-
process.stdout.write("companion: stopped.\n");
|
|
15426
|
-
return;
|
|
15427
|
-
}
|
|
15428
|
-
await sleep4(POLL_INTERVAL_MS);
|
|
15429
|
-
}
|
|
15430
|
-
process.stdout.write(
|
|
15431
|
-
`companion: didn't exit within ${TERM_GRACE_MS / 1e3}s, sending SIGKILL.
|
|
15432
|
-
`
|
|
15433
|
-
);
|
|
15434
|
-
try {
|
|
15435
|
-
kill(pid, "SIGKILL");
|
|
15436
|
-
} catch {
|
|
15437
|
-
}
|
|
15438
|
-
clearRuntimeState();
|
|
15439
|
-
process.stdout.write("companion: force-stopped.\n");
|
|
15440
|
-
}
|
|
15441
|
-
function isAlive(kill, pid) {
|
|
15442
|
-
try {
|
|
15443
|
-
kill(pid, 0);
|
|
15444
|
-
return true;
|
|
15445
|
-
} catch {
|
|
15446
|
-
return false;
|
|
15447
|
-
}
|
|
15448
|
-
}
|
|
15449
|
-
|
|
15450
|
-
// src/commands/service.ts
|
|
15451
|
-
var STARTUP_WAIT_MS = 2e4;
|
|
15452
|
-
async function installAndStartService(deps = {}) {
|
|
15453
|
-
const readState = deps.readState ?? readLiveRuntimeState;
|
|
15454
|
-
const sleep4 = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
15455
|
-
const now = deps.now ?? Date.now;
|
|
15456
|
-
const before = await serviceState(deps);
|
|
15457
|
-
const running = readState();
|
|
15458
|
-
if (running && running.pid !== before.pid) await (deps.stopRunning ?? (() => stop()))();
|
|
15459
|
-
setRunsAsService(true);
|
|
15460
|
-
const outcome = await installService(deps);
|
|
15461
|
-
for (const line of outcome.lines) write(`${INDENT}${line}`);
|
|
15462
|
-
if (!outcome.ok) {
|
|
15463
|
-
setRunsAsService(false);
|
|
15464
|
-
return false;
|
|
15465
|
-
}
|
|
15466
|
-
const deadline = now() + STARTUP_WAIT_MS;
|
|
15467
|
-
while (now() < deadline) {
|
|
15468
|
-
const state = readState();
|
|
15469
|
-
if (state?.socket) return true;
|
|
15470
|
-
await sleep4(250);
|
|
15471
|
-
}
|
|
15472
|
-
write(
|
|
15473
|
-
`${INDENT}The service is installed but Cabane hasn't come up yet. ${cabane("service status")} and ${cabane("logs")} say why.`
|
|
15474
|
-
);
|
|
15475
|
-
return false;
|
|
15476
|
-
}
|
|
15477
|
-
async function serviceInstall(deps = {}) {
|
|
15478
|
-
if (!await installAndStartService(deps)) {
|
|
15479
|
-
process.exitCode = 1;
|
|
15480
|
-
return;
|
|
15481
|
-
}
|
|
15482
|
-
write(
|
|
15483
|
-
INDENT + tick(
|
|
15484
|
-
"Cabane runs as a service. It starts at boot, restarts if it stops, and updates itself, usually between turns."
|
|
15485
|
-
)
|
|
15486
|
-
);
|
|
15487
|
-
write(`${INDENT} Status: ${cabane("service status")} Logs: ${cabane("logs")}`);
|
|
15488
|
-
}
|
|
15489
|
-
async function serviceUninstall(deps = {}) {
|
|
15490
|
-
const outcome = await uninstallService(deps);
|
|
15491
|
-
for (const line of outcome.lines) write(`${INDENT}${line}`);
|
|
15492
|
-
if (outcome.lines.length > 0) return;
|
|
15493
|
-
setRunsAsService(false);
|
|
15494
|
-
write(
|
|
15495
|
-
INDENT + tick("Removed the service. Cabane won't start at boot, and it isn't running now.")
|
|
15496
|
-
);
|
|
15497
|
-
write(`${INDENT} Start it again with ${cabane("start")}`);
|
|
15498
|
-
}
|
|
15499
|
-
async function serviceStatus(deps = {}) {
|
|
15500
|
-
const state = await serviceState(deps);
|
|
15501
|
-
for (const line of serviceRows(state)) write(line);
|
|
15502
|
-
for (const line of updateRows(readUpdateState(), safeConfig()?.update?.auto, {
|
|
15503
|
-
service: state.installed
|
|
15504
|
-
}))
|
|
15505
|
-
write(line);
|
|
15506
|
-
if (state.installed && state.state !== "running") process.exitCode = 1;
|
|
15507
|
-
}
|
|
15508
|
-
var LABEL_WIDTH = 13;
|
|
15509
|
-
var row = (label, value) => ` ${label.padEnd(LABEL_WIDTH)}${value}`;
|
|
15510
|
-
function serviceRows(state) {
|
|
15511
|
-
if (!state.installed) {
|
|
15512
|
-
return [
|
|
15513
|
-
row(
|
|
15514
|
-
"Service",
|
|
15515
|
-
state.platform ? `not installed \u2014 ${cabane("service install")} starts Cabane at boot` : "not available on this system"
|
|
15516
|
-
)
|
|
15517
|
-
];
|
|
15518
|
-
}
|
|
15519
|
-
const via = state.platform === "systemd" ? "systemd" : "launchd";
|
|
15520
|
-
const rows = [
|
|
15521
|
-
row(
|
|
15522
|
-
"Service",
|
|
15523
|
-
state.state === "running" ? `running (${via}${state.pid ? `, pid ${state.pid}` : ""})` : `installed, not running (${via}: ${state.state})`
|
|
15524
|
-
)
|
|
15525
|
-
];
|
|
15526
|
-
if (!state.nodeOk) {
|
|
15527
|
-
rows.push(
|
|
15528
|
-
` ! The service's Node at ${tildePath(state.node ?? "")} is gone. Run ${cabane("service install")} again.`
|
|
15529
|
-
);
|
|
15530
|
-
}
|
|
15531
|
-
return rows;
|
|
15532
|
-
}
|
|
15533
|
-
function updateRows(state, auto, opts = {}) {
|
|
15534
|
-
const rows = [];
|
|
15535
|
-
if (opts.version !== false) rows.push(row("Version", COMPANION_VERSION));
|
|
15536
|
-
if (state.server) {
|
|
15537
|
-
rows.push(
|
|
15538
|
-
row(
|
|
15539
|
-
"Cabane wants",
|
|
15540
|
-
`${state.server.latest} (the latest), ${state.server.minimum} or newer to run turns`
|
|
15541
|
-
)
|
|
15542
|
-
);
|
|
15543
|
-
}
|
|
15544
|
-
rows.push(
|
|
15545
|
-
row(
|
|
15546
|
-
"Auto-update",
|
|
15547
|
-
!opts.service ? `off \u2014 runs with the service: ${cabane("service install")}` : auto === true ? "on \u2014 updates install between turns when they can, and roll back if they fail" : `off \u2014 ${cabane("update --auto on")} turns it on`
|
|
15548
|
-
)
|
|
15549
|
-
);
|
|
15550
|
-
const last = state.lastResult;
|
|
15551
|
-
if (last) {
|
|
15552
|
-
const when = new Date(last.at).toLocaleDateString("en-GB", { day: "numeric", month: "short" });
|
|
15553
|
-
const why = last.reason ? `: ${last.reason}` : "";
|
|
15554
|
-
rows.push(
|
|
15555
|
-
row(
|
|
15556
|
-
"Last update",
|
|
15557
|
-
last.kind === "updated" ? `${last.from} \u2192 ${last.to} on ${when}` : last.kind === "rolled_back" ? `${last.to} rolled back to ${last.from} on ${when}${why}` : `${last.to} failed on ${when}${why}`
|
|
15558
|
-
)
|
|
15559
|
-
);
|
|
15560
|
-
}
|
|
15561
|
-
return rows;
|
|
15562
|
-
}
|
|
15563
|
-
function safeConfig() {
|
|
15564
|
-
try {
|
|
15565
|
-
return loadConfig();
|
|
15566
|
-
} catch {
|
|
15567
|
-
return null;
|
|
15568
|
-
}
|
|
15569
|
-
}
|
|
15570
|
-
function setRunsAsService(on) {
|
|
15571
|
-
const cfg = safeConfig();
|
|
15572
|
-
if (!cfg) return;
|
|
15573
|
-
const { serviceDeclined: _declined, ...rest } = cfg;
|
|
15574
|
-
saveConfig({ ...rest, update: { auto: on } });
|
|
15575
|
-
}
|
|
15576
|
-
|
|
15577
|
-
// src/commands/status.ts
|
|
15578
15624
|
var LABEL_WIDTH2 = 13;
|
|
15579
15625
|
function row2(label, value) {
|
|
15580
15626
|
return ` ${label.padEnd(LABEL_WIDTH2)}${value}`;
|
|
@@ -15618,15 +15664,19 @@ async function status(deps = {}) {
|
|
|
15618
15664
|
deviceLine({
|
|
15619
15665
|
...await readTerminalStatus(running?.socket),
|
|
15620
15666
|
running: !!running,
|
|
15621
|
-
devicesUrl:
|
|
15667
|
+
devicesUrl: devicesUrl(cfg.baseUrl)
|
|
15622
15668
|
})
|
|
15623
15669
|
)
|
|
15624
15670
|
);
|
|
15625
15671
|
for (const line of strays) out(line);
|
|
15626
|
-
|
|
15672
|
+
const container = containerWords(containerHere() === true, service2, {
|
|
15673
|
+
companion: running?.pid ?? null
|
|
15674
|
+
});
|
|
15675
|
+
for (const line of serviceRows(service2, { container })) out(line);
|
|
15627
15676
|
for (const line of updateRows(readUpdateState(), cfg.update?.auto, {
|
|
15628
15677
|
version: false,
|
|
15629
|
-
service: service2.installed
|
|
15678
|
+
service: service2.installed,
|
|
15679
|
+
container
|
|
15630
15680
|
}))
|
|
15631
15681
|
out(line);
|
|
15632
15682
|
for (const line of await agentLines2(cfg, detection)) out(line);
|
|
@@ -15760,7 +15810,7 @@ async function update(opts = {}) {
|
|
|
15760
15810
|
const auto = opts.auto === "on";
|
|
15761
15811
|
if (auto && !serviceInstalled()) {
|
|
15762
15812
|
write(
|
|
15763
|
-
`${INDENT}Cabane updates itself only when it runs as a service. Run ${cabane("service install")} to run it as one, with updates on.`
|
|
15813
|
+
containerHere() === true ? `${INDENT}Cabane doesn't update itself in a container. To update it, replace the container with a newer image.` : `${INDENT}Cabane updates itself only when it runs as a service. Run ${cabane("service install")} to run it as one, with updates on.`
|
|
15764
15814
|
);
|
|
15765
15815
|
process.exitCode = 1;
|
|
15766
15816
|
return;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cabane/companion",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.241",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "The Cabane Companion (headless): connect an agent on your machine to your Cabane workspace as a responder — drive work against your own codebase, files, and MCP servers without putting any of it in Cabane.",
|
|
6
6
|
"license": "UNLICENSED",
|