copilot-hub 0.1.25 → 0.1.28
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/apps/agent-engine/dist/config.js +19 -1
- package/apps/control-plane/dist/config.js +19 -1
- package/package.json +1 -1
- package/scripts/dist/cli.mjs +8 -19
- package/scripts/dist/codex-runtime.mjs +51 -37
- package/scripts/dist/codex-spawn.mjs +42 -0
- package/scripts/src/cli.mts +8 -24
- package/scripts/src/codex-runtime.mts +84 -38
- package/scripts/src/codex-spawn.mts +84 -0
- package/scripts/test/codex-runtime.test.mjs +59 -0
- package/scripts/test/codex-spawn.test.mjs +60 -0
|
@@ -146,7 +146,7 @@ function resolveCodexBin(rawValue) {
|
|
|
146
146
|
const value = String(rawValue ?? "").trim();
|
|
147
147
|
const normalized = value.toLowerCase();
|
|
148
148
|
if (value && normalized !== "codex") {
|
|
149
|
-
return value;
|
|
149
|
+
return normalizeConfiguredCodexBin(value);
|
|
150
150
|
}
|
|
151
151
|
if (process.platform === "win32") {
|
|
152
152
|
const npmGlobalCodex = findWindowsNpmGlobalCodexBin();
|
|
@@ -160,6 +160,24 @@ function resolveCodexBin(rawValue) {
|
|
|
160
160
|
}
|
|
161
161
|
return value || "codex";
|
|
162
162
|
}
|
|
163
|
+
function normalizeConfiguredCodexBin(value) {
|
|
164
|
+
const normalizedValue = String(value ?? "").trim();
|
|
165
|
+
if (process.platform !== "win32" || !normalizedValue) {
|
|
166
|
+
return normalizedValue;
|
|
167
|
+
}
|
|
168
|
+
const basename = path.win32.basename(normalizedValue).toLowerCase();
|
|
169
|
+
if (basename !== "codex.cmd" && basename !== "codex.bat") {
|
|
170
|
+
return normalizedValue;
|
|
171
|
+
}
|
|
172
|
+
if (path.win32.isAbsolute(normalizedValue)) {
|
|
173
|
+
const wrapperDir = path.win32.dirname(normalizedValue);
|
|
174
|
+
const entrypoint = path.win32.join(wrapperDir, "node_modules", "@openai", "codex", "bin", "codex.js");
|
|
175
|
+
if (fs.existsSync(entrypoint)) {
|
|
176
|
+
return entrypoint;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return findWindowsNpmGlobalCodexBin() || normalizedValue;
|
|
180
|
+
}
|
|
163
181
|
function findVscodeCodexExe() {
|
|
164
182
|
const userProfile = process.env.USERPROFILE;
|
|
165
183
|
if (!userProfile) {
|
|
@@ -153,7 +153,7 @@ function resolveCodexBin(rawValue) {
|
|
|
153
153
|
const value = String(rawValue ?? "").trim();
|
|
154
154
|
const normalized = value.toLowerCase();
|
|
155
155
|
if (value && normalized !== "codex") {
|
|
156
|
-
return value;
|
|
156
|
+
return normalizeConfiguredCodexBin(value);
|
|
157
157
|
}
|
|
158
158
|
if (process.platform === "win32") {
|
|
159
159
|
const npmGlobalCodex = findWindowsNpmGlobalCodexBin();
|
|
@@ -167,6 +167,24 @@ function resolveCodexBin(rawValue) {
|
|
|
167
167
|
}
|
|
168
168
|
return value || "codex";
|
|
169
169
|
}
|
|
170
|
+
function normalizeConfiguredCodexBin(value) {
|
|
171
|
+
const normalizedValue = String(value ?? "").trim();
|
|
172
|
+
if (process.platform !== "win32" || !normalizedValue) {
|
|
173
|
+
return normalizedValue;
|
|
174
|
+
}
|
|
175
|
+
const basename = path.win32.basename(normalizedValue).toLowerCase();
|
|
176
|
+
if (basename !== "codex.cmd" && basename !== "codex.bat") {
|
|
177
|
+
return normalizedValue;
|
|
178
|
+
}
|
|
179
|
+
if (path.win32.isAbsolute(normalizedValue)) {
|
|
180
|
+
const wrapperDir = path.win32.dirname(normalizedValue);
|
|
181
|
+
const entrypoint = path.win32.join(wrapperDir, "node_modules", "@openai", "codex", "bin", "codex.js");
|
|
182
|
+
if (fs.existsSync(entrypoint)) {
|
|
183
|
+
return entrypoint;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return findWindowsNpmGlobalCodexBin() || normalizedValue;
|
|
187
|
+
}
|
|
170
188
|
function findVscodeCodexExe() {
|
|
171
189
|
const userProfile = process.env.USERPROFILE;
|
|
172
190
|
if (!userProfile) {
|
package/package.json
CHANGED
package/scripts/dist/cli.mjs
CHANGED
|
@@ -5,6 +5,7 @@ import process, { stdin as input, stdout as output } from "node:process";
|
|
|
5
5
|
import { spawnSync } from "node:child_process";
|
|
6
6
|
import { createInterface } from "node:readline/promises";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
|
+
import { spawnCodexSync } from "./codex-spawn.mjs";
|
|
8
9
|
import { codexInstallPackageSpec } from "./codex-version.mjs";
|
|
9
10
|
import { initializeCopilotHubLayout, resolveCopilotHubLayout } from "./install-layout.mjs";
|
|
10
11
|
import { buildCodexCompatibilityError, buildCodexCompatibilityNotice, probeCodexVersion, resolveCodexBinForStart, resolveCompatibleInstalledCodexBin, } from "./codex-runtime.mjs";
|
|
@@ -367,22 +368,13 @@ async function ensureCompatibleCodexBinary({ autoInstall, purpose, }) {
|
|
|
367
368
|
}
|
|
368
369
|
function runCodex(codexBin, args, stdioMode) {
|
|
369
370
|
const stdio = stdioMode === "inherit" ? "inherit" : ["ignore", "pipe", "pipe"];
|
|
370
|
-
const result =
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
shell: true,
|
|
378
|
-
encoding: "utf8",
|
|
379
|
-
})
|
|
380
|
-
: spawnSync(codexBin, args, {
|
|
381
|
-
cwd: repoRoot,
|
|
382
|
-
stdio,
|
|
383
|
-
shell: false,
|
|
384
|
-
encoding: "utf8",
|
|
385
|
-
});
|
|
371
|
+
const result = spawnCodexSync({
|
|
372
|
+
codexBin,
|
|
373
|
+
args,
|
|
374
|
+
cwd: repoRoot,
|
|
375
|
+
stdio,
|
|
376
|
+
encoding: "utf8",
|
|
377
|
+
});
|
|
386
378
|
if (result.error) {
|
|
387
379
|
return {
|
|
388
380
|
ok: false,
|
|
@@ -401,9 +393,6 @@ function runCodex(codexBin, args, stdioMode) {
|
|
|
401
393
|
errorCode: "",
|
|
402
394
|
};
|
|
403
395
|
}
|
|
404
|
-
function quoteWindowsShellValue(value) {
|
|
405
|
-
return `"${String(value ?? "").replace(/"/g, '\\"')}"`;
|
|
406
|
-
}
|
|
407
396
|
function runNpm(args, stdioMode) {
|
|
408
397
|
const stdio = stdioMode === "inherit" ? "inherit" : ["ignore", "pipe", "pipe"];
|
|
409
398
|
const result = spawnNpm(args, {
|
|
@@ -2,6 +2,7 @@ import fs from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import process from "node:process";
|
|
4
4
|
import { spawnSync } from "node:child_process";
|
|
5
|
+
import { spawnCodexSync } from "./codex-spawn.mjs";
|
|
5
6
|
import { compareSemver, codexVersionRequirementLabel, extractSemver, isCodexVersionCompatible, } from "./codex-version.mjs";
|
|
6
7
|
export function resolveCodexBinForStart({ repoRoot, agentEngineEnvPath, controlPlaneEnvPath, env = process.env, }) {
|
|
7
8
|
const fromEnv = nonEmpty(env.CODEX_BIN);
|
|
@@ -41,9 +42,9 @@ export function resolveCodexBinForStart({ repoRoot, agentEngineEnvPath, controlP
|
|
|
41
42
|
userConfigured: false,
|
|
42
43
|
};
|
|
43
44
|
}
|
|
44
|
-
export function resolveCompatibleInstalledCodexBin({ repoRoot, env = process.env, }) {
|
|
45
|
+
export function resolveCompatibleInstalledCodexBin({ repoRoot, env = process.env, platform = process.platform, }) {
|
|
45
46
|
const matches = [];
|
|
46
|
-
for (const candidate of listCodexBinCandidates(env, repoRoot)) {
|
|
47
|
+
for (const candidate of listCodexBinCandidates(env, repoRoot, platform)) {
|
|
47
48
|
const probe = probeCodexVersion({
|
|
48
49
|
codexBin: candidate,
|
|
49
50
|
repoRoot,
|
|
@@ -54,7 +55,7 @@ export function resolveCompatibleInstalledCodexBin({ repoRoot, env = process.env
|
|
|
54
55
|
matches.push({
|
|
55
56
|
candidate,
|
|
56
57
|
version: probe.version,
|
|
57
|
-
priority: getCodexCandidatePriority(candidate, env, repoRoot),
|
|
58
|
+
priority: getCodexCandidatePriority(candidate, env, repoRoot, platform),
|
|
58
59
|
});
|
|
59
60
|
}
|
|
60
61
|
if (matches.length === 0) {
|
|
@@ -133,12 +134,17 @@ export function buildCodexCompatibilityError({ resolved, probe, includeInstallHi
|
|
|
133
134
|
return lines.join("\n");
|
|
134
135
|
}
|
|
135
136
|
function buildResolvedCodexBin({ value, source, env, repoRoot, }) {
|
|
136
|
-
const
|
|
137
|
+
const normalizedValue = normalizeConfiguredCodexBin({
|
|
138
|
+
value,
|
|
139
|
+
env,
|
|
140
|
+
repoRoot,
|
|
141
|
+
});
|
|
142
|
+
const normalized = String(normalizedValue ?? "")
|
|
137
143
|
.trim()
|
|
138
144
|
.toLowerCase();
|
|
139
145
|
if (normalized && normalized !== "codex") {
|
|
140
146
|
return {
|
|
141
|
-
bin:
|
|
147
|
+
bin: normalizedValue,
|
|
142
148
|
source,
|
|
143
149
|
userConfigured: true,
|
|
144
150
|
};
|
|
@@ -150,20 +156,43 @@ function buildResolvedCodexBin({ value, source, env, repoRoot, }) {
|
|
|
150
156
|
userConfigured: false,
|
|
151
157
|
};
|
|
152
158
|
}
|
|
159
|
+
export function normalizeConfiguredCodexBin({ value, env = process.env, repoRoot, platform = process.platform, }) {
|
|
160
|
+
const normalizedValue = String(value ?? "").trim();
|
|
161
|
+
if (!normalizedValue || platform !== "win32") {
|
|
162
|
+
return normalizedValue;
|
|
163
|
+
}
|
|
164
|
+
const normalizedBasename = path.win32.basename(normalizedValue).toLowerCase();
|
|
165
|
+
if (normalizedBasename !== "codex.cmd" && normalizedBasename !== "codex.bat") {
|
|
166
|
+
return normalizedValue;
|
|
167
|
+
}
|
|
168
|
+
if (path.win32.isAbsolute(normalizedValue)) {
|
|
169
|
+
const pathModule = selectPathModule(normalizedValue);
|
|
170
|
+
const wrapperDir = pathModule.dirname(normalizedValue);
|
|
171
|
+
const entrypoint = pathModule.join(wrapperDir, "node_modules", "@openai", "codex", "bin", "codex.js");
|
|
172
|
+
if (fs.existsSync(entrypoint)) {
|
|
173
|
+
return entrypoint;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return findWindowsNpmGlobalCodexBin(env, repoRoot, platform) || normalizedValue;
|
|
177
|
+
}
|
|
153
178
|
function findDetectedCodexBin(env, repoRoot) {
|
|
154
179
|
if (process.platform !== "win32") {
|
|
155
180
|
return "";
|
|
156
181
|
}
|
|
157
|
-
return findWindowsNpmGlobalCodexBin(env, repoRoot) || findVscodeCodexExe(env) || "";
|
|
182
|
+
return (findWindowsNpmGlobalCodexBin(env, repoRoot, process.platform) || findVscodeCodexExe(env) || "");
|
|
158
183
|
}
|
|
159
|
-
function listCodexBinCandidates(env, repoRoot) {
|
|
160
|
-
return dedupe([
|
|
184
|
+
function listCodexBinCandidates(env, repoRoot, platform) {
|
|
185
|
+
return dedupe([
|
|
186
|
+
"codex",
|
|
187
|
+
findWindowsNpmGlobalCodexBin(env, repoRoot, platform),
|
|
188
|
+
findVscodeCodexExe(env),
|
|
189
|
+
]);
|
|
161
190
|
}
|
|
162
|
-
function getCodexCandidatePriority(candidate, env, repoRoot) {
|
|
191
|
+
function getCodexCandidatePriority(candidate, env, repoRoot, platform) {
|
|
163
192
|
if (candidate === "codex") {
|
|
164
193
|
return 0;
|
|
165
194
|
}
|
|
166
|
-
const npmGlobal = findWindowsNpmGlobalCodexBin(env, repoRoot);
|
|
195
|
+
const npmGlobal = findWindowsNpmGlobalCodexBin(env, repoRoot, platform);
|
|
167
196
|
if (npmGlobal && candidate === npmGlobal) {
|
|
168
197
|
return 1;
|
|
169
198
|
}
|
|
@@ -197,8 +226,8 @@ function findVscodeCodexExe(env) {
|
|
|
197
226
|
}
|
|
198
227
|
return "";
|
|
199
228
|
}
|
|
200
|
-
function findWindowsNpmGlobalCodexBin(env, repoRoot) {
|
|
201
|
-
if (
|
|
229
|
+
function findWindowsNpmGlobalCodexBin(env, repoRoot, platform) {
|
|
230
|
+
if (platform !== "win32") {
|
|
202
231
|
return "";
|
|
203
232
|
}
|
|
204
233
|
const packageRoots = [];
|
|
@@ -230,6 +259,13 @@ function findWindowsNpmGlobalCodexBin(env, repoRoot) {
|
|
|
230
259
|
}
|
|
231
260
|
return "";
|
|
232
261
|
}
|
|
262
|
+
function selectPathModule(filePath) {
|
|
263
|
+
const normalized = String(filePath ?? "").trim();
|
|
264
|
+
if (/^[A-Za-z]:[\\/]/.test(normalized) || normalized.includes("\\")) {
|
|
265
|
+
return path.win32;
|
|
266
|
+
}
|
|
267
|
+
return path.posix;
|
|
268
|
+
}
|
|
233
269
|
function readNpmPrefix(repoRoot) {
|
|
234
270
|
const result = spawnNpm(["config", "get", "prefix"], repoRoot);
|
|
235
271
|
if (result.error || result.status !== 0) {
|
|
@@ -262,30 +298,11 @@ function runCodex({ codexBin, args, repoRoot, }) {
|
|
|
262
298
|
};
|
|
263
299
|
}
|
|
264
300
|
function spawnCodex(codexBin, args, repoRoot) {
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
269
|
-
shell: false,
|
|
270
|
-
encoding: "utf8",
|
|
271
|
-
});
|
|
272
|
-
}
|
|
273
|
-
if (process.platform === "win32" && /\.(cmd|bat)$/i.test(codexBin)) {
|
|
274
|
-
const commandLine = [
|
|
275
|
-
quoteWindowsShellValue(codexBin),
|
|
276
|
-
...args.map(quoteWindowsShellValue),
|
|
277
|
-
].join(" ");
|
|
278
|
-
return spawnSync(commandLine, {
|
|
279
|
-
cwd: repoRoot,
|
|
280
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
281
|
-
shell: true,
|
|
282
|
-
encoding: "utf8",
|
|
283
|
-
});
|
|
284
|
-
}
|
|
285
|
-
return spawnSync(codexBin, args, {
|
|
301
|
+
return spawnCodexSync({
|
|
302
|
+
codexBin,
|
|
303
|
+
args,
|
|
286
304
|
cwd: repoRoot,
|
|
287
305
|
stdio: ["ignore", "pipe", "pipe"],
|
|
288
|
-
shell: false,
|
|
289
306
|
encoding: "utf8",
|
|
290
307
|
});
|
|
291
308
|
}
|
|
@@ -346,9 +363,6 @@ function normalizeErrorCode(error) {
|
|
|
346
363
|
function dedupe(values) {
|
|
347
364
|
return [...new Set(values.map((value) => String(value ?? "").trim()).filter(Boolean))];
|
|
348
365
|
}
|
|
349
|
-
function quoteWindowsShellValue(value) {
|
|
350
|
-
return `"${String(value ?? "").replace(/"/g, '\\"')}"`;
|
|
351
|
-
}
|
|
352
366
|
function spawnNpm(args, repoRoot) {
|
|
353
367
|
if (process.platform === "win32") {
|
|
354
368
|
const comspec = process.env.ComSpec || "cmd.exe";
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import process from "node:process";
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
export function requiresNodeScriptCodexBin(command) {
|
|
4
|
+
return /\.(cjs|mjs|js)$/i.test(String(command ?? "").trim());
|
|
5
|
+
}
|
|
6
|
+
export function requiresShellWrappedCodexBin(command, platform = process.platform) {
|
|
7
|
+
return platform === "win32" && /\.(cmd|bat)$/i.test(String(command ?? "").trim());
|
|
8
|
+
}
|
|
9
|
+
export function buildCodexSpawnSpec({ codexBin, args, platform = process.platform, nodeBin = process.execPath, }) {
|
|
10
|
+
if (requiresNodeScriptCodexBin(codexBin)) {
|
|
11
|
+
return {
|
|
12
|
+
command: nodeBin,
|
|
13
|
+
args: [codexBin, ...args],
|
|
14
|
+
shell: false,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
if (requiresShellWrappedCodexBin(codexBin, platform)) {
|
|
18
|
+
return {
|
|
19
|
+
command: [quoteWindowsShellValue(codexBin), ...args.map(quoteWindowsShellValue)].join(" "),
|
|
20
|
+
args: [],
|
|
21
|
+
shell: true,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
return {
|
|
25
|
+
command: codexBin,
|
|
26
|
+
args: [...args],
|
|
27
|
+
shell: false,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export function spawnCodexSync({ codexBin, args, cwd, stdio, encoding = "utf8", input, }) {
|
|
31
|
+
const spec = buildCodexSpawnSpec({ codexBin, args });
|
|
32
|
+
return spawnSync(spec.command, spec.args, {
|
|
33
|
+
cwd,
|
|
34
|
+
stdio,
|
|
35
|
+
shell: spec.shell,
|
|
36
|
+
encoding,
|
|
37
|
+
...(input !== undefined ? { input } : {}),
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
function quoteWindowsShellValue(value) {
|
|
41
|
+
return `"${String(value ?? "").replace(/"/g, '\\"')}"`;
|
|
42
|
+
}
|
package/scripts/src/cli.mts
CHANGED
|
@@ -5,6 +5,7 @@ import process, { stdin as input, stdout as output } from "node:process";
|
|
|
5
5
|
import { spawnSync } from "node:child_process";
|
|
6
6
|
import { createInterface } from "node:readline/promises";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
|
+
import { spawnCodexSync } from "./codex-spawn.mjs";
|
|
8
9
|
import { codexInstallPackageSpec } from "./codex-version.mjs";
|
|
9
10
|
import { initializeCopilotHubLayout, resolveCopilotHubLayout } from "./install-layout.mjs";
|
|
10
11
|
import {
|
|
@@ -443,26 +444,13 @@ async function ensureCompatibleCodexBinary({
|
|
|
443
444
|
|
|
444
445
|
function runCodex(codexBin, args, stdioMode) {
|
|
445
446
|
const stdio: any = stdioMode === "inherit" ? "inherit" : ["ignore", "pipe", "pipe"];
|
|
446
|
-
const result =
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
{
|
|
454
|
-
cwd: repoRoot,
|
|
455
|
-
stdio,
|
|
456
|
-
shell: true,
|
|
457
|
-
encoding: "utf8",
|
|
458
|
-
},
|
|
459
|
-
)
|
|
460
|
-
: spawnSync(codexBin, args, {
|
|
461
|
-
cwd: repoRoot,
|
|
462
|
-
stdio,
|
|
463
|
-
shell: false,
|
|
464
|
-
encoding: "utf8",
|
|
465
|
-
});
|
|
447
|
+
const result = spawnCodexSync({
|
|
448
|
+
codexBin,
|
|
449
|
+
args,
|
|
450
|
+
cwd: repoRoot,
|
|
451
|
+
stdio,
|
|
452
|
+
encoding: "utf8",
|
|
453
|
+
});
|
|
466
454
|
|
|
467
455
|
if (result.error) {
|
|
468
456
|
return {
|
|
@@ -484,10 +472,6 @@ function runCodex(codexBin, args, stdioMode) {
|
|
|
484
472
|
};
|
|
485
473
|
}
|
|
486
474
|
|
|
487
|
-
function quoteWindowsShellValue(value) {
|
|
488
|
-
return `"${String(value ?? "").replace(/"/g, '\\"')}"`;
|
|
489
|
-
}
|
|
490
|
-
|
|
491
475
|
function runNpm(args, stdioMode) {
|
|
492
476
|
const stdio = stdioMode === "inherit" ? "inherit" : ["ignore", "pipe", "pipe"];
|
|
493
477
|
const result = spawnNpm(args, {
|
|
@@ -2,6 +2,7 @@ import fs from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import process from "node:process";
|
|
4
4
|
import { spawnSync } from "node:child_process";
|
|
5
|
+
import { spawnCodexSync } from "./codex-spawn.mjs";
|
|
5
6
|
import {
|
|
6
7
|
compareSemver,
|
|
7
8
|
codexVersionRequirementLabel,
|
|
@@ -81,13 +82,15 @@ export function resolveCodexBinForStart({
|
|
|
81
82
|
export function resolveCompatibleInstalledCodexBin({
|
|
82
83
|
repoRoot,
|
|
83
84
|
env = process.env,
|
|
85
|
+
platform = process.platform,
|
|
84
86
|
}: {
|
|
85
87
|
repoRoot: string;
|
|
86
88
|
env?: NodeJS.ProcessEnv;
|
|
89
|
+
platform?: NodeJS.Platform;
|
|
87
90
|
}): string {
|
|
88
91
|
const matches: Array<{ candidate: string; version: string; priority: number }> = [];
|
|
89
92
|
|
|
90
|
-
for (const candidate of listCodexBinCandidates(env, repoRoot)) {
|
|
93
|
+
for (const candidate of listCodexBinCandidates(env, repoRoot, platform)) {
|
|
91
94
|
const probe = probeCodexVersion({
|
|
92
95
|
codexBin: candidate,
|
|
93
96
|
repoRoot,
|
|
@@ -99,7 +102,7 @@ export function resolveCompatibleInstalledCodexBin({
|
|
|
99
102
|
matches.push({
|
|
100
103
|
candidate,
|
|
101
104
|
version: probe.version,
|
|
102
|
-
priority: getCodexCandidatePriority(candidate, env, repoRoot),
|
|
105
|
+
priority: getCodexCandidatePriority(candidate, env, repoRoot, platform),
|
|
103
106
|
});
|
|
104
107
|
}
|
|
105
108
|
|
|
@@ -229,12 +232,17 @@ function buildResolvedCodexBin({
|
|
|
229
232
|
env: NodeJS.ProcessEnv;
|
|
230
233
|
repoRoot: string;
|
|
231
234
|
}): ResolvedCodexBin {
|
|
232
|
-
const
|
|
235
|
+
const normalizedValue = normalizeConfiguredCodexBin({
|
|
236
|
+
value,
|
|
237
|
+
env,
|
|
238
|
+
repoRoot,
|
|
239
|
+
});
|
|
240
|
+
const normalized = String(normalizedValue ?? "")
|
|
233
241
|
.trim()
|
|
234
242
|
.toLowerCase();
|
|
235
243
|
if (normalized && normalized !== "codex") {
|
|
236
244
|
return {
|
|
237
|
-
bin:
|
|
245
|
+
bin: normalizedValue,
|
|
238
246
|
source,
|
|
239
247
|
userConfigured: true,
|
|
240
248
|
};
|
|
@@ -248,28 +256,79 @@ function buildResolvedCodexBin({
|
|
|
248
256
|
};
|
|
249
257
|
}
|
|
250
258
|
|
|
259
|
+
export function normalizeConfiguredCodexBin({
|
|
260
|
+
value,
|
|
261
|
+
env = process.env,
|
|
262
|
+
repoRoot,
|
|
263
|
+
platform = process.platform,
|
|
264
|
+
}: {
|
|
265
|
+
value: string;
|
|
266
|
+
env?: NodeJS.ProcessEnv;
|
|
267
|
+
repoRoot: string;
|
|
268
|
+
platform?: NodeJS.Platform;
|
|
269
|
+
}): string {
|
|
270
|
+
const normalizedValue = String(value ?? "").trim();
|
|
271
|
+
if (!normalizedValue || platform !== "win32") {
|
|
272
|
+
return normalizedValue;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const normalizedBasename = path.win32.basename(normalizedValue).toLowerCase();
|
|
276
|
+
if (normalizedBasename !== "codex.cmd" && normalizedBasename !== "codex.bat") {
|
|
277
|
+
return normalizedValue;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (path.win32.isAbsolute(normalizedValue)) {
|
|
281
|
+
const pathModule = selectPathModule(normalizedValue);
|
|
282
|
+
const wrapperDir = pathModule.dirname(normalizedValue);
|
|
283
|
+
const entrypoint = pathModule.join(
|
|
284
|
+
wrapperDir,
|
|
285
|
+
"node_modules",
|
|
286
|
+
"@openai",
|
|
287
|
+
"codex",
|
|
288
|
+
"bin",
|
|
289
|
+
"codex.js",
|
|
290
|
+
);
|
|
291
|
+
if (fs.existsSync(entrypoint)) {
|
|
292
|
+
return entrypoint;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return findWindowsNpmGlobalCodexBin(env, repoRoot, platform) || normalizedValue;
|
|
297
|
+
}
|
|
298
|
+
|
|
251
299
|
function findDetectedCodexBin(env: NodeJS.ProcessEnv, repoRoot: string): string {
|
|
252
300
|
if (process.platform !== "win32") {
|
|
253
301
|
return "";
|
|
254
302
|
}
|
|
255
303
|
|
|
256
|
-
return
|
|
304
|
+
return (
|
|
305
|
+
findWindowsNpmGlobalCodexBin(env, repoRoot, process.platform) || findVscodeCodexExe(env) || ""
|
|
306
|
+
);
|
|
257
307
|
}
|
|
258
308
|
|
|
259
|
-
function listCodexBinCandidates(
|
|
260
|
-
|
|
309
|
+
function listCodexBinCandidates(
|
|
310
|
+
env: NodeJS.ProcessEnv,
|
|
311
|
+
repoRoot: string,
|
|
312
|
+
platform: NodeJS.Platform,
|
|
313
|
+
): string[] {
|
|
314
|
+
return dedupe([
|
|
315
|
+
"codex",
|
|
316
|
+
findWindowsNpmGlobalCodexBin(env, repoRoot, platform),
|
|
317
|
+
findVscodeCodexExe(env),
|
|
318
|
+
]);
|
|
261
319
|
}
|
|
262
320
|
|
|
263
321
|
function getCodexCandidatePriority(
|
|
264
322
|
candidate: string,
|
|
265
323
|
env: NodeJS.ProcessEnv,
|
|
266
324
|
repoRoot: string,
|
|
325
|
+
platform: NodeJS.Platform,
|
|
267
326
|
): number {
|
|
268
327
|
if (candidate === "codex") {
|
|
269
328
|
return 0;
|
|
270
329
|
}
|
|
271
330
|
|
|
272
|
-
const npmGlobal = findWindowsNpmGlobalCodexBin(env, repoRoot);
|
|
331
|
+
const npmGlobal = findWindowsNpmGlobalCodexBin(env, repoRoot, platform);
|
|
273
332
|
if (npmGlobal && candidate === npmGlobal) {
|
|
274
333
|
return 1;
|
|
275
334
|
}
|
|
@@ -311,8 +370,12 @@ function findVscodeCodexExe(env: NodeJS.ProcessEnv): string {
|
|
|
311
370
|
return "";
|
|
312
371
|
}
|
|
313
372
|
|
|
314
|
-
function findWindowsNpmGlobalCodexBin(
|
|
315
|
-
|
|
373
|
+
function findWindowsNpmGlobalCodexBin(
|
|
374
|
+
env: NodeJS.ProcessEnv,
|
|
375
|
+
repoRoot: string,
|
|
376
|
+
platform: NodeJS.Platform,
|
|
377
|
+
): string {
|
|
378
|
+
if (platform !== "win32") {
|
|
316
379
|
return "";
|
|
317
380
|
}
|
|
318
381
|
|
|
@@ -350,6 +413,14 @@ function findWindowsNpmGlobalCodexBin(env: NodeJS.ProcessEnv, repoRoot: string):
|
|
|
350
413
|
return "";
|
|
351
414
|
}
|
|
352
415
|
|
|
416
|
+
function selectPathModule(filePath: string): typeof path.posix | typeof path.win32 {
|
|
417
|
+
const normalized = String(filePath ?? "").trim();
|
|
418
|
+
if (/^[A-Za-z]:[\\/]/.test(normalized) || normalized.includes("\\")) {
|
|
419
|
+
return path.win32;
|
|
420
|
+
}
|
|
421
|
+
return path.posix;
|
|
422
|
+
}
|
|
423
|
+
|
|
353
424
|
function readNpmPrefix(repoRoot: string): string {
|
|
354
425
|
const result = spawnNpm(["config", "get", "prefix"], repoRoot);
|
|
355
426
|
if (result.error || result.status !== 0) {
|
|
@@ -395,32 +466,11 @@ function runCodex({
|
|
|
395
466
|
}
|
|
396
467
|
|
|
397
468
|
function spawnCodex(codexBin: string, args: string[], repoRoot: string) {
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
402
|
-
shell: false,
|
|
403
|
-
encoding: "utf8",
|
|
404
|
-
});
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
if (process.platform === "win32" && /\.(cmd|bat)$/i.test(codexBin)) {
|
|
408
|
-
const commandLine = [
|
|
409
|
-
quoteWindowsShellValue(codexBin),
|
|
410
|
-
...args.map(quoteWindowsShellValue),
|
|
411
|
-
].join(" ");
|
|
412
|
-
return spawnSync(commandLine, {
|
|
413
|
-
cwd: repoRoot,
|
|
414
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
415
|
-
shell: true,
|
|
416
|
-
encoding: "utf8",
|
|
417
|
-
});
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
return spawnSync(codexBin, args, {
|
|
469
|
+
return spawnCodexSync({
|
|
470
|
+
codexBin,
|
|
471
|
+
args,
|
|
421
472
|
cwd: repoRoot,
|
|
422
473
|
stdio: ["ignore", "pipe", "pipe"],
|
|
423
|
-
shell: false,
|
|
424
474
|
encoding: "utf8",
|
|
425
475
|
});
|
|
426
476
|
}
|
|
@@ -493,10 +543,6 @@ function dedupe(values: Array<string | null | undefined>): string[] {
|
|
|
493
543
|
return [...new Set(values.map((value) => String(value ?? "").trim()).filter(Boolean))];
|
|
494
544
|
}
|
|
495
545
|
|
|
496
|
-
function quoteWindowsShellValue(value: string): string {
|
|
497
|
-
return `"${String(value ?? "").replace(/"/g, '\\"')}"`;
|
|
498
|
-
}
|
|
499
|
-
|
|
500
546
|
function spawnNpm(args: string[], repoRoot: string) {
|
|
501
547
|
if (process.platform === "win32") {
|
|
502
548
|
const comspec = process.env.ComSpec || "cmd.exe";
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import process from "node:process";
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
|
|
4
|
+
type StdioMode = "pipe" | "inherit" | ["ignore", "pipe", "pipe"];
|
|
5
|
+
|
|
6
|
+
type CodexSpawnSpec = {
|
|
7
|
+
command: string;
|
|
8
|
+
args: string[];
|
|
9
|
+
shell: boolean;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export function requiresNodeScriptCodexBin(command: string): boolean {
|
|
13
|
+
return /\.(cjs|mjs|js)$/i.test(String(command ?? "").trim());
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function requiresShellWrappedCodexBin(
|
|
17
|
+
command: string,
|
|
18
|
+
platform: NodeJS.Platform = process.platform,
|
|
19
|
+
): boolean {
|
|
20
|
+
return platform === "win32" && /\.(cmd|bat)$/i.test(String(command ?? "").trim());
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function buildCodexSpawnSpec({
|
|
24
|
+
codexBin,
|
|
25
|
+
args,
|
|
26
|
+
platform = process.platform,
|
|
27
|
+
nodeBin = process.execPath,
|
|
28
|
+
}: {
|
|
29
|
+
codexBin: string;
|
|
30
|
+
args: string[];
|
|
31
|
+
platform?: NodeJS.Platform;
|
|
32
|
+
nodeBin?: string;
|
|
33
|
+
}): CodexSpawnSpec {
|
|
34
|
+
if (requiresNodeScriptCodexBin(codexBin)) {
|
|
35
|
+
return {
|
|
36
|
+
command: nodeBin,
|
|
37
|
+
args: [codexBin, ...args],
|
|
38
|
+
shell: false,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (requiresShellWrappedCodexBin(codexBin, platform)) {
|
|
43
|
+
return {
|
|
44
|
+
command: [quoteWindowsShellValue(codexBin), ...args.map(quoteWindowsShellValue)].join(" "),
|
|
45
|
+
args: [],
|
|
46
|
+
shell: true,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
command: codexBin,
|
|
52
|
+
args: [...args],
|
|
53
|
+
shell: false,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function spawnCodexSync({
|
|
58
|
+
codexBin,
|
|
59
|
+
args,
|
|
60
|
+
cwd,
|
|
61
|
+
stdio,
|
|
62
|
+
encoding = "utf8",
|
|
63
|
+
input,
|
|
64
|
+
}: {
|
|
65
|
+
codexBin: string;
|
|
66
|
+
args: string[];
|
|
67
|
+
cwd: string;
|
|
68
|
+
stdio: StdioMode;
|
|
69
|
+
encoding?: BufferEncoding;
|
|
70
|
+
input?: string;
|
|
71
|
+
}) {
|
|
72
|
+
const spec = buildCodexSpawnSpec({ codexBin, args });
|
|
73
|
+
return spawnSync(spec.command, spec.args, {
|
|
74
|
+
cwd,
|
|
75
|
+
stdio,
|
|
76
|
+
shell: spec.shell,
|
|
77
|
+
encoding,
|
|
78
|
+
...(input !== undefined ? { input } : {}),
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function quoteWindowsShellValue(value: string): string {
|
|
83
|
+
return `"${String(value ?? "").replace(/"/g, '\\"')}"`;
|
|
84
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import test from "node:test";
|
|
6
|
+
import { normalizeConfiguredCodexBin } from "../dist/codex-runtime.mjs";
|
|
7
|
+
|
|
8
|
+
test("normalizeConfiguredCodexBin remaps absolute Windows npm wrappers to codex.js", () => {
|
|
9
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "copilot-hub-codex-bin-"));
|
|
10
|
+
const wrapperDir = path.join(tempDir, "npm");
|
|
11
|
+
const packageDir = path.join(wrapperDir, "node_modules", "@openai", "codex", "bin");
|
|
12
|
+
fs.mkdirSync(packageDir, { recursive: true });
|
|
13
|
+
|
|
14
|
+
const wrapperPath = path.join(wrapperDir, "codex.cmd");
|
|
15
|
+
const entrypointPath = path.join(packageDir, "codex.js");
|
|
16
|
+
fs.writeFileSync(wrapperPath, "@echo off\r\n", "utf8");
|
|
17
|
+
fs.writeFileSync(entrypointPath, "console.log('ok');\n", "utf8");
|
|
18
|
+
|
|
19
|
+
const resolved = normalizeConfiguredCodexBin({
|
|
20
|
+
value: wrapperPath,
|
|
21
|
+
env: {},
|
|
22
|
+
repoRoot: tempDir,
|
|
23
|
+
platform: "win32",
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
assert.equal(resolved, entrypointPath);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("normalizeConfiguredCodexBin resolves bare codex.cmd through detected npm install", () => {
|
|
30
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "copilot-hub-codex-bin-"));
|
|
31
|
+
const appDataDir = path.join(tempDir, "AppData", "Roaming");
|
|
32
|
+
const packageDir = path.join(appDataDir, "npm", "node_modules", "@openai", "codex", "bin");
|
|
33
|
+
fs.mkdirSync(packageDir, { recursive: true });
|
|
34
|
+
|
|
35
|
+
const entrypointPath = path.join(packageDir, "codex.js");
|
|
36
|
+
fs.writeFileSync(entrypointPath, "console.log('ok');\n", "utf8");
|
|
37
|
+
|
|
38
|
+
const resolved = normalizeConfiguredCodexBin({
|
|
39
|
+
value: "codex.cmd",
|
|
40
|
+
env: {
|
|
41
|
+
APPDATA: appDataDir,
|
|
42
|
+
},
|
|
43
|
+
repoRoot: tempDir,
|
|
44
|
+
platform: "win32",
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
assert.equal(resolved, entrypointPath);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("normalizeConfiguredCodexBin preserves non-wrapper commands", () => {
|
|
51
|
+
const resolved = normalizeConfiguredCodexBin({
|
|
52
|
+
value: "C:/tools/codex.exe",
|
|
53
|
+
env: {},
|
|
54
|
+
repoRoot: process.cwd(),
|
|
55
|
+
platform: "win32",
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
assert.equal(resolved, "C:/tools/codex.exe");
|
|
59
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import process from "node:process";
|
|
3
|
+
import test from "node:test";
|
|
4
|
+
import {
|
|
5
|
+
buildCodexSpawnSpec,
|
|
6
|
+
requiresNodeScriptCodexBin,
|
|
7
|
+
requiresShellWrappedCodexBin,
|
|
8
|
+
} from "../dist/codex-spawn.mjs";
|
|
9
|
+
|
|
10
|
+
test("requiresNodeScriptCodexBin matches js entrypoints only", () => {
|
|
11
|
+
assert.equal(requiresNodeScriptCodexBin("C:/tools/codex.js"), true);
|
|
12
|
+
assert.equal(requiresNodeScriptCodexBin("C:/tools/codex.mjs"), true);
|
|
13
|
+
assert.equal(requiresNodeScriptCodexBin("C:/tools/codex.cjs"), true);
|
|
14
|
+
assert.equal(requiresNodeScriptCodexBin("C:/tools/codex.exe"), false);
|
|
15
|
+
assert.equal(requiresNodeScriptCodexBin("C:/tools/codex.cmd"), false);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test("requiresShellWrappedCodexBin matches Windows batch launchers only", () => {
|
|
19
|
+
assert.equal(requiresShellWrappedCodexBin("C:/tools/codex.cmd", "win32"), true);
|
|
20
|
+
assert.equal(requiresShellWrappedCodexBin("C:/tools/codex.bat", "win32"), true);
|
|
21
|
+
assert.equal(requiresShellWrappedCodexBin("C:/tools/codex.exe", "win32"), false);
|
|
22
|
+
assert.equal(requiresShellWrappedCodexBin("C:/tools/codex.js", "win32"), false);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("buildCodexSpawnSpec routes js entrypoints through node", () => {
|
|
26
|
+
const spec = buildCodexSpawnSpec({
|
|
27
|
+
codexBin: "C:/Users/amine/AppData/Roaming/npm/node_modules/@openai/codex/bin/codex.js",
|
|
28
|
+
args: ["login", "status"],
|
|
29
|
+
platform: "win32",
|
|
30
|
+
nodeBin: process.execPath,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
assert.equal(spec.command, process.execPath);
|
|
34
|
+
assert.deepEqual(spec.args, [
|
|
35
|
+
"C:/Users/amine/AppData/Roaming/npm/node_modules/@openai/codex/bin/codex.js",
|
|
36
|
+
"login",
|
|
37
|
+
"status",
|
|
38
|
+
]);
|
|
39
|
+
assert.equal(spec.shell, false);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("buildCodexSpawnSpec keeps exe direct and cmd shell-wrapped", () => {
|
|
43
|
+
const exeSpec = buildCodexSpawnSpec({
|
|
44
|
+
codexBin: "C:/tools/codex.exe",
|
|
45
|
+
args: ["--version"],
|
|
46
|
+
platform: "win32",
|
|
47
|
+
});
|
|
48
|
+
assert.equal(exeSpec.command, "C:/tools/codex.exe");
|
|
49
|
+
assert.deepEqual(exeSpec.args, ["--version"]);
|
|
50
|
+
assert.equal(exeSpec.shell, false);
|
|
51
|
+
|
|
52
|
+
const cmdSpec = buildCodexSpawnSpec({
|
|
53
|
+
codexBin: "C:/tools/codex.cmd",
|
|
54
|
+
args: ["login", "status"],
|
|
55
|
+
platform: "win32",
|
|
56
|
+
});
|
|
57
|
+
assert.equal(cmdSpec.shell, true);
|
|
58
|
+
assert.equal(cmdSpec.args.length, 0);
|
|
59
|
+
assert.match(cmdSpec.command, /^"C:\/tools\/codex\.cmd" "login" "status"$/);
|
|
60
|
+
});
|