@cabane/companion 0.6.162 → 0.6.163

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.
Files changed (3) hide show
  1. package/dist/cli.js +353 -229
  2. package/dist/runtime.js +174 -65
  3. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -3048,14 +3048,14 @@ function readSdkUsage(usage) {
3048
3048
  function readRequestContextTokens(msg) {
3049
3049
  return readSdkUsage(msg.message?.usage)?.inputTokens;
3050
3050
  }
3051
- async function readHarnessContext(read) {
3052
- if (!read) return void 0;
3051
+ async function readHarnessContext(read2) {
3052
+ if (!read2) return void 0;
3053
3053
  let timer;
3054
3054
  const timeout = new Promise((resolve2) => {
3055
3055
  timer = setTimeout(() => resolve2(void 0), CONTEXT_READING_TIMEOUT_MS);
3056
3056
  });
3057
3057
  try {
3058
- return parseContextUsage(await Promise.race([read(), timeout]));
3058
+ return parseContextUsage(await Promise.race([read2(), timeout]));
3059
3059
  } catch {
3060
3060
  return void 0;
3061
3061
  } finally {
@@ -5412,14 +5412,14 @@ function decideResume3(stored, currentCwd) {
5412
5412
  function readUsageBaseline(raw) {
5413
5413
  if (!raw || typeof raw !== "object") return null;
5414
5414
  const rec2 = raw;
5415
- const read = (key) => {
5415
+ const read2 = (key) => {
5416
5416
  const v = rec2[key];
5417
5417
  return typeof v === "number" && Number.isFinite(v) && v >= 0 ? v : null;
5418
5418
  };
5419
- const inputTokens = read("inputTokens");
5420
- const outputTokens = read("outputTokens");
5421
- const cacheReadTokens = read("cacheReadTokens");
5422
- const cacheCreationTokens = read("cacheCreationTokens");
5419
+ const inputTokens = read2("inputTokens");
5420
+ const outputTokens = read2("outputTokens");
5421
+ const cacheReadTokens = read2("cacheReadTokens");
5422
+ const cacheCreationTokens = read2("cacheCreationTokens");
5423
5423
  if (inputTokens === null || outputTokens === null || cacheReadTokens === null || cacheCreationTokens === null) {
5424
5424
  return null;
5425
5425
  }
@@ -7244,7 +7244,7 @@ var init_opencode_lock = __esm({
7244
7244
 
7245
7245
  // src/turn-containment.ts
7246
7246
  import { execFile, execFileSync, spawn as spawn6 } from "child_process";
7247
- import { readFileSync as readFileSync10 } from "fs";
7247
+ import { readFileSync as readFileSync11 } from "fs";
7248
7248
  import { promisify } from "util";
7249
7249
  function turnScopeUnit(turnId, attempt) {
7250
7250
  const safe2 = turnId.replace(/[^A-Za-z0-9_.-]/g, "-");
@@ -7297,7 +7297,7 @@ function currentSystemdUnit(cgroupText) {
7297
7297
  }
7298
7298
  function readCurrentSystemdUnit() {
7299
7299
  try {
7300
- return currentSystemdUnit(readFileSync10("/proc/self/cgroup", "utf8"));
7300
+ return currentSystemdUnit(readFileSync11("/proc/self/cgroup", "utf8"));
7301
7301
  } catch {
7302
7302
  return null;
7303
7303
  }
@@ -7527,18 +7527,18 @@ var init_turn_containment = __esm({
7527
7527
  });
7528
7528
 
7529
7529
  // src/secrets.ts
7530
- import { existsSync as existsSync12, readFileSync as readFileSync11 } from "fs";
7531
- import { join as join19 } from "path";
7530
+ import { existsSync as existsSync12, readFileSync as readFileSync12 } from "fs";
7531
+ import { join as join20 } from "path";
7532
7532
  import { z as z15 } from "zod";
7533
7533
  function secretsPath() {
7534
- return join19(cabaneDir(), "secrets.json");
7534
+ return join20(cabaneDir(), "secrets.json");
7535
7535
  }
7536
7536
  function loadSecretStore() {
7537
7537
  const path = secretsPath();
7538
7538
  if (!existsSync12(path)) return makeStore({});
7539
7539
  let raw;
7540
7540
  try {
7541
- raw = readFileSync11(path, "utf8");
7541
+ raw = readFileSync12(path, "utf8");
7542
7542
  } catch (err) {
7543
7543
  throw new ConfigError(
7544
7544
  `couldn't read ${path}: ${err instanceof Error ? err.message : String(err)}`
@@ -8581,10 +8581,10 @@ var init_build_options = __esm({
8581
8581
  // src/codex-instructions.ts
8582
8582
  import { mkdtemp, rm, writeFile } from "fs/promises";
8583
8583
  import { tmpdir } from "os";
8584
- import { join as join20 } from "path";
8584
+ import { join as join21 } from "path";
8585
8585
  async function writeCodexInstructionsFile(contents) {
8586
- const dir2 = await mkdtemp(join20(tmpdir(), PREFIX));
8587
- const path = join20(dir2, "instructions.md");
8586
+ const dir2 = await mkdtemp(join21(tmpdir(), PREFIX));
8587
+ const path = join21(dir2, "instructions.md");
8588
8588
  await writeFile(path, contents, { encoding: "utf8", mode: 384 });
8589
8589
  return {
8590
8590
  path,
@@ -8604,13 +8604,13 @@ var init_codex_instructions = __esm({
8604
8604
  // src/codex-images.ts
8605
8605
  import { mkdtemp as mkdtemp2, rm as rm2, writeFile as writeFile2 } from "fs/promises";
8606
8606
  import { tmpdir as tmpdir2 } from "os";
8607
- import { join as join21 } from "path";
8607
+ import { join as join22 } from "path";
8608
8608
  function createCodexImagePreparer(opts) {
8609
8609
  const doFetch = opts.fetch ?? fetch;
8610
8610
  const timeoutMs = opts.timeoutMs ?? FETCH_TIMEOUT_MS;
8611
8611
  return async (urls, signal) => {
8612
8612
  signal.throwIfAborted();
8613
- const dir2 = await mkdtemp2(join21(tmpdir2(), `cabane-codex-images-${opts.turnId}-`));
8613
+ const dir2 = await mkdtemp2(join22(tmpdir2(), `cabane-codex-images-${opts.turnId}-`));
8614
8614
  const cleanup = async () => {
8615
8615
  try {
8616
8616
  await rm2(dir2, { recursive: true, force: true });
@@ -8631,7 +8631,7 @@ function createCodexImagePreparer(opts) {
8631
8631
  const ext = EXTENSIONS[type];
8632
8632
  if (!ext) throw new Error(`not a vision image type: ${type || "none"}`);
8633
8633
  const bytes = new Uint8Array(await res.arrayBuffer());
8634
- const path = join21(dir2, `image-${index + 1}.${ext}`);
8634
+ const path = join22(dir2, `image-${index + 1}.${ext}`);
8635
8635
  await writeFile2(path, bytes, { mode: 384 });
8636
8636
  return path;
8637
8637
  } catch (err) {
@@ -8682,9 +8682,9 @@ function resolveSeqFloor(sources, ctx2) {
8682
8682
  );
8683
8683
  return floor;
8684
8684
  }
8685
- function readOutboxFloor(read, turnId, log2) {
8685
+ function readOutboxFloor(read2, turnId, log2) {
8686
8686
  try {
8687
- return read(turnId);
8687
+ return read2(turnId);
8688
8688
  } catch (err) {
8689
8689
  log2.debug(
8690
8690
  { turnId, err: err instanceof Error ? err.message : String(err) },
@@ -8776,22 +8776,22 @@ var init_prepare_failure = __esm({
8776
8776
  });
8777
8777
 
8778
8778
  // src/prepared.ts
8779
- import { mkdirSync as mkdirSync13, readFileSync as readFileSync12, rmSync as rmSync9, writeFileSync as writeFileSync8, existsSync as existsSync13 } from "fs";
8780
- import { join as join22 } from "path";
8779
+ import { mkdirSync as mkdirSync13, readFileSync as readFileSync13, rmSync as rmSync9, writeFileSync as writeFileSync8, existsSync as existsSync13 } from "fs";
8780
+ import { join as join23 } from "path";
8781
8781
  function dirFor(workspaceId) {
8782
- return join22(cabaneDir(), "prepared", encodeURIComponent(workspaceId));
8782
+ return join23(cabaneDir(), "prepared", encodeURIComponent(workspaceId));
8783
8783
  }
8784
8784
  function conversationDir(workspaceId, conversationId) {
8785
- return join22(dirFor(workspaceId), encodeURIComponent(conversationId));
8785
+ return join23(dirFor(workspaceId), encodeURIComponent(conversationId));
8786
8786
  }
8787
8787
  function pathFor2(workspaceId, conversationId, agentId) {
8788
- return join22(conversationDir(workspaceId, conversationId), `${encodeURIComponent(agentId)}.json`);
8788
+ return join23(conversationDir(workspaceId, conversationId), `${encodeURIComponent(agentId)}.json`);
8789
8789
  }
8790
8790
  function readPrepared(workspaceId, conversationId, agentId) {
8791
8791
  const path = pathFor2(workspaceId, conversationId, agentId);
8792
8792
  if (!existsSync13(path)) return null;
8793
8793
  try {
8794
- const parsed = JSON.parse(readFileSync12(path, "utf8"));
8794
+ const parsed = JSON.parse(readFileSync13(path, "utf8"));
8795
8795
  if (parsed && typeof parsed.cwd === "string" && parsed.cwd.length > 0) {
8796
8796
  return {
8797
8797
  cwd: parsed.cwd,
@@ -8823,9 +8823,9 @@ var init_prepared = __esm({
8823
8823
 
8824
8824
  // src/transcript-writer.ts
8825
8825
  import { appendFileSync, chmodSync as chmodSync5, copyFileSync as copyFileSync2, mkdirSync as mkdirSync14, readdirSync as readdirSync2, rmSync as rmSync10 } from "fs";
8826
- import { basename as basename3, dirname as dirname13, join as join23 } from "path";
8826
+ import { basename as basename3, dirname as dirname12, join as join24 } from "path";
8827
8827
  function transcriptsDir() {
8828
- return join23(runnerStateDir(), "transcripts");
8828
+ return join24(runnerStateDir(), "transcripts");
8829
8829
  }
8830
8830
  function fileName(meta) {
8831
8831
  const d = new Date(meta.ts);
@@ -8851,7 +8851,7 @@ function pruneOld(dir2, retain) {
8851
8851
  const drop = files.sort().slice(0, files.length - retain);
8852
8852
  for (const f of drop) {
8853
8853
  try {
8854
- rmSync10(join23(dir2, f), { force: true });
8854
+ rmSync10(join24(dir2, f), { force: true });
8855
8855
  } catch {
8856
8856
  }
8857
8857
  }
@@ -8869,7 +8869,7 @@ var init_transcript_writer = __esm({
8869
8869
  onWarn;
8870
8870
  constructor(dir2, meta, onWarn) {
8871
8871
  this.onWarn = onWarn;
8872
- this.path = join23(dir2, fileName(meta));
8872
+ this.path = join24(dir2, fileName(meta));
8873
8873
  try {
8874
8874
  mkdirSync14(dir2, { recursive: true });
8875
8875
  try {
@@ -8898,10 +8898,10 @@ var init_transcript_writer = __esm({
8898
8898
  preserveAnomaly() {
8899
8899
  if (this.broken) return;
8900
8900
  try {
8901
- const dir2 = join23(dirname13(this.path), "anomalies");
8901
+ const dir2 = join24(dirname12(this.path), "anomalies");
8902
8902
  mkdirSync14(dir2, { recursive: true, mode: 448 });
8903
8903
  chmodSync5(dir2, 448);
8904
- const target = join23(dir2, basename3(this.path));
8904
+ const target = join24(dir2, basename3(this.path));
8905
8905
  copyFileSync2(this.path, target);
8906
8906
  chmodSync5(target, 384);
8907
8907
  pruneOld(dir2, ANOMALY_RETAIN);
@@ -10338,7 +10338,7 @@ var init_turn_execution = __esm({
10338
10338
  // src/dispatcher.ts
10339
10339
  import { randomUUID as randomUUID4 } from "crypto";
10340
10340
  import { existsSync as existsSync15, readdirSync as readdirSync3, statSync as statSync3 } from "fs";
10341
- import { join as join24 } from "path";
10341
+ import { join as join25 } from "path";
10342
10342
  function checkoutState(cwd) {
10343
10343
  if (!cwd) return { ok: false, reason: "no working directory was resolved for this turn" };
10344
10344
  if (!existsSync15(cwd)) return { ok: false, reason: `the working directory is gone (${cwd})` };
@@ -10351,7 +10351,7 @@ function checkoutState(cwd) {
10351
10351
  if (entries.length === 0) {
10352
10352
  return { ok: false, reason: `${cwd} is empty \u2014 a recreated shell, not a prepared directory` };
10353
10353
  }
10354
- const gitPath = join24(cwd, ".git");
10354
+ const gitPath = join25(cwd, ".git");
10355
10355
  if (!existsSync15(gitPath)) return { ok: true, reason: "usable" };
10356
10356
  let stat;
10357
10357
  try {
@@ -10359,7 +10359,7 @@ function checkoutState(cwd) {
10359
10359
  } catch (error) {
10360
10360
  return { ok: false, reason: `${gitPath} is unreadable (${error.message})` };
10361
10361
  }
10362
- if (stat.isDirectory() && !existsSync15(join24(gitPath, "HEAD")))
10362
+ if (stat.isDirectory() && !existsSync15(join25(gitPath, "HEAD")))
10363
10363
  return { ok: false, reason: `${gitPath} has no HEAD \u2014 an empty shell, not a checkout` };
10364
10364
  return { ok: true, reason: "usable" };
10365
10365
  }
@@ -10730,7 +10730,7 @@ import {
10730
10730
  chmodSync as chmodSync7,
10731
10731
  mkdirSync as mkdirSync17,
10732
10732
  mkdtempSync,
10733
- readFileSync as readFileSync15,
10733
+ readFileSync as readFileSync16,
10734
10734
  realpathSync as realpathSync7,
10735
10735
  rmSync as rmSync14,
10736
10736
  writeFileSync as writeFileSync10
@@ -10738,7 +10738,7 @@ import {
10738
10738
  import { createRequire as createRequire3 } from "module";
10739
10739
  import { isIP } from "net";
10740
10740
  import { homedir as homedir8, networkInterfaces, tmpdir as tmpdir4, userInfo as userInfo3 } from "os";
10741
- import { dirname as dirname15, join as join28, sep as sep5 } from "path";
10741
+ import { dirname as dirname14, join as join29, sep as sep5 } from "path";
10742
10742
  import { fileURLToPath as fileURLToPath7 } from "url";
10743
10743
  import { SandboxManager } from "@anthropic-ai/sandbox-runtime";
10744
10744
  function accountHome() {
@@ -10770,23 +10770,23 @@ async function resolvedAddresses(baseUrl) {
10770
10770
  }
10771
10771
  }
10772
10772
  function companionPackageRoot(modulePath) {
10773
- for (let dir2 = dirname15(modulePath); ; dir2 = dirname15(dir2)) {
10773
+ for (let dir2 = dirname14(modulePath); ; dir2 = dirname14(dir2)) {
10774
10774
  try {
10775
- const manifest = JSON.parse(readFileSync15(join28(dir2, "package.json"), "utf8"));
10775
+ const manifest = JSON.parse(readFileSync16(join29(dir2, "package.json"), "utf8"));
10776
10776
  if (manifest.name === "@cabane/companion") return dir2;
10777
10777
  } catch {
10778
10778
  }
10779
- if (dirname15(dir2) === dir2) return null;
10779
+ if (dirname14(dir2) === dir2) return null;
10780
10780
  }
10781
10781
  }
10782
10782
  function harnessRuntimePaths() {
10783
- const paths = [dirname15(dirname15(process.execPath))];
10783
+ const paths = [dirname14(dirname14(process.execPath))];
10784
10784
  const root = companionPackageRoot(fileURLToPath7(import.meta.url));
10785
10785
  if (root) paths.push(root);
10786
10786
  try {
10787
10787
  const require2 = createRequire3(import.meta.url);
10788
10788
  paths.push(
10789
- dirname15(realpathSync7(require2.resolve("@anthropic-ai/sandbox-runtime/package.json")))
10789
+ dirname14(realpathSync7(require2.resolve("@anthropic-ai/sandbox-runtime/package.json")))
10790
10790
  );
10791
10791
  } catch {
10792
10792
  }
@@ -10794,7 +10794,7 @@ function harnessRuntimePaths() {
10794
10794
  const bundled = resolveBundledBinary(runtime).path;
10795
10795
  if (bundled) {
10796
10796
  try {
10797
- paths.push(dirname15(realpathSync7(bundled)));
10797
+ paths.push(dirname14(realpathSync7(bundled)));
10798
10798
  } catch {
10799
10799
  }
10800
10800
  }
@@ -10805,9 +10805,9 @@ function ownAddresses() {
10805
10805
  return Object.values(networkInterfaces()).flat().flatMap((i) => i ? [i.address] : []);
10806
10806
  }
10807
10807
  function storeWalkRoots(homes, cabaneDir2) {
10808
- const companionHome = dirname15(cabaneDir2);
10809
- const parent = dirname15(companionHome);
10810
- const beside = homes.includes(companionHome) || dirname15(parent) === parent ? [] : [parent];
10808
+ const companionHome = dirname14(cabaneDir2);
10809
+ const parent = dirname14(companionHome);
10810
+ const beside = homes.includes(companionHome) || dirname14(parent) === parent ? [] : [parent];
10811
10811
  const candidates = [.../* @__PURE__ */ new Set([...homes, ...beside])];
10812
10812
  return candidates.filter(
10813
10813
  (root) => !candidates.some((other) => other !== root && isAtOrUnder(root, other))
@@ -10845,19 +10845,19 @@ async function walkCompanionStores(roots, opts = {}) {
10845
10845
  }
10846
10846
  function companionStoreDenies(input) {
10847
10847
  const { homes, cabaneDir: cabaneDir2, foundStores } = input;
10848
- const parent = dirname15(dirname15(cabaneDir2));
10848
+ const parent = dirname14(dirname14(cabaneDir2));
10849
10849
  const always = [
10850
10850
  cabaneDir2,
10851
10851
  ...homes.flatMap((home) => [
10852
- join28(home, ".cabane"),
10853
- join28(home, "*", ".cabane"),
10854
- join28(home, "*", "*", ".cabane")
10852
+ join29(home, ".cabane"),
10853
+ join29(home, "*", ".cabane"),
10854
+ join29(home, "*", "*", ".cabane")
10855
10855
  ]),
10856
10856
  // A glob anchored at the root has no directory to start from.
10857
- ...dirname15(parent) === parent ? [] : [join28(parent, "*", ".cabane")]
10857
+ ...dirname14(parent) === parent ? [] : [join29(parent, "*", ".cabane")]
10858
10858
  ];
10859
- const companionHome = dirname15(cabaneDir2);
10860
- const otherLogins = foundStores.map((store) => dirname15(store)).filter((home) => home !== companionHome).flatMap((home) => [join28(home, ".claude"), join28(home, ".codex")]).filter((path) => !input.ownState.some((own) => isAtOrUnder(own, path)));
10859
+ const companionHome = dirname14(cabaneDir2);
10860
+ const otherLogins = foundStores.map((store) => dirname14(store)).filter((home) => home !== companionHome).flatMap((home) => [join29(home, ".claude"), join29(home, ".codex")]).filter((path) => !input.ownState.some((own) => isAtOrUnder(own, path)));
10861
10861
  return [.../* @__PURE__ */ new Set([...always, ...foundStores, ...otherLogins])];
10862
10862
  }
10863
10863
  function companionStoreWatch(log2) {
@@ -10874,7 +10874,7 @@ function isAtOrUnder(path, dir2) {
10874
10874
  }
10875
10875
  function expandHome(path, home) {
10876
10876
  if (path === "~") return home;
10877
- return path.startsWith("~/") ? join28(home, path.slice(2)) : path;
10877
+ return path.startsWith("~/") ? join29(home, path.slice(2)) : path;
10878
10878
  }
10879
10879
  function stripPort(pattern) {
10880
10880
  return pattern.startsWith("[") ? pattern.replace(/^\[([^\]]+)\](:\d+)?$/, "$1") : pattern.replace(/:\d+$/, "");
@@ -10892,8 +10892,8 @@ function patternsOverlap(a, b) {
10892
10892
  }
10893
10893
  function harnessSandboxConfig(input) {
10894
10894
  const { policy } = input;
10895
- const claudeDir = input.claudeConfigDir ?? join28(input.home, ".claude");
10896
- const codexDir = input.codexHome ?? join28(input.home, ".codex");
10895
+ const claudeDir = input.claudeConfigDir ?? join29(input.home, ".claude");
10896
+ const codexDir = input.codexHome ?? join29(input.home, ".codex");
10897
10897
  const homes = [.../* @__PURE__ */ new Set([input.home, input.accountHome ?? input.home])];
10898
10898
  const expand = (paths) => (paths ?? []).map((p) => expandHome(p, input.home));
10899
10899
  const cabane2 = cabaneHostEntries(input.baseUrl, input.resolved, input.ownAddresses);
@@ -10910,7 +10910,7 @@ function harnessSandboxConfig(input) {
10910
10910
  foundStores: input.foundStores,
10911
10911
  ownState: [claudeDir, codexDir]
10912
10912
  }),
10913
- ...homes.flatMap((home) => SECRET_PATHS_UNDER_HOME.map((p) => join28(home, p))),
10913
+ ...homes.flatMap((home) => SECRET_PATHS_UNDER_HOME.map((p) => join29(home, p))),
10914
10914
  ...expand(policy.filesystem?.denyRead)
10915
10915
  ];
10916
10916
  const strict = policy.filesystem?.mode === "strict";
@@ -10918,7 +10918,7 @@ function harnessSandboxConfig(input) {
10918
10918
  const allowRead = strict ? [
10919
10919
  ...input.cwd ? [input.cwd] : [],
10920
10920
  claudeDir,
10921
- join28(input.home, ".claude.json"),
10921
+ join29(input.home, ".claude.json"),
10922
10922
  codexDir,
10923
10923
  ...input.runtimePaths ?? [],
10924
10924
  ...expand(policy.filesystem?.allowRead)
@@ -10958,7 +10958,7 @@ function harnessSandboxConfig(input) {
10958
10958
  ...input.cwd ? [input.cwd] : [],
10959
10959
  input.tmp,
10960
10960
  claudeDir,
10961
- join28(input.home, ".claude.json"),
10961
+ join29(input.home, ".claude.json"),
10962
10962
  codexDir,
10963
10963
  ...expand(policy.filesystem?.allowWrite)
10964
10964
  ],
@@ -11060,9 +11060,9 @@ async function prepareHarnessSandbox(input) {
11060
11060
  } finally {
11061
11061
  for (const name of held) delete process.env[name];
11062
11062
  }
11063
- mkdirSync17(join28(cabaneDir(), "sandbox"), { recursive: true });
11064
- const dir2 = mkdtempSync(join28(cabaneDir(), "sandbox", "turn-"));
11065
- const script = join28(dir2, "exec-sandboxed");
11063
+ mkdirSync17(join29(cabaneDir(), "sandbox"), { recursive: true });
11064
+ const dir2 = mkdtempSync(join29(cabaneDir(), "sandbox", "turn-"));
11065
+ const script = join29(dir2, "exec-sandboxed");
11066
11066
  writeFileSync10(
11067
11067
  script,
11068
11068
  [
@@ -11078,7 +11078,7 @@ async function prepareHarnessSandbox(input) {
11078
11078
  return {
11079
11079
  wrap: (command, args) => ({ command: script, args: [command, ...args] }),
11080
11080
  executableFor: (real) => {
11081
- const shim = join28(dir2, `exec-${++shims}`);
11081
+ const shim = join29(dir2, `exec-${++shims}`);
11082
11082
  writeFileSync10(shim, `#!/bin/bash
11083
11083
  exec ${shellQuote(script)} ${shellQuote(real)} "$@"
11084
11084
  `);
@@ -11166,12 +11166,12 @@ import {
11166
11166
  existsSync as existsSync19,
11167
11167
  mkdirSync as mkdirSync18,
11168
11168
  readdirSync as readdirSync6,
11169
- readFileSync as readFileSync16,
11169
+ readFileSync as readFileSync17,
11170
11170
  renameSync as renameSync12,
11171
11171
  rmSync as rmSync15,
11172
11172
  writeFileSync as writeFileSync11
11173
11173
  } from "fs";
11174
- import { join as join29 } from "path";
11174
+ import { join as join30 } from "path";
11175
11175
  var MAX_ENTRIES, MAX_AGE_MS, Outbox;
11176
11176
  var init_outbox = __esm({
11177
11177
  "src/outbox.ts"() {
@@ -11189,10 +11189,10 @@ var init_outbox = __esm({
11189
11189
  // Resolved lazily (per call) so tests that swap HOME between
11190
11190
  // cases route writes at the right tmpdir.
11191
11191
  dir() {
11192
- return join29(runnerStateDir(), "outbox", encodeURIComponent(this.workspaceId));
11192
+ return join30(runnerStateDir(), "outbox", encodeURIComponent(this.workspaceId));
11193
11193
  }
11194
11194
  fileFor(turnId, seq) {
11195
- return join29(this.dir(), `${encodeURIComponent(turnId)}__${seq}.json`);
11195
+ return join30(this.dir(), `${encodeURIComponent(turnId)}__${seq}.json`);
11196
11196
  }
11197
11197
  // Persist a commit for later draining. Atomic (temp file + rename) so a
11198
11198
  // concurrent `list()` never reads a half-written entry, then enforces the
@@ -11234,9 +11234,9 @@ var init_outbox = __esm({
11234
11234
  const entries = [];
11235
11235
  for (const name of names) {
11236
11236
  if (!name.endsWith(".json")) continue;
11237
- const full = join29(dir2, name);
11237
+ const full = join30(dir2, name);
11238
11238
  try {
11239
- const parsed = JSON.parse(readFileSync16(full, "utf8"));
11239
+ const parsed = JSON.parse(readFileSync17(full, "utf8"));
11240
11240
  if (parsed && typeof parsed.turnId === "string" && typeof parsed.seq === "number" && typeof parsed.path === "string") {
11241
11241
  entries.push(parsed);
11242
11242
  } else {
@@ -11363,7 +11363,7 @@ var init_image_manifest = __esm({
11363
11363
 
11364
11364
  // src/runner/exec-shim.ts
11365
11365
  import { chmodSync as chmodSync8, mkdirSync as mkdirSync20, mkdtempSync as mkdtempSync2, rmSync as rmSync17, writeFileSync as writeFileSync13 } from "fs";
11366
- import { join as join33 } from "path";
11366
+ import { join as join34 } from "path";
11367
11367
  function shellQuote2(value) {
11368
11368
  return `'${value.replace(/'/g, `'\\''`)}'`;
11369
11369
  }
@@ -11373,10 +11373,10 @@ function createShimDir() {
11373
11373
  return {
11374
11374
  write(argv) {
11375
11375
  if (!dir2) {
11376
- mkdirSync20(join33(cabaneDir(), "runner"), { recursive: true });
11377
- dir2 = mkdtempSync2(join33(cabaneDir(), "runner", "turn-"));
11376
+ mkdirSync20(join34(cabaneDir(), "runner"), { recursive: true });
11377
+ dir2 = mkdtempSync2(join34(cabaneDir(), "runner", "turn-"));
11378
11378
  }
11379
- const path = join33(dir2, `exec-${++count}`);
11379
+ const path = join34(dir2, `exec-${++count}`);
11380
11380
  writeFileSync13(path, `#!/bin/sh
11381
11381
  exec ${argv.map(shellQuote2).join(" ")} "$@"
11382
11382
  `);
@@ -11486,7 +11486,7 @@ __export(run_turn_exports, {
11486
11486
  runnerLogger: () => runnerLogger,
11487
11487
  stopWithLauncher: () => stopWithLauncher
11488
11488
  });
11489
- import { spawnSync } from "child_process";
11489
+ import { spawnSync as spawnSync2 } from "child_process";
11490
11490
  import pino2 from "pino";
11491
11491
  async function drainBeforeExit(outbox, api, graceMs = OUTBOX_GRACE_MS, log2) {
11492
11492
  if (outbox.size() === 0) return;
@@ -11595,7 +11595,7 @@ async function stopWithLauncher(groups, deps = {}) {
11595
11595
  process.stdout.on("error", () => {
11596
11596
  });
11597
11597
  const ownGroup = () => {
11598
- const ps = spawnSync("ps", ["-eo", "pid=,pgid="], { timeout: 5e3 });
11598
+ const ps = spawnSync2("ps", ["-eo", "pid=,pgid="], { timeout: 5e3 });
11599
11599
  if (ps.status !== 0) return null;
11600
11600
  return String(ps.stdout).split("\n").map((line) => /^\s*(\d+)\s+(\d+)\s*$/.exec(line)).filter((m) => m !== null).filter((m) => Number(m[2]) === process.pid).map((m) => Number(m[1])).filter((pid) => pid !== process.pid && pid !== ps.pid);
11601
11601
  };
@@ -13004,6 +13004,96 @@ function plainUi() {
13004
13004
  };
13005
13005
  }
13006
13006
 
13007
+ // src/stray-companion.ts
13008
+ init_config();
13009
+ import { spawnSync } from "child_process";
13010
+ import { readFileSync as readFileSync4 } from "fs";
13011
+ import { join as join10 } from "path";
13012
+ var COMPANION_ENTRY = /@cabane\/companion\/|cabane-companion|(^|[\s/])cabane\s|\.cabane\/(versions|current|boot-guard)/;
13013
+ var START = /\sstart(\s|$)/;
13014
+ function companionStarts(psOutput) {
13015
+ const found = [];
13016
+ for (const line of psOutput.split("\n")) {
13017
+ const match = /^\s*(\d+)\s+(.*)$/.exec(line);
13018
+ if (!match) continue;
13019
+ const command = match[2];
13020
+ if (COMPANION_ENTRY.test(command) && START.test(command)) {
13021
+ found.push({ pid: Number(match[1]), command });
13022
+ }
13023
+ }
13024
+ return found;
13025
+ }
13026
+ function stateDirOf(env) {
13027
+ const home = env.CABANE_COMPANION_HOME || env.HOME;
13028
+ return home ? join10(home, ".cabane") : null;
13029
+ }
13030
+ function envFromPsE(output, command) {
13031
+ const line = output.replace(/\n$/, "");
13032
+ if (!line.startsWith(command)) return {};
13033
+ const env = {};
13034
+ for (const entry of line.slice(command.length).trim().split(/ (?=[A-Za-z_][A-Za-z0-9_]*=)/)) {
13035
+ const eq = entry.indexOf("=");
13036
+ if (eq > 0) env[entry.slice(0, eq)] = entry.slice(eq + 1);
13037
+ }
13038
+ return env;
13039
+ }
13040
+ function envFromProc(pid) {
13041
+ try {
13042
+ const env = {};
13043
+ for (const entry of readFileSync4(`/proc/${pid}/environ`, "utf8").split("\0")) {
13044
+ const eq = entry.indexOf("=");
13045
+ if (eq > 0) env[entry.slice(0, eq)] = entry.slice(eq + 1);
13046
+ }
13047
+ return env;
13048
+ } catch {
13049
+ return null;
13050
+ }
13051
+ }
13052
+ function envOf(pid, command) {
13053
+ if (process.platform === "linux") return envFromProc(pid);
13054
+ const result = spawnSync("ps", ["-E", "-ww", "-o", "command=", "-p", String(pid)], {
13055
+ encoding: "utf8",
13056
+ timeout: 5e3
13057
+ });
13058
+ if (result.error || result.status !== 0) return null;
13059
+ return envFromPsE(result.stdout, command);
13060
+ }
13061
+ function findStrayCompanions(exclude = [], deps = {}) {
13062
+ if (!deps.ps && (process.platform === "win32" || typeof process.getuid !== "function")) {
13063
+ return [];
13064
+ }
13065
+ const ps = deps.ps ?? (() => {
13066
+ const result = spawnSync("ps", ["-U", String(process.getuid()), "-o", "pid=,command="], {
13067
+ encoding: "utf8",
13068
+ timeout: 5e3,
13069
+ maxBuffer: 16 * 1024 * 1024
13070
+ });
13071
+ return result.error || typeof result.stdout !== "string" ? null : result.stdout;
13072
+ });
13073
+ const listing = ps();
13074
+ if (!listing) return [];
13075
+ const dir2 = deps.dir ?? cabaneDir();
13076
+ const skip = /* @__PURE__ */ new Set([...deps.self ?? [process.pid, process.ppid], ...exclude]);
13077
+ return companionStarts(listing).filter((c) => {
13078
+ if (skip.has(c.pid)) return false;
13079
+ const env = (deps.env ?? envOf)(c.pid, c.command);
13080
+ return env !== null && stateDirOf(env) === dir2;
13081
+ });
13082
+ }
13083
+ var SERVICE_ENTRY = /\.cabane\/boot-guard/;
13084
+ function strayCompanionLines(strays, retry) {
13085
+ const one = strays.length === 1;
13086
+ return [
13087
+ `Another Cabane companion is running for this computer's ~/.cabane, and ~/.cabane has no record of it:`,
13088
+ ...strays.map((s) => ` pid ${s.pid} ${s.command}`),
13089
+ `Two companions for one ~/.cabane sign each other's agents out. Stop ${one ? "it" : "them"} first:`,
13090
+ ...strays.map(
13091
+ (s) => SERVICE_ENTRY.test(s.command) ? ` pid ${s.pid} is a service, which restarts if killed: cabane service uninstall` : ` kill ${s.pid}`
13092
+ ),
13093
+ `then run ${retry} again.`
13094
+ ];
13095
+ }
13096
+
13007
13097
  // src/service.ts
13008
13098
  init_config();
13009
13099
  import { createHash as createHash2 } from "crypto";
@@ -13012,13 +13102,13 @@ import {
13012
13102
  cpSync,
13013
13103
  existsSync as existsSync8,
13014
13104
  mkdirSync as mkdirSync9,
13015
- readFileSync as readFileSync7,
13105
+ readFileSync as readFileSync8,
13016
13106
  renameSync as renameSync7,
13017
13107
  rmSync as rmSync7,
13018
13108
  writeFileSync as writeFileSync5
13019
13109
  } from "fs";
13020
13110
  import { homedir as homedir7, userInfo as userInfo2 } from "os";
13021
- import { dirname as dirname10, join as join14, sep as sep3 } from "path";
13111
+ import { dirname as dirname10, join as join15, sep as sep3 } from "path";
13022
13112
 
13023
13113
  // src/install-layout.ts
13024
13114
  init_config();
@@ -13026,39 +13116,39 @@ import {
13026
13116
  existsSync as existsSync6,
13027
13117
  lstatSync,
13028
13118
  mkdirSync as mkdirSync5,
13029
- readFileSync as readFileSync4,
13119
+ readFileSync as readFileSync5,
13030
13120
  readdirSync,
13031
13121
  realpathSync as realpathSync4,
13032
13122
  renameSync as renameSync3,
13033
13123
  rmSync as rmSync4,
13034
13124
  symlinkSync
13035
13125
  } from "fs";
13036
- import { dirname as dirname6, join as join10 } from "path";
13126
+ import { dirname as dirname6, join as join11 } from "path";
13037
13127
  import { fileURLToPath as fileURLToPath4 } from "url";
13038
13128
  function versionsDir() {
13039
- return join10(cabaneDir(), "versions");
13129
+ return join11(cabaneDir(), "versions");
13040
13130
  }
13041
13131
  function currentLink() {
13042
- return join10(cabaneDir(), "current");
13132
+ return join11(cabaneDir(), "current");
13043
13133
  }
13044
13134
  function bootGuardPath() {
13045
- return join10(cabaneDir(), "boot-guard.mjs");
13135
+ return join11(cabaneDir(), "boot-guard.mjs");
13046
13136
  }
13047
13137
  function pinnedNodeDir() {
13048
- return join10(cabaneDir(), "node");
13138
+ return join11(cabaneDir(), "node");
13049
13139
  }
13050
13140
  function pinnedNodePath() {
13051
- return join10(pinnedNodeDir(), "bin", "node");
13141
+ return join11(pinnedNodeDir(), "bin", "node");
13052
13142
  }
13053
13143
  function versionPackageRoot(version) {
13054
- return join10(versionsDir(), version, "node_modules", "@cabane", "companion");
13144
+ return join11(versionsDir(), version, "node_modules", "@cabane", "companion");
13055
13145
  }
13056
13146
  function ownPackageRoot(moduleUrl = import.meta.url) {
13057
13147
  return dirname6(dirname6(fileURLToPath4(moduleUrl)));
13058
13148
  }
13059
13149
  function packageVersion(root) {
13060
13150
  try {
13061
- const pkg2 = JSON.parse(readFileSync4(join10(root, "package.json"), "utf8"));
13151
+ const pkg2 = JSON.parse(readFileSync5(join11(root, "package.json"), "utf8"));
13062
13152
  return typeof pkg2.version === "string" ? pkg2.version : null;
13063
13153
  } catch {
13064
13154
  return null;
@@ -13067,7 +13157,7 @@ function packageVersion(root) {
13067
13157
  function currentRoot() {
13068
13158
  try {
13069
13159
  const root = realpathSync4(currentLink());
13070
- return existsSync6(join10(root, "dist", "cli.js")) ? root : null;
13160
+ return existsSync6(join11(root, "dist", "cli.js")) ? root : null;
13071
13161
  } catch {
13072
13162
  return null;
13073
13163
  }
@@ -13090,7 +13180,7 @@ function pruneVersions(keep) {
13090
13180
  const removed = [];
13091
13181
  for (const name of readdirSync(dir2)) {
13092
13182
  if (keep.includes(name)) continue;
13093
- rmSync4(join10(dir2, name), { recursive: true, force: true });
13183
+ rmSync4(join11(dir2, name), { recursive: true, force: true });
13094
13184
  removed.push(name);
13095
13185
  }
13096
13186
  return removed;
@@ -13110,19 +13200,19 @@ function compareVersions(a, b) {
13110
13200
  // src/updater.ts
13111
13201
  import { spawn as spawn4 } from "child_process";
13112
13202
  import { copyFileSync, existsSync as existsSync7, mkdirSync as mkdirSync8, renameSync as renameSync6, rmSync as rmSync6 } from "fs";
13113
- import { dirname as dirname9, join as join13 } from "path";
13203
+ import { dirname as dirname9, join as join14 } from "path";
13114
13204
 
13115
13205
  // src/boot-rule.ts
13116
13206
  import {
13117
13207
  mkdirSync as mkdirSync6,
13118
- readFileSync as readFileSync5,
13208
+ readFileSync as readFileSync6,
13119
13209
  realpathSync as realpathSync5,
13120
13210
  renameSync as renameSync4,
13121
13211
  rmSync as rmSync5,
13122
13212
  symlinkSync as symlinkSync2,
13123
13213
  writeFileSync as writeFileSync3
13124
13214
  } from "fs";
13125
- import { dirname as dirname7, join as join11 } from "path";
13215
+ import { dirname as dirname7, join as join12 } from "path";
13126
13216
  var UNCONFIRMED_WINDOW_MS = 10 * 6e4;
13127
13217
  var MAX_STAGE_ATTEMPTS = 3;
13128
13218
  function superseded(pending, ...running) {
@@ -13145,8 +13235,8 @@ function rolledBackState(state, pending, reason, now) {
13145
13235
 
13146
13236
  // src/update-state.ts
13147
13237
  init_config();
13148
- import { mkdirSync as mkdirSync7, readFileSync as readFileSync6, renameSync as renameSync5, writeFileSync as writeFileSync4 } from "fs";
13149
- import { dirname as dirname8, join as join12 } from "path";
13238
+ import { mkdirSync as mkdirSync7, readFileSync as readFileSync7, renameSync as renameSync5, writeFileSync as writeFileSync4 } from "fs";
13239
+ import { dirname as dirname8, join as join13 } from "path";
13150
13240
  import { z as z3 } from "zod";
13151
13241
  var resultSchema = z3.object({
13152
13242
  kind: z3.enum(["updated", "rolled_back", "failed"]),
@@ -13170,11 +13260,11 @@ var stateSchema = z3.object({
13170
13260
  failed: z3.object({ version: z3.string(), attempts: z3.number().int(), at: z3.string() }).optional()
13171
13261
  });
13172
13262
  function updateStatePath(dir2 = cabaneDir()) {
13173
- return join12(dir2, "update.json");
13263
+ return join13(dir2, "update.json");
13174
13264
  }
13175
13265
  function readUpdateState(path = updateStatePath()) {
13176
13266
  try {
13177
- const parsed = stateSchema.safeParse(JSON.parse(readFileSync6(path, "utf8")));
13267
+ const parsed = stateSchema.safeParse(JSON.parse(readFileSync7(path, "utf8")));
13178
13268
  return parsed.success ? parsed.data : {};
13179
13269
  } catch {
13180
13270
  return {};
@@ -13219,7 +13309,7 @@ function rollBack(pending, reason, now = /* @__PURE__ */ new Date()) {
13219
13309
  return updateUpdateState((state) => rolledBackState(state, pending, reason, now));
13220
13310
  }
13221
13311
  function writeBootGuard(root = ownPackageRoot()) {
13222
- const built = join13(root, "dist", "boot-guard.js");
13312
+ const built = join14(root, "dist", "boot-guard.js");
13223
13313
  if (!existsSync7(built)) return false;
13224
13314
  const target = bootGuardPath();
13225
13315
  mkdirSync8(dirname9(target), { recursive: true });
@@ -13259,17 +13349,17 @@ var runProcess = (command, args, opts = {}) => new Promise((resolve2) => {
13259
13349
  async function stageVersion(version, harnesses, deps = {}) {
13260
13350
  const run = deps.run ?? runProcess;
13261
13351
  const node = deps.execPath ?? process.execPath;
13262
- const target = join13(versionsDir(), version);
13352
+ const target = join14(versionsDir(), version);
13263
13353
  const placed = versionPackageRoot(version);
13264
13354
  if (existsSync7(placed)) {
13265
13355
  if (!await verifyInstall(placed, version, harnesses, { run, execPath: node }))
13266
13356
  return { ok: true, root: placed };
13267
13357
  }
13268
- const npm = join13(dirname9(node), "npm");
13358
+ const npm = join14(dirname9(node), "npm");
13269
13359
  if (!existsSync7(npm))
13270
13360
  return { ok: false, reason: `there is no npm beside Node at ${dirname9(node)}` };
13271
13361
  mkdirSync8(versionsDir(), { recursive: true });
13272
- const staging = join13(versionsDir(), `.${version}.staging-${process.pid}`);
13362
+ const staging = join14(versionsDir(), `.${version}.staging-${process.pid}`);
13273
13363
  rmSync6(staging, { recursive: true, force: true });
13274
13364
  const installed = await run(
13275
13365
  npm,
@@ -13298,7 +13388,7 @@ async function stageVersion(version, harnesses, deps = {}) {
13298
13388
  return { ok: false, reason: `version ${version} isn't published yet`, unpublished: true };
13299
13389
  return { ok: false, reason: `npm couldn't install it: ${lastLine(installed.stderr)}` };
13300
13390
  }
13301
- const root = join13(staging, "node_modules", "@cabane", "companion");
13391
+ const root = join14(staging, "node_modules", "@cabane", "companion");
13302
13392
  const verdict = await verifyInstall(root, version, harnesses, { run, execPath: node });
13303
13393
  if (verdict) {
13304
13394
  rmSync6(staging, { recursive: true, force: true });
@@ -13310,7 +13400,7 @@ async function stageVersion(version, harnesses, deps = {}) {
13310
13400
  }
13311
13401
  async function verifyInstall(root, version, harnesses, deps) {
13312
13402
  if (packageVersion(root) !== version) return `the installed package isn't version ${version}`;
13313
- const answered = await deps.run(deps.execPath, [join13(root, "dist", "bin.js"), "--version"], {
13403
+ const answered = await deps.run(deps.execPath, [join14(root, "dist", "bin.js"), "--version"], {
13314
13404
  timeoutMs: 6e4,
13315
13405
  env: { ...process.env, CABANE_LAUNCH_SELF: "1" }
13316
13406
  });
@@ -13318,7 +13408,7 @@ async function verifyInstall(root, version, harnesses, deps) {
13318
13408
  return `it doesn't start: ${lastLine(answered.stderr) || "no version printed"}`;
13319
13409
  }
13320
13410
  if (harnesses.length === 0) return null;
13321
- const probed = await deps.run(deps.execPath, [join13(root, "dist", "postinstall.js"), "--json"], {
13411
+ const probed = await deps.run(deps.execPath, [join14(root, "dist", "postinstall.js"), "--json"], {
13322
13412
  timeoutMs: 6e4
13323
13413
  });
13324
13414
  let resolutions = {};
@@ -13606,15 +13696,15 @@ function serviceNode(execPath = process.execPath) {
13606
13696
  return existsSync8(pinnedNodePath()) ? pinnedNodePath() : execPath;
13607
13697
  }
13608
13698
  function servicePaths(platform, env = process.env, home = homedir7(), node = serviceNode()) {
13609
- const unitFile = platform === "systemd" ? join14(
13610
- env.XDG_CONFIG_HOME || join14(home, ".config"),
13699
+ const unitFile = platform === "systemd" ? join15(
13700
+ env.XDG_CONFIG_HOME || join15(home, ".config"),
13611
13701
  "systemd",
13612
13702
  "user",
13613
13703
  `${serviceName(env)}.service`
13614
- ) : join14(home, "Library", "LaunchAgents", `${launchdLabel(env)}.plist`);
13704
+ ) : join15(home, "Library", "LaunchAgents", `${launchdLabel(env)}.plist`);
13615
13705
  return {
13616
13706
  unitFile,
13617
- envFile: join14(cabaneDir(), "service.env"),
13707
+ envFile: join15(cabaneDir(), "service.env"),
13618
13708
  node,
13619
13709
  entry: bootGuardPath()
13620
13710
  };
@@ -13681,7 +13771,7 @@ function renderLaunchdPlist(paths, env = process.env, servicePath) {
13681
13771
  " <key>ThrottleInterval</key><integer>5</integer>",
13682
13772
  // The companion writes its own log; the console copy would only grow a second one.
13683
13773
  " <key>StandardOutPath</key><string>/dev/null</string>",
13684
- ` <key>StandardErrorPath</key><string>${xml(join14(cabaneDir(), "service-error.log"))}</string>`,
13774
+ ` <key>StandardErrorPath</key><string>${xml(join15(cabaneDir(), "service-error.log"))}</string>`,
13685
13775
  "</dict>",
13686
13776
  "</plist>",
13687
13777
  ""
@@ -13737,7 +13827,7 @@ async function installService(deps = {}) {
13737
13827
  const paths = servicePaths(platform, env, deps.home, node);
13738
13828
  const servicePath = await loginShellPath(run, env);
13739
13829
  mkdirSync9(dirname10(paths.envFile), { recursive: true });
13740
- const existing = existsSync8(paths.envFile) ? readFileSync7(paths.envFile, "utf8") : "";
13830
+ const existing = existsSync8(paths.envFile) ? readFileSync8(paths.envFile, "utf8") : "";
13741
13831
  writeFileSync5(paths.envFile, withServicePath(existing, servicePath), { mode: 384 });
13742
13832
  chmodSync3(paths.envFile, 384);
13743
13833
  mkdirSync9(dirname10(paths.unitFile), { recursive: true });
@@ -13821,15 +13911,15 @@ async function seedManagedInstall(deps) {
13821
13911
  }
13822
13912
  const target = versionPackageRoot(version);
13823
13913
  if (!existsSync8(target)) {
13824
- const staging = join14(versionsDir(), `.${version}.seed-${process.pid}`);
13914
+ const staging = join15(versionsDir(), `.${version}.seed-${process.pid}`);
13825
13915
  rmSync7(staging, { recursive: true, force: true });
13826
13916
  try {
13827
- cpSync(own, join14(staging, "node_modules", "@cabane", "companion"), {
13917
+ cpSync(own, join15(staging, "node_modules", "@cabane", "companion"), {
13828
13918
  recursive: true,
13829
13919
  verbatimSymlinks: true
13830
13920
  });
13831
- rmSync7(join14(versionsDir(), version), { recursive: true, force: true });
13832
- renameSync7(staging, join14(versionsDir(), version));
13921
+ rmSync7(join15(versionsDir(), version), { recursive: true, force: true });
13922
+ renameSync7(staging, join15(versionsDir(), version));
13833
13923
  } catch (err) {
13834
13924
  rmSync7(staging, { recursive: true, force: true });
13835
13925
  return {
@@ -13840,7 +13930,7 @@ async function seedManagedInstall(deps) {
13840
13930
  }
13841
13931
  const unrunnable = await verifyInstall(target, version, [], deps);
13842
13932
  if (unrunnable) {
13843
- rmSync7(join14(versionsDir(), version), { recursive: true, force: true });
13933
+ rmSync7(join15(versionsDir(), version), { recursive: true, force: true });
13844
13934
  return { ok: false, reason: `The copy of Cabane for the service can't run: ${unrunnable}.` };
13845
13935
  }
13846
13936
  pointCurrentAt(target);
@@ -13852,7 +13942,7 @@ function tildeDir() {
13852
13942
  function unitNode(platform, unitFile) {
13853
13943
  let text;
13854
13944
  try {
13855
- text = readFileSync7(unitFile, "utf8");
13945
+ text = readFileSync8(unitFile, "utf8");
13856
13946
  } catch {
13857
13947
  return null;
13858
13948
  }
@@ -13922,12 +14012,12 @@ function userName() {
13922
14012
  return process.env.USER ?? "";
13923
14013
  }
13924
14014
  }
13925
- function rewriteScriptWrappers(dir2 = join14(cabaneDir(), "bin")) {
14015
+ function rewriteScriptWrappers(dir2 = join15(cabaneDir(), "bin")) {
13926
14016
  for (const name of ["cabane", "cabane-companion"]) {
13927
- const path = join14(dir2, name);
14017
+ const path = join15(dir2, name);
13928
14018
  let text;
13929
14019
  try {
13930
- text = readFileSync7(path, "utf8");
14020
+ text = readFileSync8(path, "utf8");
13931
14021
  } catch {
13932
14022
  continue;
13933
14023
  }
@@ -14339,6 +14429,12 @@ async function setupScript(opts, deps) {
14339
14429
  ui.outro(["Nothing left to set up."]);
14340
14430
  return;
14341
14431
  }
14432
+ const strays = (deps.strays ?? findStrayCompanions)(running ? [running.pid] : []);
14433
+ if (strays.length > 0) {
14434
+ ui.cancel(strayCompanionLines(strays, cabane("setup")).join("\n"));
14435
+ process.exitCode = 1;
14436
+ return;
14437
+ }
14342
14438
  let justPaired = false;
14343
14439
  if (!paired) {
14344
14440
  const device = await (deps.pair ?? pairDevice)({
@@ -14497,41 +14593,52 @@ import {
14497
14593
  chmodSync as chmodSync4,
14498
14594
  existsSync as existsSync9,
14499
14595
  mkdirSync as mkdirSync11,
14500
- readFileSync as readFileSync8,
14596
+ readFileSync as readFileSync9,
14501
14597
  renameSync as renameSync8,
14502
14598
  rmSync as rmSync8,
14503
14599
  writeFileSync as writeFileSync6
14504
14600
  } from "fs";
14505
- import { dirname as dirname11, join as join15 } from "path";
14601
+ import { join as join16 } from "path";
14506
14602
  import { z as z4 } from "zod";
14507
- function credentialsPath() {
14508
- return join15(cabaneDir(), "credentials.json");
14603
+ var credentialMapSchema = z4.record(z4.string(), z4.string());
14604
+ function credentialsDir() {
14605
+ return join16(cabaneDir(), "credentials");
14606
+ }
14607
+ function legacyPath() {
14608
+ return join16(cabaneDir(), "credentials.json");
14509
14609
  }
14510
- var credentialStoreSchema = z4.record(z4.string(), z4.string());
14511
- function load() {
14512
- const path = credentialsPath();
14513
- if (!existsSync9(path)) return {};
14610
+ function devicePath(deviceId) {
14611
+ return join16(credentialsDir(), `${deviceId.replace(/[^A-Za-z0-9_-]/g, "_")}.json`);
14612
+ }
14613
+ function read(path) {
14614
+ if (!existsSync9(path)) return null;
14514
14615
  let raw;
14515
14616
  try {
14516
- raw = readFileSync8(path, "utf8");
14617
+ raw = readFileSync9(path, "utf8");
14517
14618
  } catch {
14518
14619
  return {};
14519
14620
  }
14520
14621
  if (raw.trim().length === 0) return {};
14521
14622
  try {
14522
- const result = credentialStoreSchema.safeParse(JSON.parse(raw));
14623
+ const result = credentialMapSchema.safeParse(JSON.parse(raw));
14523
14624
  return result.success ? result.data : {};
14524
14625
  } catch {
14525
14626
  return {};
14526
14627
  }
14527
14628
  }
14528
- function save(map) {
14529
- const path = credentialsPath();
14530
- mkdirSync11(dirname11(path), { recursive: true });
14531
- try {
14532
- chmodSync4(cabaneDir(), 448);
14533
- } catch {
14629
+ function load(deviceId) {
14630
+ return read(devicePath(deviceId)) ?? read(legacyPath()) ?? {};
14631
+ }
14632
+ function save(deviceId, map) {
14633
+ const dir2 = credentialsDir();
14634
+ mkdirSync11(dir2, { recursive: true });
14635
+ for (const d of [cabaneDir(), dir2]) {
14636
+ try {
14637
+ chmodSync4(d, 448);
14638
+ } catch {
14639
+ }
14534
14640
  }
14641
+ const path = devicePath(deviceId);
14535
14642
  const tmp = `${path}.${process.pid}.tmp`;
14536
14643
  try {
14537
14644
  writeFileSync6(tmp, JSON.stringify(map, null, 2) + "\n", { mode: 384 });
@@ -14548,31 +14655,28 @@ function save(map) {
14548
14655
  throw err;
14549
14656
  }
14550
14657
  }
14551
- function getCredential(agentId) {
14552
- return load()[agentId] ?? null;
14658
+ function getCredential(deviceId, agentId) {
14659
+ return load(deviceId)[agentId] ?? null;
14553
14660
  }
14554
- function setCredential(agentId, pat) {
14555
- const map = load();
14556
- if (map[agentId] === pat) return;
14661
+ function setCredential(deviceId, agentId, pat) {
14662
+ if (read(devicePath(deviceId))?.[agentId] === pat) return;
14663
+ const map = load(deviceId);
14557
14664
  map[agentId] = pat;
14558
- save(map);
14665
+ save(deviceId, map);
14559
14666
  }
14560
- function pruneCredentials(keepAgentIds) {
14561
- const map = load();
14667
+ function pruneCredentials(deviceId, keepAgentIds) {
14562
14668
  const keep = new Set(keepAgentIds);
14563
- let removed = 0;
14564
- for (const id of Object.keys(map)) {
14565
- if (!keep.has(id)) {
14566
- delete map[id];
14567
- removed += 1;
14568
- }
14569
- }
14570
- if (removed > 0) save(map);
14669
+ if (keep.size === 0) return [];
14670
+ const own = read(devicePath(deviceId));
14671
+ const map = own ?? load(deviceId);
14672
+ const removed = Object.keys(map).filter((id) => !keep.has(id));
14673
+ for (const id of removed) delete map[id];
14674
+ if (removed.length > 0 || own === null) save(deviceId, map);
14571
14675
  return removed;
14572
14676
  }
14573
14677
  function clearCredentials() {
14574
- const path = credentialsPath();
14575
- if (existsSync9(path)) writeFileSync6(path, "", { mode: 384 });
14678
+ rmSync8(credentialsDir(), { recursive: true, force: true });
14679
+ if (existsSync9(legacyPath())) writeFileSync6(legacyPath(), "", { mode: 384 });
14576
14680
  }
14577
14681
 
14578
14682
  // src/commands/logout.ts
@@ -14628,7 +14732,7 @@ function writeCompletedPairing(raw) {
14628
14732
  }
14629
14733
 
14630
14734
  // src/cli.ts
14631
- import { readFileSync as readFileSync19 } from "fs";
14735
+ import { readFileSync as readFileSync20 } from "fs";
14632
14736
 
14633
14737
  // src/commands/logs.ts
14634
14738
  init_cabane_command();
@@ -14720,7 +14824,7 @@ import { randomUUID as randomUUID7 } from "crypto";
14720
14824
 
14721
14825
  // src/dashboard/server.ts
14722
14826
  init_errors();
14723
- import { dirname as dirname12, join as join17 } from "path";
14827
+ import { dirname as dirname11, join as join18 } from "path";
14724
14828
  import { fileURLToPath as fileURLToPath5 } from "url";
14725
14829
  import { serve } from "@hono/node-server";
14726
14830
  import { Hono } from "hono";
@@ -14728,7 +14832,7 @@ import { Hono } from "hono";
14728
14832
  // src/dashboard/routes.ts
14729
14833
  import { openSync as openSync4, readSync, closeSync as closeSync3, fstatSync, existsSync as existsSync10 } from "fs";
14730
14834
  import { readFile } from "fs/promises";
14731
- import { extname, join as join16, normalize } from "path";
14835
+ import { extname, join as join17, normalize } from "path";
14732
14836
  import { streamSSE } from "hono/streaming";
14733
14837
 
14734
14838
  // src/state.ts
@@ -14977,13 +15081,13 @@ var CONTENT_TYPES = {
14977
15081
  function registerRoutes(app, deps) {
14978
15082
  const { supervisor, hub, staticDir } = deps;
14979
15083
  app.get("/", async (c) => {
14980
- const html = await readFile(join16(staticDir, "index.html"), "utf8");
15084
+ const html = await readFile(join17(staticDir, "index.html"), "utf8");
14981
15085
  return c.html(html);
14982
15086
  });
14983
15087
  app.get("/static/:file", async (c) => {
14984
15088
  const file = c.req.param("file");
14985
15089
  const safe2 = normalize(file).replace(/^(\.\.[/\\])+/, "");
14986
- const full = join16(staticDir, safe2);
15090
+ const full = join17(staticDir, safe2);
14987
15091
  if (!full.startsWith(staticDir) || !existsSync10(full)) return c.notFound();
14988
15092
  const body = await readFile(full);
14989
15093
  const type = CONTENT_TYPES[extname(full).toLowerCase()] ?? "application/octet-stream";
@@ -15174,7 +15278,7 @@ function isAddrInUse(err) {
15174
15278
  return Boolean(err && typeof err === "object" && "code" in err && err.code === "EADDRINUSE");
15175
15279
  }
15176
15280
  function resolveStaticDir() {
15177
- return join17(dirname12(fileURLToPath5(import.meta.url)), "static");
15281
+ return join18(dirname11(fileURLToPath5(import.meta.url)), "static");
15178
15282
  }
15179
15283
 
15180
15284
  // src/supervisor.ts
@@ -15341,20 +15445,20 @@ init_config();
15341
15445
 
15342
15446
  // src/dispatch-dedupe.ts
15343
15447
  init_config();
15344
- import { mkdirSync as mkdirSync12, readFileSync as readFileSync9, writeFileSync as writeFileSync7, renameSync as renameSync9, existsSync as existsSync11 } from "fs";
15345
- import { join as join18 } from "path";
15448
+ import { mkdirSync as mkdirSync12, readFileSync as readFileSync10, writeFileSync as writeFileSync7, renameSync as renameSync9, existsSync as existsSync11 } from "fs";
15449
+ import { join as join19 } from "path";
15346
15450
  var MAX_IDS = 256;
15347
15451
  function dir(log2) {
15348
- return join18(cabaneDir(), log2);
15452
+ return join19(cabaneDir(), log2);
15349
15453
  }
15350
15454
  function pathFor(log2, workspaceId) {
15351
- return join18(dir(log2), encodeURIComponent(workspaceId));
15455
+ return join19(dir(log2), encodeURIComponent(workspaceId));
15352
15456
  }
15353
15457
  function readIds(log2, workspaceId) {
15354
15458
  const path = pathFor(log2, workspaceId);
15355
15459
  if (!existsSync11(path)) return [];
15356
15460
  try {
15357
- return readFileSync9(path, "utf8").split("\n").map((s) => s.trim()).filter((s) => s.length > 0);
15461
+ return readFileSync10(path, "utf8").split("\n").map((s) => s.trim()).filter((s) => s.length > 0);
15358
15462
  } catch {
15359
15463
  return [];
15360
15464
  }
@@ -15385,17 +15489,17 @@ function markCompleted(workspaceId, eventId) {
15385
15489
  }
15386
15490
  var MAX_RESUME_ATTEMPTS = 3;
15387
15491
  function resumeDir() {
15388
- return join18(cabaneDir(), "resume-attempts");
15492
+ return join19(cabaneDir(), "resume-attempts");
15389
15493
  }
15390
15494
  function resumePathFor(workspaceId) {
15391
- return join18(resumeDir(), encodeURIComponent(workspaceId));
15495
+ return join19(resumeDir(), encodeURIComponent(workspaceId));
15392
15496
  }
15393
15497
  function readResumeCounts(workspaceId) {
15394
15498
  const out = /* @__PURE__ */ new Map();
15395
15499
  const path = resumePathFor(workspaceId);
15396
15500
  if (!existsSync11(path)) return out;
15397
15501
  try {
15398
- for (const line of readFileSync9(path, "utf8").split("\n")) {
15502
+ for (const line of readFileSync10(path, "utf8").split("\n")) {
15399
15503
  const trimmed = line.trim();
15400
15504
  if (!trimmed) continue;
15401
15505
  const tab = trimmed.lastIndexOf(" ");
@@ -15424,10 +15528,10 @@ function bumpResumeAttempt(workspaceId, eventId) {
15424
15528
  return next;
15425
15529
  }
15426
15530
  function turnDir() {
15427
- return join18(cabaneDir(), "turns");
15531
+ return join19(cabaneDir(), "turns");
15428
15532
  }
15429
15533
  function turnPathFor(workspaceId) {
15430
- return join18(turnDir(), encodeURIComponent(workspaceId));
15534
+ return join19(turnDir(), encodeURIComponent(workspaceId));
15431
15535
  }
15432
15536
  var TURN_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
15433
15537
  function readTurnIds(workspaceId) {
@@ -15435,7 +15539,7 @@ function readTurnIds(workspaceId) {
15435
15539
  const path = turnPathFor(workspaceId);
15436
15540
  if (!existsSync11(path)) return out;
15437
15541
  try {
15438
- for (const line of readFileSync9(path, "utf8").split("\n")) {
15542
+ for (const line of readFileSync10(path, "utf8").split("\n")) {
15439
15543
  const trimmed = line.trim();
15440
15544
  if (!trimmed) continue;
15441
15545
  const tab = trimmed.lastIndexOf(" ");
@@ -15655,13 +15759,13 @@ import {
15655
15759
  existsSync as existsSync18,
15656
15760
  mkdirSync as mkdirSync16,
15657
15761
  readdirSync as readdirSync5,
15658
- readFileSync as readFileSync14,
15762
+ readFileSync as readFileSync15,
15659
15763
  renameSync as renameSync11,
15660
15764
  rmSync as rmSync13,
15661
15765
  statSync as statSync4,
15662
15766
  writeFileSync as writeFileSync9
15663
15767
  } from "fs";
15664
- import { join as join27 } from "path";
15768
+ import { join as join28 } from "path";
15665
15769
  import { Writable } from "stream";
15666
15770
 
15667
15771
  // src/providers/docker-cli.ts
@@ -15730,11 +15834,11 @@ import {
15730
15834
  existsSync as existsSync16,
15731
15835
  mkdirSync as mkdirSync15,
15732
15836
  readdirSync as readdirSync4,
15733
- readFileSync as readFileSync13,
15837
+ readFileSync as readFileSync14,
15734
15838
  renameSync as renameSync10,
15735
15839
  rmSync as rmSync12
15736
15840
  } from "fs";
15737
- import { join as join25 } from "path";
15841
+ import { join as join26 } from "path";
15738
15842
  function routes(t) {
15739
15843
  const conv = `/api/workspaces/${t.workspaceId}/conversations/${t.conversationId}`;
15740
15844
  const agent = `${conv}/participants/agents/${t.agentId}`;
@@ -15780,7 +15884,7 @@ function publishStaged(from, to, move = atomicMove) {
15780
15884
  const staged = readdirSync4(from).filter((n) => n.endsWith(".json")).map((name) => {
15781
15885
  let entry = {};
15782
15886
  try {
15783
- entry = JSON.parse(readFileSync13(join25(from, name), "utf8"));
15887
+ entry = JSON.parse(readFileSync14(join26(from, name), "utf8"));
15784
15888
  } catch {
15785
15889
  }
15786
15890
  return { name, entry };
@@ -15793,7 +15897,7 @@ function publishStaged(from, to, move = atomicMove) {
15793
15897
  let published = 0;
15794
15898
  for (const { name } of staged) {
15795
15899
  try {
15796
- move(join25(from, name), join25(to, name));
15900
+ move(join26(from, name), join26(to, name));
15797
15901
  } catch {
15798
15902
  return { published, complete: false };
15799
15903
  }
@@ -15806,7 +15910,7 @@ function publishStaged(from, to, move = atomicMove) {
15806
15910
  // src/providers/runner-base.ts
15807
15911
  import { createHash as createHash4 } from "crypto";
15808
15912
  import { existsSync as existsSync17, realpathSync as realpathSync6 } from "fs";
15809
- import { dirname as dirname14, join as join26, sep as sep4 } from "path";
15913
+ import { dirname as dirname13, join as join27, sep as sep4 } from "path";
15810
15914
  import { fileURLToPath as fileURLToPath6 } from "url";
15811
15915
  var COMPANION_MOUNT = "/opt/cabane/companion";
15812
15916
  var BASE_DOCKERFILE = `FROM node:22-slim
@@ -15852,13 +15956,13 @@ async function ensureBaseImage(docker, image, log2) {
15852
15956
  return true;
15853
15957
  }
15854
15958
  function ownInstall(moduleFile = fileURLToPath6(import.meta.url)) {
15855
- let dir2 = dirname14(moduleFile);
15959
+ let dir2 = dirname13(moduleFile);
15856
15960
  for (let i = 0; i < 4; i++) {
15857
- if (existsSync17(join26(dir2, "dist", "cli.js")) && existsSync17(join26(dir2, "package.json"))) break;
15858
- dir2 = dirname14(dir2);
15961
+ if (existsSync17(join27(dir2, "dist", "cli.js")) && existsSync17(join27(dir2, "package.json"))) break;
15962
+ dir2 = dirname13(dir2);
15859
15963
  }
15860
- if (!existsSync17(join26(dir2, "dist", "cli.js"))) return null;
15861
- const sdk = join26(dir2, "node_modules", "@anthropic-ai", "claude-agent-sdk");
15964
+ if (!existsSync17(join27(dir2, "dist", "cli.js"))) return null;
15965
+ const sdk = join27(dir2, "node_modules", "@anthropic-ai", "claude-agent-sdk");
15862
15966
  if (!existsSync17(sdk)) return null;
15863
15967
  const root = realpathSync6(dir2);
15864
15968
  return realpathSync6(sdk).startsWith(root + sep4) ? root : null;
@@ -15997,39 +16101,39 @@ function networkCreateError(stderr) {
15997
16101
  }
15998
16102
  function sessionArchivePath(deviceDir, attemptId) {
15999
16103
  if (!UUID_RE.test(attemptId)) throw new Error(`docker: not an attempt id: ${attemptId}`);
16000
- return join27(deviceDir, "attempts", attemptId, "session.tar");
16104
+ return join28(deviceDir, "attempts", attemptId, "session.tar");
16001
16105
  }
16002
16106
  function pruneOtherAttempts(deviceDir, pair2, keep) {
16003
- const root = join27(deviceDir, "attempts");
16107
+ const root = join28(deviceDir, "attempts");
16004
16108
  if (!existsSync18(root)) return;
16005
16109
  for (const id of readdirSync5(root)) {
16006
16110
  if (id === keep || !UUID_RE.test(id)) continue;
16007
16111
  let pairOf = "";
16008
16112
  try {
16009
- pairOf = readFileSync14(join27(root, id, "pair"), "utf8");
16113
+ pairOf = readFileSync15(join28(root, id, "pair"), "utf8");
16010
16114
  } catch {
16011
16115
  continue;
16012
16116
  }
16013
16117
  if (pairOf === `${pair2.conversationId}/${pair2.agentId}`) {
16014
- rmSync13(join27(root, id), { recursive: true, force: true });
16118
+ rmSync13(join28(root, id), { recursive: true, force: true });
16015
16119
  }
16016
16120
  }
16017
16121
  }
16018
16122
  var IDLE_SESSION_ARCHIVE_MS = 30 * 24 * 60 * 6e4;
16019
16123
  function pruneIdleSessionArchives(deviceDir, now = Date.now(), maxAgeMs = IDLE_SESSION_ARCHIVE_MS) {
16020
- const root = join27(deviceDir, "attempts");
16124
+ const root = join28(deviceDir, "attempts");
16021
16125
  if (!existsSync18(root)) return [];
16022
16126
  const removed = [];
16023
16127
  for (const id of readdirSync5(root)) {
16024
16128
  if (!UUID_RE.test(id)) continue;
16025
16129
  let kept;
16026
16130
  try {
16027
- kept = statSync4(join27(root, id)).mtimeMs;
16131
+ kept = statSync4(join28(root, id)).mtimeMs;
16028
16132
  } catch {
16029
16133
  continue;
16030
16134
  }
16031
16135
  if (now - kept < maxAgeMs) continue;
16032
- rmSync13(join27(root, id), { recursive: true, force: true });
16136
+ rmSync13(join28(root, id), { recursive: true, force: true });
16033
16137
  removed.push(id);
16034
16138
  }
16035
16139
  return removed;
@@ -16230,11 +16334,11 @@ function createDockerProvider(opts) {
16230
16334
  await must(["network", "disconnect", "-f", other, name], "moving the container");
16231
16335
  if (others.includes(sharedNetwork)) await retireSharedNetwork();
16232
16336
  };
16233
- const socketRoot = opts.socketRoot ?? join27(
16337
+ const socketRoot = opts.socketRoot ?? join28(
16234
16338
  process.env.XDG_RUNTIME_DIR && existsSync18(process.env.XDG_RUNTIME_DIR) ? process.env.XDG_RUNTIME_DIR : tmpdir3(),
16235
16339
  `cabane-runners-${opts.deviceKey}`
16236
16340
  );
16237
- const socketDir = (name) => join27(socketRoot, createHash5("sha256").update(name).digest("hex").slice(0, 12));
16341
+ const socketDir = (name) => join28(socketRoot, createHash5("sha256").update(name).digest("hex").slice(0, 12));
16238
16342
  const create = async (name, attempt) => {
16239
16343
  mkdirSync16(socketRoot, { recursive: true, mode: 448 });
16240
16344
  mkdirSync16(socketDir(name), { recursive: true, mode: 457 });
@@ -16376,10 +16480,10 @@ function createDockerProvider(opts) {
16376
16480
  }
16377
16481
  };
16378
16482
  const removeIn = (name, from, names) => names.length ? execIn(name, ["sh", "-c", 'cd "$1" && shift && rm -f -- "$@"', "sh", from, ...names]) : Promise.resolve(null);
16379
- const pendingPath = (name) => join27(opts.deviceDir, "runners-pending", `${name}.json`);
16483
+ const pendingPath = (name) => join28(opts.deviceDir, "runners-pending", `${name}.json`);
16380
16484
  const readPending = (name) => {
16381
16485
  try {
16382
- const ids = JSON.parse(readFileSync14(pendingPath(name), "utf8"));
16486
+ const ids = JSON.parse(readFileSync15(pendingPath(name), "utf8"));
16383
16487
  return Array.isArray(ids) ? ids.filter((id) => typeof id === "string") : [];
16384
16488
  } catch {
16385
16489
  return [];
@@ -16390,12 +16494,12 @@ function createDockerProvider(opts) {
16390
16494
  rmSync13(pendingPath(name), { force: true });
16391
16495
  return;
16392
16496
  }
16393
- mkdirSync16(join27(opts.deviceDir, "runners-pending"), { recursive: true });
16497
+ mkdirSync16(join28(opts.deviceDir, "runners-pending"), { recursive: true });
16394
16498
  writeFileSync9(pendingPath(name), JSON.stringify([...new Set(ids)]));
16395
16499
  };
16396
16500
  const keepSession = async (name, attempt, attemptId) => {
16397
16501
  const archive = sessionArchivePath(opts.deviceDir, attemptId);
16398
- mkdirSync16(join27(archive, ".."), { recursive: true });
16502
+ mkdirSync16(join28(archive, ".."), { recursive: true });
16399
16503
  const partial = `${archive}.${process.pid}.partial`;
16400
16504
  const file = new CappedFile(partial, sessionCap);
16401
16505
  const r = await execIn(
@@ -16412,7 +16516,7 @@ function createDockerProvider(opts) {
16412
16516
  );
16413
16517
  if (r.code === 0 && !file.over) {
16414
16518
  renameSync11(partial, archive);
16415
- writeFileSync9(join27(archive, "..", "pair"), `${attempt.conversationId}/${attempt.agentId}`);
16519
+ writeFileSync9(join28(archive, "..", "pair"), `${attempt.conversationId}/${attempt.agentId}`);
16416
16520
  pruneOtherAttempts(opts.deviceDir, attempt, attemptId);
16417
16521
  await writeMarker(name, attemptId);
16418
16522
  log2.debug({ container: name, attemptId, bytes: file.bytes }, "runners: session files kept");
@@ -16426,7 +16530,7 @@ function createDockerProvider(opts) {
16426
16530
  file.over ? "runners: the session files are over the size cap and weren't kept; the next new container starts the session over" : "runners: couldn't keep the session files; the next new container starts the session over"
16427
16531
  );
16428
16532
  };
16429
- const stagingDir = (name) => join27(opts.deviceDir, "runners-pending", `${name}.outbox`);
16533
+ const stagingDir = (name) => join28(opts.deviceDir, "runners-pending", `${name}.outbox`);
16430
16534
  const ensureRunning = async (name) => {
16431
16535
  const seen = await docker.run(["inspect", "-f", "{{.State.Running}}", name]);
16432
16536
  if (seen.code !== 0) return false;
@@ -16434,7 +16538,7 @@ function createDockerProvider(opts) {
16434
16538
  return (await docker.run(["start", name])).code === 0;
16435
16539
  };
16436
16540
  const dropArchive = (attemptId) => {
16437
- rmSync13(join27(sessionArchivePath(opts.deviceDir, attemptId), ".."), {
16541
+ rmSync13(join28(sessionArchivePath(opts.deviceDir, attemptId), ".."), {
16438
16542
  recursive: true,
16439
16543
  force: true
16440
16544
  });
@@ -16503,7 +16607,7 @@ function createDockerProvider(opts) {
16503
16607
  if (seen.stdout.trim().split(" ")[0] !== "true")
16504
16608
  await must(["start", name], "starting the container");
16505
16609
  const git = await serveGitCredentials({
16506
- socketPath: join27(socketDir(name), "git.sock"),
16610
+ socketPath: join28(socketDir(name), "git.sock"),
16507
16611
  hosts: opts.gitHosts,
16508
16612
  fill: opts.gitFill,
16509
16613
  log: log2
@@ -16652,15 +16756,15 @@ function createDockerProvider(opts) {
16652
16756
  const transcripts = await readDir(name, `${CONTAINER_STATE}/transcripts`);
16653
16757
  if (transcripts) {
16654
16758
  const kept = transcripts.files.filter((f) => SAFE_NAME_RE.test(f.name));
16655
- if (kept.length) mkdirSync16(join27(opts.hostStateDir, "transcripts"), { recursive: true });
16656
- for (const f of kept) writeFileSync9(join27(opts.hostStateDir, "transcripts", f.name), f.data);
16759
+ if (kept.length) mkdirSync16(join28(opts.hostStateDir, "transcripts"), { recursive: true });
16760
+ for (const f of kept) writeFileSync9(join28(opts.hostStateDir, "transcripts", f.name), f.data);
16657
16761
  await removeIn(
16658
16762
  name,
16659
16763
  `${CONTAINER_STATE}/transcripts`,
16660
16764
  kept.map((f) => f.name)
16661
16765
  );
16662
16766
  }
16663
- const outboxRel = join27("outbox", encodeURIComponent(attempt.agentId));
16767
+ const outboxRel = join28("outbox", encodeURIComponent(attempt.agentId));
16664
16768
  const outbox = await readDir(name, `${CONTAINER_STATE}/${outboxRel}`);
16665
16769
  let unpublished = false;
16666
16770
  if (outbox) {
@@ -16686,7 +16790,7 @@ function createDockerProvider(opts) {
16686
16790
  const staged = stagingDir(name);
16687
16791
  mkdirSync16(staged, { recursive: true });
16688
16792
  writeFileSync9(
16689
- join27(staged, `${encodeURIComponent(entry.turnId)}__${entry.seq}.json`),
16793
+ join28(staged, `${encodeURIComponent(entry.turnId)}__${entry.seq}.json`),
16690
16794
  `${JSON.stringify(entry)}
16691
16795
  `
16692
16796
  );
@@ -16699,7 +16803,7 @@ function createDockerProvider(opts) {
16699
16803
  if (outbox.complete) {
16700
16804
  const published = publishStaged(
16701
16805
  stagingDir(name),
16702
- join27(opts.hostStateDir, outboxRel),
16806
+ join28(opts.hostStateDir, outboxRel),
16703
16807
  opts.moveFile
16704
16808
  );
16705
16809
  queued = published.published;
@@ -17500,7 +17604,7 @@ var CompanionSupervisor = class {
17500
17604
  saveConfig(this.config);
17501
17605
  }
17502
17606
  for (const it of items) {
17503
- if (it.credential) setCredential(it.agentId, it.credential);
17607
+ if (it.credential) setCredential(device.id, it.agentId, it.credential);
17504
17608
  }
17505
17609
  const byWorkspace = /* @__PURE__ */ new Map();
17506
17610
  for (const it of items) {
@@ -17511,8 +17615,17 @@ var CompanionSupervisor = class {
17511
17615
  for (const wsId of [...this.workspaces.keys()]) {
17512
17616
  if (!byWorkspace.has(wsId)) await this.removeWorkspace(wsId);
17513
17617
  }
17514
- for (const [wsId, list] of byWorkspace) this.reconcileWorkspace(wsId, list);
17515
- pruneCredentials(items.map((i) => i.agentId));
17618
+ for (const [wsId, list] of byWorkspace) this.reconcileWorkspace(device.id, wsId, list);
17619
+ const pruned = pruneCredentials(
17620
+ device.id,
17621
+ items.map((i) => i.agentId)
17622
+ );
17623
+ if (pruned.length > 0) {
17624
+ this.log.info(
17625
+ { deviceId: device.id, agentIds: pruned },
17626
+ `Removed the saved sign-in of ${pruned.length === 1 ? "agent" : "agents"} ${pruned.join(", ")}: no longer assigned to this device (${device.id}).`
17627
+ );
17628
+ }
17516
17629
  void this.recoverRuns();
17517
17630
  } finally {
17518
17631
  this.refreshing = false;
@@ -17526,7 +17639,7 @@ var CompanionSupervisor = class {
17526
17639
  }
17527
17640
  }
17528
17641
  // ---- reconciliation ----
17529
- reconcileWorkspace(workspaceId, list) {
17642
+ reconcileWorkspace(deviceId, workspaceId, list) {
17530
17643
  const first = list[0];
17531
17644
  let wr = this.workspaces.get(workspaceId);
17532
17645
  if (!wr) {
@@ -17552,7 +17665,7 @@ var CompanionSupervisor = class {
17552
17665
  if (!desired.has(agentId)) this.removeAgent(wr, agentId);
17553
17666
  }
17554
17667
  for (const it of list) {
17555
- const credential = it.credential ?? getCredential(it.agentId);
17668
+ const credential = it.credential ?? getCredential(deviceId, it.agentId);
17556
17669
  const runConfig = parseRunConfig(
17557
17670
  it.runConfig,
17558
17671
  (m) => this.log.warn(
@@ -18619,10 +18732,10 @@ function handleUncaught(log2, err, origin) {
18619
18732
 
18620
18733
  // src/crash-marker.ts
18621
18734
  init_config();
18622
- import { existsSync as existsSync20, mkdirSync as mkdirSync19, readFileSync as readFileSync17, rmSync as rmSync16, writeFileSync as writeFileSync12 } from "fs";
18623
- import { join as join30 } from "path";
18735
+ import { existsSync as existsSync20, mkdirSync as mkdirSync19, readFileSync as readFileSync18, rmSync as rmSync16, writeFileSync as writeFileSync12 } from "fs";
18736
+ import { join as join31 } from "path";
18624
18737
  function crashMarkerPath() {
18625
- return join30(cabaneDir(), "last-error.json");
18738
+ return join31(cabaneDir(), "last-error.json");
18626
18739
  }
18627
18740
  function recordCrash(rec2) {
18628
18741
  try {
@@ -18665,6 +18778,8 @@ async function createCompanionRuntime(opts = {}) {
18665
18778
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
18666
18779
  const pre = readLiveRuntimeState();
18667
18780
  if (pre && await verifyRuntime(pre) === "stale") clearRuntimeState();
18781
+ const strays = findStrayCompanions(pre ? [pre.pid] : []);
18782
+ if (strays.length > 0) return { ok: false, reason: "stray-companion", strays };
18668
18783
  const claim = acquireRuntimeState({
18669
18784
  pid: process.pid,
18670
18785
  startedAt,
@@ -18844,6 +18959,11 @@ async function startSupervised(opts, interactive, daemon) {
18844
18959
  if (daemon) write(`Running in the background (pid ${process.pid}). Stop with ${cabane("stop")}.`);
18845
18960
  else blank();
18846
18961
  if (!isDevicePaired()) {
18962
+ const strays = findStrayCompanions();
18963
+ if (strays.length > 0) {
18964
+ refuse(strayCompanionLines(strays, cabane("start")));
18965
+ return;
18966
+ }
18847
18967
  const paired = await pairDevice({
18848
18968
  baseUrl: resolvePairBaseUrl(opts.server),
18849
18969
  interactive,
@@ -18879,6 +18999,10 @@ async function bringUp2() {
18879
18999
  // the middle of it would be the same news, worse.
18880
19000
  onReadinessWarning: (message2) => log2.warn(message2)
18881
19001
  });
19002
+ if (!result.ok && result.reason === "stray-companion") {
19003
+ refuse(strayCompanionLines(result.strays, cabane("start")));
19004
+ return null;
19005
+ }
18882
19006
  if (!result.ok) {
18883
19007
  for (const line of alreadyRunningLines(result.existing?.pid)) write(`${INDENT}${line}`);
18884
19008
  return null;
@@ -18925,7 +19049,7 @@ init_install_kind();
18925
19049
  init_cabane_command();
18926
19050
  import { readdirSync as readdirSync7, realpathSync as realpathSync8, existsSync as existsSync21 } from "fs";
18927
19051
  import { homedir as homedir9 } from "os";
18928
- import { dirname as dirname16, join as join31 } from "path";
19052
+ import { dirname as dirname15, join as join32 } from "path";
18929
19053
  init_config();
18930
19054
  init_sandbox_policy();
18931
19055
  init_secrets();
@@ -18991,10 +19115,10 @@ function versionLine() {
18991
19115
  const install = companionInstall();
18992
19116
  const own = installedCommandPath({ install });
18993
19117
  if (install === "script") {
18994
- return `Cabane ${COMPANION_VERSION} \xB7 installed by the install script (${tildePath(join31(homedir9(), ".cabane"))})`;
19118
+ return `Cabane ${COMPANION_VERSION} \xB7 installed by the install script (${tildePath(join32(homedir9(), ".cabane"))})`;
18995
19119
  }
18996
19120
  if (own)
18997
- return `Cabane ${COMPANION_VERSION} \xB7 installed with npm (${tildePath(dirname16(dirname16(own)))})`;
19121
+ return `Cabane ${COMPANION_VERSION} \xB7 installed with npm (${tildePath(dirname15(dirname15(own)))})`;
18998
19122
  return `Cabane ${COMPANION_VERSION}`;
18999
19123
  }
19000
19124
  function otherInstallLines(running) {
@@ -19017,7 +19141,7 @@ function otherInstall(install, own) {
19017
19141
  }
19018
19142
  };
19019
19143
  if (install === "npm") {
19020
- const script = join31(
19144
+ const script = join32(
19021
19145
  homedir9(),
19022
19146
  ".cabane",
19023
19147
  "node",
@@ -19026,12 +19150,12 @@ function otherInstall(install, own) {
19026
19150
  "@cabane",
19027
19151
  "companion"
19028
19152
  );
19029
- return existsSync21(script) ? join31(homedir9(), ".cabane") : null;
19153
+ return existsSync21(script) ? join32(homedir9(), ".cabane") : null;
19030
19154
  }
19031
19155
  const found = firstOnPath("cabane-companion", process.env.PATH);
19032
19156
  if (!found) return null;
19033
- const ownAlias = own ? real(join31(dirname16(own), "cabane-companion")) : null;
19034
- return real(found) === ownAlias ? null : dirname16(found);
19157
+ const ownAlias = own ? real(join32(dirname15(own), "cabane-companion")) : null;
19158
+ return real(found) === ownAlias ? null : dirname15(found);
19035
19159
  }
19036
19160
  async function liveState() {
19037
19161
  const running = readLiveRuntimeState();
@@ -19254,8 +19378,8 @@ init_cabane_command();
19254
19378
  init_errors();
19255
19379
  init_src();
19256
19380
  init_transcript_writer();
19257
- import { existsSync as existsSync22, readFileSync as readFileSync18, readdirSync as readdirSync8 } from "fs";
19258
- import { isAbsolute, join as join32 } from "path";
19381
+ import { existsSync as existsSync22, readFileSync as readFileSync19, readdirSync as readdirSync8 } from "fs";
19382
+ import { isAbsolute, join as join33 } from "path";
19259
19383
  async function transcript(opts = {}) {
19260
19384
  const dir2 = transcriptsDir();
19261
19385
  if (opts.follow) {
@@ -19272,7 +19396,7 @@ async function transcript(opts = {}) {
19272
19396
  process.stdout.write(emptyMessage(dir2));
19273
19397
  return;
19274
19398
  }
19275
- process.stdout.write(renderFile(join32(dir2, newest)) + "\n");
19399
+ process.stdout.write(renderFile(join33(dir2, newest)) + "\n");
19276
19400
  return;
19277
19401
  }
19278
19402
  printList(dir2);
@@ -19355,7 +19479,7 @@ function isComplete(content) {
19355
19479
  async function followTranscripts(dir2) {
19356
19480
  const follower = new TranscriptFollower({
19357
19481
  listFiles: () => listFiles(dir2),
19358
- read: (f) => readFileSync18(join32(dir2, f), "utf8"),
19482
+ read: (f) => readFileSync19(join33(dir2, f), "utf8"),
19359
19483
  write: (s) => process.stdout.write(s),
19360
19484
  // CSI: cursor up `n` lines, then erase from cursor to end of screen.
19361
19485
  clearLines: (n) => process.stdout.write(`\x1B[${n}A\x1B[0J`),
@@ -19395,7 +19519,7 @@ function printList(dir2) {
19395
19519
 
19396
19520
  `);
19397
19521
  for (const f of files.slice(0, 20)) {
19398
- const { meta, outcome } = peek(join32(dir2, f));
19522
+ const { meta, outcome } = peek(join33(dir2, f));
19399
19523
  const when = fmtTime(rec(meta)?.ts);
19400
19524
  const ws = str2(rec(meta)?.workspaceSlug);
19401
19525
  const o = rec(outcome);
@@ -19417,7 +19541,7 @@ function peek(path) {
19417
19541
  let meta;
19418
19542
  let outcome;
19419
19543
  try {
19420
- for (const line of readFileSync18(path, "utf8").split("\n")) {
19544
+ for (const line of readFileSync19(path, "utf8").split("\n")) {
19421
19545
  if (!line.trim()) continue;
19422
19546
  const o = safeParse(line);
19423
19547
  const t = str2(rec(o)?.type);
@@ -19433,10 +19557,10 @@ function resolveTarget(dir2, target) {
19433
19557
  if (existsSync22(target)) return target;
19434
19558
  throw new CompanionError(`no transcript at ${target}.`);
19435
19559
  }
19436
- const exact = join32(dir2, target);
19560
+ const exact = join33(dir2, target);
19437
19561
  if (existsSync22(exact)) return exact;
19438
19562
  const matches = listFiles(dir2).filter((f) => f.includes(target));
19439
- if (matches.length === 1) return join32(dir2, matches[0]);
19563
+ if (matches.length === 1) return join33(dir2, matches[0]);
19440
19564
  if (matches.length === 0) {
19441
19565
  throw new CompanionError(
19442
19566
  `no transcript matching "${target}" in ${dir2}. Run \`${cabane("transcript")}\` to list them.`
@@ -19450,7 +19574,7 @@ function resolveTarget(dir2, target) {
19450
19574
  function renderFile(path) {
19451
19575
  let content;
19452
19576
  try {
19453
- content = readFileSync18(path, "utf8");
19577
+ content = readFileSync19(path, "utf8");
19454
19578
  } catch (err) {
19455
19579
  throw new CompanionError(
19456
19580
  `couldn't read ${path}: ${err instanceof Error ? err.message : String(err)}`
@@ -19628,7 +19752,7 @@ program.command("pair", { hidden: true }).allowExcessArguments(false).option("--
19628
19752
  });
19629
19753
  });
19630
19754
  program.command("write-paired-config", { hidden: true }).description("persist an already-completed device enrollment payload from stdin.").action(() => {
19631
- writeCompletedPairing(readFileSync19(0, "utf8"));
19755
+ writeCompletedPairing(readFileSync20(0, "utf8"));
19632
19756
  });
19633
19757
  program.command("start").description("run Cabane on this device in the background.").option("--foreground", "run attached in this terminal (for terminals and supervisors)").option("--daemon", "run in the background (the default; kept for compatibility)").addOption(new Option("--server <url>").hideHelp()).addOption(
19634
19758
  new Option("--log-level <level>", "info (default), warn, or debug").choices([