@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.
Files changed (95) hide show
  1. package/bin/ocx.mjs +222 -71
  2. package/gui/dist/assets/{index-C39tnjXO.js → index-Dx0xv2EA.js} +1 -1
  3. package/gui/dist/index.html +1 -1
  4. package/package.json +1 -1
  5. package/src/adapters/qoder/adapter.ts +69 -1
  6. package/src/adapters/qoder/scaffold-guard.ts +233 -0
  7. package/src/claude/agents-inject.ts +29 -5
  8. package/src/claude/desktop-3p.ts +31 -3
  9. package/src/claude/gateway-cache.ts +12 -21
  10. package/src/cli/capabilities.ts +28 -0
  11. package/src/cli/claude-agent-startup-sync.ts +26 -1
  12. package/src/cli/claude.ts +138 -20
  13. package/src/cli/config-command.ts +67 -1
  14. package/src/cli/connect.ts +181 -14
  15. package/src/cli/dispatch.ts +53 -9
  16. package/src/cli/doctor.ts +9 -2
  17. package/src/cli/ensure-desired-integrations.ts +10 -0
  18. package/src/cli/gui-pair-client.ts +1 -12
  19. package/src/cli/help.ts +4 -1
  20. package/src/cli/hub.ts +367 -0
  21. package/src/cli/index.ts +94 -30
  22. package/src/cli/launcher-context.ts +1 -1
  23. package/src/cli/registry.ts +43 -3
  24. package/src/cli/status.ts +325 -5
  25. package/src/cli/version-skew.ts +4 -1
  26. package/src/cli.ts +2 -2
  27. package/src/client/catalog-compatibility.ts +192 -0
  28. package/src/client/connect.ts +31 -0
  29. package/src/client/hub-client.ts +52 -0
  30. package/src/client/hub-state.ts +214 -0
  31. package/src/codex/account-usability.ts +48 -12
  32. package/src/codex/auth-api.ts +49 -5
  33. package/src/codex/catalog/effort.ts +67 -8
  34. package/src/codex/catalog/sync.ts +85 -0
  35. package/src/codex/codex-write-lock.ts +11 -2
  36. package/src/codex/desired-state.ts +47 -1
  37. package/src/codex/inject-coordination.ts +10 -5
  38. package/src/codex/inject.ts +26 -10
  39. package/src/codex/loopback-target.ts +45 -0
  40. package/src/codex/routing.ts +48 -1
  41. package/src/codex/runtime.ts +37 -3
  42. package/src/codex/sync.ts +29 -9
  43. package/src/codex/warmup.ts +21 -4
  44. package/src/config/pending-teardown.ts +1 -1
  45. package/src/config.ts +126 -12
  46. package/src/generated/compatibility-version.json +136 -76
  47. package/src/grok/status.ts +9 -1
  48. package/src/integrations/config-io.ts +54 -1
  49. package/src/lib/bun-runtime.ts +1 -1
  50. package/src/lib/gui-pair-capability.ts +27 -0
  51. package/src/lib/local-destinations.ts +162 -0
  52. package/src/lib/package-tree-integrity.ts +1 -1
  53. package/src/lib/process-control.ts +130 -20
  54. package/src/lib/service-secrets.ts +28 -0
  55. package/src/lib/test-home-guard.ts +49 -0
  56. package/src/providers/opencode-go-transport.ts +9 -1
  57. package/src/providers/quota.ts +5 -1
  58. package/src/providers/registry.ts +34 -5
  59. package/src/remote/hub-state.ts +182 -0
  60. package/src/server/auth-cors.ts +5 -0
  61. package/src/server/chat-completions.ts +6 -3
  62. package/src/server/claude-messages.ts +7 -1
  63. package/src/server/hub-state.ts +98 -0
  64. package/src/server/index.ts +124 -6
  65. package/src/server/management/api-access.ts +14 -3
  66. package/src/server/management/config-routes.ts +2 -2
  67. package/src/server/management/cursor-integration-routes.ts +13 -4
  68. package/src/server/proxy-liveness.ts +7 -1
  69. package/src/server/request-log-conversation.ts +41 -1
  70. package/src/server/responses/codex-auth-error.ts +18 -1
  71. package/src/server/responses/codex-ws-exchange.ts +36 -4
  72. package/src/server/responses/codex-ws-wire.ts +75 -4
  73. package/src/server/responses/compact.ts +20 -9
  74. package/src/server/responses/core.ts +57 -10
  75. package/src/server/responses/policy-fallback.ts +7 -1
  76. package/src/server/system-env-shell.ts +14 -2
  77. package/src/server/system-env.ts +106 -14
  78. package/src/service.ts +906 -94
  79. package/src/types/config.ts +57 -4
  80. package/src/update/badge.ts +3 -2
  81. package/src/update/index.ts +317 -64
  82. package/src/update/install-detection.d.mts +6 -0
  83. package/src/update/install-detection.mjs +73 -0
  84. package/src/update/job.ts +101 -49
  85. package/src/update/pnpm-global-install.d.mts +144 -0
  86. package/src/update/pnpm-global-install.mjs +591 -0
  87. package/src/update/pnpm-invocation.d.mts +43 -0
  88. package/src/update/pnpm-invocation.mjs +141 -0
  89. package/src/update/registry-integrity.d.mts +16 -0
  90. package/src/update/registry-integrity.mjs +37 -0
  91. package/src/update/transactional-install.d.mts +1 -1
  92. package/src/update/transactional-install.mjs +101 -7
  93. package/src/update/tray-update-plan.mjs +1 -1
  94. package/src/vision/plan.ts +13 -3
  95. 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[];