@centralu/win32-x64 0.1.0-beta.13 → 0.1.0-beta.14

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.
@@ -42886,7 +42886,7 @@ import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync10, writeSync
42886
42886
 
42887
42887
  // packages/protocol/src/brand.ts
42888
42888
  var APP_NAME = "Centralu";
42889
- var APP_VERSION = "0.1.0-beta.13";
42889
+ var APP_VERSION = "0.1.0-beta.14";
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", "starting"]);
58128
+ var MachineStatus = external_exports.enum(["connecting", "connected", "unreachable", "not_running", "versions_differ", "refused", "starting", "updating"]);
58129
58129
  var MachineSide = external_exports.object({
58130
58130
  version: external_exports.string(),
58131
58131
  protocolVersion: external_exports.number().int(),
@@ -58145,6 +58145,14 @@ var MachineVersions = external_exports.object({
58145
58145
  /** The person chose to connect without aligning (`machines.acceptVersions`); only possible when compatible */
58146
58146
  accepted: external_exports.boolean().default(false)
58147
58147
  });
58148
+ var InstalledVersion = external_exports.object({ version: external_exports.string(), node: external_exports.string() });
58149
+ var MachineOperation = external_exports.object({
58150
+ kind: external_exports.enum(["install", "update", "rollback", "uninstall"]).catch("update"),
58151
+ step: external_exports.string(),
58152
+ /** The version it brings the machine to, when there is one */
58153
+ target: external_exports.string().nullable().default(null),
58154
+ at: external_exports.number()
58155
+ });
58148
58156
  var MachineInfo = external_exports.object({
58149
58157
  id: MachineId,
58150
58158
  /** What the person calls it; shown as the group header in the sidebar */
@@ -58177,7 +58185,35 @@ var MachineInfo = external_exports.object({
58177
58185
  * because that machine blocks starting a process through WMI, and ends when the link does (`note`
58178
58186
  * says why). Null when the host was already running. Additive; an older hub sends nothing
58179
58187
  */
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)
58188
+ 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),
58189
+ /**
58190
+ * What the hub installed there, as the remote's connection line last said (docs/plans/remote-hub.md
58191
+ * §10.5, S12): `managed` when the running `centralu` is that install, `current` and `previous` (the
58192
+ * rollback target) from its pointer files. Null when the machine has not answered, or predates the
58193
+ * field. Additive
58194
+ */
58195
+ install: external_exports.object({ managed: external_exports.boolean().catch(false), current: InstalledVersion.nullable().catch(null), previous: InstalledVersion.nullable().catch(null) }).nullable().catch(null).default(null),
58196
+ /**
58197
+ * An install, update, rollback or uninstall the hub is running there, and its step, while it runs
58198
+ * (`machines.install` / `update` / `rollback` / `uninstall`). Null otherwise. Additive
58199
+ */
58200
+ operation: MachineOperation.nullable().catch(null).default(null)
58201
+ });
58202
+ var MachineInstallResult = external_exports.object({
58203
+ machine: MachineInfo,
58204
+ current: InstalledVersion,
58205
+ previous: InstalledVersion.nullable(),
58206
+ removed: external_exports.array(external_exports.string()).default([]),
58207
+ left: external_exports.array(external_exports.string()).default([])
58208
+ });
58209
+ var MachineUninstallResult = external_exports.object({ machine: MachineInfo, stopped: external_exports.boolean() });
58210
+ var HostActivity = external_exports.object({
58211
+ working: external_exports.number().int().default(0),
58212
+ approvals: external_exports.number().int().default(0),
58213
+ questions: external_exports.number().int().default(0),
58214
+ background: external_exports.number().int().default(0),
58215
+ terminals: external_exports.number().int().default(0),
58216
+ commandRuns: external_exports.number().int().default(0)
58181
58217
  });
58182
58218
 
58183
58219
  // packages/protocol/src/events.ts
@@ -60754,6 +60790,43 @@ var RpcMethods = {
60754
60790
  * question comes again. Refused when the protocols differ.
60755
60791
  */
60756
60792
  "machines.acceptVersions": { params: external_exports.object({ machineId: MachineId }), result: MachineInfo },
60793
+ /**
60794
+ * Installs this hub's version on a linked machine over its ssh (docs/plans/remote-hub.md §10.2):
60795
+ * the pinned Node and the npm packages, checked against the release's pin and the registry's
60796
+ * signed metadata, beside what is there, with `current` switched to it and `previous` kept. A
60797
+ * host running there is not touched; the next start runs the new version (`machines.update` stops
60798
+ * and starts it in order). Refused by a development hub (its version is not on npm) and for
60799
+ * a machine that runs a command of its own; answers when the install is done or refused
60800
+ */
60801
+ "machines.install": { params: external_exports.object({ machineId: MachineId }), result: MachineInstallResult },
60802
+ /**
60803
+ * Updates a linked machine to this hub's version (docs/plans/remote-hub.md §10.5): installs it beside
60804
+ * the running one, stops the running host in order (`centralu serve --stop`; without a keeper its
60805
+ * agents end and resume on the new host, a turn in progress is lost), switches `current`, starts it
60806
+ * (`serve --detach`) and waits at most 30 s for a hello from the new version. When none comes, the
60807
+ * old version is put back and started, and the call fails with the end of the new host's log. On
60808
+ * success only the current and previous versions are kept. Progress is the machine's `operation`
60809
+ * in `machine_status`. Refused like `machines.install`
60810
+ */
60811
+ "machines.update": { params: external_exports.object({ machineId: MachineId }), result: MachineInstallResult },
60812
+ /**
60813
+ * Switches a machine back to the version the hub installed before (`previous`), stopping and starting
60814
+ * its host as `machines.update` does and checking the same way. One step only: `previous` is cleared,
60815
+ * so a second rollback is not offered (a store migrated twice may not read on the older build)
60816
+ */
60817
+ "machines.rollback": { params: external_exports.object({ machineId: MachineId }), result: MachineInstallResult },
60818
+ /**
60819
+ * Stops the host there and removes what the hub installed (`<data>/remote/`), keeping the machine's
60820
+ * data and an npm install of its own. The machine stays linked
60821
+ */
60822
+ "machines.uninstall": { params: external_exports.object({ machineId: MachineId }), result: MachineUninstallResult },
60823
+ /**
60824
+ * What would stop on a linked machine if its host stopped now (`host.activity`, asked of it), for the
60825
+ * "Update <machine>" prompt. Null when the machine cannot say (unreachable, other protocol, older)
60826
+ */
60827
+ "machines.activity": { params: external_exports.object({ machineId: MachineId }), result: external_exports.object({ activity: HostActivity.nullable() }) },
60828
+ /** What would stop if this host stopped now (the `hostBusy` rule); for a hub's `machines.activity` */
60829
+ "host.activity": { params: external_exports.object({}), result: HostActivity },
60757
60830
  /**
60758
60831
  * Stops this host the way a signal would: agents, terminals, command runs and app processes
60759
60832
  * stopped in order, the store closed, then exit. For `centralu serve --stop` (docs/agent-host.md
@@ -60935,6 +61008,17 @@ function sessionBusy(s) {
60935
61008
  function hostBusy(s) {
60936
61009
  return s.terminals > 0 || s.commandRuns > 0 || s.sessions.some(sessionBusy);
60937
61010
  }
61011
+ function activityCounts(s) {
61012
+ const c = { working: 0, approvals: 0, questions: 0, background: 0, terminals: s.terminals, commandRuns: s.commandRuns };
61013
+ for (const x of s.sessions) {
61014
+ if (!sessionBusy(x)) continue;
61015
+ if (x.pendingApproval || x.state === "waiting_approval") c.approvals++;
61016
+ else if ((x.pendingQuestions?.length ?? 0) > 0) c.questions++;
61017
+ else if (BUSY_STATES.has(x.state)) c.working++;
61018
+ else c.background++;
61019
+ }
61020
+ return c;
61021
+ }
60938
61022
  function sessionIdle(s, process4) {
60939
61023
  if (s.pendingApproval) return { idle: false, reason: "approval" };
60940
61024
  if (s.pendingQuestions.length > 0) return { idle: false, reason: "question" };
@@ -62278,8 +62362,9 @@ function restoreViewHandover(settings, views2, inline, now = Date.now()) {
62278
62362
  if (!(age2 >= 0 && age2 <= VIEW_HANDOVER_MAX_AGE_MS)) return { restored: 0, skipped: list.length };
62279
62363
  const opened = new Set(views2.restore(list));
62280
62364
  for (const v of list) {
62365
+ if (typeof v !== "object" || v === null || !opened.has(v.id)) continue;
62281
62366
  const o = v.inline;
62282
- if (!opened.has(v.id) || !o || typeof o.sessionId !== "string" || typeof o.callId !== "string" || typeof o.tool !== "string") continue;
62367
+ if (!o || typeof o.sessionId !== "string" || typeof o.callId !== "string" || typeof o.tool !== "string") continue;
62283
62368
  inline?.adopt({ sessionId: o.sessionId, callId: o.callId, ref: v.app, tool: o.tool, uri: v.uri, instanceId: v.id });
62284
62369
  }
62285
62370
  return { restored: opened.size, skipped: list.length - opened.size };
@@ -94949,7 +95034,8 @@ var AppVersions2 = class {
94949
95034
  for (const name of names) {
94950
95035
  if (name.startsWith(".")) continue;
94951
95036
  try {
94952
- out.push(JSON.parse(readFileSync7(join13(dir, name, "meta.json"), "utf8")));
95037
+ const meta3 = JSON.parse(readFileSync7(join13(dir, name, "meta.json"), "utf8"));
95038
+ if (meta3 && typeof meta3 === "object" && typeof meta3.id === "string" && typeof meta3.at === "number") out.push(meta3);
94953
95039
  } catch {
94954
95040
  }
94955
95041
  }
@@ -98105,6 +98191,19 @@ var SKILL_PROPOSALS_KEY = "orchestrator_skill_proposals";
98105
98191
  var SKILLS_KEY = "orchestrator_skills";
98106
98192
  var SKILL_MAX_COUNT = 10;
98107
98193
  var SKILL_MAX_CHARS = 2e3;
98194
+ var isText = (v) => typeof v === "string";
98195
+ var isTextList = (v) => Array.isArray(v) && v.every(isText);
98196
+ function storedList(raw, has) {
98197
+ if (!raw) return [];
98198
+ let v;
98199
+ try {
98200
+ v = JSON.parse(raw);
98201
+ } catch {
98202
+ return [];
98203
+ }
98204
+ if (!Array.isArray(v)) return [];
98205
+ return v.filter((x) => typeof x === "object" && x !== null && !Array.isArray(x) && has(x));
98206
+ }
98108
98207
  var OrchestratorProposals = class {
98109
98208
  constructor(store2, apps) {
98110
98209
  this.store = store2;
@@ -98114,12 +98213,7 @@ var OrchestratorProposals = class {
98114
98213
  apps;
98115
98214
  /** MCP server proposals waiting on the person's approval */
98116
98215
  mcpProposals() {
98117
- try {
98118
- const raw = this.store.appSetting(MCP_PROPOSALS_KEY);
98119
- return raw ? JSON.parse(raw) : [];
98120
- } catch {
98121
- return [];
98122
- }
98216
+ return storedList(this.store.appSetting(MCP_PROPOSALS_KEY), (x) => isText(x.name) && isText(x.command) && isTextList(x.args));
98123
98217
  }
98124
98218
  /** Saves an MCP server proposal (propose_mcp_server) — validation and persistence only */
98125
98219
  proposeMcpServer(spec) {
@@ -98219,21 +98313,11 @@ var OrchestratorProposals = class {
98219
98313
  }
98220
98314
  /** Skill proposals waiting on the person's approval (#71) */
98221
98315
  skillProposals() {
98222
- try {
98223
- const raw = this.store.appSetting(SKILL_PROPOSALS_KEY);
98224
- return raw ? JSON.parse(raw) : [];
98225
- } catch {
98226
- return [];
98227
- }
98316
+ return storedList(this.store.appSetting(SKILL_PROPOSALS_KEY), (x) => isText(x.name) && isText(x.content));
98228
98317
  }
98229
98318
  /** Skills that have been approved and loaded into the orchestrator's role prompt (#71) */
98230
98319
  orchestratorSkills() {
98231
- try {
98232
- const raw = this.store.appSetting(SKILLS_KEY);
98233
- return raw ? JSON.parse(raw) : [];
98234
- } catch {
98235
- return [];
98236
- }
98320
+ return storedList(this.store.appSetting(SKILLS_KEY), (x) => isText(x.name) && isText(x.content));
98237
98321
  }
98238
98322
  /**
98239
98323
  * Turns approved skills into a block appended to the role prompt (#71). This is tool-agnostic text
@@ -101914,11 +101998,17 @@ var Store = class _Store {
101914
101998
  forgetProjectConsent(fromProjectId, toProjectId, kind) {
101915
101999
  return this.db.prepare(`DELETE FROM project_consents WHERE from_project_id = ? AND to_project_id = ? AND kind = ?`).run(fromProjectId, toProjectId, kind).changes > 0;
101916
102000
  }
102001
+ /**
102002
+ * Only the kinds this build knows (#384). `kind` has no CHECK, so a newer build can add one without a breaking step;
102003
+ * a row of a kind this build cannot name is left in place for that build rather than listed here, where the settings
102004
+ * screen would show it without a label and could not revoke it (`projectConsents.revoke` takes known kinds only).
102005
+ */
101917
102006
  listProjectConsents() {
101918
- return this.db.prepare(
102007
+ const rows = this.db.prepare(
101919
102008
  `SELECT from_project_id as fromProjectId, to_project_id as toProjectId, kind, decided_at as decidedAt
101920
102009
  FROM project_consents ORDER BY decided_at DESC`
101921
102010
  ).all();
102011
+ return rows.filter((r) => ProjectConsentKind.safeParse(r.kind).success);
101922
102012
  }
101923
102013
  // ── Linked machines (#82, v46) ─────────────────────────────────────────────────────────────
101924
102014
  listLinkedMachines() {
@@ -104491,7 +104581,7 @@ var SessionManager = class _SessionManager {
104491
104581
  ` : ending.type === "turn_complete" ? `[Centralu] The assigned work finished.
104492
104582
  ` : `[Centralu] The assigned work stopped before finishing.
104493
104583
  `;
104494
- const failure2 = ending.type === "error" ? `Error: ${ending.error.message.slice(0, 600)}
104584
+ const failure3 = ending.type === "error" ? `Error: ${ending.error.message.slice(0, 600)}
104495
104585
 
104496
104586
  ` : "";
104497
104587
  try {
@@ -104501,7 +104591,7 @@ var SessionManager = class _SessionManager {
104501
104591
  id: ${sessionId}
104502
104592
  Project: ${frameField(project2)}
104503
104593
 
104504
- ` + failure2 + `Last response:
104594
+ ` + failure3 + `Last response:
104505
104595
  ${preview || "(no content)"}
104506
104596
 
104507
104597
  If you need more, you can read that session's recent conversation with read_session.`,
@@ -105429,33 +105519,53 @@ ${String(stderr).slice(0, 2e3)}`
105429
105519
  * Screen preferences (UiPreferences) — read as one blob, and written back accepting **only what
105430
105520
  * changed.**
105431
105521
  *
105432
- * Corrupted JSON is treated the same as if it did not exist (parseUiPreferences). The app failing
105433
- * to start because of a single broken setting is worse than losing one value the person had chosen.
105522
+ * Corrupted JSON is treated the same as if it did not exist (parseUiPreferences), and said once on
105523
+ * stderr with the row it came from. The app failing to start because of a single broken setting is
105524
+ * worse than losing one value the person had chosen.
105434
105525
  */
105435
105526
  uiPreferences() {
105436
- const raw = this.store.appSetting(UI_PREFS_KEY);
105437
- let stored;
105438
- try {
105439
- stored = raw === null ? void 0 : JSON.parse(raw);
105440
- } catch {
105441
- stored = void 0;
105442
- }
105527
+ const stored = this.storedUiPreferences();
105443
105528
  const prefs = parseUiPreferences(stored);
105444
- const hasTextSize = typeof stored === "object" && stored !== null && "textSize" in stored;
105445
- if (!hasTextSize) {
105529
+ if (!("textSize" in stored)) {
105446
105530
  const legacy = textSizeFromLegacyStep(this.loadWorkspace()?.textScale);
105447
105531
  if (legacy !== null) {
105448
105532
  prefs.textSize = legacy;
105449
- this.store.setAppSetting(UI_PREFS_KEY, JSON.stringify(prefs));
105533
+ this.store.setAppSetting(UI_PREFS_KEY, JSON.stringify({ ...stored, textSize: legacy }));
105450
105534
  }
105451
105535
  }
105452
105536
  return prefs;
105453
105537
  }
105538
+ /**
105539
+ * Writes the patch **over the stored record, not over this build's reading of it** (#384). Another
105540
+ * build reads and writes the same row: during a host swap, and after a person goes back a release.
105541
+ * A field this build does not know, or a value it cannot read (a theme mode a newer build added),
105542
+ * stays as it was unless the patch sets it, so changing the text size in an older build does not
105543
+ * reset what a newer one chose.
105544
+ */
105454
105545
  setUiPreferences(patch) {
105455
- const next = { ...this.uiPreferences(), ...patch };
105456
- this.store.setAppSetting(UI_PREFS_KEY, JSON.stringify(next));
105457
- return next;
105546
+ const current = this.uiPreferences();
105547
+ this.store.setAppSetting(UI_PREFS_KEY, JSON.stringify({ ...this.storedUiPreferences(), ...patch }));
105548
+ if (patch.sessionTools !== void 0 && patch.sessionTools !== current.sessionTools) {
105549
+ for (const id2 of this.meta.keys()) this.appsHub?.sessionAppsChanged(id2);
105550
+ }
105551
+ return { ...current, ...patch };
105552
+ }
105553
+ /** The stored record as written, every field kept; `{}` for none or for one that is not a JSON object */
105554
+ storedUiPreferences() {
105555
+ const raw = this.store.appSetting(UI_PREFS_KEY);
105556
+ if (raw === null) return {};
105557
+ try {
105558
+ const v = JSON.parse(raw);
105559
+ if (typeof v === "object" && v !== null && !Array.isArray(v)) return v;
105560
+ } catch {
105561
+ }
105562
+ if (!this.brokenPrefsSaid) {
105563
+ this.brokenPrefsSaid = true;
105564
+ console.error(`[agent-host] app_settings.${UI_PREFS_KEY} is not a JSON object; screen preferences read as their defaults until the next change`);
105565
+ }
105566
+ return {};
105458
105567
  }
105568
+ brokenPrefsSaid = false;
105459
105569
  /** The settings screen must be able to see and delete rules (FR-3: make the outcome visible) */
105460
105570
  listApprovalRules() {
105461
105571
  return this.store.listApprovalRules().filter((r) => r.matcher).map((r) => ({
@@ -106595,7 +106705,12 @@ ${String(stderr).slice(0, 2e3)}`
106595
106705
  ...opts,
106596
106706
  onDemand: {
106597
106707
  attached: (id2) => this.appAccess?.attached(id2) ?? [],
106598
- allowed: (session2, app) => this.appAccess?.allowed(session2, app) ?? false
106708
+ /*
106709
+ * Only while the session holds the reader set (#382): attach_app and detach_app are part of it, so a
106710
+ * session the person turned the set off for (or that became a manager) would keep tools it can no
106711
+ * longer detach. The records stay; they count again once the set is back.
106712
+ */
106713
+ allowed: (session2, app) => this.toolProfileOf(session2.id) === "reader" && (this.appAccess?.allowed(session2, app) ?? false)
106599
106714
  }
106600
106715
  });
106601
106716
  this.appAccess = new AppAccess({
@@ -107454,6 +107569,29 @@ async function readUsage(q2) {
107454
107569
  if (typeof fn2 !== "function") throw new Error(UNSUPPORTED2);
107455
107570
  return toSnapshot(await fn2.call(q2));
107456
107571
  }
107572
+ var USAGE_ASK_MS = 4e3;
107573
+ async function readUsageFromAny(queries, { timeoutMs = USAGE_ASK_MS, log = (line) => console.error(line) } = {}) {
107574
+ let first = null;
107575
+ for (const [i, { query, sessionId }] of queries.entries()) {
107576
+ let timer;
107577
+ try {
107578
+ return await Promise.race([
107579
+ readUsage(query),
107580
+ new Promise((_, reject) => {
107581
+ timer = setTimeout(() => reject(new Error(`no answer within ${timeoutMs} ms`)), timeoutMs);
107582
+ })
107583
+ ]);
107584
+ } catch (e) {
107585
+ first ??= e;
107586
+ const why = e instanceof Error ? e.message : String(e);
107587
+ log(`[usage] claude: session ${sessionId ?? "unknown"} could not read usage (${i + 1} of ${queries.length}): ${why}`);
107588
+ if (why === UNSUPPORTED2) break;
107589
+ } finally {
107590
+ clearTimeout(timer);
107591
+ }
107592
+ }
107593
+ throw first instanceof Error ? first : new Error("A running session is required to read usage");
107594
+ }
107457
107595
 
107458
107596
  // packages/agent-host/src/adapters/claude/orchestrator-mcp.ts
107459
107597
  init_sdk();
@@ -108736,22 +108874,22 @@ var ClaudeSession = class {
108736
108874
  * Bash caught by an `ask` rule in the settings file reaches the callback → answering allow runs it
108737
108875
  * the same request, with no callback (old-style auto) denied — "Claude requested permissions to use Bash, but you haven't granted it yet."
108738
108876
  *
108739
- * So auto's own callback only accepts question choices, and **denies everything else the
108740
- * way it used to.** Allowing it here would let our own callback approve an `ask` rule
108877
+ * So auto's own callback only accepts question choices and Centralu's own tools (which every
108878
+ * preset lets through, #382), and **denies everything else the way it used to.** Allowing it here would let our own callback approve an `ask` rule
108741
108879
  * written into a trusted project's own `.claude/settings.json` — the opposite direction
108742
108880
  * from #92, which exists so files from the repo cannot decide approvals. (The SDK leaves a
108743
108881
  * `CLAUDE_SDK_CAN_USE_TOOL_SHADOWED` warning line for this combination.)
108744
108882
  */
108745
108883
  canUseTool: async (toolName, toolInput) => {
108884
+ if (isOrchestratorTool(toolName) && self2.orchestratorServer) {
108885
+ return { behavior: "allow", updatedInput: toolInput };
108886
+ }
108746
108887
  if (preset === "auto" && toolName !== "AskUserQuestion") {
108747
108888
  return {
108748
108889
  behavior: "deny",
108749
108890
  message: `Permission to use ${toolName} was not granted (a settings "ask" rule matched; the auto preset does not ask)`
108750
108891
  };
108751
108892
  }
108752
- if (isOrchestratorTool(toolName)) {
108753
- return { behavior: "allow", updatedInput: toolInput };
108754
- }
108755
108893
  const appTool = appToolOf(toolName);
108756
108894
  if (appTool && self2.opts.apps?.readOnly(appTool.server, appTool.tool)) {
108757
108895
  return { behavior: "allow", updatedInput: toolInput };
@@ -108781,6 +108919,7 @@ var ClaudeSession = class {
108781
108919
  }
108782
108920
  });
108783
108921
  ClaudeAdapter.liveQueries.add(q2);
108922
+ ClaudeAdapter.querySessions.set(q2, this.sessionId);
108784
108923
  this.releaseLostCalls(q2);
108785
108924
  this.goalKnown = new Promise((resolve10) => this.settleGoalKnown = resolve10);
108786
108925
  q2.supportedCommands().then((list) => {
@@ -109306,6 +109445,8 @@ var ClaudeAdapter = class _ClaudeAdapter {
109306
109445
  * older session was still alive. Insertion order is the same as start order.
109307
109446
  */
109308
109447
  static liveQueries = /* @__PURE__ */ new Set();
109448
+ /** Which session each live query belongs to, for the log line when one cannot answer (#481) */
109449
+ static querySessions = /* @__PURE__ */ new WeakMap();
109309
109450
  /** The most recently started query that is still alive. */
109310
109451
  static get lastQuery() {
109311
109452
  let last = null;
@@ -109383,9 +109524,9 @@ var ClaudeAdapter = class _ClaudeAdapter {
109383
109524
  * a Query. It throws when there is no session at all, and the manager degrades with a reason.
109384
109525
  */
109385
109526
  async listUsage() {
109386
- const q2 = _ClaudeAdapter.lastQuery;
109387
- if (!q2) throw new Error("A running session is required to read usage");
109388
- return readUsage(q2);
109527
+ const live = [..._ClaudeAdapter.liveQueries].reverse();
109528
+ if (live.length === 0) throw new Error("A running session is required to read usage");
109529
+ return readUsageFromAny(live.map((query) => ({ query, sessionId: _ClaudeAdapter.querySessions.get(query) ?? null })));
109389
109530
  }
109390
109531
  async listModels() {
109391
109532
  const q2 = _ClaudeAdapter.lastQuery;
@@ -110428,16 +110569,16 @@ function normalizeNotification(sessionId, n) {
110428
110569
  */
110429
110570
  case "turn/completed": {
110430
110571
  const turn = obj(p.turn);
110431
- const failure2 = obj(turn.error);
110432
- if (str7(turn.status) !== "failed" && !failure2.message) return [{ type: "turn_complete", sessionId }];
110433
- const detail = str7(failure2.additionalDetails);
110572
+ const failure3 = obj(turn.error);
110573
+ if (str7(turn.status) !== "failed" && !failure3.message) return [{ type: "turn_complete", sessionId }];
110574
+ const detail = str7(failure3.additionalDetails);
110434
110575
  return [
110435
110576
  {
110436
110577
  type: "error",
110437
110578
  sessionId,
110438
110579
  error: {
110439
110580
  code: "internal",
110440
- message: [str7(failure2.message) || "The turn failed", detail].filter(Boolean).join("\n"),
110581
+ message: [str7(failure3.message) || "The turn failed", detail].filter(Boolean).join("\n"),
110441
110582
  retryable: true
110442
110583
  }
110443
110584
  }
@@ -110895,6 +111036,8 @@ var CodexSession = class {
110895
111036
  elicitations = /* @__PURE__ */ new Set();
110896
111037
  /** App servers loaded onto this thread through the bridge — only elicitations under those names go to our cards */
110897
111038
  appServers = /* @__PURE__ */ new Set();
111039
+ /** Whether this thread was given Centralu's own server — only then is an elicitation under that name ours to accept (#382) */
111040
+ ownServer = false;
110898
111041
  reqCounter = 0;
110899
111042
  alwaysAllow = new AlwaysAllowRules();
110900
111043
  /**
@@ -111118,6 +111261,7 @@ var CodexSession = class {
111118
111261
  const bridge = this.opts.orchestratorBridge;
111119
111262
  const servers = {};
111120
111263
  const orchestrator = !!(this.opts.orchestratorTools && bridge);
111264
+ this.ownServer = orchestrator;
111121
111265
  if (orchestrator) {
111122
111266
  servers[ORCHESTRATOR_MCP_NAME] = {
111123
111267
  command: process.execPath,
@@ -111321,7 +111465,7 @@ var CodexSession = class {
111321
111465
  this.emit({ type: "approval_request", sessionId: this.sessionId, requestId: requestId2, detail: appApprovalDetail(p.serverName, p.message, p._meta) });
111322
111466
  return;
111323
111467
  }
111324
- const ours = p.serverName === ORCHESTRATOR_MCP_NAME;
111468
+ const ours = p.serverName === ORCHESTRATOR_MCP_NAME && this.ownServer;
111325
111469
  this.client.respond(r.id, { action: ours ? "accept" : "decline", content: null, _meta: null });
111326
111470
  return;
111327
111471
  }
@@ -112181,7 +112325,7 @@ var toInfo = (h) => ({
112181
112325
  function sessionIdsOf(panels) {
112182
112326
  return panels.flatMap((p) => p.kind === "session" ? [p.sessionId] : []);
112183
112327
  }
112184
- function createRpcHandler(mgr2, adapters2, { terminals: terminals2, updates: updates2, commands, externalApps: externalApps2, views: views2, inlineViews: inlineViews2, heldPids, themes: themes2, agentVersions: agentVersions2, machines, stopHost } = {}) {
112328
+ function createRpcHandler(mgr2, adapters2, { terminals: terminals2, updates: updates2, commands, externalApps: externalApps2, views: views2, inlineViews: inlineViews2, heldPids, themes: themes2, agentVersions: agentVersions2, machines, stopHost, activity: activity2 } = {}) {
112185
112329
  const requireThemes = () => {
112186
112330
  if (!themes2) throw Object.assign(new Error("Theme files are unavailable"), { code: "internal" });
112187
112331
  return themes2;
@@ -112768,6 +112912,15 @@ function createRpcHandler(mgr2, adapters2, { terminals: terminals2, updates: upd
112768
112912
  return { ok: true };
112769
112913
  },
112770
112914
  "machines.reconnect": async (p) => requireMachines().reconnect(RpcMethods["machines.reconnect"].params.parse(p).machineId),
112915
+ "machines.install": async (p) => requireMachines().install(RpcMethods["machines.install"].params.parse(p).machineId),
112916
+ "machines.update": async (p) => requireMachines().update(RpcMethods["machines.update"].params.parse(p).machineId),
112917
+ "machines.rollback": async (p) => requireMachines().rollback(RpcMethods["machines.rollback"].params.parse(p).machineId),
112918
+ "machines.uninstall": async (p) => requireMachines().uninstall(RpcMethods["machines.uninstall"].params.parse(p).machineId),
112919
+ "machines.activity": async (p) => ({ activity: await requireMachines().activity(RpcMethods["machines.activity"].params.parse(p).machineId) }),
112920
+ "host.activity": async () => {
112921
+ if (!activity2) throw Object.assign(new Error("This host does not report its activity"), { code: "internal" });
112922
+ return activity2();
112923
+ },
112771
112924
  "machines.acceptVersions": async (p) => requireMachines().acceptVersions(RpcMethods["machines.acceptVersions"].params.parse(p).machineId),
112772
112925
  "updates.status": async (p) => requireUpdates().check(RpcMethods["updates.status"].params.parse(p).force),
112773
112926
  "updates.setAuto": async (p) => requireUpdates().setAuto(RpcMethods["updates.setAuto"].params.parse(p).enabled),
@@ -114046,6 +114199,752 @@ async function runDrain(bound, deps) {
114046
114199
  return report;
114047
114200
  }
114048
114201
 
114202
+ // packages/agent-host/src/links/install.ts
114203
+ import { existsSync as existsSync20, readFileSync as readFileSync13 } from "node:fs";
114204
+ import { hostname as hostname3 } from "node:os";
114205
+ import { fileURLToPath as fileURLToPath6 } from "node:url";
114206
+ import { gzipSync } from "node:zlib";
114207
+
114208
+ // packages/agent-host/src/links/registry.ts
114209
+ import { createPublicKey, verify } from "node:crypto";
114210
+ var NPM_REGISTRY = "https://registry.npmjs.org";
114211
+ var PACKAGE_NAME = /^(@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/;
114212
+ var VERSION = /^[0-9A-Za-z][0-9A-Za-z.+-]{0,63}$/;
114213
+ async function getJson(url2, f) {
114214
+ const res = await f(url2, { headers: { accept: "application/json" }, signal: AbortSignal.timeout(3e4) });
114215
+ if (!res.ok) throw new Error(`The npm registry answered ${res.status} for ${url2}`);
114216
+ return res.json();
114217
+ }
114218
+ async function verifiedPackage(name, version2, opts = {}) {
114219
+ if (!PACKAGE_NAME.test(name) || !VERSION.test(version2)) throw new Error(`Not a package to install: ${name}@${version2}`);
114220
+ const registry2 = (opts.registry ?? NPM_REGISTRY).replace(/\/+$/, "");
114221
+ const f = opts.fetch ?? globalThis.fetch;
114222
+ const now = opts.now?.() ?? Date.now();
114223
+ const [meta3, keys] = await Promise.all([getJson(`${registry2}/${name}/${version2}`, f), getJson(`${registry2}/-/npm/v1/keys`, f)]);
114224
+ const m = meta3;
114225
+ if (m?.name !== name || m.version !== version2) throw new Error(`The npm registry answered for another package than ${name}@${version2}`);
114226
+ const tarball = m.dist?.tarball;
114227
+ const integrity = m.dist?.integrity;
114228
+ if (typeof integrity !== "string" || !/^sha512-[A-Za-z0-9+/]{86}==$/.test(integrity)) {
114229
+ throw new Error(`${name}@${version2} has no sha512 integrity in the registry`);
114230
+ }
114231
+ if (typeof tarball !== "string" || new URL(tarball).origin !== new URL(registry2).origin) {
114232
+ throw new Error(`${name}@${version2}'s tarball is not served by the registry (${String(tarball)})`);
114233
+ }
114234
+ const known = /* @__PURE__ */ new Map();
114235
+ for (const k2 of keys?.keys ?? []) {
114236
+ if (k2 && typeof k2.keyid === "string" && typeof k2.key === "string") known.set(k2.keyid, k2);
114237
+ }
114238
+ const signed = `${name}@${version2}:${integrity}`;
114239
+ const sigs = Array.isArray(m.dist?.signatures) ? m.dist.signatures : [];
114240
+ const ok = sigs.some((s) => {
114241
+ const key = typeof s?.keyid === "string" ? known.get(s.keyid) : void 0;
114242
+ if (!key || typeof s.sig !== "string") return false;
114243
+ if (key.expires && Date.parse(key.expires) <= now) return false;
114244
+ try {
114245
+ const pub = createPublicKey({ key: Buffer.from(key.key, "base64"), format: "der", type: "spki" });
114246
+ return verify("sha256", Buffer.from(signed), pub, Buffer.from(s.sig, "base64"));
114247
+ } catch {
114248
+ return false;
114249
+ }
114250
+ });
114251
+ if (!ok) throw new Error(`The npm registry's signature on ${name}@${version2} does not check out; nothing was installed`);
114252
+ return { name, version: version2, tarball, integrity };
114253
+ }
114254
+
114255
+ // packages/agent-host/src/links/tunnel.ts
114256
+ import { spawn as spawn5 } from "node:child_process";
114257
+ import { createConnection as createConnection2, createServer as createServer3 } from "node:net";
114258
+ function parseConnectionLine(stdout) {
114259
+ const lines = stdout.split("\n").map((l) => l.trim()).filter(Boolean);
114260
+ for (let i = lines.length - 1; i >= 0; i--) {
114261
+ let raw;
114262
+ try {
114263
+ raw = JSON.parse(lines[i]);
114264
+ } catch {
114265
+ continue;
114266
+ }
114267
+ const o = raw;
114268
+ if (o && typeof o === "object" && typeof o.v === "number") {
114269
+ if (o.v !== 1) throw new Error(`The remote's Centralu speaks connection format ${o.v}; update Centralu on this computer`);
114270
+ if (typeof o.port !== "number" || typeof o.token !== "string" || typeof o.protocolVersion !== "number") {
114271
+ throw new Error("The remote answered with an incomplete connection line");
114272
+ }
114273
+ const install = parseInstall(o.install);
114274
+ return {
114275
+ v: 1,
114276
+ port: o.port,
114277
+ token: o.token,
114278
+ version: typeof o.version === "string" ? o.version : "unknown",
114279
+ protocolVersion: o.protocolVersion,
114280
+ dataDir: typeof o.dataDir === "string" ? o.dataDir : "",
114281
+ hostRunning: o.hostRunning === true,
114282
+ ...install ? { install } : {}
114283
+ };
114284
+ }
114285
+ }
114286
+ throw new Error("The remote did not print a connection line; its Centralu may predate `centralu serve` (update it there: npm i -g centralu)");
114287
+ }
114288
+ function parseInstall(raw) {
114289
+ if (!raw || typeof raw !== "object") return null;
114290
+ const o = raw;
114291
+ const pointer = (v) => {
114292
+ const p = v;
114293
+ return p && typeof p.version === "string" && typeof p.node === "string" ? { version: p.version, node: p.node } : null;
114294
+ };
114295
+ return { managed: o.managed === true, current: pointer(o.current), previous: pointer(o.previous), node: typeof o.node === "string" ? o.node : null };
114296
+ }
114297
+ function parseDetachLine(stdout) {
114298
+ const lines = stdout.split("\n").map((l) => l.trim()).filter(Boolean);
114299
+ for (let i = lines.length - 1; i >= 0; i--) {
114300
+ let raw;
114301
+ try {
114302
+ raw = JSON.parse(lines[i]);
114303
+ } catch {
114304
+ continue;
114305
+ }
114306
+ const d2 = raw?.detach;
114307
+ if (!d2 || typeof d2 !== "object") continue;
114308
+ if (d2.ok === true) return { ok: true, line: parseConnectionLine(lines[i]), how: typeof d2.how === "string" ? d2.how : null };
114309
+ return { ok: false, reason: typeof d2.reason === "string" ? d2.reason : "failed", message: typeof d2.message === "string" ? d2.message : "centralu serve --detach failed" };
114310
+ }
114311
+ throw new Error("The remote did not say whether it started");
114312
+ }
114313
+ var WSL_DISTRO_RE = /^[A-Za-z0-9._-]{1,64}$/;
114314
+ function powershell(script) {
114315
+ const full = `[Console]::OutputEncoding=[Text.Encoding]::UTF8
114316
+ ${script}`;
114317
+ return `powershell -NoProfile -NonInteractive -EncodedCommand ${Buffer.from(full, "utf16le").toString("base64")}`;
114318
+ }
114319
+ function wslKeepAlive(spec) {
114320
+ if (spec?.shell !== "wsl") return null;
114321
+ const distro = spec.wslDistro ?? "";
114322
+ if (!WSL_DISTRO_RE.test(distro)) throw new Error(`Not a WSL distro name: ${distro || "(none)"}`);
114323
+ return `wsl.exe -d ${distro} --exec sleep infinity`;
114324
+ }
114325
+ var MANAGED_SH = 'm="${CC_DATA_DIR:-$HOME/.centralu}/remote/bin/centralu"';
114326
+ var MANAGED_PS = "$d = if ($env:CC_DATA_DIR) { $env:CC_DATA_DIR } else { Join-Path $env:USERPROFILE '.centralu' }; $m = Join-Path $d 'remote\\bin\\centralu.cmd'";
114327
+ var NOT_FOUND2 = "CENTRALU-NOT-FOUND";
114328
+ function connectionCommand(spec) {
114329
+ return remoteCommand(spec, "serve --connection");
114330
+ }
114331
+ function detachCommand(spec) {
114332
+ return remoteCommand(spec, "serve --detach");
114333
+ }
114334
+ function stopCommand(spec) {
114335
+ return remoteCommand(spec, "serve --stop");
114336
+ }
114337
+ function serveCommand(spec) {
114338
+ return remoteCommand(spec, "serve");
114339
+ }
114340
+ function remoteCommand(spec, args) {
114341
+ const custom2 = spec.command?.trim() || null;
114342
+ if (spec.shell === "powershell") {
114343
+ if (custom2) return powershell(`${custom2} ${args}`);
114344
+ return powershell(
114345
+ `${MANAGED_PS}; if (Test-Path $m) { & $m ${args} } elseif (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}' } }`
114346
+ );
114347
+ }
114348
+ const lookup = `${MANAGED_SH}; if [ -x "$m" ]; then exec "$m" ${args}; elif 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`;
114349
+ if (spec.shell === "posix") return custom2 ? `${custom2} ${args}` : `sh -c '${lookup}'`;
114350
+ const distro = spec.wslDistro ?? "";
114351
+ if (!WSL_DISTRO_RE.test(distro)) throw new Error(`Not a WSL distro name: ${distro || "(none)"}`);
114352
+ const script = custom2 ? `${custom2} ${args}` : `PATH=$(printf %s "$PATH" | tr : '\\n' | grep -v '^/mnt/' | paste -sd: -); ${lookup}`;
114353
+ const b64 = Buffer.from(script, "utf8").toString("base64");
114354
+ return powershell(`wsl.exe -d '${distro}' -- bash -lc 'echo ${b64} | base64 -d | bash -l'`);
114355
+ }
114356
+ function remoteScript(spec, script) {
114357
+ if (spec.shell === "powershell") return powershell(script.ps);
114358
+ const b64 = Buffer.from(script.sh, "utf8").toString("base64");
114359
+ if (spec.shell === "posix") return `printf %s ${b64} | base64 -d | sh`;
114360
+ const distro = spec.wslDistro ?? "";
114361
+ if (!WSL_DISTRO_RE.test(distro)) throw new Error(`Not a WSL distro name: ${distro || "(none)"}`);
114362
+ return powershell(`wsl.exe -d '${distro}' -- bash -lc 'echo ${b64} | base64 -d | bash -l'`);
114363
+ }
114364
+ var SshTunnel = class {
114365
+ constructor(opts) {
114366
+ this.opts = opts;
114367
+ if (!opts.target.trim() || opts.target.startsWith("-")) throw new Error("Not an ssh target");
114368
+ }
114369
+ opts;
114370
+ child = null;
114371
+ /** The asking ssh runs, while they run */
114372
+ asking = /* @__PURE__ */ new Set();
114373
+ downListeners = [];
114374
+ closing = false;
114375
+ /** The remote cannot start a host outside a session (WMI blocked): the forward runs it (`startHost`) */
114376
+ linkBound = false;
114377
+ async open() {
114378
+ await this.stopChild();
114379
+ this.closing = false;
114380
+ const line = await this.connectionLine().catch((err) => {
114381
+ this.ifClosed();
114382
+ throw err;
114383
+ });
114384
+ this.ifClosed();
114385
+ if (!line.hostRunning && !this.linkBound) {
114386
+ return { url: "", token: line.token, line, localPort: 0 };
114387
+ }
114388
+ const hosting = !line.hostRunning;
114389
+ let localPort = await portFree(line.port) ? line.port : await freePort();
114390
+ try {
114391
+ await this.forward(localPort, line.port, hosting);
114392
+ } catch (err) {
114393
+ if (localPort !== line.port) throw err;
114394
+ this.ifClosed();
114395
+ localPort = await freePort();
114396
+ await this.forward(localPort, line.port, hosting);
114397
+ }
114398
+ if (this.closing) {
114399
+ await this.stopChild();
114400
+ this.ifClosed();
114401
+ }
114402
+ const served = hosting ? await this.untilServing() : line;
114403
+ return { url: `ws://127.0.0.1:${localPort}`, token: served.token, line: served, localPort };
114404
+ }
114405
+ /**
114406
+ * Starts the remote's host with `centralu serve --detach` (plan §10.4, owner decision 7): it then
114407
+ * runs outside any ssh session and outlives this link. When the remote says WMI process creation
114408
+ * is blocked there (decision 3), every `open` from then on runs `centralu serve` as the forward's
114409
+ * own remote command instead, so the host lives as long as the link.
114410
+ */
114411
+ async startHost() {
114412
+ this.ifClosed();
114413
+ const remote = this.opts.remote ?? { shell: "posix" };
114414
+ const r = await this.run([...this.config(), "-T", "-o", "BatchMode=yes", "-o", "ConnectTimeout=15", "--", this.opts.target, detachCommand(remote)]);
114415
+ this.ifClosed();
114416
+ if (r.code === 255) throw new Error(`ssh could not reach ${this.opts.target}: ${lastLine(r.stderr) || "no answer"}`);
114417
+ if (r.stdout.includes(NOT_FOUND2) || r.code === 127 && !remote.command) {
114418
+ throw new Error(`Centralu is not installed on ${this.opts.target} (run \`npm i -g centralu\` there, then \`centralu serve\` once)`);
114419
+ }
114420
+ let answer;
114421
+ try {
114422
+ answer = parseDetachLine(r.stdout);
114423
+ } catch (err) {
114424
+ if (/unknown option for serve: --detach/.test(r.stderr)) {
114425
+ 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)`);
114426
+ }
114427
+ const why = lastLine(r.stderr);
114428
+ throw new Error(`${err.message}${why ? `: ${why}` : r.code ? ` (exit ${r.code})` : ""}`);
114429
+ }
114430
+ if (answer.ok) {
114431
+ this.linkBound = false;
114432
+ return { how: "detached", note: null };
114433
+ }
114434
+ if (answer.reason === "wmi_blocked") {
114435
+ this.linkBound = true;
114436
+ return { how: "link_bound", note: answer.message };
114437
+ }
114438
+ throw new Error(answer.message);
114439
+ }
114440
+ /**
114441
+ * Runs one command there the way the connection line is asked for (batch mode, `--` before the
114442
+ * target), and answers what it printed. For the installer (install.ts): the command is already in
114443
+ * the remote shell's terms. `close()` ends it like any asking ssh
114444
+ */
114445
+ async exec(command) {
114446
+ this.ifClosed();
114447
+ const r = await this.run([...this.config(), "-T", "-o", "BatchMode=yes", "-o", "ConnectTimeout=15", "--", this.opts.target, command]);
114448
+ if (r.code === 255) throw new Error(`ssh could not reach ${this.opts.target}: ${lastLine(r.stderr) || "no answer"}`);
114449
+ return r;
114450
+ }
114451
+ /** The connection line again until the link-bound host answers, or the forward that runs it ends */
114452
+ async untilServing() {
114453
+ const deadline = Date.now() + (this.opts.startTimeoutMs ?? 3e4);
114454
+ for (; ; ) {
114455
+ this.ifClosed();
114456
+ if (!this.child) throw new Error(`The ssh session that runs Centralu on ${this.opts.target} ended before it served`);
114457
+ const line = await this.connectionLine();
114458
+ if (line.hostRunning) return line;
114459
+ if (Date.now() > deadline) {
114460
+ await this.stopChild();
114461
+ throw new Error(`Centralu on ${this.opts.target} did not start serving in time`);
114462
+ }
114463
+ await new Promise((r) => setTimeout(r, 500));
114464
+ }
114465
+ }
114466
+ onDown(listener) {
114467
+ this.downListeners.push(listener);
114468
+ }
114469
+ async close() {
114470
+ this.closing = true;
114471
+ for (const c of this.asking) c.kill("SIGTERM");
114472
+ await this.stopChild();
114473
+ }
114474
+ ifClosed() {
114475
+ if (this.closing) throw new Error(`The link to ${this.opts.target} was closed`);
114476
+ }
114477
+ run(args) {
114478
+ return new Promise((resolve10, reject) => {
114479
+ const child = spawn5(this.opts.ssh ?? "ssh", args, { env: this.opts.env ?? process.env, stdio: ["ignore", "pipe", "pipe"] });
114480
+ this.asking.add(child);
114481
+ child.once("exit", () => this.asking.delete(child));
114482
+ let stdout = "";
114483
+ let stderr = "";
114484
+ child.stdout.on("data", (d2) => stdout += String(d2));
114485
+ child.stderr.on("data", (d2) => stderr += String(d2));
114486
+ child.once("error", reject);
114487
+ child.once("close", (code2) => resolve10({ code: code2, stdout, stderr }));
114488
+ });
114489
+ }
114490
+ config() {
114491
+ return this.opts.configFile ? ["-F", this.opts.configFile] : [];
114492
+ }
114493
+ async connectionLine() {
114494
+ const remote = this.opts.remote ?? { shell: "posix" };
114495
+ const base2 = [...this.config(), "-T", "-o", "BatchMode=yes", "-o", "ConnectTimeout=15", "--", this.opts.target];
114496
+ const r = await this.run([...base2, connectionCommand(remote)]);
114497
+ if (r.code === 255) throw new Error(`ssh could not reach ${this.opts.target}: ${lastLine(r.stderr) || "no answer"}`);
114498
+ if (r.stdout.includes(NOT_FOUND2) || r.code === 127 && !remote.command) {
114499
+ throw new Error(`Centralu is not installed on ${this.opts.target} (run \`npm i -g centralu\` there, then \`centralu serve\` once)`);
114500
+ }
114501
+ try {
114502
+ return parseConnectionLine(r.stdout);
114503
+ } catch (err) {
114504
+ const why = lastLine(r.stderr);
114505
+ throw new Error(`${err.message}${why ? `: ${why}` : r.code ? ` (exit ${r.code})` : ""}`);
114506
+ }
114507
+ }
114508
+ forward(localPort, remotePort, hosting = false) {
114509
+ this.ifClosed();
114510
+ const keepAlive = hosting ? serveCommand(this.opts.remote ?? { shell: "posix" }) : wslKeepAlive(this.opts.remote);
114511
+ const args = [
114512
+ ...this.config(),
114513
+ ...keepAlive ? ["-T"] : ["-N"],
114514
+ "-o",
114515
+ "BatchMode=yes",
114516
+ "-o",
114517
+ "ExitOnForwardFailure=yes",
114518
+ "-o",
114519
+ "ServerAliveInterval=15",
114520
+ "-o",
114521
+ "ServerAliveCountMax=3",
114522
+ "-o",
114523
+ "GatewayPorts=no",
114524
+ "-L",
114525
+ `127.0.0.1:${localPort}:127.0.0.1:${remotePort}`,
114526
+ "--",
114527
+ this.opts.target,
114528
+ ...keepAlive ? [keepAlive] : []
114529
+ ];
114530
+ const child = spawn5(this.opts.ssh ?? "ssh", args, { env: this.opts.env ?? process.env, stdio: ["ignore", "ignore", "pipe"] });
114531
+ this.child = child;
114532
+ let stderr = "";
114533
+ child.stderr.on("data", (d2) => stderr += String(d2));
114534
+ return new Promise((resolve10, reject) => {
114535
+ let settled = false;
114536
+ const deadline = Date.now() + (this.opts.forwardTimeoutMs ?? 2e4);
114537
+ const fail4 = (message) => {
114538
+ if (settled) return;
114539
+ settled = true;
114540
+ reject(new Error(message));
114541
+ };
114542
+ child.once("error", (e) => fail4(`Could not start ssh: ${e.message}`));
114543
+ child.once("exit", (code2, signal) => {
114544
+ if (this.child === child) this.child = null;
114545
+ if (!settled) return fail4(`The ssh forward to ${this.opts.target} ended (${signal ?? code2}): ${lastLine(stderr) || "no reason given"}`);
114546
+ if (!this.closing) for (const l of this.downListeners) l(`ssh exited (${signal ?? code2})${stderr ? `: ${lastLine(stderr)}` : ""}`);
114547
+ });
114548
+ const poll = async () => {
114549
+ while (!settled) {
114550
+ if (await accepts(localPort)) {
114551
+ settled = true;
114552
+ this.opts.log?.(`[links] forward up: 127.0.0.1:${localPort} -> ${this.opts.target}:127.0.0.1:${remotePort}`);
114553
+ return resolve10();
114554
+ }
114555
+ if (Date.now() > deadline) {
114556
+ child.kill("SIGTERM");
114557
+ return fail4(`The ssh forward to ${this.opts.target} did not come up in time`);
114558
+ }
114559
+ await new Promise((r) => setTimeout(r, 100));
114560
+ }
114561
+ };
114562
+ void poll();
114563
+ });
114564
+ }
114565
+ async stopChild() {
114566
+ const child = this.child;
114567
+ this.child = null;
114568
+ if (!child || child.exitCode !== null || child.signalCode !== null) return;
114569
+ await new Promise((resolve10) => {
114570
+ child.once("exit", () => resolve10());
114571
+ child.kill("SIGTERM");
114572
+ setTimeout(() => {
114573
+ child.kill("SIGKILL");
114574
+ resolve10();
114575
+ }, 2e3).unref();
114576
+ });
114577
+ }
114578
+ };
114579
+ function lastLine(s) {
114580
+ const lines = s.replace(/#< CLIXML[\s\S]*$/, "").split(/\r?\n/).map((l) => l.trim()).filter((l) => l && !l.startsWith("**") && !/^Node\.js v\d/.test(l) && !/^at /.test(l));
114581
+ return [...lines].reverse().find((l) => /error|denied|refused|timed out|not found|cannot|could not/i.test(l)) ?? lines.at(-1) ?? "";
114582
+ }
114583
+ function accepts(port2) {
114584
+ return new Promise((resolve10) => {
114585
+ const s = createConnection2({ host: "127.0.0.1", port: port2 });
114586
+ s.once("connect", () => {
114587
+ s.destroy();
114588
+ resolve10(true);
114589
+ });
114590
+ s.once("error", () => resolve10(false));
114591
+ });
114592
+ }
114593
+ function portFree(port2) {
114594
+ return new Promise((resolve10) => {
114595
+ const probe = createServer3();
114596
+ probe.once("error", () => resolve10(false));
114597
+ probe.listen(port2, "127.0.0.1", () => probe.close(() => resolve10(true)));
114598
+ });
114599
+ }
114600
+ function freePort() {
114601
+ return new Promise((resolve10, reject) => {
114602
+ const probe = createServer3();
114603
+ probe.once("error", reject);
114604
+ probe.listen(0, "127.0.0.1", () => {
114605
+ const { port: port2 } = probe.address();
114606
+ probe.close(() => resolve10(port2));
114607
+ });
114608
+ });
114609
+ }
114610
+
114611
+ // packages/agent-host/src/links/install.ts
114612
+ var PREFLIGHT = "CENTRALU-PREFLIGHT";
114613
+ var STEP = "CENTRALU-INSTALL";
114614
+ var MIN_GLIBC = [2, 34];
114615
+ var NEED_MB = { linux: 600, win32: 300 };
114616
+ var DATA_SH = 'd="${CC_DATA_DIR:-$HOME/.centralu}"; r="$d/remote"';
114617
+ var DATA_PS = "$d = if ($env:CC_DATA_DIR) { $env:CC_DATA_DIR } else { Join-Path $env:USERPROFILE '.centralu' }; $r = Join-Path $d 'remote'";
114618
+ var PREFLIGHT_SH = `${DATA_SH}
114619
+ p="$d"; while [ ! -d "$p" ]; do p=$(dirname "$p"); done
114620
+ musl=0; if ldd --version 2>&1 | grep -qi musl; then musl=1; fi
114621
+ for f in /lib/ld-musl-*; do [ -e "$f" ] && musl=1; done
114622
+ has() { command -v "$1" >/dev/null 2>&1 && echo 1 || echo 0; }
114623
+ fetch=; if command -v curl >/dev/null 2>&1; then fetch=curl; elif command -v wget >/dev/null 2>&1; then fetch=wget; fi
114624
+ sha=; if command -v sha256sum >/dev/null 2>&1; then sha=sha256sum; elif command -v shasum >/dev/null 2>&1; then sha=shasum; fi
114625
+ echo "${PREFLIGHT} os=$(uname -s) arch=$(uname -m) glibc=$(getconf GNU_LIBC_VERSION 2>/dev/null | sed -n 's/^glibc //p') musl=$musl freeKb=$(df -Pk "$p" 2>/dev/null | awk 'NR==2 {print $4}') tar=$(has tar) gzip=$(has gzip) fetch=$fetch sha=$sha"
114626
+ `;
114627
+ var PREFLIGHT_PS = `${DATA_PS}
114628
+ $arch = if ($env:PROCESSOR_ARCHITEW6432) { $env:PROCESSOR_ARCHITEW6432 } else { $env:PROCESSOR_ARCHITECTURE }
114629
+ $p = $d; while (-not (Test-Path $p)) { $p = Split-Path $p }
114630
+ $free = try { [math]::Floor((New-Object IO.DriveInfo ([IO.Path]::GetPathRoot((Resolve-Path $p).Path))).AvailableFreeSpace / 1024) } catch { '' }
114631
+ $tar = [int](Test-Path (Join-Path $env:SystemRoot 'System32\\tar.exe'))
114632
+ $fetch = if (Test-Path (Join-Path $env:SystemRoot 'System32\\curl.exe')) { 'curl' } else { 'iwr' }
114633
+ "${PREFLIGHT} os=Windows arch=$arch glibc= musl=0 freeKb=$free tar=$tar gzip=1 fetch=$fetch sha=Get-FileHash"
114634
+ `;
114635
+ function preflightCommand(spec) {
114636
+ return remoteScript(spec, { sh: PREFLIGHT_SH, ps: PREFLIGHT_PS });
114637
+ }
114638
+ function parsePreflight(stdout) {
114639
+ const line = stdout.split(/\r?\n/).reverse().find((l) => l.startsWith(`${PREFLIGHT} `));
114640
+ if (!line) throw new Error("The remote did not answer the install check");
114641
+ const kv2 = new Map(line.slice(PREFLIGHT.length + 1).trim().split(/\s+/).map((w) => [w.slice(0, w.indexOf("=")), w.slice(w.indexOf("=") + 1)]));
114642
+ const word = (k2) => kv2.get(k2) || null;
114643
+ const free = Number(kv2.get("freeKb"));
114644
+ return {
114645
+ os: word("os") ?? "",
114646
+ arch: word("arch") ?? "",
114647
+ glibc: word("glibc"),
114648
+ musl: kv2.get("musl") === "1",
114649
+ freeKb: kv2.get("freeKb") && Number.isFinite(free) ? free : null,
114650
+ tar: kv2.get("tar") === "1",
114651
+ gzip: kv2.get("gzip") === "1",
114652
+ fetch: word("fetch"),
114653
+ sha: word("sha")
114654
+ };
114655
+ }
114656
+ function preflight(f, runtime) {
114657
+ const no2 = (reason) => ({ ok: false, reason });
114658
+ let platform;
114659
+ if (f.os === "Linux") {
114660
+ if (f.musl) return no2("This Linux uses musl (Alpine and the like); Centralu runs on glibc 2.34 or later only");
114661
+ const arch = f.arch === "x86_64" || f.arch === "amd64" ? "x64" : f.arch === "aarch64" || f.arch === "arm64" ? "arm64" : null;
114662
+ if (!arch) return no2(`Centralu has no build for Linux on ${f.arch || "this processor"}`);
114663
+ const [maj = 0, min = 0] = (f.glibc ?? "").split(".").map(Number);
114664
+ if (!f.glibc || maj < MIN_GLIBC[0] || maj === MIN_GLIBC[0] && min < MIN_GLIBC[1]) {
114665
+ return no2(`Centralu needs glibc ${MIN_GLIBC.join(".")} or later (Ubuntu 22.04, Debian 12, RHEL 9); this machine has ${f.glibc ? `glibc ${f.glibc}` : "none that it reports"}`);
114666
+ }
114667
+ platform = `linux-${arch}`;
114668
+ } else if (f.os === "Windows") {
114669
+ if (f.arch !== "AMD64") return no2(`Centralu has no build for Windows on ${f.arch || "this processor"}`);
114670
+ platform = "win32-x64";
114671
+ } else {
114672
+ return no2(`Installing from here is for Linux and Windows; on ${f.os || "this system"}, install Centralu there (npm i -g centralu)`);
114673
+ }
114674
+ if (!runtime.node.archives[platform]) return no2(`This Centralu pins no Node for ${platform}`);
114675
+ if (!f.tar) return no2(platform === "win32-x64" ? "tar.exe is missing (it comes with Windows 10 1803 and later)" : "tar is missing there; install it and try again");
114676
+ if (!f.gzip) return no2("gzip is missing there; install it and try again");
114677
+ if (!f.fetch) return no2("Neither curl nor wget is installed there; install one and try again");
114678
+ if (!f.sha) return no2("No SHA-256 tool (sha256sum) is installed there; install coreutils and try again");
114679
+ const need = NEED_MB[platform === "win32-x64" ? "win32" : "linux"];
114680
+ if (f.freeKb !== null && f.freeKb < need * 1024) return no2(`Installing needs about ${need} MB free there; ${Math.floor(f.freeKb / 1024)} MB is`);
114681
+ return { ok: true, platform };
114682
+ }
114683
+ var PLAIN = /^[0-9A-Za-z][0-9A-Za-z.+-]{0,63}$/;
114684
+ var SAFE_URL = /^https?:\/\/[0-9A-Za-z.:-]+(\/[0-9A-Za-z._~%/-]*)?$/;
114685
+ function nodeCommand(spec, n) {
114686
+ if (!PLAIN.test(n.version) || !SAFE_URL.test(n.url) || !/^[0-9a-f]{64}$/.test(n.sha256)) throw new Error("Not a Node archive to install");
114687
+ const sh = `${DATA_SH}; V="${n.version}"; n="$r/node/v$V"
114688
+ say() { echo "${STEP} $*"; }
114689
+ if [ -x "$n/bin/node" ] && "$n/bin/node" --version >/dev/null 2>&1; then say node ok; exit 0; fi
114690
+ t="$r/.partial-node-$$"; trap 'rm -rf "$t"' EXIT
114691
+ mkdir -p "$t/x" || { say fail mkdir; exit 1; }
114692
+ if command -v curl >/dev/null 2>&1; then curl -fsSL --retry 2 -o "$t/a" "${n.url}" || { say fail download node; exit 1; }
114693
+ else wget -q -O "$t/a" "${n.url}" || { say fail download node; exit 1; }; fi
114694
+ if command -v sha256sum >/dev/null 2>&1; then h=$(sha256sum "$t/a" | cut -d" " -f1); else h=$(shasum -a 256 "$t/a" | cut -d" " -f1); fi
114695
+ [ "$h" = "${n.sha256}" ] || { say fail node_hash "$h"; exit 1; }
114696
+ tar -xzf "$t/a" -C "$t/x" --strip-components 1 || { say fail unpack node; exit 1; }
114697
+ mkdir -p "$t/keep/bin" && mv "$t/x/bin/node" "$t/keep/bin/node" || { say fail unpack node; exit 1; }
114698
+ [ ! -f "$t/x/LICENSE" ] || mv "$t/x/LICENSE" "$t/keep/LICENSE"
114699
+ "$t/keep/bin/node" --version >/dev/null 2>&1 || { say fail node_runs; exit 1; }
114700
+ mkdir -p "$r/node" && rm -rf "$n" && mv "$t/keep" "$n" || { say fail place node; exit 1; }
114701
+ say node ok
114702
+ `;
114703
+ const ps2 = `$ErrorActionPreference = 'Stop'; $ProgressPreference = 'SilentlyContinue'
114704
+ ${DATA_PS}; $n = Join-Path $r 'node\\v${n.version}'
114705
+ function Say($s) { "${STEP} $s" }
114706
+ if ((Test-Path (Join-Path $n 'node.exe'))) { Say 'node ok'; return }
114707
+ $t = Join-Path $r ".partial-node-$PID"
114708
+ try {
114709
+ New-Item -ItemType Directory -Force (Join-Path $t 'x') | Out-Null
114710
+ $a = Join-Path $t 'a.zip'
114711
+ $curl = Join-Path $env:SystemRoot 'System32\\curl.exe'
114712
+ if (Test-Path $curl) { & $curl -fsSL --retry 2 -o $a '${n.url}'; if ($LASTEXITCODE -ne 0) { throw 'download node' } }
114713
+ else { try { Invoke-WebRequest -UseBasicParsing '${n.url}' -OutFile $a } catch { throw 'download node' } }
114714
+ $h = (Get-FileHash -Algorithm SHA256 $a).Hash.ToLower()
114715
+ if ($h -ne '${n.sha256}') { throw "node_hash $h" }
114716
+ & (Join-Path $env:SystemRoot 'System32\\tar.exe') -xf $a -C (Join-Path $t 'x') --strip-components 1
114717
+ if ($LASTEXITCODE -ne 0) { throw 'unpack node' }
114718
+ $k = Join-Path $t 'keep'; New-Item -ItemType Directory -Force $k | Out-Null
114719
+ Move-Item (Join-Path $t 'x\\node.exe') $k
114720
+ if (Test-Path (Join-Path $t 'x\\LICENSE')) { Move-Item (Join-Path $t 'x\\LICENSE') $k }
114721
+ & (Join-Path $k 'node.exe') --version | Out-Null
114722
+ if ($LASTEXITCODE -ne 0) { throw 'node_runs' }
114723
+ New-Item -ItemType Directory -Force (Join-Path $r 'node') | Out-Null
114724
+ if (Test-Path $n) { Remove-Item -Recurse -Force $n }
114725
+ Move-Item $k $n
114726
+ Say 'node ok'
114727
+ } catch {
114728
+ $m = if ($_.Exception.Message) { $_.Exception.Message } else { "$_" }
114729
+ Say "fail $($m -replace '\\s+', ' ')"
114730
+ } finally {
114731
+ if (Test-Path $t) { Remove-Item -Recurse -Force $t -ErrorAction SilentlyContinue }
114732
+ }
114733
+ `;
114734
+ return remoteScript(spec, { sh, ps: ps2 });
114735
+ }
114736
+ function installCommand(spec, script, params) {
114737
+ if (!PLAIN.test(params.version) || !PLAIN.test(params.node)) throw new Error("Not a version to install");
114738
+ const gz2 = gzipSync(Buffer.from(script, "utf8"), { level: 9 }).toString("base64");
114739
+ const arg = Buffer.from(JSON.stringify(params), "utf8").toString("base64");
114740
+ const sh = `${DATA_SH}; f="$r/.install-$$.mjs"
114741
+ mkdir -p "$r" && printf %s "${gz2}" | base64 -d | gzip -dc > "$f" || { echo "${STEP} fail script"; exit 1; }
114742
+ "$r/node/v${params.node}/bin/node" "$f" "${arg}"; c=$?; rm -f "$f"; exit $c
114743
+ `;
114744
+ const ps2 = `${DATA_PS}; $f = Join-Path $r ".install-$PID.mjs"
114745
+ try {
114746
+ New-Item -ItemType Directory -Force $r | Out-Null
114747
+ $z = New-Object IO.Compression.GZipStream((New-Object IO.MemoryStream(,[Convert]::FromBase64String('${gz2}'))), [IO.Compression.CompressionMode]::Decompress)
114748
+ $o = [IO.File]::Create($f); $z.CopyTo($o); $o.Close(); $z.Close()
114749
+ } catch { "${STEP} fail script"; return }
114750
+ & (Join-Path $r 'node\\v${params.node}\\node.exe') $f '${arg}'
114751
+ Remove-Item -Force $f -ErrorAction SilentlyContinue
114752
+ `;
114753
+ return remoteScript(spec, { sh, ps: ps2 });
114754
+ }
114755
+ function failure2(target, code2, detail) {
114756
+ switch (code2) {
114757
+ case "node_hash":
114758
+ return `The Node downloaded on ${target} does not match the SHA-256 this Centralu release pinned; nothing was installed`;
114759
+ case "integrity":
114760
+ return `${detail || "A package"} downloaded on ${target} does not match the integrity the npm registry signed; nothing was installed`;
114761
+ case "download":
114762
+ return `${target} could not download ${detail || "what it needs"}; check that it can reach nodejs.org and registry.npmjs.org`;
114763
+ case "busy":
114764
+ return `Another install is running on ${target}`;
114765
+ case "no_host":
114766
+ return `The package for ${detail} carries no host; this version cannot be installed on ${target}`;
114767
+ case "no_launcher":
114768
+ return `Centralu ${detail} predates installs from the app; install a newer version`;
114769
+ case "node_runs":
114770
+ return `The Node downloaded on ${target} does not run there`;
114771
+ case "missing":
114772
+ return `Centralu ${detail} is no longer installed on ${target}`;
114773
+ case "remove":
114774
+ return `What this computer installed on ${target} could not be removed: ${detail || "no reason given"}`;
114775
+ default:
114776
+ return `Installing on ${target} failed (${[code2, detail].filter(Boolean).join(": ")})`;
114777
+ }
114778
+ }
114779
+ function readStep(run, ok, target) {
114780
+ const line = run.stdout.split(/\r?\n/).reverse().find((l) => l.startsWith(`${STEP} `));
114781
+ if (!line) {
114782
+ const why = lastLine(run.stderr);
114783
+ throw new Error(`Installing on ${target} stopped without saying why${why ? `: ${why}` : run.code ? ` (exit ${run.code})` : ""}`);
114784
+ }
114785
+ const rest = line.slice(STEP.length + 1).trim();
114786
+ if (rest.startsWith("fail")) {
114787
+ const [, code2 = "error", ...detail] = rest.split(" ");
114788
+ throw new Error(failure2(target, code2, detail.join(" ")));
114789
+ }
114790
+ if (!rest.startsWith(ok)) throw new Error(`Installing on ${target} answered something unexpected: ${rest.slice(0, 120)}`);
114791
+ return rest.slice(ok.length).trim();
114792
+ }
114793
+ async function installRemote(o) {
114794
+ o.onStep?.("preflight");
114795
+ const pf2 = preflight(parsePreflight((await o.exec(preflightCommand(o.spec))).stdout), o.runtime);
114796
+ if (!pf2.ok) throw new Error(`Cannot install on ${o.target}: ${pf2.reason}`);
114797
+ o.onStep?.("registry");
114798
+ const packages = await Promise.all([verifiedPackage("centralu", o.version, o.registry), verifiedPackage(`@centralu/${pf2.platform}`, o.version, o.registry)]);
114799
+ const archive = o.runtime.node.archives[pf2.platform];
114800
+ const nodeVersion = o.runtime.node.version;
114801
+ o.onStep?.("node");
114802
+ const url2 = `${(o.nodeDist ?? "https://nodejs.org/dist").replace(/\/+$/, "")}/v${nodeVersion}/${archive.file}`;
114803
+ readStep(await o.exec(nodeCommand(o.spec, { version: nodeVersion, url: url2, sha256: archive.sha256 })), "node ok", o.target);
114804
+ o.onStep?.("centralu");
114805
+ const params = { version: o.version, node: nodeVersion, platform: pf2.platform, hub: `${hostname3()} (Centralu ${o.version})`, packages: packages.map(({ name, tarball, integrity }) => ({ name, tarball, integrity })), ...o.activate === false ? { activate: false } : {} };
114806
+ const done = readStep(await o.exec(installCommand(o.spec, o.script, params)), "done", o.target);
114807
+ const r = JSON.parse(done);
114808
+ return { current: r.current, previous: r.previous ?? null, removed: r.removed ?? [], left: r.left ?? [], ...r.installed ? { installed: r.installed } : {} };
114809
+ }
114810
+ function bundled(name, source) {
114811
+ for (const url2 of [new URL(`./${name}`, import.meta.url), new URL(source, import.meta.url)]) {
114812
+ const file2 = fileURLToPath6(url2);
114813
+ if (existsSync20(file2)) return readFileSync13(file2, "utf8");
114814
+ }
114815
+ throw new Error(`${name} is missing from this host`);
114816
+ }
114817
+ function installScript() {
114818
+ return bundled("remote-install.mjs", "./remote-install.mjs");
114819
+ }
114820
+ function remoteRuntime() {
114821
+ return JSON.parse(bundled("remote-runtime.json", "../../../../packaging/remote-runtime.json"));
114822
+ }
114823
+
114824
+ // packages/agent-host/src/links/update.ts
114825
+ var CHECK_MS = 3e4;
114826
+ function parseStopLine(stdout) {
114827
+ for (const line of stdout.split(/\r?\n/).reverse()) {
114828
+ const t = line.trim();
114829
+ if (!t.startsWith("{")) continue;
114830
+ try {
114831
+ const o = JSON.parse(t);
114832
+ if (o.v === 1 && o.stop && typeof o.stop.ok === "boolean") {
114833
+ return { ok: o.stop.ok, wasRunning: o.stop.wasRunning === true, message: typeof o.stop.message === "string" ? o.stop.message : null };
114834
+ }
114835
+ } catch {
114836
+ }
114837
+ }
114838
+ return null;
114839
+ }
114840
+ async function stopRemoteHost(c) {
114841
+ const r = await c.exec(stopCommand(c.spec));
114842
+ if (r.stdout.includes(NOT_FOUND2)) return { wasRunning: false };
114843
+ const answer = parseStopLine(r.stdout);
114844
+ if (!answer) {
114845
+ if (/unknown option for serve: --stop/.test(r.stderr)) {
114846
+ throw new Error(`The Centralu running on ${c.target} is too old to be stopped from here: stop \`centralu serve\` there, then try again`);
114847
+ }
114848
+ const why = lastLine(r.stderr);
114849
+ throw new Error(`Stopping Centralu on ${c.target} gave no answer${why ? `: ${why}` : r.code ? ` (exit ${r.code})` : ""}`);
114850
+ }
114851
+ if (!answer.ok) throw new Error(`Centralu on ${c.target} could not be stopped: ${answer.message ?? "no reason given"}`);
114852
+ return { wasRunning: answer.wasRunning };
114853
+ }
114854
+ async function setPointers(c, run, current, previous) {
114855
+ const out = readStep(await c.exec(installCommand(c.spec, c.script, { action: "pointers", version: run.version, node: run.node, current, previous })), "done", c.target);
114856
+ return JSON.parse(out);
114857
+ }
114858
+ async function pruneThere(c, run) {
114859
+ const out = JSON.parse(readStep(await c.exec(installCommand(c.spec, c.script, { action: "prune", version: run.version, node: run.node })), "done", c.target));
114860
+ return { removed: out.removed ?? [], left: out.left ?? [] };
114861
+ }
114862
+ var TAIL_SH = `${DATA_SH}; tail -n 12 "$d/host.log" 2>/dev/null; true
114863
+ `;
114864
+ var TAIL_PS = `${DATA_PS}; Get-Content -Tail 12 (Join-Path $d 'host.log') -ErrorAction SilentlyContinue
114865
+ `;
114866
+ async function hostLogTail(c) {
114867
+ const r = await c.exec(remoteScript(c.spec, { sh: TAIL_SH, ps: TAIL_PS }));
114868
+ const lines = r.stdout.split(/\r?\n/).map((l) => l.trim()).filter(Boolean).slice(-4);
114869
+ const text2 = lines.join(" / ");
114870
+ return text2.length > 600 ? `\u2026${text2.slice(-600)}` : text2;
114871
+ }
114872
+ async function startAndCheck(c, version2) {
114873
+ c.step("start");
114874
+ try {
114875
+ await c.start();
114876
+ } catch (err) {
114877
+ return err.message;
114878
+ }
114879
+ c.step("check");
114880
+ return await c.answers(version2, CHECK_MS) ? null : `it did not answer within ${CHECK_MS / 1e3} s`;
114881
+ }
114882
+ async function putBack(c, to, why, was) {
114883
+ c.step("roll_back");
114884
+ const tail = await hostLogTail(c).catch(() => "");
114885
+ const log = tail ? ` The end of its host.log: ${tail}` : "";
114886
+ const back = was.current ? `Centralu ${was.current.version}` : "the Centralu that was there";
114887
+ try {
114888
+ await stopRemoteHost(c).catch(() => void 0);
114889
+ await setPointers(c, to, was.current, was.previous);
114890
+ if (was.running) await c.start();
114891
+ } catch (err) {
114892
+ throw new Error(`Centralu ${to.version} did not start on ${c.target} (${why}), and putting back ${back} failed too: ${err.message}.${log}`);
114893
+ }
114894
+ throw new Error(`Centralu ${to.version} did not start on ${c.target} (${why}); ${was.running ? `it runs ${back} again` : `${back} is back in place`}.${log}`);
114895
+ }
114896
+ async function updateRemote(c, o) {
114897
+ const placed = await installRemote({ ...o, exec: c.exec, spec: c.spec, target: c.target, script: c.script, activate: false, onStep: c.step });
114898
+ const next = placed.installed;
114899
+ if (!next) throw new Error(`Installing on ${c.target} answered without saying what it placed; the Centralu there may predate updates from the app`);
114900
+ const was = { current: placed.current ?? null, previous: placed.previous ?? null };
114901
+ await c.hold();
114902
+ c.step("stop");
114903
+ const { wasRunning } = await stopRemoteHost(c);
114904
+ c.step("switch");
114905
+ const keep = was.current && was.current.version !== next.version ? was.current : was.previous;
114906
+ await setPointers(c, next, next, keep);
114907
+ const why = await startAndCheck(c, next.version);
114908
+ if (why) return putBack(c, next, why, { ...was, running: wasRunning });
114909
+ c.step("prune");
114910
+ const pruned = await pruneThere(c, next).catch(() => ({ removed: [], left: [] }));
114911
+ return { current: next, previous: keep, ...pruned };
114912
+ }
114913
+ async function rollbackRemote(c, there) {
114914
+ await c.hold();
114915
+ c.step("stop");
114916
+ const { wasRunning } = await stopRemoteHost(c);
114917
+ c.step("switch");
114918
+ await setPointers(c, there.previous, there.previous, null);
114919
+ const why = await startAndCheck(c, there.previous.version);
114920
+ if (why) return putBack(c, there.previous, why, { current: there.current, previous: there.previous, running: wasRunning });
114921
+ return { current: there.previous, previous: null, removed: [], left: [] };
114922
+ }
114923
+ var UNINSTALL_SH = `${DATA_SH}
114924
+ if [ -d "$r" ]; then rm -rf "$r" || { echo "${STEP} fail remove $r"; exit 1; }; fi
114925
+ echo "${STEP} uninstalled"
114926
+ `;
114927
+ var UNINSTALL_PS = `$ErrorActionPreference = 'Stop'
114928
+ ${DATA_PS}
114929
+ try {
114930
+ if (Test-Path $r) { Remove-Item -Recurse -Force $r }
114931
+ "${STEP} uninstalled"
114932
+ } catch {
114933
+ "${STEP} fail remove $($_.Exception.Message -replace '\\s+', ' ')"
114934
+ }
114935
+ `;
114936
+ function uninstallCommand(spec) {
114937
+ return remoteScript(spec, { sh: UNINSTALL_SH, ps: UNINSTALL_PS });
114938
+ }
114939
+ async function uninstallRemote(c) {
114940
+ await c.hold();
114941
+ c.step("stop");
114942
+ const { wasRunning } = await stopRemoteHost(c);
114943
+ c.step("remove");
114944
+ readStep(await c.exec(uninstallCommand(c.spec)), "uninstalled", c.target);
114945
+ return { stopped: wasRunning };
114946
+ }
114947
+
114049
114948
  // packages/agent-host/src/links/machine-ids.ts
114050
114949
  import { randomBytes as randomBytes5 } from "node:crypto";
114051
114950
  function isMachineId(value) {
@@ -114514,6 +115413,10 @@ function channel(version2) {
114514
115413
 
114515
115414
  // packages/agent-host/src/links/links.ts
114516
115415
  var isObj2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
115416
+ var linkError2 = (message) => Object.assign(new Error(message), { code: "internal" });
115417
+ function installOf(i) {
115418
+ return i ? { managed: i.managed, current: i.current, previous: i.previous } : null;
115419
+ }
114517
115420
  function hiddenKind(s) {
114518
115421
  return isObj2(s) && (s.kind === "orchestrator" || s.kind === "coordinator");
114519
115422
  }
@@ -114547,6 +115450,19 @@ var LinkedMachine = class {
114547
115450
  opening = null;
114548
115451
  /** A refusal was already answered by asking the remote again; the next one is reported and backed off */
114549
115452
  refusedOnce = false;
115453
+ /** One install, update, rollback or uninstall at a time per machine (the remote also holds `install.lock`) */
115454
+ busy = false;
115455
+ /** That operation and its step, for the row (`MachineInfo.operation`) */
115456
+ operation = null;
115457
+ /**
115458
+ * While an update stops, switches and starts the host there, the link neither reconnects nor
115459
+ * starts a host of its own: it would start the old version between the stop and the switch
115460
+ */
115461
+ held = false;
115462
+ /** Called with the version of every hello, while an update waits for the new one */
115463
+ helloWaiters = /* @__PURE__ */ new Set();
115464
+ /** What the hub installed there, from the last connection line (plan §10.5, S12) */
115465
+ installed = null;
114550
115466
  get id() {
114551
115467
  return this.record.id;
114552
115468
  }
@@ -114570,7 +115486,9 @@ var LinkedMachine = class {
114570
115486
  lastConnectedAt: this.lastConnectedAt,
114571
115487
  localPort: this.endpoint && this.endpoint.localPort > 0 ? this.endpoint.localPort : null,
114572
115488
  sameLocalPort: this.endpoint ? this.endpoint.localPort === this.endpoint.line.port : false,
114573
- hostStarted: this.hostStarted
115489
+ hostStarted: this.hostStarted,
115490
+ install: this.installed,
115491
+ operation: this.operation
114574
115492
  };
114575
115493
  }
114576
115494
  // ── RoutedMachine ───────────────────────────────────────────────────────────────────────
@@ -114614,6 +115532,171 @@ var LinkedMachine = class {
114614
115532
  this.versions = { ...this.versions, accepted: true };
114615
115533
  this.reconnect();
114616
115534
  }
115535
+ /**
115536
+ * Installs the hub's version there (`machines.install`, install.ts, plan §10.2). A host running
115537
+ * there keeps running; a link that is not connected (nothing was installed, or nothing runs) opens
115538
+ * again at once, and finds the managed launcher first
115539
+ */
115540
+ install() {
115541
+ return this.operate("install", this.deps.hub.version, async (c, installer) => {
115542
+ const r = await installRemote({
115543
+ exec: c.exec,
115544
+ spec: c.spec,
115545
+ target: c.target,
115546
+ version: this.deps.hub.version,
115547
+ runtime: installer.runtime(),
115548
+ script: c.script,
115549
+ registry: installer.registry,
115550
+ nodeDist: installer.nodeDist,
115551
+ onStep: c.step
115552
+ });
115553
+ this.deps.log?.(`[links] ${this.name}: installed Centralu ${r.current.version} on Node ${r.current.node}${r.previous ? `; ${r.previous.version} kept to roll back to` : ""}`);
115554
+ return this.installedNow(r);
115555
+ });
115556
+ }
115557
+ /** `machines.update` (update.ts, plan §10.5): install beside, stop, switch, start, check, or put back */
115558
+ update() {
115559
+ return this.operate("update", this.deps.hub.version, async (c, installer) => {
115560
+ const r = await updateRemote(c, {
115561
+ version: this.deps.hub.version,
115562
+ runtime: installer.runtime(),
115563
+ registry: installer.registry,
115564
+ nodeDist: installer.nodeDist
115565
+ });
115566
+ this.deps.log?.(`[links] ${this.name}: updated to Centralu ${r.current.version}${r.previous ? `; ${r.previous.version} kept to roll back to` : ""}`);
115567
+ return this.installedNow(r);
115568
+ });
115569
+ }
115570
+ /** `machines.rollback`: back to `previous`, one step */
115571
+ rollback() {
115572
+ const current = this.installed?.current;
115573
+ const previous = this.installed?.previous;
115574
+ if (!current || !previous) return Promise.reject(linkError2(`There is no earlier version installed on ${this.name} to go back to`));
115575
+ return this.operate("rollback", previous.version, async (c) => {
115576
+ const r = await rollbackRemote(c, { current, previous });
115577
+ this.deps.log?.(`[links] ${this.name}: rolled back to Centralu ${r.current.version}`);
115578
+ return this.installedNow(r);
115579
+ });
115580
+ }
115581
+ /** `machines.uninstall`: stops the host there and removes what the hub installed; the data stays */
115582
+ uninstall() {
115583
+ if (!this.installed?.current) return Promise.reject(linkError2(`Nothing this computer installed is on ${this.name}`));
115584
+ return this.operate("uninstall", null, async (c) => {
115585
+ const r = await uninstallRemote(c);
115586
+ this.installed = { managed: false, current: null, previous: null };
115587
+ this.deps.log?.(`[links] ${this.name}: removed the Centralu this computer installed; its data stays`);
115588
+ return { machine: this.info(), stopped: r.stopped };
115589
+ });
115590
+ }
115591
+ /**
115592
+ * What would stop there if its host stopped now (`host.activity` on the remote), for the "Update"
115593
+ * prompt. Through the link when connected; while the link is held at `versions_differ` on one
115594
+ * protocol, through a short connection of its own. Null when the machine cannot say
115595
+ */
115596
+ activity() {
115597
+ const read = (r) => {
115598
+ const p = HostActivity.safeParse(r);
115599
+ return p.success ? p.data : null;
115600
+ };
115601
+ if (this.client?.connected) return this.client.call("host.activity", {}).then(read, () => null);
115602
+ const e = this.endpoint;
115603
+ if (!e?.url || this.versions?.compatible === false || this.held) return Promise.resolve(null);
115604
+ return new Promise((resolve10) => {
115605
+ let settled = false;
115606
+ const done = (v) => {
115607
+ if (settled) return;
115608
+ settled = true;
115609
+ clearTimeout(timer);
115610
+ client.close();
115611
+ resolve10(v);
115612
+ };
115613
+ const client = new RemoteClient(
115614
+ { ...this.deps.client, url: e.url, token: e.token, handshakeTimeoutMs: 5e3, callTimeoutMs: 5e3 },
115615
+ { hello: () => void client.call("host.activity", {}).then(read, () => null).then(done), refused: () => done(null) }
115616
+ );
115617
+ const timer = setTimeout(() => done(null), 8e3);
115618
+ client.connect();
115619
+ });
115620
+ }
115621
+ installedNow(r) {
115622
+ this.installed = { managed: true, current: r.current, previous: r.previous };
115623
+ return { machine: this.info(), current: r.current, previous: r.previous, removed: r.removed, left: r.left };
115624
+ }
115625
+ /**
115626
+ * Runs one install, update, rollback or uninstall: refused where this hub cannot install (no
115627
+ * installer or shell, a development build, a command of the person's own), one at a time, with its
115628
+ * steps in the row. Afterwards the link opens again, also after a failure
115629
+ */
115630
+ async operate(kind, target, fn2) {
115631
+ const installer = this.deps.installer;
115632
+ const tunnelExec = this.tunnel.exec?.bind(this.tunnel);
115633
+ if (!installer || !tunnelExec) throw linkError2(`This computer cannot install on ${this.name}`);
115634
+ if (this.deps.hub.dev && (kind === "install" || kind === "update")) {
115635
+ throw linkError2(`A development build is not published, so it cannot be installed on ${this.name}; install the latest release there (npm i -g centralu)`);
115636
+ }
115637
+ if (this.record.remote.command) throw linkError2(`${this.name} runs a command of its own in place of centralu; clear it to use an installed one`);
115638
+ if (this.busy) throw linkError2(`${this.name} is busy with ${this.operation?.kind ?? "another change"}; wait until it is done`);
115639
+ this.busy = true;
115640
+ const at2 = Date.now();
115641
+ const step = (s) => {
115642
+ this.deps.log?.(`[links] ${this.name}: ${kind}${target ? ` ${target}` : ""}: ${s}`);
115643
+ this.operation = { kind, step: s, target, at: at2 };
115644
+ this.deps.broadcast({ type: "machine_status", machine: this.info() });
115645
+ };
115646
+ try {
115647
+ return await fn2(this.control(tunnelExec, installer.script(), step), installer);
115648
+ } catch (err) {
115649
+ this.deps.log?.(`[links] ${this.name}: ${kind} failed: ${err.message}`);
115650
+ throw linkError2(err.message);
115651
+ } finally {
115652
+ this.busy = false;
115653
+ const wasHeld = this.held;
115654
+ this.held = false;
115655
+ this.operation = null;
115656
+ if (!this.stopped && (wasHeld || this.status !== "connected")) this.reconnect();
115657
+ this.deps.broadcast({ type: "machine_status", machine: this.info() });
115658
+ }
115659
+ }
115660
+ /** What update.ts runs things there with */
115661
+ control(exec8, script, step) {
115662
+ const hold = async () => {
115663
+ this.held = true;
115664
+ clearTimeout(this.retryTimer);
115665
+ await this.opening?.catch(() => void 0);
115666
+ clearTimeout(this.retryTimer);
115667
+ this.client?.close();
115668
+ this.client = null;
115669
+ this.setStatus("updating", null);
115670
+ };
115671
+ return {
115672
+ exec: exec8,
115673
+ spec: this.record.remote,
115674
+ target: this.record.sshTarget,
115675
+ script,
115676
+ hold,
115677
+ step,
115678
+ start: async () => {
115679
+ if (!this.tunnel.startHost) throw new Error(`This computer cannot start Centralu on ${this.name}`);
115680
+ const started = await this.tunnel.startHost();
115681
+ this.hostStarted = { ...started, at: Date.now() };
115682
+ },
115683
+ answers: (version2, ms3) => new Promise((resolve10) => {
115684
+ const waiter = (v) => {
115685
+ if (v !== version2) return;
115686
+ clearTimeout(timer);
115687
+ this.helloWaiters.delete(waiter);
115688
+ resolve10(true);
115689
+ };
115690
+ const timer = setTimeout(() => {
115691
+ this.helloWaiters.delete(waiter);
115692
+ void hold().then(() => resolve10(false));
115693
+ }, this.deps.checkMs ?? ms3);
115694
+ this.helloWaiters.add(waiter);
115695
+ this.held = false;
115696
+ this.reconnect();
115697
+ })
115698
+ };
115699
+ }
114617
115700
  async stop() {
114618
115701
  this.stopped = true;
114619
115702
  clearTimeout(this.retryTimer);
@@ -114622,6 +115705,7 @@ var LinkedMachine = class {
114622
115705
  await this.tunnel.close();
114623
115706
  }
114624
115707
  open() {
115708
+ if (this.held) return Promise.resolve();
114625
115709
  this.opening ??= this.doOpen().finally(() => {
114626
115710
  this.opening = null;
114627
115711
  });
@@ -114634,7 +115718,9 @@ var LinkedMachine = class {
114634
115718
  this.setStatus("connecting", null);
114635
115719
  let endpoint = await this.openTunnel();
114636
115720
  if (!endpoint) return;
115721
+ this.installed = installOf(endpoint.line.install);
114637
115722
  if (!endpoint.line.hostRunning && this.tunnel.startHost) {
115723
+ if (this.held) return;
114638
115724
  this.setStatus("starting", null);
114639
115725
  this.deps.log?.(`[links] ${this.name}: no Centralu host is running there; starting one (centralu serve --detach)`);
114640
115726
  let started;
@@ -114653,7 +115739,9 @@ var LinkedMachine = class {
114653
115739
  this.setStatus("starting", null);
114654
115740
  endpoint = await this.openTunnel();
114655
115741
  if (!endpoint) return;
115742
+ this.installed = installOf(endpoint.line.install);
114656
115743
  }
115744
+ if (this.held) return;
114657
115745
  this.endpoint = endpoint;
114658
115746
  if (!endpoint.line.hostRunning) {
114659
115747
  this.setStatus("not_running", `Centralu is installed on ${this.name}, but no \`centralu serve\` is running there`);
@@ -114727,6 +115815,7 @@ var LinkedMachine = class {
114727
115815
  }
114728
115816
  async onHello(client, h) {
114729
115817
  if (this.client !== client) return;
115818
+ for (const w of [...this.helloWaiters]) w(h.build?.version ?? null);
114730
115819
  if (h.build) {
114731
115820
  const v = this.check({ version: h.build.version ?? this.versions?.remote.version ?? "unknown", protocolVersion: h.build.protocolVersion, dev: h.build.commit === "dev" });
114732
115821
  if (!mayConnect(v)) return;
@@ -114775,11 +115864,12 @@ var LinkedMachine = class {
114775
115864
  if (this.stopped) return;
114776
115865
  this.client?.close();
114777
115866
  this.client = null;
115867
+ if (this.held) return;
114778
115868
  this.setStatus("unreachable", `The connection to ${this.name} ended (${reason}); reconnecting`);
114779
115869
  this.retryLater();
114780
115870
  }
114781
115871
  retryLater(fixedMs) {
114782
- if (this.stopped) return;
115872
+ if (this.stopped || this.held) return;
114783
115873
  clearTimeout(this.retryTimer);
114784
115874
  const [first, max] = this.deps.retryMs ?? [2e3, 6e4];
114785
115875
  const delay = fixedMs ?? Math.min(max, first * 2 ** this.attempt++);
@@ -114846,6 +115936,21 @@ var Links = class {
114846
115936
  m.acceptVersions();
114847
115937
  return m.info();
114848
115938
  }
115939
+ install(id2) {
115940
+ return this.need(id2).install();
115941
+ }
115942
+ update(id2) {
115943
+ return this.need(id2).update();
115944
+ }
115945
+ rollback(id2) {
115946
+ return this.need(id2).rollback();
115947
+ }
115948
+ uninstall(id2) {
115949
+ return this.need(id2).uninstall();
115950
+ }
115951
+ activity(id2) {
115952
+ return this.need(id2).activity();
115953
+ }
114849
115954
  need(id2) {
114850
115955
  const m = this.machines.get(id2);
114851
115956
  if (!m) throw Object.assign(new Error(`No linked machine ${id2}`), { code: "internal" });
@@ -115067,6 +116172,13 @@ var ROUTES = {
115067
116172
  // Stopping a remote host is `centralu serve --stop` over ssh, never a call through the link
115068
116173
  "host.stop": HUB,
115069
116174
  "machines.acceptVersions": HUB,
116175
+ "machines.install": HUB,
116176
+ "machines.update": HUB,
116177
+ "machines.rollback": HUB,
116178
+ "machines.uninstall": HUB,
116179
+ "machines.activity": HUB,
116180
+ // The hub's own answer; a hub asks a remote's through `machines.activity`
116181
+ "host.activity": HUB,
115070
116182
  // ── Callbacks of the hub's own agents ───────────────────────────────────────────────────
115071
116183
  "orchestrator.tools": INTERNAL,
115072
116184
  "orchestrator.tool": INTERNAL,
@@ -115280,338 +116392,6 @@ function combine(method, acc, r, q2) {
115280
116392
  }
115281
116393
  }
115282
116394
 
115283
- // packages/agent-host/src/links/tunnel.ts
115284
- import { spawn as spawn5 } from "node:child_process";
115285
- import { createConnection as createConnection2, createServer as createServer3 } from "node:net";
115286
- function parseConnectionLine(stdout) {
115287
- const lines = stdout.split("\n").map((l) => l.trim()).filter(Boolean);
115288
- for (let i = lines.length - 1; i >= 0; i--) {
115289
- let raw;
115290
- try {
115291
- raw = JSON.parse(lines[i]);
115292
- } catch {
115293
- continue;
115294
- }
115295
- const o = raw;
115296
- if (o && typeof o === "object" && typeof o.v === "number") {
115297
- if (o.v !== 1) throw new Error(`The remote's Centralu speaks connection format ${o.v}; update Centralu on this computer`);
115298
- if (typeof o.port !== "number" || typeof o.token !== "string" || typeof o.protocolVersion !== "number") {
115299
- throw new Error("The remote answered with an incomplete connection line");
115300
- }
115301
- const install = parseInstall(o.install);
115302
- return {
115303
- v: 1,
115304
- port: o.port,
115305
- token: o.token,
115306
- version: typeof o.version === "string" ? o.version : "unknown",
115307
- protocolVersion: o.protocolVersion,
115308
- dataDir: typeof o.dataDir === "string" ? o.dataDir : "",
115309
- hostRunning: o.hostRunning === true,
115310
- ...install ? { install } : {}
115311
- };
115312
- }
115313
- }
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)");
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
- }
115341
- var WSL_DISTRO_RE = /^[A-Za-z0-9._-]{1,64}$/;
115342
- function powershell(script) {
115343
- const full = `[Console]::OutputEncoding=[Text.Encoding]::UTF8
115344
- ${script}`;
115345
- return `powershell -NoProfile -NonInteractive -EncodedCommand ${Buffer.from(full, "utf16le").toString("base64")}`;
115346
- }
115347
- function wslKeepAlive(spec) {
115348
- if (spec?.shell !== "wsl") return null;
115349
- const distro = spec.wslDistro ?? "";
115350
- if (!WSL_DISTRO_RE.test(distro)) throw new Error(`Not a WSL distro name: ${distro || "(none)"}`);
115351
- return `wsl.exe -d ${distro} --exec sleep infinity`;
115352
- }
115353
- var NOT_FOUND2 = "CENTRALU-NOT-FOUND";
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) {
115364
- const custom2 = spec.command?.trim() || null;
115365
- if (spec.shell === "powershell") {
115366
- if (custom2) return powershell(`${custom2} ${args}`);
115367
- return powershell(
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}' } }`
115369
- );
115370
- }
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}'`;
115373
- const distro = spec.wslDistro ?? "";
115374
- if (!WSL_DISTRO_RE.test(distro)) throw new Error(`Not a WSL distro name: ${distro || "(none)"}`);
115375
- const script = custom2 ? `${custom2} ${args}` : `PATH=$(printf %s "$PATH" | tr : '\\n' | grep -v '^/mnt/' | paste -sd: -); ${lookup}`;
115376
- const b64 = Buffer.from(script, "utf8").toString("base64");
115377
- return powershell(`wsl.exe -d '${distro}' -- bash -lc 'echo ${b64} | base64 -d | bash -l'`);
115378
- }
115379
- var SshTunnel = class {
115380
- constructor(opts) {
115381
- this.opts = opts;
115382
- if (!opts.target.trim() || opts.target.startsWith("-")) throw new Error("Not an ssh target");
115383
- }
115384
- opts;
115385
- child = null;
115386
- /** The asking ssh runs, while they run */
115387
- asking = /* @__PURE__ */ new Set();
115388
- downListeners = [];
115389
- closing = false;
115390
- /** The remote cannot start a host outside a session (WMI blocked): the forward runs it (`startHost`) */
115391
- linkBound = false;
115392
- async open() {
115393
- await this.stopChild();
115394
- this.closing = false;
115395
- const line = await this.connectionLine().catch((err) => {
115396
- this.ifClosed();
115397
- throw err;
115398
- });
115399
- this.ifClosed();
115400
- if (!line.hostRunning && !this.linkBound) {
115401
- return { url: "", token: line.token, line, localPort: 0 };
115402
- }
115403
- const hosting = !line.hostRunning;
115404
- let localPort = await portFree(line.port) ? line.port : await freePort();
115405
- try {
115406
- await this.forward(localPort, line.port, hosting);
115407
- } catch (err) {
115408
- if (localPort !== line.port) throw err;
115409
- this.ifClosed();
115410
- localPort = await freePort();
115411
- await this.forward(localPort, line.port, hosting);
115412
- }
115413
- if (this.closing) {
115414
- await this.stopChild();
115415
- this.ifClosed();
115416
- }
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
- }
115469
- }
115470
- onDown(listener) {
115471
- this.downListeners.push(listener);
115472
- }
115473
- async close() {
115474
- this.closing = true;
115475
- for (const c of this.asking) c.kill("SIGTERM");
115476
- await this.stopChild();
115477
- }
115478
- ifClosed() {
115479
- if (this.closing) throw new Error(`The link to ${this.opts.target} was closed`);
115480
- }
115481
- run(args) {
115482
- return new Promise((resolve10, reject) => {
115483
- const child = spawn5(this.opts.ssh ?? "ssh", args, { env: this.opts.env ?? process.env, stdio: ["ignore", "pipe", "pipe"] });
115484
- this.asking.add(child);
115485
- child.once("exit", () => this.asking.delete(child));
115486
- let stdout = "";
115487
- let stderr = "";
115488
- child.stdout.on("data", (d2) => stdout += String(d2));
115489
- child.stderr.on("data", (d2) => stderr += String(d2));
115490
- child.once("error", reject);
115491
- child.once("close", (code2) => resolve10({ code: code2, stdout, stderr }));
115492
- });
115493
- }
115494
- config() {
115495
- return this.opts.configFile ? ["-F", this.opts.configFile] : [];
115496
- }
115497
- async connectionLine() {
115498
- const remote = this.opts.remote ?? { shell: "posix" };
115499
- const base2 = [...this.config(), "-T", "-o", "BatchMode=yes", "-o", "ConnectTimeout=15", "--", this.opts.target];
115500
- const r = await this.run([...base2, connectionCommand(remote)]);
115501
- if (r.code === 255) throw new Error(`ssh could not reach ${this.opts.target}: ${lastLine(r.stderr) || "no answer"}`);
115502
- if (r.stdout.includes(NOT_FOUND2) || r.code === 127 && !remote.command) {
115503
- throw new Error(`Centralu is not installed on ${this.opts.target} (run \`npm i -g centralu\` there, then \`centralu serve\` once)`);
115504
- }
115505
- try {
115506
- return parseConnectionLine(r.stdout);
115507
- } catch (err) {
115508
- const why = lastLine(r.stderr);
115509
- throw new Error(`${err.message}${why ? `: ${why}` : r.code ? ` (exit ${r.code})` : ""}`);
115510
- }
115511
- }
115512
- forward(localPort, remotePort, hosting = false) {
115513
- this.ifClosed();
115514
- const keepAlive = hosting ? serveCommand(this.opts.remote ?? { shell: "posix" }) : wslKeepAlive(this.opts.remote);
115515
- const args = [
115516
- ...this.config(),
115517
- ...keepAlive ? ["-T"] : ["-N"],
115518
- "-o",
115519
- "BatchMode=yes",
115520
- "-o",
115521
- "ExitOnForwardFailure=yes",
115522
- "-o",
115523
- "ServerAliveInterval=15",
115524
- "-o",
115525
- "ServerAliveCountMax=3",
115526
- "-o",
115527
- "GatewayPorts=no",
115528
- "-L",
115529
- `127.0.0.1:${localPort}:127.0.0.1:${remotePort}`,
115530
- "--",
115531
- this.opts.target,
115532
- ...keepAlive ? [keepAlive] : []
115533
- ];
115534
- const child = spawn5(this.opts.ssh ?? "ssh", args, { env: this.opts.env ?? process.env, stdio: ["ignore", "ignore", "pipe"] });
115535
- this.child = child;
115536
- let stderr = "";
115537
- child.stderr.on("data", (d2) => stderr += String(d2));
115538
- return new Promise((resolve10, reject) => {
115539
- let settled = false;
115540
- const deadline = Date.now() + (this.opts.forwardTimeoutMs ?? 2e4);
115541
- const fail4 = (message) => {
115542
- if (settled) return;
115543
- settled = true;
115544
- reject(new Error(message));
115545
- };
115546
- child.once("error", (e) => fail4(`Could not start ssh: ${e.message}`));
115547
- child.once("exit", (code2, signal) => {
115548
- if (this.child === child) this.child = null;
115549
- if (!settled) return fail4(`The ssh forward to ${this.opts.target} ended (${signal ?? code2}): ${lastLine(stderr) || "no reason given"}`);
115550
- if (!this.closing) for (const l of this.downListeners) l(`ssh exited (${signal ?? code2})${stderr ? `: ${lastLine(stderr)}` : ""}`);
115551
- });
115552
- const poll = async () => {
115553
- while (!settled) {
115554
- if (await accepts(localPort)) {
115555
- settled = true;
115556
- this.opts.log?.(`[links] forward up: 127.0.0.1:${localPort} -> ${this.opts.target}:127.0.0.1:${remotePort}`);
115557
- return resolve10();
115558
- }
115559
- if (Date.now() > deadline) {
115560
- child.kill("SIGTERM");
115561
- return fail4(`The ssh forward to ${this.opts.target} did not come up in time`);
115562
- }
115563
- await new Promise((r) => setTimeout(r, 100));
115564
- }
115565
- };
115566
- void poll();
115567
- });
115568
- }
115569
- async stopChild() {
115570
- const child = this.child;
115571
- this.child = null;
115572
- if (!child || child.exitCode !== null || child.signalCode !== null) return;
115573
- await new Promise((resolve10) => {
115574
- child.once("exit", () => resolve10());
115575
- child.kill("SIGTERM");
115576
- setTimeout(() => {
115577
- child.kill("SIGKILL");
115578
- resolve10();
115579
- }, 2e3).unref();
115580
- });
115581
- }
115582
- };
115583
- function lastLine(s) {
115584
- const lines = s.replace(/#< CLIXML[\s\S]*$/, "").split(/\r?\n/).map((l) => l.trim()).filter((l) => l && !l.startsWith("**") && !/^Node\.js v\d/.test(l) && !/^at /.test(l));
115585
- return [...lines].reverse().find((l) => /error|denied|refused|timed out|not found|cannot|could not/i.test(l)) ?? lines.at(-1) ?? "";
115586
- }
115587
- function accepts(port2) {
115588
- return new Promise((resolve10) => {
115589
- const s = createConnection2({ host: "127.0.0.1", port: port2 });
115590
- s.once("connect", () => {
115591
- s.destroy();
115592
- resolve10(true);
115593
- });
115594
- s.once("error", () => resolve10(false));
115595
- });
115596
- }
115597
- function portFree(port2) {
115598
- return new Promise((resolve10) => {
115599
- const probe = createServer3();
115600
- probe.once("error", () => resolve10(false));
115601
- probe.listen(port2, "127.0.0.1", () => probe.close(() => resolve10(true)));
115602
- });
115603
- }
115604
- function freePort() {
115605
- return new Promise((resolve10, reject) => {
115606
- const probe = createServer3();
115607
- probe.once("error", reject);
115608
- probe.listen(0, "127.0.0.1", () => {
115609
- const { port: port2 } = probe.address();
115610
- probe.close(() => resolve10(port2));
115611
- });
115612
- });
115613
- }
115614
-
115615
116395
  // packages/agent-host/src/links/stored.ts
115616
116396
  function storeRegistry(store2) {
115617
116397
  return {
@@ -115639,7 +116419,7 @@ function storeMirror(store2) {
115639
116419
  }
115640
116420
 
115641
116421
  // packages/agent-host/src/main.ts
115642
- var BUILD = true ? "b974cd2" : "dev";
116422
+ var BUILD = true ? "1a563a8" : "dev";
115643
116423
  var underKeeper = process.env.CC_KEEPER === "1";
115644
116424
  var keeperSource = process.env.CC_HOST_SOURCE;
115645
116425
  var frontDoor = process.env.CC_FRONT_DOOR;
@@ -115879,6 +116659,7 @@ var links = new Links({
115879
116659
  mirror: storeMirror(store),
115880
116660
  registry: storeRegistry(store),
115881
116661
  tunnelFor: (record3) => new SshTunnel({ target: record3.sshTarget, remote: record3.remote, log: (line) => console.error(line) }),
116662
+ installer: { runtime: remoteRuntime, script: installScript },
115882
116663
  log: (line) => console.error(line)
115883
116664
  });
115884
116665
  mgr.useLinkedSessions((id2) => links.knowsSession(id2));
@@ -115894,7 +116675,8 @@ var router = new Router({
115894
116675
  inlineViews,
115895
116676
  heldPids: held2?.heldPids,
115896
116677
  machines: links,
115897
- stopHost: startedByServe && !underKeeper ? () => stopFromRequest() : void 0
116678
+ stopHost: startedByServe && !underKeeper ? () => stopFromRequest() : void 0,
116679
+ activity: () => activityCounts(activity())
115898
116680
  }),
115899
116681
  machines: () => links.all()
115900
116682
  });