@centralu/win32-x64 0.1.0-beta.14 → 0.1.0-beta.15
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/centralu.exe
CHANGED
|
Binary file
|
|
@@ -42886,7 +42886,7 @@ import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync10, writeSync
|
|
|
42886
42886
|
|
|
42887
42887
|
// packages/protocol/src/brand.ts
|
|
42888
42888
|
var APP_NAME = "Centralu";
|
|
42889
|
-
var APP_VERSION = "0.1.0-beta.
|
|
42889
|
+
var APP_VERSION = "0.1.0-beta.15";
|
|
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";
|
|
@@ -42926,7 +42926,10 @@ function isNewerVersion(a, b2) {
|
|
|
42926
42926
|
}
|
|
42927
42927
|
return false;
|
|
42928
42928
|
}
|
|
42929
|
-
function split(
|
|
42929
|
+
function split(raw) {
|
|
42930
|
+
let v = raw.trim().replace(/^v/i, "");
|
|
42931
|
+
const plus = v.indexOf("+");
|
|
42932
|
+
if (plus !== -1) v = v.slice(0, plus);
|
|
42930
42933
|
const dash = v.indexOf("-");
|
|
42931
42934
|
const core = (dash === -1 ? v : v.slice(0, dash)).split(".").map(Number);
|
|
42932
42935
|
const pre2 = dash === -1 ? null : v.slice(dash + 1);
|
|
@@ -58153,6 +58156,11 @@ var MachineOperation = external_exports.object({
|
|
|
58153
58156
|
target: external_exports.string().nullable().default(null),
|
|
58154
58157
|
at: external_exports.number()
|
|
58155
58158
|
});
|
|
58159
|
+
var MachineAutostart = external_exports.object({
|
|
58160
|
+
on: external_exports.boolean().catch(false),
|
|
58161
|
+
how: external_exports.enum(["systemd", "task"]).nullable().catch(null),
|
|
58162
|
+
linger: external_exports.boolean().nullable().catch(null).default(null)
|
|
58163
|
+
});
|
|
58156
58164
|
var MachineInfo = external_exports.object({
|
|
58157
58165
|
id: MachineId,
|
|
58158
58166
|
/** What the person calls it; shown as the group header in the sidebar */
|
|
@@ -58197,7 +58205,13 @@ var MachineInfo = external_exports.object({
|
|
|
58197
58205
|
* An install, update, rollback or uninstall the hub is running there, and its step, while it runs
|
|
58198
58206
|
* (`machines.install` / `update` / `rollback` / `uninstall`). Null otherwise. Additive
|
|
58199
58207
|
*/
|
|
58200
|
-
operation: MachineOperation.nullable().catch(null).default(null)
|
|
58208
|
+
operation: MachineOperation.nullable().catch(null).default(null),
|
|
58209
|
+
/**
|
|
58210
|
+
* Whether the machine starts its host at boot, from its connection line and the last
|
|
58211
|
+
* `machines.autostart` (`MachineAutostart`). Null when the machine has not answered, or predates it.
|
|
58212
|
+
* Additive
|
|
58213
|
+
*/
|
|
58214
|
+
autostart: MachineAutostart.nullable().catch(null).default(null)
|
|
58201
58215
|
});
|
|
58202
58216
|
var MachineInstallResult = external_exports.object({
|
|
58203
58217
|
machine: MachineInfo,
|
|
@@ -58207,6 +58221,7 @@ var MachineInstallResult = external_exports.object({
|
|
|
58207
58221
|
left: external_exports.array(external_exports.string()).default([])
|
|
58208
58222
|
});
|
|
58209
58223
|
var MachineUninstallResult = external_exports.object({ machine: MachineInfo, stopped: external_exports.boolean() });
|
|
58224
|
+
var MachineAutostartResult = external_exports.object({ machine: MachineInfo, autostart: MachineAutostart });
|
|
58210
58225
|
var HostActivity = external_exports.object({
|
|
58211
58226
|
working: external_exports.number().int().default(0),
|
|
58212
58227
|
approvals: external_exports.number().int().default(0),
|
|
@@ -60820,6 +60835,14 @@ var RpcMethods = {
|
|
|
60820
60835
|
* data and an npm install of its own. The machine stays linked
|
|
60821
60836
|
*/
|
|
60822
60837
|
"machines.uninstall": { params: external_exports.object({ machineId: MachineId }), result: MachineUninstallResult },
|
|
60838
|
+
/**
|
|
60839
|
+
* Starts a linked machine's host at boot, or stops doing that, or (without `on`) reads whether it
|
|
60840
|
+
* does (docs/plans/remote-hub.md §10.4, owner decision 4: per machine, off by default). The
|
|
60841
|
+
* Centralu there writes its own entry (`centralu serve --autostart`): a systemd user unit with
|
|
60842
|
+
* lingering on Linux, a scheduled task at sign-in on Windows and for WSL. A host running there is
|
|
60843
|
+
* neither started nor stopped. Refused by a machine that predates it or offers none (macOS)
|
|
60844
|
+
*/
|
|
60845
|
+
"machines.autostart": { params: external_exports.object({ machineId: MachineId, on: external_exports.boolean().optional() }), result: MachineAutostartResult },
|
|
60823
60846
|
/**
|
|
60824
60847
|
* What would stop on a linked machine if its host stopped now (`host.activity`, asked of it), for the
|
|
60825
60848
|
* "Update <machine>" prompt. Null when the machine cannot say (unreachable, other protocol, older)
|
|
@@ -62090,12 +62113,7 @@ var HostServer = class _HostServer {
|
|
|
62090
62113
|
this.listenError = null;
|
|
62091
62114
|
if (err.code === "EADDRINUSE") {
|
|
62092
62115
|
reject(
|
|
62093
|
-
new Error(
|
|
62094
|
-
`Port ${this.opts.port} is already in use.
|
|
62095
|
-
\xB7 If a host is already running, just use it (start only the UI)
|
|
62096
|
-
\xB7 To clean up a leftover process: lsof -ti:${this.opts.port} | xargs kill
|
|
62097
|
-
\xB7 To use another port: pnpm host --port ${this.opts.port + 1}`
|
|
62098
|
-
)
|
|
62116
|
+
new Error(portInUseMessage(this.opts.port, this.opts.servedBy))
|
|
62099
62117
|
);
|
|
62100
62118
|
return;
|
|
62101
62119
|
}
|
|
@@ -62330,6 +62348,17 @@ function errorCode(raw) {
|
|
|
62330
62348
|
const known = ProtocolErrorCode.safeParse(raw);
|
|
62331
62349
|
return known.success ? known.data : "internal";
|
|
62332
62350
|
}
|
|
62351
|
+
function portInUseMessage(port2, servedBy) {
|
|
62352
|
+
if (servedBy === "serve") {
|
|
62353
|
+
return `Port ${port2} is already in use.
|
|
62354
|
+
\xB7 Another program listens on 127.0.0.1:${port2}; on Windows this is often WSL (wslrelay.exe) relaying a Centralu inside a distro
|
|
62355
|
+
\xB7 To use another port: centralu serve --port <n> (it is kept for the next start)`;
|
|
62356
|
+
}
|
|
62357
|
+
return `Port ${port2} is already in use.
|
|
62358
|
+
\xB7 If a host is already running, just use it (start only the UI)
|
|
62359
|
+
\xB7 To clean up a leftover process: lsof -ti:${port2} | xargs kill
|
|
62360
|
+
\xB7 To use another port: pnpm host --port ${port2 + 1}`;
|
|
62361
|
+
}
|
|
62333
62362
|
|
|
62334
62363
|
// packages/agent-host/src/view-handover.ts
|
|
62335
62364
|
var VIEW_HANDOVER_KEY = "views.handover";
|
|
@@ -98168,19 +98197,21 @@ ${summary}
|
|
|
98168
98197
|
` : "";
|
|
98169
98198
|
const verbatimMark = "\u2500\u2500 verbatim from here \u2500\u2500\n\n";
|
|
98170
98199
|
const fixed = bytes(instructions + touchedLine + summarySection + verbatimMark) + 200;
|
|
98171
|
-
const
|
|
98200
|
+
const sizes = rendered.map((e) => bytes(e.text) + 2);
|
|
98201
|
+
let size = sizes.reduce((n, s) => n + s, fixed);
|
|
98172
98202
|
let dropped = 0;
|
|
98173
|
-
while (size
|
|
98174
|
-
|
|
98203
|
+
while (size > cap && rendered.length - dropped > 1) {
|
|
98204
|
+
size -= sizes[dropped];
|
|
98175
98205
|
dropped++;
|
|
98176
98206
|
}
|
|
98177
|
-
const
|
|
98207
|
+
const kept = rendered.slice(dropped);
|
|
98208
|
+
const from = kept.length ? kept[0].seq : lastSeq;
|
|
98178
98209
|
const covers = `covers seq ${num(from)}\u2013${num(lastSeq)} of ${num(lastSeq)} \xB7 ${(cap / 1e6).toFixed(1)} MB cap` + (dropped > 0 || folded ? " \xB7 earlier material stays in the app's records" : "");
|
|
98179
98210
|
const header = `# Handoff \xB7 ${opts.name} \xB7 ${opts.tool}${opts.toTool ? ` \u2192 ${opts.toTool}` : ""}
|
|
98180
98211
|
${covers}
|
|
98181
98212
|
${touchedLine}
|
|
98182
98213
|
`;
|
|
98183
|
-
return header + instructions + summarySection + verbatimMark +
|
|
98214
|
+
return header + instructions + summarySection + verbatimMark + kept.map((e) => e.text).join("\n\n") + "\n";
|
|
98184
98215
|
}
|
|
98185
98216
|
|
|
98186
98217
|
// packages/agent-host/src/sessions/orchestrator-proposals.ts
|
|
@@ -104218,6 +104249,7 @@ var SessionManager = class _SessionManager {
|
|
|
104218
104249
|
if (m.projectId) project2 = this.store.listProjects().find((p) => p.id === m.projectId);
|
|
104219
104250
|
this.running.delete(sessionId);
|
|
104220
104251
|
this.restartAfterTurn.delete(sessionId);
|
|
104252
|
+
this.prCheckedAt.delete(sessionId);
|
|
104221
104253
|
this.meta.delete(sessionId);
|
|
104222
104254
|
await this.store.trashSession(sessionId, {
|
|
104223
104255
|
projectId: m.projectId,
|
|
@@ -104424,9 +104456,11 @@ var SessionManager = class _SessionManager {
|
|
|
104424
104456
|
* location is no longer ours — it is never looked at.
|
|
104425
104457
|
*/
|
|
104426
104458
|
async sweepOrphanHandoffNotes(projectId) {
|
|
104427
|
-
|
|
104428
|
-
|
|
104429
|
-
|
|
104459
|
+
await sweepHandoffNotes(
|
|
104460
|
+
(owner) => this.meta.has(owner) || // A session in the trash still owns its note (#204) — restoring it must find it; only purging lets it go
|
|
104461
|
+
this.store.trashedIds().has(owner) || this.store.handoffPredecessors().has(owner),
|
|
104462
|
+
projectId
|
|
104463
|
+
);
|
|
104430
104464
|
}
|
|
104431
104465
|
/**
|
|
104432
104466
|
* Changes model or permissions (FR-7). If the adapter supports it, it applies starting next turn;
|
|
@@ -106916,7 +106950,7 @@ ${String(stderr).slice(0, 2e3)}`
|
|
|
106916
106950
|
let merged = await gitBranchMerged(cwd, m.worktree.branch, m.worktree.base, trunk).catch(() => false);
|
|
106917
106951
|
if (!merged && this.ghAvailable) {
|
|
106918
106952
|
const now = Date.now();
|
|
106919
|
-
if (now - (this.prCheckedAt.get(m.id) ?? 0) >= this.prPollMs) {
|
|
106953
|
+
if (this.meta.get(m.id) === m && now - (this.prCheckedAt.get(m.id) ?? 0) >= this.prPollMs) {
|
|
106920
106954
|
this.prCheckedAt.set(m.id, now);
|
|
106921
106955
|
const pr = await this.prLookup(cwd, m.worktree.branch).catch(() => null);
|
|
106922
106956
|
if (pr === "unavailable") {
|
|
@@ -106933,6 +106967,7 @@ ${String(stderr).slice(0, 2e3)}`
|
|
|
106933
106967
|
}
|
|
106934
106968
|
}
|
|
106935
106969
|
if (!merged || this.meta.get(m.id) !== m) continue;
|
|
106970
|
+
this.prCheckedAt.delete(m.id);
|
|
106936
106971
|
m.worktreeMerged = true;
|
|
106937
106972
|
this.emit({ type: "worktree_merged", sessionId: m.id });
|
|
106938
106973
|
console.error(`[worktree] branch merged into trunk: ${m.worktree.branch} (${m.id.slice(0, 8)})`);
|
|
@@ -108880,7 +108915,7 @@ var ClaudeSession = class {
|
|
|
108880
108915
|
* from #92, which exists so files from the repo cannot decide approvals. (The SDK leaves a
|
|
108881
108916
|
* `CLAUDE_SDK_CAN_USE_TOOL_SHADOWED` warning line for this combination.)
|
|
108882
108917
|
*/
|
|
108883
|
-
canUseTool: async (toolName, toolInput) => {
|
|
108918
|
+
canUseTool: async (toolName, toolInput, options) => {
|
|
108884
108919
|
if (isOrchestratorTool(toolName) && self2.orchestratorServer) {
|
|
108885
108920
|
return { behavior: "allow", updatedInput: toolInput };
|
|
108886
108921
|
}
|
|
@@ -108905,6 +108940,7 @@ var ClaudeSession = class {
|
|
|
108905
108940
|
self2.emit({ type: "question_request", sessionId: self2.sessionId, requestId: requestId2, questions });
|
|
108906
108941
|
return new Promise((resolve10) => {
|
|
108907
108942
|
self2.questions.set(requestId2, { resolve: resolve10, input: toolInput });
|
|
108943
|
+
self2.releaseOnCancel(requestId2, options?.signal);
|
|
108908
108944
|
});
|
|
108909
108945
|
}
|
|
108910
108946
|
const detail = approvalDetail(toolName, toolInput, self2.opts.cwd);
|
|
@@ -108914,6 +108950,7 @@ var ClaudeSession = class {
|
|
|
108914
108950
|
self2.emit({ type: "approval_request", sessionId: self2.sessionId, requestId, detail });
|
|
108915
108951
|
return new Promise((resolve10) => {
|
|
108916
108952
|
self2.pending.set(requestId, { resolve: resolve10, input: toolInput });
|
|
108953
|
+
self2.releaseOnCancel(requestId, options?.signal);
|
|
108917
108954
|
});
|
|
108918
108955
|
}
|
|
108919
108956
|
}
|
|
@@ -109377,6 +109414,32 @@ var ClaudeSession = class {
|
|
|
109377
109414
|
releaseAgents(why) {
|
|
109378
109415
|
for (const e of this.stream.release(why)) this.emit(e);
|
|
109379
109416
|
}
|
|
109417
|
+
/**
|
|
109418
|
+
* Closes a card when the CLI withdraws its question. The SDK aborts the callback's `signal` when the CLI sends
|
|
109419
|
+
* `control_cancel_request` for that permission request (the tool was aborted, or the turn ended without our Stop)
|
|
109420
|
+
* and when the query closes. Nothing else tells us: without this the card stayed open forever, and answering it
|
|
109421
|
+
* resolved a promise nobody was waiting on. A card already answered or released is no longer in its map, so this
|
|
109422
|
+
* closes nothing twice.
|
|
109423
|
+
*/
|
|
109424
|
+
releaseOnCancel(requestId, signal) {
|
|
109425
|
+
if (!signal) return;
|
|
109426
|
+
const release = () => {
|
|
109427
|
+
const approval = this.pending.get(requestId);
|
|
109428
|
+
if (approval) {
|
|
109429
|
+
this.pending.delete(requestId);
|
|
109430
|
+
approval.resolve({ behavior: "deny", message: "Cancelled by Claude Code" });
|
|
109431
|
+
this.emit({ type: "approval_resolved", sessionId: this.sessionId, requestId, decision: "deny" });
|
|
109432
|
+
}
|
|
109433
|
+
const question = this.questions.get(requestId);
|
|
109434
|
+
if (question) {
|
|
109435
|
+
this.questions.delete(requestId);
|
|
109436
|
+
question.resolve({ behavior: "deny", message: "Cancelled by Claude Code" });
|
|
109437
|
+
this.emit({ type: "question_resolved", sessionId: this.sessionId, requestId });
|
|
109438
|
+
}
|
|
109439
|
+
};
|
|
109440
|
+
if (signal.aborted) release();
|
|
109441
|
+
else signal.addEventListener("abort", release, { once: true });
|
|
109442
|
+
}
|
|
109380
109443
|
/** Releases a choice that was still waiting on an answer — **never released silently**, for the same reason as approval. */
|
|
109381
109444
|
releaseQuestions(why) {
|
|
109382
109445
|
for (const [id2, open5] of this.questions) {
|
|
@@ -109684,10 +109747,19 @@ var CodexClient = class {
|
|
|
109684
109747
|
const id2 = `${this.idPrefix}${this.nextId++}`;
|
|
109685
109748
|
this.write({ id: id2, method, params });
|
|
109686
109749
|
return new Promise((resolve10, reject) => {
|
|
109687
|
-
|
|
109688
|
-
setTimeout(() => {
|
|
109750
|
+
const timer = setTimeout(() => {
|
|
109689
109751
|
if (this.pending.delete(id2)) reject(new Error(`No response for ${method}`));
|
|
109690
109752
|
}, timeoutMs);
|
|
109753
|
+
this.pending.set(id2, {
|
|
109754
|
+
resolve: (v) => {
|
|
109755
|
+
clearTimeout(timer);
|
|
109756
|
+
resolve10(v);
|
|
109757
|
+
},
|
|
109758
|
+
reject: (e) => {
|
|
109759
|
+
clearTimeout(timer);
|
|
109760
|
+
reject(e);
|
|
109761
|
+
}
|
|
109762
|
+
});
|
|
109691
109763
|
});
|
|
109692
109764
|
}
|
|
109693
109765
|
notify(method, params) {
|
|
@@ -109706,7 +109778,7 @@ var CodexClient = class {
|
|
|
109706
109778
|
this.closed = true;
|
|
109707
109779
|
this.proc.stdin.end();
|
|
109708
109780
|
const exited = await new Promise((resolve10) => {
|
|
109709
|
-
if (this.proc.exitCode !== null || this.finished) return resolve10(true);
|
|
109781
|
+
if (this.proc.exitCode !== null || this.proc.signalCode != null || this.finished) return resolve10(true);
|
|
109710
109782
|
const t = setTimeout(() => resolve10(false), 2e3);
|
|
109711
109783
|
this.proc.once("exit", () => {
|
|
109712
109784
|
clearTimeout(t);
|
|
@@ -110366,6 +110438,8 @@ var CODEX_KNOWN_NOTIFICATIONS = /* @__PURE__ */ new Set([
|
|
|
110366
110438
|
"item/reasoning/summaryPartAdded",
|
|
110367
110439
|
"thread/compacted",
|
|
110368
110440
|
"account/rateLimits/updated",
|
|
110441
|
+
// read in index.ts: closes the approval card of a request Codex ended without our answer
|
|
110442
|
+
"serverRequest/resolved",
|
|
110369
110443
|
// mapped by #290 for a child thread (CodexChildTracker); the parent's own status is still not read
|
|
110370
110444
|
"thread/status/changed",
|
|
110371
110445
|
// mapped by #304: notices, model switches and MCP servers that failed to start (thread/settings/updated in index.ts)
|
|
@@ -110383,7 +110457,6 @@ var CODEX_KNOWN_NOTIFICATIONS = /* @__PURE__ */ new Set([
|
|
|
110383
110457
|
"rawResponseItem/completed",
|
|
110384
110458
|
"rawResponse/completed",
|
|
110385
110459
|
// correctly ignored
|
|
110386
|
-
"serverRequest/resolved",
|
|
110387
110460
|
"thread/started",
|
|
110388
110461
|
"thread/archived",
|
|
110389
110462
|
"thread/unarchived",
|
|
@@ -111027,7 +111100,18 @@ var CodexSession = class {
|
|
|
111027
111100
|
* if stop is pressed very quickly, the response can arrive before the notification.
|
|
111028
111101
|
*/
|
|
111029
111102
|
turnId = null;
|
|
111030
|
-
/**
|
|
111103
|
+
/**
|
|
111104
|
+
* Turns already reported ended (`turn/completed`/`turn/failed`), the latest few. The `turn/start` response can
|
|
111105
|
+
* arrive after its own turn's `turn/completed`; taking the id from that late response would bring a finished
|
|
111106
|
+
* turn back as the target of Stop, and the next Stop would send `turn/interrupt` for a dead turn and say
|
|
111107
|
+
* "Could not stop". Bounded: only a response still in flight can name one of these.
|
|
111108
|
+
*/
|
|
111109
|
+
endedTurns = [];
|
|
111110
|
+
/**
|
|
111111
|
+
* Our requestId to the Codex server's request, with the thread and turn it was asked in. Codex ends a request
|
|
111112
|
+
* itself when its turn ends or is interrupted (and says so with `serverRequest/resolved`); the card has to close
|
|
111113
|
+
* with it, or an answer goes to a request id nobody is waiting on.
|
|
111114
|
+
*/
|
|
111031
111115
|
approvals = /* @__PURE__ */ new Map();
|
|
111032
111116
|
/**
|
|
111033
111117
|
* Cards raised for app tool approval (M4 A-5) — the answer has a different shape (elicitation's
|
|
@@ -111188,7 +111272,7 @@ var CodexSession = class {
|
|
|
111188
111272
|
}
|
|
111189
111273
|
this.threadId = threadIdOf(res) ?? this.opts.resumeExternalId;
|
|
111190
111274
|
this.threadSettings = threadSettingsOf(res);
|
|
111191
|
-
if (adopted) this.
|
|
111275
|
+
if (adopted) this.takeStartedTurn(runningTurnOf(res) ?? await this.latestRunningTurn(res));
|
|
111192
111276
|
void this.client.request("thread/goal/get", { threadId: this.threadId }).then((r) => {
|
|
111193
111277
|
const g5 = r.goal ? goalFromCodex(r.goal) : null;
|
|
111194
111278
|
if (g5) this.emit({ type: "goal", sessionId: this.sessionId, goal: g5 });
|
|
@@ -111310,6 +111394,7 @@ var CodexSession = class {
|
|
|
111310
111394
|
}
|
|
111311
111395
|
onNotification(n) {
|
|
111312
111396
|
this.noteFileChange(n);
|
|
111397
|
+
this.noteRequestEnded(n);
|
|
111313
111398
|
this.unmapped.note(n.method);
|
|
111314
111399
|
const from = n.params?.threadId;
|
|
111315
111400
|
if (typeof from === "string" && this.threadId !== null && from !== this.threadId) {
|
|
@@ -111319,7 +111404,11 @@ var CodexSession = class {
|
|
|
111319
111404
|
return;
|
|
111320
111405
|
}
|
|
111321
111406
|
if (n.method === "turn/started") this.turnId = turnIdOf(n.params);
|
|
111322
|
-
if (n.method === "turn/completed" || n.method === "turn/failed")
|
|
111407
|
+
if (n.method === "turn/completed" || n.method === "turn/failed") {
|
|
111408
|
+
this.turnId = null;
|
|
111409
|
+
const ended = turnIdOf(n.params);
|
|
111410
|
+
if (ended) this.endedTurns = [...this.endedTurns.slice(-(ENDED_TURNS_KEPT - 1)), ended];
|
|
111411
|
+
}
|
|
111323
111412
|
if (this.retrying && n.method.startsWith("item/")) {
|
|
111324
111413
|
this.retrying = false;
|
|
111325
111414
|
this.emit({ type: "activity", sessionId: this.sessionId, activity: this.activityBefore });
|
|
@@ -111421,6 +111510,34 @@ var CodexSession = class {
|
|
|
111421
111510
|
for (const early of held3) this.onChildNotification(thread, early);
|
|
111422
111511
|
}
|
|
111423
111512
|
}
|
|
111513
|
+
/**
|
|
111514
|
+
* Closes the cards of server requests Codex ended without our answer. `serverRequest/resolved` names one request
|
|
111515
|
+
* (generated `ServerRequestResolvedNotification`, codex-cli 0.160.0: `{ threadId, requestId }`); a turn that ends
|
|
111516
|
+
* takes every request asked in it, whether or not that notification comes. A request we answered is already out of
|
|
111517
|
+
* `approvals`, so its own `serverRequest/resolved` finds nothing and closes nothing twice.
|
|
111518
|
+
*/
|
|
111519
|
+
noteRequestEnded(n) {
|
|
111520
|
+
const p = n.params ?? {};
|
|
111521
|
+
if (n.method === "serverRequest/resolved") {
|
|
111522
|
+
if (typeof p.requestId !== "string" && typeof p.requestId !== "number") return;
|
|
111523
|
+
const serverId = String(p.requestId);
|
|
111524
|
+
this.closeApprovals((a) => String(a.serverId) === serverId);
|
|
111525
|
+
return;
|
|
111526
|
+
}
|
|
111527
|
+
if (n.method !== "turn/completed" && n.method !== "turn/failed") return;
|
|
111528
|
+
const turn = turnIdOf(n.params);
|
|
111529
|
+
if (typeof p.threadId !== "string" || !turn) return;
|
|
111530
|
+
this.closeApprovals((a) => a.threadId === p.threadId && a.turnId === turn);
|
|
111531
|
+
}
|
|
111532
|
+
/** Closes the matching cards as denied, without answering Codex: the request is already gone on its side */
|
|
111533
|
+
closeApprovals(match) {
|
|
111534
|
+
for (const [requestId, a] of this.approvals) {
|
|
111535
|
+
if (!match(a)) continue;
|
|
111536
|
+
this.approvals.delete(requestId);
|
|
111537
|
+
this.elicitations.delete(requestId);
|
|
111538
|
+
this.emit({ type: "approval_resolved", sessionId: this.sessionId, requestId, decision: "deny" });
|
|
111539
|
+
}
|
|
111540
|
+
}
|
|
111424
111541
|
/** Keeps a file-change item's changes until it completes — see `fileChanges` */
|
|
111425
111542
|
noteFileChange(n) {
|
|
111426
111543
|
const p = n.params ?? {};
|
|
@@ -111460,7 +111577,7 @@ var CodexSession = class {
|
|
|
111460
111577
|
const p = typeof r.params === "object" && r.params !== null ? r.params : {};
|
|
111461
111578
|
if (typeof p.serverName === "string" && this.appServers.has(p.serverName) && approvalKindOf(p._meta) === "mcp_tool_call") {
|
|
111462
111579
|
const requestId2 = `codex-req-${++this.reqCounter}`;
|
|
111463
|
-
this.approvals.set(requestId2, r
|
|
111580
|
+
this.approvals.set(requestId2, requestOrigin(r));
|
|
111464
111581
|
this.elicitations.add(requestId2);
|
|
111465
111582
|
this.emit({ type: "approval_request", sessionId: this.sessionId, requestId: requestId2, detail: appApprovalDetail(p.serverName, p.message, p._meta) });
|
|
111466
111583
|
return;
|
|
@@ -111488,7 +111605,7 @@ var CodexSession = class {
|
|
|
111488
111605
|
return;
|
|
111489
111606
|
}
|
|
111490
111607
|
const requestId = `codex-req-${++this.reqCounter}`;
|
|
111491
|
-
this.approvals.set(requestId, r
|
|
111608
|
+
this.approvals.set(requestId, requestOrigin(r));
|
|
111492
111609
|
this.emit({ type: "approval_request", sessionId: this.sessionId, requestId, detail });
|
|
111493
111610
|
}
|
|
111494
111611
|
applyRules(matchers) {
|
|
@@ -111563,9 +111680,7 @@ var CodexSession = class {
|
|
|
111563
111680
|
*/
|
|
111564
111681
|
...this.opts.effort ? { effort: this.opts.effort } : {},
|
|
111565
111682
|
...this.outputSchemaParam()
|
|
111566
|
-
}).then((res) =>
|
|
111567
|
-
this.turnId ??= turnIdOf(res);
|
|
111568
|
-
});
|
|
111683
|
+
}).then((res) => this.takeStartedTurn(turnIdOf(res)));
|
|
111569
111684
|
}).catch((e) => {
|
|
111570
111685
|
if (this.startFailed) return;
|
|
111571
111686
|
this.emit({
|
|
@@ -111585,9 +111700,7 @@ var CodexSession = class {
|
|
|
111585
111700
|
input,
|
|
111586
111701
|
...this.opts.effort ? { effort: this.opts.effort } : {},
|
|
111587
111702
|
...this.outputSchemaParam()
|
|
111588
|
-
}).then((res) => {
|
|
111589
|
-
this.turnId ??= turnIdOf(res);
|
|
111590
|
-
}).catch((e) => {
|
|
111703
|
+
}).then((res) => this.takeStartedTurn(turnIdOf(res))).catch((e) => {
|
|
111591
111704
|
this.emit({
|
|
111592
111705
|
type: "error",
|
|
111593
111706
|
sessionId: this.sessionId,
|
|
@@ -111595,6 +111708,14 @@ var CodexSession = class {
|
|
|
111595
111708
|
});
|
|
111596
111709
|
});
|
|
111597
111710
|
}
|
|
111711
|
+
/**
|
|
111712
|
+
* Takes a running turn's id from a response, unless the notifications already named one or already ended it. The
|
|
111713
|
+
* response and the notifications race: `turn/started` can come after the response (Stop pressed very quickly), and
|
|
111714
|
+
* `turn/completed` can come before it (a short turn), so a late response must not bring an ended turn back.
|
|
111715
|
+
*/
|
|
111716
|
+
takeStartedTurn(id2) {
|
|
111717
|
+
if (id2 && !this.endedTurns.includes(id2)) this.turnId ??= id2;
|
|
111718
|
+
}
|
|
111598
111719
|
/**
|
|
111599
111720
|
* A turn belonging to an agent that an app gave a schema and asked for (M4 D-1). Codex receives
|
|
111600
111721
|
* the schema **per turn** — the installed 0.153.4's generated type is
|
|
@@ -111608,9 +111729,10 @@ var CodexSession = class {
|
|
|
111608
111729
|
return this.opts.outputSchema ? { outputSchema: this.opts.outputSchema } : {};
|
|
111609
111730
|
}
|
|
111610
111731
|
respondApproval(requestId, decision, _scope, matcher) {
|
|
111611
|
-
const
|
|
111612
|
-
if (
|
|
111732
|
+
const open5 = this.approvals.get(requestId);
|
|
111733
|
+
if (open5 === void 0) return false;
|
|
111613
111734
|
this.approvals.delete(requestId);
|
|
111735
|
+
const serverId = open5.serverId;
|
|
111614
111736
|
if (this.elicitations.delete(requestId)) {
|
|
111615
111737
|
this.client.respond(serverId, {
|
|
111616
111738
|
action: decision === "deny" ? "decline" : "accept",
|
|
@@ -111659,6 +111781,10 @@ var CodexSession = class {
|
|
|
111659
111781
|
this.emit({ type: "state_change", sessionId: this.sessionId, state: "waiting_input", reason: "interrupted" });
|
|
111660
111782
|
return;
|
|
111661
111783
|
}
|
|
111784
|
+
for (const [requestId, a] of this.approvals) {
|
|
111785
|
+
if (a.threadId !== null && a.threadId !== this.threadId) continue;
|
|
111786
|
+
this.respondApproval(requestId, "deny");
|
|
111787
|
+
}
|
|
111662
111788
|
if (!this.threadId || !this.turnId) return;
|
|
111663
111789
|
void this.client.request("turn/interrupt", { threadId: this.threadId, turnId: this.turnId }).catch((err) => {
|
|
111664
111790
|
this.emit({
|
|
@@ -111801,6 +111927,15 @@ function runningTurnOf(res) {
|
|
|
111801
111927
|
}
|
|
111802
111928
|
return null;
|
|
111803
111929
|
}
|
|
111930
|
+
var ENDED_TURNS_KEPT = 8;
|
|
111931
|
+
function requestOrigin(r) {
|
|
111932
|
+
const p = typeof r.params === "object" && r.params !== null ? r.params : {};
|
|
111933
|
+
return {
|
|
111934
|
+
serverId: r.id,
|
|
111935
|
+
threadId: typeof p.threadId === "string" ? p.threadId : null,
|
|
111936
|
+
turnId: typeof p.turnId === "string" ? p.turnId : null
|
|
111937
|
+
};
|
|
111938
|
+
}
|
|
111804
111939
|
function turnIdOf(payload) {
|
|
111805
111940
|
const turn = payload?.turn;
|
|
111806
111941
|
return typeof turn?.id === "string" ? turn.id : null;
|
|
@@ -112916,6 +113051,10 @@ function createRpcHandler(mgr2, adapters2, { terminals: terminals2, updates: upd
|
|
|
112916
113051
|
"machines.update": async (p) => requireMachines().update(RpcMethods["machines.update"].params.parse(p).machineId),
|
|
112917
113052
|
"machines.rollback": async (p) => requireMachines().rollback(RpcMethods["machines.rollback"].params.parse(p).machineId),
|
|
112918
113053
|
"machines.uninstall": async (p) => requireMachines().uninstall(RpcMethods["machines.uninstall"].params.parse(p).machineId),
|
|
113054
|
+
"machines.autostart": async (p) => {
|
|
113055
|
+
const { machineId, on: on2 } = RpcMethods["machines.autostart"].params.parse(p);
|
|
113056
|
+
return requireMachines().autostart(machineId, on2);
|
|
113057
|
+
},
|
|
112919
113058
|
"machines.activity": async (p) => ({ activity: await requireMachines().activity(RpcMethods["machines.activity"].params.parse(p).machineId) }),
|
|
112920
113059
|
"host.activity": async () => {
|
|
112921
113060
|
if (!activity2) throw Object.assign(new Error("This host does not report its activity"), { code: "internal" });
|
|
@@ -114210,8 +114349,16 @@ import { createPublicKey, verify } from "node:crypto";
|
|
|
114210
114349
|
var NPM_REGISTRY = "https://registry.npmjs.org";
|
|
114211
114350
|
var PACKAGE_NAME = /^(@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/;
|
|
114212
114351
|
var VERSION = /^[0-9A-Za-z][0-9A-Za-z.+-]{0,63}$/;
|
|
114213
|
-
|
|
114352
|
+
var NotPublished = class extends Error {
|
|
114353
|
+
constructor(id2) {
|
|
114354
|
+
super(`${id2} is not on the npm registry`);
|
|
114355
|
+
this.id = id2;
|
|
114356
|
+
}
|
|
114357
|
+
id;
|
|
114358
|
+
};
|
|
114359
|
+
async function getJson(url2, f, id2) {
|
|
114214
114360
|
const res = await f(url2, { headers: { accept: "application/json" }, signal: AbortSignal.timeout(3e4) });
|
|
114361
|
+
if (res.status === 404 && id2) throw new NotPublished(id2);
|
|
114215
114362
|
if (!res.ok) throw new Error(`The npm registry answered ${res.status} for ${url2}`);
|
|
114216
114363
|
return res.json();
|
|
114217
114364
|
}
|
|
@@ -114220,7 +114367,7 @@ async function verifiedPackage(name, version2, opts = {}) {
|
|
|
114220
114367
|
const registry2 = (opts.registry ?? NPM_REGISTRY).replace(/\/+$/, "");
|
|
114221
114368
|
const f = opts.fetch ?? globalThis.fetch;
|
|
114222
114369
|
const now = opts.now?.() ?? Date.now();
|
|
114223
|
-
const [meta3, keys] = await Promise.all([getJson(`${registry2}/${name}/${version2}`, f), getJson(`${registry2}/-/npm/v1/keys`, f)]);
|
|
114370
|
+
const [meta3, keys] = await Promise.all([getJson(`${registry2}/${name}/${version2}`, f, `${name}@${version2}`), getJson(`${registry2}/-/npm/v1/keys`, f)]);
|
|
114224
114371
|
const m = meta3;
|
|
114225
114372
|
if (m?.name !== name || m.version !== version2) throw new Error(`The npm registry answered for another package than ${name}@${version2}`);
|
|
114226
114373
|
const tarball = m.dist?.tarball;
|
|
@@ -114271,6 +114418,7 @@ function parseConnectionLine(stdout) {
|
|
|
114271
114418
|
throw new Error("The remote answered with an incomplete connection line");
|
|
114272
114419
|
}
|
|
114273
114420
|
const install = parseInstall(o.install);
|
|
114421
|
+
const autostart = parseAutostart(o.autostart);
|
|
114274
114422
|
return {
|
|
114275
114423
|
v: 1,
|
|
114276
114424
|
port: o.port,
|
|
@@ -114279,7 +114427,8 @@ function parseConnectionLine(stdout) {
|
|
|
114279
114427
|
protocolVersion: o.protocolVersion,
|
|
114280
114428
|
dataDir: typeof o.dataDir === "string" ? o.dataDir : "",
|
|
114281
114429
|
hostRunning: o.hostRunning === true,
|
|
114282
|
-
...install ? { install } : {}
|
|
114430
|
+
...install ? { install } : {},
|
|
114431
|
+
...autostart ? { autostart } : {}
|
|
114283
114432
|
};
|
|
114284
114433
|
}
|
|
114285
114434
|
}
|
|
@@ -114294,6 +114443,13 @@ function parseInstall(raw) {
|
|
|
114294
114443
|
};
|
|
114295
114444
|
return { managed: o.managed === true, current: pointer(o.current), previous: pointer(o.previous), node: typeof o.node === "string" ? o.node : null };
|
|
114296
114445
|
}
|
|
114446
|
+
function parseAutostart(raw) {
|
|
114447
|
+
if (!raw || typeof raw !== "object") return null;
|
|
114448
|
+
const o = raw;
|
|
114449
|
+
if (typeof o.on !== "boolean") return null;
|
|
114450
|
+
const how = o.how === "systemd" || o.how === "task" ? o.how : null;
|
|
114451
|
+
return { on: o.on && how !== null, how, linger: typeof o.linger === "boolean" ? o.linger : null };
|
|
114452
|
+
}
|
|
114297
114453
|
function parseDetachLine(stdout) {
|
|
114298
114454
|
const lines = stdout.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
114299
114455
|
for (let i = lines.length - 1; i >= 0; i--) {
|
|
@@ -114316,6 +114472,15 @@ function powershell(script) {
|
|
|
114316
114472
|
${script}`;
|
|
114317
114473
|
return `powershell -NoProfile -NonInteractive -EncodedCommand ${Buffer.from(full, "utf16le").toString("base64")}`;
|
|
114318
114474
|
}
|
|
114475
|
+
var WINDOWS_INLINE_MAX = 7900;
|
|
114476
|
+
var PS_FROM_STDIN = "$s=[Console]::In.ReadToEnd(); $c=[Text.Encoding]::UTF8.GetString([Convert]::FromBase64String(($s -replace '[^A-Za-z0-9+/=]',''))); & ([ScriptBlock]::Create($c))";
|
|
114477
|
+
function powershellScript(script) {
|
|
114478
|
+
const inline = powershell(script);
|
|
114479
|
+
if (inline.length <= WINDOWS_INLINE_MAX) return inline;
|
|
114480
|
+
const full = `[Console]::OutputEncoding=[Text.Encoding]::UTF8
|
|
114481
|
+
${script}`;
|
|
114482
|
+
return { command: powershell(PS_FROM_STDIN), stdin: Buffer.from(full, "utf8").toString("base64") };
|
|
114483
|
+
}
|
|
114319
114484
|
function wslKeepAlive(spec) {
|
|
114320
114485
|
if (spec?.shell !== "wsl") return null;
|
|
114321
114486
|
const distro = spec.wslDistro ?? "";
|
|
@@ -114337,6 +114502,9 @@ function stopCommand(spec) {
|
|
|
114337
114502
|
function serveCommand(spec) {
|
|
114338
114503
|
return remoteCommand(spec, "serve");
|
|
114339
114504
|
}
|
|
114505
|
+
function autostartCommand(spec, action) {
|
|
114506
|
+
return remoteCommand(spec, `serve --autostart ${action}`);
|
|
114507
|
+
}
|
|
114340
114508
|
function remoteCommand(spec, args) {
|
|
114341
114509
|
const custom2 = spec.command?.trim() || null;
|
|
114342
114510
|
if (spec.shell === "powershell") {
|
|
@@ -114354,12 +114522,14 @@ function remoteCommand(spec, args) {
|
|
|
114354
114522
|
return powershell(`wsl.exe -d '${distro}' -- bash -lc 'echo ${b64} | base64 -d | bash -l'`);
|
|
114355
114523
|
}
|
|
114356
114524
|
function remoteScript(spec, script) {
|
|
114357
|
-
if (spec.shell === "powershell") return
|
|
114525
|
+
if (spec.shell === "powershell") return powershellScript(script.ps);
|
|
114358
114526
|
const b64 = Buffer.from(script.sh, "utf8").toString("base64");
|
|
114359
114527
|
if (spec.shell === "posix") return `printf %s ${b64} | base64 -d | sh`;
|
|
114360
114528
|
const distro = spec.wslDistro ?? "";
|
|
114361
114529
|
if (!WSL_DISTRO_RE.test(distro)) throw new Error(`Not a WSL distro name: ${distro || "(none)"}`);
|
|
114362
|
-
|
|
114530
|
+
const inline = powershell(`wsl.exe -d '${distro}' -- bash -lc 'echo ${b64} | base64 -d | bash -l'`);
|
|
114531
|
+
if (inline.length <= WINDOWS_INLINE_MAX) return inline;
|
|
114532
|
+
return { command: powershell(`$s=[Console]::In.ReadToEnd(); $s | wsl.exe -d '${distro}' -- bash -lc 'base64 -di | bash -l'`), stdin: b64 };
|
|
114363
114533
|
}
|
|
114364
114534
|
var SshTunnel = class {
|
|
114365
114535
|
constructor(opts) {
|
|
@@ -114372,11 +114542,18 @@ var SshTunnel = class {
|
|
|
114372
114542
|
asking = /* @__PURE__ */ new Set();
|
|
114373
114543
|
downListeners = [];
|
|
114374
114544
|
closing = false;
|
|
114545
|
+
/**
|
|
114546
|
+
* Forwards this tunnel ended on purpose (a reopen, a forward that did not come up in time): their
|
|
114547
|
+
* exit is not the link going down. Reported as down, a reopen of a live link made the link retry,
|
|
114548
|
+
* and that retry's reopen ended the new forward again, every few seconds without end
|
|
114549
|
+
*/
|
|
114550
|
+
ended = /* @__PURE__ */ new WeakSet();
|
|
114375
114551
|
/** The remote cannot start a host outside a session (WMI blocked): the forward runs it (`startHost`) */
|
|
114376
114552
|
linkBound = false;
|
|
114377
114553
|
async open() {
|
|
114378
|
-
await this.stopChild();
|
|
114379
114554
|
this.closing = false;
|
|
114555
|
+
await this.stopChild();
|
|
114556
|
+
this.ifClosed();
|
|
114380
114557
|
const line = await this.connectionLine().catch((err) => {
|
|
114381
114558
|
this.ifClosed();
|
|
114382
114559
|
throw err;
|
|
@@ -114402,6 +114579,16 @@ var SshTunnel = class {
|
|
|
114402
114579
|
const served = hosting ? await this.untilServing() : line;
|
|
114403
114580
|
return { url: `ws://127.0.0.1:${localPort}`, token: served.token, line: served, localPort };
|
|
114404
114581
|
}
|
|
114582
|
+
/**
|
|
114583
|
+
* Where Centralu runs, for a sentence: "on <target>", or "in WSL (<distro>) on <target>" (#482).
|
|
114584
|
+
* A Windows laptop can be linked twice, as Windows and as a distro, and both rows used to say
|
|
114585
|
+
* "on <target>" alike.
|
|
114586
|
+
*/
|
|
114587
|
+
get where() {
|
|
114588
|
+
const remote = this.opts.remote;
|
|
114589
|
+
if (remote?.shell !== "wsl") return `on ${this.opts.target}`;
|
|
114590
|
+
return `in WSL (${remote.wslDistro || "its default distro"}) on ${this.opts.target}`;
|
|
114591
|
+
}
|
|
114405
114592
|
/**
|
|
114406
114593
|
* Starts the remote's host with `centralu serve --detach` (plan §10.4, owner decision 7): it then
|
|
114407
114594
|
* runs outside any ssh session and outlives this link. When the remote says WMI process creation
|
|
@@ -114415,7 +114602,7 @@ var SshTunnel = class {
|
|
|
114415
114602
|
this.ifClosed();
|
|
114416
114603
|
if (r.code === 255) throw new Error(`ssh could not reach ${this.opts.target}: ${lastLine(r.stderr) || "no answer"}`);
|
|
114417
114604
|
if (r.stdout.includes(NOT_FOUND2) || r.code === 127 && !remote.command) {
|
|
114418
|
-
throw new Error(`Centralu is not installed
|
|
114605
|
+
throw new Error(`Centralu is not installed ${this.where} (run \`npm i -g centralu\` there, then \`centralu serve\` once)`);
|
|
114419
114606
|
}
|
|
114420
114607
|
let answer;
|
|
114421
114608
|
try {
|
|
@@ -114444,7 +114631,8 @@ var SshTunnel = class {
|
|
|
114444
114631
|
*/
|
|
114445
114632
|
async exec(command) {
|
|
114446
114633
|
this.ifClosed();
|
|
114447
|
-
const
|
|
114634
|
+
const [line, stdin] = typeof command === "string" ? [command, void 0] : [command.command, command.stdin];
|
|
114635
|
+
const r = await this.run([...this.config(), "-T", "-o", "BatchMode=yes", "-o", "ConnectTimeout=15", "--", this.opts.target, line], stdin);
|
|
114448
114636
|
if (r.code === 255) throw new Error(`ssh could not reach ${this.opts.target}: ${lastLine(r.stderr) || "no answer"}`);
|
|
114449
114637
|
return r;
|
|
114450
114638
|
}
|
|
@@ -114474,16 +114662,24 @@ var SshTunnel = class {
|
|
|
114474
114662
|
ifClosed() {
|
|
114475
114663
|
if (this.closing) throw new Error(`The link to ${this.opts.target} was closed`);
|
|
114476
114664
|
}
|
|
114477
|
-
run(args) {
|
|
114665
|
+
run(args, stdin) {
|
|
114478
114666
|
return new Promise((resolve10, reject) => {
|
|
114479
|
-
const child = spawn5(this.opts.ssh ?? "ssh", args, { env: this.opts.env ?? process.env, stdio: ["ignore", "pipe", "pipe"] });
|
|
114667
|
+
const child = spawn5(this.opts.ssh ?? "ssh", args, { env: this.opts.env ?? process.env, stdio: [stdin === void 0 ? "ignore" : "pipe", "pipe", "pipe"] });
|
|
114668
|
+
if (stdin !== void 0) {
|
|
114669
|
+
child.stdin.on("error", () => {
|
|
114670
|
+
});
|
|
114671
|
+
child.stdin.end(stdin);
|
|
114672
|
+
}
|
|
114480
114673
|
this.asking.add(child);
|
|
114481
114674
|
child.once("exit", () => this.asking.delete(child));
|
|
114482
114675
|
let stdout = "";
|
|
114483
114676
|
let stderr = "";
|
|
114484
114677
|
child.stdout.on("data", (d2) => stdout += String(d2));
|
|
114485
114678
|
child.stderr.on("data", (d2) => stderr += String(d2));
|
|
114486
|
-
child.once("error",
|
|
114679
|
+
child.once("error", (e) => {
|
|
114680
|
+
this.asking.delete(child);
|
|
114681
|
+
reject(e);
|
|
114682
|
+
});
|
|
114487
114683
|
child.once("close", (code2) => resolve10({ code: code2, stdout, stderr }));
|
|
114488
114684
|
});
|
|
114489
114685
|
}
|
|
@@ -114496,7 +114692,7 @@ var SshTunnel = class {
|
|
|
114496
114692
|
const r = await this.run([...base2, connectionCommand(remote)]);
|
|
114497
114693
|
if (r.code === 255) throw new Error(`ssh could not reach ${this.opts.target}: ${lastLine(r.stderr) || "no answer"}`);
|
|
114498
114694
|
if (r.stdout.includes(NOT_FOUND2) || r.code === 127 && !remote.command) {
|
|
114499
|
-
throw new Error(`Centralu is not installed
|
|
114695
|
+
throw new Error(`Centralu is not installed ${this.where} (run \`npm i -g centralu\` there, then \`centralu serve\` once)`);
|
|
114500
114696
|
}
|
|
114501
114697
|
try {
|
|
114502
114698
|
return parseConnectionLine(r.stdout);
|
|
@@ -114543,7 +114739,7 @@ var SshTunnel = class {
|
|
|
114543
114739
|
child.once("exit", (code2, signal) => {
|
|
114544
114740
|
if (this.child === child) this.child = null;
|
|
114545
114741
|
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)}` : ""}`);
|
|
114742
|
+
if (!this.closing && !this.ended.has(child)) for (const l of this.downListeners) l(`ssh exited (${signal ?? code2})${stderr ? `: ${lastLine(stderr)}` : ""}`);
|
|
114547
114743
|
});
|
|
114548
114744
|
const poll = async () => {
|
|
114549
114745
|
while (!settled) {
|
|
@@ -114553,6 +114749,7 @@ var SshTunnel = class {
|
|
|
114553
114749
|
return resolve10();
|
|
114554
114750
|
}
|
|
114555
114751
|
if (Date.now() > deadline) {
|
|
114752
|
+
this.ended.add(child);
|
|
114556
114753
|
child.kill("SIGTERM");
|
|
114557
114754
|
return fail4(`The ssh forward to ${this.opts.target} did not come up in time`);
|
|
114558
114755
|
}
|
|
@@ -114566,6 +114763,7 @@ var SshTunnel = class {
|
|
|
114566
114763
|
const child = this.child;
|
|
114567
114764
|
this.child = null;
|
|
114568
114765
|
if (!child || child.exitCode !== null || child.signalCode !== null) return;
|
|
114766
|
+
this.ended.add(child);
|
|
114569
114767
|
await new Promise((resolve10) => {
|
|
114570
114768
|
child.once("exit", () => resolve10());
|
|
114571
114769
|
child.kill("SIGTERM");
|
|
@@ -114790,12 +114988,21 @@ function readStep(run, ok, target) {
|
|
|
114790
114988
|
if (!rest.startsWith(ok)) throw new Error(`Installing on ${target} answered something unexpected: ${rest.slice(0, 120)}`);
|
|
114791
114989
|
return rest.slice(ok.length).trim();
|
|
114792
114990
|
}
|
|
114991
|
+
var hostOnlyPackage = (platform) => `@centralu/host-${platform}`;
|
|
114992
|
+
async function hostPackage(platform, version2, registry2) {
|
|
114993
|
+
try {
|
|
114994
|
+
return await verifiedPackage(hostOnlyPackage(platform), version2, registry2);
|
|
114995
|
+
} catch (err) {
|
|
114996
|
+
if (err instanceof NotPublished) return verifiedPackage(`@centralu/${platform}`, version2, registry2);
|
|
114997
|
+
throw err;
|
|
114998
|
+
}
|
|
114999
|
+
}
|
|
114793
115000
|
async function installRemote(o) {
|
|
114794
115001
|
o.onStep?.("preflight");
|
|
114795
115002
|
const pf2 = preflight(parsePreflight((await o.exec(preflightCommand(o.spec))).stdout), o.runtime);
|
|
114796
115003
|
if (!pf2.ok) throw new Error(`Cannot install on ${o.target}: ${pf2.reason}`);
|
|
114797
115004
|
o.onStep?.("registry");
|
|
114798
|
-
const packages = await Promise.all([verifiedPackage("centralu", o.version, o.registry),
|
|
115005
|
+
const packages = await Promise.all([verifiedPackage("centralu", o.version, o.registry), hostPackage(pf2.platform, o.version, o.registry)]);
|
|
114799
115006
|
const archive = o.runtime.node.archives[pf2.platform];
|
|
114800
115007
|
const nodeVersion = o.runtime.node.version;
|
|
114801
115008
|
o.onStep?.("node");
|
|
@@ -114821,6 +115028,52 @@ function remoteRuntime() {
|
|
|
114821
115028
|
return JSON.parse(bundled("remote-runtime.json", "../../../../packaging/remote-runtime.json"));
|
|
114822
115029
|
}
|
|
114823
115030
|
|
|
115031
|
+
// packages/agent-host/src/links/autostart.ts
|
|
115032
|
+
function parseAutostartLine(stdout) {
|
|
115033
|
+
for (const line of stdout.split(/\r?\n/).reverse()) {
|
|
115034
|
+
const t = line.trim();
|
|
115035
|
+
if (!t.startsWith("{")) continue;
|
|
115036
|
+
try {
|
|
115037
|
+
const o = JSON.parse(t);
|
|
115038
|
+
const state = o.v === 1 ? parseAutostart(o.autostart) : null;
|
|
115039
|
+
if (state && typeof o.autostart?.ok === "boolean") {
|
|
115040
|
+
return { ok: o.autostart.ok, state, message: typeof o.autostart.message === "string" ? o.autostart.message : null };
|
|
115041
|
+
}
|
|
115042
|
+
} catch {
|
|
115043
|
+
}
|
|
115044
|
+
}
|
|
115045
|
+
return null;
|
|
115046
|
+
}
|
|
115047
|
+
var FAILED = {
|
|
115048
|
+
on: "could not be set to start at boot",
|
|
115049
|
+
off: "could not stop starting at boot",
|
|
115050
|
+
status: "could not say whether it starts at boot"
|
|
115051
|
+
};
|
|
115052
|
+
async function autostartRemote(c, action) {
|
|
115053
|
+
const r = await c.exec(autostartCommand(c.spec, action));
|
|
115054
|
+
const unasked = (reason, message) => Object.assign(new Error(message), { reason });
|
|
115055
|
+
if (r.stdout.includes(NOT_FOUND2)) throw unasked("not_found", `Centralu is not installed on ${c.target}`);
|
|
115056
|
+
const answer = parseAutostartLine(r.stdout);
|
|
115057
|
+
if (!answer) {
|
|
115058
|
+
if (/unknown option for serve: --autostart/.test(r.stderr)) {
|
|
115059
|
+
throw unasked("too_old", `The Centralu on ${c.target} is too old to start at boot; update it first`);
|
|
115060
|
+
}
|
|
115061
|
+
const why = lastLine(r.stderr);
|
|
115062
|
+
throw new Error(`Centralu on ${c.target} gave no answer about starting at boot${why ? `: ${why}` : r.code ? ` (exit ${r.code})` : ""}`);
|
|
115063
|
+
}
|
|
115064
|
+
if (!answer.ok) throw new Error(`Centralu on ${c.target} ${FAILED[action]}: ${answer.message ?? "no reason given"}`);
|
|
115065
|
+
return answer.state;
|
|
115066
|
+
}
|
|
115067
|
+
async function removeAutostart(c) {
|
|
115068
|
+
try {
|
|
115069
|
+
await autostartRemote(c, "off");
|
|
115070
|
+
return true;
|
|
115071
|
+
} catch (err) {
|
|
115072
|
+
if (err.reason) return false;
|
|
115073
|
+
throw err;
|
|
115074
|
+
}
|
|
115075
|
+
}
|
|
115076
|
+
|
|
114824
115077
|
// packages/agent-host/src/links/update.ts
|
|
114825
115078
|
var CHECK_MS = 3e4;
|
|
114826
115079
|
function parseStopLine(stdout) {
|
|
@@ -114940,6 +115193,8 @@ async function uninstallRemote(c) {
|
|
|
114940
115193
|
await c.hold();
|
|
114941
115194
|
c.step("stop");
|
|
114942
115195
|
const { wasRunning } = await stopRemoteHost(c);
|
|
115196
|
+
c.step("autostart");
|
|
115197
|
+
await removeAutostart(c);
|
|
114943
115198
|
c.step("remove");
|
|
114944
115199
|
readStep(await c.exec(uninstallCommand(c.spec)), "uninstalled", c.target);
|
|
114945
115200
|
return { stopped: wasRunning };
|
|
@@ -115417,6 +115672,9 @@ var linkError2 = (message) => Object.assign(new Error(message), { code: "interna
|
|
|
115417
115672
|
function installOf(i) {
|
|
115418
115673
|
return i ? { managed: i.managed, current: i.current, previous: i.previous } : null;
|
|
115419
115674
|
}
|
|
115675
|
+
function autostartOf(a) {
|
|
115676
|
+
return a ? { on: a.on, how: a.how, linger: a.linger } : null;
|
|
115677
|
+
}
|
|
115420
115678
|
function hiddenKind(s) {
|
|
115421
115679
|
return isObj2(s) && (s.kind === "orchestrator" || s.kind === "coordinator");
|
|
115422
115680
|
}
|
|
@@ -115463,6 +115721,8 @@ var LinkedMachine = class {
|
|
|
115463
115721
|
helloWaiters = /* @__PURE__ */ new Set();
|
|
115464
115722
|
/** What the hub installed there, from the last connection line (plan §10.5, S12) */
|
|
115465
115723
|
installed = null;
|
|
115724
|
+
/** Whether it starts its host at boot, from the last connection line or `machines.autostart` (plan §10.4) */
|
|
115725
|
+
bootStart = null;
|
|
115466
115726
|
get id() {
|
|
115467
115727
|
return this.record.id;
|
|
115468
115728
|
}
|
|
@@ -115488,7 +115748,8 @@ var LinkedMachine = class {
|
|
|
115488
115748
|
sameLocalPort: this.endpoint ? this.endpoint.localPort === this.endpoint.line.port : false,
|
|
115489
115749
|
hostStarted: this.hostStarted,
|
|
115490
115750
|
install: this.installed,
|
|
115491
|
-
operation: this.operation
|
|
115751
|
+
operation: this.operation,
|
|
115752
|
+
autostart: this.bootStart
|
|
115492
115753
|
};
|
|
115493
115754
|
}
|
|
115494
115755
|
// ── RoutedMachine ───────────────────────────────────────────────────────────────────────
|
|
@@ -115584,10 +115845,38 @@ var LinkedMachine = class {
|
|
|
115584
115845
|
return this.operate("uninstall", null, async (c) => {
|
|
115585
115846
|
const r = await uninstallRemote(c);
|
|
115586
115847
|
this.installed = { managed: false, current: null, previous: null };
|
|
115848
|
+
if (this.bootStart) this.bootStart = { ...this.bootStart, on: false, linger: null };
|
|
115587
115849
|
this.deps.log?.(`[links] ${this.name}: removed the Centralu this computer installed; its data stays`);
|
|
115588
115850
|
return { machine: this.info(), stopped: r.stopped };
|
|
115589
115851
|
});
|
|
115590
115852
|
}
|
|
115853
|
+
/**
|
|
115854
|
+
* `machines.autostart` (plan §10.4, owner decision 4): the machine's host started at boot, or not, or
|
|
115855
|
+
* (without `on`) whether it is. The Centralu there writes its own entry; nothing starts or stops now.
|
|
115856
|
+
* Not while an install, update, rollback or uninstall runs there: the entry names a launcher that one
|
|
115857
|
+
* may be removing
|
|
115858
|
+
*/
|
|
115859
|
+
async autostart(on2) {
|
|
115860
|
+
const exec8 = this.tunnel.exec?.bind(this.tunnel);
|
|
115861
|
+
if (!exec8) throw linkError2(`This computer has no shell on ${this.name} to change how it starts`);
|
|
115862
|
+
if (this.busy) throw linkError2(`${this.name} is busy with ${this.operation?.kind ?? "another change"}; wait until it is done`);
|
|
115863
|
+
this.busy = true;
|
|
115864
|
+
try {
|
|
115865
|
+
const a = await autostartRemote({ exec: exec8, spec: this.record.remote, target: this.record.sshTarget }, on2 === void 0 ? "status" : on2 ? "on" : "off");
|
|
115866
|
+
const state = { on: a.on, how: a.how, linger: a.linger };
|
|
115867
|
+
this.bootStart = state;
|
|
115868
|
+
if (on2 !== void 0) {
|
|
115869
|
+
this.deps.log?.(`[links] ${this.name}: ${a.on ? `starts Centralu at boot (${a.how}${a.how === "systemd" && a.linger === false ? ", at login until lingering is enabled" : ""})` : "no longer starts Centralu at boot"}`);
|
|
115870
|
+
}
|
|
115871
|
+
this.deps.broadcast({ type: "machine_status", machine: this.info() });
|
|
115872
|
+
return { machine: this.info(), autostart: state };
|
|
115873
|
+
} catch (err) {
|
|
115874
|
+
this.deps.log?.(`[links] ${this.name}: autostart ${on2 === void 0 ? "status" : on2 ? "on" : "off"} failed: ${err.message}`);
|
|
115875
|
+
throw linkError2(err.message);
|
|
115876
|
+
} finally {
|
|
115877
|
+
this.busy = false;
|
|
115878
|
+
}
|
|
115879
|
+
}
|
|
115591
115880
|
/**
|
|
115592
115881
|
* What would stop there if its host stopped now (`host.activity` on the remote), for the "Update"
|
|
115593
115882
|
* prompt. Through the link when connected; while the link is held at `versions_differ` on one
|
|
@@ -115643,8 +115932,9 @@ var LinkedMachine = class {
|
|
|
115643
115932
|
this.operation = { kind, step: s, target, at: at2 };
|
|
115644
115933
|
this.deps.broadcast({ type: "machine_status", machine: this.info() });
|
|
115645
115934
|
};
|
|
115935
|
+
let result;
|
|
115646
115936
|
try {
|
|
115647
|
-
|
|
115937
|
+
result = await fn2(this.control(tunnelExec, installer.script(), step), installer);
|
|
115648
115938
|
} catch (err) {
|
|
115649
115939
|
this.deps.log?.(`[links] ${this.name}: ${kind} failed: ${err.message}`);
|
|
115650
115940
|
throw linkError2(err.message);
|
|
@@ -115656,6 +115946,7 @@ var LinkedMachine = class {
|
|
|
115656
115946
|
if (!this.stopped && (wasHeld || this.status !== "connected")) this.reconnect();
|
|
115657
115947
|
this.deps.broadcast({ type: "machine_status", machine: this.info() });
|
|
115658
115948
|
}
|
|
115949
|
+
return result && typeof result === "object" && "machine" in result ? { ...result, machine: this.info() } : result;
|
|
115659
115950
|
}
|
|
115660
115951
|
/** What update.ts runs things there with */
|
|
115661
115952
|
control(exec8, script, step) {
|
|
@@ -115719,6 +116010,7 @@ var LinkedMachine = class {
|
|
|
115719
116010
|
let endpoint = await this.openTunnel();
|
|
115720
116011
|
if (!endpoint) return;
|
|
115721
116012
|
this.installed = installOf(endpoint.line.install);
|
|
116013
|
+
this.bootStart = autostartOf(endpoint.line.autostart);
|
|
115722
116014
|
if (!endpoint.line.hostRunning && this.tunnel.startHost) {
|
|
115723
116015
|
if (this.held) return;
|
|
115724
116016
|
this.setStatus("starting", null);
|
|
@@ -115740,6 +116032,7 @@ var LinkedMachine = class {
|
|
|
115740
116032
|
endpoint = await this.openTunnel();
|
|
115741
116033
|
if (!endpoint) return;
|
|
115742
116034
|
this.installed = installOf(endpoint.line.install);
|
|
116035
|
+
this.bootStart = autostartOf(endpoint.line.autostart);
|
|
115743
116036
|
}
|
|
115744
116037
|
if (this.held) return;
|
|
115745
116038
|
this.endpoint = endpoint;
|
|
@@ -115747,6 +116040,7 @@ var LinkedMachine = class {
|
|
|
115747
116040
|
this.setStatus("not_running", `Centralu is installed on ${this.name}, but no \`centralu serve\` is running there`);
|
|
115748
116041
|
return this.retryLater();
|
|
115749
116042
|
}
|
|
116043
|
+
for (const w of [...this.helloWaiters]) w(endpoint.line.version);
|
|
115750
116044
|
const versions = this.check({ version: endpoint.line.version, protocolVersion: endpoint.line.protocolVersion, dev: false });
|
|
115751
116045
|
if (!mayConnect(versions)) return;
|
|
115752
116046
|
this.connectClient(endpoint);
|
|
@@ -115948,6 +116242,9 @@ var Links = class {
|
|
|
115948
116242
|
uninstall(id2) {
|
|
115949
116243
|
return this.need(id2).uninstall();
|
|
115950
116244
|
}
|
|
116245
|
+
autostart(id2, on2) {
|
|
116246
|
+
return this.need(id2).autostart(on2);
|
|
116247
|
+
}
|
|
115951
116248
|
activity(id2) {
|
|
115952
116249
|
return this.need(id2).activity();
|
|
115953
116250
|
}
|
|
@@ -116176,6 +116473,7 @@ var ROUTES = {
|
|
|
116176
116473
|
"machines.update": HUB,
|
|
116177
116474
|
"machines.rollback": HUB,
|
|
116178
116475
|
"machines.uninstall": HUB,
|
|
116476
|
+
"machines.autostart": HUB,
|
|
116179
116477
|
"machines.activity": HUB,
|
|
116180
116478
|
// The hub's own answer; a hub asks a remote's through `machines.activity`
|
|
116181
116479
|
"host.activity": HUB,
|
|
@@ -116313,7 +116611,11 @@ var Router = class {
|
|
|
116313
116611
|
if (kind === "projects") return list.map((p) => mark(m.q.project(p)));
|
|
116314
116612
|
return list.filter((s) => !m.hides(s)).map((s) => mark(m.q.session(s)));
|
|
116315
116613
|
}
|
|
116316
|
-
/**
|
|
116614
|
+
/**
|
|
116615
|
+
* The person's project order, split per machine: each machine keeps the order of its own. A
|
|
116616
|
+
* machine that cannot take it does not fail the rest, which has already been saved: its projects
|
|
116617
|
+
* come back from the mirror as they were, marked `unreachable`, the way `projects.list` gives them
|
|
116618
|
+
*/
|
|
116317
116619
|
async reorderProjects(machines, params) {
|
|
116318
116620
|
const ids = isObj3(params) && Array.isArray(params.orderedIds) ? params.orderedIds : [];
|
|
116319
116621
|
const perMachine = /* @__PURE__ */ new Map();
|
|
@@ -116325,7 +116627,13 @@ var Router = class {
|
|
|
116325
116627
|
}
|
|
116326
116628
|
const [mine, ...theirs] = await Promise.all([
|
|
116327
116629
|
this.opts.local("projects.reorder", { ...isObj3(params) ? params : {}, orderedIds: local }),
|
|
116328
|
-
...[...perMachine].map(async ([m, orderedIds]) =>
|
|
116630
|
+
...[...perMachine].map(async ([m, orderedIds]) => {
|
|
116631
|
+
try {
|
|
116632
|
+
return m.q.projects(await this.forward(m, "projects.reorder", { orderedIds }));
|
|
116633
|
+
} catch {
|
|
116634
|
+
return this.present(m, "projects", m.lastKnown("projects") ?? [], true);
|
|
116635
|
+
}
|
|
116636
|
+
})
|
|
116329
116637
|
]);
|
|
116330
116638
|
return [...mine, ...theirs.flat()];
|
|
116331
116639
|
}
|
|
@@ -116419,7 +116727,7 @@ function storeMirror(store2) {
|
|
|
116419
116727
|
}
|
|
116420
116728
|
|
|
116421
116729
|
// packages/agent-host/src/main.ts
|
|
116422
|
-
var BUILD = true ? "
|
|
116730
|
+
var BUILD = true ? "34635b7" : "dev";
|
|
116423
116731
|
var underKeeper = process.env.CC_KEEPER === "1";
|
|
116424
116732
|
var keeperSource = process.env.CC_HOST_SOURCE;
|
|
116425
116733
|
var frontDoor = process.env.CC_FRONT_DOOR;
|
|
@@ -116682,6 +116990,7 @@ var router = new Router({
|
|
|
116682
116990
|
});
|
|
116683
116991
|
var server = new HostServer({
|
|
116684
116992
|
port: Number(values.port),
|
|
116993
|
+
...startedByServe ? { servedBy: "serve" } : {},
|
|
116685
116994
|
token,
|
|
116686
116995
|
allowedOrigins,
|
|
116687
116996
|
onRpc: router.handle,
|
|
@@ -9,9 +9,9 @@
|
|
|
9
9
|
// not survive the way back from Windows; tunnel.ts). The sentences for the person are the hub's.
|
|
10
10
|
import { spawnSync } from 'node:child_process'
|
|
11
11
|
import { createHash, randomBytes } from 'node:crypto'
|
|
12
|
-
import { chmodSync, existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, renameSync, rmSync, writeFileSync } from 'node:fs'
|
|
12
|
+
import { chmodSync, existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs'
|
|
13
13
|
import { homedir } from 'node:os'
|
|
14
|
-
import { join } from 'node:path'
|
|
14
|
+
import { basename, join } from 'node:path'
|
|
15
15
|
import { pathToFileURL } from 'node:url'
|
|
16
16
|
|
|
17
17
|
const WORD = /^[0-9A-Za-z][0-9A-Za-z.+-]{0,63}$/
|
|
@@ -86,27 +86,79 @@ export function prune(l) {
|
|
|
86
86
|
}
|
|
87
87
|
}
|
|
88
88
|
sweep(l.versions, new Set(keep.map((p) => p.version)))
|
|
89
|
+
// What `placeVersion` moved aside and could not remove then
|
|
90
|
+
try {
|
|
91
|
+
for (const name of readdirSync(l.root).filter((n) => n.startsWith('.replaced-'))) {
|
|
92
|
+
try {
|
|
93
|
+
rmSync(join(l.root, name), { recursive: true, force: true })
|
|
94
|
+
} catch {
|
|
95
|
+
left.push(name)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
} catch {}
|
|
89
99
|
sweep(l.node, new Set(keep.map((p) => `v${p.node}`)))
|
|
90
100
|
return { removed, left }
|
|
91
101
|
}
|
|
92
102
|
|
|
93
|
-
/**
|
|
94
|
-
|
|
95
|
-
|
|
103
|
+
/** A lock folder with no pid in it, younger than this, is held: its installer has made it and not yet written the pid */
|
|
104
|
+
const LOCK_YOUNG_MS = 60_000
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* One installer at a time per data folder; a lock whose process is gone is taken over.
|
|
108
|
+
*
|
|
109
|
+
* Taking it is `mkdir`, which only one installer can do; the pid follows. A lock found with no pid is
|
|
110
|
+
* held while it is young (another installer between the two, which reading it as pid 0 once took
|
|
111
|
+
* over), and stale only after `LOCK_YOUNG_MS` (an installer that died between them). A stale lock is
|
|
112
|
+
* taken over by moving it aside first, so of two installers that both found it stale only one removes
|
|
113
|
+
* it, and one that moved a lock just replaced by a live one puts it back. Unlock removes the lock only
|
|
114
|
+
* while it is still this run's own (its `token`).
|
|
115
|
+
*/
|
|
116
|
+
export function lock(l) {
|
|
117
|
+
const token = `${process.pid}-${randomBytes(8).toString('hex')}`
|
|
118
|
+
for (let i = 0; i < 3; i++) {
|
|
96
119
|
try {
|
|
97
120
|
mkdirSync(l.lock)
|
|
98
121
|
writeFileSync(join(l.lock, 'pid'), String(process.pid))
|
|
99
|
-
|
|
122
|
+
writeFileSync(join(l.lock, 'token'), token)
|
|
123
|
+
return () => {
|
|
124
|
+
if (readFileSafe(join(l.lock, 'token')) === token) rmSync(l.lock, { recursive: true, force: true })
|
|
125
|
+
}
|
|
100
126
|
} catch (err) {
|
|
101
127
|
if (err.code !== 'EEXIST') throw err
|
|
102
|
-
const pid = Number(readFileSafe(join(l.lock, 'pid')))
|
|
103
|
-
if (pid > 0 && alive(pid)) throw new Refusal('busy', String(pid))
|
|
104
|
-
rmSync(l.lock, { recursive: true, force: true })
|
|
105
128
|
}
|
|
129
|
+
const held = readFileSafe(join(l.lock, 'pid'))
|
|
130
|
+
if (!lockIsStale(l.lock, held)) throw new Refusal('busy', held.trim())
|
|
131
|
+
const aside = `${l.lock}.${token}.stale`
|
|
132
|
+
try {
|
|
133
|
+
renameSync(l.lock, aside)
|
|
134
|
+
} catch {
|
|
135
|
+
continue // gone already, or another installer moved it first
|
|
136
|
+
}
|
|
137
|
+
const moved = readFileSafe(join(aside, 'pid'))
|
|
138
|
+
if (moved !== held) {
|
|
139
|
+
// Replaced by a live lock between reading it and moving it: not ours to remove
|
|
140
|
+
try {
|
|
141
|
+
renameSync(aside, l.lock)
|
|
142
|
+
} catch {
|
|
143
|
+
// Someone holds a newer one; this one stays aside, named, and harmless
|
|
144
|
+
}
|
|
145
|
+
throw new Refusal('busy', moved.trim())
|
|
146
|
+
}
|
|
147
|
+
rmSync(aside, { recursive: true, force: true })
|
|
106
148
|
}
|
|
107
149
|
throw new Refusal('busy')
|
|
108
150
|
}
|
|
109
151
|
|
|
152
|
+
function lockIsStale(dir, pidText) {
|
|
153
|
+
const pid = Number(pidText.trim())
|
|
154
|
+
if (pidText.trim() && pid > 0) return !alive(pid)
|
|
155
|
+
try {
|
|
156
|
+
return Date.now() - statSync(dir).mtimeMs > LOCK_YOUNG_MS
|
|
157
|
+
} catch {
|
|
158
|
+
return true // gone: the next turn takes the lock
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
110
162
|
function readFileSafe(file) {
|
|
111
163
|
try {
|
|
112
164
|
return readFileSync(file, 'utf8')
|
|
@@ -144,10 +196,17 @@ export async function fetchChecked(pkg) {
|
|
|
144
196
|
return bytes
|
|
145
197
|
}
|
|
146
198
|
|
|
147
|
-
/**
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
199
|
+
/**
|
|
200
|
+
* Which package brings the host, where the host sits in it, and what of it a remote never runs (plan
|
|
201
|
+
* S6). The host-only package (`@centralu/host-<platform>`) is the host and nothing else; a version
|
|
202
|
+
* published before it existed comes as its platform package, whose window is thrown away
|
|
203
|
+
*/
|
|
204
|
+
function hostParts(platform, packages) {
|
|
205
|
+
const hostOnly = `@centralu/host-${platform}`
|
|
206
|
+
if (packages.some((pkg) => pkg.name === hostOnly)) return { pkg: hostOnly, host: join('host', 'main.mjs'), drop: [] }
|
|
207
|
+
const pkg = `@centralu/${platform}`
|
|
208
|
+
if (platform === 'win32-x64') return { pkg, host: join('Centralu', 'resources', 'host', 'main.mjs'), drop: [join('Centralu', 'centralu.exe')] }
|
|
209
|
+
return { pkg, host: join('host', 'main.mjs'), drop: ['Centralu.AppImage'] }
|
|
151
210
|
}
|
|
152
211
|
|
|
153
212
|
function sameInstall(dir, p) {
|
|
@@ -159,6 +218,32 @@ function sameInstall(dir, p) {
|
|
|
159
218
|
}
|
|
160
219
|
}
|
|
161
220
|
|
|
221
|
+
/**
|
|
222
|
+
* Puts the unpacked `partial` folder in place as `dir`. A `dir` already there (the same version
|
|
223
|
+
* installed again with other bytes, possibly the one `current` runs) is moved aside first and removed
|
|
224
|
+
* only once the new one is in place, and put back if that fails: removing it first would leave
|
|
225
|
+
* `current` naming nothing after a failed rename, and on Windows a removal can stop halfway on the
|
|
226
|
+
* files of a host running from it (moving that folder is allowed, plan §10.5). What cannot be removed
|
|
227
|
+
* stays aside under a dot name, and the next prune tries again
|
|
228
|
+
*/
|
|
229
|
+
export function placeVersion(l, partial, dir) {
|
|
230
|
+
const aside = existsSync(dir) ? join(l.root, `.replaced-${basename(dir)}-${process.pid}-${randomBytes(4).toString('hex')}`) : null
|
|
231
|
+
if (aside) renameSync(dir, aside)
|
|
232
|
+
try {
|
|
233
|
+
renameSync(partial, dir)
|
|
234
|
+
} catch (err) {
|
|
235
|
+
if (aside) renameSync(aside, dir)
|
|
236
|
+
throw err
|
|
237
|
+
}
|
|
238
|
+
if (aside) {
|
|
239
|
+
try {
|
|
240
|
+
rmSync(aside, { recursive: true, force: true })
|
|
241
|
+
} catch {
|
|
242
|
+
// A Windows program still running from it; harmless where it is
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
162
247
|
/** Installs `p.version` beside what is there, switches `current` to it, and keeps `previous` */
|
|
163
248
|
export async function install(p, { dataDir = process.env.CC_DATA_DIR || join(homedir(), '.centralu'), platform = process.platform } = {}) {
|
|
164
249
|
if (!WORD.test(p.version) || !WORD.test(p.node)) throw new Refusal('params', 'version')
|
|
@@ -167,7 +252,7 @@ export async function install(p, { dataDir = process.env.CC_DATA_DIR || join(hom
|
|
|
167
252
|
const unlock = lock(l)
|
|
168
253
|
try {
|
|
169
254
|
const dir = join(l.versions, p.version)
|
|
170
|
-
const parts =
|
|
255
|
+
const parts = hostParts(p.platform, p.packages)
|
|
171
256
|
if (!sameInstall(dir, p)) {
|
|
172
257
|
// Everything lands in a folder of its own first; the version's folder appears by one rename
|
|
173
258
|
const partial = join(l.root, `.partial-${p.version}-${process.pid}`)
|
|
@@ -183,7 +268,7 @@ export async function install(p, { dataDir = process.env.CC_DATA_DIR || join(hom
|
|
|
183
268
|
if (r.status !== 0) throw new Refusal('unpack', `${pkg.name}: ${(r.stderr || r.error?.message || '').trim().split('\n').at(-1)}`)
|
|
184
269
|
rmSync(file, { force: true })
|
|
185
270
|
}
|
|
186
|
-
const pkgDir = join(partial, 'node_modules', '
|
|
271
|
+
const pkgDir = join(partial, 'node_modules', ...parts.pkg.split('/'))
|
|
187
272
|
if (!existsSync(join(pkgDir, parts.host))) throw new Refusal('no_host', p.platform)
|
|
188
273
|
for (const d of parts.drop) rmSync(join(pkgDir, d), { force: true })
|
|
189
274
|
writeFileSync(
|
|
@@ -191,8 +276,7 @@ export async function install(p, { dataDir = process.env.CC_DATA_DIR || join(hom
|
|
|
191
276
|
JSON.stringify({ version: p.version, node: p.node, platform: p.platform, packages: p.packages.map(({ name, integrity }) => ({ name, integrity })), installedAt: new Date().toISOString(), by: p.hub ?? null }, null, 2) + '\n',
|
|
192
277
|
)
|
|
193
278
|
mkdirSync(l.versions, { recursive: true })
|
|
194
|
-
|
|
195
|
-
renameSync(partial, dir)
|
|
279
|
+
placeVersion(l, partial, dir)
|
|
196
280
|
} finally {
|
|
197
281
|
rmSync(partial, { recursive: true, force: true })
|
|
198
282
|
}
|
|
@@ -249,8 +333,10 @@ export async function setPointers(p, { dataDir = process.env.CC_DATA_DIR || join
|
|
|
249
333
|
return { current: null, previous: null }
|
|
250
334
|
}
|
|
251
335
|
const dir = join(l.versions, current.version)
|
|
252
|
-
|
|
253
|
-
|
|
336
|
+
// Both must be there: a `previous` naming nothing would offer a rollback to a version that is gone
|
|
337
|
+
for (const v of [current, previous].filter(Boolean)) {
|
|
338
|
+
if (!existsSync(join(l.versions, v.version, 'install.json')) || !existsSync(join(l.node, `v${v.node}`))) throw new Refusal('missing', `${v.version} ${v.node}`)
|
|
339
|
+
}
|
|
254
340
|
await ensureManagedLauncher(l, dir, current.version, platform)
|
|
255
341
|
if (previous) writeAtomic(l.previous, `${previous.version} ${previous.node}\n`)
|
|
256
342
|
else rmSync(l.previous, { force: true })
|