@cabane/cli 0.1.3 → 0.1.5
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 +104 -92
- package/dist/cli.js +284 -730
- package/package.json +4 -5
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,112 @@ 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 agentName = await defaultAgentName(client);
|
|
1033
|
+
ok("setup complete");
|
|
1034
|
+
info("");
|
|
1035
|
+
info(`Your agent (@${agentName}) has no executor yet \u2014 agents run on a companion you pair,`);
|
|
1036
|
+
info("the same way they do on hosted Cabane. To bring it to life:");
|
|
1037
|
+
info(" 1. Install the Companion \u2014 the desktop app, or the headless `@cabane/companion`.");
|
|
1038
|
+
info(` 2. Sign in against this instance (${instanceUrl}) and pair the device.`);
|
|
1039
|
+
info(` 3. Assign @${agentName} to one of its connectors, then message it in the app.`);
|
|
1040
|
+
}
|
|
1041
|
+
async function healthy(port) {
|
|
1042
|
+
try {
|
|
1043
|
+
return (await fetch(`http://127.0.0.1:${port}/healthz`)).ok;
|
|
1044
|
+
} catch {
|
|
1045
|
+
return false;
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
async function resolvePat(opts, base) {
|
|
1049
|
+
if (opts.token) return opts.token.trim();
|
|
1050
|
+
if (isInteractive()) {
|
|
1051
|
+
info(`mint a token in the app: ${base} \u2192 Settings \u2192 Developer \u2192 tokens`);
|
|
1052
|
+
const t = await promptSecret("Paste a Cabane access token (cab_\u2026): ");
|
|
1053
|
+
if (t) return t;
|
|
1054
|
+
}
|
|
1055
|
+
throw new Error("no access token. Pass --token <cab_\u2026> (mint it in Settings \u2192 Developer).");
|
|
1056
|
+
}
|
|
1057
|
+
async function defaultAgentName(client) {
|
|
1058
|
+
try {
|
|
1059
|
+
const { workspaces } = await client.get("/api/workspaces");
|
|
1060
|
+
const workspace = workspaces.find((w) => w.role === "owner") ?? workspaces[0];
|
|
1061
|
+
if (!workspace) return "cabane";
|
|
1062
|
+
const { agents } = await client.get(
|
|
1063
|
+
`/api/workspaces/${workspace.id}/agents`
|
|
1249
1064
|
);
|
|
1065
|
+
const agent = workspace.defaultAgentId && agents.find((a) => a.id === workspace.defaultAgentId) || agents.find((a) => a.username === "cabane") || agents[0];
|
|
1066
|
+
return agent?.username ?? "cabane";
|
|
1067
|
+
} catch {
|
|
1068
|
+
return "cabane";
|
|
1250
1069
|
}
|
|
1251
|
-
return join8(dirname3(pkgPath), rel);
|
|
1252
1070
|
}
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1071
|
+
|
|
1072
|
+
// src/commands/status.ts
|
|
1073
|
+
import { readFileSync as readFileSync5 } from "fs";
|
|
1074
|
+
async function status() {
|
|
1075
|
+
const config = loadConfig();
|
|
1076
|
+
const paths = pathsFor(config);
|
|
1077
|
+
const state = readState2(paths.stateFile);
|
|
1078
|
+
const supervisorPid = readSupervisorPid(paths.supervisorPidFile) ?? state.supervisorPid;
|
|
1079
|
+
const supervisorUp = supervisorPid ? pidAlive(supervisorPid) : false;
|
|
1080
|
+
const serverUp = await healthy2(config.port);
|
|
1081
|
+
const line = (label, value) => {
|
|
1082
|
+
process.stdout.write(` ${label.padEnd(12)}${value}
|
|
1083
|
+
`);
|
|
1260
1084
|
};
|
|
1261
|
-
|
|
1085
|
+
process.stdout.write("cabane\n");
|
|
1086
|
+
line("version", config.currentVersion ?? "(not installed)");
|
|
1087
|
+
if (config.previousVersion) line("previous", config.previousVersion);
|
|
1088
|
+
line("port", String(config.port));
|
|
1089
|
+
line("home", paths.root);
|
|
1090
|
+
line("data", `${paths.dataDir} (pg + storage \xB7 kept across upgrades)`);
|
|
1091
|
+
line("supervisor", supervisorUp ? `up (pid ${supervisorPid})` : "down");
|
|
1092
|
+
line("server", serverUp ? `up \xB7 http://localhost:${config.port}` : "down");
|
|
1093
|
+
if (!supervisorUp) info("start it: cabane up");
|
|
1262
1094
|
}
|
|
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 });
|
|
1095
|
+
function readState2(file) {
|
|
1268
1096
|
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 });
|
|
1097
|
+
return JSON.parse(readFileSync5(file, "utf8"));
|
|
1098
|
+
} catch {
|
|
1099
|
+
return {};
|
|
1278
1100
|
}
|
|
1279
|
-
ok("bridge paired");
|
|
1280
1101
|
}
|
|
1281
|
-
function
|
|
1282
|
-
return join8(bridgeHome, ".cabane", "config.json");
|
|
1283
|
-
}
|
|
1284
|
-
function readBridgeConfig(bridgeHome) {
|
|
1102
|
+
function readSupervisorPid(file) {
|
|
1285
1103
|
try {
|
|
1286
|
-
|
|
1104
|
+
const pid = Number(readFileSync5(file, "utf8").trim());
|
|
1105
|
+
return pid > 0 ? pid : null;
|
|
1287
1106
|
} catch {
|
|
1288
1107
|
return null;
|
|
1289
1108
|
}
|
|
1290
1109
|
}
|
|
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
|
-
});
|
|
1110
|
+
async function healthy2(port) {
|
|
1111
|
+
try {
|
|
1112
|
+
return (await fetch(`http://127.0.0.1:${port}/healthz`)).ok;
|
|
1113
|
+
} catch {
|
|
1114
|
+
return false;
|
|
1343
1115
|
}
|
|
1344
|
-
}
|
|
1116
|
+
}
|
|
1345
1117
|
|
|
1346
1118
|
// src/commands/up.ts
|
|
1347
|
-
import { spawn as
|
|
1348
|
-
import { existsSync as
|
|
1349
|
-
import { join as
|
|
1119
|
+
import { spawn as spawn3 } from "child_process";
|
|
1120
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync4, openSync as openSync2, readFileSync as readFileSync6, rmSync as rmSync4, writeFileSync as writeFileSync3 } from "fs";
|
|
1121
|
+
import { join as join8 } from "path";
|
|
1350
1122
|
|
|
1351
1123
|
// src/supervisor/postgres.ts
|
|
1352
|
-
import { existsSync as
|
|
1353
|
-
import { join as
|
|
1124
|
+
import { existsSync as existsSync7 } from "fs";
|
|
1125
|
+
import { join as join7 } from "path";
|
|
1354
1126
|
import EmbeddedPostgres from "embedded-postgres";
|
|
1355
1127
|
var EmbeddedPg = class {
|
|
1356
1128
|
constructor(dataDir, port) {
|
|
@@ -1373,11 +1145,11 @@ var EmbeddedPg = class {
|
|
|
1373
1145
|
onLog: () => {
|
|
1374
1146
|
},
|
|
1375
1147
|
// quiet — pg writes its own logs under the data dir
|
|
1376
|
-
onError: (err) =>
|
|
1148
|
+
onError: (err) => warn2(`[pg] ${err instanceof Error ? err.message : String(err)}`)
|
|
1377
1149
|
});
|
|
1378
1150
|
}
|
|
1379
1151
|
initialised() {
|
|
1380
|
-
return
|
|
1152
|
+
return existsSync7(join7(this.dataDir, "PG_VERSION"));
|
|
1381
1153
|
}
|
|
1382
1154
|
/** Init (first run) + start the postmaster + ensure the app database exists. */
|
|
1383
1155
|
async start() {
|
|
@@ -1405,8 +1177,8 @@ var EmbeddedPg = class {
|
|
|
1405
1177
|
};
|
|
1406
1178
|
|
|
1407
1179
|
// src/supervisor/server.ts
|
|
1408
|
-
import { openSync
|
|
1409
|
-
import { spawn as
|
|
1180
|
+
import { openSync } from "fs";
|
|
1181
|
+
import { spawn as spawn2 } from "child_process";
|
|
1410
1182
|
var ServerChild = class {
|
|
1411
1183
|
constructor(appDir, env, port, logFile) {
|
|
1412
1184
|
this.appDir = appDir;
|
|
@@ -1439,15 +1211,15 @@ var ServerChild = class {
|
|
|
1439
1211
|
}
|
|
1440
1212
|
/** Spawn `node server.js` and wait for /healthz. */
|
|
1441
1213
|
async start() {
|
|
1442
|
-
const out =
|
|
1443
|
-
this.child =
|
|
1214
|
+
const out = openSync(this.logFile, "a");
|
|
1215
|
+
this.child = spawn2("node", ["server.js"], {
|
|
1444
1216
|
cwd: this.appDir,
|
|
1445
1217
|
env: this.childEnv(),
|
|
1446
1218
|
stdio: ["ignore", out, out]
|
|
1447
1219
|
});
|
|
1448
1220
|
this.child.on("exit", (code, signal) => {
|
|
1449
1221
|
if (code && code !== 0)
|
|
1450
|
-
|
|
1222
|
+
warn2(`server exited (code ${code}${signal ? `, ${signal}` : ""})`);
|
|
1451
1223
|
this.child = null;
|
|
1452
1224
|
});
|
|
1453
1225
|
const healthy3 = await waitUntil(() => this.healthy(), { timeoutMs: 45e3, intervalMs: 500 });
|
|
@@ -1512,18 +1284,18 @@ async function up(opts) {
|
|
|
1512
1284
|
await new Supervisor(paths, appDir, config.port).run();
|
|
1513
1285
|
}
|
|
1514
1286
|
async function launchDaemon(paths, config) {
|
|
1515
|
-
if (
|
|
1516
|
-
const pid = Number(
|
|
1287
|
+
if (existsSync8(paths.supervisorPidFile)) {
|
|
1288
|
+
const pid = Number(readFileSync6(paths.supervisorPidFile, "utf8").trim());
|
|
1517
1289
|
if (pid && pidAlive(pid)) {
|
|
1518
|
-
|
|
1290
|
+
warn2(`already running (supervisor pid ${pid}). Use \`cabane down\` to stop it.`);
|
|
1519
1291
|
return;
|
|
1520
1292
|
}
|
|
1521
1293
|
}
|
|
1522
|
-
|
|
1294
|
+
mkdirSync4(paths.logsDir, { recursive: true });
|
|
1523
1295
|
const entry = process.argv[1];
|
|
1524
1296
|
if (!entry) throw new Error("cannot locate the cabane CLI entry to run in the background.");
|
|
1525
|
-
const out =
|
|
1526
|
-
const child =
|
|
1297
|
+
const out = openSync2(paths.serverLog, "a");
|
|
1298
|
+
const child = spawn3(process.execPath, [entry, "up", "--port", String(config.port)], {
|
|
1527
1299
|
detached: true,
|
|
1528
1300
|
stdio: ["ignore", out, out]
|
|
1529
1301
|
});
|
|
@@ -1544,18 +1316,13 @@ async function launchDaemon(paths, config) {
|
|
|
1544
1316
|
throw new Error(`background start did not come healthy within 60s \u2014 see ${paths.serverLog}`);
|
|
1545
1317
|
}
|
|
1546
1318
|
ok(`running in the background on http://localhost:${config.port}`);
|
|
1547
|
-
for (const line of daemonFooter(
|
|
1319
|
+
for (const line of daemonFooter()) info(line);
|
|
1548
1320
|
}
|
|
1549
|
-
function daemonFooter(
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
lines.push("logs: cabane logs \xB7 stop: cabane down");
|
|
1555
|
-
return lines;
|
|
1556
|
-
}
|
|
1557
|
-
function resolveClaudeConfigDir(env = process.env) {
|
|
1558
|
-
return env.CLAUDE_CONFIG_DIR;
|
|
1321
|
+
function daemonFooter() {
|
|
1322
|
+
return [
|
|
1323
|
+
"next: open the URL, sign up, then pair a companion so your agents can answer \u2014 `cabane setup-agent` explains how",
|
|
1324
|
+
"logs: cabane logs \xB7 stop: cabane down"
|
|
1325
|
+
];
|
|
1559
1326
|
}
|
|
1560
1327
|
var Supervisor = class {
|
|
1561
1328
|
constructor(paths, appDir, port) {
|
|
@@ -1569,7 +1336,6 @@ var Supervisor = class {
|
|
|
1569
1336
|
port;
|
|
1570
1337
|
pg;
|
|
1571
1338
|
server = null;
|
|
1572
|
-
bridge = null;
|
|
1573
1339
|
shuttingDown = false;
|
|
1574
1340
|
buildServer() {
|
|
1575
1341
|
const config = loadConfig();
|
|
@@ -1580,9 +1346,11 @@ var Supervisor = class {
|
|
|
1580
1346
|
storageRoot: this.paths.storageRoot,
|
|
1581
1347
|
visionSecret: config.visionSecret,
|
|
1582
1348
|
...config.publicUrl ? { publicBaseUrl: config.publicUrl } : {},
|
|
1583
|
-
|
|
1584
|
-
//
|
|
1585
|
-
|
|
1349
|
+
// Persisted `cabane set-env` creds (generic server env) are layered beneath
|
|
1350
|
+
// the composed infra vars.
|
|
1351
|
+
...config.companionEnv ? { persistedEnv: config.companionEnv } : {},
|
|
1352
|
+
// CT484: the artifact's build version → the server reports it on the companion
|
|
1353
|
+
// heartbeat so a companion/server skew is detectable. Read from the installed
|
|
1586
1354
|
// tree's package.json (stamped at build); best-effort.
|
|
1587
1355
|
...(() => {
|
|
1588
1356
|
const v = readAppVersion(this.appDir);
|
|
@@ -1591,51 +1359,25 @@ var Supervisor = class {
|
|
|
1591
1359
|
});
|
|
1592
1360
|
return new ServerChild(this.appDir, env, this.port, this.paths.serverLog);
|
|
1593
1361
|
}
|
|
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
1362
|
/** Publish the supervisor pid as early as possible so `status` / `down` /
|
|
1620
1363
|
* `setup-agent` can find us the instant `up --daemon` returns (the daemon
|
|
1621
1364
|
* launcher only waits on /healthz, which comes up before the children are all
|
|
1622
1365
|
* wired). */
|
|
1623
1366
|
writePid() {
|
|
1624
|
-
|
|
1625
|
-
|
|
1367
|
+
mkdirSync4(this.paths.runDir, { recursive: true });
|
|
1368
|
+
writeFileSync3(this.paths.supervisorPidFile, `${process.pid}
|
|
1626
1369
|
`);
|
|
1627
1370
|
}
|
|
1628
1371
|
writeState() {
|
|
1629
|
-
|
|
1630
|
-
|
|
1372
|
+
mkdirSync4(this.paths.runDir, { recursive: true });
|
|
1373
|
+
writeFileSync3(this.paths.supervisorPidFile, `${process.pid}
|
|
1631
1374
|
`);
|
|
1632
|
-
|
|
1375
|
+
writeFileSync3(
|
|
1633
1376
|
this.paths.stateFile,
|
|
1634
1377
|
JSON.stringify(
|
|
1635
1378
|
{
|
|
1636
1379
|
supervisorPid: process.pid,
|
|
1637
1380
|
serverPid: this.server?.pid ?? null,
|
|
1638
|
-
bridgePid: this.bridge?.pid ?? null,
|
|
1639
1381
|
port: this.port,
|
|
1640
1382
|
// Bumped on every (re)write — a completed reload advances it, which is
|
|
1641
1383
|
// how `setup-agent` knows a restart it requested actually finished
|
|
@@ -1648,8 +1390,8 @@ var Supervisor = class {
|
|
|
1648
1390
|
);
|
|
1649
1391
|
}
|
|
1650
1392
|
async run() {
|
|
1651
|
-
|
|
1652
|
-
|
|
1393
|
+
mkdirSync4(this.paths.logsDir, { recursive: true });
|
|
1394
|
+
mkdirSync4(this.paths.storageRoot, { recursive: true });
|
|
1653
1395
|
this.writePid();
|
|
1654
1396
|
step("starting embedded Postgres\u2026");
|
|
1655
1397
|
await this.pg.start();
|
|
@@ -1657,27 +1399,25 @@ var Supervisor = class {
|
|
|
1657
1399
|
step("starting server\u2026");
|
|
1658
1400
|
await this.server.migrate();
|
|
1659
1401
|
await this.server.start();
|
|
1660
|
-
this.ensureBridge();
|
|
1661
1402
|
this.writeState();
|
|
1662
1403
|
process.on("SIGINT", () => void this.teardown("SIGINT"));
|
|
1663
1404
|
process.on("SIGTERM", () => void this.teardown("SIGTERM"));
|
|
1664
1405
|
process.on("exit", () => {
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
}
|
|
1406
|
+
const pid = this.server?.pid;
|
|
1407
|
+
if (pid) {
|
|
1408
|
+
try {
|
|
1409
|
+
process.kill(pid, "SIGKILL");
|
|
1410
|
+
} catch {
|
|
1671
1411
|
}
|
|
1672
1412
|
}
|
|
1673
1413
|
});
|
|
1674
1414
|
process.on(
|
|
1675
1415
|
"uncaughtException",
|
|
1676
|
-
(err) =>
|
|
1416
|
+
(err) => warn2(`ignoring uncaught error: ${err instanceof Error ? err.message : String(err)}`)
|
|
1677
1417
|
);
|
|
1678
1418
|
process.on(
|
|
1679
1419
|
"unhandledRejection",
|
|
1680
|
-
(err) =>
|
|
1420
|
+
(err) => warn2(`ignoring unhandled rejection: ${err instanceof Error ? err.message : String(err)}`)
|
|
1681
1421
|
);
|
|
1682
1422
|
ok("cabane is up \u2014 Ctrl-C to stop");
|
|
1683
1423
|
await new Promise(() => {
|
|
@@ -1687,207 +1427,23 @@ var Supervisor = class {
|
|
|
1687
1427
|
if (this.shuttingDown) return;
|
|
1688
1428
|
this.shuttingDown = true;
|
|
1689
1429
|
step(`${signal} \u2014 stopping children\u2026`);
|
|
1690
|
-
await this.bridge?.stop();
|
|
1691
1430
|
await this.server?.stop();
|
|
1692
1431
|
await this.pg.stop();
|
|
1693
|
-
|
|
1694
|
-
|
|
1432
|
+
rmSync4(this.paths.supervisorPidFile, { force: true });
|
|
1433
|
+
rmSync4(this.paths.stateFile, { force: true });
|
|
1695
1434
|
ok("stopped cleanly");
|
|
1696
1435
|
process.exit(0);
|
|
1697
1436
|
}
|
|
1698
1437
|
};
|
|
1699
1438
|
function readAppVersion(appDir) {
|
|
1700
1439
|
try {
|
|
1701
|
-
const pkg = JSON.parse(
|
|
1440
|
+
const pkg = JSON.parse(readFileSync6(join8(appDir, "package.json"), "utf8"));
|
|
1702
1441
|
return pkg.version ?? null;
|
|
1703
1442
|
} catch {
|
|
1704
1443
|
return null;
|
|
1705
1444
|
}
|
|
1706
1445
|
}
|
|
1707
1446
|
|
|
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
1447
|
// src/commands/update.ts
|
|
1892
1448
|
async function update(opts) {
|
|
1893
1449
|
const config = loadConfig();
|
|
@@ -1922,20 +1478,18 @@ var parsePort = (raw) => {
|
|
|
1922
1478
|
if (!Number.isInteger(n) || n <= 0 || n > 65535) throw new Error(`invalid port: ${raw}`);
|
|
1923
1479
|
return n;
|
|
1924
1480
|
};
|
|
1925
|
-
program.command("init").description("interactive first-run wizard: data location, port, and
|
|
1481
|
+
program.command("init").description("interactive first-run wizard: data location, port, and public URL").action(init);
|
|
1926
1482
|
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:
|
|
1483
|
+
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);
|
|
1484
|
+
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
1485
|
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
1486
|
program.command("set-env [pairs...]").description(
|
|
1931
|
-
"persist credentials forwarded to the
|
|
1487
|
+
"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
1488
|
).option("--unset <keys...>", "remove one or more persisted keys").option("--list", "list persisted keys (values masked)").action(setEnv);
|
|
1933
1489
|
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
1490
|
program.command("status").description("show what is installed + running").action(status);
|
|
1937
1491
|
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
|
|
1492
|
+
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
1493
|
program.parseAsync(process.argv).catch((err) => {
|
|
1940
1494
|
error(err instanceof Error ? err.message : String(err));
|
|
1941
1495
|
process.exitCode = 1;
|