@cabane/cli 0.1.4 → 0.1.6

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 +74 -87
  2. package/dist/cli.js +295 -730
  3. package/package.json +3 -3
package/dist/cli.js CHANGED
@@ -3,38 +3,13 @@
3
3
  // src/cli.ts
4
4
  import { Command } from "commander";
5
5
 
6
- // src/commands/bridge.ts
7
- import { existsSync as existsSync2, readFileSync as readFileSync2, rmSync } from "fs";
8
- import { join as join2 } from "path";
6
+ // src/commands/doctor.ts
7
+ import { writeSync } from "fs";
8
+ import { homedir as homedir2 } from "os";
9
9
 
10
- // src/log.ts
11
- var BOLD = "\x1B[1m";
12
- var RED = "\x1B[31m";
13
- var GREEN = "\x1B[32m";
14
- var YELLOW = "\x1B[33m";
15
- var RESET = "\x1B[0m";
16
- var useColor = () => process.stderr.isTTY === true && !process.env.NO_COLOR;
17
- var paint = (code, s) => useColor() ? `${code}${s}${RESET}` : s;
18
- function step(msg) {
19
- process.stderr.write(`${paint(BOLD, "\u203A")} ${msg}
20
- `);
21
- }
22
- function info(msg) {
23
- process.stderr.write(` ${msg}
24
- `);
25
- }
26
- function ok(msg) {
27
- process.stderr.write(`${paint(GREEN, "\u2713")} ${msg}
28
- `);
29
- }
30
- function warn(msg) {
31
- process.stderr.write(`${paint(YELLOW, "\u26A0")} ${msg}
32
- `);
33
- }
34
- function error(msg) {
35
- process.stderr.write(`${paint(RED, "\u2717")} ${msg}
36
- `);
37
- }
10
+ // src/config.ts
11
+ import { mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
12
+ import { dirname } from "path";
38
13
 
39
14
  // src/paths.ts
40
15
  import { existsSync, readFileSync } from "fs";
@@ -52,7 +27,7 @@ function classifyHome(dir) {
52
27
  }
53
28
  if (cfg) {
54
29
  if (typeof cfg.distBase === "string" || typeof cfg.pgPort === "number") return "selfhost";
55
- if (typeof cfg.baseUrl === "string" || typeof cfg.deviceToken === "string") return "bridge";
30
+ if (typeof cfg.baseUrl === "string" || typeof cfg.deviceToken === "string") return "companion";
56
31
  }
57
32
  return "empty";
58
33
  }
@@ -75,19 +50,17 @@ function resolvePaths(root = cabaneHome(), dataDir) {
75
50
  dataDir: data,
76
51
  pgDataDir: join(data, "pg"),
77
52
  storageRoot: join(data, "storage"),
78
- bridgeHome: join(root, "bridge"),
79
53
  logsDir: join(root, "logs"),
80
54
  serverLog: join(root, "logs", "server.log"),
81
- bridgeLog: join(root, "logs", "bridge.log"),
82
55
  runDir: join(root, "run"),
83
56
  supervisorPidFile: join(root, "run", "supervisor.pid"),
84
57
  stateFile: join(root, "run", "state.json")
85
58
  };
86
59
  }
87
- function assertNotForeignBridge(root = cabaneHome()) {
88
- if (classifyHome(root) !== "bridge") return;
60
+ function assertNotForeignCompanion(root = cabaneHome()) {
61
+ if (classifyHome(root) !== "companion") return;
89
62
  throw new Error(
90
- `${root} holds a standalone @cabane/bridge config (a device pairing), not a Cabane self-host install \u2014 refusing to write here and overwrite it.
63
+ `${root} holds a standalone @cabane/companion config (a device pairing), not a Cabane self-host install \u2014 refusing to write here and overwrite it.
91
64
  Self-host defaults to ~/.cabane-selfhost; unset CABANE_HOME, or point it at a different directory.`
92
65
  );
93
66
  }
@@ -95,6 +68,49 @@ function appDirFor(paths, version) {
95
68
  return join(paths.appsDir, version);
96
69
  }
97
70
 
71
+ // src/config.ts
72
+ var DEFAULT_DIST_BASE = "https://dist.cabane.ai";
73
+ var DEFAULT_PORT = 3e3;
74
+ var DEFAULT_PG_PORT = 55432;
75
+ function defaultConfig() {
76
+ return {
77
+ distBase: DEFAULT_DIST_BASE,
78
+ port: DEFAULT_PORT,
79
+ pgPort: DEFAULT_PG_PORT
80
+ };
81
+ }
82
+ function loadConfig(root) {
83
+ const file = resolvePaths(root).configFile;
84
+ try {
85
+ const parsed = JSON.parse(readFileSync2(file, "utf8"));
86
+ return { ...defaultConfig(), ...parsed };
87
+ } catch {
88
+ return defaultConfig();
89
+ }
90
+ }
91
+ function saveConfig(config, root) {
92
+ const file = resolvePaths(root).configFile;
93
+ mkdirSync(dirname(file), { recursive: true });
94
+ writeFileSync(file, JSON.stringify(config, null, 2) + "\n", { mode: 384 });
95
+ }
96
+ function updateConfig(mutate, root) {
97
+ const config = loadConfig(root);
98
+ mutate(config);
99
+ saveConfig(config, root);
100
+ return config;
101
+ }
102
+ function pathsFor(config, root) {
103
+ return resolvePaths(root, config.dataDir);
104
+ }
105
+
106
+ // src/doctor/checks.ts
107
+ import { createRequire } from "module";
108
+ import { existsSync as existsSync2, readFileSync as readFileSync3 } from "fs";
109
+ import { mkdtempSync, rmSync, writeFileSync as writeFileSync2 } from "fs";
110
+ import { statfs } from "fs/promises";
111
+ import { dirname as dirname2, join as join2 } from "path";
112
+ import { pathToFileURL } from "url";
113
+
98
114
  // src/proc.ts
99
115
  import { spawn } from "child_process";
100
116
  function run(cmd, args, opts = {}) {
@@ -145,92 +161,6 @@ async function portInUse(port) {
145
161
  });
146
162
  }
147
163
 
148
- // src/commands/bridge.ts
149
- var STATE_DIRS = ["cursors", "dispatched", "completed", "resume-attempts"];
150
- async function bridgeReset(opts = {}) {
151
- const paths = resolvePaths();
152
- const bridgeCabane = join2(paths.bridgeHome, ".cabane");
153
- const running = bridgeRunning(paths.stateFile);
154
- if (running && !opts.force) {
155
- warn("the bridge is running \u2014 a reset only takes effect on the next boot.");
156
- info("stop it first (`cabane down`), or re-run with `--force` to clear anyway.");
157
- return;
158
- }
159
- let cleared = 0;
160
- for (const dir of STATE_DIRS) {
161
- const target = join2(bridgeCabane, dir);
162
- if (existsSync2(target)) {
163
- rmSync(target, { recursive: true, force: true });
164
- cleared += 1;
165
- }
166
- }
167
- if (cleared === 0) {
168
- ok("bridge resume state already clean \u2014 nothing to reset.");
169
- } else {
170
- ok(`cleared bridge resume state (${cleared} of ${STATE_DIRS.length} stores).`);
171
- info(
172
- "the bridge will start fresh (no interrupted turn to replay) on the next `cabane up`."
173
- );
174
- }
175
- }
176
- function bridgeRunning(stateFile) {
177
- try {
178
- const state = JSON.parse(readFileSync2(stateFile, "utf8"));
179
- return state.bridgePid ? pidAlive(state.bridgePid) : false;
180
- } catch {
181
- return false;
182
- }
183
- }
184
-
185
- // src/commands/doctor.ts
186
- import { writeSync } from "fs";
187
- import { homedir as homedir2 } from "os";
188
-
189
- // src/config.ts
190
- import { mkdirSync, readFileSync as readFileSync3, writeFileSync } from "fs";
191
- import { dirname } from "path";
192
- var DEFAULT_DIST_BASE = "https://dist.cabane.ai";
193
- var DEFAULT_PORT = 3e3;
194
- var DEFAULT_PG_PORT = 55432;
195
- function defaultConfig() {
196
- return {
197
- distBase: DEFAULT_DIST_BASE,
198
- port: DEFAULT_PORT,
199
- pgPort: DEFAULT_PG_PORT
200
- };
201
- }
202
- function loadConfig(root) {
203
- const file = resolvePaths(root).configFile;
204
- try {
205
- const parsed = JSON.parse(readFileSync3(file, "utf8"));
206
- return { ...defaultConfig(), ...parsed };
207
- } catch {
208
- return defaultConfig();
209
- }
210
- }
211
- function saveConfig(config, root) {
212
- const file = resolvePaths(root).configFile;
213
- mkdirSync(dirname(file), { recursive: true });
214
- writeFileSync(file, JSON.stringify(config, null, 2) + "\n", { mode: 384 });
215
- }
216
- function updateConfig(mutate, root) {
217
- const config = loadConfig(root);
218
- mutate(config);
219
- saveConfig(config, root);
220
- return config;
221
- }
222
- function pathsFor(config, root) {
223
- return resolvePaths(root, config.dataDir);
224
- }
225
-
226
- // src/doctor/checks.ts
227
- import { createRequire } from "module";
228
- import { existsSync as existsSync3, readFileSync as readFileSync4 } from "fs";
229
- import { mkdtempSync, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
230
- import { statfs } from "fs/promises";
231
- import { dirname as dirname2, join as join3 } from "path";
232
- import { pathToFileURL } from "url";
233
-
234
164
  // src/doctor/types.ts
235
165
  var pass = (id, name, detail) => ({
236
166
  id,
@@ -238,7 +168,7 @@ var pass = (id, name, detail) => ({
238
168
  status: "pass",
239
169
  detail
240
170
  });
241
- var warn2 = (id, name, detail, fix) => ({
171
+ var warn = (id, name, detail, fix) => ({
242
172
  id,
243
173
  name,
244
174
  status: "warn",
@@ -269,7 +199,7 @@ function evaluateNode(current, floor) {
269
199
  const cur = parseMajor(current);
270
200
  const min = parseMajor(floor);
271
201
  if (Number.isNaN(cur) || Number.isNaN(min)) {
272
- return warn2("node", "Node", `could not compare v${current} against floor "${floor}"`);
202
+ return warn("node", "Node", `could not compare v${current} against floor "${floor}"`);
273
203
  }
274
204
  if (cur >= min) return pass("node", "Node", `v${current} (\u2265 ${min} required)`);
275
205
  return fail(
@@ -299,7 +229,7 @@ async function checkPort(ctx) {
299
229
  }
300
230
  function ownSupervisorPid(paths) {
301
231
  try {
302
- const pid = Number(readFileSync4(paths.supervisorPidFile, "utf8").trim());
232
+ const pid = Number(readFileSync3(paths.supervisorPidFile, "utf8").trim());
303
233
  return pid > 0 && pidAlive(pid) ? pid : null;
304
234
  } catch {
305
235
  return null;
@@ -319,7 +249,7 @@ function evaluateDisk(freeBytes, where) {
319
249
  );
320
250
  }
321
251
  if (freeBytes < DISK_WARN_BYTES) {
322
- return warn2(
252
+ return warn(
323
253
  "disk",
324
254
  "Disk",
325
255
  `${gb} GB free on ${where} \u2014 low`,
@@ -336,7 +266,7 @@ async function checkDisk(ctx) {
336
266
  }
337
267
  function checkStorage(ctx) {
338
268
  const root = ctx.paths.storageRoot;
339
- const target = existsSync3(root) ? root : nearestExisting(root);
269
+ const target = existsSync2(root) ? root : nearestExisting(root);
340
270
  if (!isWritableDir(target)) {
341
271
  return fail(
342
272
  "storage",
@@ -345,7 +275,7 @@ function checkStorage(ctx) {
345
275
  `Make ${target} writable, or point Cabane elsewhere with \`CABANE_HOME=<dir>\`.`
346
276
  );
347
277
  }
348
- const note = existsSync3(root) ? "" : " (will be created on first `cabane up`)";
278
+ const note = existsSync2(root) ? "" : " (will be created on first `cabane up`)";
349
279
  return pass("storage", "Storage root", `${root} \u2014 writable${note}`);
350
280
  }
351
281
  async function resolvePgBinaries(platform, arch) {
@@ -394,7 +324,7 @@ async function checkPostgres(ctx) {
394
324
  );
395
325
  }
396
326
  const dataDir = ctx.paths.pgDataDir;
397
- if (!existsSync3(join3(dataDir, "PG_VERSION"))) {
327
+ if (!existsSync2(join2(dataDir, "PG_VERSION"))) {
398
328
  return pass(
399
329
  "postgres",
400
330
  "Embedded Postgres",
@@ -403,7 +333,7 @@ async function checkPostgres(ctx) {
403
333
  }
404
334
  const stale = stalePostmaster(dataDir);
405
335
  if (stale) {
406
- return warn2(
336
+ return warn(
407
337
  "postgres",
408
338
  "Embedded Postgres",
409
339
  `data dir initialised but holds a stale postmaster.pid (pid ${stale}, not running) \u2014 ${dataDir}`,
@@ -415,7 +345,7 @@ async function checkPostgres(ctx) {
415
345
  function stalePostmaster(dataDir) {
416
346
  try {
417
347
  const pid = Number(
418
- readFileSync4(join3(dataDir, "postmaster.pid"), "utf8").split("\n")[0]?.trim()
348
+ readFileSync3(join2(dataDir, "postmaster.pid"), "utf8").split("\n")[0]?.trim()
419
349
  );
420
350
  return pid > 0 && !pidAlive(pid) ? pid : null;
421
351
  } catch {
@@ -445,154 +375,12 @@ async function hasQuarantine(file) {
445
375
  return false;
446
376
  }
447
377
  }
448
- var CREDENTIAL_SOURCE = {
449
- subscription: "Claude subscription login (~/.claude)",
450
- "env-api-key": "ANTHROPIC_API_KEY (env or `cabane set-env`)",
451
- "env-auth-token": "ANTHROPIC_AUTH_TOKEN (env or `cabane set-env`)",
452
- "settings-api-key": "ANTHROPIC_API_KEY in Claude settings.json"
453
- };
454
- function evaluateExecutor(p) {
455
- const name = "Executor (Claude Code)";
456
- if (!p.binaryFound) {
457
- return fail(
458
- "executor",
459
- name,
460
- `\`${p.binaryName}\` is not on PATH \u2014 the house bridge has no executor to run`,
461
- "Install Claude Code (or your chosen executor) and make sure it is on PATH."
462
- );
463
- }
464
- const apiKeyOnly = p.credential === "env-api-key" || p.credential === "env-auth-token" || p.credential === "settings-api-key";
465
- if (p.forceLoginMethod === "claudeai" && apiKeyOnly) {
466
- return fail(
467
- "executor",
468
- name,
469
- "a Claude Code managed policy forces subscription login (`forceLoginMethod: claudeai`), which refuses your API key",
470
- "Log in with the required method (`claude` \u2192 /login), or remove the managed-settings force-login policy."
471
- );
472
- }
473
- if (!p.credential) {
474
- return fail(
475
- "executor",
476
- name,
477
- "no executor credential is reachable by the house bridge",
478
- "Log in to Claude Code, or persist a key with `cabane set-env ANTHROPIC_API_KEY=\u2026`, or export it before `cabane up`."
479
- );
480
- }
481
- return pass(
482
- "executor",
483
- name,
484
- `\`${p.binaryName}\` present; credential reachable via ${CREDENTIAL_SOURCE[p.credential]} (reachability only \u2014 not verified against the provider)`
485
- );
486
- }
487
- function checkExecutor(ctx) {
488
- return evaluateExecutor(probeExecutor(ctx));
489
- }
490
- function probeExecutor(ctx) {
491
- const { env, homeDir } = ctx.sys;
492
- const cred = (key) => {
493
- const v = env[key] ?? ctx.config.bridgeEnv?.[key];
494
- return typeof v === "string" && v.length > 0;
495
- };
496
- const configDir = env.CLAUDE_CONFIG_DIR ?? join3(homeDir, ".claude");
497
- let credential = null;
498
- if (cred("ANTHROPIC_API_KEY")) credential = "env-api-key";
499
- else if (cred("ANTHROPIC_AUTH_TOKEN")) credential = "env-auth-token";
500
- else if (existsSync3(join3(configDir, ".credentials.json"))) credential = "subscription";
501
- else if (settingsHasApiKey(join3(configDir, "settings.json"))) credential = "settings-api-key";
502
- return {
503
- binaryName: "claude",
504
- binaryFound: onPath("claude", env),
505
- forceLoginMethod: readForceLoginMethod(ctx.sys),
506
- credential
507
- };
508
- }
509
- function settingsHasApiKey(file) {
510
- try {
511
- const s = JSON.parse(readFileSync4(file, "utf8"));
512
- return Boolean(s.env?.ANTHROPIC_API_KEY || s.apiKeyHelper);
513
- } catch {
514
- return false;
515
- }
516
- }
517
- function managedSettingsPath(sys) {
518
- switch (sys.platform) {
519
- case "darwin":
520
- return "/Library/Application Support/ClaudeCode/managed-settings.json";
521
- case "linux":
522
- return "/etc/claude-code/managed-settings.json";
523
- case "win32":
524
- return join3(sys.env.PROGRAMDATA ?? "C:\\ProgramData", "ClaudeCode", "managed-settings.json");
525
- default:
526
- return null;
527
- }
528
- }
529
- function readForceLoginMethod(sys) {
530
- const path = managedSettingsPath(sys);
531
- if (!path) return null;
532
- try {
533
- const s = JSON.parse(readFileSync4(path, "utf8"));
534
- return s.forceLoginMethod ?? null;
535
- } catch {
536
- return null;
537
- }
538
- }
539
- function onPath(bin, env) {
540
- const dirs = (env.PATH ?? "").split(process.platform === "win32" ? ";" : ":");
541
- const names = process.platform === "win32" ? [bin, `${bin}.exe`, `${bin}.cmd`] : [bin];
542
- for (const dir of dirs) {
543
- if (!dir) continue;
544
- for (const name of names) if (existsSync3(join3(dir, name))) return true;
545
- }
546
- return false;
547
- }
548
- function evaluateVersions(input) {
549
- const name = "Bridge/server version";
550
- const { serverVersion, bridgeVersion } = input;
551
- if (!serverVersion) {
552
- return skip("versions", name, "no server artifact installed yet \u2014 run `cabane install`");
553
- }
554
- if (!bridgeVersion) {
555
- return warn2(
556
- "versions",
557
- name,
558
- `the installed artifact ships no bundled bridge to check against (server ${serverVersion})`,
559
- "Reinstall the server artifact \u2014 a current build bundles its own bridge (`cabane install`)."
560
- );
561
- }
562
- if (bridgeVersion === serverVersion) {
563
- return pass("versions", name, `bundled bridge and server both ${serverVersion} (lockstep)`);
564
- }
565
- return fail(
566
- "versions",
567
- name,
568
- `bundled bridge ${bridgeVersion} \u2260 server ${serverVersion} \u2014 a version skew (they must ship from one build)`,
569
- "Reinstall a single matching artifact with `cabane install` so the bundled bridge and server are the same version."
570
- );
571
- }
572
- function checkVersions(ctx) {
573
- return evaluateVersions({
574
- serverVersion: ctx.config.currentVersion ?? null,
575
- bridgeVersion: readBundledBridgeVersion(ctx)
576
- });
577
- }
578
- function readBundledBridgeVersion(ctx) {
579
- const version = ctx.config.currentVersion;
580
- if (!version) return null;
581
- try {
582
- const pkg = JSON.parse(
583
- readFileSync4(join3(appDirFor(ctx.paths, version), "bridge", "package.json"), "utf8")
584
- );
585
- return pkg.version ?? null;
586
- } catch {
587
- return null;
588
- }
589
- }
590
378
  function installedNodeFloor(ctx) {
591
379
  const version = ctx.config.currentVersion;
592
380
  if (!version) return null;
593
381
  try {
594
382
  const manifest = JSON.parse(
595
- readFileSync4(join3(appDirFor(ctx.paths, version), "manifest.json"), "utf8")
383
+ readFileSync3(join2(appDirFor(ctx.paths, version), "manifest.json"), "utf8")
596
384
  );
597
385
  return manifest.nodeFloor ?? null;
598
386
  } catch {
@@ -601,7 +389,7 @@ function installedNodeFloor(ctx) {
601
389
  }
602
390
  function nearestExisting(path) {
603
391
  let cur = path;
604
- while (!existsSync3(cur)) {
392
+ while (!existsSync2(cur)) {
605
393
  const parent = dirname2(cur);
606
394
  if (parent === cur) break;
607
395
  cur = parent;
@@ -610,9 +398,9 @@ function nearestExisting(path) {
610
398
  }
611
399
  function isWritableDir(dir) {
612
400
  try {
613
- const probe = mkdtempSync(join3(dir, ".cabane-doctor-"));
614
- writeFileSync2(join3(probe, "w"), "");
615
- rmSync2(probe, { recursive: true, force: true });
401
+ const probe = mkdtempSync(join2(dir, ".cabane-doctor-"));
402
+ writeFileSync2(join2(probe, "w"), "");
403
+ rmSync(probe, { recursive: true, force: true });
616
404
  return true;
617
405
  } catch {
618
406
  return false;
@@ -661,11 +449,9 @@ function rollup(s) {
661
449
  // src/commands/doctor.ts
662
450
  var CHECKS = [
663
451
  { run: checkNode },
664
- { run: checkExecutor },
665
452
  { run: checkPort },
666
453
  { run: checkPostgres },
667
454
  { run: checkStorage },
668
- { run: checkVersions },
669
455
  { run: checkDisk },
670
456
  { run: checkGatekeeper }
671
457
  ];
@@ -711,17 +497,48 @@ var COLOR = {
711
497
  [STATUS_GLYPH.skip]: "[2m"
712
498
  // dim
713
499
  };
714
- var RESET2 = "[0m";
500
+ var RESET = "[0m";
715
501
  function colorize(line) {
716
502
  if (process.stdout.isTTY !== true || process.env.NO_COLOR) return line;
717
503
  const glyph = line[0];
718
504
  const code = glyph ? COLOR[glyph] : void 0;
719
- return code ? `${code}${glyph}${RESET2}${line.slice(1)}` : line;
505
+ return code ? `${code}${glyph}${RESET}${line.slice(1)}` : line;
506
+ }
507
+
508
+ // src/commands/down.ts
509
+ import { readFileSync as readFileSync4, rmSync as rmSync2 } from "fs";
510
+ import { join as join3 } from "path";
511
+
512
+ // src/log.ts
513
+ var BOLD = "\x1B[1m";
514
+ var RED = "\x1B[31m";
515
+ var GREEN = "\x1B[32m";
516
+ var YELLOW = "\x1B[33m";
517
+ var RESET2 = "\x1B[0m";
518
+ var useColor = () => process.stderr.isTTY === true && !process.env.NO_COLOR;
519
+ var paint = (code, s) => useColor() ? `${code}${s}${RESET2}` : s;
520
+ function step(msg) {
521
+ process.stderr.write(`${paint(BOLD, "\u203A")} ${msg}
522
+ `);
523
+ }
524
+ function info(msg) {
525
+ process.stderr.write(` ${msg}
526
+ `);
527
+ }
528
+ function ok(msg) {
529
+ process.stderr.write(`${paint(GREEN, "\u2713")} ${msg}
530
+ `);
531
+ }
532
+ function warn2(msg) {
533
+ process.stderr.write(`${paint(YELLOW, "\u26A0")} ${msg}
534
+ `);
535
+ }
536
+ function error(msg) {
537
+ process.stderr.write(`${paint(RED, "\u2717")} ${msg}
538
+ `);
720
539
  }
721
540
 
722
541
  // src/commands/down.ts
723
- import { readFileSync as readFileSync5, rmSync as rmSync3 } from "fs";
724
- import { join as join4 } from "path";
725
542
  async function down() {
726
543
  const config = loadConfig();
727
544
  const paths = pathsFor(config);
@@ -735,9 +552,9 @@ async function down() {
735
552
  info("no live supervisor \u2014 reaping any orphaned children.");
736
553
  }
737
554
  for (const pid of [
738
- state.bridgePid,
555
+ state.companionPid,
739
556
  state.serverPid,
740
- readPid(join4(paths.pgDataDir, "postmaster.pid"))
557
+ readPid(join3(paths.pgDataDir, "postmaster.pid"))
741
558
  ]) {
742
559
  if (pid && pidAlive(pid)) await killPid(pid);
743
560
  }
@@ -745,10 +562,10 @@ async function down() {
745
562
  timeoutMs: 15e3,
746
563
  intervalMs: 300
747
564
  });
748
- rmSync3(paths.supervisorPidFile, { force: true });
749
- rmSync3(paths.stateFile, { force: true });
565
+ rmSync2(paths.supervisorPidFile, { force: true });
566
+ rmSync2(paths.stateFile, { force: true });
750
567
  if (free) ok("stopped");
751
- else warn(`stopped, but port ${port} still looks busy \u2014 check \`cabane status\`.`);
568
+ else warn2(`stopped, but port ${port} still looks busy \u2014 check \`cabane status\`.`);
752
569
  }
753
570
  async function killPid(pid) {
754
571
  try {
@@ -766,14 +583,14 @@ async function killPid(pid) {
766
583
  }
767
584
  function readState(file) {
768
585
  try {
769
- return JSON.parse(readFileSync5(file, "utf8"));
586
+ return JSON.parse(readFileSync4(file, "utf8"));
770
587
  } catch {
771
588
  return {};
772
589
  }
773
590
  }
774
591
  function readPid(file) {
775
592
  try {
776
- const pid = Number(readFileSync5(file, "utf8").trim());
593
+ const pid = Number(readFileSync4(file, "utf8").trim());
777
594
  return pid > 0 ? pid : null;
778
595
  } catch {
779
596
  return null;
@@ -781,13 +598,13 @@ function readPid(file) {
781
598
  }
782
599
 
783
600
  // src/commands/init.ts
784
- import { existsSync as existsSync5 } from "fs";
601
+ import { existsSync as existsSync4 } from "fs";
785
602
  import { homedir as homedir3 } from "os";
786
- import { isAbsolute, join as join5, resolve } from "path";
603
+ import { isAbsolute, join as join4, resolve } from "path";
787
604
 
788
605
  // src/artifact.ts
789
606
  import { execFileSync } from "child_process";
790
- import { existsSync as existsSync4, mkdirSync as mkdirSync2 } from "fs";
607
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2 } from "fs";
791
608
  function readTarballManifest(tarball) {
792
609
  const raw = execFileSync("tar", ["-xzOf", tarball, "./manifest.json"], {
793
610
  maxBuffer: 16 * 1024 * 1024
@@ -805,7 +622,7 @@ async function npmInstall(appDir) {
805
622
  });
806
623
  }
807
624
  function isInstalled(appDir) {
808
- return existsSync4(appDir) && existsSync4(`${appDir}/node_modules`) && existsSync4(`${appDir}/server.js`);
625
+ return existsSync3(appDir) && existsSync3(`${appDir}/node_modules`) && existsSync3(`${appDir}/server.js`);
809
626
  }
810
627
 
811
628
  // src/prompt.ts
@@ -846,14 +663,13 @@ async function promptSecret(question) {
846
663
  }
847
664
 
848
665
  // src/commands/init.ts
849
- var EXECUTOR_KEY = "ANTHROPIC_API_KEY";
850
666
  async function init() {
851
667
  if (!isInteractive()) {
852
668
  throw new Error(
853
669
  "`cabane init` is an interactive wizard \u2014 run it in a terminal, or configure non-interactively with `cabane install --port \u2026` + `cabane set-env KEY=VALUE`."
854
670
  );
855
671
  }
856
- assertNotForeignBridge();
672
+ assertNotForeignCompanion();
857
673
  const root = cabaneHome();
858
674
  const config = loadConfig();
859
675
  step("Configure your self-hosted Cabane");
@@ -869,9 +685,9 @@ async function init() {
869
685
  const defaultDataDir = resolvePaths(root).dataDir;
870
686
  config.dataDir = dataDir === defaultDataDir ? void 0 : dataDir;
871
687
  const oldPg = pathsFor(loadConfig(), root).pgDataDir;
872
- const newPg = join5(dataDir, "pg");
873
- if (oldPg !== newPg && existsSync5(join5(oldPg, "PG_VERSION"))) {
874
- warn(`existing database at ${oldPg} will NOT move \u2014 a fresh one initializes at ${newPg}.`);
688
+ const newPg = join4(dataDir, "pg");
689
+ if (oldPg !== newPg && existsSync4(join4(oldPg, "PG_VERSION"))) {
690
+ warn2(`existing database at ${oldPg} will NOT move \u2014 a fresh one initializes at ${newPg}.`);
875
691
  info(
876
692
  "to keep your current data, copy that directory over, or leave the location unchanged."
877
693
  );
@@ -888,15 +704,6 @@ async function init() {
888
704
  }
889
705
  config.publicUrl = urlAnswer.replace(/\/$/, "");
890
706
  }
891
- info("");
892
- info(
893
- `Executor API key \u2014 for API-key billing (stored as ${EXECUTOR_KEY}). Leave blank if your`
894
- );
895
- info("executor is already logged in (e.g. a Claude subscription under ~/.claude).");
896
- const key = await promptSecret(`${EXECUTOR_KEY} (hidden, blank to skip): `);
897
- if (key) {
898
- config.bridgeEnv = { ...config.bridgeEnv ?? {}, [EXECUTOR_KEY]: key };
899
- }
900
707
  saveConfig(config, root);
901
708
  const paths = pathsFor(config, root);
902
709
  info("");
@@ -906,9 +713,6 @@ async function init() {
906
713
  info(` database ${paths.pgDataDir}`);
907
714
  info(` port ${config.port}`);
908
715
  info(` url ${config.publicUrl ?? `http://localhost:${config.port}`}`);
909
- info(
910
- ` ${EXECUTOR_KEY} ${config.bridgeEnv?.[EXECUTOR_KEY] ? mask(config.bridgeEnv[EXECUTOR_KEY]) : "(not set \u2014 using on-disk executor auth)"}`
911
- );
912
716
  info("");
913
717
  const installed = config.currentVersion && isInstalled(appDirFor(paths, config.currentVersion));
914
718
  if (installed) {
@@ -920,7 +724,7 @@ async function init() {
920
724
  function expandPath(input, root) {
921
725
  let p = input;
922
726
  if (p === "~") p = homedir3();
923
- else if (p.startsWith("~/")) p = join5(homedir3(), p.slice(2));
727
+ else if (p.startsWith("~/")) p = join4(homedir3(), p.slice(2));
924
728
  return isAbsolute(p) ? p : resolve(root, p);
925
729
  }
926
730
  async function promptPort(current) {
@@ -930,20 +734,16 @@ async function promptPort(current) {
930
734
  if (!answer) return current;
931
735
  const n = Number(answer);
932
736
  if (Number.isInteger(n) && n > 0 && n <= 65535) return n;
933
- warn(`invalid port: ${answer} \u2014 enter a number between 1 and 65535.`);
737
+ warn2(`invalid port: ${answer} \u2014 enter a number between 1 and 65535.`);
934
738
  }
935
739
  throw new Error("no valid port after several tries \u2014 re-run `cabane init`.");
936
740
  }
937
- function mask(value) {
938
- if (value.length <= 8) return "\u2022\u2022\u2022\u2022";
939
- return `${value.slice(0, 4)}\u2026${value.slice(-4)}`;
940
- }
941
741
 
942
742
  // src/commands/install.ts
943
743
  import { randomBytes } from "crypto";
944
- import { existsSync as existsSync6, mkdirSync as mkdirSync3, rmSync as rmSync4 } from "fs";
744
+ import { existsSync as existsSync5, mkdirSync as mkdirSync3, rmSync as rmSync3 } from "fs";
945
745
  import { tmpdir } from "os";
946
- import { join as join6 } from "path";
746
+ import { join as join5 } from "path";
947
747
 
948
748
  // src/dist.ts
949
749
  import { createHash } from "crypto";
@@ -994,10 +794,10 @@ async function downloadArtifact(distBase, token, manifest, destPath) {
994
794
 
995
795
  // src/commands/install.ts
996
796
  async function install(opts) {
997
- assertNotForeignBridge();
797
+ assertNotForeignCompanion();
998
798
  const config = loadConfig();
999
799
  const paths = pathsFor(config);
1000
- const dataExisted = existsSync6(paths.dataDir);
800
+ const dataExisted = existsSync5(paths.dataDir);
1001
801
  if (opts.distBase) config.distBase = opts.distBase.replace(/\/$/, "");
1002
802
  if (opts.port) config.port = opts.port;
1003
803
  mkdirSync3(paths.appsDir, { recursive: true });
@@ -1005,7 +805,7 @@ async function install(opts) {
1005
805
  let version;
1006
806
  let cleanup = null;
1007
807
  if (opts.from) {
1008
- if (!existsSync6(opts.from)) throw new Error(`--from tarball not found: ${opts.from}`);
808
+ if (!existsSync5(opts.from)) throw new Error(`--from tarball not found: ${opts.from}`);
1009
809
  tarball = opts.from;
1010
810
  version = readTarballManifest(tarball).version;
1011
811
  step(`installing from local tarball \u2192 version ${version}`);
@@ -1021,18 +821,18 @@ async function install(opts) {
1021
821
  }
1022
822
  version = manifest.version;
1023
823
  info(`resolved ${version} (${(manifest.bytes / 1024 / 1024).toFixed(1)} MB)`);
1024
- const tmp = join6(tmpdir(), `cabane-artifact-${Date.now().toString(36)}.tgz`);
824
+ const tmp = join5(tmpdir(), `cabane-artifact-${Date.now().toString(36)}.tgz`);
1025
825
  await downloadArtifact(config.distBase, token, manifest, tmp);
1026
826
  ok("downloaded + sha256 verified");
1027
827
  tarball = tmp;
1028
- cleanup = () => rmSync4(tmp, { force: true });
828
+ cleanup = () => rmSync3(tmp, { force: true });
1029
829
  }
1030
830
  try {
1031
831
  const appDir = appDirFor(paths, version);
1032
832
  if (isInstalled(appDir) && !opts.force) {
1033
833
  info(`version ${version} already installed \u2014 reusing (pass --force to reinstall)`);
1034
834
  } else {
1035
- rmSync4(appDir, { recursive: true, force: true });
835
+ rmSync3(appDir, { recursive: true, force: true });
1036
836
  step(`unpacking \u2192 ${appDir}`);
1037
837
  unpackArtifact(tarball, appDir);
1038
838
  step("npm install --omit=dev (runtime deps)\u2026");
@@ -1055,11 +855,11 @@ async function resolveToken(opts, config) {
1055
855
  if (opts.token) return opts.token.trim();
1056
856
  if (config.distToken) return config.distToken;
1057
857
  if (isInteractive()) {
1058
- const t = await promptSecret("Paste your Cabane access token (cabdist_\u2026): ");
858
+ const t = await promptSecret("Paste your Cabane install token (cabdist_\u2026): ");
1059
859
  if (t) return t;
1060
860
  }
1061
861
  throw new Error(
1062
- "no distribution token. Pass --token <cabdist_\u2026>, or --from <tarball> to install a local build."
862
+ "no install token. Pass --token <cabdist_\u2026>, or --from <tarball> to install a local build."
1063
863
  );
1064
864
  }
1065
865
  function persistInstall(config, version) {
@@ -1072,49 +872,45 @@ function persistInstall(config, version) {
1072
872
  }
1073
873
 
1074
874
  // src/commands/logs.ts
1075
- import { existsSync as existsSync7 } from "fs";
875
+ import { existsSync as existsSync6 } from "fs";
1076
876
  async function logs(opts) {
1077
877
  const paths = resolvePaths();
1078
878
  const n = String(opts.lines ?? 80);
1079
- const targets = [];
1080
- if (opts.server || !opts.bridge) targets.push(paths.serverLog);
1081
- if (opts.bridge || !opts.server) targets.push(paths.bridgeLog);
1082
- const present = targets.filter((t) => existsSync7(t));
1083
- if (present.length === 0) {
879
+ if (!existsSync6(paths.serverLog)) {
1084
880
  info("no logs yet \u2014 start the stack with `cabane up`.");
1085
881
  return;
1086
882
  }
1087
- const args = opts.follow ? ["-n", n, "-F", ...present] : ["-n", n, ...present];
883
+ const args = opts.follow ? ["-n", n, "-F", paths.serverLog] : ["-n", n, paths.serverLog];
1088
884
  await run("tail", args, { stdio: ["ignore", "inherit", "inherit"] });
1089
885
  }
1090
886
 
1091
887
  // src/commands/set-env.ts
1092
888
  var KEY_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
1093
- function mask2(value) {
889
+ function mask(value) {
1094
890
  if (value.length <= 8) return "\u2022\u2022\u2022\u2022";
1095
891
  return `${value.slice(0, 4)}\u2026${value.slice(-4)}`;
1096
892
  }
1097
893
  function printList() {
1098
- const env = loadConfig().bridgeEnv ?? {};
894
+ const env = loadConfig().companionEnv ?? {};
1099
895
  const keys = Object.keys(env);
1100
896
  if (keys.length === 0) {
1101
- info("no persisted bridge env. Add one: `cabane set-env ANTHROPIC_API_KEY=\u2026`");
897
+ info("no persisted server env. Add one: `cabane set-env KEY=VALUE`");
1102
898
  return;
1103
899
  }
1104
- step("persisted bridge env:");
1105
- for (const key of keys) info(`${key}=${mask2(env[key])}`);
900
+ step("persisted server env:");
901
+ for (const key of keys) info(`${key}=${mask(env[key])}`);
1106
902
  }
1107
903
  async function setEnv(pairs, opts) {
1108
904
  if (opts.list || pairs.length === 0 && !opts.unset?.length) {
1109
905
  printList();
1110
906
  return;
1111
907
  }
1112
- assertNotForeignBridge();
908
+ assertNotForeignCompanion();
1113
909
  if (opts.unset?.length) {
1114
910
  updateConfig((config) => {
1115
- if (!config.bridgeEnv) return;
1116
- for (const key of opts.unset) delete config.bridgeEnv[key];
1117
- if (Object.keys(config.bridgeEnv).length === 0) delete config.bridgeEnv;
911
+ if (!config.companionEnv) return;
912
+ for (const key of opts.unset) delete config.companionEnv[key];
913
+ if (Object.keys(config.companionEnv).length === 0) delete config.companionEnv;
1118
914
  });
1119
915
  ok(`unset ${opts.unset.join(", ")}`);
1120
916
  }
@@ -1141,16 +937,13 @@ async function setEnv(pairs, opts) {
1141
937
  }
1142
938
  if (Object.keys(parsed).length > 0) {
1143
939
  updateConfig((config) => {
1144
- config.bridgeEnv = { ...config.bridgeEnv ?? {}, ...parsed };
940
+ config.companionEnv = { ...config.companionEnv ?? {}, ...parsed };
1145
941
  });
1146
- for (const key of Object.keys(parsed)) ok(`set ${key}=${mask2(parsed[key])}`);
942
+ for (const key of Object.keys(parsed)) ok(`set ${key}=${mask(parsed[key])}`);
1147
943
  info("restart to apply: `cabane down && cabane up --daemon`");
1148
944
  }
1149
945
  }
1150
946
 
1151
- // src/commands/setup-agent.ts
1152
- import { existsSync as existsSync10 } from "fs";
1153
-
1154
947
  // src/api.ts
1155
948
  var ApiError = class extends Error {
1156
949
  constructor(message, status2, body) {
@@ -1196,10 +989,13 @@ function makeClient(base, token) {
1196
989
  }
1197
990
 
1198
991
  // src/env-compose.ts
1199
- import { join as join7 } from "path";
992
+ import { join as join6 } from "path";
1200
993
  function composeServerEnv(opts) {
1201
- const dataDir = join7(opts.appDir, "data");
994
+ const dataDir = join6(opts.appDir, "data");
1202
995
  return {
996
+ // Persisted operator creds FIRST so the composed infra vars below always win —
997
+ // an operator can't shadow DATABASE_URL / NODE_ENV.
998
+ ...opts.persistedEnv ?? {},
1203
999
  NODE_ENV: "production",
1204
1000
  PORT: String(opts.port),
1205
1001
  DATABASE_URL: opts.databaseUrl,
@@ -1209,24 +1005,11 @@ function composeServerEnv(opts) {
1209
1005
  PUBLIC_BASE_URL: opts.publicBaseUrl?.replace(/\/$/, "") || `http://localhost:${opts.port}`,
1210
1006
  VISION_URL_SIGNING_SECRET: opts.visionSecret,
1211
1007
  // Runtime file-read overrides → the artifact's own data/ tree.
1212
- CABANE_API_ROOT: join7(dataDir, "api"),
1213
- CABANE_SKILLS_DIR: join7(dataDir, "skills"),
1214
- CABANE_DOCS_CONTENT_DIR: join7(dataDir, "docs-content"),
1215
- CABANE_WELCOME_DIR: join7(dataDir, "welcome"),
1216
- ...opts.houseOwnerUserId ? { CABANE_HOUSE_OWNER_USER_ID: opts.houseOwnerUserId } : {},
1008
+ CABANE_API_ROOT: join6(dataDir, "api"),
1009
+ CABANE_SKILLS_DIR: join6(dataDir, "skills"),
1217
1010
  ...opts.appVersion ? { CABANE_APP_VERSION: opts.appVersion } : {}
1218
1011
  };
1219
1012
  }
1220
- function composeBridgeEnv(opts) {
1221
- return {
1222
- ...opts.persistedEnv,
1223
- ...opts.parentEnv,
1224
- CABANE_BRIDGE_HOME: opts.bridgeHome,
1225
- ...opts.claudeConfigDir ? { CLAUDE_CONFIG_DIR: opts.claudeConfigDir } : {},
1226
- BRIDGE_NO_OPEN: "1",
1227
- CABANE_BRIDGE_CLASS: "house"
1228
- };
1229
- }
1230
1013
  function localBaseUrl(port) {
1231
1014
  return `http://localhost:${port}`;
1232
1015
  }
@@ -1234,123 +1017,123 @@ function embeddedDatabaseUrl(port) {
1234
1017
  return `postgres://cabane:cabane@127.0.0.1:${port}/cabane`;
1235
1018
  }
1236
1019
 
1237
- // src/supervisor/house-bridge.ts
1238
- import { spawn as spawn2 } from "child_process";
1239
- import { mkdirSync as mkdirSync4, openSync, readFileSync as readFileSync6, rmSync as rmSync5, writeFileSync as writeFileSync3 } from "fs";
1240
- import { dirname as dirname3, join as join8 } from "path";
1241
- var PAIRING_VERSION = 1;
1242
- function resolveBridgeBin(appDir) {
1243
- const pkgPath = join8(appDir, "bridge", "package.json");
1244
- const pkg = JSON.parse(readFileSync6(pkgPath, "utf8"));
1245
- const rel = typeof pkg.bin === "string" ? pkg.bin : pkg.bin?.["cabane-bridge"];
1246
- if (!rel) {
1247
- throw new Error(
1248
- `the installed artifact's bridge (${pkgPath}) exposes no cabane-bridge bin \u2014 reinstall.`
1020
+ // src/commands/setup-agent.ts
1021
+ async function setupAgent(opts) {
1022
+ const config = loadConfig();
1023
+ const base = localBaseUrl(config.port);
1024
+ const instanceUrl = config.publicUrl ?? base;
1025
+ if (!await healthy(config.port)) {
1026
+ throw new Error(`server is not up on ${base} \u2014 run \`cabane up\` and sign up first.`);
1027
+ }
1028
+ const token = await resolvePat(opts, base);
1029
+ const client = makeClient(base, token);
1030
+ const me = await client.get("/api/auth/me");
1031
+ ok(`owner resolved (${me.user.id})`);
1032
+ const assignment = await defaultAgentAssignment(client);
1033
+ info("");
1034
+ if (assignment.kind === "unavailable") {
1035
+ info("Agent assignment could not be checked. Open Cabane to verify it.");
1036
+ return;
1037
+ }
1038
+ const { username, executor } = assignment.agent;
1039
+ if (executor.deviceId) {
1040
+ info(`Agent @${username} is assigned to ${executor.deviceLabel}.`);
1041
+ return;
1042
+ }
1043
+ info(`Agent @${username} has no device assigned.`);
1044
+ info(" 1. Install the Companion \u2014 the desktop app, or the headless `@cabane/companion`.");
1045
+ info(` 2. Sign in against this instance (${instanceUrl}) and pair the device.`);
1046
+ info(
1047
+ ` 3. Assign @${username} to the device, then choose a connector on it and message the agent in the app.`
1048
+ );
1049
+ }
1050
+ async function healthy(port) {
1051
+ try {
1052
+ return (await fetch(`http://127.0.0.1:${port}/healthz`)).ok;
1053
+ } catch {
1054
+ return false;
1055
+ }
1056
+ }
1057
+ async function resolvePat(opts, base) {
1058
+ if (opts.token) return opts.token.trim();
1059
+ if (isInteractive()) {
1060
+ info(`mint a token in the app: ${base} \u2192 Settings \u2192 Developer \u2192 tokens`);
1061
+ const t = await promptSecret("Paste a Cabane access token (cab_\u2026): ");
1062
+ if (t) return t;
1063
+ }
1064
+ throw new Error("no access token. Pass --token <cab_\u2026> (mint it in Settings \u2192 Developer).");
1065
+ }
1066
+ async function defaultAgentAssignment(client) {
1067
+ try {
1068
+ const { workspaces } = await client.get("/api/workspaces");
1069
+ const workspace = workspaces.find((w) => w.role === "owner") ?? workspaces[0];
1070
+ if (!workspace) return { kind: "unavailable" };
1071
+ const { agents } = await client.get(
1072
+ `/api/workspaces/${workspace.id}/agents`
1249
1073
  );
1074
+ const agent = workspace.defaultAgentId && agents.find((a) => a.id === workspace.defaultAgentId) || agents.find((a) => a.username === "cabane") || agents[0];
1075
+ if (!agent?.username || !agent.executor) return { kind: "unavailable" };
1076
+ if (agent.executor.deviceId && !agent.executor.deviceLabel) return { kind: "unavailable" };
1077
+ return { kind: "resolved", agent };
1078
+ } catch {
1079
+ return { kind: "unavailable" };
1250
1080
  }
1251
- return join8(dirname3(pkgPath), rel);
1252
1081
  }
1253
- function encodePairingString(input) {
1254
- const payload = {
1255
- v: PAIRING_VERSION,
1256
- baseUrl: input.baseUrl,
1257
- deviceToken: input.deviceToken,
1258
- ...input.deviceId ? { deviceId: input.deviceId } : {},
1259
- ...input.deviceLabel ? { deviceLabel: input.deviceLabel } : {}
1082
+
1083
+ // src/commands/status.ts
1084
+ import { readFileSync as readFileSync5 } from "fs";
1085
+ async function status() {
1086
+ const config = loadConfig();
1087
+ const paths = pathsFor(config);
1088
+ const state = readState2(paths.stateFile);
1089
+ const supervisorPid = readSupervisorPid(paths.supervisorPidFile) ?? state.supervisorPid;
1090
+ const supervisorUp = supervisorPid ? pidAlive(supervisorPid) : false;
1091
+ const serverUp = await healthy2(config.port);
1092
+ const line = (label, value) => {
1093
+ process.stdout.write(` ${label.padEnd(12)}${value}
1094
+ `);
1260
1095
  };
1261
- return Buffer.from(JSON.stringify(payload), "utf8").toString("base64url");
1096
+ process.stdout.write("cabane\n");
1097
+ line("version", config.currentVersion ?? "(not installed)");
1098
+ if (config.previousVersion) line("previous", config.previousVersion);
1099
+ line("port", String(config.port));
1100
+ line("home", paths.root);
1101
+ line("data", `${paths.dataDir} (pg + storage \xB7 kept across upgrades)`);
1102
+ line("supervisor", supervisorUp ? `up (pid ${supervisorPid})` : "down");
1103
+ line("server", serverUp ? `up \xB7 http://localhost:${config.port}` : "down");
1104
+ if (!supervisorUp) info("start it: cabane up");
1262
1105
  }
1263
- async function pairBridge(opts) {
1264
- mkdirSync4(opts.bridgeHome, { recursive: true });
1265
- const pairingString = encodePairingString(opts);
1266
- const tmp = join8(opts.bridgeHome, `.pairing-${process.pid}.txt`);
1267
- writeFileSync3(tmp, pairingString, { mode: 384 });
1106
+ function readState2(file) {
1268
1107
  try {
1269
- await run("node", [resolveBridgeBin(opts.appDir), "pair", "--file", tmp], {
1270
- env: {
1271
- PATH: process.env.PATH ?? "",
1272
- ...process.env.HOME ? { HOME: process.env.HOME } : {},
1273
- CABANE_BRIDGE_HOME: opts.bridgeHome
1274
- }
1275
- });
1276
- } finally {
1277
- rmSync5(tmp, { force: true });
1108
+ return JSON.parse(readFileSync5(file, "utf8"));
1109
+ } catch {
1110
+ return {};
1278
1111
  }
1279
- ok("bridge paired");
1280
1112
  }
1281
- function bridgeConfigPath(bridgeHome) {
1282
- return join8(bridgeHome, ".cabane", "config.json");
1283
- }
1284
- function readBridgeConfig(bridgeHome) {
1113
+ function readSupervisorPid(file) {
1285
1114
  try {
1286
- return JSON.parse(readFileSync6(bridgeConfigPath(bridgeHome), "utf8"));
1115
+ const pid = Number(readFileSync5(file, "utf8").trim());
1116
+ return pid > 0 ? pid : null;
1287
1117
  } catch {
1288
1118
  return null;
1289
1119
  }
1290
1120
  }
1291
- var BridgeChild = class {
1292
- constructor(appDir, bridgeHome, claudeConfigDir, logFile, persistedEnv) {
1293
- this.appDir = appDir;
1294
- this.bridgeHome = bridgeHome;
1295
- this.claudeConfigDir = claudeConfigDir;
1296
- this.logFile = logFile;
1297
- this.persistedEnv = persistedEnv;
1298
- }
1299
- appDir;
1300
- bridgeHome;
1301
- claudeConfigDir;
1302
- logFile;
1303
- persistedEnv;
1304
- child = null;
1305
- /** Spawn `cabane-bridge start --no-open` with the forwarded shell env under the
1306
- * house-class overrides (CT480). */
1307
- start() {
1308
- const out = openSync(this.logFile, "a");
1309
- this.child = spawn2("node", [resolveBridgeBin(this.appDir), "start", "--no-open"], {
1310
- env: composeBridgeEnv({
1311
- parentEnv: process.env,
1312
- bridgeHome: this.bridgeHome,
1313
- claudeConfigDir: this.claudeConfigDir,
1314
- persistedEnv: this.persistedEnv
1315
- }),
1316
- stdio: ["ignore", out, out]
1317
- });
1318
- this.child.on("exit", (code, signal) => {
1319
- if (code && code !== 0)
1320
- warn(`bridge exited (code ${code}${signal ? `, ${signal}` : ""})`);
1321
- this.child = null;
1322
- });
1323
- ok("house bridge started");
1324
- }
1325
- get running() {
1326
- return this.child !== null;
1327
- }
1328
- get pid() {
1329
- return this.child?.pid;
1330
- }
1331
- async stop() {
1332
- const child = this.child;
1333
- if (!child) return;
1334
- this.child = null;
1335
- await new Promise((resolve2) => {
1336
- const kill9 = setTimeout(() => child.kill("SIGKILL"), 5e3);
1337
- child.once("exit", () => {
1338
- clearTimeout(kill9);
1339
- resolve2();
1340
- });
1341
- child.kill("SIGTERM");
1342
- });
1121
+ async function healthy2(port) {
1122
+ try {
1123
+ return (await fetch(`http://127.0.0.1:${port}/healthz`)).ok;
1124
+ } catch {
1125
+ return false;
1343
1126
  }
1344
- };
1127
+ }
1345
1128
 
1346
1129
  // src/commands/up.ts
1347
- import { spawn as spawn4 } from "child_process";
1348
- import { existsSync as existsSync9, mkdirSync as mkdirSync5, openSync as openSync3, readFileSync as readFileSync7, rmSync as rmSync6, writeFileSync as writeFileSync4 } from "fs";
1349
- import { join as join10 } from "path";
1130
+ import { spawn as spawn3 } from "child_process";
1131
+ import { existsSync as existsSync8, mkdirSync as mkdirSync4, openSync as openSync2, readFileSync as readFileSync6, rmSync as rmSync4, writeFileSync as writeFileSync3 } from "fs";
1132
+ import { join as join8 } from "path";
1350
1133
 
1351
1134
  // src/supervisor/postgres.ts
1352
- import { existsSync as existsSync8 } from "fs";
1353
- import { join as join9 } from "path";
1135
+ import { existsSync as existsSync7 } from "fs";
1136
+ import { join as join7 } from "path";
1354
1137
  import EmbeddedPostgres from "embedded-postgres";
1355
1138
  var EmbeddedPg = class {
1356
1139
  constructor(dataDir, port) {
@@ -1373,11 +1156,11 @@ var EmbeddedPg = class {
1373
1156
  onLog: () => {
1374
1157
  },
1375
1158
  // quiet — pg writes its own logs under the data dir
1376
- onError: (err) => warn(`[pg] ${err instanceof Error ? err.message : String(err)}`)
1159
+ onError: (err) => warn2(`[pg] ${err instanceof Error ? err.message : String(err)}`)
1377
1160
  });
1378
1161
  }
1379
1162
  initialised() {
1380
- return existsSync8(join9(this.dataDir, "PG_VERSION"));
1163
+ return existsSync7(join7(this.dataDir, "PG_VERSION"));
1381
1164
  }
1382
1165
  /** Init (first run) + start the postmaster + ensure the app database exists. */
1383
1166
  async start() {
@@ -1405,8 +1188,8 @@ var EmbeddedPg = class {
1405
1188
  };
1406
1189
 
1407
1190
  // src/supervisor/server.ts
1408
- import { openSync as openSync2 } from "fs";
1409
- import { spawn as spawn3 } from "child_process";
1191
+ import { openSync } from "fs";
1192
+ import { spawn as spawn2 } from "child_process";
1410
1193
  var ServerChild = class {
1411
1194
  constructor(appDir, env, port, logFile) {
1412
1195
  this.appDir = appDir;
@@ -1439,15 +1222,15 @@ var ServerChild = class {
1439
1222
  }
1440
1223
  /** Spawn `node server.js` and wait for /healthz. */
1441
1224
  async start() {
1442
- const out = openSync2(this.logFile, "a");
1443
- this.child = spawn3("node", ["server.js"], {
1225
+ const out = openSync(this.logFile, "a");
1226
+ this.child = spawn2("node", ["server.js"], {
1444
1227
  cwd: this.appDir,
1445
1228
  env: this.childEnv(),
1446
1229
  stdio: ["ignore", out, out]
1447
1230
  });
1448
1231
  this.child.on("exit", (code, signal) => {
1449
1232
  if (code && code !== 0)
1450
- warn(`server exited (code ${code}${signal ? `, ${signal}` : ""})`);
1233
+ warn2(`server exited (code ${code}${signal ? `, ${signal}` : ""})`);
1451
1234
  this.child = null;
1452
1235
  });
1453
1236
  const healthy3 = await waitUntil(() => this.healthy(), { timeoutMs: 45e3, intervalMs: 500 });
@@ -1512,18 +1295,18 @@ async function up(opts) {
1512
1295
  await new Supervisor(paths, appDir, config.port).run();
1513
1296
  }
1514
1297
  async function launchDaemon(paths, config) {
1515
- if (existsSync9(paths.supervisorPidFile)) {
1516
- const pid = Number(readFileSync7(paths.supervisorPidFile, "utf8").trim());
1298
+ if (existsSync8(paths.supervisorPidFile)) {
1299
+ const pid = Number(readFileSync6(paths.supervisorPidFile, "utf8").trim());
1517
1300
  if (pid && pidAlive(pid)) {
1518
- warn(`already running (supervisor pid ${pid}). Use \`cabane down\` to stop it.`);
1301
+ warn2(`already running (supervisor pid ${pid}). Use \`cabane down\` to stop it.`);
1519
1302
  return;
1520
1303
  }
1521
1304
  }
1522
- mkdirSync5(paths.logsDir, { recursive: true });
1305
+ mkdirSync4(paths.logsDir, { recursive: true });
1523
1306
  const entry = process.argv[1];
1524
1307
  if (!entry) throw new Error("cannot locate the cabane CLI entry to run in the background.");
1525
- const out = openSync3(paths.serverLog, "a");
1526
- const child = spawn4(process.execPath, [entry, "up", "--port", String(config.port)], {
1308
+ const out = openSync2(paths.serverLog, "a");
1309
+ const child = spawn3(process.execPath, [entry, "up", "--port", String(config.port)], {
1527
1310
  detached: true,
1528
1311
  stdio: ["ignore", out, out]
1529
1312
  });
@@ -1544,18 +1327,13 @@ async function launchDaemon(paths, config) {
1544
1327
  throw new Error(`background start did not come healthy within 60s \u2014 see ${paths.serverLog}`);
1545
1328
  }
1546
1329
  ok(`running in the background on http://localhost:${config.port}`);
1547
- for (const line of daemonFooter(config)) info(line);
1548
- }
1549
- function daemonFooter(config) {
1550
- const lines = [];
1551
- if (!config.houseOwnerUserId) {
1552
- lines.push("next: open the URL, sign up, then `cabane setup-agent`");
1553
- }
1554
- lines.push("logs: cabane logs \xB7 stop: cabane down");
1555
- return lines;
1330
+ for (const line of daemonFooter()) info(line);
1556
1331
  }
1557
- function resolveClaudeConfigDir(env = process.env) {
1558
- return env.CLAUDE_CONFIG_DIR;
1332
+ function daemonFooter() {
1333
+ return [
1334
+ "next: open the URL, sign up, then pair a companion so your agents can answer \u2014 `cabane setup-agent` explains how",
1335
+ "logs: cabane logs \xB7 stop: cabane down"
1336
+ ];
1559
1337
  }
1560
1338
  var Supervisor = class {
1561
1339
  constructor(paths, appDir, port) {
@@ -1569,7 +1347,6 @@ var Supervisor = class {
1569
1347
  port;
1570
1348
  pg;
1571
1349
  server = null;
1572
- bridge = null;
1573
1350
  shuttingDown = false;
1574
1351
  buildServer() {
1575
1352
  const config = loadConfig();
@@ -1580,9 +1357,11 @@ var Supervisor = class {
1580
1357
  storageRoot: this.paths.storageRoot,
1581
1358
  visionSecret: config.visionSecret,
1582
1359
  ...config.publicUrl ? { publicBaseUrl: config.publicUrl } : {},
1583
- ...config.houseOwnerUserId ? { houseOwnerUserId: config.houseOwnerUserId } : {},
1584
- // CT484: the artifact's build version → the server reports it on the bridge
1585
- // heartbeat so a bridge/server skew is detectable. Read from the installed
1360
+ // Persisted `cabane set-env` creds (generic server env) are layered beneath
1361
+ // the composed infra vars.
1362
+ ...config.companionEnv ? { persistedEnv: config.companionEnv } : {},
1363
+ // CT484: the artifact's build version → the server reports it on the companion
1364
+ // heartbeat so a companion/server skew is detectable. Read from the installed
1586
1365
  // tree's package.json (stamped at build); best-effort.
1587
1366
  ...(() => {
1588
1367
  const v = readAppVersion(this.appDir);
@@ -1591,51 +1370,25 @@ var Supervisor = class {
1591
1370
  });
1592
1371
  return new ServerChild(this.appDir, env, this.port, this.paths.serverLog);
1593
1372
  }
1594
- /** Start the bridge child iff it is configured (registered + paired) and not
1595
- * already running. */
1596
- ensureBridge() {
1597
- if (this.bridge?.running) return;
1598
- const config = loadConfig();
1599
- const paired = readBridgeConfig(this.paths.bridgeHome)?.deviceId;
1600
- if (!config.houseDeviceId || !paired) return;
1601
- const claudeConfigDir = resolveClaudeConfigDir();
1602
- this.bridge = new BridgeChild(
1603
- this.appDir,
1604
- this.paths.bridgeHome,
1605
- claudeConfigDir,
1606
- this.paths.bridgeLog,
1607
- config.bridgeEnv
1608
- );
1609
- try {
1610
- this.bridge.start();
1611
- } catch (err) {
1612
- this.bridge = null;
1613
- warn(`house bridge could not start: ${err instanceof Error ? err.message : String(err)}`);
1614
- info(
1615
- "the server is up; agents will not answer until this is resolved (try `cabane update`)."
1616
- );
1617
- }
1618
- }
1619
1373
  /** Publish the supervisor pid as early as possible so `status` / `down` /
1620
1374
  * `setup-agent` can find us the instant `up --daemon` returns (the daemon
1621
1375
  * launcher only waits on /healthz, which comes up before the children are all
1622
1376
  * wired). */
1623
1377
  writePid() {
1624
- mkdirSync5(this.paths.runDir, { recursive: true });
1625
- writeFileSync4(this.paths.supervisorPidFile, `${process.pid}
1378
+ mkdirSync4(this.paths.runDir, { recursive: true });
1379
+ writeFileSync3(this.paths.supervisorPidFile, `${process.pid}
1626
1380
  `);
1627
1381
  }
1628
1382
  writeState() {
1629
- mkdirSync5(this.paths.runDir, { recursive: true });
1630
- writeFileSync4(this.paths.supervisorPidFile, `${process.pid}
1383
+ mkdirSync4(this.paths.runDir, { recursive: true });
1384
+ writeFileSync3(this.paths.supervisorPidFile, `${process.pid}
1631
1385
  `);
1632
- writeFileSync4(
1386
+ writeFileSync3(
1633
1387
  this.paths.stateFile,
1634
1388
  JSON.stringify(
1635
1389
  {
1636
1390
  supervisorPid: process.pid,
1637
1391
  serverPid: this.server?.pid ?? null,
1638
- bridgePid: this.bridge?.pid ?? null,
1639
1392
  port: this.port,
1640
1393
  // Bumped on every (re)write — a completed reload advances it, which is
1641
1394
  // how `setup-agent` knows a restart it requested actually finished
@@ -1648,8 +1401,8 @@ var Supervisor = class {
1648
1401
  );
1649
1402
  }
1650
1403
  async run() {
1651
- mkdirSync5(this.paths.logsDir, { recursive: true });
1652
- mkdirSync5(this.paths.storageRoot, { recursive: true });
1404
+ mkdirSync4(this.paths.logsDir, { recursive: true });
1405
+ mkdirSync4(this.paths.storageRoot, { recursive: true });
1653
1406
  this.writePid();
1654
1407
  step("starting embedded Postgres\u2026");
1655
1408
  await this.pg.start();
@@ -1657,27 +1410,25 @@ var Supervisor = class {
1657
1410
  step("starting server\u2026");
1658
1411
  await this.server.migrate();
1659
1412
  await this.server.start();
1660
- this.ensureBridge();
1661
1413
  this.writeState();
1662
1414
  process.on("SIGINT", () => void this.teardown("SIGINT"));
1663
1415
  process.on("SIGTERM", () => void this.teardown("SIGTERM"));
1664
1416
  process.on("exit", () => {
1665
- for (const pid of [this.bridge?.pid, this.server?.pid]) {
1666
- if (pid) {
1667
- try {
1668
- process.kill(pid, "SIGKILL");
1669
- } catch {
1670
- }
1417
+ const pid = this.server?.pid;
1418
+ if (pid) {
1419
+ try {
1420
+ process.kill(pid, "SIGKILL");
1421
+ } catch {
1671
1422
  }
1672
1423
  }
1673
1424
  });
1674
1425
  process.on(
1675
1426
  "uncaughtException",
1676
- (err) => warn(`ignoring uncaught error: ${err instanceof Error ? err.message : String(err)}`)
1427
+ (err) => warn2(`ignoring uncaught error: ${err instanceof Error ? err.message : String(err)}`)
1677
1428
  );
1678
1429
  process.on(
1679
1430
  "unhandledRejection",
1680
- (err) => warn(`ignoring unhandled rejection: ${err instanceof Error ? err.message : String(err)}`)
1431
+ (err) => warn2(`ignoring unhandled rejection: ${err instanceof Error ? err.message : String(err)}`)
1681
1432
  );
1682
1433
  ok("cabane is up \u2014 Ctrl-C to stop");
1683
1434
  await new Promise(() => {
@@ -1687,207 +1438,23 @@ var Supervisor = class {
1687
1438
  if (this.shuttingDown) return;
1688
1439
  this.shuttingDown = true;
1689
1440
  step(`${signal} \u2014 stopping children\u2026`);
1690
- await this.bridge?.stop();
1691
1441
  await this.server?.stop();
1692
1442
  await this.pg.stop();
1693
- rmSync6(this.paths.supervisorPidFile, { force: true });
1694
- rmSync6(this.paths.stateFile, { force: true });
1443
+ rmSync4(this.paths.supervisorPidFile, { force: true });
1444
+ rmSync4(this.paths.stateFile, { force: true });
1695
1445
  ok("stopped cleanly");
1696
1446
  process.exit(0);
1697
1447
  }
1698
1448
  };
1699
1449
  function readAppVersion(appDir) {
1700
1450
  try {
1701
- const pkg = JSON.parse(readFileSync7(join10(appDir, "package.json"), "utf8"));
1451
+ const pkg = JSON.parse(readFileSync6(join8(appDir, "package.json"), "utf8"));
1702
1452
  return pkg.version ?? null;
1703
1453
  } catch {
1704
1454
  return null;
1705
1455
  }
1706
1456
  }
1707
1457
 
1708
- // src/commands/setup-agent.ts
1709
- var DEFAULT_AGENT_MODEL = "anthropic/claude-opus-4-8";
1710
- var DEVICE_LABEL = "cabane-house-bridge";
1711
- async function setupAgent(opts) {
1712
- const paths = resolvePaths();
1713
- const config = loadConfig();
1714
- const base = localBaseUrl(config.port);
1715
- const model = opts.model ?? DEFAULT_AGENT_MODEL;
1716
- if (!await healthy(config.port)) {
1717
- throw new Error(`server is not up on ${base} \u2014 run \`cabane up\` and sign up first.`);
1718
- }
1719
- if (!existsSync10(paths.supervisorPidFile)) {
1720
- throw new Error("no running supervisor found \u2014 start the stack with `cabane up` first.");
1721
- }
1722
- const restart = async () => {
1723
- await down();
1724
- await up({ daemon: true, port: config.port });
1725
- };
1726
- const token = await resolvePat(opts, base);
1727
- const client = makeClient(base, token);
1728
- const me = await client.get("/api/auth/me");
1729
- const ownerId = me.user.id;
1730
- ok(`owner resolved (${ownerId})`);
1731
- if (config.houseOwnerUserId !== ownerId) {
1732
- config.houseOwnerUserId = ownerId;
1733
- saveConfig(config);
1734
- step("applying ops identity to the server (restart)\u2026");
1735
- await restart();
1736
- }
1737
- const appDir = appDirFor(paths, config.currentVersion ?? "");
1738
- const deviceId = await ensureHouseDevice(client, config, appDir, paths.bridgeHome, base);
1739
- await assignDefaultAgent(client, deviceId, model);
1740
- step("starting the house bridge + assigning agents (restart)\u2026");
1741
- await restart();
1742
- const beating = await waitForHeartbeat(client, deviceId);
1743
- if (beating) ok("house bridge heartbeating \u2014 agents are live");
1744
- else warn("no bridge heartbeat yet \u2014 check `cabane logs`; it may need a minute");
1745
- ok("setup complete");
1746
- info("message your agent in the app \u2014 it should answer.");
1747
- }
1748
- async function healthy(port) {
1749
- try {
1750
- return (await fetch(`http://127.0.0.1:${port}/healthz`)).ok;
1751
- } catch {
1752
- return false;
1753
- }
1754
- }
1755
- async function resolvePat(opts, base) {
1756
- if (opts.token) return opts.token.trim();
1757
- if (isInteractive()) {
1758
- info(`mint a token in the app: ${base} \u2192 Settings \u2192 Developer \u2192 tokens`);
1759
- const t = await promptSecret("Paste a Cabane access token (cab_\u2026): ");
1760
- if (t) return t;
1761
- }
1762
- throw new Error("no access token. Pass --token <cab_\u2026> (mint it in Settings \u2192 Developer).");
1763
- }
1764
- async function ensureHouseDevice(client, config, appDir, bridgeHome, base) {
1765
- step("registering the house device\u2026");
1766
- let deviceId;
1767
- let deviceToken;
1768
- try {
1769
- const res = await client.post(
1770
- "/api/bridge-devices",
1771
- { label: DEVICE_LABEL, class: "house" }
1772
- );
1773
- deviceId = res.device.id;
1774
- deviceToken = res.deviceToken;
1775
- } catch (err) {
1776
- if (err instanceof ApiError && err.status === 409 && config.houseDeviceId) {
1777
- info(`reusing already-registered house device ${config.houseDeviceId}`);
1778
- return config.houseDeviceId;
1779
- }
1780
- throw err;
1781
- }
1782
- config.houseDeviceId = deviceId;
1783
- saveConfig(config);
1784
- await pairBridge({
1785
- appDir,
1786
- bridgeHome,
1787
- baseUrl: base,
1788
- deviceToken,
1789
- deviceId,
1790
- deviceLabel: DEVICE_LABEL
1791
- });
1792
- return deviceId;
1793
- }
1794
- async function assignDefaultAgent(client, deviceId, model) {
1795
- const { workspaces } = await client.get("/api/workspaces");
1796
- const workspace = workspaces.find((w) => w.role === "owner") ?? workspaces[0];
1797
- if (!workspace) throw new Error("the account owns no workspace \u2014 create one in the app first.");
1798
- const wsPath = `/api/workspaces/${workspace.id}`;
1799
- const { agents } = await client.get(
1800
- `${wsPath}/agents`
1801
- );
1802
- const agent = workspace.defaultAgentId && agents.find((a) => a.id === workspace.defaultAgentId) || agents.find((a) => a.username === "cabane") || agents[0];
1803
- if (!agent) throw new Error("the workspace has no agent to assign.");
1804
- const { runConfig } = await client.get(
1805
- `${wsPath}/agents/${agent.id}/run-config`
1806
- );
1807
- if (!runConfig?.model) {
1808
- info(`setting @${agent.username} model \u2192 ${model}`);
1809
- await client.put(`${wsPath}/agents/${agent.id}/run-config`, { ...runConfig ?? {}, model });
1810
- }
1811
- info(`assigning @${agent.username} \u2192 the house bridge`);
1812
- await client.put(`${wsPath}/agents/${agent.id}/executor`, { executor: "bridge", deviceId });
1813
- }
1814
- async function waitForHeartbeat(client, deviceId) {
1815
- return waitUntil(
1816
- async () => {
1817
- const { devices } = await client.get(
1818
- "/api/bridge-devices"
1819
- );
1820
- return Boolean(devices.find((d) => d.id === deviceId)?.lastSeenAt);
1821
- },
1822
- { timeoutMs: 9e4, intervalMs: 3e3 }
1823
- );
1824
- }
1825
-
1826
- // src/commands/status.ts
1827
- import { existsSync as existsSync11, readFileSync as readFileSync8 } from "fs";
1828
- import { join as join11 } from "path";
1829
- function readBridgeCrash(bridgeHome) {
1830
- try {
1831
- const path = join11(bridgeHome, ".cabane", "last-error.json");
1832
- if (!existsSync11(path)) return null;
1833
- return JSON.parse(readFileSync8(path, "utf8"));
1834
- } catch {
1835
- return null;
1836
- }
1837
- }
1838
- async function status() {
1839
- const config = loadConfig();
1840
- const paths = pathsFor(config);
1841
- const state = readState2(paths.stateFile);
1842
- const supervisorPid = readSupervisorPid(paths.supervisorPidFile) ?? state.supervisorPid;
1843
- const supervisorUp = supervisorPid ? pidAlive(supervisorPid) : false;
1844
- const serverUp = await healthy2(config.port);
1845
- const bridgeUp = state.bridgePid ? pidAlive(state.bridgePid) : false;
1846
- const line = (label, value) => {
1847
- process.stdout.write(` ${label.padEnd(12)}${value}
1848
- `);
1849
- };
1850
- process.stdout.write("cabane\n");
1851
- line("version", config.currentVersion ?? "(not installed)");
1852
- if (config.previousVersion) line("previous", config.previousVersion);
1853
- line("port", String(config.port));
1854
- line("home", paths.root);
1855
- line("data", `${paths.dataDir} (pg + storage \xB7 kept across upgrades)`);
1856
- line("supervisor", supervisorUp ? `up (pid ${supervisorPid})` : "down");
1857
- line("server", serverUp ? `up \xB7 http://localhost:${config.port}` : "down");
1858
- const crash = bridgeUp ? null : readBridgeCrash(paths.bridgeHome);
1859
- line(
1860
- "bridge",
1861
- config.houseDeviceId ? bridgeUp ? "up" : crash ? `crashed \u2014 ${crash.reason}` : "configured \xB7 down" : "not set up (cabane setup-agent)"
1862
- );
1863
- if (!bridgeUp && crash) {
1864
- info("see why: cabane logs --bridge");
1865
- }
1866
- if (!supervisorUp) info("start it: cabane up");
1867
- }
1868
- function readState2(file) {
1869
- try {
1870
- return JSON.parse(readFileSync8(file, "utf8"));
1871
- } catch {
1872
- return {};
1873
- }
1874
- }
1875
- function readSupervisorPid(file) {
1876
- try {
1877
- const pid = Number(readFileSync8(file, "utf8").trim());
1878
- return pid > 0 ? pid : null;
1879
- } catch {
1880
- return null;
1881
- }
1882
- }
1883
- async function healthy2(port) {
1884
- try {
1885
- return (await fetch(`http://127.0.0.1:${port}/healthz`)).ok;
1886
- } catch {
1887
- return false;
1888
- }
1889
- }
1890
-
1891
1458
  // src/commands/update.ts
1892
1459
  async function update(opts) {
1893
1460
  const config = loadConfig();
@@ -1922,20 +1489,18 @@ var parsePort = (raw) => {
1922
1489
  if (!Number.isInteger(n) || n <= 0 || n > 65535) throw new Error(`invalid port: ${raw}`);
1923
1490
  return n;
1924
1491
  };
1925
- program.command("init").description("interactive first-run wizard: data location, port, and the executor credential").action(init);
1492
+ program.command("init").description("interactive first-run wizard: data location, port, and public URL").action(init);
1926
1493
  program.command("install").description("install the server artifact (token \u2192 gated download, or --from a local tarball)").option("--token <token>", "distribution token (cabdist_\u2026) \u2014 prompted if omitted").option("--from <tarball>", "install from a local artifact tarball instead of downloading").option("--dist-base <url>", "distribution origin to fetch from (default https://dist.cabane.ai)").option("--version <version>", "install a specific published version (default: latest)").option("--port <port>", "local port the server will listen on (default 3000)", parsePort).option("--force", "reinstall even if this version is already present").action(install);
1927
- program.command("up").description("start + supervise the stack (Postgres, server, house bridge)").option("--daemon", "run in the background (terminal returns; stop with `cabane down`)").option("--port <port>", "override the configured port", parsePort).action(up);
1928
- program.command("setup-agent").description("after browser signup: register the house bridge + put your agent on it").option("--token <token>", "a Cabane access token (cab_\u2026) \u2014 prompted if omitted").option("--model <model>", "model the agent runs (default anthropic/claude-opus-4-8)").action(setupAgent);
1494
+ program.command("up").description("start + supervise the stack (Postgres + server)").option("--daemon", "run in the background (terminal returns; stop with `cabane down`)").option("--port <port>", "override the configured port", parsePort).action(up);
1495
+ program.command("setup-agent").description("after browser signup: confirm your account and print companion-pairing guidance").option("--token <token>", "a Cabane access token (cab_\u2026) \u2014 prompted if omitted").action(setupAgent);
1929
1496
  program.command("update").description("install a newer published artifact (keeps the previous for rollback)").option("--token <token>", "distribution token (cabdist_\u2026) \u2014 defaults to the stored one").option("--dist-base <url>", "distribution origin to fetch from").option("--version <version>", "install a specific version instead of latest").action(update);
1930
1497
  program.command("set-env [pairs...]").description(
1931
- "persist credentials forwarded to the house bridge \u2014 `KEY=VALUE`, or a bare `KEY` to be prompted for the value (kept off shell history)"
1498
+ "persist generic credentials forwarded to the server \u2014 `KEY=VALUE`, or a bare `KEY` to be prompted for the value (kept off shell history)"
1932
1499
  ).option("--unset <keys...>", "remove one or more persisted keys").option("--list", "list persisted keys (values masked)").action(setEnv);
1933
1500
  program.command("down").description("stop a running stack").action(down);
1934
- var bridge = program.command("bridge").description("house-bridge maintenance");
1935
- bridge.command("reset").description("clear the house bridge\u2019s resume state (cursors + dispatched/completed dedupe)").option("--force", "clear even while the bridge is running (takes effect next boot)").action(bridgeReset);
1936
1501
  program.command("status").description("show what is installed + running").action(status);
1937
1502
  program.command("doctor").description("preflight: diagnose common self-host failure classes (runs with the stack down)").action(doctor);
1938
- program.command("logs").description("tail the server + bridge logs").option("-n, --lines <n>", "how many lines to show (default 80)", (v) => Number(v)).option("-f, --follow", "follow the logs (Ctrl-C to stop)").option("--server", "only the server log").option("--bridge", "only the bridge log").action(logs);
1503
+ program.command("logs").description("tail the server log").option("-n, --lines <n>", "how many lines to show (default 80)", (v) => Number(v)).option("-f, --follow", "follow the logs (Ctrl-C to stop)").action(logs);
1939
1504
  program.parseAsync(process.argv).catch((err) => {
1940
1505
  error(err instanceof Error ? err.message : String(err));
1941
1506
  process.exitCode = 1;