copilot-hub 0.1.26 → 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.26",
3
+ "version": "0.1.28",
4
4
  "description": "Copilot Hub CLI and runtime bundle",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -42,9 +42,9 @@ export function resolveCodexBinForStart({ repoRoot, agentEngineEnvPath, controlP
42
42
  userConfigured: false,
43
43
  };
44
44
  }
45
- export function resolveCompatibleInstalledCodexBin({ repoRoot, env = process.env, }) {
45
+ export function resolveCompatibleInstalledCodexBin({ repoRoot, env = process.env, platform = process.platform, }) {
46
46
  const matches = [];
47
- for (const candidate of listCodexBinCandidates(env, repoRoot)) {
47
+ for (const candidate of listCodexBinCandidates(env, repoRoot, platform)) {
48
48
  const probe = probeCodexVersion({
49
49
  codexBin: candidate,
50
50
  repoRoot,
@@ -55,7 +55,7 @@ export function resolveCompatibleInstalledCodexBin({ repoRoot, env = process.env
55
55
  matches.push({
56
56
  candidate,
57
57
  version: probe.version,
58
- priority: getCodexCandidatePriority(candidate, env, repoRoot),
58
+ priority: getCodexCandidatePriority(candidate, env, repoRoot, platform),
59
59
  });
60
60
  }
61
61
  if (matches.length === 0) {
@@ -134,12 +134,17 @@ export function buildCodexCompatibilityError({ resolved, probe, includeInstallHi
134
134
  return lines.join("\n");
135
135
  }
136
136
  function buildResolvedCodexBin({ value, source, env, repoRoot, }) {
137
- const normalized = String(value ?? "")
137
+ const normalizedValue = normalizeConfiguredCodexBin({
138
+ value,
139
+ env,
140
+ repoRoot,
141
+ });
142
+ const normalized = String(normalizedValue ?? "")
138
143
  .trim()
139
144
  .toLowerCase();
140
145
  if (normalized && normalized !== "codex") {
141
146
  return {
142
- bin: value,
147
+ bin: normalizedValue,
143
148
  source,
144
149
  userConfigured: true,
145
150
  };
@@ -151,20 +156,43 @@ function buildResolvedCodexBin({ value, source, env, repoRoot, }) {
151
156
  userConfigured: false,
152
157
  };
153
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
+ }
154
178
  function findDetectedCodexBin(env, repoRoot) {
155
179
  if (process.platform !== "win32") {
156
180
  return "";
157
181
  }
158
- return findWindowsNpmGlobalCodexBin(env, repoRoot) || findVscodeCodexExe(env) || "";
182
+ return (findWindowsNpmGlobalCodexBin(env, repoRoot, process.platform) || findVscodeCodexExe(env) || "");
159
183
  }
160
- function listCodexBinCandidates(env, repoRoot) {
161
- 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
+ ]);
162
190
  }
163
- function getCodexCandidatePriority(candidate, env, repoRoot) {
191
+ function getCodexCandidatePriority(candidate, env, repoRoot, platform) {
164
192
  if (candidate === "codex") {
165
193
  return 0;
166
194
  }
167
- const npmGlobal = findWindowsNpmGlobalCodexBin(env, repoRoot);
195
+ const npmGlobal = findWindowsNpmGlobalCodexBin(env, repoRoot, platform);
168
196
  if (npmGlobal && candidate === npmGlobal) {
169
197
  return 1;
170
198
  }
@@ -198,8 +226,8 @@ function findVscodeCodexExe(env) {
198
226
  }
199
227
  return "";
200
228
  }
201
- function findWindowsNpmGlobalCodexBin(env, repoRoot) {
202
- if (process.platform !== "win32") {
229
+ function findWindowsNpmGlobalCodexBin(env, repoRoot, platform) {
230
+ if (platform !== "win32") {
203
231
  return "";
204
232
  }
205
233
  const packageRoots = [];
@@ -231,6 +259,13 @@ function findWindowsNpmGlobalCodexBin(env, repoRoot) {
231
259
  }
232
260
  return "";
233
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
+ }
234
269
  function readNpmPrefix(repoRoot) {
235
270
  const result = spawnNpm(["config", "get", "prefix"], repoRoot);
236
271
  if (result.error || result.status !== 0) {
@@ -82,13 +82,15 @@ export function resolveCodexBinForStart({
82
82
  export function resolveCompatibleInstalledCodexBin({
83
83
  repoRoot,
84
84
  env = process.env,
85
+ platform = process.platform,
85
86
  }: {
86
87
  repoRoot: string;
87
88
  env?: NodeJS.ProcessEnv;
89
+ platform?: NodeJS.Platform;
88
90
  }): string {
89
91
  const matches: Array<{ candidate: string; version: string; priority: number }> = [];
90
92
 
91
- for (const candidate of listCodexBinCandidates(env, repoRoot)) {
93
+ for (const candidate of listCodexBinCandidates(env, repoRoot, platform)) {
92
94
  const probe = probeCodexVersion({
93
95
  codexBin: candidate,
94
96
  repoRoot,
@@ -100,7 +102,7 @@ export function resolveCompatibleInstalledCodexBin({
100
102
  matches.push({
101
103
  candidate,
102
104
  version: probe.version,
103
- priority: getCodexCandidatePriority(candidate, env, repoRoot),
105
+ priority: getCodexCandidatePriority(candidate, env, repoRoot, platform),
104
106
  });
105
107
  }
106
108
 
@@ -230,12 +232,17 @@ function buildResolvedCodexBin({
230
232
  env: NodeJS.ProcessEnv;
231
233
  repoRoot: string;
232
234
  }): ResolvedCodexBin {
233
- const normalized = String(value ?? "")
235
+ const normalizedValue = normalizeConfiguredCodexBin({
236
+ value,
237
+ env,
238
+ repoRoot,
239
+ });
240
+ const normalized = String(normalizedValue ?? "")
234
241
  .trim()
235
242
  .toLowerCase();
236
243
  if (normalized && normalized !== "codex") {
237
244
  return {
238
- bin: value,
245
+ bin: normalizedValue,
239
246
  source,
240
247
  userConfigured: true,
241
248
  };
@@ -249,28 +256,79 @@ function buildResolvedCodexBin({
249
256
  };
250
257
  }
251
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
+
252
299
  function findDetectedCodexBin(env: NodeJS.ProcessEnv, repoRoot: string): string {
253
300
  if (process.platform !== "win32") {
254
301
  return "";
255
302
  }
256
303
 
257
- return findWindowsNpmGlobalCodexBin(env, repoRoot) || findVscodeCodexExe(env) || "";
304
+ return (
305
+ findWindowsNpmGlobalCodexBin(env, repoRoot, process.platform) || findVscodeCodexExe(env) || ""
306
+ );
258
307
  }
259
308
 
260
- function listCodexBinCandidates(env: NodeJS.ProcessEnv, repoRoot: string): string[] {
261
- 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
+ ]);
262
319
  }
263
320
 
264
321
  function getCodexCandidatePriority(
265
322
  candidate: string,
266
323
  env: NodeJS.ProcessEnv,
267
324
  repoRoot: string,
325
+ platform: NodeJS.Platform,
268
326
  ): number {
269
327
  if (candidate === "codex") {
270
328
  return 0;
271
329
  }
272
330
 
273
- const npmGlobal = findWindowsNpmGlobalCodexBin(env, repoRoot);
331
+ const npmGlobal = findWindowsNpmGlobalCodexBin(env, repoRoot, platform);
274
332
  if (npmGlobal && candidate === npmGlobal) {
275
333
  return 1;
276
334
  }
@@ -312,8 +370,12 @@ function findVscodeCodexExe(env: NodeJS.ProcessEnv): string {
312
370
  return "";
313
371
  }
314
372
 
315
- function findWindowsNpmGlobalCodexBin(env: NodeJS.ProcessEnv, repoRoot: string): string {
316
- if (process.platform !== "win32") {
373
+ function findWindowsNpmGlobalCodexBin(
374
+ env: NodeJS.ProcessEnv,
375
+ repoRoot: string,
376
+ platform: NodeJS.Platform,
377
+ ): string {
378
+ if (platform !== "win32") {
317
379
  return "";
318
380
  }
319
381
 
@@ -351,6 +413,14 @@ function findWindowsNpmGlobalCodexBin(env: NodeJS.ProcessEnv, repoRoot: string):
351
413
  return "";
352
414
  }
353
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
+
354
424
  function readNpmPrefix(repoRoot: string): string {
355
425
  const result = spawnNpm(["config", "get", "prefix"], repoRoot);
356
426
  if (result.error || result.status !== 0) {
@@ -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
+ });