@centralu/darwin-arm64 0.1.0-beta.12 → 0.1.0-beta.13

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.
@@ -42881,12 +42881,12 @@ new Anthropic({ apiKey, dangerouslyAllowBrowser: true });
42881
42881
  import { parseArgs } from "node:util";
42882
42882
  import { randomBytes as randomBytes6 } from "node:crypto";
42883
42883
  import { homedir as homedir11 } from "node:os";
42884
- import { dirname as dirname11, join as join31 } from "node:path";
42884
+ import { dirname as dirname11, join as join30 } from "node:path";
42885
42885
  import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync10, writeSync as writeSync3 } from "node:fs";
42886
42886
 
42887
42887
  // packages/protocol/src/brand.ts
42888
42888
  var APP_NAME = "Centralu";
42889
- var APP_VERSION = "0.1.0-beta.12";
42889
+ var APP_VERSION = "0.1.0-beta.13";
42890
42890
  var APP_SLUG = "centralu";
42891
42891
  var CLIENT_INFO = { name: APP_SLUG, title: APP_NAME, version: APP_VERSION };
42892
42892
  var DATA_DIR = ".centralu";
@@ -58125,7 +58125,7 @@ function parseUiPreferences(raw) {
58125
58125
  // packages/protocol/src/machines.ts
58126
58126
  var MACHINE_ID_RE = /^[a-z][a-z0-9-]{0,31}$/;
58127
58127
  var MachineId = external_exports.string().regex(MACHINE_ID_RE, "Not a machine id");
58128
- var MachineStatus = external_exports.enum(["connecting", "connected", "unreachable", "not_running", "versions_differ", "refused"]);
58128
+ var MachineStatus = external_exports.enum(["connecting", "connected", "unreachable", "not_running", "versions_differ", "refused", "starting"]);
58129
58129
  var MachineSide = external_exports.object({
58130
58130
  version: external_exports.string(),
58131
58131
  protocolVersion: external_exports.number().int(),
@@ -58170,7 +58170,14 @@ var MachineInfo = external_exports.object({
58170
58170
  * remote machine (phase 2) need them to match (docs/agent-host.md §4.7).
58171
58171
  */
58172
58172
  localPort: external_exports.number().int().nullable().default(null),
58173
- sameLocalPort: external_exports.boolean().default(false)
58173
+ sameLocalPort: external_exports.boolean().default(false),
58174
+ /**
58175
+ * The hub started the host there, because it found none running (plan §10.9, decision 7), and how:
58176
+ * `detached` lives on whatever happens to the link; `link_bound` runs in the link's own ssh session,
58177
+ * because that machine blocks starting a process through WMI, and ends when the link does (`note`
58178
+ * says why). Null when the host was already running. Additive; an older hub sends nothing
58179
+ */
58180
+ hostStarted: external_exports.object({ how: external_exports.enum(["detached", "link_bound"]).catch("detached"), at: external_exports.number(), note: external_exports.string().nullable().default(null) }).nullable().catch(null).default(null)
58174
58181
  });
58175
58182
 
58176
58183
  // packages/protocol/src/events.ts
@@ -60747,6 +60754,14 @@ var RpcMethods = {
60747
60754
  * question comes again. Refused when the protocols differ.
60748
60755
  */
60749
60756
  "machines.acceptVersions": { params: external_exports.object({ machineId: MachineId }), result: MachineInfo },
60757
+ /**
60758
+ * Stops this host the way a signal would: agents, terminals, command runs and app processes
60759
+ * stopped in order, the store closed, then exit. For `centralu serve --stop` (docs/agent-host.md
60760
+ * §4.7): on Windows a launcher can only be killed, and killing it ends the host without its
60761
+ * shutdown. Answers before the host starts to leave. Refused by a host under the app's keeper,
60762
+ * which the app stops (and which would only start the host again).
60763
+ */
60764
+ "host.stop": { params: external_exports.object({}), result: external_exports.object({ ok: external_exports.literal(true) }) },
60750
60765
  /**
60751
60766
  * The trash (#204). Only the person reaches these: the RPC is the UI's channel, and neither the agents' tools nor
60752
60767
  * the apps' broker has a trash or purge verb. `agents.deleteSession` is the way in.
@@ -62354,15 +62369,15 @@ function buildViewCsp(csp) {
62354
62369
  const frame = sanitizeDomains(csp?.frameDomains);
62355
62370
  const base2 = sanitizeDomains(csp?.baseUriDomains);
62356
62371
  const r = resource.kept.join(" ");
62357
- const join32 = (...parts) => parts.filter(Boolean).join(" ");
62372
+ const join31 = (...parts) => parts.filter(Boolean).join(" ");
62358
62373
  const policy = [
62359
62374
  "default-src 'none'",
62360
- `script-src ${join32("'unsafe-inline'", r)}`,
62361
- `style-src ${join32("'unsafe-inline'", r)}`,
62362
- `img-src ${join32("data: blob:", r)}`,
62363
- `font-src ${join32("data:", r)}`,
62364
- `media-src ${join32("data: blob:", r)}`,
62365
- `worker-src ${join32("blob:", r)}`,
62375
+ `script-src ${join31("'unsafe-inline'", r)}`,
62376
+ `style-src ${join31("'unsafe-inline'", r)}`,
62377
+ `img-src ${join31("data: blob:", r)}`,
62378
+ `font-src ${join31("data:", r)}`,
62379
+ `media-src ${join31("data: blob:", r)}`,
62380
+ `worker-src ${join31("blob:", r)}`,
62366
62381
  `connect-src ${connect2.kept.length ? connect2.kept.join(" ") : "'none'"}`,
62367
62382
  `frame-src ${frame.kept.length ? frame.kept.join(" ") : "'none'"}`,
62368
62383
  "form-action 'none'",
@@ -62891,6 +62906,7 @@ import { devNull } from "node:os";
62891
62906
  import { promisify } from "node:util";
62892
62907
 
62893
62908
  // packages/agent-host/src/tool-launch.ts
62909
+ import { execFileSync as execFileSync2 } from "node:child_process";
62894
62910
  import { existsSync as existsSync3, readFileSync } from "node:fs";
62895
62911
  import { win32 as win323 } from "node:path";
62896
62912
 
@@ -62987,7 +63003,14 @@ var realDeps = () => ({
62987
63003
  platform: process.platform,
62988
63004
  read: (path) => readFileSync(path, "utf8"),
62989
63005
  exists: existsSync3,
62990
- node: process.execPath
63006
+ node: process.execPath,
63007
+ runJs: (script) => {
63008
+ try {
63009
+ return execFileSync2(process.execPath, [script], { encoding: "utf8", timeout: 1e4, windowsHide: true });
63010
+ } catch {
63011
+ return null;
63012
+ }
63013
+ }
62991
63014
  });
62992
63015
  function launchFor(path, deps = realDeps()) {
62993
63016
  const plain = { command: path, args: [] };
@@ -62998,10 +63021,10 @@ function launchFor(path, deps = realDeps()) {
62998
63021
  } catch {
62999
63022
  return plain;
63000
63023
  }
63024
+ const dir = win323.dirname(path);
63001
63025
  const relative10 = shimTarget(text2);
63002
- if (!relative10) return plain;
63003
- const target = win323.resolve(win323.dirname(path), relative10);
63004
- if (!deps.exists(target)) return plain;
63026
+ const target = relative10 ? win323.resolve(dir, relative10) : nodeShimEntry(text2, dir, deps);
63027
+ if (!target || !deps.exists(target)) return plain;
63005
63028
  if (/\.(c|m)?js$/i.test(target)) return { command: deps.node, args: [target] };
63006
63029
  if (/\.exe$/i.test(target)) return { command: target, args: [] };
63007
63030
  return plain;
@@ -63014,6 +63037,20 @@ function shimTarget(text2) {
63014
63037
  }
63015
63038
  return null;
63016
63039
  }
63040
+ function nodeShimEntry(text2, dir, deps) {
63041
+ const started = /^\s*"%NODE_EXE%"\s+"%(\w+)%"\s+%\*\s*$/m.exec(text2)?.[1];
63042
+ if (!started) return null;
63043
+ const set3 = (name) => new RegExp(`^\\s*SET\\s+"${name}=%~dp0\\\\?([^"%]+)"`, "im").exec(text2)?.[1]?.trim() ?? null;
63044
+ const relative10 = set3(started);
63045
+ if (!relative10) return null;
63046
+ const base2 = win323.resolve(dir, relative10);
63047
+ const prefixJs = set3("NPM_PREFIX_JS");
63048
+ if (!prefixJs || !deps.runJs) return base2;
63049
+ const prefix = deps.runJs(win323.resolve(dir, prefixJs))?.trim().split(/\r?\n/).pop()?.trim();
63050
+ if (!prefix || !win323.isAbsolute(prefix)) return base2;
63051
+ const inPrefix = win323.join(prefix, "node_modules", "npm", "bin", win323.basename(base2));
63052
+ return deps.exists(inPrefix) ? inPrefix : base2;
63053
+ }
63017
63054
  function resolveCommand(command, env = process.env, deps = realDeps()) {
63018
63055
  if (deps.platform !== "win32") return { command, args: [] };
63019
63056
  if (/[\\/]/.test(command)) return launchFor(command, deps);
@@ -63103,6 +63140,31 @@ async function runGit(cwd, args, opts = {}) {
63103
63140
  return stdout;
63104
63141
  }
63105
63142
 
63143
+ // packages/agent-host/src/os.ts
63144
+ import { homedir as homedir3 } from "node:os";
63145
+ import { posix as posix2, win32 as win324 } from "node:path";
63146
+ var HOST_PLATFORM = process.platform;
63147
+ function hasPs(platform = HOST_PLATFORM) {
63148
+ return platform !== "win32";
63149
+ }
63150
+ function clonesFiles(platform = HOST_PLATFORM) {
63151
+ return platform === "darwin";
63152
+ }
63153
+ function locksRunningPrograms(platform = HOST_PLATFORM) {
63154
+ return platform === "win32";
63155
+ }
63156
+ function pathsOf(platform = HOST_PLATFORM) {
63157
+ return platform === "win32" ? win324 : posix2;
63158
+ }
63159
+ function installedCopyPath(platform = HOST_PLATFORM, env = process.env, home = homedir3()) {
63160
+ if (platform === "darwin") return `/Applications/${APP_NAME}.app`;
63161
+ if (platform === "win32") {
63162
+ const local = env.LOCALAPPDATA || win324.join(home, "AppData", "Local");
63163
+ return win324.join(local, "Programs", APP_NAME);
63164
+ }
63165
+ return posix2.join(home, ".local/share/applications", `${APP_SLUG}.desktop`);
63166
+ }
63167
+
63106
63168
  // packages/agent-host/src/dev-services/fs.ts
63107
63169
  var MAX_TEXT = 2e6;
63108
63170
  var MAX_IMAGE_PREVIEW = IMAGE_PREVIEW_MAX_BYTES;
@@ -63276,7 +63338,7 @@ async function copyTree(src, dst, clone2 = cloneTree) {
63276
63338
  cpSync(src, dst, { recursive: true, verbatimSymlinks: true });
63277
63339
  }
63278
63340
  async function cloneTree(src, dst) {
63279
- if (process.platform !== "darwin") return false;
63341
+ if (!clonesFiles()) return false;
63280
63342
  return new Promise((done) => {
63281
63343
  const p = spawn("/bin/cp", ["-Rc", src, dst], { stdio: "ignore" });
63282
63344
  p.on("error", () => done(false));
@@ -78556,7 +78618,7 @@ var SseError = class extends Error {
78556
78618
  };
78557
78619
 
78558
78620
  // packages/agent-host/src/dev-services/kill-tree.ts
78559
- import { execFile as execFile2, execFileSync as execFileSync2 } from "node:child_process";
78621
+ import { execFile as execFile2, execFileSync as execFileSync3 } from "node:child_process";
78560
78622
  import { join as join6 } from "node:path";
78561
78623
  import { promisify as promisify2 } from "node:util";
78562
78624
  var KILL_GRACE_MS = 3e3;
@@ -78614,16 +78676,19 @@ function survivorTargets(rows, first, self2) {
78614
78676
  }
78615
78677
  function snapshot() {
78616
78678
  try {
78617
- return parsePs(execFileSync2("ps", ["-A", "-o", "pid=,ppid=,pgid="], { encoding: "utf8", timeout: 2e3 }));
78679
+ return parsePs(execFileSync3("ps", ["-A", "-o", "pid=,ppid=,pgid="], { encoding: "utf8", timeout: 2e3 }));
78618
78680
  } catch {
78619
78681
  return [];
78620
78682
  }
78621
78683
  }
78684
+ function hasProcessGroups(platform = process.platform) {
78685
+ return platform !== "win32";
78686
+ }
78622
78687
  var REAL_OS = {
78623
78688
  platform: process.platform,
78624
78689
  taskkill: (pid) => {
78625
78690
  const exe = join6(process.env.SystemRoot ?? "C:\\Windows", "System32", "taskkill.exe");
78626
- execFileSync2(exe, ["/PID", String(pid), "/T", "/F"], { stdio: "ignore", timeout: 5e3, windowsHide: true });
78691
+ execFileSync3(exe, ["/PID", String(pid), "/T", "/F"], { stdio: "ignore", timeout: 5e3, windowsHide: true });
78627
78692
  }
78628
78693
  };
78629
78694
  function shootWindows(handle, os2) {
@@ -79009,7 +79074,7 @@ var AppProcess = class _AppProcess {
79009
79074
  * has no groups to gain, and its tree is ended with `taskkill /T` (kill-tree.ts). Attached and
79010
79075
  * hidden, the app shares the host's windowless console.
79011
79076
  */
79012
- detached: process.platform !== "win32",
79077
+ detached: hasProcessGroups(),
79013
79078
  windowsHide: true
79014
79079
  });
79015
79080
  const proc = new _AppProcess(child, child.stdio[3] ?? null, log, spec.probeTimeoutMs, spawnedAt);
@@ -79076,7 +79141,7 @@ var AppProcess = class _AppProcess {
79076
79141
  this.signalOwnGroup();
79077
79142
  }
79078
79143
  this.fd3?.destroy();
79079
- const leftovers = process.platform === "win32" && !this.alive ? this.collectLeftovers() : null;
79144
+ const leftovers = !hasProcessGroups() && !this.alive ? this.collectLeftovers() : null;
79080
79145
  this.leftovers = leftovers;
79081
79146
  if (leftovers && opts.awaitTree) await leftovers;
79082
79147
  if (leftovers && !opts.awaitTree) void leftovers.finally(() => this.log.close());
@@ -99264,7 +99329,7 @@ function orchestratorMemory(reader, sessionId) {
99264
99329
  }
99265
99330
 
99266
99331
  // packages/agent-host/src/sessions/manager.ts
99267
- import { homedir as homedir4 } from "node:os";
99332
+ import { homedir as homedir5 } from "node:os";
99268
99333
  import { basename as basename2, isAbsolute as isAbsolute6, join as join20, relative as relative7, resolve as resolve4, sep as sep7 } from "node:path";
99269
99334
  import { existsSync as existsSync14, statSync as statSync9 } from "node:fs";
99270
99335
  import { exec as exec3 } from "node:child_process";
@@ -102482,8 +102547,8 @@ async function sweepAttachments(maxBytes = ATTACHMENTS_MAX_BYTES) {
102482
102547
  // packages/agent-host/src/dev-services/message-image.ts
102483
102548
  import { constants as constants5 } from "node:fs";
102484
102549
  import { open as open2, realpath as realpath3 } from "node:fs/promises";
102485
- import { homedir as homedir3 } from "node:os";
102486
- import { posix as posix2, sep as sep6, win32 as win324 } from "node:path";
102550
+ import { homedir as homedir4 } from "node:os";
102551
+ import { posix as posix3, sep as sep6, win32 as win325 } from "node:path";
102487
102552
  import { fileURLToPath as fileURLToPath4 } from "node:url";
102488
102553
  var OPEN_FLAGS = constants5.O_RDONLY | constants5.O_NONBLOCK | (constants5.O_NOFOLLOW ?? 0);
102489
102554
  var MESSAGE_IMAGE_KINDS = "PNG, JPEG, GIF or WebP";
@@ -102518,7 +102583,7 @@ function mentions(text2, written) {
102518
102583
  }
102519
102584
  return false;
102520
102585
  }
102521
- function resolveWrittenPath(written, cwd, home = homedir3(), p = sep6 === "\\" ? win324 : posix2) {
102586
+ function resolveWrittenPath(written, cwd, home = homedir4(), p = sep6 === "\\" ? win325 : posix3) {
102522
102587
  const windows = p.sep === "\\";
102523
102588
  let abs;
102524
102589
  if (/^file:/i.test(written)) {
@@ -102693,7 +102758,7 @@ var EXTERNAL_LIST_LIMIT = 200;
102693
102758
  var builderKey = (ref) => `${ref.projectId ?? "_user"}/${ref.appId}`;
102694
102759
  var APP_BUILDERS_KEY = "apps.builders";
102695
102760
  var SessionManager = class _SessionManager {
102696
- constructor(store2, adapters2, emit, endpoint, worktreeRoot = join20(homedir4(), DATA_DIR, "worktrees"), keeperOpts = {}) {
102761
+ constructor(store2, adapters2, emit, endpoint, worktreeRoot = join20(homedir5(), DATA_DIR, "worktrees"), keeperOpts = {}) {
102697
102762
  this.store = store2;
102698
102763
  this.adapters = adapters2;
102699
102764
  this.emit = emit;
@@ -106989,11 +107054,10 @@ import { promisify as promisify5 } from "node:util";
106989
107054
  // packages/agent-host/src/cli-version.ts
106990
107055
  import { execFile as execFile4 } from "node:child_process";
106991
107056
  import { readFileSync as readFileSync10, realpathSync as realpathSync5 } from "node:fs";
106992
- import { posix as posix3, win32 as win326 } from "node:path";
106993
107057
  import { promisify as promisify4 } from "node:util";
106994
107058
  var exec4 = promisify4(execFile4);
106995
107059
  var realDeps2 = () => ({
106996
- platform: process.platform,
107060
+ platform: HOST_PLATFORM,
106997
107061
  which: (name) => whichTool(name),
106998
107062
  launch: (path) => launchFor(path),
106999
107063
  realpath: (path) => realpathSync5(path),
@@ -107002,7 +107066,7 @@ var realDeps2 = () => ({
107002
107066
  });
107003
107067
  var PACKAGE_DEPTH = 3;
107004
107068
  function npmPackageVersion(file2, packageName, deps) {
107005
- const path = deps.platform === "win32" ? win326 : posix3;
107069
+ const path = pathsOf(deps.platform);
107006
107070
  let real2 = file2;
107007
107071
  try {
107008
107072
  real2 = deps.realpath(file2);
@@ -107028,7 +107092,7 @@ async function installedCliVersion(name, packageName, deps = realDeps2()) {
107028
107092
  const file2 = launch.args[0] ?? launch.command;
107029
107093
  const fromNpm = npmPackageVersion(file2, packageName, deps);
107030
107094
  if (fromNpm) return fromNpm;
107031
- const path = deps.platform === "win32" ? win326 : posix3;
107095
+ const path = pathsOf(deps.platform);
107032
107096
  let real2 = file2;
107033
107097
  try {
107034
107098
  real2 = deps.realpath(file2);
@@ -107036,7 +107100,7 @@ async function installedCliVersion(name, packageName, deps = realDeps2()) {
107036
107100
  }
107037
107101
  const named = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.exec(path.basename(real2).replace(/\.exe$/i, ""));
107038
107102
  if (named) return named[0];
107039
- if (deps.platform === "win32") return null;
107103
+ if (locksRunningPrograms(deps.platform)) return null;
107040
107104
  try {
107041
107105
  return parseCliVersion(await deps.run(launch.command, [...launch.args, "--version"]));
107042
107106
  } catch {
@@ -107108,6 +107172,9 @@ function claudeKey(exe, stat6, fs2 = realExeFs) {
107108
107172
  const raw = version2 ? `${version2}-${stat6.size}` : `${stat6.size}-${Math.floor(stat6.mtimeMs)}`;
107109
107173
  return raw.replace(/[^A-Za-z0-9._-]/g, "_");
107110
107174
  }
107175
+ function windowsStart(platform = process.platform) {
107176
+ return platform === "win32";
107177
+ }
107111
107178
  var ClaudeLinks = class {
107112
107179
  constructor(opts) {
107113
107180
  this.opts = opts;
@@ -107121,7 +107188,7 @@ var ClaudeLinks = class {
107121
107188
  /** The key new sessions start from: the last one prepared */
107122
107189
  current = null;
107123
107190
  get enabled() {
107124
- return this.opts.platform === "win32";
107191
+ return windowsStart(this.opts.platform);
107125
107192
  }
107126
107193
  base() {
107127
107194
  return join25(this.opts.root(), "tools", "claude");
@@ -107514,7 +107581,6 @@ var LocalAgentProcess = class _LocalAgentProcess extends EventEmitter3 {
107514
107581
  exitReported = false;
107515
107582
  spawnedAt = Date.now();
107516
107583
  static spawn(spec, os2) {
107517
- const platform = os2?.platform ?? process.platform;
107518
107584
  const child = spawn4(spec.command, spec.args, {
107519
107585
  cwd: spec.cwd,
107520
107586
  env: spec.env,
@@ -107523,7 +107589,7 @@ var LocalAgentProcess = class _LocalAgentProcess extends EventEmitter3 {
107523
107589
  * A group of its own, which its helpers join. Not on Windows: there `detached` means a process
107524
107590
  * with no console, whose every console child opens a visible window (app-process.ts).
107525
107591
  */
107526
- detached: platform !== "win32",
107592
+ detached: hasProcessGroups(os2?.platform),
107527
107593
  windowsHide: true
107528
107594
  });
107529
107595
  const p = new _LocalAgentProcess(child, os2);
@@ -107549,8 +107615,9 @@ var LocalAgentProcess = class _LocalAgentProcess extends EventEmitter3 {
107549
107615
  get ended() {
107550
107616
  return this.child.exitCode !== null || this.child.signalCode !== null;
107551
107617
  }
107552
- get platform() {
107553
- return this.os?.platform ?? process.platform;
107618
+ /** Whether the CLI got a group of its own (`hasProcessGroups`) */
107619
+ get grouped() {
107620
+ return hasProcessGroups(this.os?.platform);
107554
107621
  }
107555
107622
  handle() {
107556
107623
  return { pid: this.child.pid, kill: (s) => void this.child.kill(s) };
@@ -107562,7 +107629,7 @@ var LocalAgentProcess = class _LocalAgentProcess extends EventEmitter3 {
107562
107629
  kill(signal = "SIGTERM") {
107563
107630
  if (this.ended || this.child.pid === void 0) return false;
107564
107631
  this.killed = true;
107565
- if (this.platform === "win32" || signal === "SIGKILL") killTree(this.handle(), "SIGKILL", this.os);
107632
+ if (!this.grouped || signal === "SIGKILL") killTree(this.handle(), "SIGKILL", this.os);
107566
107633
  else if (signal === "SIGTERM") stopTree(this.handle(), KILL_GRACE_MS, () => !this.ended, this.os);
107567
107634
  else {
107568
107635
  try {
@@ -107588,7 +107655,7 @@ var LocalAgentProcess = class _LocalAgentProcess extends EventEmitter3 {
107588
107655
  sweep() {
107589
107656
  const pid = this.child.pid;
107590
107657
  if (pid === void 0) return;
107591
- if (this.platform !== "win32") return stopGroup(pid, KILL_GRACE_MS);
107658
+ if (this.grouped) return stopGroup(pid, KILL_GRACE_MS);
107592
107659
  const root3 = { pid, spawnedAt: this.spawnedAt, exitedAt: Date.now() };
107593
107660
  void collectOrphansWindows(root3, this.os?.listProcesses ? { listProcesses: this.os.listProcesses, taskkill: this.os.taskkill } : void 0).then((ended) => {
107594
107661
  if (ended.length > 0) console.error(`[agent] ended ${ended.length} process(es) pid ${pid} left running: ${ended.join(", ")}`);
@@ -109199,18 +109266,19 @@ var ClaudeAdapter = class _ClaudeAdapter {
109199
109266
  tool = "claude";
109200
109267
  launch;
109201
109268
  constructor(options = {}) {
109202
- const platform = options.platform ?? process.platform;
109269
+ const platform = options.platform;
109270
+ const windows = windowsStart(platform);
109203
109271
  const executable = options.executable ?? (() => toolExecutable("claude"));
109204
109272
  this.launch = {
109205
- platform,
109273
+ windows,
109206
109274
  executable,
109207
109275
  links: new ClaudeLinks({ platform, root: options.dataRoot ?? dataRoot, fs: options.fs }),
109208
- gate: new StartGate(options.startGapMs ?? (platform === "win32" ? WINDOWS_START_GAP_MS : 0)),
109276
+ gate: new StartGate(options.startGapMs ?? (windows ? WINDOWS_START_GAP_MS : 0)),
109209
109277
  retryDelay: options.retryDelay ?? (() => 3e3 + Math.floor(Math.random() * 3e3)),
109210
109278
  closing: /* @__PURE__ */ new Set(),
109211
109279
  exitGraceMs: options.exitGraceMs ?? 1500
109212
109280
  };
109213
- if (platform === "win32") {
109281
+ if (windows) {
109214
109282
  this.launch.links.keep(executable());
109215
109283
  this.launch.links.sweep();
109216
109284
  }
@@ -109266,7 +109334,7 @@ var ClaudeAdapter = class _ClaudeAdapter {
109266
109334
  const path = whichTool("claude");
109267
109335
  try {
109268
109336
  let launch = launchFor(path ?? "claude");
109269
- const exe = this.launch.platform === "win32" ? this.launch.executable() : null;
109337
+ const exe = this.launch.windows ? this.launch.executable() : null;
109270
109338
  if (exe && /\.exe$/i.test(exe)) {
109271
109339
  try {
109272
109340
  launch = { command: this.launch.links.prepare(exe).path, args: [] };
@@ -109343,7 +109411,7 @@ var ClaudeAdapter = class _ClaudeAdapter {
109343
109411
  * nothing waits: the SDK sends them SIGTERM on the host's exit, which they handle.
109344
109412
  */
109345
109413
  async settle() {
109346
- if (this.launch.platform !== "win32" || this.launch.closing.size === 0) return;
109414
+ if (!this.launch.windows || this.launch.closing.size === 0) return;
109347
109415
  let timer;
109348
109416
  await Promise.race([
109349
109417
  Promise.allSettled([...this.launch.closing]),
@@ -109373,7 +109441,7 @@ function bridgePath() {
109373
109441
 
109374
109442
  // packages/agent-host/src/adapters/codex/index.ts
109375
109443
  import { existsSync as existsSync17, realpathSync as realpathSync6 } from "node:fs";
109376
- import { homedir as homedir9 } from "node:os";
109444
+ import { homedir as homedir10 } from "node:os";
109377
109445
  import { dirname as dirname9, join as join27, resolve as resolve9 } from "node:path";
109378
109446
  import { promisify as promisify6 } from "node:util";
109379
109447
 
@@ -109525,10 +109593,10 @@ var CodexClient = class {
109525
109593
  import { createReadStream as createReadStream2 } from "node:fs";
109526
109594
  import { readdir as readdir6 } from "node:fs/promises";
109527
109595
  import { createInterface as createInterface2 } from "node:readline";
109528
- import { homedir as homedir5 } from "node:os";
109596
+ import { homedir as homedir6 } from "node:os";
109529
109597
  import { join as join26 } from "node:path";
109530
109598
  var MIN_SUMMARY_CHARS = 200;
109531
- async function findRolloutPath(threadId, sessionsDir = join26(homedir5(), ".codex", "sessions")) {
109599
+ async function findRolloutPath(threadId, sessionsDir = join26(homedir6(), ".codex", "sessions")) {
109532
109600
  try {
109533
109601
  const suffix = `-${threadId}.jsonl`;
109534
109602
  const names = await readdir6(sessionsDir, { recursive: true });
@@ -109761,7 +109829,7 @@ async function imageEventFromDisk(sessionId, path, maxBytes = IMAGE_MAX_BYTES) {
109761
109829
  }
109762
109830
 
109763
109831
  // packages/agent-host/src/adapters/codex/usage-client.ts
109764
- import { homedir as homedir6 } from "node:os";
109832
+ import { homedir as homedir7 } from "node:os";
109765
109833
 
109766
109834
  // packages/agent-host/src/adapters/codex/usage.ts
109767
109835
  var str6 = (v) => typeof v === "string" && v ? v : null;
@@ -109809,7 +109877,7 @@ async function readCodexUsage(command) {
109809
109877
  { onNotification: () => {
109810
109878
  }, onServerRequest: (r) => client.respond(r.id, {}), onExit: () => {
109811
109879
  } },
109812
- { cwd: homedir6(), command }
109880
+ { cwd: homedir7(), command }
109813
109881
  );
109814
109882
  try {
109815
109883
  await client.request("initialize", {
@@ -109830,7 +109898,7 @@ async function readCodexUsage(command) {
109830
109898
  }
109831
109899
 
109832
109900
  // packages/agent-host/src/adapters/codex/models.ts
109833
- import { homedir as homedir7 } from "node:os";
109901
+ import { homedir as homedir8 } from "node:os";
109834
109902
  var MAX_PAGES = 20;
109835
109903
  async function collectModels(fetchPage) {
109836
109904
  const out = [];
@@ -109848,7 +109916,7 @@ async function listCodexModels(command) {
109848
109916
  { onNotification: () => {
109849
109917
  }, onServerRequest: (r) => client.respond(r.id, {}), onExit: () => {
109850
109918
  } },
109851
- { cwd: homedir7(), command }
109919
+ { cwd: homedir8(), command }
109852
109920
  );
109853
109921
  try {
109854
109922
  await client.request("initialize", {
@@ -109908,7 +109976,7 @@ function toModelOptions(data) {
109908
109976
  }
109909
109977
 
109910
109978
  // packages/agent-host/src/adapters/codex/notices.ts
109911
- import { homedir as homedir8 } from "node:os";
109979
+ import { homedir as homedir9 } from "node:os";
109912
109980
  var LABELS = {
109913
109981
  configWarning: "config warning",
109914
109982
  warning: "warning",
@@ -109923,7 +109991,7 @@ var IGNORED_HEAD = /^Codex is ignoring \d+ unrecognized configuration settings?\
109923
109991
  var HYDRATION = /^Full-history hydration is deprecated\b/;
109924
109992
  var API_METHOD = /`?\b(thread|turn|review)\/[a-z]/;
109925
109993
  function tilde(path) {
109926
- const home = homedir8();
109994
+ const home = homedir9();
109927
109995
  return home && path.startsWith(`${home}/`) ? `~${path.slice(home.length)}` : path;
109928
109996
  }
109929
109997
  function ignoredSettings(text2) {
@@ -111601,7 +111669,7 @@ function threadIdOf(res) {
111601
111669
  }
111602
111670
  function codexHome() {
111603
111671
  const custom2 = process.env.CODEX_HOME?.trim();
111604
- return custom2 ? custom2 : join27(homedir9(), ".codex");
111672
+ return custom2 ? custom2 : join27(homedir10(), ".codex");
111605
111673
  }
111606
111674
  var CODEX_VERBOSITIES = ["low", "medium", "high"];
111607
111675
  var CodexAdapter = class {
@@ -112000,7 +112068,7 @@ async function cwdsOf(pids) {
112000
112068
  }
112001
112069
  }
112002
112070
  async function findStrays(roots, selfPid = process.pid, held3 = []) {
112003
- if (process.platform === "win32" || roots.length === 0) return [];
112071
+ if (!hasPs() || roots.length === 0) return [];
112004
112072
  const rows = await psRows();
112005
112073
  const candidates = rows.filter((r) => r.pid > 1 && noTty(r.tty)).map((r) => r.pid);
112006
112074
  const cwdOf = await cwdsOf(candidates);
@@ -112113,7 +112181,7 @@ var toInfo = (h) => ({
112113
112181
  function sessionIdsOf(panels) {
112114
112182
  return panels.flatMap((p) => p.kind === "session" ? [p.sessionId] : []);
112115
112183
  }
112116
- function createRpcHandler(mgr2, adapters2, { terminals: terminals2, updates: updates2, commands, externalApps: externalApps2, views: views2, inlineViews: inlineViews2, heldPids, themes: themes2, agentVersions: agentVersions2, machines } = {}) {
112184
+ function createRpcHandler(mgr2, adapters2, { terminals: terminals2, updates: updates2, commands, externalApps: externalApps2, views: views2, inlineViews: inlineViews2, heldPids, themes: themes2, agentVersions: agentVersions2, machines, stopHost } = {}) {
112117
112185
  const requireThemes = () => {
112118
112186
  if (!themes2) throw Object.assign(new Error("Theme files are unavailable"), { code: "internal" });
112119
112187
  return themes2;
@@ -112706,7 +112774,14 @@ function createRpcHandler(mgr2, adapters2, { terminals: terminals2, updates: upd
112706
112774
  "updates.setAutoApply": async (p) => requireUpdates().setAutoApply(RpcMethods["updates.setAutoApply"].params.parse(p).enabled),
112707
112775
  // Answers once the install has started, not once it has finished — see the note on
112708
112776
  // `updates.apply` in the protocol. The rest arrives as `update_status` events.
112709
- "updates.apply": async () => requireUpdates().apply()
112777
+ "updates.apply": async () => requireUpdates().apply(),
112778
+ "host.stop": async () => {
112779
+ if (!stopHost) {
112780
+ throw Object.assign(new Error("This host runs under the Centralu app, which stops it: quit the app instead"), { code: "internal" });
112781
+ }
112782
+ setTimeout(stopHost, 50);
112783
+ return { ok: true };
112784
+ }
112710
112785
  };
112711
112786
  return async (method, params) => {
112712
112787
  const name = method;
@@ -113243,15 +113318,14 @@ var CommandRunner = class {
113243
113318
  // packages/agent-host/src/updates.ts
113244
113319
  import { execFile as execFile8 } from "node:child_process";
113245
113320
  import { existsSync as existsSync19 } from "node:fs";
113246
- import { homedir as homedir10 } from "node:os";
113247
- import { join as join29 } from "node:path";
113248
113321
  var REGISTRY_URL = `https://registry.npmjs.org/${APP_SLUG}/latest`;
113249
113322
  var CHECK_INTERVAL_MS = 6 * 60 * 60 * 1e3;
113250
113323
  var SCHEDULED_TIMEOUT_MS = 3e3;
113251
113324
  var FORCED_TIMEOUT_MS = 8e3;
113252
113325
  var INSTALL_TIMEOUT_MS = 5 * 60 * 1e3;
113253
- function installedCopyPath() {
113254
- return process.platform === "darwin" ? `/Applications/${APP_NAME}.app` : join29(homedir10(), ".local/share/applications", `${APP_SLUG}.desktop`);
113326
+ function existingInstalledCopy() {
113327
+ const path = installedCopyPath();
113328
+ return existsSync19(path) ? path : null;
113255
113329
  }
113256
113330
  async function fetchLatestFromRegistry(timeoutMs) {
113257
113331
  try {
@@ -113265,12 +113339,23 @@ async function fetchLatestFromRegistry(timeoutMs) {
113265
113339
  return { ok: false, reason: "Could not reach the registry \u2014 check the network" };
113266
113340
  }
113267
113341
  }
113268
- function runCommand(file2, args) {
113269
- return new Promise((resolve10, reject) => {
113270
- execFile8(file2, args, { timeout: INSTALL_TIMEOUT_MS }, (err, _stdout, stderr) => {
113271
- if (!err) return resolve10();
113272
- const detail = String(stderr).trim().split("\n").filter(Boolean).pop();
113273
- reject(new Error(detail || err.message));
113342
+ function commandFor(file2, args, resolve10 = (f) => resolveCommand(f)) {
113343
+ const launch = resolve10(file2);
113344
+ return { file: launch.command, args: [...launch.args, ...args] };
113345
+ }
113346
+ function failureDetail(file2, stderr) {
113347
+ const lines = stderr.split(/\r?\n/).map((l) => l.trim()).filter((l) => l && !/complete log of this run/i.test(l));
113348
+ return (file2 === "npm" ? lines.at(-1) : lines[0]) ?? null;
113349
+ }
113350
+ function runCommand(file2, args, resolve10) {
113351
+ const cmd = commandFor(file2, args, resolve10);
113352
+ return new Promise((done, reject) => {
113353
+ execFile8(cmd.file, cmd.args, { timeout: INSTALL_TIMEOUT_MS, windowsHide: true }, (err, _stdout, stderr) => {
113354
+ if (!err) return done();
113355
+ if (err.code === "ENOENT") {
113356
+ return reject(new Error(`${file2} was not found on PATH (${err.message})`));
113357
+ }
113358
+ reject(new Error(failureDetail(file2, String(stderr)) || err.message));
113274
113359
  });
113275
113360
  });
113276
113361
  }
@@ -113280,6 +113365,7 @@ var UpdateService = class {
113280
113365
  this.deps = {
113281
113366
  fetchLatest: deps.fetchLatest ?? fetchLatestFromRegistry,
113282
113367
  run: deps.run ?? runCommand,
113368
+ installedCopy: deps.installedCopy ?? existingInstalledCopy,
113283
113369
  now: deps.now ?? Date.now,
113284
113370
  // If no place to persist it is given, treat auto-update as on — the same default that main.ts
113285
113371
  // passes in.
@@ -113444,12 +113530,17 @@ var UpdateService = class {
113444
113530
  return this.current();
113445
113531
  }
113446
113532
  async runApply(version2) {
113533
+ let packageInstalled = false;
113534
+ const copy = this.deps.installedCopy();
113447
113535
  try {
113448
113536
  await this.deps.run("npm", ["i", "-g", `${APP_SLUG}@${version2}`]);
113449
- if (existsSync19(installedCopyPath())) await this.deps.run(APP_SLUG, ["install"]);
113537
+ packageInstalled = true;
113538
+ if (copy) await this.deps.run(APP_SLUG, ["install"]);
113450
113539
  this.status = { ...this.status, phase: "restart_required", error: null };
113451
113540
  } catch (e) {
113452
- this.status = { ...this.status, phase: "failed", error: e.message };
113541
+ const reason = e.message;
113542
+ const error51 = packageInstalled ? `npm installed ${version2}, but the copy at ${copy} was not replaced (${reason}). Quit ${APP_NAME}, then run "${APP_SLUG} install" in a terminal.` : reason;
113543
+ this.status = { ...this.status, phase: "failed", error: error51 };
113453
113544
  }
113454
113545
  this.emit();
113455
113546
  }
@@ -113682,9 +113773,9 @@ var AgentVersionService = class {
113682
113773
  };
113683
113774
 
113684
113775
  // packages/agent-host/src/dev-services/instance-lock.ts
113685
- import { execFileSync as execFileSync3 } from "node:child_process";
113776
+ import { execFileSync as execFileSync4 } from "node:child_process";
113686
113777
  import { readFileSync as readFileSync12, unlinkSync as unlinkSync3, writeFileSync as writeFileSync7 } from "node:fs";
113687
- import { dirname as dirname10, join as join30 } from "node:path";
113778
+ import { dirname as dirname10, join as join29 } from "node:path";
113688
113779
  import Database2 from "better-sqlite3";
113689
113780
  var OWNERSHIP_FILE = "host-ownership.sqlite";
113690
113781
  var held = /* @__PURE__ */ new Set();
@@ -113698,9 +113789,9 @@ function alive(pid) {
113698
113789
  }
113699
113790
  }
113700
113791
  function processStartTime(pid) {
113701
- if (process.platform === "win32") return null;
113792
+ if (!hasPs()) return null;
113702
113793
  try {
113703
- const out = execFileSync3("ps", ["-o", "lstart=", "-p", String(pid)], {
113794
+ const out = execFileSync4("ps", ["-o", "lstart=", "-p", String(pid)], {
113704
113795
  encoding: "utf8",
113705
113796
  timeout: 2e3,
113706
113797
  stdio: ["ignore", "pipe", "ignore"],
@@ -113730,10 +113821,10 @@ function stillHeld(holder, startOf) {
113730
113821
  const now = startOf(holder.pid);
113731
113822
  return now === null || now === holder.started;
113732
113823
  }
113733
- function acquireInstanceLock(dbPath2, startOf = processStartTime, legacyFile = process.platform !== "win32") {
113824
+ function acquireInstanceLock(dbPath2, startOf = processStartTime, legacyFile = hasPs()) {
113734
113825
  if (dbPath2 === ":memory:") return { ok: true, release: () => {
113735
113826
  } };
113736
- const lockPath = join30(dirname10(dbPath2), "host.lock");
113827
+ const lockPath = join29(dirname10(dbPath2), "host.lock");
113737
113828
  const readHolder = () => {
113738
113829
  try {
113739
113830
  return parseHolder(readFileSync12(lockPath, "utf8"));
@@ -113741,7 +113832,7 @@ function acquireInstanceLock(dbPath2, startOf = processStartTime, legacyFile = p
113741
113832
  return null;
113742
113833
  }
113743
113834
  };
113744
- const owner = takeOwnership(join30(dirname10(dbPath2), OWNERSHIP_FILE));
113835
+ const owner = takeOwnership(join29(dirname10(dbPath2), OWNERSHIP_FILE));
113745
113836
  if (!owner) {
113746
113837
  const holder2 = readHolder();
113747
113838
  return { ok: false, heldByPid: holder2 && Number.isInteger(holder2.pid) && holder2.pid > 0 ? holder2.pid : null, lockPath };
@@ -114443,6 +114534,8 @@ var LinkedMachine = class {
114443
114534
  error = null;
114444
114535
  versions = null;
114445
114536
  lastConnectedAt = null;
114537
+ /** The host this link started there, if it did (owner decision 7, plan §10.9) */
114538
+ hostStarted = null;
114446
114539
  endpoint = null;
114447
114540
  client = null;
114448
114541
  /** Sessions of the remote the hub shows / hides, by the remote's id. Events of any other session are not passed on */
@@ -114476,7 +114569,8 @@ var LinkedMachine = class {
114476
114569
  versions: this.versions,
114477
114570
  lastConnectedAt: this.lastConnectedAt,
114478
114571
  localPort: this.endpoint && this.endpoint.localPort > 0 ? this.endpoint.localPort : null,
114479
- sameLocalPort: this.endpoint ? this.endpoint.localPort === this.endpoint.line.port : false
114572
+ sameLocalPort: this.endpoint ? this.endpoint.localPort === this.endpoint.line.port : false,
114573
+ hostStarted: this.hostStarted
114480
114574
  };
114481
114575
  }
114482
114576
  // ── RoutedMachine ───────────────────────────────────────────────────────────────────────
@@ -114538,17 +114632,27 @@ var LinkedMachine = class {
114538
114632
  this.client?.close();
114539
114633
  this.client = null;
114540
114634
  this.setStatus("connecting", null);
114541
- let endpoint;
114542
- try {
114543
- endpoint = await this.tunnel.open();
114544
- } catch (err) {
114635
+ let endpoint = await this.openTunnel();
114636
+ if (!endpoint) return;
114637
+ if (!endpoint.line.hostRunning && this.tunnel.startHost) {
114638
+ this.setStatus("starting", null);
114639
+ this.deps.log?.(`[links] ${this.name}: no Centralu host is running there; starting one (centralu serve --detach)`);
114640
+ let started;
114641
+ try {
114642
+ started = await this.tunnel.startHost();
114643
+ } catch (err) {
114644
+ if (this.stopped) return;
114645
+ this.setStatus("not_running", `Centralu could not be started on ${this.name}: ${err.message}`);
114646
+ return this.retryLater();
114647
+ }
114545
114648
  if (this.stopped) return;
114546
- this.setStatus("unreachable", err.message);
114547
- return this.retryLater();
114548
- }
114549
- if (this.stopped) {
114550
- await this.tunnel.close();
114551
- return;
114649
+ this.hostStarted = { ...started, at: Date.now() };
114650
+ this.deps.log?.(
114651
+ started.how === "detached" ? `[links] ${this.name}: started its host; it keeps running after this link ends` : `[links] ${this.name}: its host runs only while this link is up: ${started.note ?? "it cannot be started in the background there"}`
114652
+ );
114653
+ this.setStatus("starting", null);
114654
+ endpoint = await this.openTunnel();
114655
+ if (!endpoint) return;
114552
114656
  }
114553
114657
  this.endpoint = endpoint;
114554
114658
  if (!endpoint.line.hostRunning) {
@@ -114559,6 +114663,23 @@ var LinkedMachine = class {
114559
114663
  if (!mayConnect(versions)) return;
114560
114664
  this.connectClient(endpoint);
114561
114665
  }
114666
+ /** The tunnel's `open`, or null after reporting why not (and when the link was stopped meanwhile) */
114667
+ async openTunnel() {
114668
+ let endpoint;
114669
+ try {
114670
+ endpoint = await this.tunnel.open();
114671
+ } catch (err) {
114672
+ if (this.stopped) return null;
114673
+ this.setStatus("unreachable", err.message);
114674
+ this.retryLater();
114675
+ return null;
114676
+ }
114677
+ if (this.stopped) {
114678
+ await this.tunnel.close();
114679
+ return null;
114680
+ }
114681
+ return endpoint;
114682
+ }
114562
114683
  /** Records the versions, and holds the link at `versions_differ` when they do not allow connecting */
114563
114684
  check(remote) {
114564
114685
  const pending = compareVersions(this.deps.hub, remote, false);
@@ -114943,6 +115064,8 @@ var ROUTES = {
114943
115064
  "machines.add": HUB,
114944
115065
  "machines.remove": HUB,
114945
115066
  "machines.reconnect": HUB,
115067
+ // Stopping a remote host is `centralu serve --stop` over ssh, never a call through the link
115068
+ "host.stop": HUB,
114946
115069
  "machines.acceptVersions": HUB,
114947
115070
  // ── Callbacks of the hub's own agents ───────────────────────────────────────────────────
114948
115071
  "orchestrator.tools": INTERNAL,
@@ -115175,6 +115298,7 @@ function parseConnectionLine(stdout) {
115175
115298
  if (typeof o.port !== "number" || typeof o.token !== "string" || typeof o.protocolVersion !== "number") {
115176
115299
  throw new Error("The remote answered with an incomplete connection line");
115177
115300
  }
115301
+ const install = parseInstall(o.install);
115178
115302
  return {
115179
115303
  v: 1,
115180
115304
  port: o.port,
@@ -115182,12 +115306,38 @@ function parseConnectionLine(stdout) {
115182
115306
  version: typeof o.version === "string" ? o.version : "unknown",
115183
115307
  protocolVersion: o.protocolVersion,
115184
115308
  dataDir: typeof o.dataDir === "string" ? o.dataDir : "",
115185
- hostRunning: o.hostRunning === true
115309
+ hostRunning: o.hostRunning === true,
115310
+ ...install ? { install } : {}
115186
115311
  };
115187
115312
  }
115188
115313
  }
115189
115314
  throw new Error("The remote did not print a connection line; its Centralu may predate `centralu serve` (update it there: npm i -g centralu)");
115190
115315
  }
115316
+ function parseInstall(raw) {
115317
+ if (!raw || typeof raw !== "object") return null;
115318
+ const o = raw;
115319
+ const pointer = (v) => {
115320
+ const p = v;
115321
+ return p && typeof p.version === "string" && typeof p.node === "string" ? { version: p.version, node: p.node } : null;
115322
+ };
115323
+ return { managed: o.managed === true, current: pointer(o.current), previous: pointer(o.previous), node: typeof o.node === "string" ? o.node : null };
115324
+ }
115325
+ function parseDetachLine(stdout) {
115326
+ const lines = stdout.split("\n").map((l) => l.trim()).filter(Boolean);
115327
+ for (let i = lines.length - 1; i >= 0; i--) {
115328
+ let raw;
115329
+ try {
115330
+ raw = JSON.parse(lines[i]);
115331
+ } catch {
115332
+ continue;
115333
+ }
115334
+ const d2 = raw?.detach;
115335
+ if (!d2 || typeof d2 !== "object") continue;
115336
+ if (d2.ok === true) return { ok: true, line: parseConnectionLine(lines[i]), how: typeof d2.how === "string" ? d2.how : null };
115337
+ return { ok: false, reason: typeof d2.reason === "string" ? d2.reason : "failed", message: typeof d2.message === "string" ? d2.message : "centralu serve --detach failed" };
115338
+ }
115339
+ throw new Error("The remote did not say whether it started");
115340
+ }
115191
115341
  var WSL_DISTRO_RE = /^[A-Za-z0-9._-]{1,64}$/;
115192
115342
  function powershell(script) {
115193
115343
  const full = `[Console]::OutputEncoding=[Text.Encoding]::UTF8
@@ -115202,18 +115352,27 @@ function wslKeepAlive(spec) {
115202
115352
  }
115203
115353
  var NOT_FOUND2 = "CENTRALU-NOT-FOUND";
115204
115354
  function connectionCommand(spec) {
115355
+ return remoteCommand(spec, "serve --connection");
115356
+ }
115357
+ function detachCommand(spec) {
115358
+ return remoteCommand(spec, "serve --detach");
115359
+ }
115360
+ function serveCommand(spec) {
115361
+ return remoteCommand(spec, "serve");
115362
+ }
115363
+ function remoteCommand(spec, args) {
115205
115364
  const custom2 = spec.command?.trim() || null;
115206
115365
  if (spec.shell === "powershell") {
115207
- if (custom2) return powershell(`${custom2} serve --connection`);
115366
+ if (custom2) return powershell(`${custom2} ${args}`);
115208
115367
  return powershell(
115209
- `if (Get-Command centralu -ErrorAction SilentlyContinue) { & centralu serve --connection } else { $l = Join-Path $env:USERPROFILE '.centralu\\bin\\centralu.cmd'; if (Test-Path $l) { & $l serve --connection } else { '${NOT_FOUND2}' } }`
115368
+ `if (Get-Command centralu -ErrorAction SilentlyContinue) { & centralu ${args} } else { $l = Join-Path $env:USERPROFILE '.centralu\\bin\\centralu.cmd'; if (Test-Path $l) { & $l ${args} } else { '${NOT_FOUND2}' } }`
115210
115369
  );
115211
115370
  }
115212
- const lookup = `if command -v centralu >/dev/null 2>&1; then exec centralu serve --connection; elif [ -x "$HOME/.centralu/bin/centralu" ]; then exec "$HOME/.centralu/bin/centralu" serve --connection; else echo ${NOT_FOUND2}; exit 127; fi`;
115213
- if (spec.shell === "posix") return custom2 ? `${custom2} serve --connection` : `sh -c '${lookup}'`;
115371
+ const lookup = `if command -v centralu >/dev/null 2>&1; then exec centralu ${args}; elif [ -x "$HOME/.centralu/bin/centralu" ]; then exec "$HOME/.centralu/bin/centralu" ${args}; else echo ${NOT_FOUND2}; exit 127; fi`;
115372
+ if (spec.shell === "posix") return custom2 ? `${custom2} ${args}` : `sh -c '${lookup}'`;
115214
115373
  const distro = spec.wslDistro ?? "";
115215
115374
  if (!WSL_DISTRO_RE.test(distro)) throw new Error(`Not a WSL distro name: ${distro || "(none)"}`);
115216
- const script = custom2 ? `${custom2} serve --connection` : `PATH=$(printf %s "$PATH" | tr : '\\n' | grep -v '^/mnt/' | paste -sd: -); ${lookup}`;
115375
+ const script = custom2 ? `${custom2} ${args}` : `PATH=$(printf %s "$PATH" | tr : '\\n' | grep -v '^/mnt/' | paste -sd: -); ${lookup}`;
115217
115376
  const b64 = Buffer.from(script, "utf8").toString("base64");
115218
115377
  return powershell(`wsl.exe -d '${distro}' -- bash -lc 'echo ${b64} | base64 -d | bash -l'`);
115219
115378
  }
@@ -115228,6 +115387,8 @@ var SshTunnel = class {
115228
115387
  asking = /* @__PURE__ */ new Set();
115229
115388
  downListeners = [];
115230
115389
  closing = false;
115390
+ /** The remote cannot start a host outside a session (WMI blocked): the forward runs it (`startHost`) */
115391
+ linkBound = false;
115231
115392
  async open() {
115232
115393
  await this.stopChild();
115233
115394
  this.closing = false;
@@ -115236,23 +115397,75 @@ var SshTunnel = class {
115236
115397
  throw err;
115237
115398
  });
115238
115399
  this.ifClosed();
115239
- if (!line.hostRunning) {
115400
+ if (!line.hostRunning && !this.linkBound) {
115240
115401
  return { url: "", token: line.token, line, localPort: 0 };
115241
115402
  }
115403
+ const hosting = !line.hostRunning;
115242
115404
  let localPort = await portFree(line.port) ? line.port : await freePort();
115243
115405
  try {
115244
- await this.forward(localPort, line.port);
115406
+ await this.forward(localPort, line.port, hosting);
115245
115407
  } catch (err) {
115246
115408
  if (localPort !== line.port) throw err;
115247
115409
  this.ifClosed();
115248
115410
  localPort = await freePort();
115249
- await this.forward(localPort, line.port);
115411
+ await this.forward(localPort, line.port, hosting);
115250
115412
  }
115251
115413
  if (this.closing) {
115252
115414
  await this.stopChild();
115253
115415
  this.ifClosed();
115254
115416
  }
115255
- return { url: `ws://127.0.0.1:${localPort}`, token: line.token, line, localPort };
115417
+ const served = hosting ? await this.untilServing() : line;
115418
+ return { url: `ws://127.0.0.1:${localPort}`, token: served.token, line: served, localPort };
115419
+ }
115420
+ /**
115421
+ * Starts the remote's host with `centralu serve --detach` (plan §10.4, owner decision 7): it then
115422
+ * runs outside any ssh session and outlives this link. When the remote says WMI process creation
115423
+ * is blocked there (decision 3), every `open` from then on runs `centralu serve` as the forward's
115424
+ * own remote command instead, so the host lives as long as the link.
115425
+ */
115426
+ async startHost() {
115427
+ this.ifClosed();
115428
+ const remote = this.opts.remote ?? { shell: "posix" };
115429
+ const r = await this.run([...this.config(), "-T", "-o", "BatchMode=yes", "-o", "ConnectTimeout=15", "--", this.opts.target, detachCommand(remote)]);
115430
+ this.ifClosed();
115431
+ if (r.code === 255) throw new Error(`ssh could not reach ${this.opts.target}: ${lastLine(r.stderr) || "no answer"}`);
115432
+ if (r.stdout.includes(NOT_FOUND2) || r.code === 127 && !remote.command) {
115433
+ throw new Error(`Centralu is not installed on ${this.opts.target} (run \`npm i -g centralu\` there, then \`centralu serve\` once)`);
115434
+ }
115435
+ let answer;
115436
+ try {
115437
+ answer = parseDetachLine(r.stdout);
115438
+ } catch (err) {
115439
+ if (/unknown option for serve: --detach/.test(r.stderr)) {
115440
+ throw new Error(`Centralu on ${this.opts.target} is too old to be started from here: run \`centralu serve\` there, or update it (npm i -g centralu)`);
115441
+ }
115442
+ const why = lastLine(r.stderr);
115443
+ throw new Error(`${err.message}${why ? `: ${why}` : r.code ? ` (exit ${r.code})` : ""}`);
115444
+ }
115445
+ if (answer.ok) {
115446
+ this.linkBound = false;
115447
+ return { how: "detached", note: null };
115448
+ }
115449
+ if (answer.reason === "wmi_blocked") {
115450
+ this.linkBound = true;
115451
+ return { how: "link_bound", note: answer.message };
115452
+ }
115453
+ throw new Error(answer.message);
115454
+ }
115455
+ /** The connection line again until the link-bound host answers, or the forward that runs it ends */
115456
+ async untilServing() {
115457
+ const deadline = Date.now() + (this.opts.startTimeoutMs ?? 3e4);
115458
+ for (; ; ) {
115459
+ this.ifClosed();
115460
+ if (!this.child) throw new Error(`The ssh session that runs Centralu on ${this.opts.target} ended before it served`);
115461
+ const line = await this.connectionLine();
115462
+ if (line.hostRunning) return line;
115463
+ if (Date.now() > deadline) {
115464
+ await this.stopChild();
115465
+ throw new Error(`Centralu on ${this.opts.target} did not start serving in time`);
115466
+ }
115467
+ await new Promise((r) => setTimeout(r, 500));
115468
+ }
115256
115469
  }
115257
115470
  onDown(listener) {
115258
115471
  this.downListeners.push(listener);
@@ -115296,12 +115509,11 @@ var SshTunnel = class {
115296
115509
  throw new Error(`${err.message}${why ? `: ${why}` : r.code ? ` (exit ${r.code})` : ""}`);
115297
115510
  }
115298
115511
  }
115299
- forward(localPort, remotePort) {
115512
+ forward(localPort, remotePort, hosting = false) {
115300
115513
  this.ifClosed();
115301
- const keepAlive = wslKeepAlive(this.opts.remote);
115514
+ const keepAlive = hosting ? serveCommand(this.opts.remote ?? { shell: "posix" }) : wslKeepAlive(this.opts.remote);
115302
115515
  const args = [
115303
115516
  ...this.config(),
115304
- // A forward alone runs nothing; for WSL it runs the keep-alive instead (`wslKeepAlive`)
115305
115517
  ...keepAlive ? ["-T"] : ["-N"],
115306
115518
  "-o",
115307
115519
  "BatchMode=yes",
@@ -115427,13 +115639,15 @@ function storeMirror(store2) {
115427
115639
  }
115428
115640
 
115429
115641
  // packages/agent-host/src/main.ts
115430
- var BUILD = true ? "6d18a3e" : "dev";
115642
+ var BUILD = true ? "b974cd2" : "dev";
115431
115643
  var underKeeper = process.env.CC_KEEPER === "1";
115432
115644
  var keeperSource = process.env.CC_HOST_SOURCE;
115433
115645
  var frontDoor = process.env.CC_FRONT_DOOR;
115646
+ var startedByServe = process.env.CC_SERVE === "1";
115434
115647
  delete process.env.CC_KEEPER;
115435
115648
  delete process.env.CC_HOST_SOURCE;
115436
115649
  delete process.env.CC_FRONT_DOOR;
115650
+ delete process.env.CC_SERVE;
115437
115651
  var { values } = parseArgs({
115438
115652
  options: {
115439
115653
  port: { type: "string", default: "5175" },
@@ -115455,12 +115669,12 @@ var httpSecret = keeperToken ? deriveHttpSecret(keeperToken) : randomBytes6(32).
115455
115669
  var dbPath = values.memory ? ":memory:" : values.db ?? defaultDbPath();
115456
115670
  function defaultDbPath() {
115457
115671
  const isDev = process.env.CC_DEV === "1";
115458
- const dir = join31(homedir11(), isDev ? DATA_DIR_DEV : DATA_DIR);
115459
- const legacy = join31(homedir11(), isDev ? DATA_DIR_LEGACY.dev : DATA_DIR_LEGACY.prod);
115672
+ const dir = join30(homedir11(), isDev ? DATA_DIR_DEV : DATA_DIR);
115673
+ const legacy = join30(homedir11(), isDev ? DATA_DIR_LEGACY.dev : DATA_DIR_LEGACY.prod);
115460
115674
  movedNote = migrateLegacyDataDir(legacy, dir) ? `[agent-host] data folder moved: ${legacy} -> ${dir}` : null;
115461
115675
  mkdirSync10(dir, { recursive: true });
115462
115676
  process.env.CC_DATA_DIR = dir;
115463
- return join31(dir, "store.db");
115677
+ return join30(dir, "store.db");
115464
115678
  }
115465
115679
  var stopLog = dbPath === ":memory:" ? () => {
115466
115680
  } : teeStderrToFile(hostLogPath(dirname11(dbPath)));
@@ -115541,7 +115755,7 @@ var mgr = new SessionManager(
115541
115755
  },
115542
115756
  () => bridgeAddress(frontDoor, port, token),
115543
115757
  // Worktrees are created next to the data folder — dev and the packaged app never touch each other's worktrees
115544
- join31(dirname11(dbPath), "worktrees"),
115758
+ join30(dirname11(dbPath), "worktrees"),
115545
115759
  held2 ? { processes: held2.processes, keptSessions: held2.kept.sessionIds } : {}
115546
115760
  );
115547
115761
  var appChanges = broadcastAppChanges((e) => server.broadcast(e));
@@ -115578,7 +115792,7 @@ if (held2) {
115578
115792
  }
115579
115793
  var AUTO_UPDATE_CHECK_KEY = "updates.auto";
115580
115794
  var AUTO_APPLY_UPDATES_KEY = "updates.autoApply";
115581
- var themes = new ThemeFiles(join31(dataRoot(), "themes"), () => {
115795
+ var themes = new ThemeFiles(join30(dataRoot(), "themes"), () => {
115582
115796
  try {
115583
115797
  server.broadcast({ type: "themes_changed" });
115584
115798
  } catch {
@@ -115651,6 +115865,13 @@ if (underKeeper) {
115651
115865
  console.error(`[agent-host] could not restore the app views handed over: ${err.stack ?? err}`);
115652
115866
  }
115653
115867
  }
115868
+ var stopNow = null;
115869
+ var stopAsked = false;
115870
+ var stopFromRequest = () => {
115871
+ console.error("[agent-host] asked to stop (centralu serve --stop)");
115872
+ if (stopNow) stopNow();
115873
+ else stopAsked = true;
115874
+ };
115654
115875
  var links = new Links({
115655
115876
  hub: { version: APP_VERSION, protocolVersion: PROTOCOL_VERSION, dev: BUILD === "dev" },
115656
115877
  broadcast: (e) => server.broadcast(e),
@@ -115672,7 +115893,8 @@ var router = new Router({
115672
115893
  views,
115673
115894
  inlineViews,
115674
115895
  heldPids: held2?.heldPids,
115675
- machines: links
115896
+ machines: links,
115897
+ stopHost: startedByServe && !underKeeper ? () => stopFromRequest() : void 0
115676
115898
  }),
115677
115899
  machines: () => links.all()
115678
115900
  });
@@ -115726,7 +115948,7 @@ var stopActivity = underKeeper ? startActivityReport(
115726
115948
  `)
115727
115949
  ) : () => {
115728
115950
  };
115729
- var crashLog = join31(dirname11(dbPath), "host-errors.log");
115951
+ var crashLog = join30(dirname11(dbPath), "host-errors.log");
115730
115952
  function record2(kind, err) {
115731
115953
  const e = err;
115732
115954
  const line = `[${(/* @__PURE__ */ new Date()).toISOString()}] ${kind}: ${e?.stack ?? String(err)}
@@ -115795,6 +116017,8 @@ process.on("SIGTERM", () => void shutdown(onSignalMode, onSignalMode === "detach
115795
116017
  process.off("SIGINT", holdSignal);
115796
116018
  process.off("SIGTERM", holdSignal);
115797
116019
  if (pendingSignal) void shutdown(onSignalMode, onSignalMode === "detach");
116020
+ stopNow = () => void shutdown("stop", false);
116021
+ if (stopAsked) stopNow();
115798
116022
  held2?.children.on("stop", () => void shutdown("stop", false));
115799
116023
  if (control) {
115800
116024
  onDrain(control, {