@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.
Files changed (3) hide show
  1. package/README.md +2 -2
  2. package/dist/cli.js +257 -220
  3. 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 cabaneNamed;
335
+ if (allowed === null) return false;
330
336
  let url;
331
337
  try {
332
- url = new URL(pairedOver);
338
+ url = new URL(raw);
333
339
  } catch {
334
- return cabaneNamed;
340
+ return false;
335
341
  }
336
- return url.protocol === "http:" && url.hostname.toLowerCase() === allowed ? pairedOver.replace(/\/+$/, "") : cabaneNamed;
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 (!input.service && input.servicePlatform) {
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 hostReport() {
11636
- let container;
11647
+ function containerHere() {
11637
11648
  try {
11638
- container = inContainer(readContainerSignals());
11649
+ return inContainer(readContainerSignals());
11639
11650
  } catch {
11640
- container = null;
11651
+ return null;
11641
11652
  }
11642
- return { user: osUser(), container };
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 = `${cfg.baseUrl.replace(/\/$/, "")}/settings/devices`;
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: `${cfg.baseUrl.replace(/\/$/, "")}/settings/devices`
15654
+ devicesUrl: devicesUrl(cfg.baseUrl)
15622
15655
  })
15623
15656
  )
15624
15657
  );
15625
15658
  for (const line of strays) out(line);
15626
- for (const line of serviceRows(service2)) out(line);
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.239",
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",