@cabane/companion 0.6.239 → 0.6.240
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 +257 -220
- 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
|
|
@@ -14832,6 +14845,217 @@ function defaultSpawnDetached(args) {
|
|
|
14832
14845
|
}
|
|
14833
14846
|
}
|
|
14834
14847
|
|
|
14848
|
+
// src/commands/service.ts
|
|
14849
|
+
init_cabane_command();
|
|
14850
|
+
init_config();
|
|
14851
|
+
init_term();
|
|
14852
|
+
|
|
14853
|
+
// src/commands/stop.ts
|
|
14854
|
+
var TERM_GRACE_MS = 6e3;
|
|
14855
|
+
var POLL_INTERVAL_MS = 150;
|
|
14856
|
+
async function stop(deps = {}) {
|
|
14857
|
+
if ((deps.serviceInstalled ?? serviceInstalled)()) {
|
|
14858
|
+
const outcome = await (deps.stopService ?? stopService)();
|
|
14859
|
+
for (const line of outcome.lines) process.stdout.write(`${line}
|
|
14860
|
+
`);
|
|
14861
|
+
if (!outcome.ok) {
|
|
14862
|
+
process.exitCode = 1;
|
|
14863
|
+
return;
|
|
14864
|
+
}
|
|
14865
|
+
process.stdout.write(
|
|
14866
|
+
"companion: stopped the service. It starts again at boot, or with `cabane start`.\n"
|
|
14867
|
+
);
|
|
14868
|
+
return;
|
|
14869
|
+
}
|
|
14870
|
+
const readState = deps.readState ?? readLiveRuntimeState;
|
|
14871
|
+
const verify = deps.verify ?? ((s) => verifyRuntime(s));
|
|
14872
|
+
const kill = deps.kill ?? ((pid2, signal) => process.kill(pid2, signal));
|
|
14873
|
+
const sleep4 = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
14874
|
+
const now = deps.now ?? (() => Date.now());
|
|
14875
|
+
const state = readState();
|
|
14876
|
+
if (!state) {
|
|
14877
|
+
process.stdout.write("companion: not running (nothing to stop).\n");
|
|
14878
|
+
return;
|
|
14879
|
+
}
|
|
14880
|
+
if (await verify(state) === "stale") {
|
|
14881
|
+
clearRuntimeState();
|
|
14882
|
+
process.stdout.write("companion: not running (stale marker swept).\n");
|
|
14883
|
+
return;
|
|
14884
|
+
}
|
|
14885
|
+
const { pid } = state;
|
|
14886
|
+
process.stdout.write(`companion: stopping (pid ${pid})\u2026
|
|
14887
|
+
`);
|
|
14888
|
+
try {
|
|
14889
|
+
kill(pid, "SIGTERM");
|
|
14890
|
+
} catch {
|
|
14891
|
+
clearRuntimeState();
|
|
14892
|
+
process.stdout.write("companion: already stopped.\n");
|
|
14893
|
+
return;
|
|
14894
|
+
}
|
|
14895
|
+
const deadline = now() + TERM_GRACE_MS;
|
|
14896
|
+
while (now() < deadline) {
|
|
14897
|
+
if (!isAlive(kill, pid)) {
|
|
14898
|
+
process.stdout.write("companion: stopped.\n");
|
|
14899
|
+
return;
|
|
14900
|
+
}
|
|
14901
|
+
await sleep4(POLL_INTERVAL_MS);
|
|
14902
|
+
}
|
|
14903
|
+
process.stdout.write(
|
|
14904
|
+
`companion: didn't exit within ${TERM_GRACE_MS / 1e3}s, sending SIGKILL.
|
|
14905
|
+
`
|
|
14906
|
+
);
|
|
14907
|
+
try {
|
|
14908
|
+
kill(pid, "SIGKILL");
|
|
14909
|
+
} catch {
|
|
14910
|
+
}
|
|
14911
|
+
clearRuntimeState();
|
|
14912
|
+
process.stdout.write("companion: force-stopped.\n");
|
|
14913
|
+
}
|
|
14914
|
+
function isAlive(kill, pid) {
|
|
14915
|
+
try {
|
|
14916
|
+
kill(pid, 0);
|
|
14917
|
+
return true;
|
|
14918
|
+
} catch {
|
|
14919
|
+
return false;
|
|
14920
|
+
}
|
|
14921
|
+
}
|
|
14922
|
+
|
|
14923
|
+
// src/commands/service.ts
|
|
14924
|
+
var STARTUP_WAIT_MS = 2e4;
|
|
14925
|
+
async function installAndStartService(deps = {}) {
|
|
14926
|
+
const readState = deps.readState ?? readLiveRuntimeState;
|
|
14927
|
+
const sleep4 = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
14928
|
+
const now = deps.now ?? Date.now;
|
|
14929
|
+
const before = await serviceState(deps);
|
|
14930
|
+
const running = readState();
|
|
14931
|
+
if (running && running.pid !== before.pid) await (deps.stopRunning ?? (() => stop()))();
|
|
14932
|
+
setRunsAsService(true);
|
|
14933
|
+
const outcome = await installService(deps);
|
|
14934
|
+
for (const line of outcome.lines) write(`${INDENT}${line}`);
|
|
14935
|
+
if (!outcome.ok) {
|
|
14936
|
+
setRunsAsService(false);
|
|
14937
|
+
return false;
|
|
14938
|
+
}
|
|
14939
|
+
const deadline = now() + STARTUP_WAIT_MS;
|
|
14940
|
+
while (now() < deadline) {
|
|
14941
|
+
const state = readState();
|
|
14942
|
+
if (state?.socket) return true;
|
|
14943
|
+
await sleep4(250);
|
|
14944
|
+
}
|
|
14945
|
+
write(
|
|
14946
|
+
`${INDENT}The service is installed but Cabane hasn't come up yet. ${cabane("service status")} and ${cabane("logs")} say why.`
|
|
14947
|
+
);
|
|
14948
|
+
return false;
|
|
14949
|
+
}
|
|
14950
|
+
async function serviceInstall(deps = {}) {
|
|
14951
|
+
if (!await installAndStartService(deps)) {
|
|
14952
|
+
process.exitCode = 1;
|
|
14953
|
+
return;
|
|
14954
|
+
}
|
|
14955
|
+
write(
|
|
14956
|
+
INDENT + tick(
|
|
14957
|
+
"Cabane runs as a service. It starts at boot, restarts if it stops, and updates itself, usually between turns."
|
|
14958
|
+
)
|
|
14959
|
+
);
|
|
14960
|
+
write(`${INDENT} Status: ${cabane("service status")} Logs: ${cabane("logs")}`);
|
|
14961
|
+
}
|
|
14962
|
+
async function serviceUninstall(deps = {}) {
|
|
14963
|
+
const outcome = await uninstallService(deps);
|
|
14964
|
+
for (const line of outcome.lines) write(`${INDENT}${line}`);
|
|
14965
|
+
if (outcome.lines.length > 0) return;
|
|
14966
|
+
setRunsAsService(false);
|
|
14967
|
+
write(
|
|
14968
|
+
INDENT + tick("Removed the service. Cabane won't start at boot, and it isn't running now.")
|
|
14969
|
+
);
|
|
14970
|
+
write(`${INDENT} Start it again with ${cabane("start")}`);
|
|
14971
|
+
}
|
|
14972
|
+
async function serviceStatus(deps = {}) {
|
|
14973
|
+
const state = await serviceState(deps);
|
|
14974
|
+
const container = containerWords(containerHere() === true, state);
|
|
14975
|
+
for (const line of serviceRows(state, { container })) write(line);
|
|
14976
|
+
for (const line of updateRows(readUpdateState(), safeConfig()?.update?.auto, {
|
|
14977
|
+
service: state.installed,
|
|
14978
|
+
container
|
|
14979
|
+
}))
|
|
14980
|
+
write(line);
|
|
14981
|
+
if (state.installed && state.state !== "running") process.exitCode = 1;
|
|
14982
|
+
}
|
|
14983
|
+
var LABEL_WIDTH = 13;
|
|
14984
|
+
var row = (label, value) => ` ${label.padEnd(LABEL_WIDTH)}${value}`;
|
|
14985
|
+
function containerWords(container, state, opts = {}) {
|
|
14986
|
+
if (!container || opts.supervisedHere) return false;
|
|
14987
|
+
const serviceRuns = state.installed && state.state === "running";
|
|
14988
|
+
if (!serviceRuns) return true;
|
|
14989
|
+
return opts.companion !== void 0 && state.pid !== opts.companion;
|
|
14990
|
+
}
|
|
14991
|
+
function serviceRows(state, opts = {}) {
|
|
14992
|
+
if (opts.container) return [row("Service", "not used in a container")];
|
|
14993
|
+
if (!state.installed) {
|
|
14994
|
+
return [
|
|
14995
|
+
row(
|
|
14996
|
+
"Service",
|
|
14997
|
+
state.platform ? `not installed \u2014 ${cabane("service install")} starts Cabane at boot` : "not available on this system"
|
|
14998
|
+
)
|
|
14999
|
+
];
|
|
15000
|
+
}
|
|
15001
|
+
const via = state.platform === "systemd" ? "systemd" : "launchd";
|
|
15002
|
+
const rows = [
|
|
15003
|
+
row(
|
|
15004
|
+
"Service",
|
|
15005
|
+
state.state === "running" ? `running (${via}${state.pid ? `, pid ${state.pid}` : ""})` : `installed, not running (${via}: ${state.state})`
|
|
15006
|
+
)
|
|
15007
|
+
];
|
|
15008
|
+
if (!state.nodeOk) {
|
|
15009
|
+
rows.push(
|
|
15010
|
+
` ! The service's Node at ${tildePath(state.node ?? "")} is gone. Run ${cabane("service install")} again.`
|
|
15011
|
+
);
|
|
15012
|
+
}
|
|
15013
|
+
return rows;
|
|
15014
|
+
}
|
|
15015
|
+
function updateRows(state, auto, opts = {}) {
|
|
15016
|
+
const rows = [];
|
|
15017
|
+
if (opts.version !== false) rows.push(row("Version", COMPANION_VERSION));
|
|
15018
|
+
if (state.server) {
|
|
15019
|
+
rows.push(
|
|
15020
|
+
row(
|
|
15021
|
+
"Cabane wants",
|
|
15022
|
+
`${state.server.latest} (the latest), ${state.server.minimum} or newer to run turns`
|
|
15023
|
+
)
|
|
15024
|
+
);
|
|
15025
|
+
}
|
|
15026
|
+
rows.push(
|
|
15027
|
+
row(
|
|
15028
|
+
"Auto-update",
|
|
15029
|
+
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`
|
|
15030
|
+
)
|
|
15031
|
+
);
|
|
15032
|
+
const last = state.lastResult;
|
|
15033
|
+
if (last) {
|
|
15034
|
+
const when = new Date(last.at).toLocaleDateString("en-GB", { day: "numeric", month: "short" });
|
|
15035
|
+
const why = last.reason ? `: ${last.reason}` : "";
|
|
15036
|
+
rows.push(
|
|
15037
|
+
row(
|
|
15038
|
+
"Last update",
|
|
15039
|
+
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}`
|
|
15040
|
+
)
|
|
15041
|
+
);
|
|
15042
|
+
}
|
|
15043
|
+
return rows;
|
|
15044
|
+
}
|
|
15045
|
+
function safeConfig() {
|
|
15046
|
+
try {
|
|
15047
|
+
return loadConfig();
|
|
15048
|
+
} catch {
|
|
15049
|
+
return null;
|
|
15050
|
+
}
|
|
15051
|
+
}
|
|
15052
|
+
function setRunsAsService(on) {
|
|
15053
|
+
const cfg = safeConfig();
|
|
15054
|
+
if (!cfg) return;
|
|
15055
|
+
const { serviceDeclined: _declined, ...rest } = cfg;
|
|
15056
|
+
saveConfig({ ...rest, update: { auto: on } });
|
|
15057
|
+
}
|
|
15058
|
+
|
|
14835
15059
|
// src/terminal-status.ts
|
|
14836
15060
|
async function readTerminalStatus(socket, timeoutMs = 1e3) {
|
|
14837
15061
|
if (!socket) return emptyHeartbeatStatus();
|
|
@@ -14857,6 +15081,14 @@ async function waitForAcknowledgement(socket) {
|
|
|
14857
15081
|
}
|
|
14858
15082
|
|
|
14859
15083
|
// src/commands/start.ts
|
|
15084
|
+
var NO_SERVICE = {
|
|
15085
|
+
installed: false,
|
|
15086
|
+
platform: null,
|
|
15087
|
+
state: "not installed",
|
|
15088
|
+
pid: null,
|
|
15089
|
+
node: null,
|
|
15090
|
+
nodeOk: true
|
|
15091
|
+
};
|
|
14860
15092
|
async function start(opts = {}, deps = {}) {
|
|
14861
15093
|
setLogOverrides(opts);
|
|
14862
15094
|
setConsoleLogging(false);
|
|
@@ -14970,7 +15202,11 @@ async function startScript(opts, deps) {
|
|
|
14970
15202
|
if (service2.state !== "running" || service2.pid !== current.pid) mode = "existing";
|
|
14971
15203
|
}
|
|
14972
15204
|
}
|
|
14973
|
-
const address =
|
|
15205
|
+
const address = devicesUrl(cfg.baseUrl);
|
|
15206
|
+
const container = containerHere() === true && containerWords(true, installed ? await serviceState() : NO_SERVICE, {
|
|
15207
|
+
companion: runtime ? process.pid : running?.pid ?? null,
|
|
15208
|
+
supervisedHere: runtime !== null && process.env.CABANE_COMPANION_SUPERVISED === "1"
|
|
15209
|
+
});
|
|
14974
15210
|
if (!daemon)
|
|
14975
15211
|
for (const line of closingBlock({
|
|
14976
15212
|
mode,
|
|
@@ -14980,6 +15216,7 @@ async function startScript(opts, deps) {
|
|
|
14980
15216
|
service: installed,
|
|
14981
15217
|
servicePlatform: servicePlatform(),
|
|
14982
15218
|
autoUpdate: cfg.update?.auto !== false,
|
|
15219
|
+
container,
|
|
14983
15220
|
address
|
|
14984
15221
|
}))
|
|
14985
15222
|
write(line);
|
|
@@ -15371,210 +15608,6 @@ init_harness_copy();
|
|
|
15371
15608
|
init_secrets();
|
|
15372
15609
|
init_term();
|
|
15373
15610
|
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
15611
|
var LABEL_WIDTH2 = 13;
|
|
15579
15612
|
function row2(label, value) {
|
|
15580
15613
|
return ` ${label.padEnd(LABEL_WIDTH2)}${value}`;
|
|
@@ -15618,15 +15651,19 @@ async function status(deps = {}) {
|
|
|
15618
15651
|
deviceLine({
|
|
15619
15652
|
...await readTerminalStatus(running?.socket),
|
|
15620
15653
|
running: !!running,
|
|
15621
|
-
devicesUrl:
|
|
15654
|
+
devicesUrl: devicesUrl(cfg.baseUrl)
|
|
15622
15655
|
})
|
|
15623
15656
|
)
|
|
15624
15657
|
);
|
|
15625
15658
|
for (const line of strays) out(line);
|
|
15626
|
-
|
|
15659
|
+
const container = containerWords(containerHere() === true, service2, {
|
|
15660
|
+
companion: running?.pid ?? null
|
|
15661
|
+
});
|
|
15662
|
+
for (const line of serviceRows(service2, { container })) out(line);
|
|
15627
15663
|
for (const line of updateRows(readUpdateState(), cfg.update?.auto, {
|
|
15628
15664
|
version: false,
|
|
15629
|
-
service: service2.installed
|
|
15665
|
+
service: service2.installed,
|
|
15666
|
+
container
|
|
15630
15667
|
}))
|
|
15631
15668
|
out(line);
|
|
15632
15669
|
for (const line of await agentLines2(cfg, detection)) out(line);
|
|
@@ -15760,7 +15797,7 @@ async function update(opts = {}) {
|
|
|
15760
15797
|
const auto = opts.auto === "on";
|
|
15761
15798
|
if (auto && !serviceInstalled()) {
|
|
15762
15799
|
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.`
|
|
15800
|
+
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
15801
|
);
|
|
15765
15802
|
process.exitCode = 1;
|
|
15766
15803
|
return;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cabane/companion",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.240",
|
|
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",
|