@cabane/companion 0.6.156 → 0.6.157
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 +7 -109
- package/dist/bin.js +2 -1
- package/dist/boot-guard.js +112 -0
- package/dist/cli.js +1633 -193
- package/dist/launch.js +411 -0
- package/dist/pairing-config.js +9 -0
- package/dist/runtime.js +755 -31
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -692,16 +692,16 @@ function migrateConnectedHarnesses(cfg) {
|
|
|
692
692
|
}
|
|
693
693
|
function localAgentConfig(cfg, agent) {
|
|
694
694
|
const map = cfg.agents ?? {};
|
|
695
|
-
const
|
|
696
|
-
if (!cfg.prepareHook ||
|
|
697
|
-
return { ...
|
|
695
|
+
const row3 = map[agent.agentId] ?? map[agent.agentUsername] ?? map[`${agent.workspaceSlug}/${agent.agentUsername}`] ?? {};
|
|
696
|
+
if (!cfg.prepareHook || row3.prepareHook) return row3;
|
|
697
|
+
return { ...row3, prepareHook: cfg.prepareHook };
|
|
698
698
|
}
|
|
699
699
|
function agentSandbox(cfg, agent) {
|
|
700
700
|
const sandbox = cfg.agents?.[agent.agentId]?.sandbox;
|
|
701
701
|
return sandbox ? { sandbox } : {};
|
|
702
702
|
}
|
|
703
|
-
function runnerSandboxFor(cfg,
|
|
704
|
-
const asked =
|
|
703
|
+
function runnerSandboxFor(cfg, row3, host) {
|
|
704
|
+
const asked = row3.sandbox ?? cfg.runners?.sandbox ?? "none";
|
|
705
705
|
if (asked !== "os" || cfg.runners?.provider === "docker") return "none";
|
|
706
706
|
return host.osSandboxUsable() ? "os" : "none";
|
|
707
707
|
}
|
|
@@ -715,7 +715,7 @@ function runnerSandboxReport(cfg, agents, host) {
|
|
|
715
715
|
}
|
|
716
716
|
function agentSandboxRows(cfg) {
|
|
717
717
|
return Object.entries(cfg.agents ?? {}).flatMap(
|
|
718
|
-
([key,
|
|
718
|
+
([key, row3]) => row3.sandbox ? [[key, row3.sandbox]] : []
|
|
719
719
|
);
|
|
720
720
|
}
|
|
721
721
|
function loadConfig() {
|
|
@@ -897,8 +897,8 @@ var init_config = __esm({
|
|
|
897
897
|
}).strict();
|
|
898
898
|
AGENT_ID_KEY = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
899
899
|
agentsConfigSchema = z2.record(z2.string(), localAgentConfigSchema).superRefine((agents, ctx2) => {
|
|
900
|
-
for (const [key,
|
|
901
|
-
if (
|
|
900
|
+
for (const [key, row3] of Object.entries(agents)) {
|
|
901
|
+
if (row3.sandbox === void 0) continue;
|
|
902
902
|
if (AGENT_ID_KEY.test(key)) continue;
|
|
903
903
|
ctx2.addIssue({
|
|
904
904
|
code: z2.ZodIssueCode.custom,
|
|
@@ -965,6 +965,15 @@ var init_config = __esm({
|
|
|
965
965
|
// absent `claudeCode` block means "never offered"; without it, the block's
|
|
966
966
|
// absence marks a config from before connectors were chosen.
|
|
967
967
|
harnessesChosen: z2.boolean().optional(),
|
|
968
|
+
// Whether this companion keeps itself current: installs the version Cabane names, between
|
|
969
|
+
// turns, and rolls back one that doesn't come up (`updater.ts`). Absent means never asked,
|
|
970
|
+
// which `start` offers; off either way until someone says yes.
|
|
971
|
+
update: z2.object({
|
|
972
|
+
auto: z2.boolean()
|
|
973
|
+
}).strict().optional(),
|
|
974
|
+
// The answer to `start`'s "start at boot?" when it was no, so it isn't asked again.
|
|
975
|
+
// `cabane service install` still installs it.
|
|
976
|
+
serviceDeclined: z2.boolean().optional(),
|
|
968
977
|
// OpenCode's decline. Its block is an address, so a decline can't live there
|
|
969
978
|
// the way `claudeCode`/`codex` `enabled: false` does; without it a declined
|
|
970
979
|
// serve would be offered again on every `start`.
|
|
@@ -2682,8 +2691,8 @@ var init_policy = __esm({
|
|
|
2682
2691
|
// packages/agent-runtime/src/claude-code/model.ts
|
|
2683
2692
|
function parseClaudeCodeModel(model) {
|
|
2684
2693
|
if (!model) return null;
|
|
2685
|
-
const
|
|
2686
|
-
const bare =
|
|
2694
|
+
const sep6 = model.indexOf("/");
|
|
2695
|
+
const bare = sep6 === -1 || model.slice(0, sep6) !== "anthropic" ? model : model.slice(sep6 + 1);
|
|
2687
2696
|
return bare === CLAUDE_CODE_DEFAULT_MODEL_BARE ? null : model;
|
|
2688
2697
|
}
|
|
2689
2698
|
var CLAUDE_CODE_DEFAULT_MODEL_BARE;
|
|
@@ -3151,10 +3160,10 @@ function parseContextUsage(response) {
|
|
|
3151
3160
|
if (total === void 0 || window === void 0 || window === 0) return void 0;
|
|
3152
3161
|
const categories = [];
|
|
3153
3162
|
for (const c of Array.isArray(r.categories) ? r.categories : []) {
|
|
3154
|
-
const
|
|
3155
|
-
const tokens = count(
|
|
3156
|
-
if (typeof
|
|
3157
|
-
categories.push({ name:
|
|
3163
|
+
const row3 = c;
|
|
3164
|
+
const tokens = count(row3?.tokens);
|
|
3165
|
+
if (typeof row3?.name === "string" && tokens !== void 0 && typeof row3.kind === "string" && CATEGORY_KINDS.has(row3.kind)) {
|
|
3166
|
+
categories.push({ name: row3.name, tokens, kind: row3.kind });
|
|
3158
3167
|
}
|
|
3159
3168
|
}
|
|
3160
3169
|
const buffer = categories.filter((c) => c.kind === "buffer").reduce((sum, c) => sum + c.tokens, 0);
|
|
@@ -4236,9 +4245,9 @@ var init_policy2 = __esm({
|
|
|
4236
4245
|
|
|
4237
4246
|
// packages/agent-runtime/src/opencode/model.ts
|
|
4238
4247
|
function parseOpencodeModel(model) {
|
|
4239
|
-
const
|
|
4240
|
-
if (
|
|
4241
|
-
return { providerID: model.slice(0,
|
|
4248
|
+
const sep6 = model.indexOf("/");
|
|
4249
|
+
if (sep6 === -1) return { providerID: "", modelID: model };
|
|
4250
|
+
return { providerID: model.slice(0, sep6), modelID: model.slice(sep6 + 1) };
|
|
4242
4251
|
}
|
|
4243
4252
|
var init_model2 = __esm({
|
|
4244
4253
|
"packages/agent-runtime/src/opencode/model.ts"() {
|
|
@@ -4539,10 +4548,10 @@ async function* parseSseStream(body, sessionId, signal) {
|
|
|
4539
4548
|
const { value, done } = await reader.read();
|
|
4540
4549
|
if (done) return;
|
|
4541
4550
|
buf += decoder.decode(value, { stream: true });
|
|
4542
|
-
let
|
|
4543
|
-
while ((
|
|
4544
|
-
const frame = buf.slice(0,
|
|
4545
|
-
buf = buf.slice(
|
|
4551
|
+
let sep6;
|
|
4552
|
+
while ((sep6 = buf.indexOf("\n\n")) !== -1) {
|
|
4553
|
+
const frame = buf.slice(0, sep6);
|
|
4554
|
+
buf = buf.slice(sep6 + 2);
|
|
4546
4555
|
const data = frame.split("\n").filter((l) => l.startsWith("data:")).map((l) => l.slice(5).trim()).join("");
|
|
4547
4556
|
if (!data) continue;
|
|
4548
4557
|
let ev;
|
|
@@ -5553,8 +5562,8 @@ var init_policy3 = __esm({
|
|
|
5553
5562
|
|
|
5554
5563
|
// packages/agent-runtime/src/codex/model.ts
|
|
5555
5564
|
function parseCodexModel(model) {
|
|
5556
|
-
const
|
|
5557
|
-
const bare =
|
|
5565
|
+
const sep6 = model.indexOf("/");
|
|
5566
|
+
const bare = sep6 === -1 || model.slice(0, sep6) !== "openai" ? model : model.slice(sep6 + 1);
|
|
5558
5567
|
return CODEX_DEFAULT_MODEL_BARES.has(bare) ? null : bare;
|
|
5559
5568
|
}
|
|
5560
5569
|
var CODEX_DEFAULT_MODEL_BARES;
|
|
@@ -10249,7 +10258,7 @@ var init_turn_execution = __esm({
|
|
|
10249
10258
|
|
|
10250
10259
|
// src/dispatcher.ts
|
|
10251
10260
|
import { randomUUID as randomUUID4 } from "crypto";
|
|
10252
|
-
import { existsSync as existsSync11, readdirSync as readdirSync2, statSync as
|
|
10261
|
+
import { existsSync as existsSync11, readdirSync as readdirSync2, statSync as statSync3 } from "fs";
|
|
10253
10262
|
import { join as join17 } from "path";
|
|
10254
10263
|
function checkoutState(cwd) {
|
|
10255
10264
|
if (!cwd) return { ok: false, reason: "no working directory was resolved for this turn" };
|
|
@@ -10267,7 +10276,7 @@ function checkoutState(cwd) {
|
|
|
10267
10276
|
if (!existsSync11(gitPath)) return { ok: true, reason: "usable" };
|
|
10268
10277
|
let stat;
|
|
10269
10278
|
try {
|
|
10270
|
-
stat =
|
|
10279
|
+
stat = statSync3(gitPath);
|
|
10271
10280
|
} catch (error) {
|
|
10272
10281
|
return { ok: false, reason: `${gitPath} is unreadable (${error.message})` };
|
|
10273
10282
|
}
|
|
@@ -11079,7 +11088,7 @@ import {
|
|
|
11079
11088
|
mkdirSync as mkdirSync12,
|
|
11080
11089
|
readdirSync as readdirSync5,
|
|
11081
11090
|
readFileSync as readFileSync11,
|
|
11082
|
-
renameSync as
|
|
11091
|
+
renameSync as renameSync7,
|
|
11083
11092
|
rmSync as rmSync11,
|
|
11084
11093
|
writeFileSync as writeFileSync8
|
|
11085
11094
|
} from "fs";
|
|
@@ -11116,7 +11125,7 @@ var init_outbox = __esm({
|
|
|
11116
11125
|
const tmp = `${target}.${process.pid}.tmp`;
|
|
11117
11126
|
try {
|
|
11118
11127
|
writeFileSync8(tmp, JSON.stringify(entry) + "\n", "utf8");
|
|
11119
|
-
|
|
11128
|
+
renameSync7(tmp, target);
|
|
11120
11129
|
} catch (err) {
|
|
11121
11130
|
try {
|
|
11122
11131
|
rmSync11(tmp, { force: true });
|
|
@@ -11285,8 +11294,8 @@ var init_image_manifest = __esm({
|
|
|
11285
11294
|
});
|
|
11286
11295
|
|
|
11287
11296
|
// src/runner/exec-shim.ts
|
|
11288
|
-
import { chmodSync as
|
|
11289
|
-
import { join as
|
|
11297
|
+
import { chmodSync as chmodSync8, mkdirSync as mkdirSync20, mkdtempSync as mkdtempSync2, rmSync as rmSync17, writeFileSync as writeFileSync13 } from "fs";
|
|
11298
|
+
import { join as join31 } from "path";
|
|
11290
11299
|
function shellQuote2(value) {
|
|
11291
11300
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
11292
11301
|
}
|
|
@@ -11296,18 +11305,18 @@ function createShimDir() {
|
|
|
11296
11305
|
return {
|
|
11297
11306
|
write(argv) {
|
|
11298
11307
|
if (!dir2) {
|
|
11299
|
-
|
|
11300
|
-
dir2 = mkdtempSync2(
|
|
11308
|
+
mkdirSync20(join31(cabaneDir(), "runner"), { recursive: true });
|
|
11309
|
+
dir2 = mkdtempSync2(join31(cabaneDir(), "runner", "turn-"));
|
|
11301
11310
|
}
|
|
11302
|
-
const path =
|
|
11303
|
-
|
|
11311
|
+
const path = join31(dir2, `exec-${++count}`);
|
|
11312
|
+
writeFileSync13(path, `#!/bin/sh
|
|
11304
11313
|
exec ${argv.map(shellQuote2).join(" ")} "$@"
|
|
11305
11314
|
`);
|
|
11306
|
-
|
|
11315
|
+
chmodSync8(path, 448);
|
|
11307
11316
|
return path;
|
|
11308
11317
|
},
|
|
11309
11318
|
dispose() {
|
|
11310
|
-
if (dir2)
|
|
11319
|
+
if (dir2) rmSync17(dir2, { recursive: true, force: true });
|
|
11311
11320
|
dir2 = null;
|
|
11312
11321
|
}
|
|
11313
11322
|
};
|
|
@@ -12260,6 +12269,10 @@ async function serveConnection(socket, handlers) {
|
|
|
12260
12269
|
reply(socket, result);
|
|
12261
12270
|
return;
|
|
12262
12271
|
}
|
|
12272
|
+
if (req.cmd === "update") {
|
|
12273
|
+
reply(socket, await handlers.update(req.auto));
|
|
12274
|
+
return;
|
|
12275
|
+
}
|
|
12263
12276
|
if (req.cmd === "stop") {
|
|
12264
12277
|
reply(socket, { ok: true });
|
|
12265
12278
|
setTimeout(() => handlers.stop(), 50).unref?.();
|
|
@@ -12885,13 +12898,19 @@ init_term();
|
|
|
12885
12898
|
|
|
12886
12899
|
// src/commands/start.ts
|
|
12887
12900
|
init_config();
|
|
12888
|
-
import { spawn as
|
|
12901
|
+
import { spawn as spawn10 } from "child_process";
|
|
12889
12902
|
import { hostname as hostname3 } from "os";
|
|
12890
12903
|
init_cabane_command();
|
|
12891
12904
|
|
|
12892
12905
|
// src/logger.ts
|
|
12893
12906
|
init_config();
|
|
12894
|
-
import {
|
|
12907
|
+
import {
|
|
12908
|
+
createWriteStream,
|
|
12909
|
+
mkdirSync as mkdirSync5,
|
|
12910
|
+
openSync as openSync2,
|
|
12911
|
+
renameSync as renameSync3,
|
|
12912
|
+
statSync as statSync2
|
|
12913
|
+
} from "fs";
|
|
12895
12914
|
import { dirname as dirname4, join as join8 } from "path";
|
|
12896
12915
|
import { stripVTControlCharacters } from "util";
|
|
12897
12916
|
import pino from "pino";
|
|
@@ -12946,6 +12965,53 @@ function formatHumanLine(record) {
|
|
|
12946
12965
|
}
|
|
12947
12966
|
return line + "\n";
|
|
12948
12967
|
}
|
|
12968
|
+
var LOG_ROTATE_BYTES = 50 * 1024 * 1024;
|
|
12969
|
+
var LOG_ROTATIONS_KEPT = 3;
|
|
12970
|
+
var RotatingLog = class {
|
|
12971
|
+
constructor(path, maxBytes = LOG_ROTATE_BYTES, keep = LOG_ROTATIONS_KEPT) {
|
|
12972
|
+
this.path = path;
|
|
12973
|
+
this.maxBytes = maxBytes;
|
|
12974
|
+
this.keep = keep;
|
|
12975
|
+
this.size = sizeOf(path);
|
|
12976
|
+
this.stream = openAppend(path);
|
|
12977
|
+
}
|
|
12978
|
+
path;
|
|
12979
|
+
maxBytes;
|
|
12980
|
+
keep;
|
|
12981
|
+
stream;
|
|
12982
|
+
size;
|
|
12983
|
+
write(chunk) {
|
|
12984
|
+
const ok = this.stream.write(chunk);
|
|
12985
|
+
this.size += Buffer.byteLength(chunk);
|
|
12986
|
+
if (this.size >= this.maxBytes) this.rotate();
|
|
12987
|
+
return ok;
|
|
12988
|
+
}
|
|
12989
|
+
rotate() {
|
|
12990
|
+
this.stream.end();
|
|
12991
|
+
try {
|
|
12992
|
+
for (let i = this.keep - 1; i >= 1; i--) {
|
|
12993
|
+
try {
|
|
12994
|
+
renameSync3(`${this.path}.${i}`, `${this.path}.${i + 1}`);
|
|
12995
|
+
} catch {
|
|
12996
|
+
}
|
|
12997
|
+
}
|
|
12998
|
+
renameSync3(this.path, `${this.path}.1`);
|
|
12999
|
+
} catch {
|
|
13000
|
+
}
|
|
13001
|
+
this.stream = openAppend(this.path);
|
|
13002
|
+
this.size = sizeOf(this.path);
|
|
13003
|
+
}
|
|
13004
|
+
};
|
|
13005
|
+
function openAppend(path) {
|
|
13006
|
+
return createWriteStream("", { fd: openSync2(path, "a") });
|
|
13007
|
+
}
|
|
13008
|
+
function sizeOf(path) {
|
|
13009
|
+
try {
|
|
13010
|
+
return statSync2(path).size;
|
|
13011
|
+
} catch {
|
|
13012
|
+
return 0;
|
|
13013
|
+
}
|
|
13014
|
+
}
|
|
12949
13015
|
var cached = null;
|
|
12950
13016
|
var consoleLogging = true;
|
|
12951
13017
|
var overrides = {};
|
|
@@ -12964,7 +13030,7 @@ function configureLogger(log, config) {
|
|
|
12964
13030
|
function createLogger(destinations = {}, options = {}) {
|
|
12965
13031
|
const path = companionLogPath();
|
|
12966
13032
|
if (!destinations.file) mkdirSync5(dirname4(path), { recursive: true });
|
|
12967
|
-
const file = destinations.file ??
|
|
13033
|
+
const file = destinations.file ?? new RotatingLog(path);
|
|
12968
13034
|
const state = { format: options.logFormat ?? "human" };
|
|
12969
13035
|
const streams = [];
|
|
12970
13036
|
const human = (chunk) => {
|
|
@@ -13022,7 +13088,7 @@ import { serve } from "@hono/node-server";
|
|
|
13022
13088
|
import { Hono } from "hono";
|
|
13023
13089
|
|
|
13024
13090
|
// src/dashboard/routes.ts
|
|
13025
|
-
import { openSync as
|
|
13091
|
+
import { openSync as openSync3, readSync, closeSync as closeSync2, fstatSync, existsSync as existsSync6 } from "fs";
|
|
13026
13092
|
import { readFile } from "fs/promises";
|
|
13027
13093
|
import { extname, join as join9, normalize } from "path";
|
|
13028
13094
|
import { streamSSE } from "hono/streaming";
|
|
@@ -13406,7 +13472,7 @@ function tailFile(path, lines2) {
|
|
|
13406
13472
|
const MAX_BYTES = 256 * 1024;
|
|
13407
13473
|
let fd;
|
|
13408
13474
|
try {
|
|
13409
|
-
fd =
|
|
13475
|
+
fd = openSync3(path, "r");
|
|
13410
13476
|
const size = fstatSync(fd).size;
|
|
13411
13477
|
const start2 = Math.max(0, size - MAX_BYTES);
|
|
13412
13478
|
const len = size - start2;
|
|
@@ -13657,7 +13723,7 @@ init_config();
|
|
|
13657
13723
|
|
|
13658
13724
|
// src/dispatch-dedupe.ts
|
|
13659
13725
|
init_config();
|
|
13660
|
-
import { mkdirSync as mkdirSync6, readFileSync as readFileSync4, writeFileSync as writeFileSync4, renameSync as
|
|
13726
|
+
import { mkdirSync as mkdirSync6, readFileSync as readFileSync4, writeFileSync as writeFileSync4, renameSync as renameSync4, existsSync as existsSync7 } from "fs";
|
|
13661
13727
|
import { join as join11 } from "path";
|
|
13662
13728
|
var MAX_IDS = 256;
|
|
13663
13729
|
function dir(log) {
|
|
@@ -13775,7 +13841,7 @@ function writeTurnIds(workspaceId, turns) {
|
|
|
13775
13841
|
const path = turnPathFor(workspaceId);
|
|
13776
13842
|
const tmp = `${path}.${process.pid}.tmp`;
|
|
13777
13843
|
writeFileSync4(tmp, entries.map(([id, t]) => `${id} ${t}`).join("\n") + "\n", "utf8");
|
|
13778
|
-
|
|
13844
|
+
renameSync4(tmp, path);
|
|
13779
13845
|
return entries.length;
|
|
13780
13846
|
}
|
|
13781
13847
|
function rememberTurnId(workspaceId, eventId, turnId) {
|
|
@@ -13971,9 +14037,9 @@ import {
|
|
|
13971
14037
|
mkdirSync as mkdirSync10,
|
|
13972
14038
|
readdirSync as readdirSync4,
|
|
13973
14039
|
readFileSync as readFileSync9,
|
|
13974
|
-
renameSync as
|
|
14040
|
+
renameSync as renameSync6,
|
|
13975
14041
|
rmSync as rmSync9,
|
|
13976
|
-
statSync as
|
|
14042
|
+
statSync as statSync4,
|
|
13977
14043
|
writeFileSync as writeFileSync6
|
|
13978
14044
|
} from "fs";
|
|
13979
14045
|
import { join as join20 } from "path";
|
|
@@ -14046,7 +14112,7 @@ import {
|
|
|
14046
14112
|
mkdirSync as mkdirSync9,
|
|
14047
14113
|
readdirSync as readdirSync3,
|
|
14048
14114
|
readFileSync as readFileSync8,
|
|
14049
|
-
renameSync as
|
|
14115
|
+
renameSync as renameSync5,
|
|
14050
14116
|
rmSync as rmSync8
|
|
14051
14117
|
} from "fs";
|
|
14052
14118
|
import { join as join18 } from "path";
|
|
@@ -14082,11 +14148,11 @@ function admitCollectedEntry(raw, turn) {
|
|
|
14082
14148
|
}
|
|
14083
14149
|
var atomicMove = (from, to) => {
|
|
14084
14150
|
try {
|
|
14085
|
-
|
|
14151
|
+
renameSync5(from, to);
|
|
14086
14152
|
} catch {
|
|
14087
14153
|
const partial = `${to}.partial`;
|
|
14088
14154
|
copyFileSync2(from, partial);
|
|
14089
|
-
|
|
14155
|
+
renameSync5(partial, to);
|
|
14090
14156
|
rmSync8(from, { force: true });
|
|
14091
14157
|
}
|
|
14092
14158
|
};
|
|
@@ -14330,6 +14396,25 @@ function pruneOtherAttempts(deviceDir, pair2, keep) {
|
|
|
14330
14396
|
}
|
|
14331
14397
|
}
|
|
14332
14398
|
}
|
|
14399
|
+
var IDLE_SESSION_ARCHIVE_MS = 30 * 24 * 60 * 6e4;
|
|
14400
|
+
function pruneIdleSessionArchives(deviceDir, now = Date.now(), maxAgeMs = IDLE_SESSION_ARCHIVE_MS) {
|
|
14401
|
+
const root = join20(deviceDir, "attempts");
|
|
14402
|
+
if (!existsSync14(root)) return [];
|
|
14403
|
+
const removed = [];
|
|
14404
|
+
for (const id of readdirSync4(root)) {
|
|
14405
|
+
if (!UUID_RE.test(id)) continue;
|
|
14406
|
+
let kept;
|
|
14407
|
+
try {
|
|
14408
|
+
kept = statSync4(join20(root, id)).mtimeMs;
|
|
14409
|
+
} catch {
|
|
14410
|
+
continue;
|
|
14411
|
+
}
|
|
14412
|
+
if (now - kept < maxAgeMs) continue;
|
|
14413
|
+
rmSync9(join20(root, id), { recursive: true, force: true });
|
|
14414
|
+
removed.push(id);
|
|
14415
|
+
}
|
|
14416
|
+
return removed;
|
|
14417
|
+
}
|
|
14333
14418
|
var CappedFile = class extends Writable {
|
|
14334
14419
|
constructor(path, cap) {
|
|
14335
14420
|
super();
|
|
@@ -14707,7 +14792,7 @@ function createDockerProvider(opts) {
|
|
|
14707
14792
|
(resolve2) => file.writableFinished ? resolve2() : file.once("finish", resolve2)
|
|
14708
14793
|
);
|
|
14709
14794
|
if (r.code === 0 && !file.over) {
|
|
14710
|
-
|
|
14795
|
+
renameSync6(partial, archive);
|
|
14711
14796
|
writeFileSync6(join20(archive, "..", "pair"), `${attempt.conversationId}/${attempt.agentId}`);
|
|
14712
14797
|
pruneOtherAttempts(opts.deviceDir, attempt, attemptId);
|
|
14713
14798
|
await writeMarker(name, attemptId);
|
|
@@ -14844,7 +14929,7 @@ function createDockerProvider(opts) {
|
|
|
14844
14929
|
);
|
|
14845
14930
|
} else {
|
|
14846
14931
|
log.info(
|
|
14847
|
-
{ container: name, attemptId, bytes:
|
|
14932
|
+
{ container: name, attemptId, bytes: statSync4(archive).size },
|
|
14848
14933
|
"runners: session files restored"
|
|
14849
14934
|
);
|
|
14850
14935
|
}
|
|
@@ -15308,6 +15393,12 @@ var SseSubscriber = class {
|
|
|
15308
15393
|
this.aborted = true;
|
|
15309
15394
|
this.controller?.abort();
|
|
15310
15395
|
}
|
|
15396
|
+
// Connect again after a `stop()`, once its loop has exited.
|
|
15397
|
+
async resume() {
|
|
15398
|
+
await this.finished;
|
|
15399
|
+
this.aborted = false;
|
|
15400
|
+
this.start();
|
|
15401
|
+
}
|
|
15311
15402
|
setCursor(eventId) {
|
|
15312
15403
|
this.lastEventId = eventId;
|
|
15313
15404
|
}
|
|
@@ -15396,9 +15487,564 @@ function sleep4(ms) {
|
|
|
15396
15487
|
|
|
15397
15488
|
// src/supervisor.ts
|
|
15398
15489
|
init_transcript_writer();
|
|
15490
|
+
|
|
15491
|
+
// src/updater.ts
|
|
15492
|
+
import { spawn as spawn8 } from "child_process";
|
|
15493
|
+
import { copyFileSync as copyFileSync3, existsSync as existsSync17, mkdirSync as mkdirSync16, renameSync as renameSync11, rmSync as rmSync14 } from "fs";
|
|
15494
|
+
import { dirname as dirname12, join as join26 } from "path";
|
|
15495
|
+
|
|
15496
|
+
// src/boot-rule.ts
|
|
15497
|
+
import {
|
|
15498
|
+
mkdirSync as mkdirSync13,
|
|
15499
|
+
readFileSync as readFileSync12,
|
|
15500
|
+
realpathSync as realpathSync5,
|
|
15501
|
+
renameSync as renameSync8,
|
|
15502
|
+
rmSync as rmSync12,
|
|
15503
|
+
symlinkSync,
|
|
15504
|
+
writeFileSync as writeFileSync9
|
|
15505
|
+
} from "fs";
|
|
15506
|
+
import { dirname as dirname9, join as join23 } from "path";
|
|
15507
|
+
var UNCONFIRMED_WINDOW_MS = 10 * 6e4;
|
|
15508
|
+
var MAX_STAGE_ATTEMPTS = 3;
|
|
15509
|
+
function superseded(pending, ...running) {
|
|
15510
|
+
return !!pending && !running.includes(pending.to);
|
|
15511
|
+
}
|
|
15512
|
+
function rolledBackState(state, pending, reason, now) {
|
|
15513
|
+
const { pending: _done, ...rest } = state;
|
|
15514
|
+
return {
|
|
15515
|
+
...rest,
|
|
15516
|
+
lastResult: {
|
|
15517
|
+
kind: "rolled_back",
|
|
15518
|
+
from: pending.from,
|
|
15519
|
+
to: pending.to,
|
|
15520
|
+
at: now.toISOString(),
|
|
15521
|
+
reason: reason.replace(/\s+/g, " ").trim().slice(0, 400)
|
|
15522
|
+
},
|
|
15523
|
+
failed: { version: pending.to, attempts: MAX_STAGE_ATTEMPTS, at: now.toISOString() }
|
|
15524
|
+
};
|
|
15525
|
+
}
|
|
15526
|
+
|
|
15527
|
+
// src/install-layout.ts
|
|
15528
|
+
init_config();
|
|
15529
|
+
import {
|
|
15530
|
+
existsSync as existsSync16,
|
|
15531
|
+
lstatSync,
|
|
15532
|
+
mkdirSync as mkdirSync14,
|
|
15533
|
+
readFileSync as readFileSync13,
|
|
15534
|
+
readdirSync as readdirSync6,
|
|
15535
|
+
realpathSync as realpathSync6,
|
|
15536
|
+
renameSync as renameSync9,
|
|
15537
|
+
rmSync as rmSync13,
|
|
15538
|
+
symlinkSync as symlinkSync2
|
|
15539
|
+
} from "fs";
|
|
15540
|
+
import { dirname as dirname10, join as join24 } from "path";
|
|
15541
|
+
import { fileURLToPath as fileURLToPath6 } from "url";
|
|
15542
|
+
function versionsDir() {
|
|
15543
|
+
return join24(cabaneDir(), "versions");
|
|
15544
|
+
}
|
|
15545
|
+
function currentLink() {
|
|
15546
|
+
return join24(cabaneDir(), "current");
|
|
15547
|
+
}
|
|
15548
|
+
function bootGuardPath() {
|
|
15549
|
+
return join24(cabaneDir(), "boot-guard.mjs");
|
|
15550
|
+
}
|
|
15551
|
+
function pinnedNodeDir() {
|
|
15552
|
+
return join24(cabaneDir(), "node");
|
|
15553
|
+
}
|
|
15554
|
+
function pinnedNodePath() {
|
|
15555
|
+
return join24(pinnedNodeDir(), "bin", "node");
|
|
15556
|
+
}
|
|
15557
|
+
function versionPackageRoot(version) {
|
|
15558
|
+
return join24(versionsDir(), version, "node_modules", "@cabane", "companion");
|
|
15559
|
+
}
|
|
15560
|
+
function ownPackageRoot(moduleUrl = import.meta.url) {
|
|
15561
|
+
return dirname10(dirname10(fileURLToPath6(moduleUrl)));
|
|
15562
|
+
}
|
|
15563
|
+
function packageVersion(root) {
|
|
15564
|
+
try {
|
|
15565
|
+
const pkg2 = JSON.parse(readFileSync13(join24(root, "package.json"), "utf8"));
|
|
15566
|
+
return typeof pkg2.version === "string" ? pkg2.version : null;
|
|
15567
|
+
} catch {
|
|
15568
|
+
return null;
|
|
15569
|
+
}
|
|
15570
|
+
}
|
|
15571
|
+
function currentRoot() {
|
|
15572
|
+
try {
|
|
15573
|
+
const root = realpathSync6(currentLink());
|
|
15574
|
+
return existsSync16(join24(root, "dist", "cli.js")) ? root : null;
|
|
15575
|
+
} catch {
|
|
15576
|
+
return null;
|
|
15577
|
+
}
|
|
15578
|
+
}
|
|
15579
|
+
function pointCurrentAt(root) {
|
|
15580
|
+
const link = currentLink();
|
|
15581
|
+
mkdirSync14(dirname10(link), { recursive: true });
|
|
15582
|
+
const fresh = `${link}.${process.pid}.${Date.now()}`;
|
|
15583
|
+
symlinkSync2(root, fresh);
|
|
15584
|
+
try {
|
|
15585
|
+
renameSync9(fresh, link);
|
|
15586
|
+
} catch (err) {
|
|
15587
|
+
rmSync13(fresh, { force: true });
|
|
15588
|
+
throw err;
|
|
15589
|
+
}
|
|
15590
|
+
}
|
|
15591
|
+
function pruneVersions(keep) {
|
|
15592
|
+
const dir2 = versionsDir();
|
|
15593
|
+
if (!existsSync16(dir2)) return [];
|
|
15594
|
+
const removed = [];
|
|
15595
|
+
for (const name of readdirSync6(dir2)) {
|
|
15596
|
+
if (keep.includes(name)) continue;
|
|
15597
|
+
rmSync13(join24(dir2, name), { recursive: true, force: true });
|
|
15598
|
+
removed.push(name);
|
|
15599
|
+
}
|
|
15600
|
+
return removed;
|
|
15601
|
+
}
|
|
15602
|
+
function compareVersions(a, b) {
|
|
15603
|
+
const parts = (v) => /^\d+(\.\d+)*$/.test(v) ? v.split(".").map(Number) : null;
|
|
15604
|
+
const pa = parts(a);
|
|
15605
|
+
const pb = parts(b);
|
|
15606
|
+
if (!pa || !pb) return pa ? 1 : pb ? -1 : 0;
|
|
15607
|
+
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
15608
|
+
const d = (pa[i] ?? 0) - (pb[i] ?? 0);
|
|
15609
|
+
if (d !== 0) return d;
|
|
15610
|
+
}
|
|
15611
|
+
return 0;
|
|
15612
|
+
}
|
|
15613
|
+
|
|
15614
|
+
// src/update-state.ts
|
|
15615
|
+
init_config();
|
|
15616
|
+
import { mkdirSync as mkdirSync15, readFileSync as readFileSync14, renameSync as renameSync10, writeFileSync as writeFileSync10 } from "fs";
|
|
15617
|
+
import { dirname as dirname11, join as join25 } from "path";
|
|
15618
|
+
import { z as z18 } from "zod";
|
|
15619
|
+
var resultSchema = z18.object({
|
|
15620
|
+
kind: z18.enum(["updated", "rolled_back", "failed"]),
|
|
15621
|
+
from: z18.string(),
|
|
15622
|
+
to: z18.string(),
|
|
15623
|
+
at: z18.string(),
|
|
15624
|
+
reason: z18.string().optional()
|
|
15625
|
+
});
|
|
15626
|
+
var pendingSchema = z18.object({
|
|
15627
|
+
from: z18.string(),
|
|
15628
|
+
// The package root `current` pointed at before the swap, so a rollback can point it back.
|
|
15629
|
+
fromRoot: z18.string(),
|
|
15630
|
+
to: z18.string(),
|
|
15631
|
+
at: z18.string(),
|
|
15632
|
+
boots: z18.number().int().nonnegative()
|
|
15633
|
+
});
|
|
15634
|
+
var stateSchema = z18.object({
|
|
15635
|
+
server: z18.object({ latest: z18.string(), minimum: z18.string(), at: z18.string() }).optional(),
|
|
15636
|
+
pending: pendingSchema.optional(),
|
|
15637
|
+
lastResult: resultSchema.optional(),
|
|
15638
|
+
failed: z18.object({ version: z18.string(), attempts: z18.number().int(), at: z18.string() }).optional()
|
|
15639
|
+
});
|
|
15640
|
+
function updateStatePath(dir2 = cabaneDir()) {
|
|
15641
|
+
return join25(dir2, "update.json");
|
|
15642
|
+
}
|
|
15643
|
+
function readUpdateState(path = updateStatePath()) {
|
|
15644
|
+
try {
|
|
15645
|
+
const parsed = stateSchema.safeParse(JSON.parse(readFileSync14(path, "utf8")));
|
|
15646
|
+
return parsed.success ? parsed.data : {};
|
|
15647
|
+
} catch {
|
|
15648
|
+
return {};
|
|
15649
|
+
}
|
|
15650
|
+
}
|
|
15651
|
+
function writeUpdateState(state, path = updateStatePath()) {
|
|
15652
|
+
mkdirSync15(dirname11(path), { recursive: true });
|
|
15653
|
+
const tmp = `${path}.${process.pid}.tmp`;
|
|
15654
|
+
writeFileSync10(tmp, `${JSON.stringify(state, null, 2)}
|
|
15655
|
+
`, { mode: 384 });
|
|
15656
|
+
renameSync10(tmp, path);
|
|
15657
|
+
}
|
|
15658
|
+
function updateUpdateState(change, path = updateStatePath()) {
|
|
15659
|
+
const next = change(readUpdateState(path));
|
|
15660
|
+
writeUpdateState(next, path);
|
|
15661
|
+
return next;
|
|
15662
|
+
}
|
|
15663
|
+
|
|
15664
|
+
// src/updater.ts
|
|
15665
|
+
function updateMode(env = process.env) {
|
|
15666
|
+
return env.CABANE_COMPANION_SUPERVISED === "1" ? "supervised" : "attached";
|
|
15667
|
+
}
|
|
15668
|
+
var STAGE_RETRY_MS = 60 * 6e4;
|
|
15669
|
+
var SWAP_DRAIN_MS = 1e4;
|
|
15670
|
+
var SOURCE_BUILD_VERSION = "0.0.0";
|
|
15671
|
+
function versionToStage(input) {
|
|
15672
|
+
const { own, state, now, force } = input;
|
|
15673
|
+
if (!input.auto && !force) return null;
|
|
15674
|
+
if (own === SOURCE_BUILD_VERSION) return null;
|
|
15675
|
+
const latest = state.server?.latest;
|
|
15676
|
+
if (!latest || compareVersions(latest, own) <= 0) return null;
|
|
15677
|
+
if (state.pending) return null;
|
|
15678
|
+
const failed = state.failed;
|
|
15679
|
+
if (!force && failed?.version === latest) {
|
|
15680
|
+
if (failed.attempts >= MAX_STAGE_ATTEMPTS) return null;
|
|
15681
|
+
if (now - Date.parse(failed.at) < STAGE_RETRY_MS) return null;
|
|
15682
|
+
}
|
|
15683
|
+
return latest;
|
|
15684
|
+
}
|
|
15685
|
+
function rollBack(pending, reason, now = /* @__PURE__ */ new Date()) {
|
|
15686
|
+
if (existsSync17(pending.fromRoot)) pointCurrentAt(pending.fromRoot);
|
|
15687
|
+
return updateUpdateState((state) => rolledBackState(state, pending, reason, now));
|
|
15688
|
+
}
|
|
15689
|
+
function writeBootGuard(root = ownPackageRoot()) {
|
|
15690
|
+
const built = join26(root, "dist", "boot-guard.js");
|
|
15691
|
+
if (!existsSync17(built)) return false;
|
|
15692
|
+
const target = bootGuardPath();
|
|
15693
|
+
mkdirSync16(dirname12(target), { recursive: true });
|
|
15694
|
+
const tmp = `${target}.${process.pid}.tmp`;
|
|
15695
|
+
copyFileSync3(built, tmp);
|
|
15696
|
+
renameSync11(tmp, target);
|
|
15697
|
+
return true;
|
|
15698
|
+
}
|
|
15699
|
+
function oneLine(text) {
|
|
15700
|
+
return text.replace(/\s+/g, " ").trim().slice(0, 400);
|
|
15701
|
+
}
|
|
15702
|
+
var runProcess = (command, args, opts = {}) => new Promise((resolve2) => {
|
|
15703
|
+
let stdout = "";
|
|
15704
|
+
let stderr = "";
|
|
15705
|
+
let child;
|
|
15706
|
+
try {
|
|
15707
|
+
child = spawn8(command, [...args], {
|
|
15708
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
15709
|
+
env: opts.env ?? process.env
|
|
15710
|
+
});
|
|
15711
|
+
} catch (err) {
|
|
15712
|
+
resolve2({ code: null, stdout, stderr: err instanceof Error ? err.message : String(err) });
|
|
15713
|
+
return;
|
|
15714
|
+
}
|
|
15715
|
+
const timer = opts.timeoutMs ? setTimeout(() => child.kill("SIGKILL"), opts.timeoutMs) : null;
|
|
15716
|
+
child.stdout.on("data", (d) => stdout += d.toString());
|
|
15717
|
+
child.stderr.on("data", (d) => stderr += d.toString());
|
|
15718
|
+
child.on("error", (err) => {
|
|
15719
|
+
if (timer) clearTimeout(timer);
|
|
15720
|
+
resolve2({ code: null, stdout, stderr: stderr || err.message });
|
|
15721
|
+
});
|
|
15722
|
+
child.on("close", (code) => {
|
|
15723
|
+
if (timer) clearTimeout(timer);
|
|
15724
|
+
resolve2({ code, stdout, stderr });
|
|
15725
|
+
});
|
|
15726
|
+
});
|
|
15727
|
+
async function stageVersion(version, harnesses, deps = {}) {
|
|
15728
|
+
const run = deps.run ?? runProcess;
|
|
15729
|
+
const node = deps.execPath ?? process.execPath;
|
|
15730
|
+
const target = join26(versionsDir(), version);
|
|
15731
|
+
const placed = versionPackageRoot(version);
|
|
15732
|
+
if (existsSync17(placed)) {
|
|
15733
|
+
if (!await verifyInstall(placed, version, harnesses, { run, execPath: node }))
|
|
15734
|
+
return { ok: true, root: placed };
|
|
15735
|
+
}
|
|
15736
|
+
const npm = join26(dirname12(node), "npm");
|
|
15737
|
+
if (!existsSync17(npm))
|
|
15738
|
+
return { ok: false, reason: `there is no npm beside Node at ${dirname12(node)}` };
|
|
15739
|
+
mkdirSync16(versionsDir(), { recursive: true });
|
|
15740
|
+
const staging = join26(versionsDir(), `.${version}.staging-${process.pid}`);
|
|
15741
|
+
rmSync14(staging, { recursive: true, force: true });
|
|
15742
|
+
const installed = await run(
|
|
15743
|
+
npm,
|
|
15744
|
+
[
|
|
15745
|
+
"install",
|
|
15746
|
+
"--prefix",
|
|
15747
|
+
staging,
|
|
15748
|
+
"--include=optional",
|
|
15749
|
+
"--no-audit",
|
|
15750
|
+
"--no-fund",
|
|
15751
|
+
"--no-save",
|
|
15752
|
+
"--loglevel=error",
|
|
15753
|
+
`@cabane/companion@${version}`
|
|
15754
|
+
],
|
|
15755
|
+
{
|
|
15756
|
+
// npm's install scripts run on this Node, not whichever `node` is first on PATH.
|
|
15757
|
+
env: { ...process.env, PATH: `${dirname12(node)}:${process.env.PATH ?? ""}` },
|
|
15758
|
+
timeoutMs: 15 * 6e4
|
|
15759
|
+
}
|
|
15760
|
+
);
|
|
15761
|
+
if (installed.code !== 0) {
|
|
15762
|
+
rmSync14(staging, { recursive: true, force: true });
|
|
15763
|
+
if (/\bE404\b|\bETARGET\b|No matching version found|is not in this registry/i.test(
|
|
15764
|
+
installed.stderr
|
|
15765
|
+
))
|
|
15766
|
+
return { ok: false, reason: `version ${version} isn't published yet`, unpublished: true };
|
|
15767
|
+
return { ok: false, reason: `npm couldn't install it: ${lastLine(installed.stderr)}` };
|
|
15768
|
+
}
|
|
15769
|
+
const root = join26(staging, "node_modules", "@cabane", "companion");
|
|
15770
|
+
const verdict = await verifyInstall(root, version, harnesses, { run, execPath: node });
|
|
15771
|
+
if (verdict) {
|
|
15772
|
+
rmSync14(staging, { recursive: true, force: true });
|
|
15773
|
+
return { ok: false, reason: verdict };
|
|
15774
|
+
}
|
|
15775
|
+
rmSync14(target, { recursive: true, force: true });
|
|
15776
|
+
renameSync11(staging, target);
|
|
15777
|
+
return { ok: true, root: versionPackageRoot(version) };
|
|
15778
|
+
}
|
|
15779
|
+
async function verifyInstall(root, version, harnesses, deps) {
|
|
15780
|
+
if (packageVersion(root) !== version) return `the installed package isn't version ${version}`;
|
|
15781
|
+
const answered = await deps.run(deps.execPath, [join26(root, "dist", "bin.js"), "--version"], {
|
|
15782
|
+
timeoutMs: 6e4,
|
|
15783
|
+
env: { ...process.env, CABANE_LAUNCH_SELF: "1" }
|
|
15784
|
+
});
|
|
15785
|
+
if (answered.code !== 0 || answered.stdout.trim() !== version) {
|
|
15786
|
+
return `it doesn't start: ${lastLine(answered.stderr) || "no version printed"}`;
|
|
15787
|
+
}
|
|
15788
|
+
if (harnesses.length === 0) return null;
|
|
15789
|
+
const probed = await deps.run(deps.execPath, [join26(root, "dist", "postinstall.js"), "--json"], {
|
|
15790
|
+
timeoutMs: 6e4
|
|
15791
|
+
});
|
|
15792
|
+
let resolutions = {};
|
|
15793
|
+
try {
|
|
15794
|
+
resolutions = JSON.parse(probed.stdout);
|
|
15795
|
+
} catch {
|
|
15796
|
+
return "it couldn't say which bundled agents it has";
|
|
15797
|
+
}
|
|
15798
|
+
const missing = harnesses.filter((h) => resolutions[h]?.status !== "present");
|
|
15799
|
+
if (missing.length > 0) {
|
|
15800
|
+
return `its bundled ${missing.map(harnessName).join(" and ")} ${missing.length > 1 ? "are" : "is"} missing`;
|
|
15801
|
+
}
|
|
15802
|
+
return null;
|
|
15803
|
+
}
|
|
15804
|
+
function harnessName(h) {
|
|
15805
|
+
return h === "claude-code" ? "Claude Code" : "Codex";
|
|
15806
|
+
}
|
|
15807
|
+
function lastLine(text) {
|
|
15808
|
+
const lines2 = text.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
15809
|
+
return lines2.at(-1)?.slice(0, 300) ?? "";
|
|
15810
|
+
}
|
|
15811
|
+
var CompanionUpdater = class {
|
|
15812
|
+
constructor(deps) {
|
|
15813
|
+
this.deps = deps;
|
|
15814
|
+
this.now = deps.now ?? Date.now;
|
|
15815
|
+
}
|
|
15816
|
+
deps;
|
|
15817
|
+
staging = null;
|
|
15818
|
+
ready = null;
|
|
15819
|
+
swapping = false;
|
|
15820
|
+
watchdog = null;
|
|
15821
|
+
now;
|
|
15822
|
+
// At start: a recorded swap to a version that will never start again (someone installed or
|
|
15823
|
+
// chose another since) is dropped, or nothing would ever confirm it and no later update would
|
|
15824
|
+
// be installed.
|
|
15825
|
+
start() {
|
|
15826
|
+
const pending = readUpdateState().pending;
|
|
15827
|
+
const current = currentRoot();
|
|
15828
|
+
const currentVersion = current ? packageVersion(current) : null;
|
|
15829
|
+
if (superseded(pending, this.deps.own, ...currentVersion ? [currentVersion] : [])) {
|
|
15830
|
+
updateUpdateState(({ pending: _gone, ...rest }) => rest);
|
|
15831
|
+
this.deps.log.info(
|
|
15832
|
+
{ to: pending.to, own: this.deps.own },
|
|
15833
|
+
`Dropped the update to ${pending.to}: Cabane ${this.deps.own} was installed since`
|
|
15834
|
+
);
|
|
15835
|
+
}
|
|
15836
|
+
}
|
|
15837
|
+
// The deadline for an update this process is the result of: ten minutes from the first time
|
|
15838
|
+
// Cabane answers it without its stream connecting. Until Cabane answers, there's no verdict on
|
|
15839
|
+
// time: a server or network that is down says nothing about the version.
|
|
15840
|
+
armDeadline() {
|
|
15841
|
+
if (this.watchdog) return;
|
|
15842
|
+
this.watchdog = setTimeout(() => void this.giveUp(), UNCONFIRMED_WINDOW_MS);
|
|
15843
|
+
this.watchdog.unref?.();
|
|
15844
|
+
}
|
|
15845
|
+
stop() {
|
|
15846
|
+
if (this.watchdog) clearTimeout(this.watchdog);
|
|
15847
|
+
this.watchdog = null;
|
|
15848
|
+
}
|
|
15849
|
+
async giveUp() {
|
|
15850
|
+
const pending = readUpdateState().pending;
|
|
15851
|
+
if (!pending || pending.to !== this.deps.own) return;
|
|
15852
|
+
this.deps.log.warn(
|
|
15853
|
+
{ from: pending.from, to: pending.to },
|
|
15854
|
+
`Cabane ${pending.to} didn't connect within ${UNCONFIRMED_WINDOW_MS / 6e4} minutes of Cabane answering; going back to ${pending.from}`
|
|
15855
|
+
);
|
|
15856
|
+
rollBack(
|
|
15857
|
+
pending,
|
|
15858
|
+
`it didn't connect to Cabane within ${UNCONFIRMED_WINDOW_MS / 6e4} minutes`
|
|
15859
|
+
);
|
|
15860
|
+
await this.deps.restart();
|
|
15861
|
+
}
|
|
15862
|
+
// Auto-update is on and something will restart this companion into the new version.
|
|
15863
|
+
updatesItself() {
|
|
15864
|
+
return this.deps.mode === "supervised" && this.deps.autoUpdate();
|
|
15865
|
+
}
|
|
15866
|
+
// What the heartbeat reports.
|
|
15867
|
+
report() {
|
|
15868
|
+
const last = readUpdateState().lastResult;
|
|
15869
|
+
return { auto: this.updatesItself(), ...last ? { lastResult: last } : {} };
|
|
15870
|
+
}
|
|
15871
|
+
// A heartbeat landed. Record what the server named; confirm an update this process is the
|
|
15872
|
+
// result of; then, when there is something newer, install it and swap when idle.
|
|
15873
|
+
async onHeartbeat(server, streamConnected) {
|
|
15874
|
+
const state = updateUpdateState((s) => ({
|
|
15875
|
+
...s,
|
|
15876
|
+
...server ? { server: { ...server, at: new Date(this.now()).toISOString() } } : {}
|
|
15877
|
+
}));
|
|
15878
|
+
if (state.pending?.to === this.deps.own) {
|
|
15879
|
+
if (streamConnected) this.confirm(state.pending);
|
|
15880
|
+
else this.armDeadline();
|
|
15881
|
+
}
|
|
15882
|
+
if (this.ready) {
|
|
15883
|
+
await this.trySwap();
|
|
15884
|
+
return;
|
|
15885
|
+
}
|
|
15886
|
+
this.maybeStage(false);
|
|
15887
|
+
}
|
|
15888
|
+
confirm(pending) {
|
|
15889
|
+
this.stop();
|
|
15890
|
+
updateUpdateState((s) => {
|
|
15891
|
+
const { pending: _done, failed: _failed, ...rest } = s;
|
|
15892
|
+
return {
|
|
15893
|
+
...rest,
|
|
15894
|
+
lastResult: {
|
|
15895
|
+
kind: "updated",
|
|
15896
|
+
from: pending.from,
|
|
15897
|
+
to: pending.to,
|
|
15898
|
+
at: new Date(this.now()).toISOString()
|
|
15899
|
+
}
|
|
15900
|
+
};
|
|
15901
|
+
});
|
|
15902
|
+
const removed = pruneVersions([pending.to, pending.from]);
|
|
15903
|
+
this.deps.log.info(
|
|
15904
|
+
{ from: pending.from, to: pending.to, removed },
|
|
15905
|
+
`Updated Cabane from ${pending.from} to ${pending.to}`
|
|
15906
|
+
);
|
|
15907
|
+
}
|
|
15908
|
+
// `cabane update`, from the person: install the latest now, whatever the setting.
|
|
15909
|
+
async updateNow() {
|
|
15910
|
+
const state = readUpdateState();
|
|
15911
|
+
const latest = state.server?.latest;
|
|
15912
|
+
if (!latest) return { ok: false, message: "Cabane hasn't said which version is latest yet." };
|
|
15913
|
+
if (compareVersions(latest, this.deps.own) <= 0)
|
|
15914
|
+
return { ok: true, message: `Cabane ${this.deps.own} is up to date.` };
|
|
15915
|
+
if (state.pending?.to === latest) return { ok: true, message: this.waitingMessage(latest) };
|
|
15916
|
+
if (this.ready === latest) {
|
|
15917
|
+
void this.trySwap();
|
|
15918
|
+
return { ok: true, message: this.waitingMessage(latest) };
|
|
15919
|
+
}
|
|
15920
|
+
this.maybeStage(true);
|
|
15921
|
+
if (this.staging) await this.staging;
|
|
15922
|
+
if (this.ready !== latest) {
|
|
15923
|
+
const after = readUpdateState();
|
|
15924
|
+
if (after.pending?.to === latest) return { ok: true, message: this.waitingMessage(latest) };
|
|
15925
|
+
if (after.failed?.version === latest && after.failed.attempts === 0)
|
|
15926
|
+
return { ok: false, message: `Cabane ${latest} isn't published yet. Try again later.` };
|
|
15927
|
+
const failed = after.lastResult;
|
|
15928
|
+
return {
|
|
15929
|
+
ok: false,
|
|
15930
|
+
message: failed?.kind === "failed" && failed.to === latest ? `Couldn't update to ${latest}: ${failed.reason ?? "see cabane logs"}.` : `Couldn't update to ${latest}. cabane logs says why.`
|
|
15931
|
+
};
|
|
15932
|
+
}
|
|
15933
|
+
void this.trySwap();
|
|
15934
|
+
return { ok: true, message: this.waitingMessage(latest) };
|
|
15935
|
+
}
|
|
15936
|
+
waitingMessage(version) {
|
|
15937
|
+
if (this.deps.mode === "attached")
|
|
15938
|
+
return `Cabane ${version} is installed. It runs from the next cabane start.`;
|
|
15939
|
+
return this.deps.isIdle() ? `Updating to ${version} now.` : `Updating to ${version} once the turns running now finish.`;
|
|
15940
|
+
}
|
|
15941
|
+
maybeStage(force) {
|
|
15942
|
+
if (this.staging) return;
|
|
15943
|
+
const version = versionToStage({
|
|
15944
|
+
auto: this.updatesItself(),
|
|
15945
|
+
own: this.deps.own,
|
|
15946
|
+
state: readUpdateState(),
|
|
15947
|
+
now: this.now(),
|
|
15948
|
+
force
|
|
15949
|
+
});
|
|
15950
|
+
if (!version) return;
|
|
15951
|
+
const stage = this.deps.stage ?? stageVersion;
|
|
15952
|
+
this.staging = (async () => {
|
|
15953
|
+
this.deps.log.info({ version }, `Installing Cabane ${version} beside this one`);
|
|
15954
|
+
const outcome = await stage(version, this.deps.harnesses());
|
|
15955
|
+
if (outcome.ok) {
|
|
15956
|
+
this.ready = version;
|
|
15957
|
+
updateUpdateState((s) => {
|
|
15958
|
+
const { failed: _failed, ...rest } = s;
|
|
15959
|
+
return rest;
|
|
15960
|
+
});
|
|
15961
|
+
return;
|
|
15962
|
+
}
|
|
15963
|
+
if (outcome.unpublished) {
|
|
15964
|
+
this.deps.log.info({ version }, `Cabane ${version} isn't published yet; trying later`);
|
|
15965
|
+
updateUpdateState((s) => ({
|
|
15966
|
+
...s,
|
|
15967
|
+
failed: { version, attempts: 0, at: new Date(this.now()).toISOString() }
|
|
15968
|
+
}));
|
|
15969
|
+
return;
|
|
15970
|
+
}
|
|
15971
|
+
this.deps.log.warn({ version, reason: outcome.reason }, `Couldn't install Cabane ${version}`);
|
|
15972
|
+
updateUpdateState((s) => {
|
|
15973
|
+
const attempts = s.failed?.version === version ? s.failed.attempts + 1 : 1;
|
|
15974
|
+
const at = new Date(this.now()).toISOString();
|
|
15975
|
+
return {
|
|
15976
|
+
...s,
|
|
15977
|
+
failed: { version, attempts, at },
|
|
15978
|
+
// Reported once it has failed for good, so a registry blip isn't an email.
|
|
15979
|
+
...attempts >= MAX_STAGE_ATTEMPTS || force ? {
|
|
15980
|
+
lastResult: {
|
|
15981
|
+
kind: "failed",
|
|
15982
|
+
from: this.deps.own,
|
|
15983
|
+
to: version,
|
|
15984
|
+
at,
|
|
15985
|
+
reason: oneLine(outcome.reason)
|
|
15986
|
+
}
|
|
15987
|
+
} : {}
|
|
15988
|
+
};
|
|
15989
|
+
});
|
|
15990
|
+
})().finally(() => {
|
|
15991
|
+
this.staging = null;
|
|
15992
|
+
});
|
|
15993
|
+
void this.staging.then(() => this.trySwap());
|
|
15994
|
+
}
|
|
15995
|
+
// Swap into the ready version once nothing is running. Called on each heartbeat until it can.
|
|
15996
|
+
async trySwap() {
|
|
15997
|
+
const to = this.ready;
|
|
15998
|
+
if (!to || this.swapping) return;
|
|
15999
|
+
if (readUpdateState().pending) return;
|
|
16000
|
+
if (this.deps.mode === "attached") {
|
|
16001
|
+
this.select(to);
|
|
16002
|
+
this.deps.log.info({ version: to }, `Cabane ${to} is installed; it runs from the next start`);
|
|
16003
|
+
return;
|
|
16004
|
+
}
|
|
16005
|
+
if (!this.deps.isIdle()) return;
|
|
16006
|
+
this.swapping = true;
|
|
16007
|
+
try {
|
|
16008
|
+
await this.deps.pauseStream();
|
|
16009
|
+
const sleep5 = this.deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
16010
|
+
const deadline = this.now() + SWAP_DRAIN_MS;
|
|
16011
|
+
await sleep5(250);
|
|
16012
|
+
while (!this.deps.isIdle() && this.now() < deadline) await sleep5(250);
|
|
16013
|
+
if (!this.deps.isIdle()) {
|
|
16014
|
+
this.deps.resumeStream();
|
|
16015
|
+
return;
|
|
16016
|
+
}
|
|
16017
|
+
this.select(to);
|
|
16018
|
+
this.deps.log.info({ from: this.deps.own, to }, `Restarting into Cabane ${to}`);
|
|
16019
|
+
await this.deps.restart();
|
|
16020
|
+
} finally {
|
|
16021
|
+
this.swapping = false;
|
|
16022
|
+
}
|
|
16023
|
+
}
|
|
16024
|
+
// Record the swap and point `current` at `to`, leaving the guard that counts its starts.
|
|
16025
|
+
select(to) {
|
|
16026
|
+
const fromRoot = currentRoot() ?? ownPackageRoot();
|
|
16027
|
+
writeBootGuard();
|
|
16028
|
+
updateUpdateState((s) => ({
|
|
16029
|
+
...s,
|
|
16030
|
+
pending: {
|
|
16031
|
+
from: this.deps.own,
|
|
16032
|
+
fromRoot,
|
|
16033
|
+
to,
|
|
16034
|
+
at: new Date(this.now()).toISOString(),
|
|
16035
|
+
boots: 0
|
|
16036
|
+
}
|
|
16037
|
+
}));
|
|
16038
|
+
pointCurrentAt(versionPackageRoot(to));
|
|
16039
|
+
this.ready = null;
|
|
16040
|
+
}
|
|
16041
|
+
};
|
|
16042
|
+
|
|
16043
|
+
// src/supervisor.ts
|
|
15399
16044
|
init_version2();
|
|
15400
16045
|
init_src();
|
|
15401
16046
|
var HEARTBEAT_INTERVAL_MS = 3e4;
|
|
16047
|
+
var ARCHIVE_PRUNE_MS = 24 * 60 * 6e4;
|
|
15402
16048
|
var SHUTDOWN_HEARTBEAT_WAIT_MS = 1e3;
|
|
15403
16049
|
function positiveIntEnv(name) {
|
|
15404
16050
|
const raw = process.env[name];
|
|
@@ -15480,6 +16126,10 @@ var CompanionSupervisor = class {
|
|
|
15480
16126
|
// Which dispatches this process is running, so a redelivered frame doesn't
|
|
15481
16127
|
// run the turn a second time.
|
|
15482
16128
|
claims = new DispatchClaims();
|
|
16129
|
+
archivePruneTimer = null;
|
|
16130
|
+
// Keeps this companion current when its person said so; null until paired.
|
|
16131
|
+
updater = null;
|
|
16132
|
+
restartForUpdate;
|
|
15483
16133
|
constructor(opts) {
|
|
15484
16134
|
this.config = opts.config;
|
|
15485
16135
|
this.log = opts.log;
|
|
@@ -15489,6 +16139,10 @@ var CompanionSupervisor = class {
|
|
|
15489
16139
|
this.harnessVersions = opts.harnessVersions ?? emptyHarnessVersions();
|
|
15490
16140
|
this.exitFn = opts.exit ?? ((code) => process.exit(code));
|
|
15491
16141
|
this.reexecFn = opts.reexec ?? defaultReexec;
|
|
16142
|
+
this.restartForUpdate = opts.restartForUpdate ?? (async () => {
|
|
16143
|
+
await this.shutdown();
|
|
16144
|
+
this.exitFn(0);
|
|
16145
|
+
});
|
|
15492
16146
|
this.dispatcherFactory = opts.dispatcherFactory;
|
|
15493
16147
|
if (opts.runnerCommand) this.runnerCommand = opts.runnerCommand;
|
|
15494
16148
|
if (opts.runnerProvider) this.providerForRunners = opts.runnerProvider;
|
|
@@ -15539,6 +16193,9 @@ var CompanionSupervisor = class {
|
|
|
15539
16193
|
if (!this.runnerImage) await invalidateCompanionVolume(docker, image, source);
|
|
15540
16194
|
}
|
|
15541
16195
|
this.latchBootBundledState();
|
|
16196
|
+
this.pruneSessionArchives();
|
|
16197
|
+
this.archivePruneTimer = setInterval(() => this.pruneSessionArchives(), ARCHIVE_PRUNE_MS);
|
|
16198
|
+
this.archivePruneTimer.unref?.();
|
|
15542
16199
|
}
|
|
15543
16200
|
const sandboxRows = agentSandboxRows(this.config);
|
|
15544
16201
|
if (sandboxRows.length > 0 && this.dockerRunners()) {
|
|
@@ -15548,7 +16205,7 @@ var CompanionSupervisor = class {
|
|
|
15548
16205
|
);
|
|
15549
16206
|
}
|
|
15550
16207
|
const anySandboxed = [{}, ...sandboxRows.map(([, sandbox]) => ({ sandbox }))].some(
|
|
15551
|
-
(
|
|
16208
|
+
(row3) => this.runnerSandboxFor(row3) === "os"
|
|
15552
16209
|
);
|
|
15553
16210
|
if (anySandboxed) {
|
|
15554
16211
|
const policy = this.sandboxPolicy();
|
|
@@ -15602,6 +16259,23 @@ var CompanionSupervisor = class {
|
|
|
15602
16259
|
);
|
|
15603
16260
|
}
|
|
15604
16261
|
});
|
|
16262
|
+
this.updater = new CompanionUpdater({
|
|
16263
|
+
own: COMPANION_VERSION,
|
|
16264
|
+
mode: updateMode(),
|
|
16265
|
+
autoUpdate: () => this.config.update?.auto === true,
|
|
16266
|
+
harnesses: () => this.connectedBundledHarnesses(),
|
|
16267
|
+
isIdle: () => this.idle(),
|
|
16268
|
+
pauseStream: async () => {
|
|
16269
|
+
const sub = this.deviceSub;
|
|
16270
|
+
if (!sub) return;
|
|
16271
|
+
sub.stop();
|
|
16272
|
+
await sub.finished;
|
|
16273
|
+
},
|
|
16274
|
+
resumeStream: () => void this.deviceSub?.resume(),
|
|
16275
|
+
restart: () => this.restartForUpdate(),
|
|
16276
|
+
log: this.log
|
|
16277
|
+
});
|
|
16278
|
+
this.updater.start();
|
|
15605
16279
|
await this.refreshAssignments();
|
|
15606
16280
|
this.deviceSub.start();
|
|
15607
16281
|
const firstHeartbeat = startupHarnessRefresh.then(async () => {
|
|
@@ -15662,13 +16336,20 @@ var CompanionSupervisor = class {
|
|
|
15662
16336
|
// Omitted until the first observed failure or heal, so the server's
|
|
15663
16337
|
// status-less manifest synthesis still runs for a fresh companion.
|
|
15664
16338
|
...connectorReports.length > 0 ? { connectors: connectorReports } : {},
|
|
15665
|
-
...opencodeModels !== null ? { models: opencodeModels } : {}
|
|
16339
|
+
...opencodeModels !== null ? { models: opencodeModels } : {},
|
|
16340
|
+
...this.updater ? { update: this.updater.report() } : {}
|
|
15666
16341
|
});
|
|
15667
16342
|
this.hub.setDevice({ deviceId: res.deviceId });
|
|
15668
16343
|
this.deviceId = res.deviceId;
|
|
15669
16344
|
this.checkVersionSkew(res.serverVersion);
|
|
15670
16345
|
this.heartbeated = true;
|
|
15671
16346
|
void this.recoverRuns();
|
|
16347
|
+
void this.updater?.onHeartbeat(res.companion, this.deviceConnected).catch(
|
|
16348
|
+
(err) => this.log.warn(
|
|
16349
|
+
{ err: err instanceof Error ? err.message : String(err) },
|
|
16350
|
+
"Couldn't check for a Cabane update; trying again on the next check-in."
|
|
16351
|
+
)
|
|
16352
|
+
);
|
|
15672
16353
|
} catch (err) {
|
|
15673
16354
|
this.log.warn(
|
|
15674
16355
|
{ ...apiErrorLogFields(err) },
|
|
@@ -15991,8 +16672,8 @@ var CompanionSupervisor = class {
|
|
|
15991
16672
|
// The sandbox one agent's turns run under: its `agents` row, then the
|
|
15992
16673
|
// device's. Resolved from the current config on every ask, so a reload
|
|
15993
16674
|
// changes it; only the host probe is kept.
|
|
15994
|
-
runnerSandboxFor(
|
|
15995
|
-
return runnerSandboxFor(this.config,
|
|
16675
|
+
runnerSandboxFor(row3) {
|
|
16676
|
+
return runnerSandboxFor(this.config, row3, this.sandboxHost);
|
|
15996
16677
|
}
|
|
15997
16678
|
// Resolved against the agents this device runs right now.
|
|
15998
16679
|
runnerManifest() {
|
|
@@ -16699,9 +17380,54 @@ var CompanionSupervisor = class {
|
|
|
16699
17380
|
}
|
|
16700
17381
|
}
|
|
16701
17382
|
}
|
|
17383
|
+
// A long-lived device keeps a session archive per conversation and agent it ever ran; those
|
|
17384
|
+
// nobody has used in a month go.
|
|
17385
|
+
pruneSessionArchives() {
|
|
17386
|
+
try {
|
|
17387
|
+
const removed = pruneIdleSessionArchives(cabaneDir());
|
|
17388
|
+
if (removed.length > 0)
|
|
17389
|
+
this.log.info({ removed: removed.length }, "runners: removed idle session archives");
|
|
17390
|
+
} catch (err) {
|
|
17391
|
+
this.log.warn(
|
|
17392
|
+
{ err: err instanceof Error ? err.message : String(err) },
|
|
17393
|
+
"runners: couldn't remove idle session archives"
|
|
17394
|
+
);
|
|
17395
|
+
}
|
|
17396
|
+
}
|
|
17397
|
+
// Nothing running and nothing about to: no dispatch claimed, no turn chained, no recovery
|
|
17398
|
+
// in progress. What a self-update swap waits for.
|
|
17399
|
+
idle() {
|
|
17400
|
+
if (this.claims.size > 0 || this.recovering) return false;
|
|
17401
|
+
for (const wr of this.workspaces.values()) if (wr.chains.size > 0) return false;
|
|
17402
|
+
return true;
|
|
17403
|
+
}
|
|
17404
|
+
// The bundled harnesses this device has connected, which a new version must still carry.
|
|
17405
|
+
connectedBundledHarnesses() {
|
|
17406
|
+
return [
|
|
17407
|
+
...isClaudeCodeConnected(this.config) ? ["claude-code"] : [],
|
|
17408
|
+
...isCodexEnabled(this.config) ? ["codex"] : []
|
|
17409
|
+
];
|
|
17410
|
+
}
|
|
17411
|
+
// `cabane update`, over the control socket: set whether this companion keeps itself current
|
|
17412
|
+
// (`auto`), or install the latest now.
|
|
17413
|
+
async update(auto) {
|
|
17414
|
+
if (auto !== void 0) {
|
|
17415
|
+
this.config = { ...this.config, update: { auto } };
|
|
17416
|
+
saveConfig(this.config);
|
|
17417
|
+
if (auto) this.kickHeartbeat();
|
|
17418
|
+
return {
|
|
17419
|
+
ok: true,
|
|
17420
|
+
message: auto ? "Cabane will keep itself up to date. Updates install between turns and roll back if they fail." : "Cabane won't update itself. Run cabane update to update."
|
|
17421
|
+
};
|
|
17422
|
+
}
|
|
17423
|
+
if (!this.updater) return { ok: false, message: "This computer isn't paired yet." };
|
|
17424
|
+
return this.updater.updateNow();
|
|
17425
|
+
}
|
|
16702
17426
|
// ---- control actions ----
|
|
16703
17427
|
async shutdown() {
|
|
16704
17428
|
this.stopped = true;
|
|
17429
|
+
this.updater?.stop();
|
|
17430
|
+
if (this.archivePruneTimer) clearInterval(this.archivePruneTimer);
|
|
16705
17431
|
if (this.heartbeatTimer) clearInterval(this.heartbeatTimer);
|
|
16706
17432
|
if (this.pollTimer) clearInterval(this.pollTimer);
|
|
16707
17433
|
const heartbeat = this.inFlightHeartbeat;
|
|
@@ -16736,9 +17462,9 @@ async function waitForBoundedHeartbeat(heartbeat) {
|
|
|
16736
17462
|
}
|
|
16737
17463
|
function defaultReexec() {
|
|
16738
17464
|
clearRuntimeState();
|
|
16739
|
-
void import("child_process").then(({ spawn:
|
|
17465
|
+
void import("child_process").then(({ spawn: spawn11 }) => {
|
|
16740
17466
|
try {
|
|
16741
|
-
const child =
|
|
17467
|
+
const child = spawn11(process.execPath, process.argv.slice(1), {
|
|
16742
17468
|
stdio: "inherit",
|
|
16743
17469
|
detached: false
|
|
16744
17470
|
});
|
|
@@ -16812,22 +17538,22 @@ function handleUncaught(log, err, origin) {
|
|
|
16812
17538
|
|
|
16813
17539
|
// src/crash-marker.ts
|
|
16814
17540
|
init_config();
|
|
16815
|
-
import { existsSync as
|
|
16816
|
-
import { join as
|
|
17541
|
+
import { existsSync as existsSync18, mkdirSync as mkdirSync17, readFileSync as readFileSync15, rmSync as rmSync15, writeFileSync as writeFileSync11 } from "fs";
|
|
17542
|
+
import { join as join27 } from "path";
|
|
16817
17543
|
function crashMarkerPath() {
|
|
16818
|
-
return
|
|
17544
|
+
return join27(cabaneDir(), "last-error.json");
|
|
16819
17545
|
}
|
|
16820
17546
|
function recordCrash(rec2) {
|
|
16821
17547
|
try {
|
|
16822
|
-
|
|
16823
|
-
|
|
17548
|
+
mkdirSync17(cabaneDir(), { recursive: true });
|
|
17549
|
+
writeFileSync11(crashMarkerPath(), JSON.stringify(rec2, null, 2) + "\n");
|
|
16824
17550
|
} catch {
|
|
16825
17551
|
}
|
|
16826
17552
|
}
|
|
16827
17553
|
function clearCrash() {
|
|
16828
17554
|
try {
|
|
16829
17555
|
const path = crashMarkerPath();
|
|
16830
|
-
if (
|
|
17556
|
+
if (existsSync18(path)) rmSync15(path, { force: true });
|
|
16831
17557
|
} catch {
|
|
16832
17558
|
}
|
|
16833
17559
|
}
|
|
@@ -16882,11 +17608,14 @@ async function createCompanionRuntime(opts = {}) {
|
|
|
16882
17608
|
// process behind the marker pid is this companion (not a recycled pid).
|
|
16883
17609
|
instanceId
|
|
16884
17610
|
});
|
|
17611
|
+
let restartForUpdate = async () => {
|
|
17612
|
+
};
|
|
16885
17613
|
const supervisor = new CompanionSupervisor({
|
|
16886
17614
|
config: cfg,
|
|
16887
17615
|
log,
|
|
16888
17616
|
hub,
|
|
16889
|
-
...opts.checkHarness ? { checkHarness: opts.checkHarness } : {}
|
|
17617
|
+
...opts.checkHarness ? { checkHarness: opts.checkHarness } : {},
|
|
17618
|
+
restartForUpdate: () => restartForUpdate()
|
|
16890
17619
|
});
|
|
16891
17620
|
await supervisor.start();
|
|
16892
17621
|
const connectHarness = async (runtime, serverUrl) => {
|
|
@@ -16911,6 +17640,7 @@ async function createCompanionRuntime(opts = {}) {
|
|
|
16911
17640
|
control = await startControlServer({
|
|
16912
17641
|
status: () => hub.statusJson(),
|
|
16913
17642
|
connect: (runtime, serverUrl) => connectHarness(runtime, serverUrl),
|
|
17643
|
+
update: (auto) => supervisor.update(auto),
|
|
16914
17644
|
stop: () => void supervisor.requestStop()
|
|
16915
17645
|
});
|
|
16916
17646
|
if (opts.dashboard) {
|
|
@@ -16963,6 +17693,10 @@ async function createCompanionRuntime(opts = {}) {
|
|
|
16963
17693
|
} catch {
|
|
16964
17694
|
}
|
|
16965
17695
|
};
|
|
17696
|
+
restartForUpdate = async () => {
|
|
17697
|
+
await stop2();
|
|
17698
|
+
process.exit(0);
|
|
17699
|
+
};
|
|
16966
17700
|
return {
|
|
16967
17701
|
ok: true,
|
|
16968
17702
|
runtime: {
|
|
@@ -16984,41 +17718,437 @@ async function closeSurfaces(control, dashboard) {
|
|
|
16984
17718
|
// src/commands/start.ts
|
|
16985
17719
|
init_term();
|
|
16986
17720
|
|
|
16987
|
-
// src/
|
|
16988
|
-
init_cabane_command();
|
|
16989
|
-
init_cli_entry();
|
|
17721
|
+
// src/service.ts
|
|
16990
17722
|
init_config();
|
|
16991
|
-
import {
|
|
16992
|
-
import {
|
|
16993
|
-
|
|
16994
|
-
|
|
16995
|
-
|
|
16996
|
-
|
|
16997
|
-
|
|
16998
|
-
|
|
16999
|
-
|
|
17000
|
-
|
|
17001
|
-
|
|
17002
|
-
|
|
17003
|
-
|
|
17004
|
-
|
|
17005
|
-
|
|
17006
|
-
|
|
17007
|
-
|
|
17008
|
-
|
|
17009
|
-
|
|
17010
|
-
|
|
17011
|
-
|
|
17012
|
-
|
|
17013
|
-
|
|
17014
|
-
|
|
17015
|
-
|
|
17723
|
+
import { createHash as createHash6 } from "crypto";
|
|
17724
|
+
import {
|
|
17725
|
+
chmodSync as chmodSync7,
|
|
17726
|
+
cpSync,
|
|
17727
|
+
existsSync as existsSync19,
|
|
17728
|
+
mkdirSync as mkdirSync18,
|
|
17729
|
+
readFileSync as readFileSync16,
|
|
17730
|
+
renameSync as renameSync12,
|
|
17731
|
+
rmSync as rmSync16,
|
|
17732
|
+
writeFileSync as writeFileSync12
|
|
17733
|
+
} from "fs";
|
|
17734
|
+
import { homedir as homedir7, userInfo as userInfo3 } from "os";
|
|
17735
|
+
import { dirname as dirname13, join as join28, sep as sep5 } from "path";
|
|
17736
|
+
function servicePlatform(platform = process.platform) {
|
|
17737
|
+
if (platform === "linux") return "systemd";
|
|
17738
|
+
if (platform === "darwin") return "launchd";
|
|
17739
|
+
return null;
|
|
17740
|
+
}
|
|
17741
|
+
function serviceName(env = process.env) {
|
|
17742
|
+
const home = env.CABANE_COMPANION_HOME;
|
|
17743
|
+
if (!home) return "cabane-companion";
|
|
17744
|
+
return `cabane-companion-${createHash6("sha256").update(home).digest("hex").slice(0, 8)}`;
|
|
17745
|
+
}
|
|
17746
|
+
function launchdLabel(env = process.env) {
|
|
17747
|
+
return serviceName(env).replace(/^cabane-companion/, "ai.cabane.companion").replace("-", ".");
|
|
17748
|
+
}
|
|
17749
|
+
function serviceNode(execPath = process.execPath) {
|
|
17750
|
+
return existsSync19(pinnedNodePath()) ? pinnedNodePath() : execPath;
|
|
17751
|
+
}
|
|
17752
|
+
function servicePaths(platform, env = process.env, home = homedir7(), node = serviceNode()) {
|
|
17753
|
+
const unitFile = platform === "systemd" ? join28(
|
|
17754
|
+
env.XDG_CONFIG_HOME || join28(home, ".config"),
|
|
17755
|
+
"systemd",
|
|
17756
|
+
"user",
|
|
17757
|
+
`${serviceName(env)}.service`
|
|
17758
|
+
) : join28(home, "Library", "LaunchAgents", `${launchdLabel(env)}.plist`);
|
|
17759
|
+
return {
|
|
17760
|
+
unitFile,
|
|
17761
|
+
envFile: join28(cabaneDir(), "service.env"),
|
|
17762
|
+
node,
|
|
17763
|
+
entry: bootGuardPath()
|
|
17764
|
+
};
|
|
17765
|
+
}
|
|
17766
|
+
function unitEnvironment(env) {
|
|
17767
|
+
return {
|
|
17768
|
+
CABANE_COMPANION_SUPERVISED: "1",
|
|
17769
|
+
...env.CABANE_COMPANION_HOME ? { CABANE_COMPANION_HOME: env.CABANE_COMPANION_HOME } : {}
|
|
17770
|
+
};
|
|
17771
|
+
}
|
|
17772
|
+
function renderSystemdUnit(paths, env = process.env) {
|
|
17773
|
+
const environment = Object.entries(unitEnvironment(env)).map(
|
|
17774
|
+
([key, value]) => `Environment=${systemdQuote(`${key}=${value}`)}`
|
|
17775
|
+
);
|
|
17776
|
+
return [
|
|
17777
|
+
"# Written by `cabane service install`. Remove with `cabane service uninstall`.",
|
|
17778
|
+
"[Unit]",
|
|
17779
|
+
"Description=Cabane companion",
|
|
17780
|
+
"Wants=network-online.target",
|
|
17781
|
+
"After=network-online.target",
|
|
17782
|
+
// Never stop restarting: a machine nobody watches has nobody to reset a failed unit.
|
|
17783
|
+
"StartLimitIntervalSec=0",
|
|
17784
|
+
"",
|
|
17785
|
+
"[Service]",
|
|
17786
|
+
"Type=simple",
|
|
17787
|
+
`ExecStart=${systemdQuote(paths.node)} ${systemdQuote(paths.entry)} start --foreground`,
|
|
17788
|
+
"Restart=always",
|
|
17789
|
+
"RestartSec=5",
|
|
17790
|
+
...environment,
|
|
17791
|
+
`EnvironmentFile=-${paths.envFile}`,
|
|
17792
|
+
"",
|
|
17793
|
+
"[Install]",
|
|
17794
|
+
"WantedBy=default.target",
|
|
17795
|
+
""
|
|
17796
|
+
].join("\n");
|
|
17797
|
+
}
|
|
17798
|
+
function systemdQuote(value) {
|
|
17799
|
+
return /[\s"\\]/.test(value) ? `"${value.replace(/(["\\])/g, "\\$1")}"` : value;
|
|
17800
|
+
}
|
|
17801
|
+
function renderLaunchdPlist(paths, env = process.env, servicePath) {
|
|
17802
|
+
const xml = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
17803
|
+
const vars = { ...unitEnvironment(env), PATH: servicePath };
|
|
17804
|
+
return [
|
|
17805
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
17806
|
+
'<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
|
|
17807
|
+
"<!-- Written by `cabane service install`. Remove with `cabane service uninstall`. -->",
|
|
17808
|
+
'<plist version="1.0">',
|
|
17809
|
+
"<dict>",
|
|
17810
|
+
` <key>Label</key><string>${xml(launchdLabel(env))}</string>`,
|
|
17811
|
+
" <key>ProgramArguments</key>",
|
|
17812
|
+
" <array>",
|
|
17813
|
+
...[paths.node, paths.entry, "start", "--foreground"].map(
|
|
17814
|
+
(arg) => ` <string>${xml(arg)}</string>`
|
|
17815
|
+
),
|
|
17816
|
+
" </array>",
|
|
17817
|
+
" <key>EnvironmentVariables</key>",
|
|
17818
|
+
" <dict>",
|
|
17819
|
+
...Object.entries(vars).map(
|
|
17820
|
+
([key, value]) => ` <key>${xml(key)}</key><string>${xml(value)}</string>`
|
|
17821
|
+
),
|
|
17822
|
+
" </dict>",
|
|
17823
|
+
" <key>RunAtLoad</key><true/>",
|
|
17824
|
+
" <key>KeepAlive</key><true/>",
|
|
17825
|
+
" <key>ThrottleInterval</key><integer>5</integer>",
|
|
17826
|
+
// The companion writes its own log; the console copy would only grow a second one.
|
|
17827
|
+
" <key>StandardOutPath</key><string>/dev/null</string>",
|
|
17828
|
+
` <key>StandardErrorPath</key><string>${xml(join28(cabaneDir(), "service-error.log"))}</string>`,
|
|
17829
|
+
"</dict>",
|
|
17830
|
+
"</plist>",
|
|
17831
|
+
""
|
|
17832
|
+
].join("\n");
|
|
17833
|
+
}
|
|
17834
|
+
function withServicePath(existing, path) {
|
|
17835
|
+
const kept = existing.split("\n").filter((line) => line.trim() && !/^\s*PATH\s*=/.test(line));
|
|
17836
|
+
return [
|
|
17837
|
+
"# The Cabane service reads this. PATH is your login shell's, written by `cabane service install`.",
|
|
17838
|
+
"# Add a headless sign-in here if this machine has no keychain, e.g. CLAUDE_CODE_OAUTH_TOKEN=\u2026",
|
|
17839
|
+
`PATH=${path}`,
|
|
17840
|
+
...kept.filter(
|
|
17841
|
+
(line) => !line.startsWith("# The Cabane service reads this") && !line.startsWith("# Add a headless sign-in here")
|
|
17842
|
+
),
|
|
17843
|
+
""
|
|
17844
|
+
].join("\n");
|
|
17845
|
+
}
|
|
17846
|
+
async function loginShellPath(run = runProcess, env = process.env) {
|
|
17847
|
+
const shell = env.SHELL;
|
|
17848
|
+
if (shell) {
|
|
17849
|
+
const r = await run(shell, ["-l", "-c", 'printf "%s" "$PATH"'], { timeoutMs: 1e4 });
|
|
17850
|
+
const path = r.stdout.trim().split("\n").at(-1)?.trim();
|
|
17851
|
+
if (r.code === 0 && path && path.includes("/")) return path;
|
|
17852
|
+
}
|
|
17853
|
+
return env.PATH ?? "/usr/local/bin:/usr/bin:/bin";
|
|
17854
|
+
}
|
|
17855
|
+
async function installService(deps = {}) {
|
|
17856
|
+
const run = deps.run ?? runProcess;
|
|
17857
|
+
const env = deps.env ?? process.env;
|
|
17858
|
+
const platform = servicePlatform(deps.platform);
|
|
17859
|
+
if (!platform)
|
|
17860
|
+
return {
|
|
17861
|
+
ok: false,
|
|
17862
|
+
lines: [
|
|
17863
|
+
"Running Cabane as a service works on macOS and Linux. Start it with cabane start here."
|
|
17864
|
+
]
|
|
17865
|
+
};
|
|
17866
|
+
const node = serviceNode(deps.execPath);
|
|
17867
|
+
const seeded = await seedManagedInstall({
|
|
17868
|
+
run,
|
|
17869
|
+
execPath: node,
|
|
17870
|
+
...deps.own ? { own: deps.own } : {}
|
|
17016
17871
|
});
|
|
17017
|
-
|
|
17018
|
-
|
|
17019
|
-
|
|
17020
|
-
|
|
17021
|
-
|
|
17872
|
+
if (!seeded.ok) return { ok: false, lines: [seeded.reason] };
|
|
17873
|
+
if (!writeBootGuard(seeded.root))
|
|
17874
|
+
return {
|
|
17875
|
+
ok: false,
|
|
17876
|
+
lines: [
|
|
17877
|
+
"This install has no boot guard to run the service with. Reinstall Cabane, then run cabane service install again."
|
|
17878
|
+
]
|
|
17879
|
+
};
|
|
17880
|
+
rewriteScriptWrappers();
|
|
17881
|
+
const paths = servicePaths(platform, env, deps.home, node);
|
|
17882
|
+
const servicePath = await loginShellPath(run, env);
|
|
17883
|
+
mkdirSync18(dirname13(paths.envFile), { recursive: true });
|
|
17884
|
+
const existing = existsSync19(paths.envFile) ? readFileSync16(paths.envFile, "utf8") : "";
|
|
17885
|
+
writeFileSync12(paths.envFile, withServicePath(existing, servicePath), { mode: 384 });
|
|
17886
|
+
chmodSync7(paths.envFile, 384);
|
|
17887
|
+
mkdirSync18(dirname13(paths.unitFile), { recursive: true });
|
|
17888
|
+
const outcome = await writeAndStartUnit(platform, paths, env, servicePath, run);
|
|
17889
|
+
if (!outcome.ok) await removeUnit(platform, paths, env, run);
|
|
17890
|
+
return outcome;
|
|
17891
|
+
}
|
|
17892
|
+
async function writeAndStartUnit(platform, paths, env, servicePath, run) {
|
|
17893
|
+
const lines2 = [];
|
|
17894
|
+
if (platform === "systemd") {
|
|
17895
|
+
writeAtomic(paths.unitFile, renderSystemdUnit(paths, env));
|
|
17896
|
+
const unit = `${serviceName(env)}.service`;
|
|
17897
|
+
for (const args of [
|
|
17898
|
+
["--user", "daemon-reload"],
|
|
17899
|
+
["--user", "enable", unit],
|
|
17900
|
+
["--user", "restart", unit]
|
|
17901
|
+
]) {
|
|
17902
|
+
const r = await run("systemctl", args, { timeoutMs: 3e4 });
|
|
17903
|
+
if (r.code !== 0)
|
|
17904
|
+
return {
|
|
17905
|
+
ok: false,
|
|
17906
|
+
lines: [
|
|
17907
|
+
`systemctl ${args.join(" ")} failed: ${r.stderr.trim() || `exit ${r.code}`}`,
|
|
17908
|
+
"This needs a systemd user session. Start Cabane with cabane start instead."
|
|
17909
|
+
]
|
|
17910
|
+
};
|
|
17911
|
+
}
|
|
17912
|
+
const linger = await run("loginctl", ["show-user", userName(), "--property=Linger"], {
|
|
17913
|
+
timeoutMs: 1e4
|
|
17914
|
+
});
|
|
17915
|
+
if (linger.code === 0 && /Linger=no/.test(linger.stdout)) {
|
|
17916
|
+
lines2.push(
|
|
17917
|
+
"! It stops when you log out of this computer. To keep it running after logout, run once:",
|
|
17918
|
+
` sudo loginctl enable-linger ${userName()}`
|
|
17919
|
+
);
|
|
17920
|
+
}
|
|
17921
|
+
} else {
|
|
17922
|
+
writeAtomic(paths.unitFile, renderLaunchdPlist(paths, env, servicePath));
|
|
17923
|
+
const domain = `gui/${process.getuid?.() ?? userInfo3().uid}`;
|
|
17924
|
+
await run("launchctl", ["bootout", `${domain}/${launchdLabel(env)}`], { timeoutMs: 3e4 });
|
|
17925
|
+
const r = await run("launchctl", ["bootstrap", domain, paths.unitFile], { timeoutMs: 3e4 });
|
|
17926
|
+
if (r.code !== 0)
|
|
17927
|
+
return {
|
|
17928
|
+
ok: false,
|
|
17929
|
+
lines: [`launchctl bootstrap failed: ${r.stderr.trim() || `exit ${r.code}`}`]
|
|
17930
|
+
};
|
|
17931
|
+
}
|
|
17932
|
+
return { ok: true, lines: lines2 };
|
|
17933
|
+
}
|
|
17934
|
+
async function uninstallService(deps = {}) {
|
|
17935
|
+
const run = deps.run ?? runProcess;
|
|
17936
|
+
const env = deps.env ?? process.env;
|
|
17937
|
+
const platform = servicePlatform(deps.platform);
|
|
17938
|
+
if (!platform) return { ok: true, lines: [] };
|
|
17939
|
+
const paths = servicePaths(platform, env, deps.home);
|
|
17940
|
+
if (!existsSync19(paths.unitFile)) return { ok: true, lines: ["No Cabane service is installed."] };
|
|
17941
|
+
await removeUnit(platform, paths, env, run);
|
|
17942
|
+
return { ok: true, lines: [] };
|
|
17943
|
+
}
|
|
17944
|
+
async function removeUnit(platform, paths, env, run) {
|
|
17945
|
+
if (platform === "systemd") {
|
|
17946
|
+
await run("systemctl", ["--user", "disable", "--now", `${serviceName(env)}.service`], {
|
|
17947
|
+
timeoutMs: 3e4
|
|
17948
|
+
});
|
|
17949
|
+
rmSync16(paths.unitFile, { force: true });
|
|
17950
|
+
await run("systemctl", ["--user", "daemon-reload"], { timeoutMs: 3e4 });
|
|
17951
|
+
} else {
|
|
17952
|
+
const domain = `gui/${process.getuid?.() ?? userInfo3().uid}`;
|
|
17953
|
+
await run("launchctl", ["bootout", `${domain}/${launchdLabel(env)}`], { timeoutMs: 3e4 });
|
|
17954
|
+
rmSync16(paths.unitFile, { force: true });
|
|
17955
|
+
}
|
|
17956
|
+
}
|
|
17957
|
+
async function seedManagedInstall(deps) {
|
|
17958
|
+
const current = currentRoot();
|
|
17959
|
+
if (current && current.startsWith(versionsDir() + sep5)) return { ok: true, root: current };
|
|
17960
|
+
const own = deps.own ?? ownPackageRoot();
|
|
17961
|
+
const version = packageVersion(own);
|
|
17962
|
+
if (!version || version === "0.0.0") {
|
|
17963
|
+
pointCurrentAt(own);
|
|
17964
|
+
return { ok: true, root: own };
|
|
17965
|
+
}
|
|
17966
|
+
const target = versionPackageRoot(version);
|
|
17967
|
+
if (!existsSync19(target)) {
|
|
17968
|
+
const staging = join28(versionsDir(), `.${version}.seed-${process.pid}`);
|
|
17969
|
+
rmSync16(staging, { recursive: true, force: true });
|
|
17970
|
+
try {
|
|
17971
|
+
cpSync(own, join28(staging, "node_modules", "@cabane", "companion"), {
|
|
17972
|
+
recursive: true,
|
|
17973
|
+
verbatimSymlinks: true
|
|
17974
|
+
});
|
|
17975
|
+
rmSync16(join28(versionsDir(), version), { recursive: true, force: true });
|
|
17976
|
+
renameSync12(staging, join28(versionsDir(), version));
|
|
17977
|
+
} catch (err) {
|
|
17978
|
+
rmSync16(staging, { recursive: true, force: true });
|
|
17979
|
+
return {
|
|
17980
|
+
ok: false,
|
|
17981
|
+
reason: `Couldn't copy Cabane into ${tildeDir()}/versions for the service: ${err instanceof Error ? err.message : String(err)}`
|
|
17982
|
+
};
|
|
17983
|
+
}
|
|
17984
|
+
}
|
|
17985
|
+
const unrunnable = await verifyInstall(target, version, [], deps);
|
|
17986
|
+
if (unrunnable) {
|
|
17987
|
+
rmSync16(join28(versionsDir(), version), { recursive: true, force: true });
|
|
17988
|
+
return { ok: false, reason: `The copy of Cabane for the service can't run: ${unrunnable}.` };
|
|
17989
|
+
}
|
|
17990
|
+
pointCurrentAt(target);
|
|
17991
|
+
return { ok: true, root: target };
|
|
17992
|
+
}
|
|
17993
|
+
function tildeDir() {
|
|
17994
|
+
return cabaneDir().replace(homedir7(), "~");
|
|
17995
|
+
}
|
|
17996
|
+
function unitNode(platform, unitFile) {
|
|
17997
|
+
let text;
|
|
17998
|
+
try {
|
|
17999
|
+
text = readFileSync16(unitFile, "utf8");
|
|
18000
|
+
} catch {
|
|
18001
|
+
return null;
|
|
18002
|
+
}
|
|
18003
|
+
const found = platform === "systemd" ? /^ExecStart=("(?:[^"\\]|\\.)*"|\S+)/m.exec(text)?.[1] : /<key>ProgramArguments<\/key>\s*<array>\s*<string>([^<]*)<\/string>/.exec(text)?.[1];
|
|
18004
|
+
if (!found) return null;
|
|
18005
|
+
return found.startsWith('"') ? found.slice(1, -1).replace(/\\(.)/g, "$1") : found;
|
|
18006
|
+
}
|
|
18007
|
+
async function serviceState(deps = {}) {
|
|
18008
|
+
const run = deps.run ?? runProcess;
|
|
18009
|
+
const env = deps.env ?? process.env;
|
|
18010
|
+
const platform = servicePlatform(deps.platform);
|
|
18011
|
+
const none = {
|
|
18012
|
+
installed: false,
|
|
18013
|
+
platform,
|
|
18014
|
+
state: "not installed",
|
|
18015
|
+
pid: null,
|
|
18016
|
+
node: null,
|
|
18017
|
+
nodeOk: true
|
|
18018
|
+
};
|
|
18019
|
+
if (!platform) return none;
|
|
18020
|
+
const paths = servicePaths(platform, env, deps.home);
|
|
18021
|
+
if (!existsSync19(paths.unitFile)) return none;
|
|
18022
|
+
const node = unitNode(platform, paths.unitFile);
|
|
18023
|
+
const nodeOk = node === null || existsSync19(node);
|
|
18024
|
+
if (platform === "systemd") {
|
|
18025
|
+
const r2 = await run(
|
|
18026
|
+
"systemctl",
|
|
18027
|
+
["--user", "show", `${serviceName(env)}.service`, "--property=ActiveState,SubState,MainPID"],
|
|
18028
|
+
{ timeoutMs: 1e4 }
|
|
18029
|
+
);
|
|
18030
|
+
const prop = (name) => new RegExp(`^${name}=(.*)$`, "m").exec(r2.stdout)?.[1] ?? "";
|
|
18031
|
+
const active = prop("ActiveState");
|
|
18032
|
+
const pid2 = Number(prop("MainPID")) || null;
|
|
18033
|
+
return {
|
|
18034
|
+
installed: true,
|
|
18035
|
+
platform,
|
|
18036
|
+
state: active === "active" ? "running" : prop("SubState") || active || "unknown",
|
|
18037
|
+
pid: pid2,
|
|
18038
|
+
node,
|
|
18039
|
+
nodeOk
|
|
18040
|
+
};
|
|
18041
|
+
}
|
|
18042
|
+
const domain = `gui/${process.getuid?.() ?? userInfo3().uid}`;
|
|
18043
|
+
const r = await run("launchctl", ["print", `${domain}/${launchdLabel(env)}`], {
|
|
18044
|
+
timeoutMs: 1e4
|
|
18045
|
+
});
|
|
18046
|
+
if (r.code !== 0)
|
|
18047
|
+
return { installed: true, platform, state: "not loaded", pid: null, node, nodeOk };
|
|
18048
|
+
const state = /^\s*state = (.+)$/m.exec(r.stdout)?.[1]?.trim() ?? "unknown";
|
|
18049
|
+
const pid = Number(/^\s*pid = (\d+)$/m.exec(r.stdout)?.[1]) || null;
|
|
18050
|
+
return { installed: true, platform, state, pid, node, nodeOk };
|
|
18051
|
+
}
|
|
18052
|
+
function serviceInstalled(deps = {}) {
|
|
18053
|
+
const platform = servicePlatform(deps.platform);
|
|
18054
|
+
if (!platform) return false;
|
|
18055
|
+
return existsSync19(servicePaths(platform, deps.env ?? process.env, deps.home).unitFile);
|
|
18056
|
+
}
|
|
18057
|
+
function writeAtomic(path, content) {
|
|
18058
|
+
const tmp = `${path}.${process.pid}.tmp`;
|
|
18059
|
+
writeFileSync12(tmp, content);
|
|
18060
|
+
renameSync12(tmp, path);
|
|
18061
|
+
}
|
|
18062
|
+
function userName() {
|
|
18063
|
+
try {
|
|
18064
|
+
return userInfo3().username;
|
|
18065
|
+
} catch {
|
|
18066
|
+
return process.env.USER ?? "";
|
|
18067
|
+
}
|
|
18068
|
+
}
|
|
18069
|
+
function rewriteScriptWrappers(dir2 = join28(cabaneDir(), "bin")) {
|
|
18070
|
+
for (const name of ["cabane", "cabane-companion"]) {
|
|
18071
|
+
const path = join28(dir2, name);
|
|
18072
|
+
let text;
|
|
18073
|
+
try {
|
|
18074
|
+
text = readFileSync16(path, "utf8");
|
|
18075
|
+
} catch {
|
|
18076
|
+
continue;
|
|
18077
|
+
}
|
|
18078
|
+
if (!text.includes("Written by https://cabane.ai/install.sh")) continue;
|
|
18079
|
+
if (!text.includes("/dist/cli.js")) continue;
|
|
18080
|
+
writeAtomic(path, text.replace("/dist/cli.js", "/dist/bin.js"));
|
|
18081
|
+
chmodSync7(path, 493);
|
|
18082
|
+
}
|
|
18083
|
+
}
|
|
18084
|
+
async function startService(deps = {}) {
|
|
18085
|
+
return controlService("start", deps);
|
|
18086
|
+
}
|
|
18087
|
+
async function stopService(deps = {}) {
|
|
18088
|
+
return controlService("stop", deps);
|
|
18089
|
+
}
|
|
18090
|
+
async function controlService(action, deps) {
|
|
18091
|
+
const run = deps.run ?? runProcess;
|
|
18092
|
+
const env = deps.env ?? process.env;
|
|
18093
|
+
const platform = servicePlatform(deps.platform);
|
|
18094
|
+
if (!platform) return { ok: false, lines: [] };
|
|
18095
|
+
const paths = servicePaths(platform, env, deps.home);
|
|
18096
|
+
const r = platform === "systemd" ? await run("systemctl", ["--user", action, `${serviceName(env)}.service`], {
|
|
18097
|
+
timeoutMs: 3e4
|
|
18098
|
+
}) : action === "start" ? await run(
|
|
18099
|
+
"launchctl",
|
|
18100
|
+
["bootstrap", `gui/${process.getuid?.() ?? userInfo3().uid}`, paths.unitFile],
|
|
18101
|
+
{
|
|
18102
|
+
timeoutMs: 3e4
|
|
18103
|
+
}
|
|
18104
|
+
) : await run(
|
|
18105
|
+
"launchctl",
|
|
18106
|
+
["bootout", `gui/${process.getuid?.() ?? userInfo3().uid}/${launchdLabel(env)}`],
|
|
18107
|
+
{
|
|
18108
|
+
timeoutMs: 3e4
|
|
18109
|
+
}
|
|
18110
|
+
);
|
|
18111
|
+
return r.code === 0 ? { ok: true, lines: [] } : {
|
|
18112
|
+
ok: false,
|
|
18113
|
+
lines: [`Couldn't ${action} the Cabane service: ${r.stderr.trim() || `exit ${r.code}`}`]
|
|
18114
|
+
};
|
|
18115
|
+
}
|
|
18116
|
+
|
|
18117
|
+
// src/commands/daemon.ts
|
|
18118
|
+
init_cabane_command();
|
|
18119
|
+
init_cli_entry();
|
|
18120
|
+
init_config();
|
|
18121
|
+
import { spawn as spawn9 } from "child_process";
|
|
18122
|
+
import { closeSync as closeSync3, mkdirSync as mkdirSync19, openSync as openSync4 } from "fs";
|
|
18123
|
+
var STARTUP_TIMEOUT_MS = 8e3;
|
|
18124
|
+
var POLL_INTERVAL_MS = 150;
|
|
18125
|
+
async function startDaemon(opts = {}, deps = {}) {
|
|
18126
|
+
const ensureRuntime = deps.ensureRuntime ?? warnAboutHarnessReadiness;
|
|
18127
|
+
const readState = deps.readState ?? readLiveRuntimeState;
|
|
18128
|
+
const verify = deps.verify ?? ((s) => verifyRuntime(s));
|
|
18129
|
+
const spawnDetached = deps.spawnDetached ?? defaultSpawnDetached;
|
|
18130
|
+
const sleep5 = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
18131
|
+
const now = deps.now ?? (() => Date.now());
|
|
18132
|
+
const quiet = opts.report === "failures";
|
|
18133
|
+
const cfg = requireStartConfig({
|
|
18134
|
+
// Unset → `requireStartConfig`'s own `requireConfig`, the real one.
|
|
18135
|
+
...deps.requireCfg ? { requireCfg: deps.requireCfg } : {},
|
|
18136
|
+
...deps.save ? { save: deps.save } : {},
|
|
18137
|
+
// The child won't log it (the block is written by then), and this process owns
|
|
18138
|
+
// the user's terminal — so the one-time record is one line, here.
|
|
18139
|
+
onMigrated: () => {
|
|
18140
|
+
if (!quiet) {
|
|
18141
|
+
process.stdout.write(
|
|
18142
|
+
"Carried Claude Code over as a connected harness on this device \u2014 connectors are chosen now, not detected.\n"
|
|
18143
|
+
);
|
|
18144
|
+
}
|
|
18145
|
+
}
|
|
18146
|
+
});
|
|
18147
|
+
ensureRuntime(cfg, {
|
|
18148
|
+
// The onboarding script already said what's connectable, in its own
|
|
18149
|
+
// words. Saying it again here, on stderr, in the middle of the closing block
|
|
18150
|
+
// would be the same news twice.
|
|
18151
|
+
...quiet ? { warn: () => {
|
|
17022
18152
|
} } : {}
|
|
17023
18153
|
});
|
|
17024
18154
|
const existing = readState();
|
|
@@ -17079,10 +18209,10 @@ function alreadyRunningLines(pid) {
|
|
|
17079
18209
|
}
|
|
17080
18210
|
function defaultSpawnDetached(args) {
|
|
17081
18211
|
const cliPath = companionCliEntry();
|
|
17082
|
-
|
|
17083
|
-
const logFd =
|
|
18212
|
+
mkdirSync19(cabaneDir(), { recursive: true });
|
|
18213
|
+
const logFd = openSync4(companionLogPath(), "a");
|
|
17084
18214
|
try {
|
|
17085
|
-
return
|
|
18215
|
+
return spawn9(process.execPath, [cliPath, ...args], {
|
|
17086
18216
|
detached: true,
|
|
17087
18217
|
stdio: ["ignore", logFd, logFd],
|
|
17088
18218
|
env: { ...process.env, CABANE_COMPANION_DAEMON: "1" }
|
|
@@ -17092,6 +18222,209 @@ function defaultSpawnDetached(args) {
|
|
|
17092
18222
|
}
|
|
17093
18223
|
}
|
|
17094
18224
|
|
|
18225
|
+
// src/commands/service.ts
|
|
18226
|
+
init_cabane_command();
|
|
18227
|
+
init_config();
|
|
18228
|
+
init_term();
|
|
18229
|
+
init_version2();
|
|
18230
|
+
|
|
18231
|
+
// src/commands/stop.ts
|
|
18232
|
+
var TERM_GRACE_MS = 6e3;
|
|
18233
|
+
var POLL_INTERVAL_MS2 = 150;
|
|
18234
|
+
async function stop(deps = {}) {
|
|
18235
|
+
if ((deps.serviceInstalled ?? serviceInstalled)()) {
|
|
18236
|
+
const outcome = await (deps.stopService ?? stopService)();
|
|
18237
|
+
for (const line of outcome.lines) process.stdout.write(`${line}
|
|
18238
|
+
`);
|
|
18239
|
+
if (!outcome.ok) {
|
|
18240
|
+
process.exitCode = 1;
|
|
18241
|
+
return;
|
|
18242
|
+
}
|
|
18243
|
+
process.stdout.write(
|
|
18244
|
+
"companion: stopped the service. It starts again at boot, or with `cabane start`.\n"
|
|
18245
|
+
);
|
|
18246
|
+
return;
|
|
18247
|
+
}
|
|
18248
|
+
const readState = deps.readState ?? readLiveRuntimeState;
|
|
18249
|
+
const verify = deps.verify ?? ((s) => verifyRuntime(s));
|
|
18250
|
+
const kill = deps.kill ?? ((pid2, signal) => process.kill(pid2, signal));
|
|
18251
|
+
const sleep5 = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
18252
|
+
const now = deps.now ?? (() => Date.now());
|
|
18253
|
+
const state = readState();
|
|
18254
|
+
if (!state) {
|
|
18255
|
+
process.stdout.write("companion: not running (nothing to stop).\n");
|
|
18256
|
+
return;
|
|
18257
|
+
}
|
|
18258
|
+
if (await verify(state) === "stale") {
|
|
18259
|
+
clearRuntimeState();
|
|
18260
|
+
process.stdout.write("companion: not running (stale marker swept).\n");
|
|
18261
|
+
return;
|
|
18262
|
+
}
|
|
18263
|
+
const { pid } = state;
|
|
18264
|
+
process.stdout.write(`companion: stopping (pid ${pid})\u2026
|
|
18265
|
+
`);
|
|
18266
|
+
try {
|
|
18267
|
+
kill(pid, "SIGTERM");
|
|
18268
|
+
} catch {
|
|
18269
|
+
clearRuntimeState();
|
|
18270
|
+
process.stdout.write("companion: already stopped.\n");
|
|
18271
|
+
return;
|
|
18272
|
+
}
|
|
18273
|
+
const deadline = now() + TERM_GRACE_MS;
|
|
18274
|
+
while (now() < deadline) {
|
|
18275
|
+
if (!isAlive(kill, pid)) {
|
|
18276
|
+
process.stdout.write("companion: stopped.\n");
|
|
18277
|
+
return;
|
|
18278
|
+
}
|
|
18279
|
+
await sleep5(POLL_INTERVAL_MS2);
|
|
18280
|
+
}
|
|
18281
|
+
process.stdout.write(
|
|
18282
|
+
`companion: didn't exit within ${TERM_GRACE_MS / 1e3}s, sending SIGKILL.
|
|
18283
|
+
`
|
|
18284
|
+
);
|
|
18285
|
+
try {
|
|
18286
|
+
kill(pid, "SIGKILL");
|
|
18287
|
+
} catch {
|
|
18288
|
+
}
|
|
18289
|
+
clearRuntimeState();
|
|
18290
|
+
process.stdout.write("companion: force-stopped.\n");
|
|
18291
|
+
}
|
|
18292
|
+
function isAlive(kill, pid) {
|
|
18293
|
+
try {
|
|
18294
|
+
kill(pid, 0);
|
|
18295
|
+
return true;
|
|
18296
|
+
} catch {
|
|
18297
|
+
return false;
|
|
18298
|
+
}
|
|
18299
|
+
}
|
|
18300
|
+
|
|
18301
|
+
// src/commands/service.ts
|
|
18302
|
+
var STARTUP_WAIT_MS = 2e4;
|
|
18303
|
+
async function installAndStartService(deps = {}) {
|
|
18304
|
+
const readState = deps.readState ?? readLiveRuntimeState;
|
|
18305
|
+
const sleep5 = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
18306
|
+
const now = deps.now ?? Date.now;
|
|
18307
|
+
const before = await serviceState(deps);
|
|
18308
|
+
const running = readState();
|
|
18309
|
+
if (running && running.pid !== before.pid) await (deps.stopRunning ?? (() => stop()))();
|
|
18310
|
+
setRunsAsService(true);
|
|
18311
|
+
const outcome = await installService(deps);
|
|
18312
|
+
for (const line of outcome.lines) write(`${INDENT}${line}`);
|
|
18313
|
+
if (!outcome.ok) {
|
|
18314
|
+
setRunsAsService(false);
|
|
18315
|
+
return false;
|
|
18316
|
+
}
|
|
18317
|
+
const deadline = now() + STARTUP_WAIT_MS;
|
|
18318
|
+
while (now() < deadline) {
|
|
18319
|
+
const state = readState();
|
|
18320
|
+
if (state?.socket) return true;
|
|
18321
|
+
await sleep5(250);
|
|
18322
|
+
}
|
|
18323
|
+
write(
|
|
18324
|
+
`${INDENT}The service is installed but Cabane hasn't come up yet. ${cabane("service status")} and ${cabane("logs")} say why.`
|
|
18325
|
+
);
|
|
18326
|
+
return false;
|
|
18327
|
+
}
|
|
18328
|
+
async function serviceInstall(deps = {}) {
|
|
18329
|
+
if (!await installAndStartService(deps)) {
|
|
18330
|
+
process.exitCode = 1;
|
|
18331
|
+
return;
|
|
18332
|
+
}
|
|
18333
|
+
write(
|
|
18334
|
+
INDENT + tick(
|
|
18335
|
+
"Cabane runs as a service. It starts at boot, restarts if it stops, and updates itself between turns."
|
|
18336
|
+
)
|
|
18337
|
+
);
|
|
18338
|
+
write(`${INDENT} Status: ${cabane("service status")} Logs: ${cabane("logs")}`);
|
|
18339
|
+
}
|
|
18340
|
+
async function serviceUninstall(deps = {}) {
|
|
18341
|
+
const outcome = await uninstallService(deps);
|
|
18342
|
+
for (const line of outcome.lines) write(`${INDENT}${line}`);
|
|
18343
|
+
if (outcome.lines.length > 0) return;
|
|
18344
|
+
setRunsAsService(false);
|
|
18345
|
+
write(
|
|
18346
|
+
INDENT + tick("Removed the service. Cabane won't start at boot, and it isn't running now.")
|
|
18347
|
+
);
|
|
18348
|
+
write(`${INDENT} Start it again with ${cabane("start")}`);
|
|
18349
|
+
}
|
|
18350
|
+
async function serviceStatus(deps = {}) {
|
|
18351
|
+
const state = await serviceState(deps);
|
|
18352
|
+
for (const line of serviceRows(state)) write(line);
|
|
18353
|
+
for (const line of updateRows(readUpdateState(), safeConfig()?.update?.auto, {
|
|
18354
|
+
service: state.installed
|
|
18355
|
+
}))
|
|
18356
|
+
write(line);
|
|
18357
|
+
if (state.installed && state.state !== "running") process.exitCode = 1;
|
|
18358
|
+
}
|
|
18359
|
+
var LABEL_WIDTH = 13;
|
|
18360
|
+
var row = (label, value) => ` ${label.padEnd(LABEL_WIDTH)}${value}`;
|
|
18361
|
+
function serviceRows(state) {
|
|
18362
|
+
if (!state.installed) {
|
|
18363
|
+
return [
|
|
18364
|
+
row(
|
|
18365
|
+
"Service",
|
|
18366
|
+
state.platform ? `not installed \u2014 ${cabane("service install")} starts Cabane at boot` : "not available on this system"
|
|
18367
|
+
)
|
|
18368
|
+
];
|
|
18369
|
+
}
|
|
18370
|
+
const via = state.platform === "systemd" ? "systemd" : "launchd";
|
|
18371
|
+
const rows = [
|
|
18372
|
+
row(
|
|
18373
|
+
"Service",
|
|
18374
|
+
state.state === "running" ? `running (${via}${state.pid ? `, pid ${state.pid}` : ""})` : `installed, not running (${via}: ${state.state})`
|
|
18375
|
+
)
|
|
18376
|
+
];
|
|
18377
|
+
if (!state.nodeOk) {
|
|
18378
|
+
rows.push(
|
|
18379
|
+
` ! The service's Node at ${tildePath(state.node ?? "")} is gone. Run ${cabane("service install")} again.`
|
|
18380
|
+
);
|
|
18381
|
+
}
|
|
18382
|
+
return rows;
|
|
18383
|
+
}
|
|
18384
|
+
function updateRows(state, auto, opts = {}) {
|
|
18385
|
+
const rows = [];
|
|
18386
|
+
if (opts.version !== false) rows.push(row("Version", COMPANION_VERSION));
|
|
18387
|
+
if (state.server) {
|
|
18388
|
+
rows.push(
|
|
18389
|
+
row(
|
|
18390
|
+
"Cabane wants",
|
|
18391
|
+
`${state.server.latest} (the latest), ${state.server.minimum} or newer to run turns`
|
|
18392
|
+
)
|
|
18393
|
+
);
|
|
18394
|
+
}
|
|
18395
|
+
rows.push(
|
|
18396
|
+
row(
|
|
18397
|
+
"Auto-update",
|
|
18398
|
+
!opts.service ? `off \u2014 runs with the service: ${cabane("service install")}` : auto === true ? "on \u2014 updates install between turns and roll back if they fail" : `off \u2014 ${cabane("update --auto on")} turns it on`
|
|
18399
|
+
)
|
|
18400
|
+
);
|
|
18401
|
+
const last = state.lastResult;
|
|
18402
|
+
if (last) {
|
|
18403
|
+
const when = new Date(last.at).toLocaleDateString("en-GB", { day: "numeric", month: "short" });
|
|
18404
|
+
const why = last.reason ? `: ${last.reason}` : "";
|
|
18405
|
+
rows.push(
|
|
18406
|
+
row(
|
|
18407
|
+
"Last update",
|
|
18408
|
+
last.kind === "updated" ? `${last.from} \u2192 ${last.to} on ${when}` : last.kind === "rolled_back" ? `${last.to} rolled back to ${last.from} on ${when}${why}` : `${last.to} failed on ${when}${why}`
|
|
18409
|
+
)
|
|
18410
|
+
);
|
|
18411
|
+
}
|
|
18412
|
+
return rows;
|
|
18413
|
+
}
|
|
18414
|
+
function safeConfig() {
|
|
18415
|
+
try {
|
|
18416
|
+
return loadConfig();
|
|
18417
|
+
} catch {
|
|
18418
|
+
return null;
|
|
18419
|
+
}
|
|
18420
|
+
}
|
|
18421
|
+
function setRunsAsService(on) {
|
|
18422
|
+
const cfg = safeConfig();
|
|
18423
|
+
if (!cfg) return;
|
|
18424
|
+
const { serviceDeclined: _declined, ...rest } = cfg;
|
|
18425
|
+
saveConfig({ ...rest, update: { auto: on } });
|
|
18426
|
+
}
|
|
18427
|
+
|
|
17095
18428
|
// src/commands/start.ts
|
|
17096
18429
|
var FORCE_EXIT_MS = 4e3;
|
|
17097
18430
|
var MAX_CODES = 3;
|
|
@@ -17116,6 +18449,10 @@ async function startScript(opts, interactive) {
|
|
|
17116
18449
|
await startSupervised(opts, interactive, daemon);
|
|
17117
18450
|
return;
|
|
17118
18451
|
}
|
|
18452
|
+
if (serviceInstalled()) {
|
|
18453
|
+
await startTheService();
|
|
18454
|
+
return;
|
|
18455
|
+
}
|
|
17119
18456
|
await startForPerson(opts, interactive);
|
|
17120
18457
|
}
|
|
17121
18458
|
async function surveyAgents(cfg, deps = {}) {
|
|
@@ -17499,7 +18836,7 @@ function openLink(link, spinner) {
|
|
|
17499
18836
|
`${INDENT}Couldn't open a browser here. Open this link on any device: ${link}`
|
|
17500
18837
|
);
|
|
17501
18838
|
try {
|
|
17502
|
-
const child =
|
|
18839
|
+
const child = spawn10(opener, [link], { stdio: "ignore", detached: true });
|
|
17503
18840
|
child.once("error", failed);
|
|
17504
18841
|
child.once("exit", (code) => {
|
|
17505
18842
|
if (code !== 0) failed();
|
|
@@ -17510,32 +18847,67 @@ function openLink(link, spinner) {
|
|
|
17510
18847
|
}
|
|
17511
18848
|
}
|
|
17512
18849
|
async function handOffToBackground(runtime, ctx2) {
|
|
18850
|
+
const { asService } = ctx2.justPaired ? await askHowToRun() : { asService: false };
|
|
17513
18851
|
await runtime.stop();
|
|
17514
|
-
|
|
17515
|
-
|
|
17516
|
-
|
|
18852
|
+
if (asService) {
|
|
18853
|
+
if (!await installAndStartService()) {
|
|
18854
|
+
write(`${INDENT}Running it in the background instead.`);
|
|
18855
|
+
if (!(await startDaemon({ report: "failures", ...ctx2 })).started) return;
|
|
18856
|
+
}
|
|
18857
|
+
} else {
|
|
18858
|
+
const outcome = await startDaemon({ report: "failures", ...ctx2 });
|
|
18859
|
+
if (!outcome.started) {
|
|
18860
|
+
return;
|
|
18861
|
+
}
|
|
17517
18862
|
}
|
|
18863
|
+
const supervised = asService && serviceInstalled();
|
|
17518
18864
|
for (const label of ctx2.connected) write(INDENT + tick(`${label} connected.`));
|
|
17519
18865
|
for (const runtime2 of ctx2.signedOut) {
|
|
17520
18866
|
write(
|
|
17521
18867
|
`${INDENT}! ${HARNESS_LABELS[runtime2]} is connected here but signed out. Run ${loginCommand(runtime2)} to use it again.`
|
|
17522
18868
|
);
|
|
17523
18869
|
}
|
|
18870
|
+
const where = supervised ? "as a service, and starts at boot" : "in the background";
|
|
17524
18871
|
write(
|
|
17525
18872
|
INDENT + tick(
|
|
17526
|
-
ctx2.justPaired ?
|
|
18873
|
+
ctx2.justPaired ? `Cabane is running ${where}. You can close this window.` : `Cabane is running ${where}.`
|
|
17527
18874
|
)
|
|
17528
18875
|
);
|
|
17529
18876
|
write(` Stop: ${cabane("stop")} Logs: ${cabane("logs")}`);
|
|
17530
18877
|
if (ctx2.justPaired) {
|
|
17531
|
-
|
|
17532
|
-
|
|
17533
|
-
|
|
17534
|
-
|
|
18878
|
+
if (!supervised) {
|
|
18879
|
+
blank();
|
|
18880
|
+
write(
|
|
18881
|
+
`${INDENT}! It won't restart on its own after a reboot \u2014 run ${cabane("start")} again, or ${cabane("service install")} to start it at boot.`
|
|
18882
|
+
);
|
|
18883
|
+
}
|
|
17535
18884
|
blank();
|
|
17536
18885
|
write(`${INDENT}All set on this side. Finish up in Cabane.`);
|
|
17537
18886
|
}
|
|
17538
18887
|
}
|
|
18888
|
+
async function askHowToRun(deps = {}) {
|
|
18889
|
+
const ask = deps.confirm ?? confirm;
|
|
18890
|
+
const cfg = safeLoadConfig();
|
|
18891
|
+
if (!cfg || !servicePlatform() || cfg.serviceDeclined || serviceInstalled())
|
|
18892
|
+
return { asService: false };
|
|
18893
|
+
blank();
|
|
18894
|
+
const asService = await ask(
|
|
18895
|
+
"Keep Cabane running and up to date on this computer? It starts at boot, restarts if it stops, and updates itself between turns."
|
|
18896
|
+
);
|
|
18897
|
+
if (!asService) saveConfig({ ...cfg, serviceDeclined: true });
|
|
18898
|
+
blank();
|
|
18899
|
+
return { asService };
|
|
18900
|
+
}
|
|
18901
|
+
async function startTheService() {
|
|
18902
|
+
const outcome = await startService();
|
|
18903
|
+
for (const line of outcome.lines) write(`${INDENT}${line}`);
|
|
18904
|
+
if (!outcome.ok) {
|
|
18905
|
+
process.exitCode = 1;
|
|
18906
|
+
return;
|
|
18907
|
+
}
|
|
18908
|
+
write(INDENT + tick("Cabane runs as a service here, and is starting."));
|
|
18909
|
+
write(` Status: ${cabane("service status")} Logs: ${cabane("logs")}`);
|
|
18910
|
+
}
|
|
17539
18911
|
async function runAttached(runtime) {
|
|
17540
18912
|
await new Promise((resolve2) => {
|
|
17541
18913
|
let shuttingDown = false;
|
|
@@ -17607,7 +18979,7 @@ async function pair(opts = {}, deps = {}) {
|
|
|
17607
18979
|
}
|
|
17608
18980
|
|
|
17609
18981
|
// src/cli.ts
|
|
17610
|
-
import { readFileSync as
|
|
18982
|
+
import { readFileSync as readFileSync18 } from "fs";
|
|
17611
18983
|
|
|
17612
18984
|
// src/commands/logs.ts
|
|
17613
18985
|
init_cabane_command();
|
|
@@ -17691,18 +19063,18 @@ async function logs(opts = {}) {
|
|
|
17691
19063
|
// src/commands/status.ts
|
|
17692
19064
|
init_bundled_binaries();
|
|
17693
19065
|
init_cabane_command();
|
|
17694
|
-
import { readdirSync as
|
|
17695
|
-
import { homedir as
|
|
17696
|
-
import { dirname as
|
|
19066
|
+
import { readdirSync as readdirSync7, realpathSync as realpathSync7, existsSync as existsSync20 } from "fs";
|
|
19067
|
+
import { homedir as homedir8 } from "os";
|
|
19068
|
+
import { dirname as dirname14, join as join29 } from "path";
|
|
17697
19069
|
init_config();
|
|
17698
19070
|
init_sandbox_policy();
|
|
17699
19071
|
init_secrets();
|
|
17700
19072
|
init_term();
|
|
17701
19073
|
init_transcript_writer();
|
|
17702
19074
|
init_version2();
|
|
17703
|
-
var
|
|
17704
|
-
function
|
|
17705
|
-
return ` ${label.padEnd(
|
|
19075
|
+
var LABEL_WIDTH2 = 13;
|
|
19076
|
+
function row2(label, value) {
|
|
19077
|
+
return ` ${label.padEnd(LABEL_WIDTH2)}${value}`;
|
|
17706
19078
|
}
|
|
17707
19079
|
async function status() {
|
|
17708
19080
|
const out = (line) => void process.stdout.write(`${line}
|
|
@@ -17720,29 +19092,36 @@ async function status() {
|
|
|
17720
19092
|
process.exitCode = 1;
|
|
17721
19093
|
return;
|
|
17722
19094
|
}
|
|
17723
|
-
out(
|
|
19095
|
+
out(row2("Paired with", `${cfg.baseUrl}${cfg.deviceLabel ? ` as ${cfg.deviceLabel}` : ""}`));
|
|
17724
19096
|
const running = await liveState();
|
|
17725
19097
|
let harnesses = null;
|
|
17726
19098
|
if (running) {
|
|
17727
19099
|
const mode = running.daemon ? "in the background" : "in a terminal";
|
|
17728
19100
|
const uptime = formatUptime(running.startedAt);
|
|
17729
|
-
out(
|
|
19101
|
+
out(row2("Running", `${mode} (pid ${running.pid}${uptime ? `, up ${uptime}` : ""})`));
|
|
17730
19102
|
harnesses = await runningHarnesses(running);
|
|
17731
19103
|
} else {
|
|
17732
|
-
out(
|
|
17733
|
-
}
|
|
19104
|
+
out(row2("Running", `no \u2014 start it with ${cabane("start")}`));
|
|
19105
|
+
}
|
|
19106
|
+
const service2 = await serviceState();
|
|
19107
|
+
for (const line of serviceRows(service2)) out(line);
|
|
19108
|
+
for (const line of updateRows(readUpdateState(), cfg.update?.auto, {
|
|
19109
|
+
version: false,
|
|
19110
|
+
service: service2.installed
|
|
19111
|
+
}))
|
|
19112
|
+
out(line);
|
|
17734
19113
|
for (const line of await agentLines(cfg, harnesses)) out(line);
|
|
17735
|
-
out(
|
|
17736
|
-
out(
|
|
19114
|
+
out(row2("Logs", `${cabane("logs")} (${tildePath(companionLogPath())})`));
|
|
19115
|
+
out(row2("Transcripts", transcriptsLine()));
|
|
17737
19116
|
const names = loadSecretStoreTolerant().names();
|
|
17738
19117
|
out(
|
|
17739
|
-
names.length > 0 ?
|
|
19118
|
+
names.length > 0 ? row2("Secrets", `${names.length} declared (${names.join(", ")})`) : row2(
|
|
17740
19119
|
"Secrets",
|
|
17741
19120
|
"none declared \u2014 add `${VAR}` values to ~/.cabane/secrets.json for MCP servers that need them"
|
|
17742
19121
|
)
|
|
17743
19122
|
);
|
|
17744
19123
|
if (cfg.prepareHook) {
|
|
17745
|
-
out(
|
|
19124
|
+
out(row2("Prepare hook", `${cfg.prepareHook.command} (device-level, all agents)`));
|
|
17746
19125
|
}
|
|
17747
19126
|
for (const line of sandboxStatusLines(cfg)) out(line);
|
|
17748
19127
|
const overrides2 = Object.keys(cfg.agents ?? {});
|
|
@@ -17752,10 +19131,10 @@ function versionLine() {
|
|
|
17752
19131
|
const install = companionInstall();
|
|
17753
19132
|
const own = installedCommandPath({ install });
|
|
17754
19133
|
if (install === "script") {
|
|
17755
|
-
return `Cabane ${COMPANION_VERSION} \xB7 installed by the install script (${tildePath(
|
|
19134
|
+
return `Cabane ${COMPANION_VERSION} \xB7 installed by the install script (${tildePath(join29(homedir8(), ".cabane"))})`;
|
|
17756
19135
|
}
|
|
17757
19136
|
if (own)
|
|
17758
|
-
return `Cabane ${COMPANION_VERSION} \xB7 installed with npm (${tildePath(
|
|
19137
|
+
return `Cabane ${COMPANION_VERSION} \xB7 installed with npm (${tildePath(dirname14(dirname14(own)))})`;
|
|
17759
19138
|
return `Cabane ${COMPANION_VERSION}`;
|
|
17760
19139
|
}
|
|
17761
19140
|
function otherInstallLines(running) {
|
|
@@ -17772,14 +19151,14 @@ function otherInstallLines(running) {
|
|
|
17772
19151
|
function otherInstall(install, own) {
|
|
17773
19152
|
const real = (p) => {
|
|
17774
19153
|
try {
|
|
17775
|
-
return
|
|
19154
|
+
return realpathSync7(p);
|
|
17776
19155
|
} catch {
|
|
17777
19156
|
return null;
|
|
17778
19157
|
}
|
|
17779
19158
|
};
|
|
17780
19159
|
if (install === "npm") {
|
|
17781
|
-
const script =
|
|
17782
|
-
|
|
19160
|
+
const script = join29(
|
|
19161
|
+
homedir8(),
|
|
17783
19162
|
".cabane",
|
|
17784
19163
|
"node",
|
|
17785
19164
|
"lib",
|
|
@@ -17787,12 +19166,12 @@ function otherInstall(install, own) {
|
|
|
17787
19166
|
"@cabane",
|
|
17788
19167
|
"companion"
|
|
17789
19168
|
);
|
|
17790
|
-
return
|
|
19169
|
+
return existsSync20(script) ? join29(homedir8(), ".cabane") : null;
|
|
17791
19170
|
}
|
|
17792
19171
|
const found = firstOnPath("cabane-companion", process.env.PATH);
|
|
17793
19172
|
if (!found) return null;
|
|
17794
|
-
const ownAlias = own ? real(
|
|
17795
|
-
return real(found) === ownAlias ? null :
|
|
19173
|
+
const ownAlias = own ? real(join29(dirname14(own), "cabane-companion")) : null;
|
|
19174
|
+
return real(found) === ownAlias ? null : dirname14(found);
|
|
17796
19175
|
}
|
|
17797
19176
|
async function liveState() {
|
|
17798
19177
|
const running = readLiveRuntimeState();
|
|
@@ -17819,18 +19198,18 @@ async function agentLines(cfg, running) {
|
|
|
17819
19198
|
const label = HARNESS_LABELS[runtime];
|
|
17820
19199
|
const connected = runtime === "codex" ? !!cfg.codex && cfg.codex.enabled !== false : !!cfg.claudeCode && cfg.claudeCode.enabled !== false;
|
|
17821
19200
|
if (!connected) {
|
|
17822
|
-
lines2.push(
|
|
19201
|
+
lines2.push(row2(label, "not connected"));
|
|
17823
19202
|
continue;
|
|
17824
19203
|
}
|
|
17825
19204
|
const fromRunning = running?.find((h) => h.runtime === runtime);
|
|
17826
19205
|
const signIn = fromRunning ? { status: fromRunning.signIn, version: fromRunning.version } : await localSignIn(runtime);
|
|
17827
|
-
lines2.push(
|
|
19206
|
+
lines2.push(row2(label, signInText(runtime, signIn.status, signIn.version)));
|
|
17828
19207
|
}
|
|
17829
19208
|
if (cfg.opencode?.serverUrl) {
|
|
17830
19209
|
const fromRunning = running?.find((h) => h.runtime === "opencode");
|
|
17831
19210
|
const answering = fromRunning ? fromRunning.state === "exposed" : null;
|
|
17832
19211
|
lines2.push(
|
|
17833
|
-
|
|
19212
|
+
row2(
|
|
17834
19213
|
HARNESS_LABELS.opencode,
|
|
17835
19214
|
`${cfg.opencode.serverUrl}${answering === false ? " \u2014 not answering" : ""}`
|
|
17836
19215
|
)
|
|
@@ -17886,7 +19265,7 @@ function transcriptsLine() {
|
|
|
17886
19265
|
let count = 0;
|
|
17887
19266
|
let newest;
|
|
17888
19267
|
try {
|
|
17889
|
-
const files =
|
|
19268
|
+
const files = readdirSync7(dir2).filter((f) => f.endsWith(".jsonl")).sort();
|
|
17890
19269
|
count = files.length;
|
|
17891
19270
|
newest = files[files.length - 1];
|
|
17892
19271
|
} catch {
|
|
@@ -17906,61 +19285,107 @@ function formatUptime(startedAt) {
|
|
|
17906
19285
|
return remMin ? `${hr}h${remMin}m` : `${hr}h`;
|
|
17907
19286
|
}
|
|
17908
19287
|
|
|
17909
|
-
// src/commands/
|
|
17910
|
-
|
|
17911
|
-
|
|
17912
|
-
|
|
17913
|
-
|
|
17914
|
-
|
|
17915
|
-
|
|
17916
|
-
const
|
|
17917
|
-
|
|
17918
|
-
|
|
17919
|
-
|
|
17920
|
-
|
|
19288
|
+
// src/commands/update.ts
|
|
19289
|
+
init_config();
|
|
19290
|
+
init_term();
|
|
19291
|
+
init_version2();
|
|
19292
|
+
init_cabane_command();
|
|
19293
|
+
var UPDATE_TIMEOUT_MS = 20 * 6e4;
|
|
19294
|
+
async function update(opts = {}) {
|
|
19295
|
+
const running = readLiveRuntimeState();
|
|
19296
|
+
if (opts.auto) {
|
|
19297
|
+
const auto = opts.auto === "on";
|
|
19298
|
+
if (auto && !serviceInstalled()) {
|
|
19299
|
+
write(
|
|
19300
|
+
`${INDENT}Cabane updates itself only when it runs as a service. Run ${cabane("service install")} to run it as one, with updates on.`
|
|
19301
|
+
);
|
|
19302
|
+
process.exitCode = 1;
|
|
19303
|
+
return;
|
|
19304
|
+
}
|
|
19305
|
+
if (running?.socket) {
|
|
19306
|
+
const r = await controlRequest(running.socket, { cmd: "update", auto });
|
|
19307
|
+
write(`${INDENT}${r.ok ? tick(r.message) : r.message}`);
|
|
19308
|
+
return;
|
|
19309
|
+
}
|
|
19310
|
+
const cfg2 = loadConfig();
|
|
19311
|
+
if (!cfg2) {
|
|
19312
|
+
write(`${INDENT}This computer isn't set up yet. Run ${cabane("start")} first.`);
|
|
19313
|
+
process.exitCode = 1;
|
|
19314
|
+
return;
|
|
19315
|
+
}
|
|
19316
|
+
saveConfig({ ...cfg2, update: { auto } });
|
|
19317
|
+
write(
|
|
19318
|
+
INDENT + tick(
|
|
19319
|
+
auto ? "Cabane will keep itself up to date. Updates install between turns and roll back if they fail." : "Cabane won't update itself."
|
|
19320
|
+
)
|
|
19321
|
+
);
|
|
17921
19322
|
return;
|
|
17922
19323
|
}
|
|
17923
|
-
if (
|
|
17924
|
-
|
|
17925
|
-
|
|
19324
|
+
if (running?.socket) {
|
|
19325
|
+
const r = await controlRequest(
|
|
19326
|
+
running.socket,
|
|
19327
|
+
{ cmd: "update" },
|
|
19328
|
+
UPDATE_TIMEOUT_MS
|
|
19329
|
+
);
|
|
19330
|
+
write(`${INDENT}${r.ok ? tick(r.message) : r.message}`);
|
|
19331
|
+
if (!r.ok) process.exitCode = 1;
|
|
17926
19332
|
return;
|
|
17927
19333
|
}
|
|
17928
|
-
const
|
|
17929
|
-
|
|
17930
|
-
|
|
17931
|
-
|
|
17932
|
-
|
|
17933
|
-
|
|
17934
|
-
clearRuntimeState();
|
|
17935
|
-
process.stdout.write("companion: already stopped.\n");
|
|
19334
|
+
const latest = readUpdateState().server?.latest;
|
|
19335
|
+
if (!latest) {
|
|
19336
|
+
write(
|
|
19337
|
+
`${INDENT}Cabane hasn't said which version is latest yet. Run ${cabane("start")}, and it updates from there.`
|
|
19338
|
+
);
|
|
19339
|
+
process.exitCode = 1;
|
|
17936
19340
|
return;
|
|
17937
19341
|
}
|
|
17938
|
-
|
|
17939
|
-
|
|
17940
|
-
|
|
17941
|
-
process.stdout.write("companion: stopped.\n");
|
|
17942
|
-
return;
|
|
17943
|
-
}
|
|
17944
|
-
await sleep5(POLL_INTERVAL_MS2);
|
|
19342
|
+
if (compareVersions(latest, COMPANION_VERSION) <= 0) {
|
|
19343
|
+
write(INDENT + tick(`Cabane ${COMPANION_VERSION} is up to date.`));
|
|
19344
|
+
return;
|
|
17945
19345
|
}
|
|
17946
|
-
|
|
17947
|
-
|
|
17948
|
-
|
|
19346
|
+
const cfg = loadConfig();
|
|
19347
|
+
const harnesses = cfg ? [
|
|
19348
|
+
...isClaudeCodeConnected(cfg) ? ["claude-code"] : [],
|
|
19349
|
+
...isCodexEnabled(cfg) ? ["codex"] : []
|
|
19350
|
+
] : [];
|
|
19351
|
+
write(
|
|
19352
|
+
`${INDENT}Installing Cabane ${latest}. It brings its own copies of the agents, so this can take a few minutes.`
|
|
17949
19353
|
);
|
|
17950
|
-
|
|
17951
|
-
|
|
17952
|
-
|
|
17953
|
-
|
|
17954
|
-
|
|
17955
|
-
|
|
17956
|
-
}
|
|
17957
|
-
|
|
17958
|
-
|
|
17959
|
-
|
|
17960
|
-
|
|
17961
|
-
|
|
17962
|
-
|
|
19354
|
+
const outcome = await stageVersion(latest, harnesses);
|
|
19355
|
+
if (!outcome.ok) {
|
|
19356
|
+
if (outcome.unpublished) {
|
|
19357
|
+
write(`${INDENT}Cabane ${latest} isn't published yet. Try again later.`);
|
|
19358
|
+
process.exitCode = 1;
|
|
19359
|
+
return;
|
|
19360
|
+
}
|
|
19361
|
+
updateUpdateState((s) => ({
|
|
19362
|
+
...s,
|
|
19363
|
+
lastResult: {
|
|
19364
|
+
kind: "failed",
|
|
19365
|
+
from: COMPANION_VERSION,
|
|
19366
|
+
to: latest,
|
|
19367
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
19368
|
+
reason: outcome.reason
|
|
19369
|
+
}
|
|
19370
|
+
}));
|
|
19371
|
+
write(`${INDENT}Couldn't update to ${latest}: ${outcome.reason}.`);
|
|
19372
|
+
process.exitCode = 1;
|
|
19373
|
+
return;
|
|
17963
19374
|
}
|
|
19375
|
+
const fromRoot = currentRoot() ?? ownPackageRoot();
|
|
19376
|
+
updateUpdateState((s) => ({
|
|
19377
|
+
...s,
|
|
19378
|
+
pending: {
|
|
19379
|
+
from: COMPANION_VERSION,
|
|
19380
|
+
fromRoot,
|
|
19381
|
+
to: latest,
|
|
19382
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
19383
|
+
boots: 0
|
|
19384
|
+
}
|
|
19385
|
+
}));
|
|
19386
|
+
writeBootGuard();
|
|
19387
|
+
pointCurrentAt(outcome.root);
|
|
19388
|
+
write(INDENT + tick(`Updated to ${latest}. Start it with ${cabane("start")}`));
|
|
17964
19389
|
}
|
|
17965
19390
|
|
|
17966
19391
|
// src/commands/transcript.ts
|
|
@@ -17968,8 +19393,8 @@ init_cabane_command();
|
|
|
17968
19393
|
init_errors();
|
|
17969
19394
|
init_src();
|
|
17970
19395
|
init_transcript_writer();
|
|
17971
|
-
import { existsSync as
|
|
17972
|
-
import { isAbsolute, join as
|
|
19396
|
+
import { existsSync as existsSync21, readFileSync as readFileSync17, readdirSync as readdirSync8 } from "fs";
|
|
19397
|
+
import { isAbsolute, join as join30 } from "path";
|
|
17973
19398
|
async function transcript(opts = {}) {
|
|
17974
19399
|
const dir2 = transcriptsDir();
|
|
17975
19400
|
if (opts.follow) {
|
|
@@ -17986,7 +19411,7 @@ async function transcript(opts = {}) {
|
|
|
17986
19411
|
process.stdout.write(emptyMessage(dir2));
|
|
17987
19412
|
return;
|
|
17988
19413
|
}
|
|
17989
|
-
process.stdout.write(renderFile(
|
|
19414
|
+
process.stdout.write(renderFile(join30(dir2, newest)) + "\n");
|
|
17990
19415
|
return;
|
|
17991
19416
|
}
|
|
17992
19417
|
printList(dir2);
|
|
@@ -18069,7 +19494,7 @@ function isComplete(content) {
|
|
|
18069
19494
|
async function followTranscripts(dir2) {
|
|
18070
19495
|
const follower = new TranscriptFollower({
|
|
18071
19496
|
listFiles: () => listFiles(dir2),
|
|
18072
|
-
read: (f) =>
|
|
19497
|
+
read: (f) => readFileSync17(join30(dir2, f), "utf8"),
|
|
18073
19498
|
write: (s) => process.stdout.write(s),
|
|
18074
19499
|
// CSI: cursor up `n` lines, then erase from cursor to end of screen.
|
|
18075
19500
|
clearLines: (n) => process.stdout.write(`\x1B[${n}A\x1B[0J`),
|
|
@@ -18094,7 +19519,7 @@ async function followTranscripts(dir2) {
|
|
|
18094
19519
|
}
|
|
18095
19520
|
function listFiles(dir2) {
|
|
18096
19521
|
try {
|
|
18097
|
-
return
|
|
19522
|
+
return readdirSync8(dir2).filter((f) => f.endsWith(".jsonl")).sort().reverse();
|
|
18098
19523
|
} catch {
|
|
18099
19524
|
return [];
|
|
18100
19525
|
}
|
|
@@ -18109,7 +19534,7 @@ function printList(dir2) {
|
|
|
18109
19534
|
|
|
18110
19535
|
`);
|
|
18111
19536
|
for (const f of files.slice(0, 20)) {
|
|
18112
|
-
const { meta, outcome } = peek(
|
|
19537
|
+
const { meta, outcome } = peek(join30(dir2, f));
|
|
18113
19538
|
const when = fmtTime(rec(meta)?.ts);
|
|
18114
19539
|
const ws = str2(rec(meta)?.workspaceSlug);
|
|
18115
19540
|
const o = rec(outcome);
|
|
@@ -18131,7 +19556,7 @@ function peek(path) {
|
|
|
18131
19556
|
let meta;
|
|
18132
19557
|
let outcome;
|
|
18133
19558
|
try {
|
|
18134
|
-
for (const line of
|
|
19559
|
+
for (const line of readFileSync17(path, "utf8").split("\n")) {
|
|
18135
19560
|
if (!line.trim()) continue;
|
|
18136
19561
|
const o = safeParse(line);
|
|
18137
19562
|
const t = str2(rec(o)?.type);
|
|
@@ -18144,13 +19569,13 @@ function peek(path) {
|
|
|
18144
19569
|
}
|
|
18145
19570
|
function resolveTarget(dir2, target) {
|
|
18146
19571
|
if (isAbsolute(target) || target.includes("/")) {
|
|
18147
|
-
if (
|
|
19572
|
+
if (existsSync21(target)) return target;
|
|
18148
19573
|
throw new CompanionError(`no transcript at ${target}.`);
|
|
18149
19574
|
}
|
|
18150
|
-
const exact =
|
|
18151
|
-
if (
|
|
19575
|
+
const exact = join30(dir2, target);
|
|
19576
|
+
if (existsSync21(exact)) return exact;
|
|
18152
19577
|
const matches = listFiles(dir2).filter((f) => f.includes(target));
|
|
18153
|
-
if (matches.length === 1) return
|
|
19578
|
+
if (matches.length === 1) return join30(dir2, matches[0]);
|
|
18154
19579
|
if (matches.length === 0) {
|
|
18155
19580
|
throw new CompanionError(
|
|
18156
19581
|
`no transcript matching "${target}" in ${dir2}. Run \`${cabane("transcript")}\` to list them.`
|
|
@@ -18164,7 +19589,7 @@ function resolveTarget(dir2, target) {
|
|
|
18164
19589
|
function renderFile(path) {
|
|
18165
19590
|
let content;
|
|
18166
19591
|
try {
|
|
18167
|
-
content =
|
|
19592
|
+
content = readFileSync17(path, "utf8");
|
|
18168
19593
|
} catch (err) {
|
|
18169
19594
|
throw new CompanionError(
|
|
18170
19595
|
`couldn't read ${path}: ${err instanceof Error ? err.message : String(err)}`
|
|
@@ -18324,7 +19749,7 @@ program.command("pair").description(
|
|
|
18324
19749
|
});
|
|
18325
19750
|
});
|
|
18326
19751
|
program.command("write-paired-config", { hidden: true }).description("persist an already-completed device enrollment payload from stdin.").action(() => {
|
|
18327
|
-
writeCompletedPairing(
|
|
19752
|
+
writeCompletedPairing(readFileSync18(0, "utf8"));
|
|
18328
19753
|
});
|
|
18329
19754
|
program.command("start").description("pair this device if needed, connect an agent on it, and run in the background.").option("--foreground", "run attached in this terminal (for terminals and supervisors)").option("--daemon", "run in the background (the default; kept for compatibility)").option("--server <url>", "the cabane instance to pair with, if this device isn\u2019t paired yet").addOption(
|
|
18330
19755
|
new Option("--log-level <level>", "info (default), warn, or debug").choices([
|
|
@@ -18348,6 +19773,21 @@ program.command("start").description("pair this device if needed, connect an age
|
|
|
18348
19773
|
program.command("connect").description("connect an agent on this machine to Cabane (claude-code, codex, opencode).").argument("<harness>", "claude-code, codex, or opencode").option("--url <url>", "the `opencode serve` URL (opencode only)").action(async (harness, opts) => {
|
|
18349
19774
|
await connect2(harness, { ...opts.url !== void 0 ? { serverUrl: opts.url } : {} });
|
|
18350
19775
|
});
|
|
19776
|
+
program.command("update").description(
|
|
19777
|
+
"update to the latest version now, or set whether Cabane keeps itself up to date (--auto on|off)."
|
|
19778
|
+
).addOption(new Option("--auto <setting>", "on or off").choices(["on", "off"])).action(async (opts) => {
|
|
19779
|
+
await update(opts.auto ? { auto: opts.auto } : {});
|
|
19780
|
+
});
|
|
19781
|
+
var service = program.command("service").description("run Cabane as a service that starts at boot and restarts if it stops.");
|
|
19782
|
+
service.command("install").description("install the service and start it (systemd on Linux, launchd on macOS).").action(async () => {
|
|
19783
|
+
await serviceInstall();
|
|
19784
|
+
});
|
|
19785
|
+
service.command("uninstall").description("stop the service and remove it.").action(async () => {
|
|
19786
|
+
await serviceUninstall();
|
|
19787
|
+
});
|
|
19788
|
+
service.command("status").description("show the service's state, this version, and how updates are going.").action(async () => {
|
|
19789
|
+
await serviceStatus();
|
|
19790
|
+
});
|
|
18351
19791
|
program.command("stop").description("stop a running companion (SIGTERM, then force-kill after a timeout).").action(async () => {
|
|
18352
19792
|
await stop();
|
|
18353
19793
|
});
|