@cabane/companion 0.6.25 → 0.6.27
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/dist/cli.js +731 -174
- package/dist/runtime.js +26 -15
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -5,8 +5,22 @@ import { Command } from "commander";
|
|
|
5
5
|
|
|
6
6
|
// src/commands/daemon.ts
|
|
7
7
|
import { spawn as spawn4 } from "child_process";
|
|
8
|
-
import { closeSync as closeSync2, mkdirSync as
|
|
8
|
+
import { closeSync as closeSync2, mkdirSync as mkdirSync5, openSync as openSync2 } from "fs";
|
|
9
|
+
|
|
10
|
+
// src/cli-entry.ts
|
|
11
|
+
import { existsSync } from "fs";
|
|
9
12
|
import { fileURLToPath } from "url";
|
|
13
|
+
var RELATIVE_CANDIDATES = ["./cli.js", "../dist/cli.js", "../cli.js"];
|
|
14
|
+
function companionCliEntry(deps = {}) {
|
|
15
|
+
const exists = deps.exists ?? existsSync;
|
|
16
|
+
const candidates = deps.candidates ?? RELATIVE_CANDIDATES.map((rel) => fileURLToPath(new URL(rel, import.meta.url)));
|
|
17
|
+
for (const candidate of candidates) {
|
|
18
|
+
if (exists(candidate)) return candidate;
|
|
19
|
+
}
|
|
20
|
+
const argv1 = "argv1" in deps ? deps.argv1 : process.argv[1];
|
|
21
|
+
if (argv1 && exists(argv1)) return argv1;
|
|
22
|
+
return candidates[0] ?? "";
|
|
23
|
+
}
|
|
10
24
|
|
|
11
25
|
// src/config.ts
|
|
12
26
|
import {
|
|
@@ -16,7 +30,7 @@ import {
|
|
|
16
30
|
renameSync,
|
|
17
31
|
rmSync,
|
|
18
32
|
writeFileSync,
|
|
19
|
-
existsSync
|
|
33
|
+
existsSync as existsSync2
|
|
20
34
|
} from "fs";
|
|
21
35
|
import { homedir, userInfo } from "os";
|
|
22
36
|
import { dirname, join } from "path";
|
|
@@ -362,7 +376,7 @@ function localAgentConfig(cfg, agent) {
|
|
|
362
376
|
}
|
|
363
377
|
function loadConfig() {
|
|
364
378
|
const path = configPath();
|
|
365
|
-
if (!
|
|
379
|
+
if (!existsSync2(path)) return null;
|
|
366
380
|
let raw;
|
|
367
381
|
try {
|
|
368
382
|
raw = readFileSync(path, "utf8");
|
|
@@ -398,7 +412,7 @@ function loadConfigTolerant() {
|
|
|
398
412
|
const path = configPath();
|
|
399
413
|
const empty = {};
|
|
400
414
|
const fresh = { local: empty, note: null, hadPriorConfig: false };
|
|
401
|
-
if (!
|
|
415
|
+
if (!existsSync2(path)) return fresh;
|
|
402
416
|
let raw;
|
|
403
417
|
try {
|
|
404
418
|
raw = readFileSync(path, "utf8");
|
|
@@ -485,7 +499,7 @@ function requireConfig() {
|
|
|
485
499
|
}
|
|
486
500
|
function deleteConfig() {
|
|
487
501
|
const path = configPath();
|
|
488
|
-
if (
|
|
502
|
+
if (existsSync2(path)) {
|
|
489
503
|
writeFileSync(path, "", { mode: 384 });
|
|
490
504
|
}
|
|
491
505
|
}
|
|
@@ -692,7 +706,7 @@ async function warnAboutHarnessReadiness(cfg, deps = {}) {
|
|
|
692
706
|
|
|
693
707
|
// src/runtime-file.ts
|
|
694
708
|
import {
|
|
695
|
-
existsSync as
|
|
709
|
+
existsSync as existsSync3,
|
|
696
710
|
readFileSync as readFileSync2,
|
|
697
711
|
rmSync as rmSync2,
|
|
698
712
|
writeFileSync as writeFileSync2,
|
|
@@ -732,11 +746,11 @@ function acquireRuntimeState(state) {
|
|
|
732
746
|
}
|
|
733
747
|
function clearRuntimeState() {
|
|
734
748
|
const path = runtimePath();
|
|
735
|
-
if (
|
|
749
|
+
if (existsSync3(path)) rmSync2(path, { force: true });
|
|
736
750
|
}
|
|
737
751
|
function readLiveRuntimeState() {
|
|
738
752
|
const path = runtimePath();
|
|
739
|
-
if (!
|
|
753
|
+
if (!existsSync3(path)) return null;
|
|
740
754
|
let parsed;
|
|
741
755
|
try {
|
|
742
756
|
parsed = JSON.parse(readFileSync2(path, "utf8"));
|
|
@@ -780,6 +794,383 @@ function trimSlash(s) {
|
|
|
780
794
|
return s.endsWith("/") ? s.slice(0, -1) : s;
|
|
781
795
|
}
|
|
782
796
|
|
|
797
|
+
// src/service/index.ts
|
|
798
|
+
import { dirname as dirname4 } from "path";
|
|
799
|
+
|
|
800
|
+
// src/service/host.ts
|
|
801
|
+
import { spawnSync } from "child_process";
|
|
802
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync4, readFileSync as readFileSync3, rmSync as rmSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
803
|
+
import { homedir as homedir2 } from "os";
|
|
804
|
+
var RUN_TIMEOUT_MS = 1e4;
|
|
805
|
+
function defaultServiceHost() {
|
|
806
|
+
if (process.env.VITEST) {
|
|
807
|
+
throw new Error(
|
|
808
|
+
"defaultServiceHost() was reached during a test run \u2014 it shells out to the real service manager. Inject a fake host (apps/companion/test/service-host-fake.ts) instead."
|
|
809
|
+
);
|
|
810
|
+
}
|
|
811
|
+
return {
|
|
812
|
+
platform: process.platform,
|
|
813
|
+
env: process.env,
|
|
814
|
+
home: homedir2(),
|
|
815
|
+
uid: process.getuid?.() ?? 0,
|
|
816
|
+
execPath: process.execPath,
|
|
817
|
+
cliPath: companionCliEntry(),
|
|
818
|
+
logPath: companionLogPath(),
|
|
819
|
+
fs: {
|
|
820
|
+
read: (path) => {
|
|
821
|
+
try {
|
|
822
|
+
return readFileSync3(path, "utf8");
|
|
823
|
+
} catch {
|
|
824
|
+
return null;
|
|
825
|
+
}
|
|
826
|
+
},
|
|
827
|
+
write: (path, contents) => writeFileSync3(path, contents, "utf8"),
|
|
828
|
+
remove: (path) => rmSync3(path, { force: true }),
|
|
829
|
+
exists: (path) => existsSync4(path),
|
|
830
|
+
mkdirp: (dir2) => {
|
|
831
|
+
mkdirSync4(dir2, { recursive: true });
|
|
832
|
+
}
|
|
833
|
+
},
|
|
834
|
+
run: (cmd, args) => {
|
|
835
|
+
const res = spawnSync(cmd, args, { encoding: "utf8", timeout: RUN_TIMEOUT_MS });
|
|
836
|
+
return {
|
|
837
|
+
ok: res.status === 0,
|
|
838
|
+
stdout: res.stdout ?? "",
|
|
839
|
+
stderr: res.stderr ?? (res.error ? res.error.message : "")
|
|
840
|
+
};
|
|
841
|
+
}
|
|
842
|
+
};
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
// src/service/launchd.ts
|
|
846
|
+
import { join as join4 } from "path";
|
|
847
|
+
var LAUNCHD_LABEL = "ai.cabane.companion";
|
|
848
|
+
function launchAgentPath(home) {
|
|
849
|
+
return join4(home, "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`);
|
|
850
|
+
}
|
|
851
|
+
function renderPlist(input) {
|
|
852
|
+
const args = input.programArguments.map((a) => ` <string>${xml(a)}</string>`).join("\n");
|
|
853
|
+
const env = Object.entries(input.environment).map(([k, v]) => ` <key>${xml(k)}</key>
|
|
854
|
+
<string>${xml(v)}</string>`).join("\n");
|
|
855
|
+
return [
|
|
856
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
857
|
+
'<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
|
|
858
|
+
'<plist version="1.0">',
|
|
859
|
+
"<dict>",
|
|
860
|
+
" <key>Label</key>",
|
|
861
|
+
` <string>${LAUNCHD_LABEL}</string>`,
|
|
862
|
+
" <key>ProgramArguments</key>",
|
|
863
|
+
" <array>",
|
|
864
|
+
args,
|
|
865
|
+
" </array>",
|
|
866
|
+
" <key>RunAtLoad</key>",
|
|
867
|
+
" <true/>",
|
|
868
|
+
" <key>KeepAlive</key>",
|
|
869
|
+
" <dict>",
|
|
870
|
+
" <key>SuccessfulExit</key>",
|
|
871
|
+
" <false/>",
|
|
872
|
+
" </dict>",
|
|
873
|
+
" <key>EnvironmentVariables</key>",
|
|
874
|
+
" <dict>",
|
|
875
|
+
env,
|
|
876
|
+
" </dict>",
|
|
877
|
+
" <key>StandardOutPath</key>",
|
|
878
|
+
` <string>${xml(input.logPath)}</string>`,
|
|
879
|
+
" <key>StandardErrorPath</key>",
|
|
880
|
+
` <string>${xml(input.logPath)}</string>`,
|
|
881
|
+
"</dict>",
|
|
882
|
+
"</plist>",
|
|
883
|
+
""
|
|
884
|
+
].join("\n");
|
|
885
|
+
}
|
|
886
|
+
function domain(host) {
|
|
887
|
+
return `gui/${host.uid}`;
|
|
888
|
+
}
|
|
889
|
+
function target(host) {
|
|
890
|
+
return `${domain(host)}/${LAUNCHD_LABEL}`;
|
|
891
|
+
}
|
|
892
|
+
function launchdStart(host, plistPath) {
|
|
893
|
+
host.run("launchctl", ["bootout", target(host)]);
|
|
894
|
+
const res = host.run("launchctl", ["bootstrap", domain(host), plistPath]);
|
|
895
|
+
return res.ok ? { ok: true } : { ok: false, detail: firstLine(res.stderr || res.stdout) };
|
|
896
|
+
}
|
|
897
|
+
function launchdStop(host) {
|
|
898
|
+
const res = host.run("launchctl", ["bootout", target(host)]);
|
|
899
|
+
if (res.ok || notLoaded(res.stderr + res.stdout)) return { ok: true };
|
|
900
|
+
return { ok: false, detail: firstLine(res.stderr || res.stdout) };
|
|
901
|
+
}
|
|
902
|
+
function launchdProbe(host) {
|
|
903
|
+
const res = host.run("launchctl", ["print", target(host)]);
|
|
904
|
+
if (res.ok) {
|
|
905
|
+
const state = /state\s*=\s*(\w+)/.exec(res.stdout)?.[1];
|
|
906
|
+
return { ownership: "held", running: state ? state === "running" : "unknown" };
|
|
907
|
+
}
|
|
908
|
+
return notLoaded(res.stderr + res.stdout) ? { ownership: "clear", running: false } : { ownership: "unknown", running: "unknown" };
|
|
909
|
+
}
|
|
910
|
+
function launchdRemove(host, plistPath) {
|
|
911
|
+
const stopped = launchdStop(host);
|
|
912
|
+
if (!stopped.ok) return stopped;
|
|
913
|
+
host.fs.remove(plistPath);
|
|
914
|
+
return { ok: true };
|
|
915
|
+
}
|
|
916
|
+
var NOT_LOADED = new RegExp(
|
|
917
|
+
`no such process|(could not find|not find service)[^\\n]*${LAUNCHD_LABEL.replace(
|
|
918
|
+
/[.*+?^${}()|[\]\\]/g,
|
|
919
|
+
"\\$&"
|
|
920
|
+
)}`,
|
|
921
|
+
"i"
|
|
922
|
+
);
|
|
923
|
+
function notLoaded(output) {
|
|
924
|
+
return NOT_LOADED.test(output);
|
|
925
|
+
}
|
|
926
|
+
function firstLine(s) {
|
|
927
|
+
return s.trim().split("\n")[0] ?? "";
|
|
928
|
+
}
|
|
929
|
+
function xml(value) {
|
|
930
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
// src/service/systemd.ts
|
|
934
|
+
import { dirname as dirname3, join as join5 } from "path";
|
|
935
|
+
var SYSTEMD_UNIT = "cabane-companion.service";
|
|
936
|
+
function systemdUnitPath(host) {
|
|
937
|
+
const configHome = host.env.XDG_CONFIG_HOME?.trim() ? host.env.XDG_CONFIG_HOME.trim() : join5(host.home, ".config");
|
|
938
|
+
return join5(configHome, "systemd", "user", SYSTEMD_UNIT);
|
|
939
|
+
}
|
|
940
|
+
function renderUnit(input) {
|
|
941
|
+
const [exec, ...rest] = input.programArguments;
|
|
942
|
+
const execStart = [quote(exec ?? ""), ...rest.map(quote)].join(" ");
|
|
943
|
+
const env = Object.entries(input.environment).map(([k, v]) => `Environment=${k}=${v}`);
|
|
944
|
+
return [
|
|
945
|
+
"[Unit]",
|
|
946
|
+
"Description=Cabane Companion \u2014 keeps this device answering while you are logged in",
|
|
947
|
+
"After=network-online.target",
|
|
948
|
+
"Wants=network-online.target",
|
|
949
|
+
"",
|
|
950
|
+
"[Service]",
|
|
951
|
+
"Type=simple",
|
|
952
|
+
...env,
|
|
953
|
+
`ExecStart=${execStart}`,
|
|
954
|
+
"Restart=on-failure",
|
|
955
|
+
"RestartSec=5",
|
|
956
|
+
"",
|
|
957
|
+
"[Install]",
|
|
958
|
+
"WantedBy=default.target"
|
|
959
|
+
].join("\n") + "\n";
|
|
960
|
+
}
|
|
961
|
+
function systemdReload(host) {
|
|
962
|
+
host.run("systemctl", ["--user", "daemon-reload"]);
|
|
963
|
+
}
|
|
964
|
+
function systemdStart(host) {
|
|
965
|
+
systemdReload(host);
|
|
966
|
+
const enabled = host.run("systemctl", ["--user", "enable", SYSTEMD_UNIT]);
|
|
967
|
+
if (!enabled.ok) return { ok: false, detail: firstLine2(enabled.stderr || enabled.stdout) };
|
|
968
|
+
const started = host.run("systemctl", ["--user", "restart", SYSTEMD_UNIT]);
|
|
969
|
+
if (!started.ok) return { ok: false, detail: firstLine2(started.stderr || started.stdout) };
|
|
970
|
+
return { ok: true };
|
|
971
|
+
}
|
|
972
|
+
function systemdStop(host) {
|
|
973
|
+
const res = host.run("systemctl", ["--user", "stop", SYSTEMD_UNIT]);
|
|
974
|
+
if (res.ok || notLoaded2(res.stderr + res.stdout)) return { ok: true };
|
|
975
|
+
return { ok: false, detail: firstLine2(res.stderr || res.stdout) };
|
|
976
|
+
}
|
|
977
|
+
function systemdProbe(host) {
|
|
978
|
+
const state = host.run("systemctl", ["--user", "is-active", SYSTEMD_UNIT]).stdout.trim();
|
|
979
|
+
if (state === "active") return { ownership: "held", running: true };
|
|
980
|
+
if (state === "activating" || state === "reloading" || state === "deactivating") {
|
|
981
|
+
return { ownership: "held", running: "transitional" };
|
|
982
|
+
}
|
|
983
|
+
if (state === "inactive" || state === "failed") return { ownership: "clear", running: false };
|
|
984
|
+
return { ownership: "unknown", running: "unknown" };
|
|
985
|
+
}
|
|
986
|
+
function systemdWantsLinkPath(host) {
|
|
987
|
+
return join5(dirname3(systemdUnitPath(host)), "default.target.wants", SYSTEMD_UNIT);
|
|
988
|
+
}
|
|
989
|
+
function systemdInstalled(host) {
|
|
990
|
+
return host.fs.exists(systemdUnitPath(host)) || host.fs.exists(systemdWantsLinkPath(host));
|
|
991
|
+
}
|
|
992
|
+
function systemdRemove(host, unitPath) {
|
|
993
|
+
const stopped = host.run("systemctl", ["--user", "disable", "--now", SYSTEMD_UNIT]);
|
|
994
|
+
if (!(stopped.ok || notLoaded2(stopped.stderr + stopped.stdout))) {
|
|
995
|
+
return { ok: false, detail: firstLine2(stopped.stderr || stopped.stdout) };
|
|
996
|
+
}
|
|
997
|
+
host.fs.remove(unitPath);
|
|
998
|
+
const link = systemdWantsLinkPath(host);
|
|
999
|
+
if (host.fs.exists(link)) host.fs.remove(link);
|
|
1000
|
+
systemdReload(host);
|
|
1001
|
+
return { ok: true };
|
|
1002
|
+
}
|
|
1003
|
+
var NOT_LOADED2 = new RegExp(
|
|
1004
|
+
`unit (file )?${SYSTEMD_UNIT.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")} (not loaded|does not exist|not found|could not be found)`,
|
|
1005
|
+
"i"
|
|
1006
|
+
);
|
|
1007
|
+
function notLoaded2(output) {
|
|
1008
|
+
return NOT_LOADED2.test(output);
|
|
1009
|
+
}
|
|
1010
|
+
function readLinger(host) {
|
|
1011
|
+
const res = host.run("loginctl", ["show-user", String(host.uid), "-p", "Linger"]);
|
|
1012
|
+
if (!res.ok) return "unknown";
|
|
1013
|
+
const match = /Linger=(\w+)/.exec(res.stdout);
|
|
1014
|
+
if (!match) return "unknown";
|
|
1015
|
+
return match[1] === "yes" ? "yes" : "no";
|
|
1016
|
+
}
|
|
1017
|
+
function tryEnableLinger(host) {
|
|
1018
|
+
host.run("loginctl", ["enable-linger", String(host.uid)]);
|
|
1019
|
+
return readLinger(host);
|
|
1020
|
+
}
|
|
1021
|
+
function isRemoteSession(env) {
|
|
1022
|
+
return Boolean(env.SSH_CONNECTION || env.SSH_TTY || env.SSH_CLIENT);
|
|
1023
|
+
}
|
|
1024
|
+
function quote(value) {
|
|
1025
|
+
return `"${value.replace(/(["\\])/g, "\\$1")}"`;
|
|
1026
|
+
}
|
|
1027
|
+
function firstLine2(s) {
|
|
1028
|
+
return s.trim().split("\n")[0] ?? "";
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
// src/service/index.ts
|
|
1032
|
+
function detectServiceManager(host = defaultServiceHost()) {
|
|
1033
|
+
if (host.platform === "darwin") return "launchd";
|
|
1034
|
+
if (host.platform !== "linux") return "none";
|
|
1035
|
+
return host.run("systemctl", ["--user", "show-environment"]).ok ? "systemd-user" : "none";
|
|
1036
|
+
}
|
|
1037
|
+
function programArguments(host, opts) {
|
|
1038
|
+
const args = [host.execPath, host.cliPath, "start", "--foreground", "--no-open"];
|
|
1039
|
+
if (opts.port !== void 0) args.push("--port", String(opts.port));
|
|
1040
|
+
return args;
|
|
1041
|
+
}
|
|
1042
|
+
function environment(host) {
|
|
1043
|
+
return {
|
|
1044
|
+
// PATH CAPTURE. launchd's default PATH is `/usr/bin:/bin:/usr/sbin:/sbin`
|
|
1045
|
+
// and a systemd user manager's is barely better — neither has `claude`,
|
|
1046
|
+
// `codex` or `opencode` on it. A companion that runs but can't find its
|
|
1047
|
+
// harness is worse than one that isn't running, because the device reads
|
|
1048
|
+
// Online. So we bake in the PATH of the shell that ran `start`, and because
|
|
1049
|
+
// it's part of the rendered content, a PATH that later drifts re-renders on
|
|
1050
|
+
// the next `start` like any other change.
|
|
1051
|
+
PATH: host.env.PATH ?? "",
|
|
1052
|
+
CABANE_COMPANION_DAEMON: "1"
|
|
1053
|
+
};
|
|
1054
|
+
}
|
|
1055
|
+
function unitPathFor(host, manager) {
|
|
1056
|
+
if (manager === "launchd") return launchAgentPath(host.home);
|
|
1057
|
+
if (manager === "systemd-user") return systemdUnitPath(host);
|
|
1058
|
+
return null;
|
|
1059
|
+
}
|
|
1060
|
+
function renderFor(host, manager, opts) {
|
|
1061
|
+
const input = { programArguments: programArguments(host, opts), environment: environment(host) };
|
|
1062
|
+
return manager === "launchd" ? renderPlist({ ...input, logPath: host.logPath }) : renderUnit(input);
|
|
1063
|
+
}
|
|
1064
|
+
function installService(opts = {}, host = defaultServiceHost()) {
|
|
1065
|
+
const manager = detectServiceManager(host);
|
|
1066
|
+
if (manager === "none") return { installed: false, reason: "unsupported" };
|
|
1067
|
+
const path = unitPathFor(host, manager);
|
|
1068
|
+
if (!path) return { installed: false, reason: "unsupported" };
|
|
1069
|
+
const desired = renderFor(host, manager, opts);
|
|
1070
|
+
const existing = host.fs.read(path);
|
|
1071
|
+
const hadUnit = existing !== null;
|
|
1072
|
+
const changed = existing !== desired;
|
|
1073
|
+
if (changed) {
|
|
1074
|
+
host.fs.mkdirp(dirname4(path));
|
|
1075
|
+
host.fs.write(path, desired);
|
|
1076
|
+
}
|
|
1077
|
+
if (manager === "systemd-user") {
|
|
1078
|
+
const linger = ensureLinger(host);
|
|
1079
|
+
if (linger !== "yes" && isRemoteSession(host.env)) {
|
|
1080
|
+
systemdRemove(host, path);
|
|
1081
|
+
return failedInstall(
|
|
1082
|
+
host,
|
|
1083
|
+
manager,
|
|
1084
|
+
"linger-unavailable",
|
|
1085
|
+
"a systemd --user service would stop when this SSH session ends (lingering is off and could not be enabled)"
|
|
1086
|
+
);
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
const started = manager === "launchd" ? launchdStart(host, path) : systemdStart(host);
|
|
1090
|
+
if (!started.ok) {
|
|
1091
|
+
if (!hadUnit) removeService(host, manager, path);
|
|
1092
|
+
return failedInstall(host, manager, "command-failed", started.detail);
|
|
1093
|
+
}
|
|
1094
|
+
return { installed: true, manager, changed };
|
|
1095
|
+
}
|
|
1096
|
+
function failedInstall(host, manager, reason, detail) {
|
|
1097
|
+
return {
|
|
1098
|
+
installed: false,
|
|
1099
|
+
reason,
|
|
1100
|
+
...detail ? { detail } : {},
|
|
1101
|
+
...managerOwnership(host, manager) === "clear" ? {} : { leftBehind: manager }
|
|
1102
|
+
};
|
|
1103
|
+
}
|
|
1104
|
+
function managerProbe(host, manager) {
|
|
1105
|
+
if (manager === "launchd") return launchdProbe(host);
|
|
1106
|
+
if (manager === "systemd-user") return systemdProbe(host);
|
|
1107
|
+
return { ownership: "clear", running: false };
|
|
1108
|
+
}
|
|
1109
|
+
function managerOwnership(host, manager) {
|
|
1110
|
+
return managerProbe(host, manager).ownership;
|
|
1111
|
+
}
|
|
1112
|
+
function refreshInstalledService(opts = {}, host = defaultServiceHost()) {
|
|
1113
|
+
const manager = detectServiceManager(host);
|
|
1114
|
+
const path = unitPathFor(host, manager);
|
|
1115
|
+
if (!path || !host.fs.exists(path)) return false;
|
|
1116
|
+
const desired = renderFor(host, manager, opts);
|
|
1117
|
+
if (host.fs.read(path) === desired) return false;
|
|
1118
|
+
host.fs.write(path, desired);
|
|
1119
|
+
if (manager === "systemd-user") systemdReload(host);
|
|
1120
|
+
return true;
|
|
1121
|
+
}
|
|
1122
|
+
function stopService(host = defaultServiceHost()) {
|
|
1123
|
+
const manager = detectServiceManager(host);
|
|
1124
|
+
const path = unitPathFor(host, manager);
|
|
1125
|
+
if (manager === "none" || !path || !managerHasClaim(host, manager, path)) {
|
|
1126
|
+
return { handled: false, ok: true };
|
|
1127
|
+
}
|
|
1128
|
+
const res = manager === "launchd" ? launchdStop(host) : systemdStop(host);
|
|
1129
|
+
return {
|
|
1130
|
+
handled: true,
|
|
1131
|
+
ok: res.ok,
|
|
1132
|
+
manager,
|
|
1133
|
+
...res.detail ? { detail: res.detail } : {}
|
|
1134
|
+
};
|
|
1135
|
+
}
|
|
1136
|
+
function disableService(host = defaultServiceHost()) {
|
|
1137
|
+
const manager = detectServiceManager(host);
|
|
1138
|
+
const path = unitPathFor(host, manager);
|
|
1139
|
+
if (manager === "none" || !path) return { handled: false, ok: true };
|
|
1140
|
+
if (!managerHasClaim(host, manager, path)) return { handled: false, ok: true, manager };
|
|
1141
|
+
const res = removeService(host, manager, path);
|
|
1142
|
+
return { handled: true, ok: res.ok, manager, ...res.detail ? { detail: res.detail } : {} };
|
|
1143
|
+
}
|
|
1144
|
+
function managerHasClaim(host, manager, path) {
|
|
1145
|
+
if (manager === "none") return false;
|
|
1146
|
+
if (host.fs.exists(path)) return true;
|
|
1147
|
+
if (manager === "systemd-user" && systemdInstalled(host)) return true;
|
|
1148
|
+
return managerOwnership(host, manager) !== "clear";
|
|
1149
|
+
}
|
|
1150
|
+
function removeService(host, manager, path) {
|
|
1151
|
+
return manager === "launchd" ? launchdRemove(host, path) : systemdRemove(host, path);
|
|
1152
|
+
}
|
|
1153
|
+
function serviceStatus(host = defaultServiceHost()) {
|
|
1154
|
+
const manager = detectServiceManager(host);
|
|
1155
|
+
const path = unitPathFor(host, manager);
|
|
1156
|
+
const probe = managerProbe(host, manager);
|
|
1157
|
+
const installed = manager === "none" || !path ? false : host.fs.exists(path) || probe.ownership === "held" || manager === "systemd-user" && systemdInstalled(host);
|
|
1158
|
+
const running = manager === "none" ? false : probe.running;
|
|
1159
|
+
return {
|
|
1160
|
+
manager,
|
|
1161
|
+
unitPath: path,
|
|
1162
|
+
installed,
|
|
1163
|
+
running,
|
|
1164
|
+
linger: manager === "systemd-user" ? readLinger(host) : null,
|
|
1165
|
+
remoteSession: isRemoteSession(host.env)
|
|
1166
|
+
};
|
|
1167
|
+
}
|
|
1168
|
+
function ensureLinger(host) {
|
|
1169
|
+
const current = readLinger(host);
|
|
1170
|
+
if (current === "yes") return current;
|
|
1171
|
+
return tryEnableLinger(host);
|
|
1172
|
+
}
|
|
1173
|
+
|
|
783
1174
|
// src/commands/daemon.ts
|
|
784
1175
|
var STARTUP_TIMEOUT_MS = 8e3;
|
|
785
1176
|
var POLL_INTERVAL_MS = 150;
|
|
@@ -790,6 +1181,11 @@ async function startDaemon(opts = {}, deps = {}) {
|
|
|
790
1181
|
const spawnDetached = deps.spawnDetached ?? defaultSpawnDetached;
|
|
791
1182
|
const sleep4 = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
792
1183
|
const now = deps.now ?? (() => Date.now());
|
|
1184
|
+
const install = deps.installService ?? ((o) => installService(o));
|
|
1185
|
+
const refresh = deps.refreshService ?? ((o) => refreshInstalledService(o));
|
|
1186
|
+
const disable = deps.disableService ?? (() => disableService());
|
|
1187
|
+
const isTty = deps.isTty ?? (() => Boolean(process.stdin.isTTY));
|
|
1188
|
+
const serviceOpts = opts.port !== void 0 ? { port: opts.port } : {};
|
|
793
1189
|
const { cfg, claudeOnPath: claudeOnPath2 } = await requireStartConfig({
|
|
794
1190
|
// Unset → `requireStartConfig`'s own `requireConfig`, the real one.
|
|
795
1191
|
...deps.requireCfg ? { requireCfg: deps.requireCfg } : {},
|
|
@@ -809,30 +1205,61 @@ async function startDaemon(opts = {}, deps = {}) {
|
|
|
809
1205
|
const existing = readState();
|
|
810
1206
|
if (existing) {
|
|
811
1207
|
if (await verify(existing) !== "stale") {
|
|
1208
|
+
const rerendered = isTty() ? refresh(serviceOpts) : false;
|
|
812
1209
|
process.stdout.write(
|
|
813
1210
|
`Cabane Companion is already running (pid ${existing.pid}).
|
|
814
1211
|
\u2192 Dashboard: ${existing.url}
|
|
815
1212
|
Stop it first with \`cabane-companion stop\` if you want to relaunch.
|
|
816
|
-
`
|
|
1213
|
+
` + (rerendered ? "Autostart: the login service was updated for this install \u2014 it takes effect at the next login, or now with `cabane-companion stop` then `start`.\n" : "")
|
|
817
1214
|
);
|
|
818
1215
|
return;
|
|
819
1216
|
}
|
|
820
1217
|
clearRuntimeState();
|
|
821
1218
|
}
|
|
1219
|
+
let service2 = isTty() ? install(serviceOpts) : { installed: false, reason: "unsupported" };
|
|
822
1220
|
const args = ["start", "--no-open"];
|
|
823
1221
|
if (opts.port !== void 0) args.push("--port", String(opts.port));
|
|
824
|
-
const
|
|
825
|
-
|
|
1222
|
+
const launchDetached = () => {
|
|
1223
|
+
const spawned = spawnDetached(args);
|
|
1224
|
+
spawned.unref();
|
|
1225
|
+
return spawned;
|
|
1226
|
+
};
|
|
826
1227
|
const ready = (s) => !!s && !!s.url;
|
|
827
|
-
const
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
1228
|
+
const waitForReady = async () => {
|
|
1229
|
+
const deadline = now() + STARTUP_TIMEOUT_MS;
|
|
1230
|
+
let seen = readState();
|
|
1231
|
+
while (!ready(seen) && now() < deadline) {
|
|
1232
|
+
await sleep4(POLL_INTERVAL_MS);
|
|
1233
|
+
seen = readState();
|
|
1234
|
+
}
|
|
1235
|
+
return ready(seen) ? seen : null;
|
|
1236
|
+
};
|
|
1237
|
+
if (!service2.installed && service2.leftBehind) {
|
|
1238
|
+
refuseDouble(service2.leftBehind, service2.detail);
|
|
1239
|
+
process.exitCode = 1;
|
|
1240
|
+
return;
|
|
832
1241
|
}
|
|
833
|
-
|
|
1242
|
+
let child = service2.installed ? null : launchDetached();
|
|
1243
|
+
let state = await waitForReady();
|
|
1244
|
+
if (!state && service2.installed) {
|
|
834
1245
|
process.stdout.write(
|
|
835
|
-
|
|
1246
|
+
`${service2.manager} started the companion but it didn't report ready within ${STARTUP_TIMEOUT_MS / 1e3}s \u2014 removing the login service.
|
|
1247
|
+
`
|
|
1248
|
+
);
|
|
1249
|
+
const removed = disable();
|
|
1250
|
+
if (!removed.handled || !removed.ok) {
|
|
1251
|
+
refuseDouble(service2.manager, removed.detail);
|
|
1252
|
+
process.exitCode = 1;
|
|
1253
|
+
return;
|
|
1254
|
+
}
|
|
1255
|
+
process.stdout.write("Launching it directly instead.\n");
|
|
1256
|
+
service2 = { installed: false, reason: "command-failed", detail: "the service never came up" };
|
|
1257
|
+
child = launchDetached();
|
|
1258
|
+
state = await waitForReady();
|
|
1259
|
+
}
|
|
1260
|
+
if (!state) {
|
|
1261
|
+
process.stdout.write(
|
|
1262
|
+
`Cabane Companion was launched (pid ${child?.pid ?? "?"}) but didn't report ready within ${STARTUP_TIMEOUT_MS / 1e3}s.
|
|
836
1263
|
Check ${companionLogPath()} for errors, or run \`cabane-companion status\`.
|
|
837
1264
|
`
|
|
838
1265
|
);
|
|
@@ -845,12 +1272,28 @@ Check ${companionLogPath()} for errors, or run \`cabane-companion status\`.
|
|
|
845
1272
|
Logs: ${companionLogPath()}
|
|
846
1273
|
Status: cabane-companion status
|
|
847
1274
|
Stop: cabane-companion stop
|
|
1275
|
+
`
|
|
1276
|
+
);
|
|
1277
|
+
if (service2.installed) {
|
|
1278
|
+
process.stdout.write(`Autostart: enabled (${service2.manager}) \u2014 it starts again at login.
|
|
1279
|
+
`);
|
|
1280
|
+
} else if (service2.reason !== "unsupported") {
|
|
1281
|
+
process.stdout.write(
|
|
1282
|
+
`Autostart: not enabled \u2014 ${service2.detail ?? "the service manager refused the install"}. Running detached instead; it won't come back after a reboot.
|
|
1283
|
+
`
|
|
1284
|
+
);
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
function refuseDouble(manager, detail) {
|
|
1288
|
+
process.stdout.write(
|
|
1289
|
+
`${manager} still has the login service${detail ? ` (${detail})` : ""} \u2014 not launching a second companion beside a service that may still own one.
|
|
1290
|
+
Check \`cabane-companion service status\`, then \`cabane-companion service disable\`, and run \`cabane-companion start --daemon\` again.
|
|
848
1291
|
`
|
|
849
1292
|
);
|
|
850
1293
|
}
|
|
851
1294
|
function defaultSpawnDetached(args) {
|
|
852
|
-
const cliPath =
|
|
853
|
-
|
|
1295
|
+
const cliPath = companionCliEntry();
|
|
1296
|
+
mkdirSync5(cabaneDir(), { recursive: true });
|
|
854
1297
|
const logFd = openSync2(companionLogPath(), "a");
|
|
855
1298
|
try {
|
|
856
1299
|
return spawn4(process.execPath, [cliPath, ...args], {
|
|
@@ -869,25 +1312,25 @@ import { confirm } from "@inquirer/prompts";
|
|
|
869
1312
|
// src/credentials.ts
|
|
870
1313
|
import {
|
|
871
1314
|
chmodSync as chmodSync2,
|
|
872
|
-
existsSync as
|
|
873
|
-
mkdirSync as
|
|
874
|
-
readFileSync as
|
|
1315
|
+
existsSync as existsSync5,
|
|
1316
|
+
mkdirSync as mkdirSync6,
|
|
1317
|
+
readFileSync as readFileSync4,
|
|
875
1318
|
renameSync as renameSync2,
|
|
876
|
-
rmSync as
|
|
877
|
-
writeFileSync as
|
|
1319
|
+
rmSync as rmSync4,
|
|
1320
|
+
writeFileSync as writeFileSync4
|
|
878
1321
|
} from "fs";
|
|
879
|
-
import { dirname as
|
|
1322
|
+
import { dirname as dirname5, join as join6 } from "path";
|
|
880
1323
|
import { z as z3 } from "zod";
|
|
881
1324
|
function credentialsPath() {
|
|
882
|
-
return
|
|
1325
|
+
return join6(cabaneDir(), "credentials.json");
|
|
883
1326
|
}
|
|
884
1327
|
var credentialStoreSchema = z3.record(z3.string(), z3.string());
|
|
885
1328
|
function load() {
|
|
886
1329
|
const path = credentialsPath();
|
|
887
|
-
if (!
|
|
1330
|
+
if (!existsSync5(path)) return {};
|
|
888
1331
|
let raw;
|
|
889
1332
|
try {
|
|
890
|
-
raw =
|
|
1333
|
+
raw = readFileSync4(path, "utf8");
|
|
891
1334
|
} catch {
|
|
892
1335
|
return {};
|
|
893
1336
|
}
|
|
@@ -901,14 +1344,14 @@ function load() {
|
|
|
901
1344
|
}
|
|
902
1345
|
function save(map) {
|
|
903
1346
|
const path = credentialsPath();
|
|
904
|
-
|
|
1347
|
+
mkdirSync6(dirname5(path), { recursive: true });
|
|
905
1348
|
try {
|
|
906
1349
|
chmodSync2(cabaneDir(), 448);
|
|
907
1350
|
} catch {
|
|
908
1351
|
}
|
|
909
1352
|
const tmp = `${path}.${process.pid}.tmp`;
|
|
910
1353
|
try {
|
|
911
|
-
|
|
1354
|
+
writeFileSync4(tmp, JSON.stringify(map, null, 2) + "\n", { mode: 384 });
|
|
912
1355
|
try {
|
|
913
1356
|
chmodSync2(tmp, 384);
|
|
914
1357
|
} catch {
|
|
@@ -916,7 +1359,7 @@ function save(map) {
|
|
|
916
1359
|
renameSync2(tmp, path);
|
|
917
1360
|
} catch (err) {
|
|
918
1361
|
try {
|
|
919
|
-
|
|
1362
|
+
rmSync4(tmp, { force: true });
|
|
920
1363
|
} catch {
|
|
921
1364
|
}
|
|
922
1365
|
throw err;
|
|
@@ -946,7 +1389,7 @@ function pruneCredentials(keepAgentIds) {
|
|
|
946
1389
|
}
|
|
947
1390
|
function clearCredentials() {
|
|
948
1391
|
const path = credentialsPath();
|
|
949
|
-
if (
|
|
1392
|
+
if (existsSync5(path)) writeFileSync4(path, "", { mode: 384 });
|
|
950
1393
|
}
|
|
951
1394
|
|
|
952
1395
|
// src/commands/logout.ts
|
|
@@ -1132,7 +1575,71 @@ async function pair(opts = {}) {
|
|
|
1132
1575
|
}
|
|
1133
1576
|
|
|
1134
1577
|
// src/cli.ts
|
|
1135
|
-
import { readFileSync as
|
|
1578
|
+
import { readFileSync as readFileSync12 } from "fs";
|
|
1579
|
+
|
|
1580
|
+
// src/commands/service.ts
|
|
1581
|
+
function serviceStatusCommand(deps = {}) {
|
|
1582
|
+
const status2 = (deps.status ?? serviceStatus)();
|
|
1583
|
+
if (status2.manager === "none") {
|
|
1584
|
+
process.stdout.write(
|
|
1585
|
+
"service: not supported on this machine yet \u2014 `cabane-companion start` runs the companion detached instead (it stops at the next reboot).\n"
|
|
1586
|
+
);
|
|
1587
|
+
return;
|
|
1588
|
+
}
|
|
1589
|
+
process.stdout.write(`manager: ${status2.manager}
|
|
1590
|
+
`);
|
|
1591
|
+
process.stdout.write(
|
|
1592
|
+
`unit: ${status2.unitPath}${status2.installed ? "" : " (not written)"}
|
|
1593
|
+
`
|
|
1594
|
+
);
|
|
1595
|
+
process.stdout.write(`installed: ${status2.installed ? "yes" : "no"}
|
|
1596
|
+
`);
|
|
1597
|
+
process.stdout.write(`running: ${runningLine(status2.running)}
|
|
1598
|
+
`);
|
|
1599
|
+
if (status2.manager === "systemd-user") {
|
|
1600
|
+
process.stdout.write(`linger: ${status2.linger}${lingerNote(status2)}
|
|
1601
|
+
`);
|
|
1602
|
+
}
|
|
1603
|
+
if (status2.installed) {
|
|
1604
|
+
process.stdout.write("disable: cabane-companion service disable\n");
|
|
1605
|
+
} else {
|
|
1606
|
+
process.stdout.write("install: cabane-companion start (installs it for you)\n");
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
function serviceDisableCommand(deps = {}) {
|
|
1610
|
+
const disable = deps.disable ?? disableService;
|
|
1611
|
+
const res = disable();
|
|
1612
|
+
if (!res.handled) {
|
|
1613
|
+
process.stdout.write("service: nothing installed (nothing to remove).\n");
|
|
1614
|
+
return;
|
|
1615
|
+
}
|
|
1616
|
+
if (!res.ok) {
|
|
1617
|
+
process.stdout.write(
|
|
1618
|
+
`service: nothing was removed \u2014 ${res.detail ?? "the service manager reported an error"}.
|
|
1619
|
+
The unit is still installed and may still be running; \`cabane-companion service status\` shows where it stands.
|
|
1620
|
+
`
|
|
1621
|
+
);
|
|
1622
|
+
process.exitCode = 1;
|
|
1623
|
+
return;
|
|
1624
|
+
}
|
|
1625
|
+
process.stdout.write(
|
|
1626
|
+
"service: stopped and removed. The companion no longer starts at login; `cabane-companion start` sets it up again.\n"
|
|
1627
|
+
);
|
|
1628
|
+
}
|
|
1629
|
+
function runningLine(running) {
|
|
1630
|
+
if (running === "transitional") {
|
|
1631
|
+
return "starting or stopping \u2014 the manager reported a state in transition; ask again in a moment";
|
|
1632
|
+
}
|
|
1633
|
+
if (running === "unknown") return "unknown \u2014 the service manager didn't answer";
|
|
1634
|
+
return running ? "yes" : "no";
|
|
1635
|
+
}
|
|
1636
|
+
function lingerNote(status2) {
|
|
1637
|
+
if (status2.linger === "yes") return " (survives logout)";
|
|
1638
|
+
if (status2.remoteSession) {
|
|
1639
|
+
return " \u2014 without lingering a --user service stops when this SSH session ends";
|
|
1640
|
+
}
|
|
1641
|
+
return " \u2014 the service runs while you are logged in";
|
|
1642
|
+
}
|
|
1136
1643
|
|
|
1137
1644
|
// src/browser.ts
|
|
1138
1645
|
import { spawn as spawn5 } from "child_process";
|
|
@@ -1169,15 +1676,15 @@ function shouldAutoOpen(opts) {
|
|
|
1169
1676
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
1170
1677
|
|
|
1171
1678
|
// src/dashboard/server.ts
|
|
1172
|
-
import { dirname as
|
|
1679
|
+
import { dirname as dirname6, join as join8 } from "path";
|
|
1173
1680
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
1174
1681
|
import { serve } from "@hono/node-server";
|
|
1175
1682
|
import { Hono } from "hono";
|
|
1176
1683
|
|
|
1177
1684
|
// src/dashboard/routes.ts
|
|
1178
|
-
import { openSync as openSync3, readSync, closeSync as closeSync3, fstatSync, existsSync as
|
|
1685
|
+
import { openSync as openSync3, readSync, closeSync as closeSync3, fstatSync, existsSync as existsSync6 } from "fs";
|
|
1179
1686
|
import { readFile } from "fs/promises";
|
|
1180
|
-
import { extname, join as
|
|
1687
|
+
import { extname, join as join7, normalize } from "path";
|
|
1181
1688
|
import { streamSSE } from "hono/streaming";
|
|
1182
1689
|
|
|
1183
1690
|
// src/state.ts
|
|
@@ -1426,14 +1933,14 @@ var CONTENT_TYPES = {
|
|
|
1426
1933
|
function registerRoutes(app, deps) {
|
|
1427
1934
|
const { supervisor, hub, staticDir } = deps;
|
|
1428
1935
|
app.get("/", async (c) => {
|
|
1429
|
-
const html = await readFile(
|
|
1936
|
+
const html = await readFile(join7(staticDir, "index.html"), "utf8");
|
|
1430
1937
|
return c.html(html);
|
|
1431
1938
|
});
|
|
1432
1939
|
app.get("/static/:file", async (c) => {
|
|
1433
1940
|
const file = c.req.param("file");
|
|
1434
1941
|
const safe3 = normalize(file).replace(/^(\.\.[/\\])+/, "");
|
|
1435
|
-
const full =
|
|
1436
|
-
if (!full.startsWith(staticDir) || !
|
|
1942
|
+
const full = join7(staticDir, safe3);
|
|
1943
|
+
if (!full.startsWith(staticDir) || !existsSync6(full)) return c.notFound();
|
|
1437
1944
|
const body = await readFile(full);
|
|
1438
1945
|
const type = CONTENT_TYPES[extname(full).toLowerCase()] ?? "application/octet-stream";
|
|
1439
1946
|
c.header("content-type", type);
|
|
@@ -1560,7 +2067,7 @@ function clampLimit(raw, fallback, max = 200) {
|
|
|
1560
2067
|
return Math.min(Math.floor(n), max);
|
|
1561
2068
|
}
|
|
1562
2069
|
function tailFile(path, lines) {
|
|
1563
|
-
if (!
|
|
2070
|
+
if (!existsSync6(path)) return [];
|
|
1564
2071
|
const MAX_BYTES = 256 * 1024;
|
|
1565
2072
|
let fd;
|
|
1566
2073
|
try {
|
|
@@ -1649,7 +2156,7 @@ function isAddrInUse(err) {
|
|
|
1649
2156
|
return Boolean(err && typeof err === "object" && "code" in err && err.code === "EADDRINUSE");
|
|
1650
2157
|
}
|
|
1651
2158
|
function resolveStaticDir() {
|
|
1652
|
-
return
|
|
2159
|
+
return join8(dirname6(fileURLToPath2(import.meta.url)), "static");
|
|
1653
2160
|
}
|
|
1654
2161
|
|
|
1655
2162
|
// src/api.ts
|
|
@@ -2125,21 +2632,21 @@ function errorMessage2(status2, body) {
|
|
|
2125
2632
|
}
|
|
2126
2633
|
|
|
2127
2634
|
// src/cursor.ts
|
|
2128
|
-
import { mkdirSync as
|
|
2129
|
-
import { join as
|
|
2635
|
+
import { mkdirSync as mkdirSync7, readFileSync as readFileSync5, writeFileSync as writeFileSync5, existsSync as existsSync7 } from "fs";
|
|
2636
|
+
import { join as join9 } from "path";
|
|
2130
2637
|
function pathFor(workspaceId) {
|
|
2131
|
-
return
|
|
2638
|
+
return join9(cabaneDir(), "cursors", encodeURIComponent(workspaceId));
|
|
2132
2639
|
}
|
|
2133
2640
|
function readCursor(workspaceId) {
|
|
2134
2641
|
const path = pathFor(workspaceId);
|
|
2135
|
-
if (!
|
|
2136
|
-
const raw =
|
|
2642
|
+
if (!existsSync7(path)) return null;
|
|
2643
|
+
const raw = readFileSync5(path, "utf8").trim();
|
|
2137
2644
|
return raw.length > 0 ? raw : null;
|
|
2138
2645
|
}
|
|
2139
2646
|
function writeCursor(workspaceId, eventId) {
|
|
2140
2647
|
const path = pathFor(workspaceId);
|
|
2141
|
-
|
|
2142
|
-
|
|
2648
|
+
mkdirSync7(join9(cabaneDir(), "cursors"), { recursive: true });
|
|
2649
|
+
writeFileSync5(path, eventId + "\n", "utf8");
|
|
2143
2650
|
}
|
|
2144
2651
|
|
|
2145
2652
|
// src/cursor-tracker.ts
|
|
@@ -2182,20 +2689,20 @@ var CursorTracker = class {
|
|
|
2182
2689
|
};
|
|
2183
2690
|
|
|
2184
2691
|
// src/dispatch-dedupe.ts
|
|
2185
|
-
import { mkdirSync as
|
|
2186
|
-
import { join as
|
|
2692
|
+
import { mkdirSync as mkdirSync8, readFileSync as readFileSync6, writeFileSync as writeFileSync6, existsSync as existsSync8 } from "fs";
|
|
2693
|
+
import { join as join10 } from "path";
|
|
2187
2694
|
var MAX_IDS = 256;
|
|
2188
2695
|
function dir(log) {
|
|
2189
|
-
return
|
|
2696
|
+
return join10(cabaneDir(), log);
|
|
2190
2697
|
}
|
|
2191
2698
|
function pathFor2(log, workspaceId) {
|
|
2192
|
-
return
|
|
2699
|
+
return join10(dir(log), encodeURIComponent(workspaceId));
|
|
2193
2700
|
}
|
|
2194
2701
|
function readIds(log, workspaceId) {
|
|
2195
2702
|
const path = pathFor2(log, workspaceId);
|
|
2196
|
-
if (!
|
|
2703
|
+
if (!existsSync8(path)) return [];
|
|
2197
2704
|
try {
|
|
2198
|
-
return
|
|
2705
|
+
return readFileSync6(path, "utf8").split("\n").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
2199
2706
|
} catch {
|
|
2200
2707
|
return [];
|
|
2201
2708
|
}
|
|
@@ -2208,8 +2715,8 @@ function mark(log, workspaceId, eventId) {
|
|
|
2208
2715
|
if (ids.includes(eventId)) return;
|
|
2209
2716
|
ids.push(eventId);
|
|
2210
2717
|
const trimmed = ids.length > MAX_IDS ? ids.slice(-MAX_IDS) : ids;
|
|
2211
|
-
|
|
2212
|
-
|
|
2718
|
+
mkdirSync8(dir(log), { recursive: true });
|
|
2719
|
+
writeFileSync6(pathFor2(log, workspaceId), trimmed.join("\n") + "\n", "utf8");
|
|
2213
2720
|
}
|
|
2214
2721
|
function hasDispatched(workspaceId, eventId) {
|
|
2215
2722
|
return has("dispatched", workspaceId, eventId);
|
|
@@ -2225,17 +2732,17 @@ function markCompleted(workspaceId, eventId) {
|
|
|
2225
2732
|
}
|
|
2226
2733
|
var MAX_RESUME_ATTEMPTS = 3;
|
|
2227
2734
|
function resumeDir() {
|
|
2228
|
-
return
|
|
2735
|
+
return join10(cabaneDir(), "resume-attempts");
|
|
2229
2736
|
}
|
|
2230
2737
|
function resumePathFor(workspaceId) {
|
|
2231
|
-
return
|
|
2738
|
+
return join10(resumeDir(), encodeURIComponent(workspaceId));
|
|
2232
2739
|
}
|
|
2233
2740
|
function readResumeCounts(workspaceId) {
|
|
2234
2741
|
const out = /* @__PURE__ */ new Map();
|
|
2235
2742
|
const path = resumePathFor(workspaceId);
|
|
2236
|
-
if (!
|
|
2743
|
+
if (!existsSync8(path)) return out;
|
|
2237
2744
|
try {
|
|
2238
|
-
for (const line of
|
|
2745
|
+
for (const line of readFileSync6(path, "utf8").split("\n")) {
|
|
2239
2746
|
const trimmed = line.trim();
|
|
2240
2747
|
if (!trimmed) continue;
|
|
2241
2748
|
const tab = trimmed.lastIndexOf(" ");
|
|
@@ -2255,8 +2762,8 @@ function bumpResumeAttempt(workspaceId, eventId) {
|
|
|
2255
2762
|
counts.set(eventId, next);
|
|
2256
2763
|
const entries = [...counts.entries()];
|
|
2257
2764
|
const trimmed = entries.length > MAX_IDS ? entries.slice(-MAX_IDS) : entries;
|
|
2258
|
-
|
|
2259
|
-
|
|
2765
|
+
mkdirSync8(resumeDir(), { recursive: true });
|
|
2766
|
+
writeFileSync6(
|
|
2260
2767
|
resumePathFor(workspaceId),
|
|
2261
2768
|
trimmed.map(([id, c]) => `${id} ${c}`).join("\n") + "\n",
|
|
2262
2769
|
"utf8"
|
|
@@ -5968,8 +6475,8 @@ var ConnectorHealthStore = class {
|
|
|
5968
6475
|
|
|
5969
6476
|
// src/dispatcher.ts
|
|
5970
6477
|
import { randomUUID } from "crypto";
|
|
5971
|
-
import { appendFileSync as appendFileSync2, existsSync as
|
|
5972
|
-
import { join as
|
|
6478
|
+
import { appendFileSync as appendFileSync2, existsSync as existsSync11, mkdirSync as mkdirSync11, statSync } from "fs";
|
|
6479
|
+
import { join as join15 } from "path";
|
|
5973
6480
|
|
|
5974
6481
|
// src/summon.ts
|
|
5975
6482
|
import { z as z12 } from "zod";
|
|
@@ -6302,11 +6809,11 @@ function trimSlash3(s) {
|
|
|
6302
6809
|
// src/codex-instructions.ts
|
|
6303
6810
|
import { mkdtemp, rm, writeFile } from "fs/promises";
|
|
6304
6811
|
import { tmpdir } from "os";
|
|
6305
|
-
import { join as
|
|
6812
|
+
import { join as join11 } from "path";
|
|
6306
6813
|
var PREFIX = "cabane-codex-instructions-";
|
|
6307
6814
|
async function writeCodexInstructionsFile(contents) {
|
|
6308
|
-
const dir2 = await mkdtemp(
|
|
6309
|
-
const path =
|
|
6815
|
+
const dir2 = await mkdtemp(join11(tmpdir(), PREFIX));
|
|
6816
|
+
const path = join11(dir2, "instructions.md");
|
|
6310
6817
|
await writeFile(path, contents, { encoding: "utf8", mode: 384 });
|
|
6311
6818
|
return {
|
|
6312
6819
|
path,
|
|
@@ -6317,22 +6824,22 @@ async function writeCodexInstructionsFile(contents) {
|
|
|
6317
6824
|
}
|
|
6318
6825
|
|
|
6319
6826
|
// src/prepared.ts
|
|
6320
|
-
import { mkdirSync as
|
|
6321
|
-
import { join as
|
|
6827
|
+
import { mkdirSync as mkdirSync9, readFileSync as readFileSync7, rmSync as rmSync5, writeFileSync as writeFileSync7, existsSync as existsSync9 } from "fs";
|
|
6828
|
+
import { join as join12 } from "path";
|
|
6322
6829
|
function dirFor(workspaceId) {
|
|
6323
|
-
return
|
|
6830
|
+
return join12(cabaneDir(), "prepared", encodeURIComponent(workspaceId));
|
|
6324
6831
|
}
|
|
6325
6832
|
function conversationDir(workspaceId, conversationId) {
|
|
6326
|
-
return
|
|
6833
|
+
return join12(dirFor(workspaceId), encodeURIComponent(conversationId));
|
|
6327
6834
|
}
|
|
6328
6835
|
function pathFor3(workspaceId, conversationId, agentId) {
|
|
6329
|
-
return
|
|
6836
|
+
return join12(conversationDir(workspaceId, conversationId), `${encodeURIComponent(agentId)}.json`);
|
|
6330
6837
|
}
|
|
6331
6838
|
function readPrepared(workspaceId, conversationId, agentId) {
|
|
6332
6839
|
const path = pathFor3(workspaceId, conversationId, agentId);
|
|
6333
|
-
if (!
|
|
6840
|
+
if (!existsSync9(path)) return null;
|
|
6334
6841
|
try {
|
|
6335
|
-
const parsed = JSON.parse(
|
|
6842
|
+
const parsed = JSON.parse(readFileSync7(path, "utf8"));
|
|
6336
6843
|
if (parsed && typeof parsed.cwd === "string" && parsed.cwd.length > 0) {
|
|
6337
6844
|
return {
|
|
6338
6845
|
cwd: parsed.cwd,
|
|
@@ -6345,32 +6852,32 @@ function readPrepared(workspaceId, conversationId, agentId) {
|
|
|
6345
6852
|
}
|
|
6346
6853
|
}
|
|
6347
6854
|
function writePrepared(workspaceId, conversationId, agentId, result) {
|
|
6348
|
-
|
|
6349
|
-
|
|
6855
|
+
mkdirSync9(conversationDir(workspaceId, conversationId), { recursive: true });
|
|
6856
|
+
writeFileSync7(
|
|
6350
6857
|
pathFor3(workspaceId, conversationId, agentId),
|
|
6351
6858
|
JSON.stringify(result) + "\n",
|
|
6352
6859
|
"utf8"
|
|
6353
6860
|
);
|
|
6354
6861
|
}
|
|
6355
6862
|
function clearPrepared(workspaceId, conversationId, agentId) {
|
|
6356
|
-
|
|
6863
|
+
rmSync5(pathFor3(workspaceId, conversationId, agentId), { force: true });
|
|
6357
6864
|
}
|
|
6358
6865
|
|
|
6359
6866
|
// src/secrets.ts
|
|
6360
|
-
import { existsSync as
|
|
6361
|
-
import { join as
|
|
6867
|
+
import { existsSync as existsSync10, readFileSync as readFileSync8 } from "fs";
|
|
6868
|
+
import { join as join13 } from "path";
|
|
6362
6869
|
import { z as z13 } from "zod";
|
|
6363
6870
|
var PLACEHOLDER_RE = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
|
|
6364
6871
|
function secretsPath() {
|
|
6365
|
-
return
|
|
6872
|
+
return join13(cabaneDir(), "secrets.json");
|
|
6366
6873
|
}
|
|
6367
6874
|
var secretStoreSchema = z13.record(z13.string(), z13.string());
|
|
6368
6875
|
function loadSecretStore() {
|
|
6369
6876
|
const path = secretsPath();
|
|
6370
|
-
if (!
|
|
6877
|
+
if (!existsSync10(path)) return makeStore({});
|
|
6371
6878
|
let raw;
|
|
6372
6879
|
try {
|
|
6373
|
-
raw =
|
|
6880
|
+
raw = readFileSync8(path, "utf8");
|
|
6374
6881
|
} catch (err) {
|
|
6375
6882
|
throw new ConfigError(
|
|
6376
6883
|
`couldn't read ${path}: ${err instanceof Error ? err.message : String(err)}`
|
|
@@ -6448,10 +6955,10 @@ function resolveMcpSecrets(mcpServers, store) {
|
|
|
6448
6955
|
}
|
|
6449
6956
|
|
|
6450
6957
|
// src/transcript-writer.ts
|
|
6451
|
-
import { appendFileSync, chmodSync as chmodSync3, mkdirSync as
|
|
6452
|
-
import { join as
|
|
6958
|
+
import { appendFileSync, chmodSync as chmodSync3, mkdirSync as mkdirSync10, readdirSync, rmSync as rmSync6 } from "fs";
|
|
6959
|
+
import { join as join14 } from "path";
|
|
6453
6960
|
function transcriptsDir() {
|
|
6454
|
-
return
|
|
6961
|
+
return join14(cabaneDir(), "transcripts");
|
|
6455
6962
|
}
|
|
6456
6963
|
var RETAIN = 200;
|
|
6457
6964
|
var TranscriptWriter = class {
|
|
@@ -6460,9 +6967,9 @@ var TranscriptWriter = class {
|
|
|
6460
6967
|
onWarn;
|
|
6461
6968
|
constructor(dir2, meta, onWarn) {
|
|
6462
6969
|
this.onWarn = onWarn;
|
|
6463
|
-
this.path =
|
|
6970
|
+
this.path = join14(dir2, fileName(meta));
|
|
6464
6971
|
try {
|
|
6465
|
-
|
|
6972
|
+
mkdirSync10(dir2, { recursive: true });
|
|
6466
6973
|
try {
|
|
6467
6974
|
chmodSync3(dir2, 448);
|
|
6468
6975
|
} catch {
|
|
@@ -6519,7 +7026,7 @@ function pruneOld(dir2, retain) {
|
|
|
6519
7026
|
const drop = files.sort().slice(0, files.length - retain);
|
|
6520
7027
|
for (const f of drop) {
|
|
6521
7028
|
try {
|
|
6522
|
-
|
|
7029
|
+
rmSync6(join14(dir2, f), { force: true });
|
|
6523
7030
|
} catch {
|
|
6524
7031
|
}
|
|
6525
7032
|
}
|
|
@@ -6642,9 +7149,9 @@ var TurnCommitter = class {
|
|
|
6642
7149
|
// commit. A self-target is stripped here (mirror of the in-app self-strip); the
|
|
6643
7150
|
// server strips it again and resolves / ignores an unknown id.
|
|
6644
7151
|
summonField() {
|
|
6645
|
-
const
|
|
6646
|
-
if (!
|
|
6647
|
-
return { dispatch:
|
|
7152
|
+
const target2 = this.deps.summonState.agentId;
|
|
7153
|
+
if (!target2 || target2 === this.deps.agentId) return {};
|
|
7154
|
+
return { dispatch: target2 };
|
|
6648
7155
|
}
|
|
6649
7156
|
// CT326: resolve the per-turn ask into the `ask` field for a `final` commit.
|
|
6650
7157
|
// The server validates the target (must be a workspace member/owner) and
|
|
@@ -6819,16 +7326,16 @@ var DEFAULT_AGENT_IDLE_TIMEOUT_MS = 10 * 6e4;
|
|
|
6819
7326
|
var DEFAULT_AGENT_TOTAL_TIMEOUT_MS = 6 * 60 * 6e4;
|
|
6820
7327
|
function checkoutState(cwd) {
|
|
6821
7328
|
if (!cwd) return { ok: false, reason: "no checkout was resolved for this turn" };
|
|
6822
|
-
if (!
|
|
6823
|
-
const gitPath =
|
|
6824
|
-
if (!
|
|
7329
|
+
if (!existsSync11(cwd)) return { ok: false, reason: `the checkout directory is gone (${cwd})` };
|
|
7330
|
+
const gitPath = join15(cwd, ".git");
|
|
7331
|
+
if (!existsSync11(gitPath)) return { ok: false, reason: `${cwd} holds no git metadata` };
|
|
6825
7332
|
let stat;
|
|
6826
7333
|
try {
|
|
6827
7334
|
stat = statSync(gitPath);
|
|
6828
7335
|
} catch (error) {
|
|
6829
7336
|
return { ok: false, reason: `${gitPath} is unreadable (${error.message})` };
|
|
6830
7337
|
}
|
|
6831
|
-
if (stat.isDirectory() && !
|
|
7338
|
+
if (stat.isDirectory() && !existsSync11(join15(gitPath, "HEAD")))
|
|
6832
7339
|
return { ok: false, reason: `${gitPath} has no HEAD \u2014 an empty shell, not a checkout` };
|
|
6833
7340
|
return { ok: true, reason: "usable" };
|
|
6834
7341
|
}
|
|
@@ -7019,7 +7526,7 @@ var Dispatcher = class {
|
|
|
7019
7526
|
let seqCounter = 0;
|
|
7020
7527
|
const nextSeq = () => ++seqCounter;
|
|
7021
7528
|
let effectiveCwd = localCwd ?? cabaneCwd;
|
|
7022
|
-
if (effectiveCwd && !
|
|
7529
|
+
if (effectiveCwd && !existsSync11(effectiveCwd)) {
|
|
7023
7530
|
turnLog.warn(
|
|
7024
7531
|
{ cwd: effectiveCwd },
|
|
7025
7532
|
"dispatcher: configured working directory does not exist on this device \u2014 falling back to the process cwd"
|
|
@@ -7157,9 +7664,9 @@ ${reason}`,
|
|
|
7157
7664
|
}
|
|
7158
7665
|
let turnEnv = hookEnv;
|
|
7159
7666
|
if (effectiveCwd && turnContext.runtime === "codex") {
|
|
7160
|
-
const tmpDir =
|
|
7667
|
+
const tmpDir = join15(effectiveCwd, "node_modules", ".cache", "cabane-tmp", turnId);
|
|
7161
7668
|
try {
|
|
7162
|
-
|
|
7669
|
+
mkdirSync11(tmpDir, { recursive: true });
|
|
7163
7670
|
turnEnv = { ...hookEnv, TMPDIR: tmpDir };
|
|
7164
7671
|
} catch (err) {
|
|
7165
7672
|
turnLog.warn(
|
|
@@ -7298,11 +7805,11 @@ ${reason}`,
|
|
|
7298
7805
|
const totalTimeoutMs = this.opts.totalTimeoutMs ?? DEFAULT_AGENT_TOTAL_TIMEOUT_MS;
|
|
7299
7806
|
const closeTurnReceipt = (ok, reason) => {
|
|
7300
7807
|
if (!turnReceiptPath) return;
|
|
7301
|
-
const
|
|
7808
|
+
const target2 = turnReceiptPath;
|
|
7302
7809
|
turnReceiptPath = null;
|
|
7303
7810
|
try {
|
|
7304
7811
|
appendFileSync2(
|
|
7305
|
-
|
|
7812
|
+
target2,
|
|
7306
7813
|
`${JSON.stringify({
|
|
7307
7814
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7308
7815
|
event: "settled",
|
|
@@ -7342,7 +7849,7 @@ ${reason}`,
|
|
|
7342
7849
|
}
|
|
7343
7850
|
return this.concludeBeforeRun(payload, turnLog, startedAt, reason);
|
|
7344
7851
|
}
|
|
7345
|
-
const receiptPath =
|
|
7852
|
+
const receiptPath = join15(effectiveCwd, ".git", "cabane", "readiness.jsonl");
|
|
7346
7853
|
const receiptLine = (fields) => `${JSON.stringify({
|
|
7347
7854
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7348
7855
|
taskId: hookEnv.CABANE_TASK_ID,
|
|
@@ -7366,7 +7873,7 @@ ${reason}`,
|
|
|
7366
7873
|
})}
|
|
7367
7874
|
`;
|
|
7368
7875
|
try {
|
|
7369
|
-
|
|
7876
|
+
mkdirSync11(join15(effectiveCwd, ".git", "cabane"), { recursive: true });
|
|
7370
7877
|
appendFileSync2(
|
|
7371
7878
|
receiptPath,
|
|
7372
7879
|
// `starting` is the honest classification before the proof has run. The
|
|
@@ -8032,15 +8539,15 @@ async function enumerateOpencodeModels(serverUrl, fetchImpl = fetch) {
|
|
|
8032
8539
|
|
|
8033
8540
|
// src/outbox.ts
|
|
8034
8541
|
import {
|
|
8035
|
-
existsSync as
|
|
8036
|
-
mkdirSync as
|
|
8542
|
+
existsSync as existsSync12,
|
|
8543
|
+
mkdirSync as mkdirSync12,
|
|
8037
8544
|
readdirSync as readdirSync2,
|
|
8038
|
-
readFileSync as
|
|
8545
|
+
readFileSync as readFileSync9,
|
|
8039
8546
|
renameSync as renameSync3,
|
|
8040
|
-
rmSync as
|
|
8041
|
-
writeFileSync as
|
|
8547
|
+
rmSync as rmSync7,
|
|
8548
|
+
writeFileSync as writeFileSync8
|
|
8042
8549
|
} from "fs";
|
|
8043
|
-
import { join as
|
|
8550
|
+
import { join as join16 } from "path";
|
|
8044
8551
|
var MAX_ENTRIES = 2e3;
|
|
8045
8552
|
var MAX_AGE_MS = 24 * 60 * 60 * 1e3;
|
|
8046
8553
|
var Outbox = class {
|
|
@@ -8053,25 +8560,25 @@ var Outbox = class {
|
|
|
8053
8560
|
// Resolved lazily (per call, like cursor.ts) so tests that swap HOME between
|
|
8054
8561
|
// cases route writes at the right tmpdir.
|
|
8055
8562
|
dir() {
|
|
8056
|
-
return
|
|
8563
|
+
return join16(cabaneDir(), "outbox", encodeURIComponent(this.workspaceId));
|
|
8057
8564
|
}
|
|
8058
8565
|
fileFor(turnId, seq) {
|
|
8059
|
-
return
|
|
8566
|
+
return join16(this.dir(), `${encodeURIComponent(turnId)}__${seq}.json`);
|
|
8060
8567
|
}
|
|
8061
8568
|
// Persist a commit for later draining. Atomic (temp file + rename) so a
|
|
8062
8569
|
// concurrent `list()` never reads a half-written entry, then enforces the
|
|
8063
8570
|
// per-workspace bounds.
|
|
8064
8571
|
persist(entry) {
|
|
8065
8572
|
const dir2 = this.dir();
|
|
8066
|
-
|
|
8067
|
-
const
|
|
8068
|
-
const tmp = `${
|
|
8573
|
+
mkdirSync12(dir2, { recursive: true });
|
|
8574
|
+
const target2 = this.fileFor(entry.turnId, entry.seq);
|
|
8575
|
+
const tmp = `${target2}.${process.pid}.tmp`;
|
|
8069
8576
|
try {
|
|
8070
|
-
|
|
8071
|
-
renameSync3(tmp,
|
|
8577
|
+
writeFileSync8(tmp, JSON.stringify(entry) + "\n", "utf8");
|
|
8578
|
+
renameSync3(tmp, target2);
|
|
8072
8579
|
} catch (err) {
|
|
8073
8580
|
try {
|
|
8074
|
-
|
|
8581
|
+
rmSync7(tmp, { force: true });
|
|
8075
8582
|
} catch {
|
|
8076
8583
|
}
|
|
8077
8584
|
this.log?.warn(
|
|
@@ -8088,7 +8595,7 @@ var Outbox = class {
|
|
|
8088
8595
|
// wedging the drain.
|
|
8089
8596
|
list() {
|
|
8090
8597
|
const dir2 = this.dir();
|
|
8091
|
-
if (!
|
|
8598
|
+
if (!existsSync12(dir2)) return [];
|
|
8092
8599
|
let names;
|
|
8093
8600
|
try {
|
|
8094
8601
|
names = readdirSync2(dir2);
|
|
@@ -8098,9 +8605,9 @@ var Outbox = class {
|
|
|
8098
8605
|
const entries = [];
|
|
8099
8606
|
for (const name of names) {
|
|
8100
8607
|
if (!name.endsWith(".json")) continue;
|
|
8101
|
-
const full =
|
|
8608
|
+
const full = join16(dir2, name);
|
|
8102
8609
|
try {
|
|
8103
|
-
const parsed = JSON.parse(
|
|
8610
|
+
const parsed = JSON.parse(readFileSync9(full, "utf8"));
|
|
8104
8611
|
if (parsed && typeof parsed.turnId === "string" && typeof parsed.seq === "number" && typeof parsed.path === "string") {
|
|
8105
8612
|
entries.push(parsed);
|
|
8106
8613
|
} else {
|
|
@@ -8118,13 +8625,13 @@ var Outbox = class {
|
|
|
8118
8625
|
// Remove a delivered (or terminally-discarded) entry. No-op if already gone.
|
|
8119
8626
|
remove(turnId, seq) {
|
|
8120
8627
|
try {
|
|
8121
|
-
|
|
8628
|
+
rmSync7(this.fileFor(turnId, seq), { force: true });
|
|
8122
8629
|
} catch {
|
|
8123
8630
|
}
|
|
8124
8631
|
}
|
|
8125
8632
|
size() {
|
|
8126
8633
|
const dir2 = this.dir();
|
|
8127
|
-
if (!
|
|
8634
|
+
if (!existsSync12(dir2)) return 0;
|
|
8128
8635
|
try {
|
|
8129
8636
|
return readdirSync2(dir2).filter((n) => n.endsWith(".json")).length;
|
|
8130
8637
|
} catch {
|
|
@@ -8137,7 +8644,7 @@ var Outbox = class {
|
|
|
8137
8644
|
"companion outbox: dropping unreadable entry"
|
|
8138
8645
|
);
|
|
8139
8646
|
try {
|
|
8140
|
-
|
|
8647
|
+
rmSync7(full, { force: true });
|
|
8141
8648
|
} catch {
|
|
8142
8649
|
}
|
|
8143
8650
|
}
|
|
@@ -8377,6 +8884,7 @@ var CompanionSupervisor = class {
|
|
|
8377
8884
|
refreshing = false;
|
|
8378
8885
|
stopped = false;
|
|
8379
8886
|
draining = false;
|
|
8887
|
+
restartPending = false;
|
|
8380
8888
|
// CT484: latch so the companion/server version-skew warning is logged once, not
|
|
8381
8889
|
// on every 30s heartbeat.
|
|
8382
8890
|
versionSkewWarned = false;
|
|
@@ -8487,7 +8995,8 @@ var CompanionSupervisor = class {
|
|
|
8487
8995
|
// actually landed (`harnessSignals` non-null) — an absent field means "we
|
|
8488
8996
|
// didn't look this beat" and leaves the server's stored suggestion alone,
|
|
8489
8997
|
// the same fail-soft contract `models` keeps.
|
|
8490
|
-
...this.harnessSignals ? { detectedRuntimes: detectedRuntimesFor(deriveHarnessSnapshot(this.harnessSignals)) } : {}
|
|
8998
|
+
...this.harnessSignals ? { detectedRuntimes: detectedRuntimesFor(deriveHarnessSnapshot(this.harnessSignals)) } : {},
|
|
8999
|
+
...this.restartPending ? { restarting: true } : {}
|
|
8491
9000
|
});
|
|
8492
9001
|
this.hub.setDevice({ deviceId: res.deviceId });
|
|
8493
9002
|
this.deviceId = res.deviceId;
|
|
@@ -9125,32 +9634,41 @@ var CompanionSupervisor = class {
|
|
|
9125
9634
|
);
|
|
9126
9635
|
}
|
|
9127
9636
|
async drainForRestart(graceMs) {
|
|
9128
|
-
this.
|
|
9129
|
-
if (this.heartbeatTimer) clearInterval(this.heartbeatTimer);
|
|
9130
|
-
if (this.pollTimer) clearInterval(this.pollTimer);
|
|
9637
|
+
this.restartPending = true;
|
|
9131
9638
|
if (this.inFlightHeartbeat) await this.inFlightHeartbeat;
|
|
9132
9639
|
if (!this.deviceApi) throw new Error("cannot establish deploy drain before pairing");
|
|
9133
|
-
await this.
|
|
9134
|
-
|
|
9135
|
-
const
|
|
9640
|
+
await this.sendHeartbeat();
|
|
9641
|
+
const drainStartedAt = Date.now();
|
|
9642
|
+
const deadline = Date.now() + Math.max(0, graceMs);
|
|
9136
9643
|
let timedOut = false;
|
|
9137
|
-
|
|
9644
|
+
while (true) {
|
|
9645
|
+
const turns = [...this.workspaces.values()].flatMap((wr) => [...wr.chains.values()]);
|
|
9646
|
+
if (turns.length === 0) break;
|
|
9647
|
+
const remainingMs = deadline - Date.now();
|
|
9648
|
+
if (remainingMs <= 0) {
|
|
9649
|
+
timedOut = true;
|
|
9650
|
+
break;
|
|
9651
|
+
}
|
|
9138
9652
|
let timer;
|
|
9139
9653
|
await Promise.race([
|
|
9140
9654
|
Promise.allSettled(turns),
|
|
9141
9655
|
new Promise((resolve) => {
|
|
9142
|
-
timer = setTimeout(
|
|
9143
|
-
() => {
|
|
9144
|
-
timedOut = true;
|
|
9145
|
-
resolve();
|
|
9146
|
-
},
|
|
9147
|
-
Math.max(0, graceMs)
|
|
9148
|
-
);
|
|
9656
|
+
timer = setTimeout(resolve, remainingMs);
|
|
9149
9657
|
timer.unref?.();
|
|
9150
9658
|
})
|
|
9151
9659
|
]);
|
|
9152
9660
|
if (timer) clearTimeout(timer);
|
|
9153
9661
|
}
|
|
9662
|
+
this.log.info(
|
|
9663
|
+
{ waitedMs: Date.now() - drainStartedAt, timedOut },
|
|
9664
|
+
timedOut ? "companion: deploy drain grace elapsed; fencing admission for restart" : "companion: deploy drain reached a quiet point; fencing admission for restart"
|
|
9665
|
+
);
|
|
9666
|
+
this.draining = true;
|
|
9667
|
+
if (this.heartbeatTimer) clearInterval(this.heartbeatTimer);
|
|
9668
|
+
if (this.pollTimer) clearInterval(this.pollTimer);
|
|
9669
|
+
if (this.inFlightHeartbeat) await this.inFlightHeartbeat;
|
|
9670
|
+
await this.deviceApi.beginDrain();
|
|
9671
|
+
for (const wr of this.workspaces.values()) wr.sub?.stop();
|
|
9154
9672
|
await Promise.allSettled(
|
|
9155
9673
|
[...this.workspaces.values()].flatMap(
|
|
9156
9674
|
(wr) => [...wr.agents.values()].map((agent) => agent.api.drainOutbox())
|
|
@@ -9241,22 +9759,22 @@ function handleUncaught(log, err, origin) {
|
|
|
9241
9759
|
}
|
|
9242
9760
|
|
|
9243
9761
|
// src/crash-marker.ts
|
|
9244
|
-
import { existsSync as
|
|
9245
|
-
import { join as
|
|
9762
|
+
import { existsSync as existsSync13, mkdirSync as mkdirSync13, readFileSync as readFileSync10, rmSync as rmSync8, writeFileSync as writeFileSync9 } from "fs";
|
|
9763
|
+
import { join as join17 } from "path";
|
|
9246
9764
|
function crashMarkerPath() {
|
|
9247
|
-
return
|
|
9765
|
+
return join17(cabaneDir(), "last-error.json");
|
|
9248
9766
|
}
|
|
9249
9767
|
function recordCrash(rec2) {
|
|
9250
9768
|
try {
|
|
9251
|
-
|
|
9252
|
-
|
|
9769
|
+
mkdirSync13(cabaneDir(), { recursive: true });
|
|
9770
|
+
writeFileSync9(crashMarkerPath(), JSON.stringify(rec2, null, 2) + "\n");
|
|
9253
9771
|
} catch {
|
|
9254
9772
|
}
|
|
9255
9773
|
}
|
|
9256
9774
|
function clearCrash() {
|
|
9257
9775
|
try {
|
|
9258
9776
|
const path = crashMarkerPath();
|
|
9259
|
-
if (
|
|
9777
|
+
if (existsSync13(path)) rmSync8(path, { force: true });
|
|
9260
9778
|
} catch {
|
|
9261
9779
|
}
|
|
9262
9780
|
}
|
|
@@ -9467,7 +9985,8 @@ companion: deploy drain requested (${graceMs}ms grace)\u2026
|
|
|
9467
9985
|
|
|
9468
9986
|
// src/commands/status.ts
|
|
9469
9987
|
import { readdirSync as readdirSync3 } from "fs";
|
|
9470
|
-
async function status() {
|
|
9988
|
+
async function status(deps = {}) {
|
|
9989
|
+
const readService = deps.service ?? serviceStatus;
|
|
9471
9990
|
const cfg = loadConfig();
|
|
9472
9991
|
if (!cfg || !cfg.deviceToken) {
|
|
9473
9992
|
process.stdout.write(
|
|
@@ -9503,6 +10022,13 @@ async function status() {
|
|
|
9503
10022
|
} else {
|
|
9504
10023
|
process.stdout.write(
|
|
9505
10024
|
`companion: not running \u2014 \`cabane-companion start\` (foreground) or \`cabane-companion start --daemon\` (background)
|
|
10025
|
+
`
|
|
10026
|
+
);
|
|
10027
|
+
}
|
|
10028
|
+
const svc = readService();
|
|
10029
|
+
if (svc.installed) {
|
|
10030
|
+
process.stdout.write(
|
|
10031
|
+
`autostart: ${svc.manager} \u2014 starts at login` + (svc.manager === "systemd-user" && svc.linger !== "yes" ? ", while you are logged in" : "") + ` (details: cabane-companion service status)
|
|
9506
10032
|
`
|
|
9507
10033
|
);
|
|
9508
10034
|
}
|
|
@@ -9552,12 +10078,34 @@ function formatUptime(startedAt) {
|
|
|
9552
10078
|
// src/commands/stop.ts
|
|
9553
10079
|
var TERM_GRACE_MS = 6e3;
|
|
9554
10080
|
var POLL_INTERVAL_MS2 = 150;
|
|
10081
|
+
var SERVICE_STOP_GRACE_MS = 3e3;
|
|
9555
10082
|
async function stop(deps = {}) {
|
|
9556
10083
|
const readState = deps.readState ?? readLiveRuntimeState;
|
|
9557
10084
|
const verify = deps.verify ?? ((s) => verifyRuntime(s));
|
|
9558
10085
|
const kill = deps.kill ?? ((pid2, signal) => process.kill(pid2, signal));
|
|
9559
10086
|
const sleep4 = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
9560
10087
|
const now = deps.now ?? (() => Date.now());
|
|
10088
|
+
const stopSvc = deps.stopService ?? (() => stopService());
|
|
10089
|
+
const svc = stopSvc();
|
|
10090
|
+
if (svc.handled) {
|
|
10091
|
+
if (!svc.ok) {
|
|
10092
|
+
process.stdout.write(
|
|
10093
|
+
`companion: ${svc.manager} wouldn't stop the service${svc.detail ? ` (${svc.detail})` : ""}.
|
|
10094
|
+
The service manager owns this process \u2014 stopping it directly would just be restarted.
|
|
10095
|
+
Check \`cabane-companion service status\`, or remove it with \`cabane-companion service disable\`.
|
|
10096
|
+
`
|
|
10097
|
+
);
|
|
10098
|
+
process.exitCode = 1;
|
|
10099
|
+
return;
|
|
10100
|
+
}
|
|
10101
|
+
const deadline2 = now() + SERVICE_STOP_GRACE_MS;
|
|
10102
|
+
while (readState() && now() < deadline2) await sleep4(POLL_INTERVAL_MS2);
|
|
10103
|
+
if (!readState()) {
|
|
10104
|
+
process.stdout.write(`companion: stopped (${svc.manager} service stopped).
|
|
10105
|
+
`);
|
|
10106
|
+
return;
|
|
10107
|
+
}
|
|
10108
|
+
}
|
|
9561
10109
|
const state = readState();
|
|
9562
10110
|
if (!state) {
|
|
9563
10111
|
process.stdout.write("companion: not running (nothing to stop).\n");
|
|
@@ -9607,8 +10155,8 @@ function isAlive(kill, pid) {
|
|
|
9607
10155
|
}
|
|
9608
10156
|
|
|
9609
10157
|
// src/commands/transcript.ts
|
|
9610
|
-
import { existsSync as
|
|
9611
|
-
import { isAbsolute, join as
|
|
10158
|
+
import { existsSync as existsSync14, readFileSync as readFileSync11, readdirSync as readdirSync4 } from "fs";
|
|
10159
|
+
import { isAbsolute, join as join18 } from "path";
|
|
9612
10160
|
async function transcript(opts = {}) {
|
|
9613
10161
|
const dir2 = transcriptsDir();
|
|
9614
10162
|
if (opts.follow) {
|
|
@@ -9625,7 +10173,7 @@ async function transcript(opts = {}) {
|
|
|
9625
10173
|
process.stdout.write(emptyMessage(dir2));
|
|
9626
10174
|
return;
|
|
9627
10175
|
}
|
|
9628
|
-
process.stdout.write(renderFile(
|
|
10176
|
+
process.stdout.write(renderFile(join18(dir2, newest)) + "\n");
|
|
9629
10177
|
return;
|
|
9630
10178
|
}
|
|
9631
10179
|
printList(dir2);
|
|
@@ -9708,7 +10256,7 @@ function isComplete(content) {
|
|
|
9708
10256
|
async function followTranscripts(dir2) {
|
|
9709
10257
|
const follower = new TranscriptFollower({
|
|
9710
10258
|
listFiles: () => listFiles(dir2),
|
|
9711
|
-
read: (f) =>
|
|
10259
|
+
read: (f) => readFileSync11(join18(dir2, f), "utf8"),
|
|
9712
10260
|
write: (s) => process.stdout.write(s),
|
|
9713
10261
|
// CSI: cursor up `n` lines, then erase from cursor to end of screen.
|
|
9714
10262
|
clearLines: (n) => process.stdout.write(`\x1B[${n}A\x1B[0J`),
|
|
@@ -9748,7 +10296,7 @@ function printList(dir2) {
|
|
|
9748
10296
|
|
|
9749
10297
|
`);
|
|
9750
10298
|
for (const f of files.slice(0, 20)) {
|
|
9751
|
-
const { meta, outcome } = peek(
|
|
10299
|
+
const { meta, outcome } = peek(join18(dir2, f));
|
|
9752
10300
|
const when = fmtTime(rec(meta)?.ts);
|
|
9753
10301
|
const ws = str2(rec(meta)?.workspaceSlug);
|
|
9754
10302
|
const o = rec(outcome);
|
|
@@ -9769,7 +10317,7 @@ function peek(path) {
|
|
|
9769
10317
|
let meta;
|
|
9770
10318
|
let outcome;
|
|
9771
10319
|
try {
|
|
9772
|
-
for (const line of
|
|
10320
|
+
for (const line of readFileSync11(path, "utf8").split("\n")) {
|
|
9773
10321
|
if (!line.trim()) continue;
|
|
9774
10322
|
const o = safeParse(line);
|
|
9775
10323
|
const t = str2(rec(o)?.type);
|
|
@@ -9780,29 +10328,29 @@ function peek(path) {
|
|
|
9780
10328
|
}
|
|
9781
10329
|
return { meta, outcome };
|
|
9782
10330
|
}
|
|
9783
|
-
function resolveTarget(dir2,
|
|
9784
|
-
if (isAbsolute(
|
|
9785
|
-
if (
|
|
9786
|
-
throw new CompanionError(`no transcript at ${
|
|
10331
|
+
function resolveTarget(dir2, target2) {
|
|
10332
|
+
if (isAbsolute(target2) || target2.includes("/")) {
|
|
10333
|
+
if (existsSync14(target2)) return target2;
|
|
10334
|
+
throw new CompanionError(`no transcript at ${target2}.`);
|
|
9787
10335
|
}
|
|
9788
|
-
const exact =
|
|
9789
|
-
if (
|
|
9790
|
-
const matches = listFiles(dir2).filter((f) => f.includes(
|
|
9791
|
-
if (matches.length === 1) return
|
|
10336
|
+
const exact = join18(dir2, target2);
|
|
10337
|
+
if (existsSync14(exact)) return exact;
|
|
10338
|
+
const matches = listFiles(dir2).filter((f) => f.includes(target2));
|
|
10339
|
+
if (matches.length === 1) return join18(dir2, matches[0]);
|
|
9792
10340
|
if (matches.length === 0) {
|
|
9793
10341
|
throw new CompanionError(
|
|
9794
|
-
`no transcript matching "${
|
|
10342
|
+
`no transcript matching "${target2}" in ${dir2}. Run \`cabane-companion transcript\` to list them.`
|
|
9795
10343
|
);
|
|
9796
10344
|
}
|
|
9797
10345
|
throw new CompanionError(
|
|
9798
|
-
`"${
|
|
10346
|
+
`"${target2}" matches ${matches.length} transcripts \u2014 be more specific:
|
|
9799
10347
|
` + matches.slice(0, 10).map((m) => ` ${m}`).join("\n")
|
|
9800
10348
|
);
|
|
9801
10349
|
}
|
|
9802
10350
|
function renderFile(path) {
|
|
9803
10351
|
let content;
|
|
9804
10352
|
try {
|
|
9805
|
-
content =
|
|
10353
|
+
content = readFileSync11(path, "utf8");
|
|
9806
10354
|
} catch (err) {
|
|
9807
10355
|
throw new CompanionError(
|
|
9808
10356
|
`couldn't read ${path}: ${err instanceof Error ? err.message : String(err)}`
|
|
@@ -9939,28 +10487,37 @@ program.command("pair").description(
|
|
|
9939
10487
|
});
|
|
9940
10488
|
});
|
|
9941
10489
|
program.command("write-paired-config", { hidden: true }).description("persist an already-completed device enrollment payload from stdin.").action(() => {
|
|
9942
|
-
writeCompletedPairing(
|
|
10490
|
+
writeCompletedPairing(readFileSync12(0, "utf8"));
|
|
9943
10491
|
});
|
|
9944
|
-
program.command("start").description("pull this device\u2019s assigned agents from cabane and run them.").option("--open", "open the dashboard in a browser on startup (default: off)").option("--no-open", "don't auto-open the dashboard (overrides config.autoOpen)").option("--daemon", "run detached in the background (terminal returns; replies keep landing)").option("--port <port>", "dashboard port (default 7474; falls through if taken)", parsePort).action(
|
|
9945
|
-
|
|
9946
|
-
|
|
10492
|
+
program.command("start").description("pull this device\u2019s assigned agents from cabane and run them.").option("--open", "open the dashboard in a browser on startup (default: off)").option("--no-open", "don't auto-open the dashboard (overrides config.autoOpen)").option("--daemon", "run detached in the background (terminal returns; replies keep landing)").option("--foreground", "run attached in this terminal (never installs the login service)").option("--port <port>", "dashboard port (default 7474; falls through if taken)", parsePort).action(
|
|
10493
|
+
async (opts) => {
|
|
10494
|
+
if (opts.daemon && !opts.foreground) {
|
|
10495
|
+
await startDaemon({
|
|
10496
|
+
...opts.port !== void 0 ? { port: opts.port } : {}
|
|
10497
|
+
});
|
|
10498
|
+
return;
|
|
10499
|
+
}
|
|
10500
|
+
await start({
|
|
10501
|
+
// commander sets `open` to true for `--open`, false for `--no-open`, and
|
|
10502
|
+
// leaves it undefined when neither is passed (respect config + env).
|
|
10503
|
+
...opts.open !== void 0 ? { open: opts.open } : {},
|
|
9947
10504
|
...opts.port !== void 0 ? { port: opts.port } : {}
|
|
9948
10505
|
});
|
|
9949
|
-
return;
|
|
9950
10506
|
}
|
|
9951
|
-
|
|
9952
|
-
// commander sets `open` to true for `--open`, false for `--no-open`, and
|
|
9953
|
-
// leaves it undefined when neither is passed (respect config + env).
|
|
9954
|
-
...opts.open !== void 0 ? { open: opts.open } : {},
|
|
9955
|
-
...opts.port !== void 0 ? { port: opts.port } : {}
|
|
9956
|
-
});
|
|
9957
|
-
});
|
|
10507
|
+
);
|
|
9958
10508
|
program.command("stop").description("stop a running companion (SIGTERM, then force-kill after a timeout).").action(async () => {
|
|
9959
10509
|
await stop();
|
|
9960
10510
|
});
|
|
9961
10511
|
program.command("status").description("print the companion's local state (pairing, secrets, log path).").action(async () => {
|
|
9962
10512
|
await status();
|
|
9963
10513
|
});
|
|
10514
|
+
var service = program.command("service").description("inspect or remove the login service that starts the companion when you log in.");
|
|
10515
|
+
service.command("status").description("print the service manager, whether the unit is installed and running.").action(() => {
|
|
10516
|
+
serviceStatusCommand();
|
|
10517
|
+
});
|
|
10518
|
+
service.command("disable").description("stop the companion and remove the login service, leaving the machine clean.").action(() => {
|
|
10519
|
+
serviceDisableCommand();
|
|
10520
|
+
});
|
|
9964
10521
|
program.command("transcript").description("show the full agent transcript for a recent dispatch (the agent's whole turn).").argument("[file]", "a transcript filename or substring; omit to list recent transcripts").option("--last", "render the most recent transcript").option("-f, --follow", "watch for new turns and live-render them as they land (Ctrl-C to stop)").action(async (file, opts) => {
|
|
9965
10522
|
await transcript({
|
|
9966
10523
|
...file !== void 0 ? { target: file } : {},
|