@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.
- package/README.md +74 -87
- package/dist/cli.js +295 -730
- 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/
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
6
|
+
// src/commands/doctor.ts
|
|
7
|
+
import { writeSync } from "fs";
|
|
8
|
+
import { homedir as homedir2 } from "os";
|
|
9
9
|
|
|
10
|
-
// src/
|
|
11
|
-
|
|
12
|
-
|
|
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 "
|
|
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
|
|
88
|
-
if (classifyHome(root) !== "
|
|
60
|
+
function assertNotForeignCompanion(root = cabaneHome()) {
|
|
61
|
+
if (classifyHome(root) !== "companion") return;
|
|
89
62
|
throw new Error(
|
|
90
|
-
`${root} holds a standalone @cabane/
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
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 =
|
|
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 =
|
|
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 (!
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 (!
|
|
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(
|
|
614
|
-
writeFileSync2(
|
|
615
|
-
|
|
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
|
|
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}${
|
|
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.
|
|
555
|
+
state.companionPid,
|
|
739
556
|
state.serverPid,
|
|
740
|
-
readPid(
|
|
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
|
-
|
|
749
|
-
|
|
565
|
+
rmSync2(paths.supervisorPidFile, { force: true });
|
|
566
|
+
rmSync2(paths.stateFile, { force: true });
|
|
750
567
|
if (free) ok("stopped");
|
|
751
|
-
else
|
|
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(
|
|
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(
|
|
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
|
|
601
|
+
import { existsSync as existsSync4 } from "fs";
|
|
785
602
|
import { homedir as homedir3 } from "os";
|
|
786
|
-
import { isAbsolute, join as
|
|
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
|
|
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
|
|
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
|
-
|
|
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 =
|
|
873
|
-
if (oldPg !== newPg &&
|
|
874
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
744
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync3, rmSync as rmSync3 } from "fs";
|
|
945
745
|
import { tmpdir } from "os";
|
|
946
|
-
import { join as
|
|
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
|
-
|
|
797
|
+
assertNotForeignCompanion();
|
|
998
798
|
const config = loadConfig();
|
|
999
799
|
const paths = pathsFor(config);
|
|
1000
|
-
const dataExisted =
|
|
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 (!
|
|
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 =
|
|
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 = () =>
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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",
|
|
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
|
|
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().
|
|
894
|
+
const env = loadConfig().companionEnv ?? {};
|
|
1099
895
|
const keys = Object.keys(env);
|
|
1100
896
|
if (keys.length === 0) {
|
|
1101
|
-
info("no persisted
|
|
897
|
+
info("no persisted server env. Add one: `cabane set-env KEY=VALUE`");
|
|
1102
898
|
return;
|
|
1103
899
|
}
|
|
1104
|
-
step("persisted
|
|
1105
|
-
for (const key of keys) info(`${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
|
-
|
|
908
|
+
assertNotForeignCompanion();
|
|
1113
909
|
if (opts.unset?.length) {
|
|
1114
910
|
updateConfig((config) => {
|
|
1115
|
-
if (!config.
|
|
1116
|
-
for (const key of opts.unset) delete config.
|
|
1117
|
-
if (Object.keys(config.
|
|
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.
|
|
940
|
+
config.companionEnv = { ...config.companionEnv ?? {}, ...parsed };
|
|
1145
941
|
});
|
|
1146
|
-
for (const key of Object.keys(parsed)) ok(`set ${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
|
|
992
|
+
import { join as join6 } from "path";
|
|
1200
993
|
function composeServerEnv(opts) {
|
|
1201
|
-
const dataDir =
|
|
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:
|
|
1213
|
-
CABANE_SKILLS_DIR:
|
|
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/
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
const
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
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
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
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
|
|
1282
|
-
return join8(bridgeHome, ".cabane", "config.json");
|
|
1283
|
-
}
|
|
1284
|
-
function readBridgeConfig(bridgeHome) {
|
|
1113
|
+
function readSupervisorPid(file) {
|
|
1285
1114
|
try {
|
|
1286
|
-
|
|
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
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
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
|
|
1348
|
-
import { existsSync as
|
|
1349
|
-
import { join as
|
|
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
|
|
1353
|
-
import { join as
|
|
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) =>
|
|
1159
|
+
onError: (err) => warn2(`[pg] ${err instanceof Error ? err.message : String(err)}`)
|
|
1377
1160
|
});
|
|
1378
1161
|
}
|
|
1379
1162
|
initialised() {
|
|
1380
|
-
return
|
|
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
|
|
1409
|
-
import { spawn as
|
|
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 =
|
|
1443
|
-
this.child =
|
|
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
|
-
|
|
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 (
|
|
1516
|
-
const pid = Number(
|
|
1298
|
+
if (existsSync8(paths.supervisorPidFile)) {
|
|
1299
|
+
const pid = Number(readFileSync6(paths.supervisorPidFile, "utf8").trim());
|
|
1517
1300
|
if (pid && pidAlive(pid)) {
|
|
1518
|
-
|
|
1301
|
+
warn2(`already running (supervisor pid ${pid}). Use \`cabane down\` to stop it.`);
|
|
1519
1302
|
return;
|
|
1520
1303
|
}
|
|
1521
1304
|
}
|
|
1522
|
-
|
|
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 =
|
|
1526
|
-
const child =
|
|
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(
|
|
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
|
|
1558
|
-
return
|
|
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
|
-
|
|
1584
|
-
//
|
|
1585
|
-
|
|
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
|
-
|
|
1625
|
-
|
|
1378
|
+
mkdirSync4(this.paths.runDir, { recursive: true });
|
|
1379
|
+
writeFileSync3(this.paths.supervisorPidFile, `${process.pid}
|
|
1626
1380
|
`);
|
|
1627
1381
|
}
|
|
1628
1382
|
writeState() {
|
|
1629
|
-
|
|
1630
|
-
|
|
1383
|
+
mkdirSync4(this.paths.runDir, { recursive: true });
|
|
1384
|
+
writeFileSync3(this.paths.supervisorPidFile, `${process.pid}
|
|
1631
1385
|
`);
|
|
1632
|
-
|
|
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
|
-
|
|
1652
|
-
|
|
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
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
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) =>
|
|
1427
|
+
(err) => warn2(`ignoring uncaught error: ${err instanceof Error ? err.message : String(err)}`)
|
|
1677
1428
|
);
|
|
1678
1429
|
process.on(
|
|
1679
1430
|
"unhandledRejection",
|
|
1680
|
-
(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
|
-
|
|
1694
|
-
|
|
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(
|
|
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
|
|
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
|
|
1928
|
-
program.command("setup-agent").description("after browser signup:
|
|
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
|
|
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
|
|
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;
|