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.
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "copilot-hub",
3
- "version": "0.1.25",
3
+ "version": "0.1.28",
4
4
  "description": "Copilot Hub CLI and runtime bundle",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -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 = process.platform === "win32" && /\.(cmd|bat)$/i.test(String(codexBin ?? ""))
371
- ? spawnSync([
372
- quoteWindowsShellValue(codexBin),
373
- ...args.map((arg) => quoteWindowsShellValue(arg)),
374
- ].join(" "), {
375
- cwd: repoRoot,
376
- stdio,
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 normalized = String(value ?? "")
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: value,
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(["codex", findWindowsNpmGlobalCodexBin(env, repoRoot), findVscodeCodexExe(env)]);
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 (process.platform !== "win32") {
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
- if (/\.(cjs|mjs|js)$/i.test(codexBin)) {
266
- return spawnSync(process.execPath, [codexBin, ...args], {
267
- cwd: repoRoot,
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
+ }
@@ -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
- process.platform === "win32" && /\.(cmd|bat)$/i.test(String(codexBin ?? ""))
448
- ? spawnSync(
449
- [
450
- quoteWindowsShellValue(codexBin),
451
- ...args.map((arg) => quoteWindowsShellValue(arg)),
452
- ].join(" "),
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 normalized = String(value ?? "")
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: value,
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 findWindowsNpmGlobalCodexBin(env, repoRoot) || findVscodeCodexExe(env) || "";
304
+ return (
305
+ findWindowsNpmGlobalCodexBin(env, repoRoot, process.platform) || findVscodeCodexExe(env) || ""
306
+ );
257
307
  }
258
308
 
259
- function listCodexBinCandidates(env: NodeJS.ProcessEnv, repoRoot: string): string[] {
260
- return dedupe(["codex", findWindowsNpmGlobalCodexBin(env, repoRoot), findVscodeCodexExe(env)]);
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(env: NodeJS.ProcessEnv, repoRoot: string): string {
315
- if (process.platform !== "win32") {
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
- if (/\.(cjs|mjs|js)$/i.test(codexBin)) {
399
- return spawnSync(process.execPath, [codexBin, ...args], {
400
- cwd: repoRoot,
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
+ });