@centralu/darwin-arm64 0.1.0-beta.12 → 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.
- package/Centralu.app/Contents/Info.plist +2 -2
- package/Centralu.app/Contents/MacOS/centralu +0 -0
- package/Centralu.app/Contents/MacOS/centralu-keeper +0 -0
- package/Centralu.app/Contents/Resources/content/centralu-keeper +0 -0
- package/Centralu.app/Contents/Resources/content/content-manifest.json +18 -6
- package/Centralu.app/Contents/Resources/content/content-manifest.json.sig +1 -1
- package/Centralu.app/Contents/Resources/content/host/bundle-info.json +2 -2
- package/Centralu.app/Contents/Resources/content/host/main.mjs +1397 -391
- package/Centralu.app/Contents/Resources/content/host/remote-install.mjs +290 -0
- package/Centralu.app/Contents/Resources/content/host/remote-runtime.json +21 -0
- package/Centralu.app/Contents/Resources/resources/host/bundle-info.json +2 -2
- package/Centralu.app/Contents/Resources/resources/host/main.mjs +1397 -391
- package/Centralu.app/Contents/Resources/resources/host/remote-install.mjs +290 -0
- package/Centralu.app/Contents/Resources/resources/host/remote-runtime.json +21 -0
- package/Centralu.app/Contents/_CodeSignature/CodeResources +60 -16
- package/package.json +1 -1
|
@@ -42881,12 +42881,12 @@ new Anthropic({ apiKey, dangerouslyAllowBrowser: true });
|
|
|
42881
42881
|
import { parseArgs } from "node:util";
|
|
42882
42882
|
import { randomBytes as randomBytes6 } from "node:crypto";
|
|
42883
42883
|
import { homedir as homedir11 } from "node:os";
|
|
42884
|
-
import { dirname as dirname11, join as
|
|
42884
|
+
import { dirname as dirname11, join as join30 } from "node:path";
|
|
42885
42885
|
import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync10, writeSync as writeSync3 } from "node:fs";
|
|
42886
42886
|
|
|
42887
42887
|
// packages/protocol/src/brand.ts
|
|
42888
42888
|
var APP_NAME = "Centralu";
|
|
42889
|
-
var APP_VERSION = "0.1.0-beta.
|
|
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"]);
|
|
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 */
|
|
@@ -58170,7 +58178,42 @@ var MachineInfo = external_exports.object({
|
|
|
58170
58178
|
* remote machine (phase 2) need them to match (docs/agent-host.md §4.7).
|
|
58171
58179
|
*/
|
|
58172
58180
|
localPort: external_exports.number().int().nullable().default(null),
|
|
58173
|
-
sameLocalPort: external_exports.boolean().default(false)
|
|
58181
|
+
sameLocalPort: external_exports.boolean().default(false),
|
|
58182
|
+
/**
|
|
58183
|
+
* The hub started the host there, because it found none running (plan §10.9, decision 7), and how:
|
|
58184
|
+
* `detached` lives on whatever happens to the link; `link_bound` runs in the link's own ssh session,
|
|
58185
|
+
* because that machine blocks starting a process through WMI, and ends when the link does (`note`
|
|
58186
|
+
* says why). Null when the host was already running. Additive; an older hub sends nothing
|
|
58187
|
+
*/
|
|
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)
|
|
58174
58217
|
});
|
|
58175
58218
|
|
|
58176
58219
|
// packages/protocol/src/events.ts
|
|
@@ -60747,6 +60790,51 @@ var RpcMethods = {
|
|
|
60747
60790
|
* question comes again. Refused when the protocols differ.
|
|
60748
60791
|
*/
|
|
60749
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 },
|
|
60830
|
+
/**
|
|
60831
|
+
* Stops this host the way a signal would: agents, terminals, command runs and app processes
|
|
60832
|
+
* stopped in order, the store closed, then exit. For `centralu serve --stop` (docs/agent-host.md
|
|
60833
|
+
* §4.7): on Windows a launcher can only be killed, and killing it ends the host without its
|
|
60834
|
+
* shutdown. Answers before the host starts to leave. Refused by a host under the app's keeper,
|
|
60835
|
+
* which the app stops (and which would only start the host again).
|
|
60836
|
+
*/
|
|
60837
|
+
"host.stop": { params: external_exports.object({}), result: external_exports.object({ ok: external_exports.literal(true) }) },
|
|
60750
60838
|
/**
|
|
60751
60839
|
* The trash (#204). Only the person reaches these: the RPC is the UI's channel, and neither the agents' tools nor
|
|
60752
60840
|
* the apps' broker has a trash or purge verb. `agents.deleteSession` is the way in.
|
|
@@ -60920,6 +61008,17 @@ function sessionBusy(s) {
|
|
|
60920
61008
|
function hostBusy(s) {
|
|
60921
61009
|
return s.terminals > 0 || s.commandRuns > 0 || s.sessions.some(sessionBusy);
|
|
60922
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
|
+
}
|
|
60923
61022
|
function sessionIdle(s, process4) {
|
|
60924
61023
|
if (s.pendingApproval) return { idle: false, reason: "approval" };
|
|
60925
61024
|
if (s.pendingQuestions.length > 0) return { idle: false, reason: "question" };
|
|
@@ -62263,8 +62362,9 @@ function restoreViewHandover(settings, views2, inline, now = Date.now()) {
|
|
|
62263
62362
|
if (!(age2 >= 0 && age2 <= VIEW_HANDOVER_MAX_AGE_MS)) return { restored: 0, skipped: list.length };
|
|
62264
62363
|
const opened = new Set(views2.restore(list));
|
|
62265
62364
|
for (const v of list) {
|
|
62365
|
+
if (typeof v !== "object" || v === null || !opened.has(v.id)) continue;
|
|
62266
62366
|
const o = v.inline;
|
|
62267
|
-
if (!
|
|
62367
|
+
if (!o || typeof o.sessionId !== "string" || typeof o.callId !== "string" || typeof o.tool !== "string") continue;
|
|
62268
62368
|
inline?.adopt({ sessionId: o.sessionId, callId: o.callId, ref: v.app, tool: o.tool, uri: v.uri, instanceId: v.id });
|
|
62269
62369
|
}
|
|
62270
62370
|
return { restored: opened.size, skipped: list.length - opened.size };
|
|
@@ -62354,15 +62454,15 @@ function buildViewCsp(csp) {
|
|
|
62354
62454
|
const frame = sanitizeDomains(csp?.frameDomains);
|
|
62355
62455
|
const base2 = sanitizeDomains(csp?.baseUriDomains);
|
|
62356
62456
|
const r = resource.kept.join(" ");
|
|
62357
|
-
const
|
|
62457
|
+
const join31 = (...parts) => parts.filter(Boolean).join(" ");
|
|
62358
62458
|
const policy = [
|
|
62359
62459
|
"default-src 'none'",
|
|
62360
|
-
`script-src ${
|
|
62361
|
-
`style-src ${
|
|
62362
|
-
`img-src ${
|
|
62363
|
-
`font-src ${
|
|
62364
|
-
`media-src ${
|
|
62365
|
-
`worker-src ${
|
|
62460
|
+
`script-src ${join31("'unsafe-inline'", r)}`,
|
|
62461
|
+
`style-src ${join31("'unsafe-inline'", r)}`,
|
|
62462
|
+
`img-src ${join31("data: blob:", r)}`,
|
|
62463
|
+
`font-src ${join31("data:", r)}`,
|
|
62464
|
+
`media-src ${join31("data: blob:", r)}`,
|
|
62465
|
+
`worker-src ${join31("blob:", r)}`,
|
|
62366
62466
|
`connect-src ${connect2.kept.length ? connect2.kept.join(" ") : "'none'"}`,
|
|
62367
62467
|
`frame-src ${frame.kept.length ? frame.kept.join(" ") : "'none'"}`,
|
|
62368
62468
|
"form-action 'none'",
|
|
@@ -62891,6 +62991,7 @@ import { devNull } from "node:os";
|
|
|
62891
62991
|
import { promisify } from "node:util";
|
|
62892
62992
|
|
|
62893
62993
|
// packages/agent-host/src/tool-launch.ts
|
|
62994
|
+
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
62894
62995
|
import { existsSync as existsSync3, readFileSync } from "node:fs";
|
|
62895
62996
|
import { win32 as win323 } from "node:path";
|
|
62896
62997
|
|
|
@@ -62987,7 +63088,14 @@ var realDeps = () => ({
|
|
|
62987
63088
|
platform: process.platform,
|
|
62988
63089
|
read: (path) => readFileSync(path, "utf8"),
|
|
62989
63090
|
exists: existsSync3,
|
|
62990
|
-
node: process.execPath
|
|
63091
|
+
node: process.execPath,
|
|
63092
|
+
runJs: (script) => {
|
|
63093
|
+
try {
|
|
63094
|
+
return execFileSync2(process.execPath, [script], { encoding: "utf8", timeout: 1e4, windowsHide: true });
|
|
63095
|
+
} catch {
|
|
63096
|
+
return null;
|
|
63097
|
+
}
|
|
63098
|
+
}
|
|
62991
63099
|
});
|
|
62992
63100
|
function launchFor(path, deps = realDeps()) {
|
|
62993
63101
|
const plain = { command: path, args: [] };
|
|
@@ -62998,10 +63106,10 @@ function launchFor(path, deps = realDeps()) {
|
|
|
62998
63106
|
} catch {
|
|
62999
63107
|
return plain;
|
|
63000
63108
|
}
|
|
63109
|
+
const dir = win323.dirname(path);
|
|
63001
63110
|
const relative10 = shimTarget(text2);
|
|
63002
|
-
|
|
63003
|
-
|
|
63004
|
-
if (!deps.exists(target)) return plain;
|
|
63111
|
+
const target = relative10 ? win323.resolve(dir, relative10) : nodeShimEntry(text2, dir, deps);
|
|
63112
|
+
if (!target || !deps.exists(target)) return plain;
|
|
63005
63113
|
if (/\.(c|m)?js$/i.test(target)) return { command: deps.node, args: [target] };
|
|
63006
63114
|
if (/\.exe$/i.test(target)) return { command: target, args: [] };
|
|
63007
63115
|
return plain;
|
|
@@ -63014,6 +63122,20 @@ function shimTarget(text2) {
|
|
|
63014
63122
|
}
|
|
63015
63123
|
return null;
|
|
63016
63124
|
}
|
|
63125
|
+
function nodeShimEntry(text2, dir, deps) {
|
|
63126
|
+
const started = /^\s*"%NODE_EXE%"\s+"%(\w+)%"\s+%\*\s*$/m.exec(text2)?.[1];
|
|
63127
|
+
if (!started) return null;
|
|
63128
|
+
const set3 = (name) => new RegExp(`^\\s*SET\\s+"${name}=%~dp0\\\\?([^"%]+)"`, "im").exec(text2)?.[1]?.trim() ?? null;
|
|
63129
|
+
const relative10 = set3(started);
|
|
63130
|
+
if (!relative10) return null;
|
|
63131
|
+
const base2 = win323.resolve(dir, relative10);
|
|
63132
|
+
const prefixJs = set3("NPM_PREFIX_JS");
|
|
63133
|
+
if (!prefixJs || !deps.runJs) return base2;
|
|
63134
|
+
const prefix = deps.runJs(win323.resolve(dir, prefixJs))?.trim().split(/\r?\n/).pop()?.trim();
|
|
63135
|
+
if (!prefix || !win323.isAbsolute(prefix)) return base2;
|
|
63136
|
+
const inPrefix = win323.join(prefix, "node_modules", "npm", "bin", win323.basename(base2));
|
|
63137
|
+
return deps.exists(inPrefix) ? inPrefix : base2;
|
|
63138
|
+
}
|
|
63017
63139
|
function resolveCommand(command, env = process.env, deps = realDeps()) {
|
|
63018
63140
|
if (deps.platform !== "win32") return { command, args: [] };
|
|
63019
63141
|
if (/[\\/]/.test(command)) return launchFor(command, deps);
|
|
@@ -63103,6 +63225,31 @@ async function runGit(cwd, args, opts = {}) {
|
|
|
63103
63225
|
return stdout;
|
|
63104
63226
|
}
|
|
63105
63227
|
|
|
63228
|
+
// packages/agent-host/src/os.ts
|
|
63229
|
+
import { homedir as homedir3 } from "node:os";
|
|
63230
|
+
import { posix as posix2, win32 as win324 } from "node:path";
|
|
63231
|
+
var HOST_PLATFORM = process.platform;
|
|
63232
|
+
function hasPs(platform = HOST_PLATFORM) {
|
|
63233
|
+
return platform !== "win32";
|
|
63234
|
+
}
|
|
63235
|
+
function clonesFiles(platform = HOST_PLATFORM) {
|
|
63236
|
+
return platform === "darwin";
|
|
63237
|
+
}
|
|
63238
|
+
function locksRunningPrograms(platform = HOST_PLATFORM) {
|
|
63239
|
+
return platform === "win32";
|
|
63240
|
+
}
|
|
63241
|
+
function pathsOf(platform = HOST_PLATFORM) {
|
|
63242
|
+
return platform === "win32" ? win324 : posix2;
|
|
63243
|
+
}
|
|
63244
|
+
function installedCopyPath(platform = HOST_PLATFORM, env = process.env, home = homedir3()) {
|
|
63245
|
+
if (platform === "darwin") return `/Applications/${APP_NAME}.app`;
|
|
63246
|
+
if (platform === "win32") {
|
|
63247
|
+
const local = env.LOCALAPPDATA || win324.join(home, "AppData", "Local");
|
|
63248
|
+
return win324.join(local, "Programs", APP_NAME);
|
|
63249
|
+
}
|
|
63250
|
+
return posix2.join(home, ".local/share/applications", `${APP_SLUG}.desktop`);
|
|
63251
|
+
}
|
|
63252
|
+
|
|
63106
63253
|
// packages/agent-host/src/dev-services/fs.ts
|
|
63107
63254
|
var MAX_TEXT = 2e6;
|
|
63108
63255
|
var MAX_IMAGE_PREVIEW = IMAGE_PREVIEW_MAX_BYTES;
|
|
@@ -63276,7 +63423,7 @@ async function copyTree(src, dst, clone2 = cloneTree) {
|
|
|
63276
63423
|
cpSync(src, dst, { recursive: true, verbatimSymlinks: true });
|
|
63277
63424
|
}
|
|
63278
63425
|
async function cloneTree(src, dst) {
|
|
63279
|
-
if (
|
|
63426
|
+
if (!clonesFiles()) return false;
|
|
63280
63427
|
return new Promise((done) => {
|
|
63281
63428
|
const p = spawn("/bin/cp", ["-Rc", src, dst], { stdio: "ignore" });
|
|
63282
63429
|
p.on("error", () => done(false));
|
|
@@ -78556,7 +78703,7 @@ var SseError = class extends Error {
|
|
|
78556
78703
|
};
|
|
78557
78704
|
|
|
78558
78705
|
// packages/agent-host/src/dev-services/kill-tree.ts
|
|
78559
|
-
import { execFile as execFile2, execFileSync as
|
|
78706
|
+
import { execFile as execFile2, execFileSync as execFileSync3 } from "node:child_process";
|
|
78560
78707
|
import { join as join6 } from "node:path";
|
|
78561
78708
|
import { promisify as promisify2 } from "node:util";
|
|
78562
78709
|
var KILL_GRACE_MS = 3e3;
|
|
@@ -78614,16 +78761,19 @@ function survivorTargets(rows, first, self2) {
|
|
|
78614
78761
|
}
|
|
78615
78762
|
function snapshot() {
|
|
78616
78763
|
try {
|
|
78617
|
-
return parsePs(
|
|
78764
|
+
return parsePs(execFileSync3("ps", ["-A", "-o", "pid=,ppid=,pgid="], { encoding: "utf8", timeout: 2e3 }));
|
|
78618
78765
|
} catch {
|
|
78619
78766
|
return [];
|
|
78620
78767
|
}
|
|
78621
78768
|
}
|
|
78769
|
+
function hasProcessGroups(platform = process.platform) {
|
|
78770
|
+
return platform !== "win32";
|
|
78771
|
+
}
|
|
78622
78772
|
var REAL_OS = {
|
|
78623
78773
|
platform: process.platform,
|
|
78624
78774
|
taskkill: (pid) => {
|
|
78625
78775
|
const exe = join6(process.env.SystemRoot ?? "C:\\Windows", "System32", "taskkill.exe");
|
|
78626
|
-
|
|
78776
|
+
execFileSync3(exe, ["/PID", String(pid), "/T", "/F"], { stdio: "ignore", timeout: 5e3, windowsHide: true });
|
|
78627
78777
|
}
|
|
78628
78778
|
};
|
|
78629
78779
|
function shootWindows(handle, os2) {
|
|
@@ -79009,7 +79159,7 @@ var AppProcess = class _AppProcess {
|
|
|
79009
79159
|
* has no groups to gain, and its tree is ended with `taskkill /T` (kill-tree.ts). Attached and
|
|
79010
79160
|
* hidden, the app shares the host's windowless console.
|
|
79011
79161
|
*/
|
|
79012
|
-
detached:
|
|
79162
|
+
detached: hasProcessGroups(),
|
|
79013
79163
|
windowsHide: true
|
|
79014
79164
|
});
|
|
79015
79165
|
const proc = new _AppProcess(child, child.stdio[3] ?? null, log, spec.probeTimeoutMs, spawnedAt);
|
|
@@ -79076,7 +79226,7 @@ var AppProcess = class _AppProcess {
|
|
|
79076
79226
|
this.signalOwnGroup();
|
|
79077
79227
|
}
|
|
79078
79228
|
this.fd3?.destroy();
|
|
79079
|
-
const leftovers =
|
|
79229
|
+
const leftovers = !hasProcessGroups() && !this.alive ? this.collectLeftovers() : null;
|
|
79080
79230
|
this.leftovers = leftovers;
|
|
79081
79231
|
if (leftovers && opts.awaitTree) await leftovers;
|
|
79082
79232
|
if (leftovers && !opts.awaitTree) void leftovers.finally(() => this.log.close());
|
|
@@ -94884,7 +95034,8 @@ var AppVersions2 = class {
|
|
|
94884
95034
|
for (const name of names) {
|
|
94885
95035
|
if (name.startsWith(".")) continue;
|
|
94886
95036
|
try {
|
|
94887
|
-
|
|
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);
|
|
94888
95039
|
} catch {
|
|
94889
95040
|
}
|
|
94890
95041
|
}
|
|
@@ -98040,6 +98191,19 @@ var SKILL_PROPOSALS_KEY = "orchestrator_skill_proposals";
|
|
|
98040
98191
|
var SKILLS_KEY = "orchestrator_skills";
|
|
98041
98192
|
var SKILL_MAX_COUNT = 10;
|
|
98042
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
|
+
}
|
|
98043
98207
|
var OrchestratorProposals = class {
|
|
98044
98208
|
constructor(store2, apps) {
|
|
98045
98209
|
this.store = store2;
|
|
@@ -98049,12 +98213,7 @@ var OrchestratorProposals = class {
|
|
|
98049
98213
|
apps;
|
|
98050
98214
|
/** MCP server proposals waiting on the person's approval */
|
|
98051
98215
|
mcpProposals() {
|
|
98052
|
-
|
|
98053
|
-
const raw = this.store.appSetting(MCP_PROPOSALS_KEY);
|
|
98054
|
-
return raw ? JSON.parse(raw) : [];
|
|
98055
|
-
} catch {
|
|
98056
|
-
return [];
|
|
98057
|
-
}
|
|
98216
|
+
return storedList(this.store.appSetting(MCP_PROPOSALS_KEY), (x) => isText(x.name) && isText(x.command) && isTextList(x.args));
|
|
98058
98217
|
}
|
|
98059
98218
|
/** Saves an MCP server proposal (propose_mcp_server) — validation and persistence only */
|
|
98060
98219
|
proposeMcpServer(spec) {
|
|
@@ -98154,21 +98313,11 @@ var OrchestratorProposals = class {
|
|
|
98154
98313
|
}
|
|
98155
98314
|
/** Skill proposals waiting on the person's approval (#71) */
|
|
98156
98315
|
skillProposals() {
|
|
98157
|
-
|
|
98158
|
-
const raw = this.store.appSetting(SKILL_PROPOSALS_KEY);
|
|
98159
|
-
return raw ? JSON.parse(raw) : [];
|
|
98160
|
-
} catch {
|
|
98161
|
-
return [];
|
|
98162
|
-
}
|
|
98316
|
+
return storedList(this.store.appSetting(SKILL_PROPOSALS_KEY), (x) => isText(x.name) && isText(x.content));
|
|
98163
98317
|
}
|
|
98164
98318
|
/** Skills that have been approved and loaded into the orchestrator's role prompt (#71) */
|
|
98165
98319
|
orchestratorSkills() {
|
|
98166
|
-
|
|
98167
|
-
const raw = this.store.appSetting(SKILLS_KEY);
|
|
98168
|
-
return raw ? JSON.parse(raw) : [];
|
|
98169
|
-
} catch {
|
|
98170
|
-
return [];
|
|
98171
|
-
}
|
|
98320
|
+
return storedList(this.store.appSetting(SKILLS_KEY), (x) => isText(x.name) && isText(x.content));
|
|
98172
98321
|
}
|
|
98173
98322
|
/**
|
|
98174
98323
|
* Turns approved skills into a block appended to the role prompt (#71). This is tool-agnostic text
|
|
@@ -99264,7 +99413,7 @@ function orchestratorMemory(reader, sessionId) {
|
|
|
99264
99413
|
}
|
|
99265
99414
|
|
|
99266
99415
|
// packages/agent-host/src/sessions/manager.ts
|
|
99267
|
-
import { homedir as
|
|
99416
|
+
import { homedir as homedir5 } from "node:os";
|
|
99268
99417
|
import { basename as basename2, isAbsolute as isAbsolute6, join as join20, relative as relative7, resolve as resolve4, sep as sep7 } from "node:path";
|
|
99269
99418
|
import { existsSync as existsSync14, statSync as statSync9 } from "node:fs";
|
|
99270
99419
|
import { exec as exec3 } from "node:child_process";
|
|
@@ -101849,11 +101998,17 @@ var Store = class _Store {
|
|
|
101849
101998
|
forgetProjectConsent(fromProjectId, toProjectId, kind) {
|
|
101850
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;
|
|
101851
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
|
+
*/
|
|
101852
102006
|
listProjectConsents() {
|
|
101853
|
-
|
|
102007
|
+
const rows = this.db.prepare(
|
|
101854
102008
|
`SELECT from_project_id as fromProjectId, to_project_id as toProjectId, kind, decided_at as decidedAt
|
|
101855
102009
|
FROM project_consents ORDER BY decided_at DESC`
|
|
101856
102010
|
).all();
|
|
102011
|
+
return rows.filter((r) => ProjectConsentKind.safeParse(r.kind).success);
|
|
101857
102012
|
}
|
|
101858
102013
|
// ── Linked machines (#82, v46) ─────────────────────────────────────────────────────────────
|
|
101859
102014
|
listLinkedMachines() {
|
|
@@ -102482,8 +102637,8 @@ async function sweepAttachments(maxBytes = ATTACHMENTS_MAX_BYTES) {
|
|
|
102482
102637
|
// packages/agent-host/src/dev-services/message-image.ts
|
|
102483
102638
|
import { constants as constants5 } from "node:fs";
|
|
102484
102639
|
import { open as open2, realpath as realpath3 } from "node:fs/promises";
|
|
102485
|
-
import { homedir as
|
|
102486
|
-
import { posix as
|
|
102640
|
+
import { homedir as homedir4 } from "node:os";
|
|
102641
|
+
import { posix as posix3, sep as sep6, win32 as win325 } from "node:path";
|
|
102487
102642
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
102488
102643
|
var OPEN_FLAGS = constants5.O_RDONLY | constants5.O_NONBLOCK | (constants5.O_NOFOLLOW ?? 0);
|
|
102489
102644
|
var MESSAGE_IMAGE_KINDS = "PNG, JPEG, GIF or WebP";
|
|
@@ -102518,7 +102673,7 @@ function mentions(text2, written) {
|
|
|
102518
102673
|
}
|
|
102519
102674
|
return false;
|
|
102520
102675
|
}
|
|
102521
|
-
function resolveWrittenPath(written, cwd, home =
|
|
102676
|
+
function resolveWrittenPath(written, cwd, home = homedir4(), p = sep6 === "\\" ? win325 : posix3) {
|
|
102522
102677
|
const windows = p.sep === "\\";
|
|
102523
102678
|
let abs;
|
|
102524
102679
|
if (/^file:/i.test(written)) {
|
|
@@ -102693,7 +102848,7 @@ var EXTERNAL_LIST_LIMIT = 200;
|
|
|
102693
102848
|
var builderKey = (ref) => `${ref.projectId ?? "_user"}/${ref.appId}`;
|
|
102694
102849
|
var APP_BUILDERS_KEY = "apps.builders";
|
|
102695
102850
|
var SessionManager = class _SessionManager {
|
|
102696
|
-
constructor(store2, adapters2, emit, endpoint, worktreeRoot = join20(
|
|
102851
|
+
constructor(store2, adapters2, emit, endpoint, worktreeRoot = join20(homedir5(), DATA_DIR, "worktrees"), keeperOpts = {}) {
|
|
102697
102852
|
this.store = store2;
|
|
102698
102853
|
this.adapters = adapters2;
|
|
102699
102854
|
this.emit = emit;
|
|
@@ -104426,7 +104581,7 @@ var SessionManager = class _SessionManager {
|
|
|
104426
104581
|
` : ending.type === "turn_complete" ? `[Centralu] The assigned work finished.
|
|
104427
104582
|
` : `[Centralu] The assigned work stopped before finishing.
|
|
104428
104583
|
`;
|
|
104429
|
-
const
|
|
104584
|
+
const failure3 = ending.type === "error" ? `Error: ${ending.error.message.slice(0, 600)}
|
|
104430
104585
|
|
|
104431
104586
|
` : "";
|
|
104432
104587
|
try {
|
|
@@ -104436,7 +104591,7 @@ var SessionManager = class _SessionManager {
|
|
|
104436
104591
|
id: ${sessionId}
|
|
104437
104592
|
Project: ${frameField(project2)}
|
|
104438
104593
|
|
|
104439
|
-
` +
|
|
104594
|
+
` + failure3 + `Last response:
|
|
104440
104595
|
${preview || "(no content)"}
|
|
104441
104596
|
|
|
104442
104597
|
If you need more, you can read that session's recent conversation with read_session.`,
|
|
@@ -105364,33 +105519,53 @@ ${String(stderr).slice(0, 2e3)}`
|
|
|
105364
105519
|
* Screen preferences (UiPreferences) — read as one blob, and written back accepting **only what
|
|
105365
105520
|
* changed.**
|
|
105366
105521
|
*
|
|
105367
|
-
* Corrupted JSON is treated the same as if it did not exist (parseUiPreferences)
|
|
105368
|
-
* to start because of a single broken setting is
|
|
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.
|
|
105369
105525
|
*/
|
|
105370
105526
|
uiPreferences() {
|
|
105371
|
-
const
|
|
105372
|
-
let stored;
|
|
105373
|
-
try {
|
|
105374
|
-
stored = raw === null ? void 0 : JSON.parse(raw);
|
|
105375
|
-
} catch {
|
|
105376
|
-
stored = void 0;
|
|
105377
|
-
}
|
|
105527
|
+
const stored = this.storedUiPreferences();
|
|
105378
105528
|
const prefs = parseUiPreferences(stored);
|
|
105379
|
-
|
|
105380
|
-
if (!hasTextSize) {
|
|
105529
|
+
if (!("textSize" in stored)) {
|
|
105381
105530
|
const legacy = textSizeFromLegacyStep(this.loadWorkspace()?.textScale);
|
|
105382
105531
|
if (legacy !== null) {
|
|
105383
105532
|
prefs.textSize = legacy;
|
|
105384
|
-
this.store.setAppSetting(UI_PREFS_KEY, JSON.stringify(
|
|
105533
|
+
this.store.setAppSetting(UI_PREFS_KEY, JSON.stringify({ ...stored, textSize: legacy }));
|
|
105385
105534
|
}
|
|
105386
105535
|
}
|
|
105387
105536
|
return prefs;
|
|
105388
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
|
+
*/
|
|
105389
105545
|
setUiPreferences(patch) {
|
|
105390
|
-
const
|
|
105391
|
-
this.store.setAppSetting(UI_PREFS_KEY, JSON.stringify(
|
|
105392
|
-
|
|
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 {};
|
|
105393
105567
|
}
|
|
105568
|
+
brokenPrefsSaid = false;
|
|
105394
105569
|
/** The settings screen must be able to see and delete rules (FR-3: make the outcome visible) */
|
|
105395
105570
|
listApprovalRules() {
|
|
105396
105571
|
return this.store.listApprovalRules().filter((r) => r.matcher).map((r) => ({
|
|
@@ -106530,7 +106705,12 @@ ${String(stderr).slice(0, 2e3)}`
|
|
|
106530
106705
|
...opts,
|
|
106531
106706
|
onDemand: {
|
|
106532
106707
|
attached: (id2) => this.appAccess?.attached(id2) ?? [],
|
|
106533
|
-
|
|
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)
|
|
106534
106714
|
}
|
|
106535
106715
|
});
|
|
106536
106716
|
this.appAccess = new AppAccess({
|
|
@@ -106989,11 +107169,10 @@ import { promisify as promisify5 } from "node:util";
|
|
|
106989
107169
|
// packages/agent-host/src/cli-version.ts
|
|
106990
107170
|
import { execFile as execFile4 } from "node:child_process";
|
|
106991
107171
|
import { readFileSync as readFileSync10, realpathSync as realpathSync5 } from "node:fs";
|
|
106992
|
-
import { posix as posix3, win32 as win326 } from "node:path";
|
|
106993
107172
|
import { promisify as promisify4 } from "node:util";
|
|
106994
107173
|
var exec4 = promisify4(execFile4);
|
|
106995
107174
|
var realDeps2 = () => ({
|
|
106996
|
-
platform:
|
|
107175
|
+
platform: HOST_PLATFORM,
|
|
106997
107176
|
which: (name) => whichTool(name),
|
|
106998
107177
|
launch: (path) => launchFor(path),
|
|
106999
107178
|
realpath: (path) => realpathSync5(path),
|
|
@@ -107002,7 +107181,7 @@ var realDeps2 = () => ({
|
|
|
107002
107181
|
});
|
|
107003
107182
|
var PACKAGE_DEPTH = 3;
|
|
107004
107183
|
function npmPackageVersion(file2, packageName, deps) {
|
|
107005
|
-
const path = deps.platform
|
|
107184
|
+
const path = pathsOf(deps.platform);
|
|
107006
107185
|
let real2 = file2;
|
|
107007
107186
|
try {
|
|
107008
107187
|
real2 = deps.realpath(file2);
|
|
@@ -107028,7 +107207,7 @@ async function installedCliVersion(name, packageName, deps = realDeps2()) {
|
|
|
107028
107207
|
const file2 = launch.args[0] ?? launch.command;
|
|
107029
107208
|
const fromNpm = npmPackageVersion(file2, packageName, deps);
|
|
107030
107209
|
if (fromNpm) return fromNpm;
|
|
107031
|
-
const path = deps.platform
|
|
107210
|
+
const path = pathsOf(deps.platform);
|
|
107032
107211
|
let real2 = file2;
|
|
107033
107212
|
try {
|
|
107034
107213
|
real2 = deps.realpath(file2);
|
|
@@ -107036,7 +107215,7 @@ async function installedCliVersion(name, packageName, deps = realDeps2()) {
|
|
|
107036
107215
|
}
|
|
107037
107216
|
const named = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.exec(path.basename(real2).replace(/\.exe$/i, ""));
|
|
107038
107217
|
if (named) return named[0];
|
|
107039
|
-
if (deps.platform
|
|
107218
|
+
if (locksRunningPrograms(deps.platform)) return null;
|
|
107040
107219
|
try {
|
|
107041
107220
|
return parseCliVersion(await deps.run(launch.command, [...launch.args, "--version"]));
|
|
107042
107221
|
} catch {
|
|
@@ -107108,6 +107287,9 @@ function claudeKey(exe, stat6, fs2 = realExeFs) {
|
|
|
107108
107287
|
const raw = version2 ? `${version2}-${stat6.size}` : `${stat6.size}-${Math.floor(stat6.mtimeMs)}`;
|
|
107109
107288
|
return raw.replace(/[^A-Za-z0-9._-]/g, "_");
|
|
107110
107289
|
}
|
|
107290
|
+
function windowsStart(platform = process.platform) {
|
|
107291
|
+
return platform === "win32";
|
|
107292
|
+
}
|
|
107111
107293
|
var ClaudeLinks = class {
|
|
107112
107294
|
constructor(opts) {
|
|
107113
107295
|
this.opts = opts;
|
|
@@ -107121,7 +107303,7 @@ var ClaudeLinks = class {
|
|
|
107121
107303
|
/** The key new sessions start from: the last one prepared */
|
|
107122
107304
|
current = null;
|
|
107123
107305
|
get enabled() {
|
|
107124
|
-
return this.opts.platform
|
|
107306
|
+
return windowsStart(this.opts.platform);
|
|
107125
107307
|
}
|
|
107126
107308
|
base() {
|
|
107127
107309
|
return join25(this.opts.root(), "tools", "claude");
|
|
@@ -107387,6 +107569,29 @@ async function readUsage(q2) {
|
|
|
107387
107569
|
if (typeof fn2 !== "function") throw new Error(UNSUPPORTED2);
|
|
107388
107570
|
return toSnapshot(await fn2.call(q2));
|
|
107389
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
|
+
}
|
|
107390
107595
|
|
|
107391
107596
|
// packages/agent-host/src/adapters/claude/orchestrator-mcp.ts
|
|
107392
107597
|
init_sdk();
|
|
@@ -107514,7 +107719,6 @@ var LocalAgentProcess = class _LocalAgentProcess extends EventEmitter3 {
|
|
|
107514
107719
|
exitReported = false;
|
|
107515
107720
|
spawnedAt = Date.now();
|
|
107516
107721
|
static spawn(spec, os2) {
|
|
107517
|
-
const platform = os2?.platform ?? process.platform;
|
|
107518
107722
|
const child = spawn4(spec.command, spec.args, {
|
|
107519
107723
|
cwd: spec.cwd,
|
|
107520
107724
|
env: spec.env,
|
|
@@ -107523,7 +107727,7 @@ var LocalAgentProcess = class _LocalAgentProcess extends EventEmitter3 {
|
|
|
107523
107727
|
* A group of its own, which its helpers join. Not on Windows: there `detached` means a process
|
|
107524
107728
|
* with no console, whose every console child opens a visible window (app-process.ts).
|
|
107525
107729
|
*/
|
|
107526
|
-
detached: platform
|
|
107730
|
+
detached: hasProcessGroups(os2?.platform),
|
|
107527
107731
|
windowsHide: true
|
|
107528
107732
|
});
|
|
107529
107733
|
const p = new _LocalAgentProcess(child, os2);
|
|
@@ -107549,8 +107753,9 @@ var LocalAgentProcess = class _LocalAgentProcess extends EventEmitter3 {
|
|
|
107549
107753
|
get ended() {
|
|
107550
107754
|
return this.child.exitCode !== null || this.child.signalCode !== null;
|
|
107551
107755
|
}
|
|
107552
|
-
|
|
107553
|
-
|
|
107756
|
+
/** Whether the CLI got a group of its own (`hasProcessGroups`) */
|
|
107757
|
+
get grouped() {
|
|
107758
|
+
return hasProcessGroups(this.os?.platform);
|
|
107554
107759
|
}
|
|
107555
107760
|
handle() {
|
|
107556
107761
|
return { pid: this.child.pid, kill: (s) => void this.child.kill(s) };
|
|
@@ -107562,7 +107767,7 @@ var LocalAgentProcess = class _LocalAgentProcess extends EventEmitter3 {
|
|
|
107562
107767
|
kill(signal = "SIGTERM") {
|
|
107563
107768
|
if (this.ended || this.child.pid === void 0) return false;
|
|
107564
107769
|
this.killed = true;
|
|
107565
|
-
if (this.
|
|
107770
|
+
if (!this.grouped || signal === "SIGKILL") killTree(this.handle(), "SIGKILL", this.os);
|
|
107566
107771
|
else if (signal === "SIGTERM") stopTree(this.handle(), KILL_GRACE_MS, () => !this.ended, this.os);
|
|
107567
107772
|
else {
|
|
107568
107773
|
try {
|
|
@@ -107588,7 +107793,7 @@ var LocalAgentProcess = class _LocalAgentProcess extends EventEmitter3 {
|
|
|
107588
107793
|
sweep() {
|
|
107589
107794
|
const pid = this.child.pid;
|
|
107590
107795
|
if (pid === void 0) return;
|
|
107591
|
-
if (this.
|
|
107796
|
+
if (this.grouped) return stopGroup(pid, KILL_GRACE_MS);
|
|
107592
107797
|
const root3 = { pid, spawnedAt: this.spawnedAt, exitedAt: Date.now() };
|
|
107593
107798
|
void collectOrphansWindows(root3, this.os?.listProcesses ? { listProcesses: this.os.listProcesses, taskkill: this.os.taskkill } : void 0).then((ended) => {
|
|
107594
107799
|
if (ended.length > 0) console.error(`[agent] ended ${ended.length} process(es) pid ${pid} left running: ${ended.join(", ")}`);
|
|
@@ -108669,22 +108874,22 @@ var ClaudeSession = class {
|
|
|
108669
108874
|
* Bash caught by an `ask` rule in the settings file reaches the callback → answering allow runs it
|
|
108670
108875
|
* the same request, with no callback (old-style auto) denied — "Claude requested permissions to use Bash, but you haven't granted it yet."
|
|
108671
108876
|
*
|
|
108672
|
-
* So auto's own callback only accepts question choices
|
|
108673
|
-
* 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
|
|
108674
108879
|
* written into a trusted project's own `.claude/settings.json` — the opposite direction
|
|
108675
108880
|
* from #92, which exists so files from the repo cannot decide approvals. (The SDK leaves a
|
|
108676
108881
|
* `CLAUDE_SDK_CAN_USE_TOOL_SHADOWED` warning line for this combination.)
|
|
108677
108882
|
*/
|
|
108678
108883
|
canUseTool: async (toolName, toolInput) => {
|
|
108884
|
+
if (isOrchestratorTool(toolName) && self2.orchestratorServer) {
|
|
108885
|
+
return { behavior: "allow", updatedInput: toolInput };
|
|
108886
|
+
}
|
|
108679
108887
|
if (preset === "auto" && toolName !== "AskUserQuestion") {
|
|
108680
108888
|
return {
|
|
108681
108889
|
behavior: "deny",
|
|
108682
108890
|
message: `Permission to use ${toolName} was not granted (a settings "ask" rule matched; the auto preset does not ask)`
|
|
108683
108891
|
};
|
|
108684
108892
|
}
|
|
108685
|
-
if (isOrchestratorTool(toolName)) {
|
|
108686
|
-
return { behavior: "allow", updatedInput: toolInput };
|
|
108687
|
-
}
|
|
108688
108893
|
const appTool = appToolOf(toolName);
|
|
108689
108894
|
if (appTool && self2.opts.apps?.readOnly(appTool.server, appTool.tool)) {
|
|
108690
108895
|
return { behavior: "allow", updatedInput: toolInput };
|
|
@@ -108714,6 +108919,7 @@ var ClaudeSession = class {
|
|
|
108714
108919
|
}
|
|
108715
108920
|
});
|
|
108716
108921
|
ClaudeAdapter.liveQueries.add(q2);
|
|
108922
|
+
ClaudeAdapter.querySessions.set(q2, this.sessionId);
|
|
108717
108923
|
this.releaseLostCalls(q2);
|
|
108718
108924
|
this.goalKnown = new Promise((resolve10) => this.settleGoalKnown = resolve10);
|
|
108719
108925
|
q2.supportedCommands().then((list) => {
|
|
@@ -109199,18 +109405,19 @@ var ClaudeAdapter = class _ClaudeAdapter {
|
|
|
109199
109405
|
tool = "claude";
|
|
109200
109406
|
launch;
|
|
109201
109407
|
constructor(options = {}) {
|
|
109202
|
-
const platform = options.platform
|
|
109408
|
+
const platform = options.platform;
|
|
109409
|
+
const windows = windowsStart(platform);
|
|
109203
109410
|
const executable = options.executable ?? (() => toolExecutable("claude"));
|
|
109204
109411
|
this.launch = {
|
|
109205
|
-
|
|
109412
|
+
windows,
|
|
109206
109413
|
executable,
|
|
109207
109414
|
links: new ClaudeLinks({ platform, root: options.dataRoot ?? dataRoot, fs: options.fs }),
|
|
109208
|
-
gate: new StartGate(options.startGapMs ?? (
|
|
109415
|
+
gate: new StartGate(options.startGapMs ?? (windows ? WINDOWS_START_GAP_MS : 0)),
|
|
109209
109416
|
retryDelay: options.retryDelay ?? (() => 3e3 + Math.floor(Math.random() * 3e3)),
|
|
109210
109417
|
closing: /* @__PURE__ */ new Set(),
|
|
109211
109418
|
exitGraceMs: options.exitGraceMs ?? 1500
|
|
109212
109419
|
};
|
|
109213
|
-
if (
|
|
109420
|
+
if (windows) {
|
|
109214
109421
|
this.launch.links.keep(executable());
|
|
109215
109422
|
this.launch.links.sweep();
|
|
109216
109423
|
}
|
|
@@ -109238,6 +109445,8 @@ var ClaudeAdapter = class _ClaudeAdapter {
|
|
|
109238
109445
|
* older session was still alive. Insertion order is the same as start order.
|
|
109239
109446
|
*/
|
|
109240
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();
|
|
109241
109450
|
/** The most recently started query that is still alive. */
|
|
109242
109451
|
static get lastQuery() {
|
|
109243
109452
|
let last = null;
|
|
@@ -109266,7 +109475,7 @@ var ClaudeAdapter = class _ClaudeAdapter {
|
|
|
109266
109475
|
const path = whichTool("claude");
|
|
109267
109476
|
try {
|
|
109268
109477
|
let launch = launchFor(path ?? "claude");
|
|
109269
|
-
const exe = this.launch.
|
|
109478
|
+
const exe = this.launch.windows ? this.launch.executable() : null;
|
|
109270
109479
|
if (exe && /\.exe$/i.test(exe)) {
|
|
109271
109480
|
try {
|
|
109272
109481
|
launch = { command: this.launch.links.prepare(exe).path, args: [] };
|
|
@@ -109315,9 +109524,9 @@ var ClaudeAdapter = class _ClaudeAdapter {
|
|
|
109315
109524
|
* a Query. It throws when there is no session at all, and the manager degrades with a reason.
|
|
109316
109525
|
*/
|
|
109317
109526
|
async listUsage() {
|
|
109318
|
-
const
|
|
109319
|
-
if (
|
|
109320
|
-
return
|
|
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 })));
|
|
109321
109530
|
}
|
|
109322
109531
|
async listModels() {
|
|
109323
109532
|
const q2 = _ClaudeAdapter.lastQuery;
|
|
@@ -109343,7 +109552,7 @@ var ClaudeAdapter = class _ClaudeAdapter {
|
|
|
109343
109552
|
* nothing waits: the SDK sends them SIGTERM on the host's exit, which they handle.
|
|
109344
109553
|
*/
|
|
109345
109554
|
async settle() {
|
|
109346
|
-
if (this.launch.
|
|
109555
|
+
if (!this.launch.windows || this.launch.closing.size === 0) return;
|
|
109347
109556
|
let timer;
|
|
109348
109557
|
await Promise.race([
|
|
109349
109558
|
Promise.allSettled([...this.launch.closing]),
|
|
@@ -109373,7 +109582,7 @@ function bridgePath() {
|
|
|
109373
109582
|
|
|
109374
109583
|
// packages/agent-host/src/adapters/codex/index.ts
|
|
109375
109584
|
import { existsSync as existsSync17, realpathSync as realpathSync6 } from "node:fs";
|
|
109376
|
-
import { homedir as
|
|
109585
|
+
import { homedir as homedir10 } from "node:os";
|
|
109377
109586
|
import { dirname as dirname9, join as join27, resolve as resolve9 } from "node:path";
|
|
109378
109587
|
import { promisify as promisify6 } from "node:util";
|
|
109379
109588
|
|
|
@@ -109525,10 +109734,10 @@ var CodexClient = class {
|
|
|
109525
109734
|
import { createReadStream as createReadStream2 } from "node:fs";
|
|
109526
109735
|
import { readdir as readdir6 } from "node:fs/promises";
|
|
109527
109736
|
import { createInterface as createInterface2 } from "node:readline";
|
|
109528
|
-
import { homedir as
|
|
109737
|
+
import { homedir as homedir6 } from "node:os";
|
|
109529
109738
|
import { join as join26 } from "node:path";
|
|
109530
109739
|
var MIN_SUMMARY_CHARS = 200;
|
|
109531
|
-
async function findRolloutPath(threadId, sessionsDir = join26(
|
|
109740
|
+
async function findRolloutPath(threadId, sessionsDir = join26(homedir6(), ".codex", "sessions")) {
|
|
109532
109741
|
try {
|
|
109533
109742
|
const suffix = `-${threadId}.jsonl`;
|
|
109534
109743
|
const names = await readdir6(sessionsDir, { recursive: true });
|
|
@@ -109761,7 +109970,7 @@ async function imageEventFromDisk(sessionId, path, maxBytes = IMAGE_MAX_BYTES) {
|
|
|
109761
109970
|
}
|
|
109762
109971
|
|
|
109763
109972
|
// packages/agent-host/src/adapters/codex/usage-client.ts
|
|
109764
|
-
import { homedir as
|
|
109973
|
+
import { homedir as homedir7 } from "node:os";
|
|
109765
109974
|
|
|
109766
109975
|
// packages/agent-host/src/adapters/codex/usage.ts
|
|
109767
109976
|
var str6 = (v) => typeof v === "string" && v ? v : null;
|
|
@@ -109809,7 +110018,7 @@ async function readCodexUsage(command) {
|
|
|
109809
110018
|
{ onNotification: () => {
|
|
109810
110019
|
}, onServerRequest: (r) => client.respond(r.id, {}), onExit: () => {
|
|
109811
110020
|
} },
|
|
109812
|
-
{ cwd:
|
|
110021
|
+
{ cwd: homedir7(), command }
|
|
109813
110022
|
);
|
|
109814
110023
|
try {
|
|
109815
110024
|
await client.request("initialize", {
|
|
@@ -109830,7 +110039,7 @@ async function readCodexUsage(command) {
|
|
|
109830
110039
|
}
|
|
109831
110040
|
|
|
109832
110041
|
// packages/agent-host/src/adapters/codex/models.ts
|
|
109833
|
-
import { homedir as
|
|
110042
|
+
import { homedir as homedir8 } from "node:os";
|
|
109834
110043
|
var MAX_PAGES = 20;
|
|
109835
110044
|
async function collectModels(fetchPage) {
|
|
109836
110045
|
const out = [];
|
|
@@ -109848,7 +110057,7 @@ async function listCodexModels(command) {
|
|
|
109848
110057
|
{ onNotification: () => {
|
|
109849
110058
|
}, onServerRequest: (r) => client.respond(r.id, {}), onExit: () => {
|
|
109850
110059
|
} },
|
|
109851
|
-
{ cwd:
|
|
110060
|
+
{ cwd: homedir8(), command }
|
|
109852
110061
|
);
|
|
109853
110062
|
try {
|
|
109854
110063
|
await client.request("initialize", {
|
|
@@ -109908,7 +110117,7 @@ function toModelOptions(data) {
|
|
|
109908
110117
|
}
|
|
109909
110118
|
|
|
109910
110119
|
// packages/agent-host/src/adapters/codex/notices.ts
|
|
109911
|
-
import { homedir as
|
|
110120
|
+
import { homedir as homedir9 } from "node:os";
|
|
109912
110121
|
var LABELS = {
|
|
109913
110122
|
configWarning: "config warning",
|
|
109914
110123
|
warning: "warning",
|
|
@@ -109923,7 +110132,7 @@ var IGNORED_HEAD = /^Codex is ignoring \d+ unrecognized configuration settings?\
|
|
|
109923
110132
|
var HYDRATION = /^Full-history hydration is deprecated\b/;
|
|
109924
110133
|
var API_METHOD = /`?\b(thread|turn|review)\/[a-z]/;
|
|
109925
110134
|
function tilde(path) {
|
|
109926
|
-
const home =
|
|
110135
|
+
const home = homedir9();
|
|
109927
110136
|
return home && path.startsWith(`${home}/`) ? `~${path.slice(home.length)}` : path;
|
|
109928
110137
|
}
|
|
109929
110138
|
function ignoredSettings(text2) {
|
|
@@ -110360,16 +110569,16 @@ function normalizeNotification(sessionId, n) {
|
|
|
110360
110569
|
*/
|
|
110361
110570
|
case "turn/completed": {
|
|
110362
110571
|
const turn = obj(p.turn);
|
|
110363
|
-
const
|
|
110364
|
-
if (str7(turn.status) !== "failed" && !
|
|
110365
|
-
const detail = str7(
|
|
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);
|
|
110366
110575
|
return [
|
|
110367
110576
|
{
|
|
110368
110577
|
type: "error",
|
|
110369
110578
|
sessionId,
|
|
110370
110579
|
error: {
|
|
110371
110580
|
code: "internal",
|
|
110372
|
-
message: [str7(
|
|
110581
|
+
message: [str7(failure3.message) || "The turn failed", detail].filter(Boolean).join("\n"),
|
|
110373
110582
|
retryable: true
|
|
110374
110583
|
}
|
|
110375
110584
|
}
|
|
@@ -110827,6 +111036,8 @@ var CodexSession = class {
|
|
|
110827
111036
|
elicitations = /* @__PURE__ */ new Set();
|
|
110828
111037
|
/** App servers loaded onto this thread through the bridge — only elicitations under those names go to our cards */
|
|
110829
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;
|
|
110830
111041
|
reqCounter = 0;
|
|
110831
111042
|
alwaysAllow = new AlwaysAllowRules();
|
|
110832
111043
|
/**
|
|
@@ -111050,6 +111261,7 @@ var CodexSession = class {
|
|
|
111050
111261
|
const bridge = this.opts.orchestratorBridge;
|
|
111051
111262
|
const servers = {};
|
|
111052
111263
|
const orchestrator = !!(this.opts.orchestratorTools && bridge);
|
|
111264
|
+
this.ownServer = orchestrator;
|
|
111053
111265
|
if (orchestrator) {
|
|
111054
111266
|
servers[ORCHESTRATOR_MCP_NAME] = {
|
|
111055
111267
|
command: process.execPath,
|
|
@@ -111253,7 +111465,7 @@ var CodexSession = class {
|
|
|
111253
111465
|
this.emit({ type: "approval_request", sessionId: this.sessionId, requestId: requestId2, detail: appApprovalDetail(p.serverName, p.message, p._meta) });
|
|
111254
111466
|
return;
|
|
111255
111467
|
}
|
|
111256
|
-
const ours = p.serverName === ORCHESTRATOR_MCP_NAME;
|
|
111468
|
+
const ours = p.serverName === ORCHESTRATOR_MCP_NAME && this.ownServer;
|
|
111257
111469
|
this.client.respond(r.id, { action: ours ? "accept" : "decline", content: null, _meta: null });
|
|
111258
111470
|
return;
|
|
111259
111471
|
}
|
|
@@ -111601,7 +111813,7 @@ function threadIdOf(res) {
|
|
|
111601
111813
|
}
|
|
111602
111814
|
function codexHome() {
|
|
111603
111815
|
const custom2 = process.env.CODEX_HOME?.trim();
|
|
111604
|
-
return custom2 ? custom2 : join27(
|
|
111816
|
+
return custom2 ? custom2 : join27(homedir10(), ".codex");
|
|
111605
111817
|
}
|
|
111606
111818
|
var CODEX_VERBOSITIES = ["low", "medium", "high"];
|
|
111607
111819
|
var CodexAdapter = class {
|
|
@@ -112000,7 +112212,7 @@ async function cwdsOf(pids) {
|
|
|
112000
112212
|
}
|
|
112001
112213
|
}
|
|
112002
112214
|
async function findStrays(roots, selfPid = process.pid, held3 = []) {
|
|
112003
|
-
if (
|
|
112215
|
+
if (!hasPs() || roots.length === 0) return [];
|
|
112004
112216
|
const rows = await psRows();
|
|
112005
112217
|
const candidates = rows.filter((r) => r.pid > 1 && noTty(r.tty)).map((r) => r.pid);
|
|
112006
112218
|
const cwdOf = await cwdsOf(candidates);
|
|
@@ -112113,7 +112325,7 @@ var toInfo = (h) => ({
|
|
|
112113
112325
|
function sessionIdsOf(panels) {
|
|
112114
112326
|
return panels.flatMap((p) => p.kind === "session" ? [p.sessionId] : []);
|
|
112115
112327
|
}
|
|
112116
|
-
function createRpcHandler(mgr2, adapters2, { terminals: terminals2, updates: updates2, commands, externalApps: externalApps2, views: views2, inlineViews: inlineViews2, heldPids, themes: themes2, agentVersions: agentVersions2, machines } = {}) {
|
|
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 } = {}) {
|
|
112117
112329
|
const requireThemes = () => {
|
|
112118
112330
|
if (!themes2) throw Object.assign(new Error("Theme files are unavailable"), { code: "internal" });
|
|
112119
112331
|
return themes2;
|
|
@@ -112700,13 +112912,29 @@ function createRpcHandler(mgr2, adapters2, { terminals: terminals2, updates: upd
|
|
|
112700
112912
|
return { ok: true };
|
|
112701
112913
|
},
|
|
112702
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
|
+
},
|
|
112703
112924
|
"machines.acceptVersions": async (p) => requireMachines().acceptVersions(RpcMethods["machines.acceptVersions"].params.parse(p).machineId),
|
|
112704
112925
|
"updates.status": async (p) => requireUpdates().check(RpcMethods["updates.status"].params.parse(p).force),
|
|
112705
112926
|
"updates.setAuto": async (p) => requireUpdates().setAuto(RpcMethods["updates.setAuto"].params.parse(p).enabled),
|
|
112706
112927
|
"updates.setAutoApply": async (p) => requireUpdates().setAutoApply(RpcMethods["updates.setAutoApply"].params.parse(p).enabled),
|
|
112707
112928
|
// Answers once the install has started, not once it has finished — see the note on
|
|
112708
112929
|
// `updates.apply` in the protocol. The rest arrives as `update_status` events.
|
|
112709
|
-
"updates.apply": async () => requireUpdates().apply()
|
|
112930
|
+
"updates.apply": async () => requireUpdates().apply(),
|
|
112931
|
+
"host.stop": async () => {
|
|
112932
|
+
if (!stopHost) {
|
|
112933
|
+
throw Object.assign(new Error("This host runs under the Centralu app, which stops it: quit the app instead"), { code: "internal" });
|
|
112934
|
+
}
|
|
112935
|
+
setTimeout(stopHost, 50);
|
|
112936
|
+
return { ok: true };
|
|
112937
|
+
}
|
|
112710
112938
|
};
|
|
112711
112939
|
return async (method, params) => {
|
|
112712
112940
|
const name = method;
|
|
@@ -113243,15 +113471,14 @@ var CommandRunner = class {
|
|
|
113243
113471
|
// packages/agent-host/src/updates.ts
|
|
113244
113472
|
import { execFile as execFile8 } from "node:child_process";
|
|
113245
113473
|
import { existsSync as existsSync19 } from "node:fs";
|
|
113246
|
-
import { homedir as homedir10 } from "node:os";
|
|
113247
|
-
import { join as join29 } from "node:path";
|
|
113248
113474
|
var REGISTRY_URL = `https://registry.npmjs.org/${APP_SLUG}/latest`;
|
|
113249
113475
|
var CHECK_INTERVAL_MS = 6 * 60 * 60 * 1e3;
|
|
113250
113476
|
var SCHEDULED_TIMEOUT_MS = 3e3;
|
|
113251
113477
|
var FORCED_TIMEOUT_MS = 8e3;
|
|
113252
113478
|
var INSTALL_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
113253
|
-
function
|
|
113254
|
-
|
|
113479
|
+
function existingInstalledCopy() {
|
|
113480
|
+
const path = installedCopyPath();
|
|
113481
|
+
return existsSync19(path) ? path : null;
|
|
113255
113482
|
}
|
|
113256
113483
|
async function fetchLatestFromRegistry(timeoutMs) {
|
|
113257
113484
|
try {
|
|
@@ -113265,12 +113492,23 @@ async function fetchLatestFromRegistry(timeoutMs) {
|
|
|
113265
113492
|
return { ok: false, reason: "Could not reach the registry \u2014 check the network" };
|
|
113266
113493
|
}
|
|
113267
113494
|
}
|
|
113268
|
-
function
|
|
113269
|
-
|
|
113270
|
-
|
|
113271
|
-
|
|
113272
|
-
|
|
113273
|
-
|
|
113495
|
+
function commandFor(file2, args, resolve10 = (f) => resolveCommand(f)) {
|
|
113496
|
+
const launch = resolve10(file2);
|
|
113497
|
+
return { file: launch.command, args: [...launch.args, ...args] };
|
|
113498
|
+
}
|
|
113499
|
+
function failureDetail(file2, stderr) {
|
|
113500
|
+
const lines = stderr.split(/\r?\n/).map((l) => l.trim()).filter((l) => l && !/complete log of this run/i.test(l));
|
|
113501
|
+
return (file2 === "npm" ? lines.at(-1) : lines[0]) ?? null;
|
|
113502
|
+
}
|
|
113503
|
+
function runCommand(file2, args, resolve10) {
|
|
113504
|
+
const cmd = commandFor(file2, args, resolve10);
|
|
113505
|
+
return new Promise((done, reject) => {
|
|
113506
|
+
execFile8(cmd.file, cmd.args, { timeout: INSTALL_TIMEOUT_MS, windowsHide: true }, (err, _stdout, stderr) => {
|
|
113507
|
+
if (!err) return done();
|
|
113508
|
+
if (err.code === "ENOENT") {
|
|
113509
|
+
return reject(new Error(`${file2} was not found on PATH (${err.message})`));
|
|
113510
|
+
}
|
|
113511
|
+
reject(new Error(failureDetail(file2, String(stderr)) || err.message));
|
|
113274
113512
|
});
|
|
113275
113513
|
});
|
|
113276
113514
|
}
|
|
@@ -113280,6 +113518,7 @@ var UpdateService = class {
|
|
|
113280
113518
|
this.deps = {
|
|
113281
113519
|
fetchLatest: deps.fetchLatest ?? fetchLatestFromRegistry,
|
|
113282
113520
|
run: deps.run ?? runCommand,
|
|
113521
|
+
installedCopy: deps.installedCopy ?? existingInstalledCopy,
|
|
113283
113522
|
now: deps.now ?? Date.now,
|
|
113284
113523
|
// If no place to persist it is given, treat auto-update as on — the same default that main.ts
|
|
113285
113524
|
// passes in.
|
|
@@ -113444,12 +113683,17 @@ var UpdateService = class {
|
|
|
113444
113683
|
return this.current();
|
|
113445
113684
|
}
|
|
113446
113685
|
async runApply(version2) {
|
|
113686
|
+
let packageInstalled = false;
|
|
113687
|
+
const copy = this.deps.installedCopy();
|
|
113447
113688
|
try {
|
|
113448
113689
|
await this.deps.run("npm", ["i", "-g", `${APP_SLUG}@${version2}`]);
|
|
113449
|
-
|
|
113690
|
+
packageInstalled = true;
|
|
113691
|
+
if (copy) await this.deps.run(APP_SLUG, ["install"]);
|
|
113450
113692
|
this.status = { ...this.status, phase: "restart_required", error: null };
|
|
113451
113693
|
} catch (e) {
|
|
113452
|
-
|
|
113694
|
+
const reason = e.message;
|
|
113695
|
+
const error51 = packageInstalled ? `npm installed ${version2}, but the copy at ${copy} was not replaced (${reason}). Quit ${APP_NAME}, then run "${APP_SLUG} install" in a terminal.` : reason;
|
|
113696
|
+
this.status = { ...this.status, phase: "failed", error: error51 };
|
|
113453
113697
|
}
|
|
113454
113698
|
this.emit();
|
|
113455
113699
|
}
|
|
@@ -113682,9 +113926,9 @@ var AgentVersionService = class {
|
|
|
113682
113926
|
};
|
|
113683
113927
|
|
|
113684
113928
|
// packages/agent-host/src/dev-services/instance-lock.ts
|
|
113685
|
-
import { execFileSync as
|
|
113929
|
+
import { execFileSync as execFileSync4 } from "node:child_process";
|
|
113686
113930
|
import { readFileSync as readFileSync12, unlinkSync as unlinkSync3, writeFileSync as writeFileSync7 } from "node:fs";
|
|
113687
|
-
import { dirname as dirname10, join as
|
|
113931
|
+
import { dirname as dirname10, join as join29 } from "node:path";
|
|
113688
113932
|
import Database2 from "better-sqlite3";
|
|
113689
113933
|
var OWNERSHIP_FILE = "host-ownership.sqlite";
|
|
113690
113934
|
var held = /* @__PURE__ */ new Set();
|
|
@@ -113698,9 +113942,9 @@ function alive(pid) {
|
|
|
113698
113942
|
}
|
|
113699
113943
|
}
|
|
113700
113944
|
function processStartTime(pid) {
|
|
113701
|
-
if (
|
|
113945
|
+
if (!hasPs()) return null;
|
|
113702
113946
|
try {
|
|
113703
|
-
const out =
|
|
113947
|
+
const out = execFileSync4("ps", ["-o", "lstart=", "-p", String(pid)], {
|
|
113704
113948
|
encoding: "utf8",
|
|
113705
113949
|
timeout: 2e3,
|
|
113706
113950
|
stdio: ["ignore", "pipe", "ignore"],
|
|
@@ -113730,10 +113974,10 @@ function stillHeld(holder, startOf) {
|
|
|
113730
113974
|
const now = startOf(holder.pid);
|
|
113731
113975
|
return now === null || now === holder.started;
|
|
113732
113976
|
}
|
|
113733
|
-
function acquireInstanceLock(dbPath2, startOf = processStartTime, legacyFile =
|
|
113977
|
+
function acquireInstanceLock(dbPath2, startOf = processStartTime, legacyFile = hasPs()) {
|
|
113734
113978
|
if (dbPath2 === ":memory:") return { ok: true, release: () => {
|
|
113735
113979
|
} };
|
|
113736
|
-
const lockPath =
|
|
113980
|
+
const lockPath = join29(dirname10(dbPath2), "host.lock");
|
|
113737
113981
|
const readHolder = () => {
|
|
113738
113982
|
try {
|
|
113739
113983
|
return parseHolder(readFileSync12(lockPath, "utf8"));
|
|
@@ -113741,7 +113985,7 @@ function acquireInstanceLock(dbPath2, startOf = processStartTime, legacyFile = p
|
|
|
113741
113985
|
return null;
|
|
113742
113986
|
}
|
|
113743
113987
|
};
|
|
113744
|
-
const owner = takeOwnership(
|
|
113988
|
+
const owner = takeOwnership(join29(dirname10(dbPath2), OWNERSHIP_FILE));
|
|
113745
113989
|
if (!owner) {
|
|
113746
113990
|
const holder2 = readHolder();
|
|
113747
113991
|
return { ok: false, heldByPid: holder2 && Number.isInteger(holder2.pid) && holder2.pid > 0 ? holder2.pid : null, lockPath };
|
|
@@ -113955,6 +114199,752 @@ async function runDrain(bound, deps) {
|
|
|
113955
114199
|
return report;
|
|
113956
114200
|
}
|
|
113957
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
|
+
|
|
113958
114948
|
// packages/agent-host/src/links/machine-ids.ts
|
|
113959
114949
|
import { randomBytes as randomBytes5 } from "node:crypto";
|
|
113960
114950
|
function isMachineId(value) {
|
|
@@ -114423,6 +115413,10 @@ function channel(version2) {
|
|
|
114423
115413
|
|
|
114424
115414
|
// packages/agent-host/src/links/links.ts
|
|
114425
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
|
+
}
|
|
114426
115420
|
function hiddenKind(s) {
|
|
114427
115421
|
return isObj2(s) && (s.kind === "orchestrator" || s.kind === "coordinator");
|
|
114428
115422
|
}
|
|
@@ -114443,6 +115437,8 @@ var LinkedMachine = class {
|
|
|
114443
115437
|
error = null;
|
|
114444
115438
|
versions = null;
|
|
114445
115439
|
lastConnectedAt = null;
|
|
115440
|
+
/** The host this link started there, if it did (owner decision 7, plan §10.9) */
|
|
115441
|
+
hostStarted = null;
|
|
114446
115442
|
endpoint = null;
|
|
114447
115443
|
client = null;
|
|
114448
115444
|
/** Sessions of the remote the hub shows / hides, by the remote's id. Events of any other session are not passed on */
|
|
@@ -114454,6 +115450,19 @@ var LinkedMachine = class {
|
|
|
114454
115450
|
opening = null;
|
|
114455
115451
|
/** A refusal was already answered by asking the remote again; the next one is reported and backed off */
|
|
114456
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;
|
|
114457
115466
|
get id() {
|
|
114458
115467
|
return this.record.id;
|
|
114459
115468
|
}
|
|
@@ -114476,7 +115485,10 @@ var LinkedMachine = class {
|
|
|
114476
115485
|
versions: this.versions,
|
|
114477
115486
|
lastConnectedAt: this.lastConnectedAt,
|
|
114478
115487
|
localPort: this.endpoint && this.endpoint.localPort > 0 ? this.endpoint.localPort : null,
|
|
114479
|
-
sameLocalPort: this.endpoint ? this.endpoint.localPort === this.endpoint.line.port : false
|
|
115488
|
+
sameLocalPort: this.endpoint ? this.endpoint.localPort === this.endpoint.line.port : false,
|
|
115489
|
+
hostStarted: this.hostStarted,
|
|
115490
|
+
install: this.installed,
|
|
115491
|
+
operation: this.operation
|
|
114480
115492
|
};
|
|
114481
115493
|
}
|
|
114482
115494
|
// ── RoutedMachine ───────────────────────────────────────────────────────────────────────
|
|
@@ -114520,6 +115532,171 @@ var LinkedMachine = class {
|
|
|
114520
115532
|
this.versions = { ...this.versions, accepted: true };
|
|
114521
115533
|
this.reconnect();
|
|
114522
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
|
+
}
|
|
114523
115700
|
async stop() {
|
|
114524
115701
|
this.stopped = true;
|
|
114525
115702
|
clearTimeout(this.retryTimer);
|
|
@@ -114528,6 +115705,7 @@ var LinkedMachine = class {
|
|
|
114528
115705
|
await this.tunnel.close();
|
|
114529
115706
|
}
|
|
114530
115707
|
open() {
|
|
115708
|
+
if (this.held) return Promise.resolve();
|
|
114531
115709
|
this.opening ??= this.doOpen().finally(() => {
|
|
114532
115710
|
this.opening = null;
|
|
114533
115711
|
});
|
|
@@ -114538,18 +115716,32 @@ var LinkedMachine = class {
|
|
|
114538
115716
|
this.client?.close();
|
|
114539
115717
|
this.client = null;
|
|
114540
115718
|
this.setStatus("connecting", null);
|
|
114541
|
-
let endpoint;
|
|
114542
|
-
|
|
114543
|
-
|
|
114544
|
-
|
|
115719
|
+
let endpoint = await this.openTunnel();
|
|
115720
|
+
if (!endpoint) return;
|
|
115721
|
+
this.installed = installOf(endpoint.line.install);
|
|
115722
|
+
if (!endpoint.line.hostRunning && this.tunnel.startHost) {
|
|
115723
|
+
if (this.held) return;
|
|
115724
|
+
this.setStatus("starting", null);
|
|
115725
|
+
this.deps.log?.(`[links] ${this.name}: no Centralu host is running there; starting one (centralu serve --detach)`);
|
|
115726
|
+
let started;
|
|
115727
|
+
try {
|
|
115728
|
+
started = await this.tunnel.startHost();
|
|
115729
|
+
} catch (err) {
|
|
115730
|
+
if (this.stopped) return;
|
|
115731
|
+
this.setStatus("not_running", `Centralu could not be started on ${this.name}: ${err.message}`);
|
|
115732
|
+
return this.retryLater();
|
|
115733
|
+
}
|
|
114545
115734
|
if (this.stopped) return;
|
|
114546
|
-
this.
|
|
114547
|
-
|
|
114548
|
-
|
|
114549
|
-
|
|
114550
|
-
|
|
114551
|
-
|
|
115735
|
+
this.hostStarted = { ...started, at: Date.now() };
|
|
115736
|
+
this.deps.log?.(
|
|
115737
|
+
started.how === "detached" ? `[links] ${this.name}: started its host; it keeps running after this link ends` : `[links] ${this.name}: its host runs only while this link is up: ${started.note ?? "it cannot be started in the background there"}`
|
|
115738
|
+
);
|
|
115739
|
+
this.setStatus("starting", null);
|
|
115740
|
+
endpoint = await this.openTunnel();
|
|
115741
|
+
if (!endpoint) return;
|
|
115742
|
+
this.installed = installOf(endpoint.line.install);
|
|
114552
115743
|
}
|
|
115744
|
+
if (this.held) return;
|
|
114553
115745
|
this.endpoint = endpoint;
|
|
114554
115746
|
if (!endpoint.line.hostRunning) {
|
|
114555
115747
|
this.setStatus("not_running", `Centralu is installed on ${this.name}, but no \`centralu serve\` is running there`);
|
|
@@ -114559,6 +115751,23 @@ var LinkedMachine = class {
|
|
|
114559
115751
|
if (!mayConnect(versions)) return;
|
|
114560
115752
|
this.connectClient(endpoint);
|
|
114561
115753
|
}
|
|
115754
|
+
/** The tunnel's `open`, or null after reporting why not (and when the link was stopped meanwhile) */
|
|
115755
|
+
async openTunnel() {
|
|
115756
|
+
let endpoint;
|
|
115757
|
+
try {
|
|
115758
|
+
endpoint = await this.tunnel.open();
|
|
115759
|
+
} catch (err) {
|
|
115760
|
+
if (this.stopped) return null;
|
|
115761
|
+
this.setStatus("unreachable", err.message);
|
|
115762
|
+
this.retryLater();
|
|
115763
|
+
return null;
|
|
115764
|
+
}
|
|
115765
|
+
if (this.stopped) {
|
|
115766
|
+
await this.tunnel.close();
|
|
115767
|
+
return null;
|
|
115768
|
+
}
|
|
115769
|
+
return endpoint;
|
|
115770
|
+
}
|
|
114562
115771
|
/** Records the versions, and holds the link at `versions_differ` when they do not allow connecting */
|
|
114563
115772
|
check(remote) {
|
|
114564
115773
|
const pending = compareVersions(this.deps.hub, remote, false);
|
|
@@ -114606,6 +115815,7 @@ var LinkedMachine = class {
|
|
|
114606
115815
|
}
|
|
114607
115816
|
async onHello(client, h) {
|
|
114608
115817
|
if (this.client !== client) return;
|
|
115818
|
+
for (const w of [...this.helloWaiters]) w(h.build?.version ?? null);
|
|
114609
115819
|
if (h.build) {
|
|
114610
115820
|
const v = this.check({ version: h.build.version ?? this.versions?.remote.version ?? "unknown", protocolVersion: h.build.protocolVersion, dev: h.build.commit === "dev" });
|
|
114611
115821
|
if (!mayConnect(v)) return;
|
|
@@ -114654,11 +115864,12 @@ var LinkedMachine = class {
|
|
|
114654
115864
|
if (this.stopped) return;
|
|
114655
115865
|
this.client?.close();
|
|
114656
115866
|
this.client = null;
|
|
115867
|
+
if (this.held) return;
|
|
114657
115868
|
this.setStatus("unreachable", `The connection to ${this.name} ended (${reason}); reconnecting`);
|
|
114658
115869
|
this.retryLater();
|
|
114659
115870
|
}
|
|
114660
115871
|
retryLater(fixedMs) {
|
|
114661
|
-
if (this.stopped) return;
|
|
115872
|
+
if (this.stopped || this.held) return;
|
|
114662
115873
|
clearTimeout(this.retryTimer);
|
|
114663
115874
|
const [first, max] = this.deps.retryMs ?? [2e3, 6e4];
|
|
114664
115875
|
const delay = fixedMs ?? Math.min(max, first * 2 ** this.attempt++);
|
|
@@ -114725,6 +115936,21 @@ var Links = class {
|
|
|
114725
115936
|
m.acceptVersions();
|
|
114726
115937
|
return m.info();
|
|
114727
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
|
+
}
|
|
114728
115954
|
need(id2) {
|
|
114729
115955
|
const m = this.machines.get(id2);
|
|
114730
115956
|
if (!m) throw Object.assign(new Error(`No linked machine ${id2}`), { code: "internal" });
|
|
@@ -114943,7 +116169,16 @@ var ROUTES = {
|
|
|
114943
116169
|
"machines.add": HUB,
|
|
114944
116170
|
"machines.remove": HUB,
|
|
114945
116171
|
"machines.reconnect": HUB,
|
|
116172
|
+
// Stopping a remote host is `centralu serve --stop` over ssh, never a call through the link
|
|
116173
|
+
"host.stop": HUB,
|
|
114946
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,
|
|
114947
116182
|
// ── Callbacks of the hub's own agents ───────────────────────────────────────────────────
|
|
114948
116183
|
"orchestrator.tools": INTERNAL,
|
|
114949
116184
|
"orchestrator.tool": INTERNAL,
|
|
@@ -115157,249 +116392,6 @@ function combine(method, acc, r, q2) {
|
|
|
115157
116392
|
}
|
|
115158
116393
|
}
|
|
115159
116394
|
|
|
115160
|
-
// packages/agent-host/src/links/tunnel.ts
|
|
115161
|
-
import { spawn as spawn5 } from "node:child_process";
|
|
115162
|
-
import { createConnection as createConnection2, createServer as createServer3 } from "node:net";
|
|
115163
|
-
function parseConnectionLine(stdout) {
|
|
115164
|
-
const lines = stdout.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
115165
|
-
for (let i = lines.length - 1; i >= 0; i--) {
|
|
115166
|
-
let raw;
|
|
115167
|
-
try {
|
|
115168
|
-
raw = JSON.parse(lines[i]);
|
|
115169
|
-
} catch {
|
|
115170
|
-
continue;
|
|
115171
|
-
}
|
|
115172
|
-
const o = raw;
|
|
115173
|
-
if (o && typeof o === "object" && typeof o.v === "number") {
|
|
115174
|
-
if (o.v !== 1) throw new Error(`The remote's Centralu speaks connection format ${o.v}; update Centralu on this computer`);
|
|
115175
|
-
if (typeof o.port !== "number" || typeof o.token !== "string" || typeof o.protocolVersion !== "number") {
|
|
115176
|
-
throw new Error("The remote answered with an incomplete connection line");
|
|
115177
|
-
}
|
|
115178
|
-
return {
|
|
115179
|
-
v: 1,
|
|
115180
|
-
port: o.port,
|
|
115181
|
-
token: o.token,
|
|
115182
|
-
version: typeof o.version === "string" ? o.version : "unknown",
|
|
115183
|
-
protocolVersion: o.protocolVersion,
|
|
115184
|
-
dataDir: typeof o.dataDir === "string" ? o.dataDir : "",
|
|
115185
|
-
hostRunning: o.hostRunning === true
|
|
115186
|
-
};
|
|
115187
|
-
}
|
|
115188
|
-
}
|
|
115189
|
-
throw new Error("The remote did not print a connection line; its Centralu may predate `centralu serve` (update it there: npm i -g centralu)");
|
|
115190
|
-
}
|
|
115191
|
-
var WSL_DISTRO_RE = /^[A-Za-z0-9._-]{1,64}$/;
|
|
115192
|
-
function powershell(script) {
|
|
115193
|
-
const full = `[Console]::OutputEncoding=[Text.Encoding]::UTF8
|
|
115194
|
-
${script}`;
|
|
115195
|
-
return `powershell -NoProfile -NonInteractive -EncodedCommand ${Buffer.from(full, "utf16le").toString("base64")}`;
|
|
115196
|
-
}
|
|
115197
|
-
function wslKeepAlive(spec) {
|
|
115198
|
-
if (spec?.shell !== "wsl") return null;
|
|
115199
|
-
const distro = spec.wslDistro ?? "";
|
|
115200
|
-
if (!WSL_DISTRO_RE.test(distro)) throw new Error(`Not a WSL distro name: ${distro || "(none)"}`);
|
|
115201
|
-
return `wsl.exe -d ${distro} --exec sleep infinity`;
|
|
115202
|
-
}
|
|
115203
|
-
var NOT_FOUND2 = "CENTRALU-NOT-FOUND";
|
|
115204
|
-
function connectionCommand(spec) {
|
|
115205
|
-
const custom2 = spec.command?.trim() || null;
|
|
115206
|
-
if (spec.shell === "powershell") {
|
|
115207
|
-
if (custom2) return powershell(`${custom2} serve --connection`);
|
|
115208
|
-
return powershell(
|
|
115209
|
-
`if (Get-Command centralu -ErrorAction SilentlyContinue) { & centralu serve --connection } else { $l = Join-Path $env:USERPROFILE '.centralu\\bin\\centralu.cmd'; if (Test-Path $l) { & $l serve --connection } else { '${NOT_FOUND2}' } }`
|
|
115210
|
-
);
|
|
115211
|
-
}
|
|
115212
|
-
const lookup = `if command -v centralu >/dev/null 2>&1; then exec centralu serve --connection; elif [ -x "$HOME/.centralu/bin/centralu" ]; then exec "$HOME/.centralu/bin/centralu" serve --connection; else echo ${NOT_FOUND2}; exit 127; fi`;
|
|
115213
|
-
if (spec.shell === "posix") return custom2 ? `${custom2} serve --connection` : `sh -c '${lookup}'`;
|
|
115214
|
-
const distro = spec.wslDistro ?? "";
|
|
115215
|
-
if (!WSL_DISTRO_RE.test(distro)) throw new Error(`Not a WSL distro name: ${distro || "(none)"}`);
|
|
115216
|
-
const script = custom2 ? `${custom2} serve --connection` : `PATH=$(printf %s "$PATH" | tr : '\\n' | grep -v '^/mnt/' | paste -sd: -); ${lookup}`;
|
|
115217
|
-
const b64 = Buffer.from(script, "utf8").toString("base64");
|
|
115218
|
-
return powershell(`wsl.exe -d '${distro}' -- bash -lc 'echo ${b64} | base64 -d | bash -l'`);
|
|
115219
|
-
}
|
|
115220
|
-
var SshTunnel = class {
|
|
115221
|
-
constructor(opts) {
|
|
115222
|
-
this.opts = opts;
|
|
115223
|
-
if (!opts.target.trim() || opts.target.startsWith("-")) throw new Error("Not an ssh target");
|
|
115224
|
-
}
|
|
115225
|
-
opts;
|
|
115226
|
-
child = null;
|
|
115227
|
-
/** The asking ssh runs, while they run */
|
|
115228
|
-
asking = /* @__PURE__ */ new Set();
|
|
115229
|
-
downListeners = [];
|
|
115230
|
-
closing = false;
|
|
115231
|
-
async open() {
|
|
115232
|
-
await this.stopChild();
|
|
115233
|
-
this.closing = false;
|
|
115234
|
-
const line = await this.connectionLine().catch((err) => {
|
|
115235
|
-
this.ifClosed();
|
|
115236
|
-
throw err;
|
|
115237
|
-
});
|
|
115238
|
-
this.ifClosed();
|
|
115239
|
-
if (!line.hostRunning) {
|
|
115240
|
-
return { url: "", token: line.token, line, localPort: 0 };
|
|
115241
|
-
}
|
|
115242
|
-
let localPort = await portFree(line.port) ? line.port : await freePort();
|
|
115243
|
-
try {
|
|
115244
|
-
await this.forward(localPort, line.port);
|
|
115245
|
-
} catch (err) {
|
|
115246
|
-
if (localPort !== line.port) throw err;
|
|
115247
|
-
this.ifClosed();
|
|
115248
|
-
localPort = await freePort();
|
|
115249
|
-
await this.forward(localPort, line.port);
|
|
115250
|
-
}
|
|
115251
|
-
if (this.closing) {
|
|
115252
|
-
await this.stopChild();
|
|
115253
|
-
this.ifClosed();
|
|
115254
|
-
}
|
|
115255
|
-
return { url: `ws://127.0.0.1:${localPort}`, token: line.token, line, localPort };
|
|
115256
|
-
}
|
|
115257
|
-
onDown(listener) {
|
|
115258
|
-
this.downListeners.push(listener);
|
|
115259
|
-
}
|
|
115260
|
-
async close() {
|
|
115261
|
-
this.closing = true;
|
|
115262
|
-
for (const c of this.asking) c.kill("SIGTERM");
|
|
115263
|
-
await this.stopChild();
|
|
115264
|
-
}
|
|
115265
|
-
ifClosed() {
|
|
115266
|
-
if (this.closing) throw new Error(`The link to ${this.opts.target} was closed`);
|
|
115267
|
-
}
|
|
115268
|
-
run(args) {
|
|
115269
|
-
return new Promise((resolve10, reject) => {
|
|
115270
|
-
const child = spawn5(this.opts.ssh ?? "ssh", args, { env: this.opts.env ?? process.env, stdio: ["ignore", "pipe", "pipe"] });
|
|
115271
|
-
this.asking.add(child);
|
|
115272
|
-
child.once("exit", () => this.asking.delete(child));
|
|
115273
|
-
let stdout = "";
|
|
115274
|
-
let stderr = "";
|
|
115275
|
-
child.stdout.on("data", (d2) => stdout += String(d2));
|
|
115276
|
-
child.stderr.on("data", (d2) => stderr += String(d2));
|
|
115277
|
-
child.once("error", reject);
|
|
115278
|
-
child.once("close", (code2) => resolve10({ code: code2, stdout, stderr }));
|
|
115279
|
-
});
|
|
115280
|
-
}
|
|
115281
|
-
config() {
|
|
115282
|
-
return this.opts.configFile ? ["-F", this.opts.configFile] : [];
|
|
115283
|
-
}
|
|
115284
|
-
async connectionLine() {
|
|
115285
|
-
const remote = this.opts.remote ?? { shell: "posix" };
|
|
115286
|
-
const base2 = [...this.config(), "-T", "-o", "BatchMode=yes", "-o", "ConnectTimeout=15", "--", this.opts.target];
|
|
115287
|
-
const r = await this.run([...base2, connectionCommand(remote)]);
|
|
115288
|
-
if (r.code === 255) throw new Error(`ssh could not reach ${this.opts.target}: ${lastLine(r.stderr) || "no answer"}`);
|
|
115289
|
-
if (r.stdout.includes(NOT_FOUND2) || r.code === 127 && !remote.command) {
|
|
115290
|
-
throw new Error(`Centralu is not installed on ${this.opts.target} (run \`npm i -g centralu\` there, then \`centralu serve\` once)`);
|
|
115291
|
-
}
|
|
115292
|
-
try {
|
|
115293
|
-
return parseConnectionLine(r.stdout);
|
|
115294
|
-
} catch (err) {
|
|
115295
|
-
const why = lastLine(r.stderr);
|
|
115296
|
-
throw new Error(`${err.message}${why ? `: ${why}` : r.code ? ` (exit ${r.code})` : ""}`);
|
|
115297
|
-
}
|
|
115298
|
-
}
|
|
115299
|
-
forward(localPort, remotePort) {
|
|
115300
|
-
this.ifClosed();
|
|
115301
|
-
const keepAlive = wslKeepAlive(this.opts.remote);
|
|
115302
|
-
const args = [
|
|
115303
|
-
...this.config(),
|
|
115304
|
-
// A forward alone runs nothing; for WSL it runs the keep-alive instead (`wslKeepAlive`)
|
|
115305
|
-
...keepAlive ? ["-T"] : ["-N"],
|
|
115306
|
-
"-o",
|
|
115307
|
-
"BatchMode=yes",
|
|
115308
|
-
"-o",
|
|
115309
|
-
"ExitOnForwardFailure=yes",
|
|
115310
|
-
"-o",
|
|
115311
|
-
"ServerAliveInterval=15",
|
|
115312
|
-
"-o",
|
|
115313
|
-
"ServerAliveCountMax=3",
|
|
115314
|
-
"-o",
|
|
115315
|
-
"GatewayPorts=no",
|
|
115316
|
-
"-L",
|
|
115317
|
-
`127.0.0.1:${localPort}:127.0.0.1:${remotePort}`,
|
|
115318
|
-
"--",
|
|
115319
|
-
this.opts.target,
|
|
115320
|
-
...keepAlive ? [keepAlive] : []
|
|
115321
|
-
];
|
|
115322
|
-
const child = spawn5(this.opts.ssh ?? "ssh", args, { env: this.opts.env ?? process.env, stdio: ["ignore", "ignore", "pipe"] });
|
|
115323
|
-
this.child = child;
|
|
115324
|
-
let stderr = "";
|
|
115325
|
-
child.stderr.on("data", (d2) => stderr += String(d2));
|
|
115326
|
-
return new Promise((resolve10, reject) => {
|
|
115327
|
-
let settled = false;
|
|
115328
|
-
const deadline = Date.now() + (this.opts.forwardTimeoutMs ?? 2e4);
|
|
115329
|
-
const fail4 = (message) => {
|
|
115330
|
-
if (settled) return;
|
|
115331
|
-
settled = true;
|
|
115332
|
-
reject(new Error(message));
|
|
115333
|
-
};
|
|
115334
|
-
child.once("error", (e) => fail4(`Could not start ssh: ${e.message}`));
|
|
115335
|
-
child.once("exit", (code2, signal) => {
|
|
115336
|
-
if (this.child === child) this.child = null;
|
|
115337
|
-
if (!settled) return fail4(`The ssh forward to ${this.opts.target} ended (${signal ?? code2}): ${lastLine(stderr) || "no reason given"}`);
|
|
115338
|
-
if (!this.closing) for (const l of this.downListeners) l(`ssh exited (${signal ?? code2})${stderr ? `: ${lastLine(stderr)}` : ""}`);
|
|
115339
|
-
});
|
|
115340
|
-
const poll = async () => {
|
|
115341
|
-
while (!settled) {
|
|
115342
|
-
if (await accepts(localPort)) {
|
|
115343
|
-
settled = true;
|
|
115344
|
-
this.opts.log?.(`[links] forward up: 127.0.0.1:${localPort} -> ${this.opts.target}:127.0.0.1:${remotePort}`);
|
|
115345
|
-
return resolve10();
|
|
115346
|
-
}
|
|
115347
|
-
if (Date.now() > deadline) {
|
|
115348
|
-
child.kill("SIGTERM");
|
|
115349
|
-
return fail4(`The ssh forward to ${this.opts.target} did not come up in time`);
|
|
115350
|
-
}
|
|
115351
|
-
await new Promise((r) => setTimeout(r, 100));
|
|
115352
|
-
}
|
|
115353
|
-
};
|
|
115354
|
-
void poll();
|
|
115355
|
-
});
|
|
115356
|
-
}
|
|
115357
|
-
async stopChild() {
|
|
115358
|
-
const child = this.child;
|
|
115359
|
-
this.child = null;
|
|
115360
|
-
if (!child || child.exitCode !== null || child.signalCode !== null) return;
|
|
115361
|
-
await new Promise((resolve10) => {
|
|
115362
|
-
child.once("exit", () => resolve10());
|
|
115363
|
-
child.kill("SIGTERM");
|
|
115364
|
-
setTimeout(() => {
|
|
115365
|
-
child.kill("SIGKILL");
|
|
115366
|
-
resolve10();
|
|
115367
|
-
}, 2e3).unref();
|
|
115368
|
-
});
|
|
115369
|
-
}
|
|
115370
|
-
};
|
|
115371
|
-
function lastLine(s) {
|
|
115372
|
-
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));
|
|
115373
|
-
return [...lines].reverse().find((l) => /error|denied|refused|timed out|not found|cannot|could not/i.test(l)) ?? lines.at(-1) ?? "";
|
|
115374
|
-
}
|
|
115375
|
-
function accepts(port2) {
|
|
115376
|
-
return new Promise((resolve10) => {
|
|
115377
|
-
const s = createConnection2({ host: "127.0.0.1", port: port2 });
|
|
115378
|
-
s.once("connect", () => {
|
|
115379
|
-
s.destroy();
|
|
115380
|
-
resolve10(true);
|
|
115381
|
-
});
|
|
115382
|
-
s.once("error", () => resolve10(false));
|
|
115383
|
-
});
|
|
115384
|
-
}
|
|
115385
|
-
function portFree(port2) {
|
|
115386
|
-
return new Promise((resolve10) => {
|
|
115387
|
-
const probe = createServer3();
|
|
115388
|
-
probe.once("error", () => resolve10(false));
|
|
115389
|
-
probe.listen(port2, "127.0.0.1", () => probe.close(() => resolve10(true)));
|
|
115390
|
-
});
|
|
115391
|
-
}
|
|
115392
|
-
function freePort() {
|
|
115393
|
-
return new Promise((resolve10, reject) => {
|
|
115394
|
-
const probe = createServer3();
|
|
115395
|
-
probe.once("error", reject);
|
|
115396
|
-
probe.listen(0, "127.0.0.1", () => {
|
|
115397
|
-
const { port: port2 } = probe.address();
|
|
115398
|
-
probe.close(() => resolve10(port2));
|
|
115399
|
-
});
|
|
115400
|
-
});
|
|
115401
|
-
}
|
|
115402
|
-
|
|
115403
116395
|
// packages/agent-host/src/links/stored.ts
|
|
115404
116396
|
function storeRegistry(store2) {
|
|
115405
116397
|
return {
|
|
@@ -115427,13 +116419,15 @@ function storeMirror(store2) {
|
|
|
115427
116419
|
}
|
|
115428
116420
|
|
|
115429
116421
|
// packages/agent-host/src/main.ts
|
|
115430
|
-
var BUILD = true ? "
|
|
116422
|
+
var BUILD = true ? "1a563a8" : "dev";
|
|
115431
116423
|
var underKeeper = process.env.CC_KEEPER === "1";
|
|
115432
116424
|
var keeperSource = process.env.CC_HOST_SOURCE;
|
|
115433
116425
|
var frontDoor = process.env.CC_FRONT_DOOR;
|
|
116426
|
+
var startedByServe = process.env.CC_SERVE === "1";
|
|
115434
116427
|
delete process.env.CC_KEEPER;
|
|
115435
116428
|
delete process.env.CC_HOST_SOURCE;
|
|
115436
116429
|
delete process.env.CC_FRONT_DOOR;
|
|
116430
|
+
delete process.env.CC_SERVE;
|
|
115437
116431
|
var { values } = parseArgs({
|
|
115438
116432
|
options: {
|
|
115439
116433
|
port: { type: "string", default: "5175" },
|
|
@@ -115455,12 +116449,12 @@ var httpSecret = keeperToken ? deriveHttpSecret(keeperToken) : randomBytes6(32).
|
|
|
115455
116449
|
var dbPath = values.memory ? ":memory:" : values.db ?? defaultDbPath();
|
|
115456
116450
|
function defaultDbPath() {
|
|
115457
116451
|
const isDev = process.env.CC_DEV === "1";
|
|
115458
|
-
const dir =
|
|
115459
|
-
const legacy =
|
|
116452
|
+
const dir = join30(homedir11(), isDev ? DATA_DIR_DEV : DATA_DIR);
|
|
116453
|
+
const legacy = join30(homedir11(), isDev ? DATA_DIR_LEGACY.dev : DATA_DIR_LEGACY.prod);
|
|
115460
116454
|
movedNote = migrateLegacyDataDir(legacy, dir) ? `[agent-host] data folder moved: ${legacy} -> ${dir}` : null;
|
|
115461
116455
|
mkdirSync10(dir, { recursive: true });
|
|
115462
116456
|
process.env.CC_DATA_DIR = dir;
|
|
115463
|
-
return
|
|
116457
|
+
return join30(dir, "store.db");
|
|
115464
116458
|
}
|
|
115465
116459
|
var stopLog = dbPath === ":memory:" ? () => {
|
|
115466
116460
|
} : teeStderrToFile(hostLogPath(dirname11(dbPath)));
|
|
@@ -115541,7 +116535,7 @@ var mgr = new SessionManager(
|
|
|
115541
116535
|
},
|
|
115542
116536
|
() => bridgeAddress(frontDoor, port, token),
|
|
115543
116537
|
// Worktrees are created next to the data folder — dev and the packaged app never touch each other's worktrees
|
|
115544
|
-
|
|
116538
|
+
join30(dirname11(dbPath), "worktrees"),
|
|
115545
116539
|
held2 ? { processes: held2.processes, keptSessions: held2.kept.sessionIds } : {}
|
|
115546
116540
|
);
|
|
115547
116541
|
var appChanges = broadcastAppChanges((e) => server.broadcast(e));
|
|
@@ -115578,7 +116572,7 @@ if (held2) {
|
|
|
115578
116572
|
}
|
|
115579
116573
|
var AUTO_UPDATE_CHECK_KEY = "updates.auto";
|
|
115580
116574
|
var AUTO_APPLY_UPDATES_KEY = "updates.autoApply";
|
|
115581
|
-
var themes = new ThemeFiles(
|
|
116575
|
+
var themes = new ThemeFiles(join30(dataRoot(), "themes"), () => {
|
|
115582
116576
|
try {
|
|
115583
116577
|
server.broadcast({ type: "themes_changed" });
|
|
115584
116578
|
} catch {
|
|
@@ -115651,6 +116645,13 @@ if (underKeeper) {
|
|
|
115651
116645
|
console.error(`[agent-host] could not restore the app views handed over: ${err.stack ?? err}`);
|
|
115652
116646
|
}
|
|
115653
116647
|
}
|
|
116648
|
+
var stopNow = null;
|
|
116649
|
+
var stopAsked = false;
|
|
116650
|
+
var stopFromRequest = () => {
|
|
116651
|
+
console.error("[agent-host] asked to stop (centralu serve --stop)");
|
|
116652
|
+
if (stopNow) stopNow();
|
|
116653
|
+
else stopAsked = true;
|
|
116654
|
+
};
|
|
115654
116655
|
var links = new Links({
|
|
115655
116656
|
hub: { version: APP_VERSION, protocolVersion: PROTOCOL_VERSION, dev: BUILD === "dev" },
|
|
115656
116657
|
broadcast: (e) => server.broadcast(e),
|
|
@@ -115658,6 +116659,7 @@ var links = new Links({
|
|
|
115658
116659
|
mirror: storeMirror(store),
|
|
115659
116660
|
registry: storeRegistry(store),
|
|
115660
116661
|
tunnelFor: (record3) => new SshTunnel({ target: record3.sshTarget, remote: record3.remote, log: (line) => console.error(line) }),
|
|
116662
|
+
installer: { runtime: remoteRuntime, script: installScript },
|
|
115661
116663
|
log: (line) => console.error(line)
|
|
115662
116664
|
});
|
|
115663
116665
|
mgr.useLinkedSessions((id2) => links.knowsSession(id2));
|
|
@@ -115672,7 +116674,9 @@ var router = new Router({
|
|
|
115672
116674
|
views,
|
|
115673
116675
|
inlineViews,
|
|
115674
116676
|
heldPids: held2?.heldPids,
|
|
115675
|
-
machines: links
|
|
116677
|
+
machines: links,
|
|
116678
|
+
stopHost: startedByServe && !underKeeper ? () => stopFromRequest() : void 0,
|
|
116679
|
+
activity: () => activityCounts(activity())
|
|
115676
116680
|
}),
|
|
115677
116681
|
machines: () => links.all()
|
|
115678
116682
|
});
|
|
@@ -115726,7 +116730,7 @@ var stopActivity = underKeeper ? startActivityReport(
|
|
|
115726
116730
|
`)
|
|
115727
116731
|
) : () => {
|
|
115728
116732
|
};
|
|
115729
|
-
var crashLog =
|
|
116733
|
+
var crashLog = join30(dirname11(dbPath), "host-errors.log");
|
|
115730
116734
|
function record2(kind, err) {
|
|
115731
116735
|
const e = err;
|
|
115732
116736
|
const line = `[${(/* @__PURE__ */ new Date()).toISOString()}] ${kind}: ${e?.stack ?? String(err)}
|
|
@@ -115795,6 +116799,8 @@ process.on("SIGTERM", () => void shutdown(onSignalMode, onSignalMode === "detach
|
|
|
115795
116799
|
process.off("SIGINT", holdSignal);
|
|
115796
116800
|
process.off("SIGTERM", holdSignal);
|
|
115797
116801
|
if (pendingSignal) void shutdown(onSignalMode, onSignalMode === "detach");
|
|
116802
|
+
stopNow = () => void shutdown("stop", false);
|
|
116803
|
+
if (stopAsked) stopNow();
|
|
115798
116804
|
held2?.children.on("stop", () => void shutdown("stop", false));
|
|
115799
116805
|
if (control) {
|
|
115800
116806
|
onDrain(control, {
|