@cabane/companion 0.6.34 → 0.6.36
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 +128 -36
- package/dist/cli.js +233 -696
- package/dist/runtime.js +50 -18
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -806,8 +806,12 @@ async function warnAboutHarnessReadiness(cfg, deps = {}) {
|
|
|
806
806
|
...claudeInstalled ? ["Claude Code"] : [],
|
|
807
807
|
...codexInstalled ? ["Codex"] : []
|
|
808
808
|
];
|
|
809
|
+
const connectCommands = [
|
|
810
|
+
...claudeInstalled ? ["`cabane-companion connect claude-code`"] : [],
|
|
811
|
+
...codexInstalled ? ["`cabane-companion connect codex`"] : []
|
|
812
|
+
];
|
|
809
813
|
warn(
|
|
810
|
-
"No harness is connected on this device yet, so no agent turn can run here. " + (installed.length > 0 ? `We found ${installed.join(" and ")} on this machine \u2014 connect ${installed.length > 1 ? "one" : "it"}
|
|
814
|
+
"No harness is connected on this device yet, so no agent turn can run here. " + (installed.length > 0 ? `We found ${installed.join(" and ")} on this machine \u2014 connect ${installed.length > 1 ? "one with " : "it with "}${connectCommands.join(" or ")} and turns start routing here.` : "Install a harness and sign in \u2014 Claude Code (`npm i -g @anthropic-ai/claude-code`), the Codex CLI (`codex login`), or `opencode serve` \u2014 then connect it with `cabane-companion connect <harness>` (pass `--url <url>` for opencode).")
|
|
811
815
|
);
|
|
812
816
|
}
|
|
813
817
|
|
|
@@ -1010,7 +1014,7 @@ async function shakeOutHarness(runtime, cfg, deps = {}) {
|
|
|
1010
1014
|
try {
|
|
1011
1015
|
if (runtime === "opencode") {
|
|
1012
1016
|
const url = cfg.opencode?.serverUrl;
|
|
1013
|
-
if (!url) return "
|
|
1017
|
+
if (!url) return "absent";
|
|
1014
1018
|
return await probeOpencode(url) !== null ? "ok" : "failed";
|
|
1015
1019
|
}
|
|
1016
1020
|
const { auth, presence } = runtime === "codex" ? {
|
|
@@ -1020,11 +1024,13 @@ async function shakeOutHarness(runtime, cfg, deps = {}) {
|
|
|
1020
1024
|
auth: ["claude", ["auth", "status"]],
|
|
1021
1025
|
presence: ["claude", ["--version"]]
|
|
1022
1026
|
};
|
|
1027
|
+
const presenceRun = await run(presence[0], [...presence[1]]);
|
|
1028
|
+
if (presenceRun.error === "spawn") return "absent";
|
|
1029
|
+
if (presenceRun.code !== 0) return "unverified";
|
|
1023
1030
|
const authRun = await run(auth[0], [...auth[1]]);
|
|
1024
1031
|
if (authRun.code === 0) return "ok";
|
|
1025
1032
|
if (looksUnsupported(authRun.output)) {
|
|
1026
|
-
|
|
1027
|
-
return presenceRun.code === 0 ? "unverified" : "failed";
|
|
1033
|
+
return "unverified";
|
|
1028
1034
|
}
|
|
1029
1035
|
return "failed";
|
|
1030
1036
|
} catch {
|
|
@@ -1032,6 +1038,7 @@ async function shakeOutHarness(runtime, cfg, deps = {}) {
|
|
|
1032
1038
|
}
|
|
1033
1039
|
}
|
|
1034
1040
|
function connectedLine(runtime, verdict) {
|
|
1041
|
+
if (verdict === "absent") throw new Error("an absent harness cannot be connected");
|
|
1035
1042
|
const label = HARNESS_LABELS[runtime];
|
|
1036
1043
|
if (verdict !== "failed") return `${label} connected.`;
|
|
1037
1044
|
return `${label} connected \u2014 ${FAILED_SUFFIX[runtime]}`;
|
|
@@ -1041,6 +1048,14 @@ var FAILED_SUFFIX = {
|
|
|
1041
1048
|
codex: "it doesn\u2019t look signed in yet. Run `codex login` once, then it\u2019s ready.",
|
|
1042
1049
|
opencode: "its server isn\u2019t answering. Start `opencode serve`, then it\u2019s ready."
|
|
1043
1050
|
};
|
|
1051
|
+
function absentLine(runtime) {
|
|
1052
|
+
if (runtime === "opencode") {
|
|
1053
|
+
return "No opencode server is configured \u2014 run `opencode serve` and connect with `--url`.";
|
|
1054
|
+
}
|
|
1055
|
+
const label = HARNESS_LABELS[runtime];
|
|
1056
|
+
const login = runtime === "codex" ? "`codex login`" : "`claude`";
|
|
1057
|
+
return `${label} isn\u2019t installed on this machine \u2014 install it and sign in (${login}), then run this again.`;
|
|
1058
|
+
}
|
|
1044
1059
|
function looksUnsupported(output) {
|
|
1045
1060
|
return /unrecognized|unknown (sub)?command|unexpected argument|invalid (sub)?command|no such (sub)?command|usage:|did you mean/i.test(
|
|
1046
1061
|
output
|
|
@@ -1049,17 +1064,17 @@ function looksUnsupported(output) {
|
|
|
1049
1064
|
function runBounded(command, args) {
|
|
1050
1065
|
return new Promise((resolve) => {
|
|
1051
1066
|
let settled = false;
|
|
1052
|
-
const done = (
|
|
1067
|
+
const done = (result) => {
|
|
1053
1068
|
if (settled) return;
|
|
1054
1069
|
settled = true;
|
|
1055
1070
|
clearTimeout(timer);
|
|
1056
|
-
resolve(
|
|
1071
|
+
resolve(result);
|
|
1057
1072
|
};
|
|
1058
1073
|
let child;
|
|
1059
1074
|
try {
|
|
1060
1075
|
child = spawn4(command, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
1061
1076
|
} catch {
|
|
1062
|
-
resolve({ code: null, output: "" });
|
|
1077
|
+
resolve({ code: null, output: "", error: "spawn" });
|
|
1063
1078
|
return;
|
|
1064
1079
|
}
|
|
1065
1080
|
let out = "";
|
|
@@ -1070,11 +1085,11 @@ function runBounded(command, args) {
|
|
|
1070
1085
|
child.stderr?.on("data", capture);
|
|
1071
1086
|
const timer = setTimeout(() => {
|
|
1072
1087
|
child.kill("SIGKILL");
|
|
1073
|
-
done(null, out);
|
|
1088
|
+
done({ code: null, output: out, error: "timeout" });
|
|
1074
1089
|
}, CHECK_TIMEOUT_MS);
|
|
1075
1090
|
timer.unref?.();
|
|
1076
|
-
child.once("error", () => done(null, out));
|
|
1077
|
-
child.once("exit", (code) => done(code, out));
|
|
1091
|
+
child.once("error", () => done({ code: null, output: out, error: "spawn" }));
|
|
1092
|
+
child.once("exit", (code) => done({ code, output: out }));
|
|
1078
1093
|
});
|
|
1079
1094
|
}
|
|
1080
1095
|
|
|
@@ -1271,11 +1286,23 @@ async function connect2(raw, opts = {}) {
|
|
|
1271
1286
|
}
|
|
1272
1287
|
const live = await liveSocket();
|
|
1273
1288
|
if (live) {
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1289
|
+
let result;
|
|
1290
|
+
try {
|
|
1291
|
+
result = await controlRequest(
|
|
1292
|
+
live,
|
|
1293
|
+
{
|
|
1294
|
+
cmd: "connect",
|
|
1295
|
+
runtime,
|
|
1296
|
+
...opts.serverUrl ? { serverUrl: opts.serverUrl } : {}
|
|
1297
|
+
},
|
|
1298
|
+
1e4
|
|
1299
|
+
);
|
|
1300
|
+
} catch (err) {
|
|
1301
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
1302
|
+
throw new CompanionError(
|
|
1303
|
+
`Couldn\u2019t reach the running companion \u2014 ${detail}; try again, or \`cabane-companion stop\` and \`start\`.`
|
|
1304
|
+
);
|
|
1305
|
+
}
|
|
1279
1306
|
if (!result.ok) throw new CompanionError(result.message);
|
|
1280
1307
|
write(INDENT + tick(result.message));
|
|
1281
1308
|
return;
|
|
@@ -1307,8 +1334,9 @@ async function connect2(raw, opts = {}) {
|
|
|
1307
1334
|
}
|
|
1308
1335
|
next = { ...cfg, opencode: { serverUrl } };
|
|
1309
1336
|
}
|
|
1310
|
-
saveConfig(next);
|
|
1311
1337
|
const verdict = await shakeOutHarness(runtime, next);
|
|
1338
|
+
if (verdict === "absent") throw new CompanionError(absentLine(runtime));
|
|
1339
|
+
saveConfig(next);
|
|
1312
1340
|
write(INDENT + tick(connectedLine(runtime, verdict)));
|
|
1313
1341
|
write(`${INDENT}Not running yet \u2014 start it with: cabane-companion start`);
|
|
1314
1342
|
}
|
|
@@ -1639,30 +1667,10 @@ async function pair(opts = {}) {
|
|
|
1639
1667
|
}
|
|
1640
1668
|
|
|
1641
1669
|
// src/cli.ts
|
|
1642
|
-
import { readFileSync as
|
|
1670
|
+
import { readFileSync as readFileSync11 } from "fs";
|
|
1643
1671
|
|
|
1644
|
-
// src/
|
|
1645
|
-
import {
|
|
1646
|
-
|
|
1647
|
-
// src/service/host.ts
|
|
1648
|
-
import { spawnSync } from "child_process";
|
|
1649
|
-
import { existsSync as existsSync6, mkdirSync as mkdirSync6, readFileSync as readFileSync4, rmSync as rmSync5, writeFileSync as writeFileSync4 } from "fs";
|
|
1650
|
-
import { homedir as homedir3 } from "os";
|
|
1651
|
-
|
|
1652
|
-
// src/cli-entry.ts
|
|
1653
|
-
import { existsSync as existsSync5 } from "fs";
|
|
1654
|
-
import { fileURLToPath } from "url";
|
|
1655
|
-
var RELATIVE_CANDIDATES = ["./cli.js", "../dist/cli.js", "../cli.js"];
|
|
1656
|
-
function companionCliEntry(deps = {}) {
|
|
1657
|
-
const exists = deps.exists ?? existsSync5;
|
|
1658
|
-
const candidates = deps.candidates ?? RELATIVE_CANDIDATES.map((rel) => fileURLToPath(new URL(rel, import.meta.url)));
|
|
1659
|
-
for (const candidate of candidates) {
|
|
1660
|
-
if (exists(candidate)) return candidate;
|
|
1661
|
-
}
|
|
1662
|
-
const argv1 = "argv1" in deps ? deps.argv1 : process.argv[1];
|
|
1663
|
-
if (argv1 && exists(argv1)) return argv1;
|
|
1664
|
-
return candidates[0] ?? "";
|
|
1665
|
-
}
|
|
1672
|
+
// src/commands/start.ts
|
|
1673
|
+
import { hostname as hostname2 } from "os";
|
|
1666
1674
|
|
|
1667
1675
|
// src/logger.ts
|
|
1668
1676
|
import { createWriteStream, mkdirSync as mkdirSync5 } from "fs";
|
|
@@ -1692,473 +1700,55 @@ function consoleMessageFormat(log, messageKey) {
|
|
|
1692
1700
|
return short ? `${short} ${msg}` : msg;
|
|
1693
1701
|
}
|
|
1694
1702
|
var cached = null;
|
|
1695
|
-
|
|
1696
|
-
|
|
1703
|
+
var consoleLogging = true;
|
|
1704
|
+
function setConsoleLogging(enabled) {
|
|
1705
|
+
consoleLogging = enabled;
|
|
1706
|
+
}
|
|
1707
|
+
function createLogger(destinations = {}) {
|
|
1697
1708
|
const path = companionLogPath();
|
|
1698
|
-
mkdirSync5(dirname3(path), { recursive: true });
|
|
1709
|
+
if (!destinations.file) mkdirSync5(dirname3(path), { recursive: true });
|
|
1699
1710
|
const streams = [];
|
|
1700
1711
|
if (process.env.CABANE_COMPANION_DAEMON !== "1") {
|
|
1701
1712
|
const consoleStream = pretty({
|
|
1702
1713
|
colorize: true,
|
|
1703
1714
|
ignore: CONSOLE_IGNORE,
|
|
1704
|
-
messageFormat: (log, messageKey) => consoleMessageFormat(log, messageKey)
|
|
1715
|
+
messageFormat: (log, messageKey) => consoleMessageFormat(log, messageKey),
|
|
1716
|
+
...destinations.console ? { destination: destinations.console } : {}
|
|
1705
1717
|
});
|
|
1706
|
-
streams.push({
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
}
|
|
1712
|
-
|
|
1713
|
-
// src/service/host.ts
|
|
1714
|
-
var RUN_TIMEOUT_MS = 1e4;
|
|
1715
|
-
function defaultServiceHost() {
|
|
1716
|
-
if (process.env.VITEST) {
|
|
1717
|
-
throw new Error(
|
|
1718
|
-
"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."
|
|
1719
|
-
);
|
|
1720
|
-
}
|
|
1721
|
-
return {
|
|
1722
|
-
platform: process.platform,
|
|
1723
|
-
env: process.env,
|
|
1724
|
-
home: homedir3(),
|
|
1725
|
-
uid: process.getuid?.() ?? 0,
|
|
1726
|
-
execPath: process.execPath,
|
|
1727
|
-
cliPath: companionCliEntry(),
|
|
1728
|
-
logPath: companionLogPath(),
|
|
1729
|
-
fs: {
|
|
1730
|
-
read: (path) => {
|
|
1731
|
-
try {
|
|
1732
|
-
return readFileSync4(path, "utf8");
|
|
1733
|
-
} catch {
|
|
1734
|
-
return null;
|
|
1718
|
+
streams.push({
|
|
1719
|
+
level: "info",
|
|
1720
|
+
stream: {
|
|
1721
|
+
write(chunk) {
|
|
1722
|
+
if (consoleLogging) consoleStream.write(chunk);
|
|
1735
1723
|
}
|
|
1736
|
-
},
|
|
1737
|
-
write: (path, contents) => writeFileSync4(path, contents, "utf8"),
|
|
1738
|
-
remove: (path) => rmSync5(path, { force: true }),
|
|
1739
|
-
exists: (path) => existsSync6(path),
|
|
1740
|
-
mkdirp: (dir2) => {
|
|
1741
|
-
mkdirSync6(dir2, { recursive: true });
|
|
1742
1724
|
}
|
|
1743
|
-
}
|
|
1744
|
-
run: (cmd, args) => {
|
|
1745
|
-
const res = spawnSync(cmd, args, { encoding: "utf8", timeout: RUN_TIMEOUT_MS });
|
|
1746
|
-
return {
|
|
1747
|
-
ok: res.status === 0,
|
|
1748
|
-
stdout: res.stdout ?? "",
|
|
1749
|
-
stderr: res.stderr ?? (res.error ? res.error.message : "")
|
|
1750
|
-
};
|
|
1751
|
-
}
|
|
1752
|
-
};
|
|
1753
|
-
}
|
|
1754
|
-
|
|
1755
|
-
// src/service/launchd.ts
|
|
1756
|
-
import { join as join6 } from "path";
|
|
1757
|
-
var LAUNCHD_LABEL = "ai.cabane.companion";
|
|
1758
|
-
function launchAgentPath(home) {
|
|
1759
|
-
return join6(home, "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`);
|
|
1760
|
-
}
|
|
1761
|
-
function renderPlist(input) {
|
|
1762
|
-
const args = input.programArguments.map((a) => ` <string>${xml(a)}</string>`).join("\n");
|
|
1763
|
-
const env = Object.entries(input.environment).map(([k, v]) => ` <key>${xml(k)}</key>
|
|
1764
|
-
<string>${xml(v)}</string>`).join("\n");
|
|
1765
|
-
return [
|
|
1766
|
-
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
1767
|
-
'<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
|
|
1768
|
-
'<plist version="1.0">',
|
|
1769
|
-
"<dict>",
|
|
1770
|
-
" <key>Label</key>",
|
|
1771
|
-
` <string>${LAUNCHD_LABEL}</string>`,
|
|
1772
|
-
" <key>ProgramArguments</key>",
|
|
1773
|
-
" <array>",
|
|
1774
|
-
args,
|
|
1775
|
-
" </array>",
|
|
1776
|
-
" <key>RunAtLoad</key>",
|
|
1777
|
-
" <true/>",
|
|
1778
|
-
" <key>KeepAlive</key>",
|
|
1779
|
-
" <dict>",
|
|
1780
|
-
" <key>SuccessfulExit</key>",
|
|
1781
|
-
" <false/>",
|
|
1782
|
-
" </dict>",
|
|
1783
|
-
" <key>EnvironmentVariables</key>",
|
|
1784
|
-
" <dict>",
|
|
1785
|
-
env,
|
|
1786
|
-
" </dict>",
|
|
1787
|
-
" <key>StandardOutPath</key>",
|
|
1788
|
-
` <string>${xml(input.logPath)}</string>`,
|
|
1789
|
-
" <key>StandardErrorPath</key>",
|
|
1790
|
-
` <string>${xml(input.logPath)}</string>`,
|
|
1791
|
-
"</dict>",
|
|
1792
|
-
"</plist>",
|
|
1793
|
-
""
|
|
1794
|
-
].join("\n");
|
|
1795
|
-
}
|
|
1796
|
-
function domain(host) {
|
|
1797
|
-
return `gui/${host.uid}`;
|
|
1798
|
-
}
|
|
1799
|
-
function target(host) {
|
|
1800
|
-
return `${domain(host)}/${LAUNCHD_LABEL}`;
|
|
1801
|
-
}
|
|
1802
|
-
function launchdStart(host, plistPath) {
|
|
1803
|
-
host.run("launchctl", ["bootout", target(host)]);
|
|
1804
|
-
const res = host.run("launchctl", ["bootstrap", domain(host), plistPath]);
|
|
1805
|
-
return res.ok ? { ok: true } : { ok: false, detail: firstLine(res.stderr || res.stdout) };
|
|
1806
|
-
}
|
|
1807
|
-
function launchdStop(host) {
|
|
1808
|
-
const res = host.run("launchctl", ["bootout", target(host)]);
|
|
1809
|
-
if (res.ok || notLoaded(res.stderr + res.stdout)) return { ok: true };
|
|
1810
|
-
return { ok: false, detail: firstLine(res.stderr || res.stdout) };
|
|
1811
|
-
}
|
|
1812
|
-
function launchdProbe(host) {
|
|
1813
|
-
const res = host.run("launchctl", ["print", target(host)]);
|
|
1814
|
-
if (res.ok) {
|
|
1815
|
-
const state = /state\s*=\s*(\w+)/.exec(res.stdout)?.[1];
|
|
1816
|
-
return { ownership: "held", running: state ? state === "running" : "unknown" };
|
|
1817
|
-
}
|
|
1818
|
-
return notLoaded(res.stderr + res.stdout) ? { ownership: "clear", running: false } : { ownership: "unknown", running: "unknown" };
|
|
1819
|
-
}
|
|
1820
|
-
function launchdRemove(host, plistPath) {
|
|
1821
|
-
const stopped = launchdStop(host);
|
|
1822
|
-
if (!stopped.ok) return stopped;
|
|
1823
|
-
host.fs.remove(plistPath);
|
|
1824
|
-
return { ok: true };
|
|
1825
|
-
}
|
|
1826
|
-
var NOT_LOADED = new RegExp(
|
|
1827
|
-
`no such process|(could not find|not find service)[^\\n]*${LAUNCHD_LABEL.replace(
|
|
1828
|
-
/[.*+?^${}()|[\]\\]/g,
|
|
1829
|
-
"\\$&"
|
|
1830
|
-
)}`,
|
|
1831
|
-
"i"
|
|
1832
|
-
);
|
|
1833
|
-
function notLoaded(output) {
|
|
1834
|
-
return NOT_LOADED.test(output);
|
|
1835
|
-
}
|
|
1836
|
-
function firstLine(s) {
|
|
1837
|
-
return s.trim().split("\n")[0] ?? "";
|
|
1838
|
-
}
|
|
1839
|
-
function xml(value) {
|
|
1840
|
-
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
1841
|
-
}
|
|
1842
|
-
|
|
1843
|
-
// src/service/systemd.ts
|
|
1844
|
-
import { dirname as dirname4, join as join7 } from "path";
|
|
1845
|
-
var SYSTEMD_UNIT = "cabane-companion.service";
|
|
1846
|
-
function systemdUnitPath(host) {
|
|
1847
|
-
const configHome = host.env.XDG_CONFIG_HOME?.trim() ? host.env.XDG_CONFIG_HOME.trim() : join7(host.home, ".config");
|
|
1848
|
-
return join7(configHome, "systemd", "user", SYSTEMD_UNIT);
|
|
1849
|
-
}
|
|
1850
|
-
function renderUnit(input) {
|
|
1851
|
-
const [exec, ...rest] = input.programArguments;
|
|
1852
|
-
const execStart = [quote(exec ?? ""), ...rest.map(quote)].join(" ");
|
|
1853
|
-
const env = Object.entries(input.environment).map(([k, v]) => `Environment=${k}=${v}`);
|
|
1854
|
-
return [
|
|
1855
|
-
"[Unit]",
|
|
1856
|
-
"Description=Cabane Companion \u2014 keeps this device answering while you are logged in",
|
|
1857
|
-
"After=network-online.target",
|
|
1858
|
-
"Wants=network-online.target",
|
|
1859
|
-
"",
|
|
1860
|
-
"[Service]",
|
|
1861
|
-
"Type=simple",
|
|
1862
|
-
...env,
|
|
1863
|
-
`ExecStart=${execStart}`,
|
|
1864
|
-
"Restart=on-failure",
|
|
1865
|
-
"RestartSec=5",
|
|
1866
|
-
"",
|
|
1867
|
-
"[Install]",
|
|
1868
|
-
"WantedBy=default.target"
|
|
1869
|
-
].join("\n") + "\n";
|
|
1870
|
-
}
|
|
1871
|
-
function systemdReload(host) {
|
|
1872
|
-
host.run("systemctl", ["--user", "daemon-reload"]);
|
|
1873
|
-
}
|
|
1874
|
-
function systemdStart(host) {
|
|
1875
|
-
systemdReload(host);
|
|
1876
|
-
const enabled = host.run("systemctl", ["--user", "enable", SYSTEMD_UNIT]);
|
|
1877
|
-
if (!enabled.ok) return { ok: false, detail: firstLine2(enabled.stderr || enabled.stdout) };
|
|
1878
|
-
const started = host.run("systemctl", ["--user", "restart", SYSTEMD_UNIT]);
|
|
1879
|
-
if (!started.ok) return { ok: false, detail: firstLine2(started.stderr || started.stdout) };
|
|
1880
|
-
return { ok: true };
|
|
1881
|
-
}
|
|
1882
|
-
function systemdStop(host) {
|
|
1883
|
-
const res = host.run("systemctl", ["--user", "stop", SYSTEMD_UNIT]);
|
|
1884
|
-
if (res.ok || notLoaded2(res.stderr + res.stdout)) return { ok: true };
|
|
1885
|
-
return { ok: false, detail: firstLine2(res.stderr || res.stdout) };
|
|
1886
|
-
}
|
|
1887
|
-
function systemdProbe(host) {
|
|
1888
|
-
const state = host.run("systemctl", ["--user", "is-active", SYSTEMD_UNIT]).stdout.trim();
|
|
1889
|
-
if (state === "active") return { ownership: "held", running: true };
|
|
1890
|
-
if (state === "activating" || state === "reloading" || state === "deactivating") {
|
|
1891
|
-
return { ownership: "held", running: "transitional" };
|
|
1892
|
-
}
|
|
1893
|
-
if (state === "inactive" || state === "failed") return { ownership: "clear", running: false };
|
|
1894
|
-
return { ownership: "unknown", running: "unknown" };
|
|
1895
|
-
}
|
|
1896
|
-
function systemdWantsLinkPath(host) {
|
|
1897
|
-
return join7(dirname4(systemdUnitPath(host)), "default.target.wants", SYSTEMD_UNIT);
|
|
1898
|
-
}
|
|
1899
|
-
function systemdInstalled(host) {
|
|
1900
|
-
return host.fs.exists(systemdUnitPath(host)) || host.fs.exists(systemdWantsLinkPath(host));
|
|
1901
|
-
}
|
|
1902
|
-
function systemdRemove(host, unitPath) {
|
|
1903
|
-
const stopped = host.run("systemctl", ["--user", "disable", "--now", SYSTEMD_UNIT]);
|
|
1904
|
-
if (!(stopped.ok || notLoaded2(stopped.stderr + stopped.stdout))) {
|
|
1905
|
-
return { ok: false, detail: firstLine2(stopped.stderr || stopped.stdout) };
|
|
1906
|
-
}
|
|
1907
|
-
host.fs.remove(unitPath);
|
|
1908
|
-
const link = systemdWantsLinkPath(host);
|
|
1909
|
-
if (host.fs.exists(link)) host.fs.remove(link);
|
|
1910
|
-
systemdReload(host);
|
|
1911
|
-
return { ok: true };
|
|
1912
|
-
}
|
|
1913
|
-
var NOT_LOADED2 = new RegExp(
|
|
1914
|
-
`unit (file )?${SYSTEMD_UNIT.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")} (not loaded|does not exist|not found|could not be found)`,
|
|
1915
|
-
"i"
|
|
1916
|
-
);
|
|
1917
|
-
function notLoaded2(output) {
|
|
1918
|
-
return NOT_LOADED2.test(output);
|
|
1919
|
-
}
|
|
1920
|
-
function readLinger(host) {
|
|
1921
|
-
const res = host.run("loginctl", ["show-user", String(host.uid), "-p", "Linger"]);
|
|
1922
|
-
if (!res.ok) return "unknown";
|
|
1923
|
-
const match = /Linger=(\w+)/.exec(res.stdout);
|
|
1924
|
-
if (!match) return "unknown";
|
|
1925
|
-
return match[1] === "yes" ? "yes" : "no";
|
|
1926
|
-
}
|
|
1927
|
-
function tryEnableLinger(host) {
|
|
1928
|
-
host.run("loginctl", ["enable-linger", String(host.uid)]);
|
|
1929
|
-
return readLinger(host);
|
|
1930
|
-
}
|
|
1931
|
-
function isRemoteSession(env) {
|
|
1932
|
-
return Boolean(env.SSH_CONNECTION || env.SSH_TTY || env.SSH_CLIENT);
|
|
1933
|
-
}
|
|
1934
|
-
function quote(value) {
|
|
1935
|
-
return `"${value.replace(/(["\\])/g, "\\$1")}"`;
|
|
1936
|
-
}
|
|
1937
|
-
function firstLine2(s) {
|
|
1938
|
-
return s.trim().split("\n")[0] ?? "";
|
|
1939
|
-
}
|
|
1940
|
-
|
|
1941
|
-
// src/service/index.ts
|
|
1942
|
-
function detectServiceManager(host = defaultServiceHost()) {
|
|
1943
|
-
if (host.platform === "darwin") return "launchd";
|
|
1944
|
-
if (host.platform !== "linux") return "none";
|
|
1945
|
-
return host.run("systemctl", ["--user", "show-environment"]).ok ? "systemd-user" : "none";
|
|
1946
|
-
}
|
|
1947
|
-
function programArguments(host, _opts) {
|
|
1948
|
-
return [host.execPath, host.cliPath, "start", "--foreground"];
|
|
1949
|
-
}
|
|
1950
|
-
function environment(host) {
|
|
1951
|
-
return {
|
|
1952
|
-
// PATH CAPTURE. launchd's default PATH is `/usr/bin:/bin:/usr/sbin:/sbin`
|
|
1953
|
-
// and a systemd user manager's is barely better — neither has `claude`,
|
|
1954
|
-
// `codex` or `opencode` on it. A companion that runs but can't find its
|
|
1955
|
-
// harness is worse than one that isn't running, because the device reads
|
|
1956
|
-
// Online. So we bake in the PATH of the shell that ran `start`, and because
|
|
1957
|
-
// it's part of the rendered content, a PATH that later drifts re-renders on
|
|
1958
|
-
// the next `start` like any other change.
|
|
1959
|
-
PATH: host.env.PATH ?? "",
|
|
1960
|
-
CABANE_COMPANION_DAEMON: "1"
|
|
1961
|
-
};
|
|
1962
|
-
}
|
|
1963
|
-
function unitPathFor(host, manager) {
|
|
1964
|
-
if (manager === "launchd") return launchAgentPath(host.home);
|
|
1965
|
-
if (manager === "systemd-user") return systemdUnitPath(host);
|
|
1966
|
-
return null;
|
|
1967
|
-
}
|
|
1968
|
-
function renderFor(host, manager, opts) {
|
|
1969
|
-
const input = { programArguments: programArguments(host, opts), environment: environment(host) };
|
|
1970
|
-
return manager === "launchd" ? renderPlist({ ...input, logPath: host.logPath }) : renderUnit(input);
|
|
1971
|
-
}
|
|
1972
|
-
function installService(opts = {}, host = defaultServiceHost()) {
|
|
1973
|
-
const manager = detectServiceManager(host);
|
|
1974
|
-
if (manager === "none") return { installed: false, reason: "unsupported" };
|
|
1975
|
-
const path = unitPathFor(host, manager);
|
|
1976
|
-
if (!path) return { installed: false, reason: "unsupported" };
|
|
1977
|
-
const desired = renderFor(host, manager, opts);
|
|
1978
|
-
const existing = host.fs.read(path);
|
|
1979
|
-
const hadUnit = existing !== null;
|
|
1980
|
-
const changed = existing !== desired;
|
|
1981
|
-
if (changed) {
|
|
1982
|
-
host.fs.mkdirp(dirname5(path));
|
|
1983
|
-
host.fs.write(path, desired);
|
|
1984
|
-
}
|
|
1985
|
-
if (manager === "systemd-user") {
|
|
1986
|
-
const linger = ensureLinger(host);
|
|
1987
|
-
if (linger !== "yes" && isRemoteSession(host.env)) {
|
|
1988
|
-
systemdRemove(host, path);
|
|
1989
|
-
return failedInstall(
|
|
1990
|
-
host,
|
|
1991
|
-
manager,
|
|
1992
|
-
"linger-unavailable",
|
|
1993
|
-
"a systemd --user service would stop when this SSH session ends (lingering is off and could not be enabled)"
|
|
1994
|
-
);
|
|
1995
|
-
}
|
|
1996
|
-
}
|
|
1997
|
-
const started = manager === "launchd" ? launchdStart(host, path) : systemdStart(host);
|
|
1998
|
-
if (!started.ok) {
|
|
1999
|
-
if (!hadUnit) removeService(host, manager, path);
|
|
2000
|
-
return failedInstall(host, manager, "command-failed", started.detail);
|
|
2001
|
-
}
|
|
2002
|
-
return { installed: true, manager, changed };
|
|
2003
|
-
}
|
|
2004
|
-
function failedInstall(host, manager, reason, detail) {
|
|
2005
|
-
return {
|
|
2006
|
-
installed: false,
|
|
2007
|
-
reason,
|
|
2008
|
-
...detail ? { detail } : {},
|
|
2009
|
-
...managerOwnership(host, manager) === "clear" ? {} : { leftBehind: manager }
|
|
2010
|
-
};
|
|
2011
|
-
}
|
|
2012
|
-
function managerProbe(host, manager) {
|
|
2013
|
-
if (manager === "launchd") return launchdProbe(host);
|
|
2014
|
-
if (manager === "systemd-user") return systemdProbe(host);
|
|
2015
|
-
return { ownership: "clear", running: false };
|
|
2016
|
-
}
|
|
2017
|
-
function managerOwnership(host, manager) {
|
|
2018
|
-
return managerProbe(host, manager).ownership;
|
|
2019
|
-
}
|
|
2020
|
-
function refreshInstalledService(opts = {}, host = defaultServiceHost()) {
|
|
2021
|
-
const manager = detectServiceManager(host);
|
|
2022
|
-
const path = unitPathFor(host, manager);
|
|
2023
|
-
if (!path || !host.fs.exists(path)) return false;
|
|
2024
|
-
const desired = renderFor(host, manager, opts);
|
|
2025
|
-
if (host.fs.read(path) === desired) return false;
|
|
2026
|
-
host.fs.write(path, desired);
|
|
2027
|
-
if (manager === "systemd-user") systemdReload(host);
|
|
2028
|
-
return true;
|
|
2029
|
-
}
|
|
2030
|
-
function stopService(host = defaultServiceHost()) {
|
|
2031
|
-
const manager = detectServiceManager(host);
|
|
2032
|
-
const path = unitPathFor(host, manager);
|
|
2033
|
-
if (manager === "none" || !path || !managerHasClaim(host, manager, path)) {
|
|
2034
|
-
return { handled: false, ok: true };
|
|
2035
|
-
}
|
|
2036
|
-
const res = manager === "launchd" ? launchdStop(host) : systemdStop(host);
|
|
2037
|
-
return {
|
|
2038
|
-
handled: true,
|
|
2039
|
-
ok: res.ok,
|
|
2040
|
-
manager,
|
|
2041
|
-
...res.detail ? { detail: res.detail } : {}
|
|
2042
|
-
};
|
|
2043
|
-
}
|
|
2044
|
-
function disableService(host = defaultServiceHost()) {
|
|
2045
|
-
const manager = detectServiceManager(host);
|
|
2046
|
-
const path = unitPathFor(host, manager);
|
|
2047
|
-
if (manager === "none" || !path) return { handled: false, ok: true };
|
|
2048
|
-
if (!managerHasClaim(host, manager, path)) return { handled: false, ok: true, manager };
|
|
2049
|
-
const res = removeService(host, manager, path);
|
|
2050
|
-
return { handled: true, ok: res.ok, manager, ...res.detail ? { detail: res.detail } : {} };
|
|
2051
|
-
}
|
|
2052
|
-
function managerHasClaim(host, manager, path) {
|
|
2053
|
-
if (manager === "none") return false;
|
|
2054
|
-
if (host.fs.exists(path)) return true;
|
|
2055
|
-
if (manager === "systemd-user" && systemdInstalled(host)) return true;
|
|
2056
|
-
return managerOwnership(host, manager) !== "clear";
|
|
2057
|
-
}
|
|
2058
|
-
function removeService(host, manager, path) {
|
|
2059
|
-
return manager === "launchd" ? launchdRemove(host, path) : systemdRemove(host, path);
|
|
2060
|
-
}
|
|
2061
|
-
function serviceStatus(host = defaultServiceHost()) {
|
|
2062
|
-
const manager = detectServiceManager(host);
|
|
2063
|
-
const path = unitPathFor(host, manager);
|
|
2064
|
-
const probe = managerProbe(host, manager);
|
|
2065
|
-
const installed = manager === "none" || !path ? false : host.fs.exists(path) || probe.ownership === "held" || manager === "systemd-user" && systemdInstalled(host);
|
|
2066
|
-
const running = manager === "none" ? false : probe.running;
|
|
2067
|
-
return {
|
|
2068
|
-
manager,
|
|
2069
|
-
unitPath: path,
|
|
2070
|
-
installed,
|
|
2071
|
-
running,
|
|
2072
|
-
linger: manager === "systemd-user" ? readLinger(host) : null,
|
|
2073
|
-
remoteSession: isRemoteSession(host.env)
|
|
2074
|
-
};
|
|
2075
|
-
}
|
|
2076
|
-
function ensureLinger(host) {
|
|
2077
|
-
const current = readLinger(host);
|
|
2078
|
-
if (current === "yes") return current;
|
|
2079
|
-
return tryEnableLinger(host);
|
|
2080
|
-
}
|
|
2081
|
-
|
|
2082
|
-
// src/commands/service.ts
|
|
2083
|
-
function serviceStatusCommand(deps = {}) {
|
|
2084
|
-
const status2 = (deps.status ?? serviceStatus)();
|
|
2085
|
-
if (status2.manager === "none") {
|
|
2086
|
-
process.stdout.write(
|
|
2087
|
-
"service: not supported on this machine yet \u2014 `cabane-companion start` runs the companion detached instead (it stops at the next reboot).\n"
|
|
2088
|
-
);
|
|
2089
|
-
return;
|
|
2090
|
-
}
|
|
2091
|
-
process.stdout.write(`manager: ${status2.manager}
|
|
2092
|
-
`);
|
|
2093
|
-
process.stdout.write(
|
|
2094
|
-
`unit: ${status2.unitPath}${status2.installed ? "" : " (not written)"}
|
|
2095
|
-
`
|
|
2096
|
-
);
|
|
2097
|
-
process.stdout.write(`installed: ${status2.installed ? "yes" : "no"}
|
|
2098
|
-
`);
|
|
2099
|
-
process.stdout.write(`running: ${runningLine(status2.running)}
|
|
2100
|
-
`);
|
|
2101
|
-
if (status2.manager === "systemd-user") {
|
|
2102
|
-
process.stdout.write(`linger: ${status2.linger}${lingerNote(status2)}
|
|
2103
|
-
`);
|
|
2104
|
-
}
|
|
2105
|
-
if (status2.installed) {
|
|
2106
|
-
process.stdout.write("disable: cabane-companion service disable\n");
|
|
2107
|
-
} else {
|
|
2108
|
-
process.stdout.write("install: cabane-companion start (installs it for you)\n");
|
|
2109
|
-
}
|
|
2110
|
-
}
|
|
2111
|
-
function serviceDisableCommand(deps = {}) {
|
|
2112
|
-
const disable = deps.disable ?? disableService;
|
|
2113
|
-
const res = disable();
|
|
2114
|
-
if (!res.handled) {
|
|
2115
|
-
process.stdout.write("service: nothing installed (nothing to remove).\n");
|
|
2116
|
-
return;
|
|
2117
|
-
}
|
|
2118
|
-
if (!res.ok) {
|
|
2119
|
-
process.stdout.write(
|
|
2120
|
-
`service: nothing was removed \u2014 ${res.detail ?? "the service manager reported an error"}.
|
|
2121
|
-
The unit is still installed and may still be running; \`cabane-companion service status\` shows where it stands.
|
|
2122
|
-
`
|
|
2123
|
-
);
|
|
2124
|
-
process.exitCode = 1;
|
|
2125
|
-
return;
|
|
2126
|
-
}
|
|
2127
|
-
process.stdout.write(
|
|
2128
|
-
"service: stopped and removed. The companion no longer starts at login; `cabane-companion start` sets it up again.\n"
|
|
2129
|
-
);
|
|
2130
|
-
}
|
|
2131
|
-
function runningLine(running) {
|
|
2132
|
-
if (running === "transitional") {
|
|
2133
|
-
return "starting or stopping \u2014 the manager reported a state in transition; ask again in a moment";
|
|
1725
|
+
});
|
|
2134
1726
|
}
|
|
2135
|
-
|
|
2136
|
-
|
|
1727
|
+
streams.push({
|
|
1728
|
+
level: "debug",
|
|
1729
|
+
stream: destinations.file ?? createWriteStream(path, { flags: "a" })
|
|
1730
|
+
});
|
|
1731
|
+
return pino({ level: "debug" }, pino.multistream(streams));
|
|
2137
1732
|
}
|
|
2138
|
-
function
|
|
2139
|
-
if (
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
}
|
|
2143
|
-
return " \u2014 the service runs while you are logged in";
|
|
1733
|
+
function getLogger() {
|
|
1734
|
+
if (cached) return cached;
|
|
1735
|
+
cached = createLogger();
|
|
1736
|
+
return cached;
|
|
2144
1737
|
}
|
|
2145
1738
|
|
|
2146
|
-
// src/commands/start.ts
|
|
2147
|
-
import { hostname as hostname2 } from "os";
|
|
2148
|
-
|
|
2149
1739
|
// src/runtime.ts
|
|
2150
1740
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
2151
1741
|
|
|
2152
1742
|
// src/dashboard/server.ts
|
|
2153
|
-
import { dirname as
|
|
2154
|
-
import { fileURLToPath
|
|
1743
|
+
import { dirname as dirname4, join as join7 } from "path";
|
|
1744
|
+
import { fileURLToPath } from "url";
|
|
2155
1745
|
import { serve } from "@hono/node-server";
|
|
2156
1746
|
import { Hono } from "hono";
|
|
2157
1747
|
|
|
2158
1748
|
// src/dashboard/routes.ts
|
|
2159
|
-
import { openSync as openSync2, readSync, closeSync as closeSync2, fstatSync, existsSync as
|
|
1749
|
+
import { openSync as openSync2, readSync, closeSync as closeSync2, fstatSync, existsSync as existsSync5 } from "fs";
|
|
2160
1750
|
import { readFile } from "fs/promises";
|
|
2161
|
-
import { extname, join as
|
|
1751
|
+
import { extname, join as join6, normalize } from "path";
|
|
2162
1752
|
import { streamSSE } from "hono/streaming";
|
|
2163
1753
|
|
|
2164
1754
|
// src/state.ts
|
|
@@ -2407,14 +1997,14 @@ var CONTENT_TYPES = {
|
|
|
2407
1997
|
function registerRoutes(app, deps) {
|
|
2408
1998
|
const { supervisor, hub, staticDir } = deps;
|
|
2409
1999
|
app.get("/", async (c) => {
|
|
2410
|
-
const html = await readFile(
|
|
2000
|
+
const html = await readFile(join6(staticDir, "index.html"), "utf8");
|
|
2411
2001
|
return c.html(html);
|
|
2412
2002
|
});
|
|
2413
2003
|
app.get("/static/:file", async (c) => {
|
|
2414
2004
|
const file = c.req.param("file");
|
|
2415
2005
|
const safe3 = normalize(file).replace(/^(\.\.[/\\])+/, "");
|
|
2416
|
-
const full =
|
|
2417
|
-
if (!full.startsWith(staticDir) || !
|
|
2006
|
+
const full = join6(staticDir, safe3);
|
|
2007
|
+
if (!full.startsWith(staticDir) || !existsSync5(full)) return c.notFound();
|
|
2418
2008
|
const body = await readFile(full);
|
|
2419
2009
|
const type = CONTENT_TYPES[extname(full).toLowerCase()] ?? "application/octet-stream";
|
|
2420
2010
|
c.header("content-type", type);
|
|
@@ -2541,7 +2131,7 @@ function clampLimit(raw, fallback, max = 200) {
|
|
|
2541
2131
|
return Math.min(Math.floor(n), max);
|
|
2542
2132
|
}
|
|
2543
2133
|
function tailFile(path, lines) {
|
|
2544
|
-
if (!
|
|
2134
|
+
if (!existsSync5(path)) return [];
|
|
2545
2135
|
const MAX_BYTES = 256 * 1024;
|
|
2546
2136
|
let fd;
|
|
2547
2137
|
try {
|
|
@@ -2630,7 +2220,7 @@ function isAddrInUse(err) {
|
|
|
2630
2220
|
return Boolean(err && typeof err === "object" && "code" in err && err.code === "EADDRINUSE");
|
|
2631
2221
|
}
|
|
2632
2222
|
function resolveStaticDir() {
|
|
2633
|
-
return
|
|
2223
|
+
return join7(dirname4(fileURLToPath(import.meta.url)), "static");
|
|
2634
2224
|
}
|
|
2635
2225
|
|
|
2636
2226
|
// src/api.ts
|
|
@@ -3106,21 +2696,21 @@ function errorMessage2(status2, body) {
|
|
|
3106
2696
|
}
|
|
3107
2697
|
|
|
3108
2698
|
// src/cursor.ts
|
|
3109
|
-
import { mkdirSync as
|
|
3110
|
-
import { join as
|
|
2699
|
+
import { mkdirSync as mkdirSync6, readFileSync as readFileSync4, writeFileSync as writeFileSync4, existsSync as existsSync6 } from "fs";
|
|
2700
|
+
import { join as join8 } from "path";
|
|
3111
2701
|
function pathFor(workspaceId) {
|
|
3112
|
-
return
|
|
2702
|
+
return join8(cabaneDir(), "cursors", encodeURIComponent(workspaceId));
|
|
3113
2703
|
}
|
|
3114
2704
|
function readCursor(workspaceId) {
|
|
3115
2705
|
const path = pathFor(workspaceId);
|
|
3116
|
-
if (!
|
|
3117
|
-
const raw =
|
|
2706
|
+
if (!existsSync6(path)) return null;
|
|
2707
|
+
const raw = readFileSync4(path, "utf8").trim();
|
|
3118
2708
|
return raw.length > 0 ? raw : null;
|
|
3119
2709
|
}
|
|
3120
2710
|
function writeCursor(workspaceId, eventId) {
|
|
3121
2711
|
const path = pathFor(workspaceId);
|
|
3122
|
-
|
|
3123
|
-
|
|
2712
|
+
mkdirSync6(join8(cabaneDir(), "cursors"), { recursive: true });
|
|
2713
|
+
writeFileSync4(path, eventId + "\n", "utf8");
|
|
3124
2714
|
}
|
|
3125
2715
|
|
|
3126
2716
|
// src/cursor-tracker.ts
|
|
@@ -3163,20 +2753,20 @@ var CursorTracker = class {
|
|
|
3163
2753
|
};
|
|
3164
2754
|
|
|
3165
2755
|
// src/dispatch-dedupe.ts
|
|
3166
|
-
import { mkdirSync as
|
|
3167
|
-
import { join as
|
|
2756
|
+
import { mkdirSync as mkdirSync7, readFileSync as readFileSync5, writeFileSync as writeFileSync5, existsSync as existsSync7 } from "fs";
|
|
2757
|
+
import { join as join9 } from "path";
|
|
3168
2758
|
var MAX_IDS = 256;
|
|
3169
2759
|
function dir(log) {
|
|
3170
|
-
return
|
|
2760
|
+
return join9(cabaneDir(), log);
|
|
3171
2761
|
}
|
|
3172
2762
|
function pathFor2(log, workspaceId) {
|
|
3173
|
-
return
|
|
2763
|
+
return join9(dir(log), encodeURIComponent(workspaceId));
|
|
3174
2764
|
}
|
|
3175
2765
|
function readIds(log, workspaceId) {
|
|
3176
2766
|
const path = pathFor2(log, workspaceId);
|
|
3177
|
-
if (!
|
|
2767
|
+
if (!existsSync7(path)) return [];
|
|
3178
2768
|
try {
|
|
3179
|
-
return
|
|
2769
|
+
return readFileSync5(path, "utf8").split("\n").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
3180
2770
|
} catch {
|
|
3181
2771
|
return [];
|
|
3182
2772
|
}
|
|
@@ -3189,8 +2779,8 @@ function mark(log, workspaceId, eventId) {
|
|
|
3189
2779
|
if (ids.includes(eventId)) return;
|
|
3190
2780
|
ids.push(eventId);
|
|
3191
2781
|
const trimmed = ids.length > MAX_IDS ? ids.slice(-MAX_IDS) : ids;
|
|
3192
|
-
|
|
3193
|
-
|
|
2782
|
+
mkdirSync7(dir(log), { recursive: true });
|
|
2783
|
+
writeFileSync5(pathFor2(log, workspaceId), trimmed.join("\n") + "\n", "utf8");
|
|
3194
2784
|
}
|
|
3195
2785
|
function hasDispatched(workspaceId, eventId) {
|
|
3196
2786
|
return has("dispatched", workspaceId, eventId);
|
|
@@ -3206,17 +2796,17 @@ function markCompleted(workspaceId, eventId) {
|
|
|
3206
2796
|
}
|
|
3207
2797
|
var MAX_RESUME_ATTEMPTS = 3;
|
|
3208
2798
|
function resumeDir() {
|
|
3209
|
-
return
|
|
2799
|
+
return join9(cabaneDir(), "resume-attempts");
|
|
3210
2800
|
}
|
|
3211
2801
|
function resumePathFor(workspaceId) {
|
|
3212
|
-
return
|
|
2802
|
+
return join9(resumeDir(), encodeURIComponent(workspaceId));
|
|
3213
2803
|
}
|
|
3214
2804
|
function readResumeCounts(workspaceId) {
|
|
3215
2805
|
const out = /* @__PURE__ */ new Map();
|
|
3216
2806
|
const path = resumePathFor(workspaceId);
|
|
3217
|
-
if (!
|
|
2807
|
+
if (!existsSync7(path)) return out;
|
|
3218
2808
|
try {
|
|
3219
|
-
for (const line of
|
|
2809
|
+
for (const line of readFileSync5(path, "utf8").split("\n")) {
|
|
3220
2810
|
const trimmed = line.trim();
|
|
3221
2811
|
if (!trimmed) continue;
|
|
3222
2812
|
const tab = trimmed.lastIndexOf(" ");
|
|
@@ -3236,8 +2826,8 @@ function bumpResumeAttempt(workspaceId, eventId) {
|
|
|
3236
2826
|
counts.set(eventId, next);
|
|
3237
2827
|
const entries = [...counts.entries()];
|
|
3238
2828
|
const trimmed = entries.length > MAX_IDS ? entries.slice(-MAX_IDS) : entries;
|
|
3239
|
-
|
|
3240
|
-
|
|
2829
|
+
mkdirSync7(resumeDir(), { recursive: true });
|
|
2830
|
+
writeFileSync5(
|
|
3241
2831
|
resumePathFor(workspaceId),
|
|
3242
2832
|
trimmed.map(([id, c]) => `${id} ${c}`).join("\n") + "\n",
|
|
3243
2833
|
"utf8"
|
|
@@ -6935,8 +6525,8 @@ var ConnectorHealthStore = class {
|
|
|
6935
6525
|
|
|
6936
6526
|
// src/dispatcher.ts
|
|
6937
6527
|
import { randomUUID } from "crypto";
|
|
6938
|
-
import { appendFileSync as appendFileSync2, existsSync as
|
|
6939
|
-
import { join as
|
|
6528
|
+
import { appendFileSync as appendFileSync2, existsSync as existsSync10, mkdirSync as mkdirSync10, readdirSync as readdirSync2, statSync } from "fs";
|
|
6529
|
+
import { join as join14 } from "path";
|
|
6940
6530
|
|
|
6941
6531
|
// src/summon.ts
|
|
6942
6532
|
import { z as z12 } from "zod";
|
|
@@ -7269,11 +6859,11 @@ function trimSlash2(s) {
|
|
|
7269
6859
|
// src/codex-instructions.ts
|
|
7270
6860
|
import { mkdtemp, rm, writeFile } from "fs/promises";
|
|
7271
6861
|
import { tmpdir } from "os";
|
|
7272
|
-
import { join as
|
|
6862
|
+
import { join as join10 } from "path";
|
|
7273
6863
|
var PREFIX = "cabane-codex-instructions-";
|
|
7274
6864
|
async function writeCodexInstructionsFile(contents) {
|
|
7275
|
-
const dir2 = await mkdtemp(
|
|
7276
|
-
const path =
|
|
6865
|
+
const dir2 = await mkdtemp(join10(tmpdir(), PREFIX));
|
|
6866
|
+
const path = join10(dir2, "instructions.md");
|
|
7277
6867
|
await writeFile(path, contents, { encoding: "utf8", mode: 384 });
|
|
7278
6868
|
return {
|
|
7279
6869
|
path,
|
|
@@ -7284,22 +6874,22 @@ async function writeCodexInstructionsFile(contents) {
|
|
|
7284
6874
|
}
|
|
7285
6875
|
|
|
7286
6876
|
// src/prepared.ts
|
|
7287
|
-
import { mkdirSync as
|
|
7288
|
-
import { join as
|
|
6877
|
+
import { mkdirSync as mkdirSync8, readFileSync as readFileSync6, rmSync as rmSync5, writeFileSync as writeFileSync6, existsSync as existsSync8 } from "fs";
|
|
6878
|
+
import { join as join11 } from "path";
|
|
7289
6879
|
function dirFor(workspaceId) {
|
|
7290
|
-
return
|
|
6880
|
+
return join11(cabaneDir(), "prepared", encodeURIComponent(workspaceId));
|
|
7291
6881
|
}
|
|
7292
6882
|
function conversationDir(workspaceId, conversationId) {
|
|
7293
|
-
return
|
|
6883
|
+
return join11(dirFor(workspaceId), encodeURIComponent(conversationId));
|
|
7294
6884
|
}
|
|
7295
6885
|
function pathFor3(workspaceId, conversationId, agentId) {
|
|
7296
|
-
return
|
|
6886
|
+
return join11(conversationDir(workspaceId, conversationId), `${encodeURIComponent(agentId)}.json`);
|
|
7297
6887
|
}
|
|
7298
6888
|
function readPrepared(workspaceId, conversationId, agentId) {
|
|
7299
6889
|
const path = pathFor3(workspaceId, conversationId, agentId);
|
|
7300
|
-
if (!
|
|
6890
|
+
if (!existsSync8(path)) return null;
|
|
7301
6891
|
try {
|
|
7302
|
-
const parsed = JSON.parse(
|
|
6892
|
+
const parsed = JSON.parse(readFileSync6(path, "utf8"));
|
|
7303
6893
|
if (parsed && typeof parsed.cwd === "string" && parsed.cwd.length > 0) {
|
|
7304
6894
|
return {
|
|
7305
6895
|
cwd: parsed.cwd,
|
|
@@ -7312,32 +6902,32 @@ function readPrepared(workspaceId, conversationId, agentId) {
|
|
|
7312
6902
|
}
|
|
7313
6903
|
}
|
|
7314
6904
|
function writePrepared(workspaceId, conversationId, agentId, result) {
|
|
7315
|
-
|
|
7316
|
-
|
|
6905
|
+
mkdirSync8(conversationDir(workspaceId, conversationId), { recursive: true });
|
|
6906
|
+
writeFileSync6(
|
|
7317
6907
|
pathFor3(workspaceId, conversationId, agentId),
|
|
7318
6908
|
JSON.stringify(result) + "\n",
|
|
7319
6909
|
"utf8"
|
|
7320
6910
|
);
|
|
7321
6911
|
}
|
|
7322
6912
|
function clearPrepared(workspaceId, conversationId, agentId) {
|
|
7323
|
-
|
|
6913
|
+
rmSync5(pathFor3(workspaceId, conversationId, agentId), { force: true });
|
|
7324
6914
|
}
|
|
7325
6915
|
|
|
7326
6916
|
// src/secrets.ts
|
|
7327
|
-
import { existsSync as
|
|
7328
|
-
import { join as
|
|
6917
|
+
import { existsSync as existsSync9, readFileSync as readFileSync7 } from "fs";
|
|
6918
|
+
import { join as join12 } from "path";
|
|
7329
6919
|
import { z as z13 } from "zod";
|
|
7330
6920
|
var PLACEHOLDER_RE = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
|
|
7331
6921
|
function secretsPath() {
|
|
7332
|
-
return
|
|
6922
|
+
return join12(cabaneDir(), "secrets.json");
|
|
7333
6923
|
}
|
|
7334
6924
|
var secretStoreSchema = z13.record(z13.string(), z13.string());
|
|
7335
6925
|
function loadSecretStore() {
|
|
7336
6926
|
const path = secretsPath();
|
|
7337
|
-
if (!
|
|
6927
|
+
if (!existsSync9(path)) return makeStore({});
|
|
7338
6928
|
let raw;
|
|
7339
6929
|
try {
|
|
7340
|
-
raw =
|
|
6930
|
+
raw = readFileSync7(path, "utf8");
|
|
7341
6931
|
} catch (err) {
|
|
7342
6932
|
throw new ConfigError(
|
|
7343
6933
|
`couldn't read ${path}: ${err instanceof Error ? err.message : String(err)}`
|
|
@@ -7415,10 +7005,10 @@ function resolveMcpSecrets(mcpServers, store) {
|
|
|
7415
7005
|
}
|
|
7416
7006
|
|
|
7417
7007
|
// src/transcript-writer.ts
|
|
7418
|
-
import { appendFileSync, chmodSync as chmodSync3, mkdirSync as
|
|
7419
|
-
import { join as
|
|
7008
|
+
import { appendFileSync, chmodSync as chmodSync3, mkdirSync as mkdirSync9, readdirSync, rmSync as rmSync6 } from "fs";
|
|
7009
|
+
import { join as join13 } from "path";
|
|
7420
7010
|
function transcriptsDir() {
|
|
7421
|
-
return
|
|
7011
|
+
return join13(cabaneDir(), "transcripts");
|
|
7422
7012
|
}
|
|
7423
7013
|
var RETAIN = 200;
|
|
7424
7014
|
var TranscriptWriter = class {
|
|
@@ -7427,9 +7017,9 @@ var TranscriptWriter = class {
|
|
|
7427
7017
|
onWarn;
|
|
7428
7018
|
constructor(dir2, meta, onWarn) {
|
|
7429
7019
|
this.onWarn = onWarn;
|
|
7430
|
-
this.path =
|
|
7020
|
+
this.path = join13(dir2, fileName(meta));
|
|
7431
7021
|
try {
|
|
7432
|
-
|
|
7022
|
+
mkdirSync9(dir2, { recursive: true });
|
|
7433
7023
|
try {
|
|
7434
7024
|
chmodSync3(dir2, 448);
|
|
7435
7025
|
} catch {
|
|
@@ -7486,7 +7076,7 @@ function pruneOld(dir2, retain) {
|
|
|
7486
7076
|
const drop = files.sort().slice(0, files.length - retain);
|
|
7487
7077
|
for (const f of drop) {
|
|
7488
7078
|
try {
|
|
7489
|
-
|
|
7079
|
+
rmSync6(join13(dir2, f), { force: true });
|
|
7490
7080
|
} catch {
|
|
7491
7081
|
}
|
|
7492
7082
|
}
|
|
@@ -7609,9 +7199,9 @@ var TurnCommitter = class {
|
|
|
7609
7199
|
// commit. A self-target is stripped here (mirror of the in-app self-strip); the
|
|
7610
7200
|
// server strips it again and resolves / ignores an unknown id.
|
|
7611
7201
|
summonField() {
|
|
7612
|
-
const
|
|
7613
|
-
if (!
|
|
7614
|
-
return { dispatch:
|
|
7202
|
+
const target = this.deps.summonState.agentId;
|
|
7203
|
+
if (!target || target === this.deps.agentId) return {};
|
|
7204
|
+
return { dispatch: target };
|
|
7615
7205
|
}
|
|
7616
7206
|
// CT326: resolve the per-turn ask into the `ask` field for a `final` commit.
|
|
7617
7207
|
// The server validates the target (must be a workspace member/owner) and
|
|
@@ -7832,7 +7422,7 @@ var DEFAULT_AGENT_IDLE_TIMEOUT_MS = 10 * 6e4;
|
|
|
7832
7422
|
var DEFAULT_AGENT_TOTAL_TIMEOUT_MS = 6 * 60 * 6e4;
|
|
7833
7423
|
function checkoutState(cwd) {
|
|
7834
7424
|
if (!cwd) return { ok: false, reason: "no working directory was resolved for this turn" };
|
|
7835
|
-
if (!
|
|
7425
|
+
if (!existsSync10(cwd)) return { ok: false, reason: `the working directory is gone (${cwd})` };
|
|
7836
7426
|
let entries;
|
|
7837
7427
|
try {
|
|
7838
7428
|
entries = readdirSync2(cwd);
|
|
@@ -7842,15 +7432,15 @@ function checkoutState(cwd) {
|
|
|
7842
7432
|
if (entries.length === 0) {
|
|
7843
7433
|
return { ok: false, reason: `${cwd} is empty \u2014 a recreated shell, not a prepared directory` };
|
|
7844
7434
|
}
|
|
7845
|
-
const gitPath =
|
|
7846
|
-
if (!
|
|
7435
|
+
const gitPath = join14(cwd, ".git");
|
|
7436
|
+
if (!existsSync10(gitPath)) return { ok: true, reason: "usable" };
|
|
7847
7437
|
let stat;
|
|
7848
7438
|
try {
|
|
7849
7439
|
stat = statSync(gitPath);
|
|
7850
7440
|
} catch (error) {
|
|
7851
7441
|
return { ok: false, reason: `${gitPath} is unreadable (${error.message})` };
|
|
7852
7442
|
}
|
|
7853
|
-
if (stat.isDirectory() && !
|
|
7443
|
+
if (stat.isDirectory() && !existsSync10(join14(gitPath, "HEAD")))
|
|
7854
7444
|
return { ok: false, reason: `${gitPath} has no HEAD \u2014 an empty shell, not a checkout` };
|
|
7855
7445
|
return { ok: true, reason: "usable" };
|
|
7856
7446
|
}
|
|
@@ -8041,7 +7631,7 @@ var Dispatcher = class {
|
|
|
8041
7631
|
let seqCounter = 0;
|
|
8042
7632
|
const nextSeq = () => ++seqCounter;
|
|
8043
7633
|
let effectiveCwd = localCwd ?? cabaneCwd;
|
|
8044
|
-
if (effectiveCwd && !
|
|
7634
|
+
if (effectiveCwd && !existsSync10(effectiveCwd)) {
|
|
8045
7635
|
turnLog.warn(
|
|
8046
7636
|
{ cwd: effectiveCwd },
|
|
8047
7637
|
"dispatcher: configured working directory does not exist on this device \u2014 falling back to the process cwd"
|
|
@@ -8305,11 +7895,11 @@ ${reason}`,
|
|
|
8305
7895
|
const totalTimeoutMs = this.opts.totalTimeoutMs ?? DEFAULT_AGENT_TOTAL_TIMEOUT_MS;
|
|
8306
7896
|
const closeTurnReceipt = (ok, reason) => {
|
|
8307
7897
|
if (!turnReceiptPath) return;
|
|
8308
|
-
const
|
|
7898
|
+
const target = turnReceiptPath;
|
|
8309
7899
|
turnReceiptPath = null;
|
|
8310
7900
|
try {
|
|
8311
7901
|
appendFileSync2(
|
|
8312
|
-
|
|
7902
|
+
target,
|
|
8313
7903
|
`${JSON.stringify({
|
|
8314
7904
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8315
7905
|
event: "settled",
|
|
@@ -8349,7 +7939,7 @@ ${reason}`,
|
|
|
8349
7939
|
}
|
|
8350
7940
|
return this.concludeBeforeRun(payload, turnLog, startedAt, reason);
|
|
8351
7941
|
}
|
|
8352
|
-
const receiptPath =
|
|
7942
|
+
const receiptPath = join14(effectiveCwd, ".git", "cabane", "readiness.jsonl");
|
|
8353
7943
|
const receiptLine = (fields) => `${JSON.stringify({
|
|
8354
7944
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8355
7945
|
taskId: hookEnv.CABANE_TASK_ID,
|
|
@@ -8373,7 +7963,7 @@ ${reason}`,
|
|
|
8373
7963
|
})}
|
|
8374
7964
|
`;
|
|
8375
7965
|
try {
|
|
8376
|
-
|
|
7966
|
+
mkdirSync10(join14(effectiveCwd, ".git", "cabane"), { recursive: true });
|
|
8377
7967
|
appendFileSync2(
|
|
8378
7968
|
receiptPath,
|
|
8379
7969
|
// `starting` is the honest classification before the proof has run. The
|
|
@@ -8844,15 +8434,15 @@ async function enumerateOpencodeModels(serverUrl, fetchImpl = fetch) {
|
|
|
8844
8434
|
|
|
8845
8435
|
// src/outbox.ts
|
|
8846
8436
|
import {
|
|
8847
|
-
existsSync as
|
|
8848
|
-
mkdirSync as
|
|
8437
|
+
existsSync as existsSync11,
|
|
8438
|
+
mkdirSync as mkdirSync11,
|
|
8849
8439
|
readdirSync as readdirSync3,
|
|
8850
|
-
readFileSync as
|
|
8440
|
+
readFileSync as readFileSync8,
|
|
8851
8441
|
renameSync as renameSync3,
|
|
8852
|
-
rmSync as
|
|
8853
|
-
writeFileSync as
|
|
8442
|
+
rmSync as rmSync7,
|
|
8443
|
+
writeFileSync as writeFileSync7
|
|
8854
8444
|
} from "fs";
|
|
8855
|
-
import { join as
|
|
8445
|
+
import { join as join15 } from "path";
|
|
8856
8446
|
var MAX_ENTRIES = 2e3;
|
|
8857
8447
|
var MAX_AGE_MS = 24 * 60 * 60 * 1e3;
|
|
8858
8448
|
var Outbox = class {
|
|
@@ -8865,25 +8455,25 @@ var Outbox = class {
|
|
|
8865
8455
|
// Resolved lazily (per call, like cursor.ts) so tests that swap HOME between
|
|
8866
8456
|
// cases route writes at the right tmpdir.
|
|
8867
8457
|
dir() {
|
|
8868
|
-
return
|
|
8458
|
+
return join15(cabaneDir(), "outbox", encodeURIComponent(this.workspaceId));
|
|
8869
8459
|
}
|
|
8870
8460
|
fileFor(turnId, seq) {
|
|
8871
|
-
return
|
|
8461
|
+
return join15(this.dir(), `${encodeURIComponent(turnId)}__${seq}.json`);
|
|
8872
8462
|
}
|
|
8873
8463
|
// Persist a commit for later draining. Atomic (temp file + rename) so a
|
|
8874
8464
|
// concurrent `list()` never reads a half-written entry, then enforces the
|
|
8875
8465
|
// per-workspace bounds.
|
|
8876
8466
|
persist(entry) {
|
|
8877
8467
|
const dir2 = this.dir();
|
|
8878
|
-
|
|
8879
|
-
const
|
|
8880
|
-
const tmp = `${
|
|
8468
|
+
mkdirSync11(dir2, { recursive: true });
|
|
8469
|
+
const target = this.fileFor(entry.turnId, entry.seq);
|
|
8470
|
+
const tmp = `${target}.${process.pid}.tmp`;
|
|
8881
8471
|
try {
|
|
8882
|
-
|
|
8883
|
-
renameSync3(tmp,
|
|
8472
|
+
writeFileSync7(tmp, JSON.stringify(entry) + "\n", "utf8");
|
|
8473
|
+
renameSync3(tmp, target);
|
|
8884
8474
|
} catch (err) {
|
|
8885
8475
|
try {
|
|
8886
|
-
|
|
8476
|
+
rmSync7(tmp, { force: true });
|
|
8887
8477
|
} catch {
|
|
8888
8478
|
}
|
|
8889
8479
|
this.log?.warn(
|
|
@@ -8900,7 +8490,7 @@ var Outbox = class {
|
|
|
8900
8490
|
// wedging the drain.
|
|
8901
8491
|
list() {
|
|
8902
8492
|
const dir2 = this.dir();
|
|
8903
|
-
if (!
|
|
8493
|
+
if (!existsSync11(dir2)) return [];
|
|
8904
8494
|
let names;
|
|
8905
8495
|
try {
|
|
8906
8496
|
names = readdirSync3(dir2);
|
|
@@ -8910,9 +8500,9 @@ var Outbox = class {
|
|
|
8910
8500
|
const entries = [];
|
|
8911
8501
|
for (const name of names) {
|
|
8912
8502
|
if (!name.endsWith(".json")) continue;
|
|
8913
|
-
const full =
|
|
8503
|
+
const full = join15(dir2, name);
|
|
8914
8504
|
try {
|
|
8915
|
-
const parsed = JSON.parse(
|
|
8505
|
+
const parsed = JSON.parse(readFileSync8(full, "utf8"));
|
|
8916
8506
|
if (parsed && typeof parsed.turnId === "string" && typeof parsed.seq === "number" && typeof parsed.path === "string") {
|
|
8917
8507
|
entries.push(parsed);
|
|
8918
8508
|
} else {
|
|
@@ -8930,13 +8520,13 @@ var Outbox = class {
|
|
|
8930
8520
|
// Remove a delivered (or terminally-discarded) entry. No-op if already gone.
|
|
8931
8521
|
remove(turnId, seq) {
|
|
8932
8522
|
try {
|
|
8933
|
-
|
|
8523
|
+
rmSync7(this.fileFor(turnId, seq), { force: true });
|
|
8934
8524
|
} catch {
|
|
8935
8525
|
}
|
|
8936
8526
|
}
|
|
8937
8527
|
size() {
|
|
8938
8528
|
const dir2 = this.dir();
|
|
8939
|
-
if (!
|
|
8529
|
+
if (!existsSync11(dir2)) return 0;
|
|
8940
8530
|
try {
|
|
8941
8531
|
return readdirSync3(dir2).filter((n) => n.endsWith(".json")).length;
|
|
8942
8532
|
} catch {
|
|
@@ -8949,7 +8539,7 @@ var Outbox = class {
|
|
|
8949
8539
|
"companion outbox: dropping unreadable entry"
|
|
8950
8540
|
);
|
|
8951
8541
|
try {
|
|
8952
|
-
|
|
8542
|
+
rmSync7(full, { force: true });
|
|
8953
8543
|
} catch {
|
|
8954
8544
|
}
|
|
8955
8545
|
}
|
|
@@ -10111,22 +9701,22 @@ function handleUncaught(log, err, origin) {
|
|
|
10111
9701
|
}
|
|
10112
9702
|
|
|
10113
9703
|
// src/crash-marker.ts
|
|
10114
|
-
import { existsSync as
|
|
10115
|
-
import { join as
|
|
9704
|
+
import { existsSync as existsSync12, mkdirSync as mkdirSync12, readFileSync as readFileSync9, rmSync as rmSync8, writeFileSync as writeFileSync8 } from "fs";
|
|
9705
|
+
import { join as join16 } from "path";
|
|
10116
9706
|
function crashMarkerPath() {
|
|
10117
|
-
return
|
|
9707
|
+
return join16(cabaneDir(), "last-error.json");
|
|
10118
9708
|
}
|
|
10119
9709
|
function recordCrash(rec2) {
|
|
10120
9710
|
try {
|
|
10121
|
-
|
|
10122
|
-
|
|
9711
|
+
mkdirSync12(cabaneDir(), { recursive: true });
|
|
9712
|
+
writeFileSync8(crashMarkerPath(), JSON.stringify(rec2, null, 2) + "\n");
|
|
10123
9713
|
} catch {
|
|
10124
9714
|
}
|
|
10125
9715
|
}
|
|
10126
9716
|
function clearCrash() {
|
|
10127
9717
|
try {
|
|
10128
9718
|
const path = crashMarkerPath();
|
|
10129
|
-
if (
|
|
9719
|
+
if (existsSync12(path)) rmSync8(path, { force: true });
|
|
10130
9720
|
} catch {
|
|
10131
9721
|
}
|
|
10132
9722
|
}
|
|
@@ -10201,11 +9791,13 @@ async function createCompanionRuntime(opts = {}) {
|
|
|
10201
9791
|
});
|
|
10202
9792
|
await supervisor.start();
|
|
10203
9793
|
const connectHarness = async (runtime, serverUrl) => {
|
|
9794
|
+
const candidate = runtime === "opencode" ? { ...supervisor.currentConfig(), opencode: { serverUrl: serverUrl ?? "" } } : supervisor.currentConfig();
|
|
9795
|
+
const verdict = await shakeOutHarness(runtime, candidate);
|
|
9796
|
+
if (verdict === "absent") return { ok: false, error: absentLine(runtime) };
|
|
10204
9797
|
const result = await supervisor.enableHarness(
|
|
10205
9798
|
runtime === "opencode" ? { runtime: "opencode", serverUrl: serverUrl ?? "" } : { runtime }
|
|
10206
9799
|
);
|
|
10207
9800
|
if (!result.ok) return { ok: false, error: result.error };
|
|
10208
|
-
const verdict = await shakeOutHarness(runtime, supervisor.currentConfig());
|
|
10209
9801
|
return { ok: true, message: connectedLine(runtime, verdict) };
|
|
10210
9802
|
};
|
|
10211
9803
|
const control = await startControlServer({
|
|
@@ -10276,7 +9868,24 @@ async function closeSurfaces(control, dashboard) {
|
|
|
10276
9868
|
|
|
10277
9869
|
// src/commands/daemon.ts
|
|
10278
9870
|
import { spawn as spawn5 } from "child_process";
|
|
10279
|
-
import { closeSync as closeSync3, mkdirSync as
|
|
9871
|
+
import { closeSync as closeSync3, mkdirSync as mkdirSync13, openSync as openSync3 } from "fs";
|
|
9872
|
+
|
|
9873
|
+
// src/cli-entry.ts
|
|
9874
|
+
import { existsSync as existsSync13 } from "fs";
|
|
9875
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
9876
|
+
var RELATIVE_CANDIDATES = ["./cli.js", "../dist/cli.js", "../cli.js"];
|
|
9877
|
+
function companionCliEntry(deps = {}) {
|
|
9878
|
+
const exists = deps.exists ?? existsSync13;
|
|
9879
|
+
const candidates = deps.candidates ?? RELATIVE_CANDIDATES.map((rel) => fileURLToPath2(new URL(rel, import.meta.url)));
|
|
9880
|
+
for (const candidate of candidates) {
|
|
9881
|
+
if (exists(candidate)) return candidate;
|
|
9882
|
+
}
|
|
9883
|
+
const argv1 = "argv1" in deps ? deps.argv1 : process.argv[1];
|
|
9884
|
+
if (argv1 && exists(argv1)) return argv1;
|
|
9885
|
+
return candidates[0] ?? "";
|
|
9886
|
+
}
|
|
9887
|
+
|
|
9888
|
+
// src/commands/daemon.ts
|
|
10280
9889
|
var STARTUP_TIMEOUT_MS = 8e3;
|
|
10281
9890
|
var POLL_INTERVAL_MS = 150;
|
|
10282
9891
|
async function startDaemon(opts = {}, deps = {}) {
|
|
@@ -10286,11 +9895,6 @@ async function startDaemon(opts = {}, deps = {}) {
|
|
|
10286
9895
|
const spawnDetached = deps.spawnDetached ?? defaultSpawnDetached;
|
|
10287
9896
|
const sleep4 = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
10288
9897
|
const now = deps.now ?? (() => Date.now());
|
|
10289
|
-
const install = deps.installService ?? ((o) => installService(o));
|
|
10290
|
-
const refresh = deps.refreshService ?? ((o) => refreshInstalledService(o));
|
|
10291
|
-
const disable = deps.disableService ?? (() => disableService());
|
|
10292
|
-
const isTty = deps.isTty ?? (() => Boolean(process.stdin.isTTY));
|
|
10293
|
-
const serviceOpts = {};
|
|
10294
9898
|
const quiet = opts.report === "failures";
|
|
10295
9899
|
const { cfg, claudeOnPath: claudeOnPath2 } = await requireStartConfig({
|
|
10296
9900
|
// Unset → `requireStartConfig`'s own `requireConfig`, the real one.
|
|
@@ -10318,22 +9922,16 @@ async function startDaemon(opts = {}, deps = {}) {
|
|
|
10318
9922
|
const existing = readState();
|
|
10319
9923
|
if (existing) {
|
|
10320
9924
|
if (await verify(existing) !== "stale") {
|
|
10321
|
-
const rerendered = isTty() ? refresh(serviceOpts) : false;
|
|
10322
9925
|
process.stdout.write(
|
|
10323
9926
|
`Cabane Companion is already running (pid ${existing.pid}).
|
|
10324
9927
|
Stop it first with \`cabane-companion stop\` if you want to relaunch.
|
|
10325
9928
|
Connect a harness to the running companion: cabane-companion connect claude-code
|
|
10326
|
-
`
|
|
9929
|
+
`
|
|
10327
9930
|
);
|
|
10328
|
-
return {
|
|
10329
|
-
started: false,
|
|
10330
|
-
service: { installed: false, reason: "unsupported" },
|
|
10331
|
-
state: existing
|
|
10332
|
-
};
|
|
9931
|
+
return { started: false, state: existing };
|
|
10333
9932
|
}
|
|
10334
9933
|
clearRuntimeState();
|
|
10335
9934
|
}
|
|
10336
|
-
let service2 = isTty() ? install(serviceOpts) : { installed: false, reason: "unsupported" };
|
|
10337
9935
|
const args = ["start", "--foreground"];
|
|
10338
9936
|
const launchDetached = () => {
|
|
10339
9937
|
const spawned = spawnDetached(args);
|
|
@@ -10350,29 +9948,8 @@ Connect a harness to the running companion: cabane-companion connect claude-code
|
|
|
10350
9948
|
}
|
|
10351
9949
|
return ready(seen) ? seen : null;
|
|
10352
9950
|
};
|
|
10353
|
-
|
|
10354
|
-
|
|
10355
|
-
process.exitCode = 1;
|
|
10356
|
-
return { started: false, service: service2, state: null };
|
|
10357
|
-
}
|
|
10358
|
-
let child = service2.installed ? null : launchDetached();
|
|
10359
|
-
let state = await waitForReady();
|
|
10360
|
-
if (!state && service2.installed) {
|
|
10361
|
-
process.stdout.write(
|
|
10362
|
-
`${service2.manager} started the companion but it didn't report ready within ${STARTUP_TIMEOUT_MS / 1e3}s \u2014 removing the login service.
|
|
10363
|
-
`
|
|
10364
|
-
);
|
|
10365
|
-
const removed = disable();
|
|
10366
|
-
if (!removed.handled || !removed.ok) {
|
|
10367
|
-
refuseDouble(service2.manager, removed.detail);
|
|
10368
|
-
process.exitCode = 1;
|
|
10369
|
-
return { started: false, service: service2, state: null };
|
|
10370
|
-
}
|
|
10371
|
-
process.stdout.write("Launching it directly instead.\n");
|
|
10372
|
-
service2 = { installed: false, reason: "command-failed", detail: "the service never came up" };
|
|
10373
|
-
child = launchDetached();
|
|
10374
|
-
state = await waitForReady();
|
|
10375
|
-
}
|
|
9951
|
+
const child = launchDetached();
|
|
9952
|
+
const state = await waitForReady();
|
|
10376
9953
|
if (!state) {
|
|
10377
9954
|
process.stdout.write(
|
|
10378
9955
|
`Cabane Companion was launched (pid ${child?.pid ?? "?"}) but didn't report ready within ${STARTUP_TIMEOUT_MS / 1e3}s.
|
|
@@ -10380,9 +9957,9 @@ Check ${companionLogPath()} for errors, or run \`cabane-companion status\`.
|
|
|
10380
9957
|
`
|
|
10381
9958
|
);
|
|
10382
9959
|
process.exitCode = 1;
|
|
10383
|
-
return { started: false,
|
|
9960
|
+
return { started: false, state: null };
|
|
10384
9961
|
}
|
|
10385
|
-
if (quiet) return { started: true,
|
|
9962
|
+
if (quiet) return { started: true, state };
|
|
10386
9963
|
process.stdout.write(
|
|
10387
9964
|
`Cabane Companion started in the background (pid ${state.pid}).
|
|
10388
9965
|
Logs: ${companionLogPath()}
|
|
@@ -10390,27 +9967,11 @@ Status: cabane-companion status
|
|
|
10390
9967
|
Stop: cabane-companion stop
|
|
10391
9968
|
`
|
|
10392
9969
|
);
|
|
10393
|
-
|
|
10394
|
-
process.stdout.write(`Autostart: enabled (${service2.manager}) \u2014 it starts again at login.
|
|
10395
|
-
`);
|
|
10396
|
-
} else if (service2.reason !== "unsupported") {
|
|
10397
|
-
process.stdout.write(
|
|
10398
|
-
`Autostart: not enabled \u2014 ${service2.detail ?? "the service manager refused the install"}. Running detached instead; it won't come back after a reboot.
|
|
10399
|
-
`
|
|
10400
|
-
);
|
|
10401
|
-
}
|
|
10402
|
-
return { started: true, service: service2, state };
|
|
10403
|
-
}
|
|
10404
|
-
function refuseDouble(manager, detail) {
|
|
10405
|
-
process.stdout.write(
|
|
10406
|
-
`${manager} still has the login service${detail ? ` (${detail})` : ""} \u2014 not launching a second companion beside a service that may still own one.
|
|
10407
|
-
Check \`cabane-companion service status\`, then \`cabane-companion service disable\`, and run \`cabane-companion start --daemon\` again.
|
|
10408
|
-
`
|
|
10409
|
-
);
|
|
9970
|
+
return { started: true, state };
|
|
10410
9971
|
}
|
|
10411
9972
|
function defaultSpawnDetached(args) {
|
|
10412
9973
|
const cliPath = companionCliEntry();
|
|
10413
|
-
|
|
9974
|
+
mkdirSync13(cabaneDir(), { recursive: true });
|
|
10414
9975
|
const logFd = openSync3(companionLogPath(), "a");
|
|
10415
9976
|
try {
|
|
10416
9977
|
return spawn5(process.execPath, [cliPath, ...args], {
|
|
@@ -10430,6 +9991,15 @@ var DEPLOY_GRACE_MS = 6.5 * 60 * 60 * 1e3;
|
|
|
10430
9991
|
var MAX_CODES = 3;
|
|
10431
9992
|
async function start(opts = {}) {
|
|
10432
9993
|
const interactive = isInteractive();
|
|
9994
|
+
if (!interactive) return startScript(opts, false);
|
|
9995
|
+
setConsoleLogging(false);
|
|
9996
|
+
try {
|
|
9997
|
+
await startScript(opts, true);
|
|
9998
|
+
} finally {
|
|
9999
|
+
setConsoleLogging(true);
|
|
10000
|
+
}
|
|
10001
|
+
}
|
|
10002
|
+
async function startScript(opts, interactive) {
|
|
10433
10003
|
const running = await liveCompanion();
|
|
10434
10004
|
if (running) {
|
|
10435
10005
|
reportAlreadyRunning(running.pid);
|
|
@@ -10439,7 +10009,9 @@ async function start(opts = {}) {
|
|
|
10439
10009
|
let paired = null;
|
|
10440
10010
|
if (!isDevicePaired()) {
|
|
10441
10011
|
paired = await pairHere(opts, interactive);
|
|
10442
|
-
if (!paired)
|
|
10012
|
+
if (!paired) {
|
|
10013
|
+
return;
|
|
10014
|
+
}
|
|
10443
10015
|
const { note } = writePairedConfig(paired);
|
|
10444
10016
|
if (note) getLogger().warn({ note }, "companion: salvaged an older config on pairing");
|
|
10445
10017
|
}
|
|
@@ -10470,6 +10042,7 @@ async function start(opts = {}) {
|
|
|
10470
10042
|
write(` Stop: Ctrl-C Logs: ${tildePath(companionLogPath())}`);
|
|
10471
10043
|
blank();
|
|
10472
10044
|
write(`${INDENT}Listening for messages\u2026`);
|
|
10045
|
+
setConsoleLogging(true);
|
|
10473
10046
|
} else {
|
|
10474
10047
|
if (!offered.printedSomething) blank();
|
|
10475
10048
|
write(
|
|
@@ -10619,11 +10192,7 @@ async function handOffToBackground(runtime, ctx) {
|
|
|
10619
10192
|
return;
|
|
10620
10193
|
}
|
|
10621
10194
|
if (ctx.spaceAbove) blank();
|
|
10622
|
-
write(
|
|
10623
|
-
INDENT + tick(
|
|
10624
|
-
outcome.service.installed ? "Cabane companion is running in the background, and will start again when you log in." : "Cabane companion is running in the background."
|
|
10625
|
-
)
|
|
10626
|
-
);
|
|
10195
|
+
write(INDENT + tick("Cabane companion is running in the background."));
|
|
10627
10196
|
write(` Stop: cabane-companion stop Logs: ${tildePath(companionLogPath())}`);
|
|
10628
10197
|
if (ctx.justPaired) {
|
|
10629
10198
|
blank();
|
|
@@ -10687,8 +10256,7 @@ companion: deploy drain requested (${graceMs}ms grace)\u2026
|
|
|
10687
10256
|
|
|
10688
10257
|
// src/commands/status.ts
|
|
10689
10258
|
import { readdirSync as readdirSync4 } from "fs";
|
|
10690
|
-
async function status(
|
|
10691
|
-
const readService = deps.service ?? serviceStatus;
|
|
10259
|
+
async function status() {
|
|
10692
10260
|
const cfg = loadConfig();
|
|
10693
10261
|
if (!cfg || !cfg.deviceToken) {
|
|
10694
10262
|
process.stdout.write(
|
|
@@ -10724,13 +10292,6 @@ async function status(deps = {}) {
|
|
|
10724
10292
|
"companion: not running \u2014 `cabane-companion start` (backgrounds itself; `--foreground` stays attached)\n"
|
|
10725
10293
|
);
|
|
10726
10294
|
}
|
|
10727
|
-
const svc = readService();
|
|
10728
|
-
if (svc.installed) {
|
|
10729
|
-
process.stdout.write(
|
|
10730
|
-
`autostart: ${svc.manager} \u2014 starts at login` + (svc.manager === "systemd-user" && svc.linger !== "yes" ? ", while you are logged in" : "") + ` (details: cabane-companion service status)
|
|
10731
|
-
`
|
|
10732
|
-
);
|
|
10733
|
-
}
|
|
10734
10295
|
process.stdout.write(
|
|
10735
10296
|
"agents: pulled from cabane at runtime \u2014 see this device in Cabane for its assigned agents, their run state, and any missing-secret warnings.\n"
|
|
10736
10297
|
);
|
|
@@ -10777,34 +10338,12 @@ function formatUptime(startedAt) {
|
|
|
10777
10338
|
// src/commands/stop.ts
|
|
10778
10339
|
var TERM_GRACE_MS = 6e3;
|
|
10779
10340
|
var POLL_INTERVAL_MS2 = 150;
|
|
10780
|
-
var SERVICE_STOP_GRACE_MS = 3e3;
|
|
10781
10341
|
async function stop(deps = {}) {
|
|
10782
10342
|
const readState = deps.readState ?? readLiveRuntimeState;
|
|
10783
10343
|
const verify = deps.verify ?? ((s) => verifyRuntime(s));
|
|
10784
10344
|
const kill = deps.kill ?? ((pid2, signal) => process.kill(pid2, signal));
|
|
10785
10345
|
const sleep4 = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
10786
10346
|
const now = deps.now ?? (() => Date.now());
|
|
10787
|
-
const stopSvc = deps.stopService ?? (() => stopService());
|
|
10788
|
-
const svc = stopSvc();
|
|
10789
|
-
if (svc.handled) {
|
|
10790
|
-
if (!svc.ok) {
|
|
10791
|
-
process.stdout.write(
|
|
10792
|
-
`companion: ${svc.manager} wouldn't stop the service${svc.detail ? ` (${svc.detail})` : ""}.
|
|
10793
|
-
The service manager owns this process \u2014 stopping it directly would just be restarted.
|
|
10794
|
-
Check \`cabane-companion service status\`, or remove it with \`cabane-companion service disable\`.
|
|
10795
|
-
`
|
|
10796
|
-
);
|
|
10797
|
-
process.exitCode = 1;
|
|
10798
|
-
return;
|
|
10799
|
-
}
|
|
10800
|
-
const deadline2 = now() + SERVICE_STOP_GRACE_MS;
|
|
10801
|
-
while (readState() && now() < deadline2) await sleep4(POLL_INTERVAL_MS2);
|
|
10802
|
-
if (!readState()) {
|
|
10803
|
-
process.stdout.write(`companion: stopped (${svc.manager} service stopped).
|
|
10804
|
-
`);
|
|
10805
|
-
return;
|
|
10806
|
-
}
|
|
10807
|
-
}
|
|
10808
10347
|
const state = readState();
|
|
10809
10348
|
if (!state) {
|
|
10810
10349
|
process.stdout.write("companion: not running (nothing to stop).\n");
|
|
@@ -10854,8 +10393,8 @@ function isAlive(kill, pid) {
|
|
|
10854
10393
|
}
|
|
10855
10394
|
|
|
10856
10395
|
// src/commands/transcript.ts
|
|
10857
|
-
import { existsSync as
|
|
10858
|
-
import { isAbsolute, join as
|
|
10396
|
+
import { existsSync as existsSync14, readFileSync as readFileSync10, readdirSync as readdirSync5 } from "fs";
|
|
10397
|
+
import { isAbsolute, join as join17 } from "path";
|
|
10859
10398
|
async function transcript(opts = {}) {
|
|
10860
10399
|
const dir2 = transcriptsDir();
|
|
10861
10400
|
if (opts.follow) {
|
|
@@ -10872,7 +10411,7 @@ async function transcript(opts = {}) {
|
|
|
10872
10411
|
process.stdout.write(emptyMessage(dir2));
|
|
10873
10412
|
return;
|
|
10874
10413
|
}
|
|
10875
|
-
process.stdout.write(renderFile(
|
|
10414
|
+
process.stdout.write(renderFile(join17(dir2, newest)) + "\n");
|
|
10876
10415
|
return;
|
|
10877
10416
|
}
|
|
10878
10417
|
printList(dir2);
|
|
@@ -10955,7 +10494,7 @@ function isComplete(content) {
|
|
|
10955
10494
|
async function followTranscripts(dir2) {
|
|
10956
10495
|
const follower = new TranscriptFollower({
|
|
10957
10496
|
listFiles: () => listFiles(dir2),
|
|
10958
|
-
read: (f) =>
|
|
10497
|
+
read: (f) => readFileSync10(join17(dir2, f), "utf8"),
|
|
10959
10498
|
write: (s) => process.stdout.write(s),
|
|
10960
10499
|
// CSI: cursor up `n` lines, then erase from cursor to end of screen.
|
|
10961
10500
|
clearLines: (n) => process.stdout.write(`\x1B[${n}A\x1B[0J`),
|
|
@@ -10995,7 +10534,7 @@ function printList(dir2) {
|
|
|
10995
10534
|
|
|
10996
10535
|
`);
|
|
10997
10536
|
for (const f of files.slice(0, 20)) {
|
|
10998
|
-
const { meta, outcome } = peek(
|
|
10537
|
+
const { meta, outcome } = peek(join17(dir2, f));
|
|
10999
10538
|
const when = fmtTime(rec(meta)?.ts);
|
|
11000
10539
|
const ws = str2(rec(meta)?.workspaceSlug);
|
|
11001
10540
|
const o = rec(outcome);
|
|
@@ -11016,7 +10555,7 @@ function peek(path) {
|
|
|
11016
10555
|
let meta;
|
|
11017
10556
|
let outcome;
|
|
11018
10557
|
try {
|
|
11019
|
-
for (const line of
|
|
10558
|
+
for (const line of readFileSync10(path, "utf8").split("\n")) {
|
|
11020
10559
|
if (!line.trim()) continue;
|
|
11021
10560
|
const o = safeParse(line);
|
|
11022
10561
|
const t = str2(rec(o)?.type);
|
|
@@ -11027,29 +10566,29 @@ function peek(path) {
|
|
|
11027
10566
|
}
|
|
11028
10567
|
return { meta, outcome };
|
|
11029
10568
|
}
|
|
11030
|
-
function resolveTarget(dir2,
|
|
11031
|
-
if (isAbsolute(
|
|
11032
|
-
if (
|
|
11033
|
-
throw new CompanionError(`no transcript at ${
|
|
10569
|
+
function resolveTarget(dir2, target) {
|
|
10570
|
+
if (isAbsolute(target) || target.includes("/")) {
|
|
10571
|
+
if (existsSync14(target)) return target;
|
|
10572
|
+
throw new CompanionError(`no transcript at ${target}.`);
|
|
11034
10573
|
}
|
|
11035
|
-
const exact =
|
|
11036
|
-
if (
|
|
11037
|
-
const matches = listFiles(dir2).filter((f) => f.includes(
|
|
11038
|
-
if (matches.length === 1) return
|
|
10574
|
+
const exact = join17(dir2, target);
|
|
10575
|
+
if (existsSync14(exact)) return exact;
|
|
10576
|
+
const matches = listFiles(dir2).filter((f) => f.includes(target));
|
|
10577
|
+
if (matches.length === 1) return join17(dir2, matches[0]);
|
|
11039
10578
|
if (matches.length === 0) {
|
|
11040
10579
|
throw new CompanionError(
|
|
11041
|
-
`no transcript matching "${
|
|
10580
|
+
`no transcript matching "${target}" in ${dir2}. Run \`cabane-companion transcript\` to list them.`
|
|
11042
10581
|
);
|
|
11043
10582
|
}
|
|
11044
10583
|
throw new CompanionError(
|
|
11045
|
-
`"${
|
|
10584
|
+
`"${target}" matches ${matches.length} transcripts \u2014 be more specific:
|
|
11046
10585
|
` + matches.slice(0, 10).map((m) => ` ${m}`).join("\n")
|
|
11047
10586
|
);
|
|
11048
10587
|
}
|
|
11049
10588
|
function renderFile(path) {
|
|
11050
10589
|
let content;
|
|
11051
10590
|
try {
|
|
11052
|
-
content =
|
|
10591
|
+
content = readFileSync10(path, "utf8");
|
|
11053
10592
|
} catch (err) {
|
|
11054
10593
|
throw new CompanionError(
|
|
11055
10594
|
`couldn't read ${path}: ${err instanceof Error ? err.message : String(err)}`
|
|
@@ -11186,11 +10725,11 @@ program.command("pair").description(
|
|
|
11186
10725
|
});
|
|
11187
10726
|
});
|
|
11188
10727
|
program.command("write-paired-config", { hidden: true }).description("persist an already-completed device enrollment payload from stdin.").action(() => {
|
|
11189
|
-
writeCompletedPairing(
|
|
10728
|
+
writeCompletedPairing(readFileSync11(0, "utf8"));
|
|
11190
10729
|
});
|
|
11191
10730
|
program.command("start").description(
|
|
11192
10731
|
"pair this device if needed, connect a coding agent on it, and run in the background."
|
|
11193
|
-
).option("--foreground", "run attached in this terminal (
|
|
10732
|
+
).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(new Option("--open").hideHelp()).addOption(new Option("--no-open").hideHelp()).addOption(new Option("--port <port>").hideHelp()).action(async (opts) => {
|
|
11194
10733
|
await start({
|
|
11195
10734
|
...opts.foreground ? { foreground: true } : {},
|
|
11196
10735
|
...opts.server !== void 0 ? { server: opts.server } : {}
|
|
@@ -11205,13 +10744,6 @@ program.command("stop").description("stop a running companion (SIGTERM, then for
|
|
|
11205
10744
|
program.command("status").description("print the companion's local state (pairing, secrets, log path).").action(async () => {
|
|
11206
10745
|
await status();
|
|
11207
10746
|
});
|
|
11208
|
-
var service = program.command("service").description("inspect or remove the login service that starts the companion when you log in.");
|
|
11209
|
-
service.command("status").description("print the service manager, whether the unit is installed and running.").action(() => {
|
|
11210
|
-
serviceStatusCommand();
|
|
11211
|
-
});
|
|
11212
|
-
service.command("disable").description("stop the companion and remove the login service, leaving the machine clean.").action(() => {
|
|
11213
|
-
serviceDisableCommand();
|
|
11214
|
-
});
|
|
11215
10747
|
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) => {
|
|
11216
10748
|
await transcript({
|
|
11217
10749
|
...file !== void 0 ? { target: file } : {},
|
|
@@ -11242,7 +10774,12 @@ program.parseAsync(process.argv).catch((err) => {
|
|
|
11242
10774
|
process.exitCode = 130;
|
|
11243
10775
|
return;
|
|
11244
10776
|
}
|
|
11245
|
-
|
|
10777
|
+
setConsoleLogging(false);
|
|
10778
|
+
try {
|
|
10779
|
+
getLogger().error({ err }, "companion: command failed");
|
|
10780
|
+
} catch {
|
|
10781
|
+
}
|
|
10782
|
+
process.stderr.write(`${err instanceof Error ? err.message : String(err)}
|
|
11246
10783
|
`);
|
|
11247
10784
|
process.exitCode = 1;
|
|
11248
10785
|
});
|