@bitkyc08/opencodex 2.50.0 → 2.51.0
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/bin/ocx.mjs +222 -71
- package/gui/dist/assets/{index-C39tnjXO.js → index-D7BdZpZm.js} +1 -1
- package/gui/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/adapters/qoder/adapter.ts +69 -1
- package/src/adapters/qoder/scaffold-guard.ts +233 -0
- package/src/claude/agents-inject.ts +29 -5
- package/src/claude/desktop-3p.ts +31 -3
- package/src/claude/gateway-cache.ts +12 -21
- package/src/cli/capabilities.ts +28 -0
- package/src/cli/claude-agent-startup-sync.ts +26 -1
- package/src/cli/claude.ts +138 -20
- package/src/cli/config-command.ts +67 -1
- package/src/cli/connect.ts +181 -14
- package/src/cli/dispatch.ts +53 -9
- package/src/cli/doctor.ts +9 -2
- package/src/cli/ensure-desired-integrations.ts +10 -0
- package/src/cli/gui-pair-client.ts +1 -12
- package/src/cli/help.ts +4 -1
- package/src/cli/hub.ts +367 -0
- package/src/cli/index.ts +94 -30
- package/src/cli/launcher-context.ts +1 -1
- package/src/cli/registry.ts +43 -3
- package/src/cli/status.ts +325 -5
- package/src/cli/version-skew.ts +4 -1
- package/src/cli.ts +2 -2
- package/src/client/catalog-compatibility.ts +192 -0
- package/src/client/connect.ts +31 -0
- package/src/client/hub-client.ts +52 -0
- package/src/client/hub-state.ts +214 -0
- package/src/codex/account-usability.ts +48 -12
- package/src/codex/auth-api.ts +49 -5
- package/src/codex/catalog/effort.ts +67 -8
- package/src/codex/catalog/sync.ts +85 -0
- package/src/codex/codex-write-lock.ts +11 -2
- package/src/codex/desired-state.ts +47 -1
- package/src/codex/inject-coordination.ts +10 -5
- package/src/codex/inject.ts +26 -10
- package/src/codex/loopback-target.ts +45 -0
- package/src/codex/routing.ts +48 -1
- package/src/codex/runtime.ts +37 -3
- package/src/codex/sync.ts +29 -9
- package/src/codex/warmup.ts +21 -4
- package/src/config/pending-teardown.ts +1 -1
- package/src/config.ts +126 -12
- package/src/generated/compatibility-version.json +136 -76
- package/src/grok/status.ts +9 -1
- package/src/integrations/config-io.ts +54 -1
- package/src/lib/bun-runtime.ts +1 -1
- package/src/lib/gui-pair-capability.ts +27 -0
- package/src/lib/local-destinations.ts +162 -0
- package/src/lib/package-tree-integrity.ts +1 -1
- package/src/lib/process-control.ts +130 -20
- package/src/lib/service-secrets.ts +28 -0
- package/src/lib/test-home-guard.ts +49 -0
- package/src/providers/opencode-go-transport.ts +9 -1
- package/src/providers/quota.ts +5 -1
- package/src/providers/registry.ts +34 -5
- package/src/remote/hub-state.ts +182 -0
- package/src/server/auth-cors.ts +5 -0
- package/src/server/chat-completions.ts +6 -3
- package/src/server/claude-messages.ts +7 -1
- package/src/server/hub-state.ts +98 -0
- package/src/server/index.ts +124 -6
- package/src/server/management/api-access.ts +14 -3
- package/src/server/management/config-routes.ts +2 -2
- package/src/server/management/cursor-integration-routes.ts +13 -4
- package/src/server/proxy-liveness.ts +7 -1
- package/src/server/request-log-conversation.ts +41 -1
- package/src/server/responses/codex-auth-error.ts +18 -1
- package/src/server/responses/codex-ws-exchange.ts +36 -4
- package/src/server/responses/codex-ws-wire.ts +75 -4
- package/src/server/responses/compact.ts +20 -9
- package/src/server/responses/core.ts +57 -10
- package/src/server/responses/policy-fallback.ts +7 -1
- package/src/server/system-env-shell.ts +14 -2
- package/src/server/system-env.ts +106 -14
- package/src/service.ts +906 -94
- package/src/types/config.ts +57 -4
- package/src/update/badge.ts +3 -2
- package/src/update/index.ts +317 -64
- package/src/update/install-detection.d.mts +6 -0
- package/src/update/install-detection.mjs +73 -0
- package/src/update/job.ts +101 -49
- package/src/update/pnpm-global-install.d.mts +144 -0
- package/src/update/pnpm-global-install.mjs +591 -0
- package/src/update/pnpm-invocation.d.mts +43 -0
- package/src/update/pnpm-invocation.mjs +141 -0
- package/src/update/registry-integrity.d.mts +16 -0
- package/src/update/registry-integrity.mjs +37 -0
- package/src/update/transactional-install.d.mts +1 -1
- package/src/update/transactional-install.mjs +101 -7
- package/src/update/tray-update-plan.mjs +1 -1
- package/src/vision/plan.ts +13 -3
- package/src/vision/routed-describe.ts +51 -20
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { realpathSync } from "node:fs";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Infer the package manager from the path of the running package.
|
|
5
|
+
*
|
|
6
|
+
* PATH is deliberately not consulted here. A machine can have npm, pnpm, and Bun
|
|
7
|
+
* installed at the same time; the package layout is the evidence of which manager owns
|
|
8
|
+
* the files that the updater must change. The legacy `global/<version>` spelling is only
|
|
9
|
+
* accepted when the adjacent filesystem metadata also looks like a pnpm global group.
|
|
10
|
+
*
|
|
11
|
+
* The real path is considered in addition to the spelling visible to the module loader.
|
|
12
|
+
* This matters when Node is launched with preserved symlinks: a pnpm package can be
|
|
13
|
+
* exposed through an npm-looking prefix while its target is still under pnpm's global
|
|
14
|
+
* virtual store.
|
|
15
|
+
*/
|
|
16
|
+
export function detectInstallFromPath(packagePath, deps = {}) {
|
|
17
|
+
const exists = deps.exists;
|
|
18
|
+
const candidates = [String(packagePath)];
|
|
19
|
+
try {
|
|
20
|
+
const resolved = (deps.realpath ?? realpathSync)(String(packagePath));
|
|
21
|
+
if (resolved && !candidates.includes(resolved)) candidates.push(resolved);
|
|
22
|
+
} catch {
|
|
23
|
+
// Synthetic paths in source-level checks, and a partially removed install, have no
|
|
24
|
+
// realpath. The lexical path still carries the evidence when it is available.
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
let sawNodeModules = false;
|
|
28
|
+
for (const candidate of candidates) {
|
|
29
|
+
const detected = detectInstallCandidate(candidate, exists);
|
|
30
|
+
if (detected === "pnpm" || detected === "bun") return detected;
|
|
31
|
+
if (detected === "npm") sawNodeModules = true;
|
|
32
|
+
}
|
|
33
|
+
return sawNodeModules ? "npm" : "source";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function detectInstallCandidate(packagePath, exists) {
|
|
37
|
+
const normalized = String(packagePath).replaceAll("\\", "/");
|
|
38
|
+
const segments = normalized.split("/").filter(Boolean);
|
|
39
|
+
// Windows paths are case-insensitive. Treating the structural marker this way also
|
|
40
|
+
// keeps a preserved-symlink path from being downgraded merely because its casing came
|
|
41
|
+
// from a Windows API or a user-created junction.
|
|
42
|
+
if (!segments.some(segment => segment.toLowerCase() === "node_modules")) return "source";
|
|
43
|
+
|
|
44
|
+
// Strong signatures survive normal symlink resolution: the v10 isolated virtual store
|
|
45
|
+
// and v11 global virtual store are both manager-owned paths. Do not classify an arbitrary
|
|
46
|
+
// npm prefix such as `/opt/global/v11` from its directory name alone.
|
|
47
|
+
if (
|
|
48
|
+
/(?:^|\/)node_modules\/\.pnpm(?:\/|$)/i.test(normalized)
|
|
49
|
+
|| /(?:^|\/)store\/v\d+\/links(?:\/|$)/i.test(normalized)
|
|
50
|
+
) return "pnpm";
|
|
51
|
+
|
|
52
|
+
// `--preserve-symlinks` can leave a v10/v11 group path visible. Corroborate it with the
|
|
53
|
+
// group's virtual store or the v11 global store before selecting pnpm.
|
|
54
|
+
const globalMatch = normalized.match(/^(.*\/global\/(?:v)?\d+)(?:\/[^/]+)*\/node_modules(?:\/|$)/i);
|
|
55
|
+
if (globalMatch && exists) {
|
|
56
|
+
const globalRoot = globalMatch[1];
|
|
57
|
+
const groupRoot = normalized.slice(0, normalized.toLowerCase().indexOf("/node_modules"));
|
|
58
|
+
if (
|
|
59
|
+
exists(`${groupRoot}/node_modules/.pnpm`)
|
|
60
|
+
|| exists(`${groupRoot}/node_modules/.modules.yaml`)
|
|
61
|
+
|| exists(`${groupRoot}/node_modules/.pnpm/lock.yaml`)
|
|
62
|
+
|| exists(`${globalRoot}/store`)
|
|
63
|
+
|| exists(`${globalRoot}/pnpm-lock.yaml`)
|
|
64
|
+
) return "pnpm";
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Bun's global layout is specifically `.bun/install/global/node_modules`. A bare `.bun`
|
|
68
|
+
// directory is not enough: npm projects can quite legitimately live under a dot-directory
|
|
69
|
+
// with that name, especially on Windows where package paths are often user-selected.
|
|
70
|
+
if (/(?:^|\/)\.bun\/install\/global\/node_modules(?:\/|$)/i.test(normalized)) return "bun";
|
|
71
|
+
|
|
72
|
+
return "npm";
|
|
73
|
+
}
|
package/src/update/job.ts
CHANGED
|
@@ -37,7 +37,10 @@ import {
|
|
|
37
37
|
latestVersion,
|
|
38
38
|
updateCommand,
|
|
39
39
|
updateCommandStr,
|
|
40
|
+
resolveCurrentPnpmGlobalOwner,
|
|
41
|
+
resolvePnpmActiveLauncher,
|
|
40
42
|
} from "./index";
|
|
43
|
+
import type { PnpmGlobalOwner } from "./pnpm-global-install.mjs";
|
|
41
44
|
import { isNewer } from "./notify";
|
|
42
45
|
import { isRealBunBinary } from "../lib/bun-binary-validator.mjs";
|
|
43
46
|
import { handoffWindowsTrayForUpdate, planWindowsTrayUpdate } from "./tray-update-plan.mjs";
|
|
@@ -132,6 +135,10 @@ function nodeBin(): string {
|
|
|
132
135
|
return process.platform === "win32" ? "node.exe" : "node";
|
|
133
136
|
}
|
|
134
137
|
|
|
138
|
+
function usesNodeLauncher(installer: Installer): boolean {
|
|
139
|
+
return installer === "npm" || installer === "pnpm";
|
|
140
|
+
}
|
|
141
|
+
|
|
135
142
|
/**
|
|
136
143
|
* Strict bind script: exit 0 only after listen+close. Any listen error (including
|
|
137
144
|
* Windows ghost-TCB failures under Bun) is busy — matches published `ocx start`
|
|
@@ -436,11 +443,13 @@ export function updateExecutionCommand(
|
|
|
436
443
|
launcher = packageLauncherPath(),
|
|
437
444
|
resolvedVersion?: string | null,
|
|
438
445
|
): { bin: string; args: string[]; display: string } {
|
|
439
|
-
if (installer
|
|
446
|
+
if (usesNodeLauncher(installer)) {
|
|
440
447
|
const bin = nodeBin();
|
|
441
448
|
const args = [launcher, "update", "--tag", channel];
|
|
442
449
|
// The Node launcher self-update re-resolves the tag at its own time — a residual
|
|
443
450
|
// divergence window this path cannot close (documented, not claimed immutable).
|
|
451
|
+
// Both npm and pnpm use it so package files are never replaced by the running Bun
|
|
452
|
+
// process; the launcher then selects the manager-native update implementation.
|
|
444
453
|
return { bin, args, display: formatCommand(bin, args) };
|
|
445
454
|
}
|
|
446
455
|
if (installer === "bun") {
|
|
@@ -467,7 +476,7 @@ export function restartCommand(
|
|
|
467
476
|
// Default to the in-place refresh: `install` always registers, while repair reuses a healthy
|
|
468
477
|
// Windows scheduler definition and re-registers only when the live definition is stale.
|
|
469
478
|
const svcArgs = serviceInstalled ? [launcher, ...(serviceArgs ?? ["service", "repair"])] : startArgs;
|
|
470
|
-
if (installer
|
|
479
|
+
if (usesNodeLauncher(installer)) {
|
|
471
480
|
const bin = nodeBin();
|
|
472
481
|
const args = svcArgs;
|
|
473
482
|
return { mode, bin, args, display: formatCommand(bin, args) };
|
|
@@ -920,8 +929,9 @@ function spawnDetachedStart(
|
|
|
920
929
|
job: UpdateJobState,
|
|
921
930
|
installer: Installer,
|
|
922
931
|
port?: number,
|
|
932
|
+
launcher = packageLauncherPath(),
|
|
923
933
|
): ChildProcess {
|
|
924
|
-
const cmd = restartCommand(false, installer,
|
|
934
|
+
const cmd = restartCommand(false, installer, launcher, port);
|
|
925
935
|
const env = { ...process.env };
|
|
926
936
|
delete env.OCX_SERVICE;
|
|
927
937
|
updateJob(job, {}, `$ ${cmd.display}`);
|
|
@@ -961,7 +971,7 @@ function spawnDetachedStart(
|
|
|
961
971
|
return child;
|
|
962
972
|
}
|
|
963
973
|
|
|
964
|
-
/** Identity snapshot used to prove
|
|
974
|
+
/** Identity snapshot used to prove a package-manager self-update replaced the pre-update process. */
|
|
965
975
|
export interface RestartProxyIdentity {
|
|
966
976
|
pid: number | null;
|
|
967
977
|
version?: string;
|
|
@@ -970,7 +980,9 @@ export interface RestartProxyIdentity {
|
|
|
970
980
|
/** Test seam: the wait/spawn pair is injectable so the restart path is verifiable. */
|
|
971
981
|
export interface RestartIo {
|
|
972
982
|
waitForPort?: typeof reclaimListenPort;
|
|
973
|
-
spawnStart?: (job: UpdateJobState, installer: Installer, port?: number) => void;
|
|
983
|
+
spawnStart?: (job: UpdateJobState, installer: Installer, port?: number, launcher?: string) => void;
|
|
984
|
+
/** The package launcher verified after a pnpm group switch or rollback. */
|
|
985
|
+
packageLauncherPathFn?: () => string;
|
|
974
986
|
serviceInstalledFn?: () => boolean;
|
|
975
987
|
/**
|
|
976
988
|
* After a service reinstall exits 0, only trust the service path when this is true.
|
|
@@ -1090,13 +1102,14 @@ async function restartAfterUpdate(
|
|
|
1090
1102
|
svcArgs = serviceReinstallArgs();
|
|
1091
1103
|
} catch { /* fallback to default service install */ }
|
|
1092
1104
|
}
|
|
1093
|
-
const
|
|
1105
|
+
const launcher = io.packageLauncherPathFn?.() ?? packageLauncherPath();
|
|
1106
|
+
const cmd = restartCommand(serviceInstalled, job.installer, launcher, port, svcArgs);
|
|
1094
1107
|
const waitFn = io.waitForPort ?? reclaimListenPort;
|
|
1095
1108
|
const listPids = io.listListenPidsFn ?? listListenPids;
|
|
1096
1109
|
const verifyOcx = io.verifyOcxFn ?? verifyPidIdentity;
|
|
1097
1110
|
const aliveFn = io.isAliveFn ?? isProcessAlive;
|
|
1098
1111
|
// Pre-update PID plus any ocx still LISTENing on the captured port. After a
|
|
1099
|
-
// stop-first
|
|
1112
|
+
// stop-first package-manager self-update Windows often leaves a respawned bun/node child
|
|
1100
1113
|
// that is not the captured PID; treating it as protected blocks reclaim and
|
|
1101
1114
|
// the direct-start fallback never binds.
|
|
1102
1115
|
const reclaimKillAllowlist = (): number[] => {
|
|
@@ -1294,7 +1307,7 @@ async function restartAfterUpdate(
|
|
|
1294
1307
|
// Injected spawnStart keeps unit tests deterministic (one call). Production path
|
|
1295
1308
|
// retries on missing /healthz after prepare + ghost-LISTEN clear.
|
|
1296
1309
|
if (io.spawnStart) {
|
|
1297
|
-
io.spawnStart(job, job.installer, port);
|
|
1310
|
+
io.spawnStart(job, job.installer, port, launcher);
|
|
1298
1311
|
return;
|
|
1299
1312
|
}
|
|
1300
1313
|
const sleep = io.sleepMs ?? ((ms: number) => new Promise<void>(r => setTimeout(r, ms)));
|
|
@@ -1352,7 +1365,7 @@ async function restartAfterUpdate(
|
|
|
1352
1365
|
);
|
|
1353
1366
|
continue;
|
|
1354
1367
|
}
|
|
1355
|
-
lastChild = spawnDetachedStart(job, job.installer, port);
|
|
1368
|
+
lastChild = spawnDetachedStart(job, job.installer, port, launcher);
|
|
1356
1369
|
const healthDeadline = Date.now() + perAttemptHealthMs;
|
|
1357
1370
|
while (Date.now() < healthDeadline) {
|
|
1358
1371
|
if (await probe(port, hostname)) return;
|
|
@@ -1422,11 +1435,18 @@ export function restartAfterUpdateForTests(
|
|
|
1422
1435
|
return restartAfterUpdate(job, captured, io);
|
|
1423
1436
|
}
|
|
1424
1437
|
|
|
1425
|
-
function restartFailureHint(port: number): string {
|
|
1438
|
+
function restartFailureHint(port: number, installer: Installer): string {
|
|
1439
|
+
const reinstall = installer === "pnpm"
|
|
1440
|
+
? "pnpm add -g --allow-build=bun @bitkyc08/opencodex"
|
|
1441
|
+
: installer === "bun"
|
|
1442
|
+
? "bun add -g @bitkyc08/opencodex"
|
|
1443
|
+
: installer === "source"
|
|
1444
|
+
? "git pull && bun install"
|
|
1445
|
+
: "npm install -g --allow-scripts=bun @bitkyc08/opencodex";
|
|
1426
1446
|
return `Update installed, but the restarted proxy did not stay healthy on port ${port}. `
|
|
1427
1447
|
+ `Try 'ocx start --port ${port}'. `
|
|
1428
1448
|
+ "If the update log shows bun postinstall or EPERM warnings, "
|
|
1429
|
-
+
|
|
1449
|
+
+ `reinstall with '${reinstall}'.`;
|
|
1430
1450
|
}
|
|
1431
1451
|
|
|
1432
1452
|
type AwaitHealthyResult =
|
|
@@ -1517,7 +1537,7 @@ async function confirmRestartedProxy(
|
|
|
1517
1537
|
status: "failed",
|
|
1518
1538
|
restarted: false,
|
|
1519
1539
|
error,
|
|
1520
|
-
}, restartFailureHint(port));
|
|
1540
|
+
}, restartFailureHint(port, job.installer));
|
|
1521
1541
|
return false;
|
|
1522
1542
|
}
|
|
1523
1543
|
|
|
@@ -1559,7 +1579,7 @@ async function defaultProbeProxyIdentity(
|
|
|
1559
1579
|
* when the pre-update PID was captured, and/or /healthz reporting the job's target
|
|
1560
1580
|
* version when PID evidence is unavailable.
|
|
1561
1581
|
*/
|
|
1562
|
-
export function
|
|
1582
|
+
export function packageManagerSelfUpdateRestartEvidence(
|
|
1563
1583
|
job: Pick<UpdateJobState, "latestVersion">,
|
|
1564
1584
|
captured: { oldPid?: number },
|
|
1565
1585
|
identity: RestartProxyIdentity | null,
|
|
@@ -1601,21 +1621,25 @@ export function npmSelfUpdateRestartEvidence(
|
|
|
1601
1621
|
return { ok: false, reason: "no pre-update PID capture and no expected-version match" };
|
|
1602
1622
|
}
|
|
1603
1623
|
|
|
1624
|
+
// Kept as a compatibility export for callers and existing integrations that used the
|
|
1625
|
+
// original npm-specific name before pnpm became a supported package-manager path.
|
|
1626
|
+
export const npmSelfUpdateRestartEvidence = packageManagerSelfUpdateRestartEvidence;
|
|
1627
|
+
|
|
1604
1628
|
/**
|
|
1605
1629
|
* Post-install restart for the GUI worker.
|
|
1606
1630
|
*
|
|
1607
|
-
* npm installs run `node ocx.mjs update`, which already stops the proxy and reinstalls /
|
|
1631
|
+
* npm and pnpm installs run `node ocx.mjs update`, which already stops the proxy and reinstalls /
|
|
1608
1632
|
* starts the service (or falls back to a direct start). A second `service install` here
|
|
1609
1633
|
* calls `stopWindows()` on that healthy listener, then often fails elevation from the
|
|
1610
1634
|
* non-interactive worker — leaving the captured port (default 10100) dead until a manual
|
|
1611
|
-
* restart. Prefer confirming the
|
|
1635
|
+
* restart. Prefer confirming the package-manager self-update's own restart first; only re-run restart
|
|
1612
1636
|
* when that probe fails. Bun/source installs still always take the explicit restart path.
|
|
1613
1637
|
*
|
|
1614
|
-
* Probe-first applies only to service-managed npm installs: without a service, `ocx.mjs`
|
|
1638
|
+
* Probe-first applies only to service-managed npm/pnpm installs: without a service, `ocx.mjs`
|
|
1615
1639
|
* only prints `ocx start` and never brings the proxy back, so waiting would always burn
|
|
1616
1640
|
* the full health timeout. Skipping also requires update-correlated evidence (PID change
|
|
1617
1641
|
* and/or target version) so a surviving pre-update process cannot look like success.
|
|
1618
|
-
* After an explicit
|
|
1642
|
+
* After an explicit package-manager restart the same evidence is required again — health alone is
|
|
1619
1643
|
* not enough when a no-op restart or failed port reclaim leaves the old proxy up.
|
|
1620
1644
|
*
|
|
1621
1645
|
* Browser-dashboard update recovery must not require a viable Background Service: when
|
|
@@ -1628,10 +1652,10 @@ export async function finishGuiUpdateRestart(
|
|
|
1628
1652
|
installer: Installer,
|
|
1629
1653
|
io: RestartIo = {},
|
|
1630
1654
|
): Promise<boolean> {
|
|
1631
|
-
if (installer
|
|
1655
|
+
if (usesNodeLauncher(installer)) {
|
|
1632
1656
|
const serviceInstalled = (io.serviceInstalledFn ?? isServiceInstalled)();
|
|
1633
1657
|
if (serviceInstalled) {
|
|
1634
|
-
// Stop-first
|
|
1658
|
+
// Stop-first package-manager update leaves a dead PID's LISTEN row. Polling /healthz for the
|
|
1635
1659
|
// full 30s against that zombie keeps ESTABLISHED TCBs alive and blocks bind.
|
|
1636
1660
|
// If nothing live owns the port, skip straight to explicit restart. A failed
|
|
1637
1661
|
// listener scan must not look like "no listeners" — fall back to /healthz.
|
|
@@ -1646,13 +1670,13 @@ export async function finishGuiUpdateRestart(
|
|
|
1646
1670
|
? scan.pids.filter(pid => pid !== process.pid && aliveFn(pid))
|
|
1647
1671
|
: null;
|
|
1648
1672
|
if (liveListeners !== null && liveListeners.length === 0) {
|
|
1649
|
-
updateJob(job, {},
|
|
1673
|
+
updateJob(job, {}, `${installer} self-update did not leave a live listener; performing explicit restart...`);
|
|
1650
1674
|
} else {
|
|
1651
1675
|
if (!scan.ok) {
|
|
1652
1676
|
updateJob(
|
|
1653
1677
|
job,
|
|
1654
1678
|
{},
|
|
1655
|
-
|
|
1679
|
+
`Listener scan inconclusive after ${installer} self-update; probing /healthz before deciding on explicit restart...`,
|
|
1656
1680
|
);
|
|
1657
1681
|
}
|
|
1658
1682
|
const already = await awaitRestartedProxyHealthy(job, captured, io);
|
|
@@ -1661,42 +1685,42 @@ export async function finishGuiUpdateRestart(
|
|
|
1661
1685
|
captured.port,
|
|
1662
1686
|
captured.hostname,
|
|
1663
1687
|
);
|
|
1664
|
-
const evidence =
|
|
1688
|
+
const evidence = packageManagerSelfUpdateRestartEvidence(job, captured, identity);
|
|
1665
1689
|
if (evidence.ok) {
|
|
1666
1690
|
updateJob(
|
|
1667
1691
|
job,
|
|
1668
1692
|
{},
|
|
1669
|
-
`Proxy already healthy on ${captured.hostname}:${captured.port} after
|
|
1693
|
+
`Proxy already healthy on ${captured.hostname}:${captured.port} after ${installer} self-update (${evidence.detail}); skipping redundant restart.`,
|
|
1670
1694
|
);
|
|
1671
1695
|
return true;
|
|
1672
1696
|
}
|
|
1673
1697
|
updateJob(
|
|
1674
1698
|
job,
|
|
1675
1699
|
{},
|
|
1676
|
-
|
|
1700
|
+
`${installer} self-update left a healthy proxy but ${evidence.reason}; performing explicit restart...`,
|
|
1677
1701
|
);
|
|
1678
1702
|
} else {
|
|
1679
|
-
updateJob(job, {},
|
|
1703
|
+
updateJob(job, {}, `${installer} self-update did not leave a healthy proxy; performing explicit restart...`);
|
|
1680
1704
|
}
|
|
1681
1705
|
}
|
|
1682
1706
|
}
|
|
1683
1707
|
}
|
|
1684
1708
|
const restartFn = io.restartAfterUpdateFn ?? restartAfterUpdate;
|
|
1685
1709
|
await restartFn(job, captured, io);
|
|
1686
|
-
if (installer
|
|
1710
|
+
if (!usesNodeLauncher(installer)) {
|
|
1687
1711
|
// Bun/source: health alone remains enough unless a richer identity probe is supplied.
|
|
1688
1712
|
if (!io.probeProxyIdentity) return confirmRestartedProxy(job, captured, io);
|
|
1689
1713
|
}
|
|
1690
|
-
return
|
|
1714
|
+
return confirmPackageManagerExplicitRestart(job, captured, io);
|
|
1691
1715
|
}
|
|
1692
1716
|
|
|
1693
1717
|
/**
|
|
1694
|
-
* After an explicit
|
|
1718
|
+
* After an explicit package-manager (or identity-aware) restart, require update-correlated
|
|
1695
1719
|
* evidence — not merely a healthy OpenCodex listener. A no-op restart or a
|
|
1696
1720
|
* failed port reclaim can leave the pre-update process on the captured port;
|
|
1697
1721
|
* `confirmRestartedProxy` alone would treat that as success.
|
|
1698
1722
|
*/
|
|
1699
|
-
async function
|
|
1723
|
+
async function confirmPackageManagerExplicitRestart(
|
|
1700
1724
|
job: UpdateJobState,
|
|
1701
1725
|
captured: { port: number; hostname: string; oldPid?: number },
|
|
1702
1726
|
io: RestartIo = {},
|
|
@@ -1712,7 +1736,7 @@ async function confirmNpmExplicitRestart(
|
|
|
1712
1736
|
status: "failed",
|
|
1713
1737
|
restarted: false,
|
|
1714
1738
|
error,
|
|
1715
|
-
}, restartFailureHint(port));
|
|
1739
|
+
}, restartFailureHint(port, job.installer));
|
|
1716
1740
|
return false;
|
|
1717
1741
|
}
|
|
1718
1742
|
|
|
@@ -1720,13 +1744,13 @@ async function confirmNpmExplicitRestart(
|
|
|
1720
1744
|
captured.port,
|
|
1721
1745
|
captured.hostname,
|
|
1722
1746
|
);
|
|
1723
|
-
const evidence =
|
|
1747
|
+
const evidence = packageManagerSelfUpdateRestartEvidence(job, captured, identity);
|
|
1724
1748
|
if (!evidence.ok) {
|
|
1725
1749
|
updateJob(job, {
|
|
1726
1750
|
status: "failed",
|
|
1727
1751
|
restarted: false,
|
|
1728
1752
|
error: `proxy restart did not show update-correlated identity (${evidence.reason})`,
|
|
1729
|
-
}, restartFailureHint(captured.port));
|
|
1753
|
+
}, restartFailureHint(captured.port, job.installer));
|
|
1730
1754
|
return false;
|
|
1731
1755
|
}
|
|
1732
1756
|
|
|
@@ -1748,10 +1772,16 @@ async function confirmNpmExplicitRestart(
|
|
|
1748
1772
|
*/
|
|
1749
1773
|
export interface GuiUpdateWorkerIo {
|
|
1750
1774
|
cachePreflightFn?: () => { ok: boolean; reason: string };
|
|
1751
|
-
/** Force the resolved update target. A source checkout otherwise aborts before the
|
|
1775
|
+
/** Force the resolved update target. A source checkout otherwise aborts before the update branch. */
|
|
1752
1776
|
checkForUpdateFn?: (channel: Channel) => ReturnType<typeof checkForUpdate>;
|
|
1753
1777
|
/** Bypass the registry integrity probe, which runs before the cache gate and needs network. */
|
|
1754
1778
|
integrityFn?: (version: string | null) => ReturnType<typeof checkUpdatePackageIntegrity>;
|
|
1779
|
+
/** Override pnpm ownership discovery for an isolated worker test. */
|
|
1780
|
+
resolvePnpmOwnerFn?: () => ReturnType<typeof resolveCurrentPnpmGlobalOwner>;
|
|
1781
|
+
/** Resolve the launcher only after the pnpm update has verified its active group and shims. */
|
|
1782
|
+
resolvePnpmActiveLauncherFn?: (owner: PnpmGlobalOwner) => string | null;
|
|
1783
|
+
/** Restart seams used by focused worker tests; the verified launcher is always injected. */
|
|
1784
|
+
restartIo?: RestartIo;
|
|
1755
1785
|
runCommandFn?: (
|
|
1756
1786
|
job: UpdateJobState,
|
|
1757
1787
|
bin: string,
|
|
@@ -1786,6 +1816,9 @@ export async function runGuiUpdateWorker(
|
|
|
1786
1816
|
};
|
|
1787
1817
|
let trayWasInstalled = false;
|
|
1788
1818
|
let trayWasRunning = false;
|
|
1819
|
+
let activeLauncher = packageLauncherPath();
|
|
1820
|
+
let activeLauncherVerified = true;
|
|
1821
|
+
let pnpmOwner: PnpmGlobalOwner | undefined;
|
|
1789
1822
|
if (!job) {
|
|
1790
1823
|
job = {
|
|
1791
1824
|
id: jobId,
|
|
@@ -1809,10 +1842,18 @@ export async function runGuiUpdateWorker(
|
|
|
1809
1842
|
throw new Error(check.reason ?? "No update is available");
|
|
1810
1843
|
}
|
|
1811
1844
|
|
|
1845
|
+
if (check.installer === "pnpm") {
|
|
1846
|
+
const ownerResult = (io.resolvePnpmOwnerFn ?? resolveCurrentPnpmGlobalOwner)();
|
|
1847
|
+
if (!ownerResult.ok) throw new Error(`Could not identify pnpm's owning global installation: ${ownerResult.reason}`);
|
|
1848
|
+
pnpmOwner = ownerResult.owner;
|
|
1849
|
+
}
|
|
1850
|
+
|
|
1812
1851
|
// Pre-flight integrity metadata check (same lanes as the CLI): anomalous registry
|
|
1813
1852
|
// metadata for a resolved version fails the job BEFORE anything is spawned or the
|
|
1814
1853
|
// proxy is stopped; transient registry failure degrades to a logged skip.
|
|
1815
|
-
const integrity =
|
|
1854
|
+
const integrity = io.integrityFn
|
|
1855
|
+
? io.integrityFn(check.latestVersion)
|
|
1856
|
+
: checkUpdatePackageIntegrity(check.latestVersion, spawnSync, check.installer, pnpmOwner);
|
|
1816
1857
|
if (integrity.ok === false) {
|
|
1817
1858
|
updateJob(job, { status: "failed", error: integrity.reason });
|
|
1818
1859
|
return;
|
|
@@ -1865,15 +1906,19 @@ export async function runGuiUpdateWorker(
|
|
|
1865
1906
|
/* [Decision Log]
|
|
1866
1907
|
- 목적: GUI 요청 처리 프로세스가 자신이 실행 중인 패키지를 직접 덮어쓰지 않도록 업데이트를 별도 worker에서 수행한다.
|
|
1867
1908
|
- 대안 분석: (1) 서버에서 runUpdate 직접 호출: process.exit/stdio/실행 파일 교체 위험. (2) GUI에서 CLI 명령 안내만 제공: 자동 업데이트 UX 부족. (3) 숨은 worker가 Node launcher/Bun 전역 명령을 실행: 상태 추적과 안전한 재시작이 가능.
|
|
1868
|
-
- 선택 근거: 현재 CLI의
|
|
1909
|
+
- 선택 근거: 현재 CLI의 package-manager self-update 우회를 재사용하면서도 GUI 서버 요청 생명주기와 설치 작업을 분리할 수 있어 가장 안정적이다.
|
|
1869
1910
|
*/
|
|
1870
1911
|
const result = (io.runCommandFn ?? runLoggedCommand)(job, cmd.bin, cmd.args, UPDATE_TIMEOUT_MS);
|
|
1871
1912
|
if (result.status !== 0) {
|
|
1872
|
-
if (
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1913
|
+
if (check.installer === "pnpm") {
|
|
1914
|
+
const verifiedLauncher = pnpmOwner
|
|
1915
|
+
? (io.resolvePnpmActiveLauncherFn ?? resolvePnpmActiveLauncher)(pnpmOwner)
|
|
1916
|
+
: null;
|
|
1917
|
+
if (verifiedLauncher) activeLauncher = verifiedLauncher;
|
|
1918
|
+
else activeLauncherVerified = false;
|
|
1919
|
+
}
|
|
1920
|
+
if (trayWasRunning && activeLauncherVerified) {
|
|
1921
|
+
runLoggedCommand(job, process.execPath, [activeLauncher, "tray", "start"], 15_000);
|
|
1877
1922
|
}
|
|
1878
1923
|
updateJob(job, {
|
|
1879
1924
|
status: "failed",
|
|
@@ -1884,29 +1929,36 @@ export async function runGuiUpdateWorker(
|
|
|
1884
1929
|
return;
|
|
1885
1930
|
}
|
|
1886
1931
|
|
|
1932
|
+
if (check.installer === "pnpm") {
|
|
1933
|
+
const verifiedLauncher = (io.resolvePnpmActiveLauncherFn ?? resolvePnpmActiveLauncher)(pnpmOwner!);
|
|
1934
|
+
if (!verifiedLauncher) throw new Error("pnpm update succeeded but no verified active launcher remains");
|
|
1935
|
+
activeLauncher = verifiedLauncher;
|
|
1936
|
+
}
|
|
1937
|
+
|
|
1887
1938
|
if (trayWasInstalled) {
|
|
1888
|
-
const trayArgs =
|
|
1889
|
-
const tray = runLoggedCommand(job, process.execPath, trayArgs, 20_000);
|
|
1939
|
+
const trayArgs = planWindowsTrayUpdate({ installed: trayWasInstalled, running: trayWasRunning }).installArgs;
|
|
1940
|
+
const tray = runLoggedCommand(job, process.execPath, [activeLauncher, ...trayArgs], 20_000);
|
|
1890
1941
|
if (tray.status !== 0) {
|
|
1891
1942
|
updateJob(job, {}, "Windows tray refresh failed; run 'ocx tray install'.");
|
|
1892
|
-
if (trayWasRunning) runLoggedCommand(job, process.execPath,
|
|
1943
|
+
if (trayWasRunning) runLoggedCommand(job, process.execPath, [activeLauncher, "tray", "start"], 15_000);
|
|
1893
1944
|
}
|
|
1894
1945
|
}
|
|
1895
1946
|
|
|
1896
1947
|
if (restart) {
|
|
1897
1948
|
job = updateJob(job, { status: "restarting" }, "Update installed. Restarting proxy...");
|
|
1898
|
-
if (!(await finishGuiUpdateRestart(job, captured, check.installer
|
|
1949
|
+
if (!(await finishGuiUpdateRestart(job, captured, check.installer, {
|
|
1950
|
+
...io.restartIo,
|
|
1951
|
+
packageLauncherPathFn: () => activeLauncher,
|
|
1952
|
+
}))) return;
|
|
1899
1953
|
updateJob(job, { status: "succeeded", restarted: true }, "Restart requested and proxy is healthy.");
|
|
1900
1954
|
return;
|
|
1901
1955
|
}
|
|
1902
1956
|
|
|
1903
1957
|
updateJob(job, { status: "succeeded", restarted: false }, "Update installed. Restart the proxy to use the new version.");
|
|
1904
1958
|
} catch (err) {
|
|
1905
|
-
if (
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
startWindowsTray();
|
|
1909
|
-
} catch { /* retain the primary worker failure */ }
|
|
1959
|
+
if (check.installer === "pnpm") activeLauncherVerified = false;
|
|
1960
|
+
if (trayWasRunning && activeLauncherVerified) {
|
|
1961
|
+
runLoggedCommand(job, process.execPath, [activeLauncher, "tray", "start"], 15_000);
|
|
1910
1962
|
}
|
|
1911
1963
|
updateJob(job, {
|
|
1912
1964
|
status: "failed",
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
export interface PnpmRunResult {
|
|
2
|
+
status: number | null;
|
|
3
|
+
stdout?: string | Uint8Array | null;
|
|
4
|
+
stderr?: string | Uint8Array | null;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export type RunPnpm = (args: readonly string[], capture?: boolean) => PnpmRunResult;
|
|
8
|
+
export type RunPnpmCandidate = (
|
|
9
|
+
commandPath: string,
|
|
10
|
+
args: readonly string[],
|
|
11
|
+
capture?: boolean,
|
|
12
|
+
) => PnpmRunResult;
|
|
13
|
+
|
|
14
|
+
export interface PnpmGlobalOwner {
|
|
15
|
+
commandPath: string;
|
|
16
|
+
packagePath: string;
|
|
17
|
+
/** Base passed to pnpm's --global-dir; pnpm creates the versioned group below it. */
|
|
18
|
+
globalDir: string;
|
|
19
|
+
/** Actual versioned global group reported by pnpm root/list. */
|
|
20
|
+
globalRoot: string;
|
|
21
|
+
globalBinDir: string;
|
|
22
|
+
version?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type PnpmGlobalOwnerResult =
|
|
26
|
+
| { ok: true; owner: PnpmGlobalOwner }
|
|
27
|
+
| { ok: false; reason: string };
|
|
28
|
+
|
|
29
|
+
export type PnpmVerificationResult = { ok: boolean; reason?: string };
|
|
30
|
+
|
|
31
|
+
export declare const PNPM_BUILD_APPROVAL: string;
|
|
32
|
+
|
|
33
|
+
export declare function pnpmGlobalCommandArgs(
|
|
34
|
+
args: readonly string[],
|
|
35
|
+
owner: PnpmGlobalOwner,
|
|
36
|
+
): string[];
|
|
37
|
+
|
|
38
|
+
export declare function pnpmOwnerEnvironment(
|
|
39
|
+
owner: PnpmGlobalOwner,
|
|
40
|
+
env?: Record<string, string | undefined>,
|
|
41
|
+
platform?: NodeJS.Platform,
|
|
42
|
+
): Record<string, string | undefined>;
|
|
43
|
+
|
|
44
|
+
export interface PnpmOwnerInvocation {
|
|
45
|
+
file: string;
|
|
46
|
+
args: string[];
|
|
47
|
+
options: { windowsVerbatimArguments?: boolean };
|
|
48
|
+
env: Record<string, string | undefined>;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export declare function pnpmOwnerInvocation(
|
|
52
|
+
owner: PnpmGlobalOwner,
|
|
53
|
+
args: readonly string[],
|
|
54
|
+
platform?: NodeJS.Platform,
|
|
55
|
+
env?: Record<string, string | undefined>,
|
|
56
|
+
): PnpmOwnerInvocation | null;
|
|
57
|
+
|
|
58
|
+
export declare function verifyPnpmGlobalShims(
|
|
59
|
+
packageDir: string,
|
|
60
|
+
globalBinDir: string,
|
|
61
|
+
platform?: NodeJS.Platform,
|
|
62
|
+
exists?: (path: string) => boolean,
|
|
63
|
+
): PnpmVerificationResult;
|
|
64
|
+
|
|
65
|
+
export type PnpmGlobalPackage =
|
|
66
|
+
| {
|
|
67
|
+
ok: true;
|
|
68
|
+
version: string;
|
|
69
|
+
path: string;
|
|
70
|
+
globalDir?: string;
|
|
71
|
+
globalRoot?: string;
|
|
72
|
+
globalBinDir?: string;
|
|
73
|
+
}
|
|
74
|
+
| { ok: false; reason: string };
|
|
75
|
+
|
|
76
|
+
export interface PnpmGlobalReadConstraints {
|
|
77
|
+
owner?: PnpmGlobalOwner;
|
|
78
|
+
expectedPackagePath?: string;
|
|
79
|
+
expectedGlobalDir?: string;
|
|
80
|
+
expectedGlobalRoot?: string;
|
|
81
|
+
globalBinDir?: string;
|
|
82
|
+
/** Set false only for pre-update owner binding; post-update reads verify shims by default. */
|
|
83
|
+
checkShims?: boolean;
|
|
84
|
+
platform?: NodeJS.Platform;
|
|
85
|
+
verifyShims?: (
|
|
86
|
+
packageDir: string,
|
|
87
|
+
globalBinDir: string,
|
|
88
|
+
platform?: NodeJS.Platform,
|
|
89
|
+
) => PnpmVerificationResult;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export declare function readPnpmGlobalPackage(
|
|
93
|
+
packageName: string,
|
|
94
|
+
runPnpm: RunPnpm,
|
|
95
|
+
verify?: (packageDir: string, expectedVersion?: string) => PnpmVerificationResult,
|
|
96
|
+
constraints?: PnpmGlobalReadConstraints,
|
|
97
|
+
): PnpmGlobalPackage;
|
|
98
|
+
|
|
99
|
+
export declare function resolvePnpmGlobalOwner(options: {
|
|
100
|
+
packageName: string;
|
|
101
|
+
packagePath: string;
|
|
102
|
+
commandPaths: readonly string[];
|
|
103
|
+
runningShimPath?: string;
|
|
104
|
+
runPnpm: RunPnpmCandidate;
|
|
105
|
+
verify?: (packageDir: string, expectedVersion?: string) => PnpmVerificationResult;
|
|
106
|
+
platform?: NodeJS.Platform;
|
|
107
|
+
}): PnpmGlobalOwnerResult;
|
|
108
|
+
|
|
109
|
+
export type PnpmGlobalUpdateResult =
|
|
110
|
+
| {
|
|
111
|
+
ok: true;
|
|
112
|
+
phase: "done";
|
|
113
|
+
version: string;
|
|
114
|
+
path: string;
|
|
115
|
+
globalDir: string;
|
|
116
|
+
globalBinDir: string;
|
|
117
|
+
}
|
|
118
|
+
| {
|
|
119
|
+
ok: false;
|
|
120
|
+
phase: "preflight" | "install" | "rollback";
|
|
121
|
+
rolledBack?: boolean;
|
|
122
|
+
activePath?: string;
|
|
123
|
+
globalDir?: string;
|
|
124
|
+
globalBinDir?: string;
|
|
125
|
+
error: string;
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
export declare function runPnpmGlobalUpdate(options: {
|
|
129
|
+
packageName: string;
|
|
130
|
+
currentVersion?: string;
|
|
131
|
+
targetVersion?: string;
|
|
132
|
+
tag: string;
|
|
133
|
+
owner: PnpmGlobalOwner;
|
|
134
|
+
runningPackagePath?: string;
|
|
135
|
+
runPnpm: RunPnpm;
|
|
136
|
+
verify?: (packageDir: string, expectedVersion?: string) => PnpmVerificationResult;
|
|
137
|
+
verifyShims?: (
|
|
138
|
+
packageDir: string,
|
|
139
|
+
globalBinDir: string,
|
|
140
|
+
platform?: NodeJS.Platform,
|
|
141
|
+
) => PnpmVerificationResult;
|
|
142
|
+
platform?: NodeJS.Platform;
|
|
143
|
+
log?: (line: string) => void;
|
|
144
|
+
}): PnpmGlobalUpdateResult;
|