@bitkyc08/opencodex 2.49.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/AGENTS_INSTALL.md +9 -1
- package/README.md +3 -0
- package/bin/ocx.mjs +222 -71
- package/gui/dist/assets/index-D7BdZpZm.js +115 -0
- 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/claude/inbound.ts +17 -5
- package/src/cli/account-api.ts +18 -3
- package/src/cli/account-auth.ts +8 -1
- package/src/cli/account-extended.ts +2 -1
- package/src/cli/account.ts +1 -0
- package/src/cli/capabilities.ts +43 -1
- 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 +99 -31
- package/src/cli/launcher-context.ts +1 -1
- package/src/cli/models-runtime.ts +8 -3
- package/src/cli/observe.ts +13 -3
- 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/clients/config-export/zcode.ts +24 -0
- package/src/codex/account-runtime-state.ts +6 -1
- package/src/codex/account-store.ts +72 -9
- package/src/codex/account-usability.ts +50 -13
- package/src/codex/auth-api.ts +156 -28
- package/src/codex/auth-context.ts +21 -0
- package/src/codex/catalog/effort.ts +67 -8
- package/src/codex/catalog/parsing.ts +23 -0
- package/src/codex/catalog/provider-fetch.ts +71 -2
- package/src/codex/catalog/sync.ts +99 -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 +29 -12
- package/src/codex/loopback-target.ts +45 -0
- package/src/codex/quota-auto-refresh.ts +6 -1
- package/src/codex/quota.ts +54 -8
- 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/combos/index.ts +2 -0
- package/src/combos/resolve.ts +52 -0
- package/src/config/pending-teardown.ts +1 -1
- package/src/config.ts +184 -12
- package/src/generated/compatibility-version.json +188 -116
- 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/errors.ts +8 -0
- 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/privacy.ts +25 -0
- 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/oauth/health.ts +47 -12
- package/src/oauth/index.ts +46 -8
- package/src/oauth/token-guardian.ts +32 -6
- package/src/providers/google-ai-studio-model-discovery.ts +74 -0
- package/src/providers/opencode-go-transport.ts +9 -1
- package/src/providers/opencode-zen-rate-limit.ts +75 -0
- package/src/providers/quota.ts +20 -1
- package/src/providers/registry.ts +35 -6
- package/src/remote/hub-state.ts +182 -0
- package/src/server/auth-cors.ts +11 -0
- package/src/server/chat-completions.ts +10 -7
- package/src/server/chat-native.ts +10 -1
- package/src/server/claude-messages.ts +12 -6
- package/src/server/hub-state.ts +98 -0
- package/src/server/images.ts +2 -2
- package/src/server/index.ts +149 -8
- 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/management/logs-usage-routes.ts +4 -1
- package/src/server/management/model-rows.ts +16 -1
- package/src/server/management/oauth-account-routes.ts +6 -2
- package/src/server/management/provider-routes.ts +9 -2
- package/src/server/management/request-history-routes.ts +4 -2
- package/src/server/management/route-registry.ts +5 -4
- package/src/server/management/shared.ts +66 -3
- package/src/server/management-api.ts +1 -1
- package/src/server/proxy-liveness.ts +7 -1
- package/src/server/request-decompress.ts +91 -3
- package/src/server/request-log-conversation.ts +41 -1
- package/src/server/request-log.ts +10 -0
- 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 +76 -5
- package/src/server/responses/compact.ts +28 -11
- package/src/server/responses/context-overflow.ts +11 -0
- package/src/server/responses/core.ts +201 -48
- package/src/server/responses/policy-fallback.ts +13 -3
- package/src/server/search.ts +2 -2
- package/src/server/system-env-shell.ts +14 -2
- package/src/server/system-env.ts +106 -14
- package/src/service.ts +965 -68
- package/src/types/accounts.ts +18 -0
- package/src/types/config.ts +93 -4
- package/src/types/provider.ts +56 -0
- package/src/types.ts +4 -0
- 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
- package/src/web-search/ollama-executor.ts +127 -0
- package/src/web-search/passthrough-bridge.ts +761 -0
- package/gui/dist/assets/index-BtyONQrZ.js +0 -115
package/src/update/index.ts
CHANGED
|
@@ -3,13 +3,23 @@ import { STOP_HISTORY_INCOMPLETE_EXIT_CODE } from "./stop-contract.mjs";
|
|
|
3
3
|
import { proxyIdentityAt } from "../server/proxy-liveness";
|
|
4
4
|
import { probeProxyLiveness } from "./proxy-liveness-probe.mjs";
|
|
5
5
|
import { decidePostStopUpdate } from "./stop-decision.mjs";
|
|
6
|
-
import { readFileSync, readdirSync } from "node:fs";
|
|
6
|
+
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
|
-
import { dirname, join } from "node:path";
|
|
8
|
+
import { dirname, join, resolve } from "node:path";
|
|
9
9
|
import { getConfigDir, loadConfig } from "../config";
|
|
10
10
|
import { readPid, readRuntimePort } from "../config/process-state";
|
|
11
11
|
import { pendingTeardownOutstanding } from "../config/pending-teardown";
|
|
12
12
|
import { npmInvocation } from "./npm-invocation.mjs";
|
|
13
|
+
import { pnpmInvocation, pnpmInvocationForPath, resolvePnpmCommands } from "./pnpm-invocation.mjs";
|
|
14
|
+
import { detectInstallFromPath } from "./install-detection.mjs";
|
|
15
|
+
import {
|
|
16
|
+
pnpmOwnerInvocation,
|
|
17
|
+
readPnpmGlobalPackage,
|
|
18
|
+
resolvePnpmGlobalOwner,
|
|
19
|
+
runPnpmGlobalUpdate,
|
|
20
|
+
} from "./pnpm-global-install.mjs";
|
|
21
|
+
import type { PnpmGlobalOwner, PnpmGlobalOwnerResult } from "./pnpm-global-install.mjs";
|
|
22
|
+
import { checkRegistryPackageIntegrity } from "./registry-integrity.mjs";
|
|
13
23
|
import {
|
|
14
24
|
npmCachePreflightFailureMessage,
|
|
15
25
|
runNpmCachePreflight,
|
|
@@ -35,13 +45,101 @@ export function historyRestoreIncomplete(configDir = getConfigDir()): boolean {
|
|
|
35
45
|
export const PKG = "@bitkyc08/opencodex";
|
|
36
46
|
const HERE = dirname(fileURLToPath(import.meta.url)); // .../opencodex/src/update
|
|
37
47
|
|
|
38
|
-
export type Installer = "bun" | "npm" | "source";
|
|
48
|
+
export type Installer = "bun" | "npm" | "pnpm" | "source";
|
|
39
49
|
export type Channel = "latest" | "preview";
|
|
40
50
|
|
|
41
51
|
/** Infer how opencodex is installed from the running module's path. */
|
|
42
52
|
export function detectInstall(): Installer {
|
|
43
|
-
|
|
44
|
-
|
|
53
|
+
return detectInstallFromPath(HERE, { exists: existsSync });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function packageRoot(): string {
|
|
57
|
+
return resolve(HERE, "..", "..");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function runningPnpmShimPath(): string | undefined {
|
|
61
|
+
const invoked = process.argv[1];
|
|
62
|
+
if (!invoked) return undefined;
|
|
63
|
+
const name = invoked.replaceAll("\\", "/").split("/").at(-1)?.toLowerCase();
|
|
64
|
+
if (!new Set(["ocx", "opencodex", "ocx.cmd", "opencodex.cmd", "ocx.ps1", "opencodex.ps1"]).has(name ?? "")) {
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
return resolve(invoked);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function runPnpmCandidate(
|
|
71
|
+
commandPath: string,
|
|
72
|
+
args: readonly string[],
|
|
73
|
+
capture = false,
|
|
74
|
+
): { status: number | null; stdout?: string | null; stderr?: string | null } {
|
|
75
|
+
const invocation = pnpmInvocationForPath(commandPath, args);
|
|
76
|
+
if (!invocation) return { status: 1 };
|
|
77
|
+
return spawnSync(invocation.file, invocation.args, {
|
|
78
|
+
stdio: capture ? "pipe" : "ignore",
|
|
79
|
+
encoding: "utf8",
|
|
80
|
+
timeout: 20_000,
|
|
81
|
+
windowsHide: true,
|
|
82
|
+
...invocation.options,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Resolve the exact pnpm executable/group/bin that own this package. */
|
|
87
|
+
export function resolveCurrentPnpmGlobalOwner(): PnpmGlobalOwnerResult {
|
|
88
|
+
return resolvePnpmGlobalOwner({
|
|
89
|
+
packageName: PKG,
|
|
90
|
+
packagePath: packageRoot(),
|
|
91
|
+
commandPaths: resolvePnpmCommands(),
|
|
92
|
+
runningShimPath: runningPnpmShimPath(),
|
|
93
|
+
runPnpm: runPnpmCandidate,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function ownerPnpmTarget(
|
|
98
|
+
owner: PnpmGlobalOwner,
|
|
99
|
+
args: readonly string[],
|
|
100
|
+
): { bin: string; args: string[]; options: { windowsVerbatimArguments?: boolean }; env: Record<string, string | undefined> } | null {
|
|
101
|
+
const invocation = pnpmOwnerInvocation(owner, args);
|
|
102
|
+
if (!invocation) return null;
|
|
103
|
+
return {
|
|
104
|
+
bin: invocation.file,
|
|
105
|
+
args: invocation.args,
|
|
106
|
+
options: invocation.options,
|
|
107
|
+
env: invocation.env,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function runOwnedPnpm(
|
|
112
|
+
owner: PnpmGlobalOwner,
|
|
113
|
+
args: readonly string[],
|
|
114
|
+
capture: boolean,
|
|
115
|
+
stdio: "inherit" | "pipe" | "ignore" = capture ? "pipe" : "inherit",
|
|
116
|
+
): { status: number | null; stdout?: string | null; stderr?: string | null } {
|
|
117
|
+
const target = ownerPnpmTarget(owner, args);
|
|
118
|
+
if (!target) return { status: 1 };
|
|
119
|
+
return spawnSync(target.bin, target.args, {
|
|
120
|
+
stdio,
|
|
121
|
+
encoding: "utf8",
|
|
122
|
+
timeout: 180_000,
|
|
123
|
+
windowsHide: true,
|
|
124
|
+
env: target.env,
|
|
125
|
+
...target.options,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Re-read the owning group's active package and return its verified launcher. */
|
|
130
|
+
export function resolvePnpmActiveLauncher(owner: PnpmGlobalOwner): string | null {
|
|
131
|
+
const active = readPnpmGlobalPackage(
|
|
132
|
+
PKG,
|
|
133
|
+
(args, capture = false) => runOwnedPnpm(owner, args, capture),
|
|
134
|
+
undefined,
|
|
135
|
+
{
|
|
136
|
+
owner,
|
|
137
|
+
expectedGlobalDir: owner.globalDir,
|
|
138
|
+
expectedGlobalRoot: owner.globalRoot,
|
|
139
|
+
globalBinDir: owner.globalBinDir,
|
|
140
|
+
},
|
|
141
|
+
);
|
|
142
|
+
return active.ok ? join(active.path, "bin", "ocx.mjs") : null;
|
|
45
143
|
}
|
|
46
144
|
|
|
47
145
|
export function currentVersion(): string {
|
|
@@ -63,14 +161,57 @@ export function updateTag(current: string): Channel {
|
|
|
63
161
|
return defaultUpdateTag(current);
|
|
64
162
|
}
|
|
65
163
|
|
|
66
|
-
|
|
164
|
+
type SpawnTarget = {
|
|
165
|
+
bin: string;
|
|
166
|
+
args: string[];
|
|
167
|
+
options: { windowsVerbatimArguments?: boolean };
|
|
168
|
+
env?: Record<string, string | undefined>;
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
function npmSpawnTarget(args: readonly string[]): SpawnTarget | null {
|
|
67
172
|
const invocation = npmInvocation(args);
|
|
68
173
|
if (!invocation) return null;
|
|
69
174
|
return { bin: invocation.file, args: invocation.args, options: invocation.options };
|
|
70
175
|
}
|
|
71
176
|
|
|
72
|
-
function
|
|
177
|
+
function pnpmSpawnTarget(args: readonly string[], owner?: PnpmGlobalOwner): SpawnTarget | null {
|
|
178
|
+
if (owner) {
|
|
179
|
+
const invocation = pnpmOwnerInvocation(owner, args);
|
|
180
|
+
if (!invocation) return null;
|
|
181
|
+
return {
|
|
182
|
+
bin: invocation.file,
|
|
183
|
+
args: invocation.args,
|
|
184
|
+
options: invocation.options,
|
|
185
|
+
env: invocation.env,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
const invocation = pnpmInvocation(args);
|
|
189
|
+
if (!invocation) return null;
|
|
190
|
+
return { bin: invocation.file, args: invocation.args, options: invocation.options };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function registrySpawnTarget(
|
|
194
|
+
installer: Installer,
|
|
195
|
+
args: readonly string[],
|
|
196
|
+
owner?: PnpmGlobalOwner,
|
|
197
|
+
): SpawnTarget | null {
|
|
198
|
+
// A pnpm command without an owner would silently fall back to the first PATH
|
|
199
|
+
// candidate. That is unsafe when multiple PNPM_HOME installations expose the
|
|
200
|
+
// same version, so registry queries use the same hard binding as mutation.
|
|
201
|
+
return installer === "pnpm"
|
|
202
|
+
? owner ? pnpmSpawnTarget(args, owner) : null
|
|
203
|
+
: npmSpawnTarget(args);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function selectedPnpmOwner(owner?: PnpmGlobalOwner): PnpmGlobalOwner | undefined {
|
|
207
|
+
if (owner) return owner;
|
|
208
|
+
const result = resolveCurrentPnpmGlobalOwner();
|
|
209
|
+
return result.ok ? result.owner : undefined;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function updateSpawnTarget(bin: string, args: readonly string[]): SpawnTarget | null {
|
|
73
213
|
if (bin === "npm") return npmSpawnTarget(args);
|
|
214
|
+
if (bin === "pnpm") return pnpmSpawnTarget(args);
|
|
74
215
|
if (process.platform === "win32" && bin === "bun") {
|
|
75
216
|
return { bin: process.execPath, args: [...args], options: {} };
|
|
76
217
|
}
|
|
@@ -95,28 +236,46 @@ function logSpawnOutput(label: string, result: { stdout?: string | Buffer | null
|
|
|
95
236
|
if (stderr) console.error(stderr.length > 4000 ? `${label}${stderr.slice(-4000)}` : stderr);
|
|
96
237
|
}
|
|
97
238
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
239
|
+
function shellQuote(value: string): string {
|
|
240
|
+
if (process.platform === "win32") return `"${value.replaceAll("\"", "\\\"")}"`;
|
|
241
|
+
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function launcherStartHint(launcher: string, port: number): string {
|
|
245
|
+
return `${shellQuote(process.execPath)} ${shellQuote(launcher)} start --port ${Math.trunc(port)}`;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** Latest published version from the registry (best-effort; null if the manager isn't available). */
|
|
249
|
+
export function latestVersion(
|
|
250
|
+
tag: string,
|
|
251
|
+
installer: Installer = detectInstall(),
|
|
252
|
+
owner?: PnpmGlobalOwner,
|
|
253
|
+
): string | null {
|
|
254
|
+
const resolvedOwner = installer === "pnpm" ? selectedPnpmOwner(owner) : undefined;
|
|
255
|
+
if (installer === "pnpm" && !resolvedOwner) return null;
|
|
256
|
+
const manager = registrySpawnTarget(installer, ["view", `${PKG}@${tag}`, "version"], resolvedOwner);
|
|
257
|
+
if (!manager) return null;
|
|
258
|
+
const r = spawnSync(manager.bin, manager.args, {
|
|
103
259
|
encoding: "utf8",
|
|
104
260
|
timeout: 12000,
|
|
105
261
|
windowsHide: true,
|
|
106
|
-
...
|
|
262
|
+
...(manager.env ? { env: manager.env } : {}),
|
|
263
|
+
...manager.options,
|
|
107
264
|
});
|
|
108
|
-
return r.status === 0 ? (r.stdout.trim() || null) : null;
|
|
265
|
+
return r.status === 0 && typeof r.stdout === "string" ? (r.stdout.trim() || null) : null;
|
|
109
266
|
}
|
|
110
267
|
|
|
111
268
|
/** The global-install command opencodex would run to update on this channel. */
|
|
112
269
|
export function updateCommand(installer: Installer, tag: Channel, resolvedVersion?: string | null): { bin: string; args: string[] } {
|
|
113
|
-
const bin = installer === "bun" ? "bun" : "npm";
|
|
114
270
|
// Immutable target: when the registry resolved a concrete version, install exactly
|
|
115
271
|
// that version — the dist-tag can move between resolution and install (TOCTOU).
|
|
116
272
|
const target = resolvedVersion || tag;
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
: ["
|
|
273
|
+
if (installer === "bun") return { bin: "bun", args: ["add", "-g", `${PKG}@${target}`] };
|
|
274
|
+
if (installer === "pnpm") {
|
|
275
|
+
return { bin: "pnpm", args: ["add", "-g", "--allow-build=bun", `${PKG}@${target}`] };
|
|
276
|
+
}
|
|
277
|
+
const bin = "npm";
|
|
278
|
+
const args = ["install", "-g", `${PKG}@${target}`];
|
|
120
279
|
return { bin, args };
|
|
121
280
|
}
|
|
122
281
|
|
|
@@ -138,26 +297,33 @@ export function updateCommandStr(installer: Installer, tag: Channel, resolvedVer
|
|
|
138
297
|
export function checkUpdatePackageIntegrity(
|
|
139
298
|
version: string | null,
|
|
140
299
|
spawn: typeof spawnSync = spawnSync,
|
|
300
|
+
installer: Installer = detectInstall(),
|
|
301
|
+
owner?: PnpmGlobalOwner,
|
|
141
302
|
): { ok: true; integrity: string } | { ok: false; reason: string } | { ok: "skipped"; reason: string } {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
303
|
+
const resolvedOwner = installer === "pnpm" ? selectedPnpmOwner(owner) : undefined;
|
|
304
|
+
if (installer === "pnpm" && !resolvedOwner) {
|
|
305
|
+
return { ok: false, reason: "could not identify pnpm's owning global installation" };
|
|
306
|
+
}
|
|
307
|
+
const manager = registrySpawnTarget(installer, ["view", `${PKG}@${version}`, "dist.integrity"], resolvedOwner);
|
|
308
|
+
if (!manager) return { ok: "skipped", reason: `${installer} executable was not found on a trusted PATH entry` };
|
|
309
|
+
const result = checkRegistryPackageIntegrity(PKG, version, args => {
|
|
310
|
+
const target = registrySpawnTarget(installer, args, resolvedOwner);
|
|
311
|
+
if (!target) return { status: 1 };
|
|
312
|
+
return spawn(target.bin, target.args, {
|
|
313
|
+
encoding: "utf8",
|
|
314
|
+
timeout: 12000,
|
|
315
|
+
windowsHide: true,
|
|
316
|
+
...(target.env ? { env: target.env } : {}),
|
|
317
|
+
...target.options,
|
|
318
|
+
});
|
|
319
|
+
});
|
|
320
|
+
return result;
|
|
156
321
|
}
|
|
157
322
|
|
|
158
323
|
/**
|
|
159
|
-
* `ocx update` fallback for source checkouts and Bun global installs. npm global installs
|
|
160
|
-
* in the Node bin launcher before Bun starts, so Windows does not replace the running
|
|
324
|
+
* `ocx update` fallback for source checkouts and Bun global installs. npm and pnpm global installs
|
|
325
|
+
* are updated in the Node bin launcher before Bun starts, so Windows does not replace the running
|
|
326
|
+
* Bun binary.
|
|
161
327
|
*/
|
|
162
328
|
export async function runUpdate(): Promise<void> {
|
|
163
329
|
const installer = detectInstall();
|
|
@@ -170,7 +336,13 @@ export async function runUpdate(): Promise<void> {
|
|
|
170
336
|
return;
|
|
171
337
|
}
|
|
172
338
|
|
|
173
|
-
const
|
|
339
|
+
const ownerResult = installer === "pnpm" ? resolveCurrentPnpmGlobalOwner() : undefined;
|
|
340
|
+
if (installer === "pnpm" && (!ownerResult || !ownerResult.ok)) {
|
|
341
|
+
console.error(`⚠️ ${ownerResult?.reason ?? "Could not identify pnpm's owning global installation"}. Aborting before stopping the proxy.`);
|
|
342
|
+
process.exit(1);
|
|
343
|
+
}
|
|
344
|
+
const owner = ownerResult?.ok ? ownerResult.owner : undefined;
|
|
345
|
+
const latest = latestVersion(tag, installer, owner);
|
|
174
346
|
if (latest && latest === current) {
|
|
175
347
|
console.log(`Already on the latest ${tag} version (v${latest}).`);
|
|
176
348
|
return;
|
|
@@ -178,7 +350,7 @@ export async function runUpdate(): Promise<void> {
|
|
|
178
350
|
|
|
179
351
|
// Pre-flight integrity metadata check — runs BEFORE the proxy is stopped so an
|
|
180
352
|
// anomalous registry entry aborts without unloading the running service.
|
|
181
|
-
const integrity = checkUpdatePackageIntegrity(latest);
|
|
353
|
+
const integrity = checkUpdatePackageIntegrity(latest, spawnSync, installer, owner);
|
|
182
354
|
if (integrity.ok === false) {
|
|
183
355
|
console.error(`⚠️ ${integrity.reason} — aborting the update before stopping the proxy.`);
|
|
184
356
|
process.exit(1);
|
|
@@ -198,9 +370,11 @@ export async function runUpdate(): Promise<void> {
|
|
|
198
370
|
}
|
|
199
371
|
|
|
200
372
|
const { bin, args: cmdArgs } = updateCommand(installer, tag, latest);
|
|
201
|
-
const target =
|
|
373
|
+
const target = installer === "pnpm" && owner
|
|
374
|
+
? pnpmSpawnTarget(cmdArgs, owner)
|
|
375
|
+
: updateSpawnTarget(bin, cmdArgs);
|
|
202
376
|
if (!target) {
|
|
203
|
-
console.error(
|
|
377
|
+
console.error(`⚠️ Could not resolve ${bin} from a trusted absolute PATH entry; aborting before stopping the proxy.`);
|
|
204
378
|
process.exit(1);
|
|
205
379
|
}
|
|
206
380
|
|
|
@@ -257,7 +431,9 @@ export async function runUpdate(): Promise<void> {
|
|
|
257
431
|
// shared client config still points at a proxy that is gone; installing over that
|
|
258
432
|
// silently skips the recovery the receipt was written to trigger (#3008).
|
|
259
433
|
// Full `ocx stop` semantics (drain, service stop, restore).
|
|
434
|
+
let stopAttempted = false;
|
|
260
435
|
if (serviceWasInstalled || readPid() || readRuntimePort() || pendingTeardownOutstanding()) {
|
|
436
|
+
stopAttempted = true;
|
|
261
437
|
console.log("⏹ Stopping the running proxy before updating...");
|
|
262
438
|
const stopStdio = updateChildStdio();
|
|
263
439
|
const stop = spawnSync(process.execPath, selfLaunchArgv(["stop"]), {
|
|
@@ -266,7 +442,7 @@ export async function runUpdate(): Promise<void> {
|
|
|
266
442
|
windowsHide: true,
|
|
267
443
|
});
|
|
268
444
|
if (stopStdio === "pipe") logSpawnOutput("", stop);
|
|
269
|
-
// One decision, shared with the
|
|
445
|
+
// One decision, shared with the package launcher (#3008). The two lanes disagreeing about
|
|
270
446
|
// the same situation is how this shipped fixed on one side only. Absent PID and runtime
|
|
271
447
|
// files are weak evidence - a crashed-but-listening proxy leaves none - so the captured
|
|
272
448
|
// endpoint is asked, and `null` from proxyIdentityAt covers refusal AND timeout alike.
|
|
@@ -310,35 +486,96 @@ export async function runUpdate(): Promise<void> {
|
|
|
310
486
|
console.log(`Updating${latest ? ` to v${latest}` : ""}…\n$ ${bin} ${cmdArgs.join(" ")}`);
|
|
311
487
|
|
|
312
488
|
const installStdio = updateChildStdio();
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
489
|
+
// Every post-update action below receives this path. For pnpm it is replaced only
|
|
490
|
+
// by a path returned after tree+shim verification; on rollback, activePath is
|
|
491
|
+
// likewise returned only after the old group has been verified again.
|
|
492
|
+
let postUpdateLauncher = join(packageRoot(), "bin", "ocx.mjs");
|
|
493
|
+
// The pnpm owner preflight has verified this package tree and global group. Keep that exact
|
|
494
|
+
// package path as the recovery starting point; the path returned by the update transaction
|
|
495
|
+
// replaces it only after post-update tree+shim verification succeeds.
|
|
496
|
+
if (installer === "pnpm" && owner) {
|
|
497
|
+
postUpdateLauncher = join(owner.packagePath, "bin", "ocx.mjs");
|
|
498
|
+
}
|
|
499
|
+
let postUpdateLauncherUsable = true;
|
|
500
|
+
let r: {
|
|
501
|
+
status: number | null;
|
|
502
|
+
signal?: NodeJS.Signals | null;
|
|
503
|
+
stdout?: string | Buffer | null;
|
|
504
|
+
stderr?: string | Buffer | null;
|
|
505
|
+
};
|
|
506
|
+
if (installer === "pnpm") {
|
|
507
|
+
let update: ReturnType<typeof runPnpmGlobalUpdate>;
|
|
508
|
+
try {
|
|
509
|
+
update = runPnpmGlobalUpdate({
|
|
510
|
+
packageName: PKG,
|
|
511
|
+
currentVersion: current,
|
|
512
|
+
targetVersion: latest || undefined,
|
|
513
|
+
tag,
|
|
514
|
+
owner: owner!,
|
|
515
|
+
runningPackagePath: packageRoot(),
|
|
516
|
+
runPnpm: (args, capture = false) => runOwnedPnpm(
|
|
517
|
+
owner!,
|
|
518
|
+
args,
|
|
519
|
+
capture,
|
|
520
|
+
capture ? "pipe" : installStdio,
|
|
521
|
+
),
|
|
522
|
+
log: line => console.log(line),
|
|
523
|
+
});
|
|
524
|
+
} catch {
|
|
525
|
+
// A thrown verifier/runner error means the active group is unknown. Mark the
|
|
526
|
+
// launcher unusable and let the failure lane report a manual recovery path.
|
|
527
|
+
update = {
|
|
528
|
+
ok: false,
|
|
529
|
+
phase: "rollback",
|
|
530
|
+
rolledBack: false,
|
|
531
|
+
error: "pnpm update failed unexpectedly; active package could not be verified",
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
if (update.ok) {
|
|
535
|
+
postUpdateLauncher = join(update.path, "bin", "ocx.mjs");
|
|
536
|
+
postUpdateLauncherUsable = true;
|
|
537
|
+
r = { status: 0, signal: null, stdout: "", stderr: "" };
|
|
538
|
+
} else {
|
|
539
|
+
postUpdateLauncherUsable = Boolean(update.activePath);
|
|
540
|
+
if (update.activePath) postUpdateLauncher = join(update.activePath, "bin", "ocx.mjs");
|
|
541
|
+
console.error(`⚠️ ${update.error}${update.rolledBack ? "." : " Manual recovery may be required."}`);
|
|
542
|
+
r = { status: 1, signal: null, stdout: "", stderr: "" };
|
|
543
|
+
}
|
|
544
|
+
} else {
|
|
545
|
+
r = spawnSync(target.bin, target.args, {
|
|
546
|
+
stdio: installStdio,
|
|
547
|
+
encoding: installStdio === "pipe" ? "utf8" : undefined,
|
|
548
|
+
timeout: 180000,
|
|
549
|
+
windowsHide: true,
|
|
550
|
+
...target.options,
|
|
551
|
+
});
|
|
552
|
+
}
|
|
320
553
|
if (installStdio === "pipe") logSpawnOutput("", r);
|
|
321
554
|
if (r.status === 0) {
|
|
322
555
|
console.log(`\n✅ Updated${latest ? ` to v${latest}` : ""}.`);
|
|
323
|
-
// Re-
|
|
324
|
-
//
|
|
556
|
+
// Re-enter through the verified active package launcher. This keeps the Codex
|
|
557
|
+
// shim, tray, service and proxy recovery paths on the same package/group that
|
|
558
|
+
// pnpm selected, including when the update changed the global link target.
|
|
325
559
|
try {
|
|
326
|
-
const { isCodexShimInstalled
|
|
560
|
+
const { isCodexShimInstalled } = await import("../codex/shim");
|
|
327
561
|
if (isCodexShimInstalled()) {
|
|
328
|
-
const
|
|
329
|
-
|
|
562
|
+
const shim = spawnSync(process.execPath, [postUpdateLauncher, "codex-shim", "install"], {
|
|
563
|
+
stdio: "inherit",
|
|
564
|
+
windowsHide: true,
|
|
565
|
+
});
|
|
566
|
+
if (shim.status !== 0) console.warn("⚠️ Shim repair skipped: run 'ocx codex-shim install'.");
|
|
330
567
|
}
|
|
331
|
-
} catch
|
|
332
|
-
console.warn(
|
|
568
|
+
} catch {
|
|
569
|
+
console.warn("⚠️ Shim repair skipped; run 'ocx codex-shim install'.");
|
|
333
570
|
}
|
|
334
571
|
if (trayWasInstalled) {
|
|
335
|
-
const trayArgs =
|
|
336
|
-
const tray = spawnSync(process.execPath, trayArgs, { stdio: "inherit", windowsHide: true });
|
|
572
|
+
const trayArgs = planWindowsTrayUpdate({ installed: trayWasInstalled, running: trayWasRunning }).installArgs;
|
|
573
|
+
const tray = spawnSync(process.execPath, [postUpdateLauncher, ...trayArgs], { stdio: "inherit", windowsHide: true });
|
|
337
574
|
if (tray.status === 0) {
|
|
338
575
|
console.log("🔧 Refreshed Windows tray startup paths.");
|
|
339
576
|
} else {
|
|
340
577
|
console.warn("⚠️ Windows tray refresh failed. Run 'ocx tray install'.");
|
|
341
|
-
if (trayWasRunning) spawnSync(process.execPath,
|
|
578
|
+
if (trayWasRunning) spawnSync(process.execPath, [postUpdateLauncher, "tray", "start"], { stdio: "ignore", windowsHide: true });
|
|
342
579
|
}
|
|
343
580
|
}
|
|
344
581
|
// The stop above unloaded any managed service; repair it with the NEW files
|
|
@@ -362,7 +599,7 @@ export async function runUpdate(): Promise<void> {
|
|
|
362
599
|
process.env.OCX_BAKE_PORT = String(capturedListen.port);
|
|
363
600
|
try {
|
|
364
601
|
const svcStdio = updateChildStdio();
|
|
365
|
-
const svc = spawnSync(process.execPath,
|
|
602
|
+
const svc = spawnSync(process.execPath, [postUpdateLauncher, ...serviceReinstallArgs()], {
|
|
366
603
|
stdio: svcStdio,
|
|
367
604
|
encoding: svcStdio === "pipe" ? "utf8" : undefined,
|
|
368
605
|
windowsHide: true,
|
|
@@ -407,7 +644,7 @@ export async function runUpdate(): Promise<void> {
|
|
|
407
644
|
: " Run 'ocx service repair' to refresh the background service and see why it failed.");
|
|
408
645
|
const env = { ...process.env };
|
|
409
646
|
delete env.OCX_SERVICE;
|
|
410
|
-
const child = spawn(process.execPath,
|
|
647
|
+
const child = spawn(process.execPath, [postUpdateLauncher, "start", "--port", String(capturedListen.port)], {
|
|
411
648
|
detached: true,
|
|
412
649
|
stdio: "ignore",
|
|
413
650
|
windowsHide: true,
|
|
@@ -422,14 +659,30 @@ export async function runUpdate(): Promise<void> {
|
|
|
422
659
|
else process.env.OCX_BAKE_PORT = prevBake;
|
|
423
660
|
}
|
|
424
661
|
} else {
|
|
425
|
-
console.log(`Restart the proxy:
|
|
662
|
+
console.log(`Restart the proxy: ${launcherStartHint(postUpdateLauncher, capturedListen.port)}`);
|
|
426
663
|
}
|
|
427
664
|
} else {
|
|
428
|
-
if (trayWasRunning) {
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
665
|
+
if (stopAttempted && trayWasRunning && postUpdateLauncherUsable) {
|
|
666
|
+
spawnSync(process.execPath, [postUpdateLauncher, "tray", "start"], { stdio: "ignore", windowsHide: true });
|
|
667
|
+
}
|
|
668
|
+
if (stopAttempted && serviceWasInstalled && postUpdateLauncherUsable) {
|
|
669
|
+
const service = spawnSync(process.execPath, [postUpdateLauncher, "service", "repair"], {
|
|
670
|
+
stdio: "inherit",
|
|
671
|
+
windowsHide: true,
|
|
672
|
+
});
|
|
673
|
+
if (service.status !== 0) console.warn("⚠️ Previous background service could not be restored; run 'ocx service repair'.");
|
|
674
|
+
} else if (stopAttempted && postUpdateLauncherUsable) {
|
|
675
|
+
const env = { ...process.env };
|
|
676
|
+
delete env.OCX_SERVICE;
|
|
677
|
+
const child = spawn(process.execPath, [postUpdateLauncher, "start", "--port", String(capturedListen.port)], {
|
|
678
|
+
detached: true,
|
|
679
|
+
stdio: "ignore",
|
|
680
|
+
windowsHide: true,
|
|
681
|
+
env: withProcessRuntimeProvenance(env),
|
|
682
|
+
});
|
|
683
|
+
child.unref();
|
|
684
|
+
} else if (stopAttempted) {
|
|
685
|
+
console.error("opencodex: no verified active launcher remains for automatic recovery; reinstall opencodex manually.");
|
|
433
686
|
}
|
|
434
687
|
console.error(`\n⚠️ Update failed (${bin} exit ${r.status ?? "?"}). Try manually: ${bin} ${cmdArgs.join(" ")}`);
|
|
435
688
|
process.exit(1);
|
|
@@ -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
|
+
}
|