@bitkyc08/opencodex 2.50.0 → 2.52.0-preview.20260911
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-Dx0xv2EA.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,591 @@
|
|
|
1
|
+
import { existsSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
2
|
+
import { basename, dirname, isAbsolute, join, relative, resolve, win32 } from "node:path";
|
|
3
|
+
import { pnpmInvocationForPath } from "./pnpm-invocation.mjs";
|
|
4
|
+
import { verifyPnpmInstallTree } from "./transactional-install.mjs";
|
|
5
|
+
|
|
6
|
+
export const PNPM_BUILD_APPROVAL = "--allow-build=bun";
|
|
7
|
+
|
|
8
|
+
function outputText(value) {
|
|
9
|
+
if (typeof value === "string") return value;
|
|
10
|
+
if (value instanceof Uint8Array) return new TextDecoder().decode(value);
|
|
11
|
+
return "";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function pathKey(value, platform = process.platform) {
|
|
15
|
+
const raw = String(value).replaceAll("\\", "/");
|
|
16
|
+
const normalise = path => {
|
|
17
|
+
const result = path.replaceAll("\\", "/");
|
|
18
|
+
return platform === "win32" ? result.toLowerCase() : result;
|
|
19
|
+
};
|
|
20
|
+
try {
|
|
21
|
+
return normalise(realpathSync.native(raw));
|
|
22
|
+
} catch {
|
|
23
|
+
return normalise(resolve(raw));
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function lexicalPathKey(value, platform = process.platform) {
|
|
28
|
+
const result = resolve(String(value).replaceAll("\\", "/")).replaceAll("\\", "/");
|
|
29
|
+
return platform === "win32" ? result.toLowerCase() : result;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function samePath(left, right, platform = process.platform) {
|
|
33
|
+
return pathKey(left, platform) === pathKey(right, platform)
|
|
34
|
+
|| lexicalPathKey(left, platform) === lexicalPathKey(right, platform);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function singleAbsoluteCommandPath(runPnpm, commandPath, args, platform = process.platform) {
|
|
38
|
+
let result;
|
|
39
|
+
try {
|
|
40
|
+
result = runPnpm(commandPath, args, true);
|
|
41
|
+
} catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
if (result?.status !== 0) return null;
|
|
45
|
+
const lines = outputText(result.stdout).split(/\r?\n/).map(line => line.trim()).filter(Boolean);
|
|
46
|
+
if (lines.length !== 1 || /^(?:undefined|null)$/i.test(lines[0])) return null;
|
|
47
|
+
return isAbsolutePath(lines[0], platform) ? lines[0] : null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function configPathValue(runPnpm, commandPath, key, platform = process.platform) {
|
|
51
|
+
return singleAbsoluteCommandPath(runPnpm, commandPath, ["config", "get", key], platform);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function absoluteConfigPath(value, platform = process.platform) {
|
|
55
|
+
return typeof value === "string" && isAbsolutePath(value, platform) ? value : null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function isAbsolutePath(value, platform = process.platform) {
|
|
59
|
+
if (typeof value !== "string") return false;
|
|
60
|
+
return isAbsolute(value) || (
|
|
61
|
+
platform === "win32"
|
|
62
|
+
&& /^(?:[A-Za-z]:[\\/]|\\\\|\/)/.test(String(value))
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function pathDirname(value, platform = process.platform) {
|
|
67
|
+
return platform === "win32" ? win32.dirname(String(value)) : dirname(value);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function pathBasename(value, platform = process.platform) {
|
|
71
|
+
return platform === "win32" ? win32.basename(String(value)) : basename(value);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Normalize pnpm list/root output to the versioned global group, not its node_modules root. */
|
|
75
|
+
function normaliseGlobalRoot(value, platform = process.platform) {
|
|
76
|
+
if (!isAbsolutePath(value, platform)) return null;
|
|
77
|
+
const trimmed = String(value).replace(/[\\/]+$/, "");
|
|
78
|
+
return pathBasename(trimmed, platform).toLowerCase() === "node_modules"
|
|
79
|
+
? pathDirname(trimmed, platform)
|
|
80
|
+
: trimmed;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** pnpm's --global-dir is the base; pnpm appends the major-version group below it. */
|
|
84
|
+
function globalDirFromRoot(globalRoot, platform = process.platform) {
|
|
85
|
+
const separator = platform === "win32" ? /[\\/]/ : /\//;
|
|
86
|
+
const match = String(globalRoot).match(new RegExp(`^(.*)${separator.source}(?:v)?\\d+$`, "i"));
|
|
87
|
+
return match?.[1] || globalRoot;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function globalRootMatchesDir(globalRoot, globalDir, platform = process.platform) {
|
|
91
|
+
return samePath(globalRoot, globalDir, platform)
|
|
92
|
+
|| samePath(pathDirname(globalRoot, platform), globalDir, platform);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Resolve manager-owned global paths, including pnpm defaults that config get leaves undefined. */
|
|
96
|
+
function resolveGlobalPaths(commandPath, runPnpm, listedRoot, platform = process.platform) {
|
|
97
|
+
const listedGlobalRoot = normaliseGlobalRoot(listedRoot, platform);
|
|
98
|
+
const commandGlobalRoot = normaliseGlobalRoot(
|
|
99
|
+
singleAbsoluteCommandPath(runPnpm, commandPath, ["root", "-g"], platform),
|
|
100
|
+
platform,
|
|
101
|
+
);
|
|
102
|
+
if (listedGlobalRoot && commandGlobalRoot && !samePath(listedGlobalRoot, commandGlobalRoot, platform)) {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
const globalRoot = commandGlobalRoot ?? listedGlobalRoot;
|
|
106
|
+
if (!globalRoot) return null;
|
|
107
|
+
|
|
108
|
+
const configuredGlobalDir = absoluteConfigPath(configPathValue(runPnpm, commandPath, "global-dir", platform), platform);
|
|
109
|
+
const globalDir = configuredGlobalDir ?? globalDirFromRoot(globalRoot, platform);
|
|
110
|
+
const configuredGlobalBinDir = absoluteConfigPath(configPathValue(runPnpm, commandPath, "global-bin-dir", platform), platform);
|
|
111
|
+
const globalBinDir = configuredGlobalBinDir
|
|
112
|
+
?? absoluteConfigPath(singleAbsoluteCommandPath(runPnpm, commandPath, ["bin", "-g"], platform), platform);
|
|
113
|
+
if (!globalDir || !globalBinDir || !globalRootMatchesDir(globalRoot, globalDir, platform)) return null;
|
|
114
|
+
return { globalDir, globalRoot, globalBinDir };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function packageEntryFromRoot(root, packageName) {
|
|
118
|
+
const maps = [root?.dependencies, root?.devDependencies, root?.optionalDependencies];
|
|
119
|
+
for (const dependencies of maps) {
|
|
120
|
+
if (!dependencies || typeof dependencies !== "object") continue;
|
|
121
|
+
const direct = dependencies[packageName];
|
|
122
|
+
if (direct && typeof direct === "object") return direct;
|
|
123
|
+
}
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function inspectListOutput(stdout, packageName) {
|
|
128
|
+
let roots;
|
|
129
|
+
try {
|
|
130
|
+
roots = JSON.parse(outputText(stdout));
|
|
131
|
+
} catch {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
if (!Array.isArray(roots)) return null;
|
|
135
|
+
for (const root of roots) {
|
|
136
|
+
const entry = packageEntryFromRoot(root, packageName);
|
|
137
|
+
if (entry) return { root, entry };
|
|
138
|
+
}
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function ownerGlobalArgs(args, owner) {
|
|
143
|
+
const input = [...args];
|
|
144
|
+
if (!owner) return input;
|
|
145
|
+
const command = input[0];
|
|
146
|
+
if (!["add", "install", "update", "list", "remove", "uninstall"].includes(command)) return input;
|
|
147
|
+
const rest = [];
|
|
148
|
+
for (let index = 1; index < input.length; index += 1) {
|
|
149
|
+
const arg = input[index];
|
|
150
|
+
// The owner is authoritative. Remove an accidentally inherited/supplied value rather
|
|
151
|
+
// than relying on duplicate pnpm flags having stable precedence across pnpm 10/11.
|
|
152
|
+
if (arg === "--global-dir" || arg.startsWith("--global-dir=")) {
|
|
153
|
+
if (arg === "--global-dir") index += 1;
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
if (arg === "--config.global-bin-dir" || arg.startsWith("--config.global-bin-dir=")) {
|
|
157
|
+
if (arg === "--config.global-bin-dir") index += 1;
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
rest.push(arg);
|
|
161
|
+
}
|
|
162
|
+
return [
|
|
163
|
+
command,
|
|
164
|
+
`--global-dir=${owner.globalDir}`,
|
|
165
|
+
`--config.global-bin-dir=${owner.globalBinDir}`,
|
|
166
|
+
...rest,
|
|
167
|
+
];
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Add the selected pnpm global group and bin directory to a command's config. */
|
|
171
|
+
export function pnpmGlobalCommandArgs(args, owner) {
|
|
172
|
+
return ownerGlobalArgs(args, owner);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Add the owning global bin directory to PATH for commands such as pnpm's bin
|
|
177
|
+
* validation. Do not replace PATH: registry auth and the selected pnpm executable
|
|
178
|
+
* can depend on the rest of the inherited environment.
|
|
179
|
+
*/
|
|
180
|
+
export function pnpmOwnerEnvironment(owner, env = process.env, platform = process.platform) {
|
|
181
|
+
const key = platform === "win32" && env.Path !== undefined && env.PATH === undefined ? "Path" : "PATH";
|
|
182
|
+
const delimiter = platform === "win32" ? ";" : ":";
|
|
183
|
+
const existing = env[key] ?? env.PATH ?? env.Path ?? "";
|
|
184
|
+
const entries = String(existing).split(delimiter).filter(Boolean);
|
|
185
|
+
if (!entries.some(entry => samePath(entry, owner.globalBinDir, platform))) entries.unshift(owner.globalBinDir);
|
|
186
|
+
return { ...env, [key]: entries.join(delimiter) };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** Build an invocation for the already-selected pnpm executable and global group. */
|
|
190
|
+
export function pnpmOwnerInvocation(owner, args, platform = process.platform, env = process.env) {
|
|
191
|
+
const ownerEnv = pnpmOwnerEnvironment(owner, env, platform);
|
|
192
|
+
const invocation = pnpmInvocationForPath(
|
|
193
|
+
owner.commandPath,
|
|
194
|
+
ownerGlobalArgs(args, owner),
|
|
195
|
+
platform,
|
|
196
|
+
ownerEnv,
|
|
197
|
+
);
|
|
198
|
+
return invocation ? { ...invocation, env: ownerEnv } : null;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function shimNames(platform) {
|
|
202
|
+
return platform === "win32"
|
|
203
|
+
? ["ocx.cmd", "ocx.ps1", "opencodex.cmd", "opencodex.ps1"]
|
|
204
|
+
: ["ocx", "opencodex"];
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function targetVariants(packageDir, globalBinDir, platform) {
|
|
208
|
+
const packageCandidates = [packageDir];
|
|
209
|
+
try { packageCandidates.push(realpathSync(packageDir)); } catch { /* keep lexical path */ }
|
|
210
|
+
const binCandidates = [globalBinDir];
|
|
211
|
+
try { binCandidates.push(realpathSync(globalBinDir)); } catch { /* keep lexical path */ }
|
|
212
|
+
const launcherCandidates = packageCandidates.map(candidate => join(candidate, "bin", "ocx.mjs"));
|
|
213
|
+
const relativeCandidates = binCandidates.flatMap(bin => launcherCandidates.map(candidate => relative(bin, candidate)));
|
|
214
|
+
return [...new Set([...launcherCandidates, ...relativeCandidates].map(value => {
|
|
215
|
+
const normalised = String(value).replaceAll("\\", "/").replace(/^\.\//, "");
|
|
216
|
+
return platform === "win32" ? normalised.toLowerCase() : normalised;
|
|
217
|
+
}))].filter(Boolean);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function shimPointsToPackage(shimPath, packageDir, globalBinDir, platform) {
|
|
221
|
+
try {
|
|
222
|
+
if (samePath(shimPath, join(packageDir, "bin", "ocx.mjs"), platform)) return true;
|
|
223
|
+
} catch { /* fall through to text inspection */ }
|
|
224
|
+
let text;
|
|
225
|
+
try {
|
|
226
|
+
text = readFileSync(shimPath, "utf8").replaceAll("\\", "/");
|
|
227
|
+
if (platform === "win32") text = text.toLowerCase();
|
|
228
|
+
} catch {
|
|
229
|
+
return false;
|
|
230
|
+
}
|
|
231
|
+
// A comment containing the new path is not a launcher. Generated cmd/PowerShell
|
|
232
|
+
// shims are simple enough that the target appears on an executable line; discard
|
|
233
|
+
// shebang/hash and REM lines before matching so a stale or hand-edited shim cannot
|
|
234
|
+
// pass verification by mentioning the right package in a comment.
|
|
235
|
+
const executableText = text.split(/\r?\n/).filter(line => {
|
|
236
|
+
const trimmed = line.trim();
|
|
237
|
+
return !trimmed.startsWith("#") && !/^rem(?:\s|$)/i.test(trimmed);
|
|
238
|
+
}).join("\n");
|
|
239
|
+
const expectedLauncher = join(packageDir, "bin", "ocx.mjs");
|
|
240
|
+
if (shimTargetPaths(shimPath, executableText, platform).some(target => samePath(target, expectedLauncher, platform))) {
|
|
241
|
+
return true;
|
|
242
|
+
}
|
|
243
|
+
return targetVariants(packageDir, globalBinDir, platform).some(target => executableText.includes(target));
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function shimIsRunnable(shimPath, platform) {
|
|
247
|
+
if (platform === "win32") return true;
|
|
248
|
+
try {
|
|
249
|
+
return (statSync(shimPath).mode & 0o111) !== 0;
|
|
250
|
+
} catch {
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function shimTargetPaths(shimPath, text, platform) {
|
|
256
|
+
const targets = [];
|
|
257
|
+
const launcherPattern = /((?:\$basedir(?:_win)?|\$PSScriptRoot|%~dp0|[A-Za-z]:[\\/]|\/|\.\.?[\\/])[^"'`\r\n]*[\\/]bin[\\/]ocx\.mjs)/gi;
|
|
258
|
+
const shimDir = pathDirname(shimPath, platform);
|
|
259
|
+
for (const match of text.matchAll(launcherPattern)) {
|
|
260
|
+
let target = match[1];
|
|
261
|
+
target = target.replace(/\$basedir_win|\$basedir|\$PSScriptRoot|%~dp0/gi, shimDir);
|
|
262
|
+
target = target.replaceAll("\\", "/");
|
|
263
|
+
targets.push(isAbsolutePath(target, platform)
|
|
264
|
+
? target
|
|
265
|
+
: platform === "win32" ? win32.resolve(shimDir, target) : resolve(shimDir, target));
|
|
266
|
+
}
|
|
267
|
+
return targets;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Verify both generated command names and all launcher forms pnpm supports for
|
|
272
|
+
* the current platform. A package list/tree check is not enough: a stale shim
|
|
273
|
+
* can still execute the old global group after pnpm changes the active link.
|
|
274
|
+
*/
|
|
275
|
+
export function verifyPnpmGlobalShims(
|
|
276
|
+
packageDir,
|
|
277
|
+
globalBinDir,
|
|
278
|
+
platform = process.platform,
|
|
279
|
+
exists = existsSync,
|
|
280
|
+
) {
|
|
281
|
+
if (!isAbsolutePath(globalBinDir, platform)) {
|
|
282
|
+
return { ok: false, reason: "pnpm global bin directory is not absolute" };
|
|
283
|
+
}
|
|
284
|
+
const missing = [];
|
|
285
|
+
for (const name of shimNames(platform)) {
|
|
286
|
+
const path = join(globalBinDir, name);
|
|
287
|
+
if (
|
|
288
|
+
!exists(path)
|
|
289
|
+
|| !shimIsRunnable(path, platform)
|
|
290
|
+
|| !shimPointsToPackage(path, packageDir, globalBinDir, platform)
|
|
291
|
+
) missing.push(name);
|
|
292
|
+
}
|
|
293
|
+
return missing.length === 0
|
|
294
|
+
? { ok: true }
|
|
295
|
+
: { ok: false, reason: `pnpm generated shim verification failed (${missing.join(", ")})` };
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Read and verify the package manager's active global link. `owner` constraints
|
|
300
|
+
* make the listing a proof of the group selected during preflight, not merely a
|
|
301
|
+
* successful listing from whichever pnpm happens to be first on PATH.
|
|
302
|
+
*/
|
|
303
|
+
export function readPnpmGlobalPackage(
|
|
304
|
+
packageName,
|
|
305
|
+
runPnpm,
|
|
306
|
+
verify = verifyPnpmInstallTree,
|
|
307
|
+
constraints = {},
|
|
308
|
+
) {
|
|
309
|
+
const platform = constraints.platform ?? process.platform;
|
|
310
|
+
const expectedGlobalDir = constraints.expectedGlobalDir ?? constraints.owner?.globalDir;
|
|
311
|
+
const expectedGlobalRoot = constraints.expectedGlobalRoot ?? constraints.owner?.globalRoot;
|
|
312
|
+
const globalBinDir = constraints.globalBinDir ?? constraints.owner?.globalBinDir;
|
|
313
|
+
let result;
|
|
314
|
+
try {
|
|
315
|
+
result = runPnpm(ownerGlobalArgs(["list", "-g", "--depth=0", "--json", packageName], constraints.owner), true);
|
|
316
|
+
} catch {
|
|
317
|
+
return { ok: false, reason: "pnpm global package listing failed" };
|
|
318
|
+
}
|
|
319
|
+
if (result?.status !== 0) return { ok: false, reason: "pnpm global package listing failed" };
|
|
320
|
+
|
|
321
|
+
const inspected = inspectListOutput(result.stdout, packageName);
|
|
322
|
+
const entry = inspected?.entry;
|
|
323
|
+
const version = typeof entry?.version === "string" ? entry.version.trim() : "";
|
|
324
|
+
const packagePath = typeof entry?.path === "string" ? entry.path : "";
|
|
325
|
+
if (!version || !packagePath || !isAbsolutePath(packagePath, platform)) {
|
|
326
|
+
return { ok: false, reason: "pnpm did not report a valid active global package" };
|
|
327
|
+
}
|
|
328
|
+
if (constraints.expectedPackagePath && !samePath(packagePath, constraints.expectedPackagePath, platform)) {
|
|
329
|
+
return { ok: false, reason: "pnpm active package is not the running package" };
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const rootPath = normaliseGlobalRoot(inspected?.root?.path, platform);
|
|
333
|
+
if (expectedGlobalRoot || expectedGlobalDir) {
|
|
334
|
+
// pnpm versions have reported either the global group or its node_modules root;
|
|
335
|
+
// accept both, but require the root when the caller is proving ownership. Without
|
|
336
|
+
// it a successful package listing cannot distinguish a command that ignored the
|
|
337
|
+
// pinned group from the selected group.
|
|
338
|
+
if (!rootPath) return { ok: false, reason: "pnpm did not report the selected global group" };
|
|
339
|
+
const rootMatches = expectedGlobalRoot
|
|
340
|
+
? samePath(rootPath, expectedGlobalRoot, platform)
|
|
341
|
+
: globalRootMatchesDir(rootPath, expectedGlobalDir, platform);
|
|
342
|
+
if (!rootMatches) return { ok: false, reason: "pnpm listed a different global group" };
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
let tree;
|
|
346
|
+
try {
|
|
347
|
+
tree = verify(packagePath, version);
|
|
348
|
+
} catch {
|
|
349
|
+
return { ok: false, reason: "the active pnpm package could not be verified" };
|
|
350
|
+
}
|
|
351
|
+
if (!tree?.ok) return { ok: false, reason: "the active pnpm package failed verification" };
|
|
352
|
+
|
|
353
|
+
// A valid package tree/group is enough to bind the owner before an update. The
|
|
354
|
+
// existing shim may be stale from an older pnpm run; post-update and rollback
|
|
355
|
+
// reads leave this enabled so a successful transaction must produce fresh shims.
|
|
356
|
+
if (globalBinDir && constraints.checkShims !== false) {
|
|
357
|
+
const shims = (constraints.verifyShims ?? verifyPnpmGlobalShims)(
|
|
358
|
+
packagePath,
|
|
359
|
+
globalBinDir,
|
|
360
|
+
platform,
|
|
361
|
+
);
|
|
362
|
+
if (!shims?.ok) return { ok: false, reason: shims?.reason ?? "pnpm global shims failed verification" };
|
|
363
|
+
}
|
|
364
|
+
return {
|
|
365
|
+
ok: true,
|
|
366
|
+
version,
|
|
367
|
+
path: packagePath,
|
|
368
|
+
globalDir: expectedGlobalDir,
|
|
369
|
+
globalRoot: rootPath || expectedGlobalRoot,
|
|
370
|
+
globalBinDir,
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function statusText(status) {
|
|
375
|
+
return status === null || status === undefined ? "?" : String(status);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function packageSpec(packageName, versionOrTag) {
|
|
379
|
+
return `${packageName}@${versionOrTag}`;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function listGlobalPackage(commandPath, packageName, runPnpm) {
|
|
383
|
+
try {
|
|
384
|
+
return runPnpm(commandPath, ["list", "-g", "--depth=0", "--json", packageName], true);
|
|
385
|
+
} catch {
|
|
386
|
+
return null;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Find the pnpm executable and global group that own the running package. Every
|
|
392
|
+
* candidate is inspected independently; this matters when two pnpm homes expose
|
|
393
|
+
* the same pnpm version but only one owns the current package/shim.
|
|
394
|
+
*/
|
|
395
|
+
export function resolvePnpmGlobalOwner({
|
|
396
|
+
packageName,
|
|
397
|
+
packagePath,
|
|
398
|
+
commandPaths,
|
|
399
|
+
runningShimPath,
|
|
400
|
+
runPnpm,
|
|
401
|
+
verify = verifyPnpmInstallTree,
|
|
402
|
+
platform = process.platform,
|
|
403
|
+
}) {
|
|
404
|
+
if (!isAbsolutePath(packagePath, platform)) return { ok: false, reason: "running pnpm package path is not absolute" };
|
|
405
|
+
let lastReason = "no pnpm global installation owns the running package";
|
|
406
|
+
|
|
407
|
+
for (const commandPath of commandPaths ?? []) {
|
|
408
|
+
const list = listGlobalPackage(commandPath, packageName, runPnpm);
|
|
409
|
+
if (list?.status !== 0) continue;
|
|
410
|
+
const inspected = inspectListOutput(list.stdout, packageName);
|
|
411
|
+
const listedPath = typeof inspected?.entry?.path === "string" ? inspected.entry.path : "";
|
|
412
|
+
if (!listedPath || !isAbsolutePath(listedPath, platform) || !samePath(listedPath, packagePath, platform)) continue;
|
|
413
|
+
|
|
414
|
+
const paths = resolveGlobalPaths(commandPath, runPnpm, inspected?.root?.path, platform);
|
|
415
|
+
if (!paths) {
|
|
416
|
+
lastReason = "pnpm owns the running package but did not report its global group and bin directory";
|
|
417
|
+
continue;
|
|
418
|
+
}
|
|
419
|
+
const { globalDir, globalRoot, globalBinDir } = paths;
|
|
420
|
+
|
|
421
|
+
// If the process was entered through a generated command shim, its directory is
|
|
422
|
+
// another owner fact. This disambiguates two pnpm homes that expose the same pnpm
|
|
423
|
+
// version and (for example after a copied prefix) report the same package path.
|
|
424
|
+
// Direct `node bin/ocx.mjs` and Windows shims that invoke the package path do not
|
|
425
|
+
// provide a usable shim path, so they continue to rely on the package/group proof.
|
|
426
|
+
if (runningShimPath) {
|
|
427
|
+
const shimName = String(runningShimPath).replaceAll("\\", "/").split("/").at(-1)?.toLowerCase();
|
|
428
|
+
const isCommandShim = ["ocx", "opencodex", "ocx.cmd", "opencodex.cmd", "ocx.ps1", "opencodex.ps1"].includes(shimName ?? "");
|
|
429
|
+
if (isCommandShim && !samePath(dirname(runningShimPath), globalBinDir, platform)) {
|
|
430
|
+
lastReason = "pnpm package owner did not match the running global shim";
|
|
431
|
+
continue;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const owner = { commandPath, packagePath: listedPath, globalDir, globalRoot, globalBinDir };
|
|
436
|
+
const active = readPnpmGlobalPackage(
|
|
437
|
+
packageName,
|
|
438
|
+
(args, capture = false) => runPnpm(commandPath, args, capture),
|
|
439
|
+
verify,
|
|
440
|
+
{
|
|
441
|
+
owner,
|
|
442
|
+
expectedPackagePath: packagePath,
|
|
443
|
+
expectedGlobalDir: globalDir,
|
|
444
|
+
expectedGlobalRoot: globalRoot,
|
|
445
|
+
globalBinDir,
|
|
446
|
+
// Owner binding must remain possible when an older pnpm invocation left
|
|
447
|
+
// the top-level shim stale; the transaction verifies it after mutation.
|
|
448
|
+
checkShims: false,
|
|
449
|
+
platform,
|
|
450
|
+
},
|
|
451
|
+
);
|
|
452
|
+
if (active.ok) return { ok: true, owner: { ...owner, packagePath: active.path, version: active.version } };
|
|
453
|
+
lastReason = active.reason;
|
|
454
|
+
}
|
|
455
|
+
return { ok: false, reason: lastReason };
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Update a pnpm global package through pnpm itself. Never rename or remove files in
|
|
460
|
+
* the pnpm store. Every command is pinned to the owner discovered before the proxy
|
|
461
|
+
* is stopped. The pre-update proof accepts an existing stale shim; success and
|
|
462
|
+
* rollback require a valid tree plus fresh generated shims.
|
|
463
|
+
*/
|
|
464
|
+
export function runPnpmGlobalUpdate({
|
|
465
|
+
packageName,
|
|
466
|
+
currentVersion,
|
|
467
|
+
targetVersion,
|
|
468
|
+
tag,
|
|
469
|
+
owner,
|
|
470
|
+
runningPackagePath,
|
|
471
|
+
runPnpm,
|
|
472
|
+
verify = verifyPnpmInstallTree,
|
|
473
|
+
verifyShims = verifyPnpmGlobalShims,
|
|
474
|
+
platform = process.platform,
|
|
475
|
+
log = () => {},
|
|
476
|
+
}) {
|
|
477
|
+
if (
|
|
478
|
+
!owner?.commandPath
|
|
479
|
+
|| !owner?.globalDir
|
|
480
|
+
|| !owner?.globalRoot
|
|
481
|
+
|| !owner?.globalBinDir
|
|
482
|
+
|| !isAbsolutePath(owner.commandPath, platform)
|
|
483
|
+
|| !isAbsolutePath(owner.globalDir, platform)
|
|
484
|
+
|| !isAbsolutePath(owner.globalBinDir, platform)
|
|
485
|
+
) {
|
|
486
|
+
return { ok: false, phase: "preflight", error: "pnpm global owner was not pinned" };
|
|
487
|
+
}
|
|
488
|
+
const constraints = {
|
|
489
|
+
owner,
|
|
490
|
+
expectedPackagePath: runningPackagePath ?? owner.packagePath,
|
|
491
|
+
expectedGlobalDir: owner.globalDir,
|
|
492
|
+
expectedGlobalRoot: owner.globalRoot,
|
|
493
|
+
globalBinDir: owner.globalBinDir,
|
|
494
|
+
verifyShims,
|
|
495
|
+
platform,
|
|
496
|
+
};
|
|
497
|
+
const before = readPnpmGlobalPackage(
|
|
498
|
+
packageName,
|
|
499
|
+
runPnpm,
|
|
500
|
+
verify,
|
|
501
|
+
{ ...constraints, checkShims: false },
|
|
502
|
+
);
|
|
503
|
+
if (!before.ok) return { ok: false, phase: "preflight", error: before.reason };
|
|
504
|
+
if (currentVersion && before.version !== currentVersion) {
|
|
505
|
+
return { ok: false, phase: "preflight", error: "pnpm's active package does not match the running package" };
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
const requested = targetVersion || tag;
|
|
509
|
+
const spec = packageSpec(packageName, requested);
|
|
510
|
+
log(`Updating ${spec} with pnpm…`);
|
|
511
|
+
let install;
|
|
512
|
+
try {
|
|
513
|
+
install = runPnpm(ownerGlobalArgs(["add", "-g", PNPM_BUILD_APPROVAL, spec], owner), false);
|
|
514
|
+
} catch {
|
|
515
|
+
install = { status: 1 };
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// The package path may switch to a different group link, so only constrain
|
|
519
|
+
// the group/bin pair after the command; the active package path is intentionally open.
|
|
520
|
+
const after = readPnpmGlobalPackage(
|
|
521
|
+
packageName,
|
|
522
|
+
runPnpm,
|
|
523
|
+
verify,
|
|
524
|
+
{ ...constraints, expectedPackagePath: undefined },
|
|
525
|
+
);
|
|
526
|
+
const targetMatches = after.ok && (!targetVersion || after.version === targetVersion);
|
|
527
|
+
if (install?.status === 0 && targetMatches) {
|
|
528
|
+
return {
|
|
529
|
+
ok: true,
|
|
530
|
+
phase: "done",
|
|
531
|
+
version: after.version,
|
|
532
|
+
path: after.path,
|
|
533
|
+
globalDir: owner.globalDir,
|
|
534
|
+
globalBinDir: owner.globalBinDir,
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
const installReason = install?.status !== 0
|
|
539
|
+
? `pnpm update failed (${statusText(install?.status)})`
|
|
540
|
+
: after.ok
|
|
541
|
+
? "pnpm update produced an unexpected active package version"
|
|
542
|
+
: "pnpm update completed but the active package failed verification";
|
|
543
|
+
|
|
544
|
+
// A failed command may have left the original active group intact. Do not create a
|
|
545
|
+
// second update transaction in that case; the verified previous package is already safe.
|
|
546
|
+
if (after.ok && after.version === before.version) {
|
|
547
|
+
return {
|
|
548
|
+
ok: false,
|
|
549
|
+
phase: "install",
|
|
550
|
+
rolledBack: true,
|
|
551
|
+
activePath: after.path,
|
|
552
|
+
globalDir: owner.globalDir,
|
|
553
|
+
globalBinDir: owner.globalBinDir,
|
|
554
|
+
error: `${installReason}; previous version remains active`,
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
log(`Restoring ${packageName}@${before.version} with pnpm…`);
|
|
559
|
+
let rollback;
|
|
560
|
+
try {
|
|
561
|
+
rollback = runPnpm(ownerGlobalArgs(["add", "-g", PNPM_BUILD_APPROVAL, packageSpec(packageName, before.version)], owner), false);
|
|
562
|
+
} catch {
|
|
563
|
+
rollback = { status: 1 };
|
|
564
|
+
}
|
|
565
|
+
const restored = readPnpmGlobalPackage(
|
|
566
|
+
packageName,
|
|
567
|
+
runPnpm,
|
|
568
|
+
verify,
|
|
569
|
+
{ ...constraints, expectedPackagePath: undefined },
|
|
570
|
+
);
|
|
571
|
+
if (rollback?.status === 0 && restored.ok && restored.version === before.version) {
|
|
572
|
+
return {
|
|
573
|
+
ok: false,
|
|
574
|
+
phase: "rollback",
|
|
575
|
+
rolledBack: true,
|
|
576
|
+
activePath: restored.path,
|
|
577
|
+
globalDir: owner.globalDir,
|
|
578
|
+
globalBinDir: owner.globalBinDir,
|
|
579
|
+
error: `${installReason}; previous version restored`,
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
return {
|
|
583
|
+
ok: false,
|
|
584
|
+
phase: "rollback",
|
|
585
|
+
rolledBack: false,
|
|
586
|
+
...(restored.ok ? { activePath: restored.path } : {}),
|
|
587
|
+
globalDir: owner.globalDir,
|
|
588
|
+
globalBinDir: owner.globalBinDir,
|
|
589
|
+
error: `${installReason}; previous version could not be verified after rollback`,
|
|
590
|
+
};
|
|
591
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export interface PnpmInvocationDeps {
|
|
2
|
+
cwd?: string;
|
|
3
|
+
exists?: (path: string) => boolean;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export interface PnpmInvocation {
|
|
7
|
+
file: string;
|
|
8
|
+
args: string[];
|
|
9
|
+
options: { windowsVerbatimArguments?: boolean };
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export declare function resolvePnpmCommands(
|
|
13
|
+
platform?: NodeJS.Platform,
|
|
14
|
+
env?: Record<string, string | undefined>,
|
|
15
|
+
deps?: PnpmInvocationDeps,
|
|
16
|
+
): string[];
|
|
17
|
+
|
|
18
|
+
export declare function pnpmInvocationForPath(
|
|
19
|
+
pnpm: string,
|
|
20
|
+
args: readonly string[],
|
|
21
|
+
platform?: NodeJS.Platform,
|
|
22
|
+
env?: Record<string, string | undefined>,
|
|
23
|
+
): PnpmInvocation | null;
|
|
24
|
+
|
|
25
|
+
export declare function resolvePnpmCommand(
|
|
26
|
+
platform?: NodeJS.Platform,
|
|
27
|
+
env?: Record<string, string | undefined>,
|
|
28
|
+
deps?: PnpmInvocationDeps,
|
|
29
|
+
): string | null;
|
|
30
|
+
|
|
31
|
+
export declare function pnpmInvocation(
|
|
32
|
+
args: readonly string[],
|
|
33
|
+
platform?: NodeJS.Platform,
|
|
34
|
+
env?: Record<string, string | undefined>,
|
|
35
|
+
deps?: PnpmInvocationDeps,
|
|
36
|
+
): PnpmInvocation | null;
|
|
37
|
+
|
|
38
|
+
export declare function pnpmInvocations(
|
|
39
|
+
args: readonly string[],
|
|
40
|
+
platform?: NodeJS.Platform,
|
|
41
|
+
env?: Record<string, string | undefined>,
|
|
42
|
+
deps?: PnpmInvocationDeps,
|
|
43
|
+
): PnpmInvocation[];
|