@withtruing/companion 0.13.2 → 0.13.3

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/README.md CHANGED
@@ -215,7 +215,7 @@ truing connect \
215
215
  the companion's own Codex home. This is separate from your normal Codex login:
216
216
 
217
217
  ```powershell
218
- New-Item -ItemType Directory -Force "$HOME\.product-console-companion\codex-home" | Out-Null; $env:CODEX_HOME="$HOME\.product-console-companion\codex-home"; codex login; Remove-Item Env:CODEX_HOME
218
+ New-Item -ItemType Directory -Force "$HOME\.product-console-companion\codex-home" | Out-Null; $env:CODEX_HOME="$HOME\.product-console-companion\codex-home"; codex.cmd login; Remove-Item Env:CODEX_HOME
219
219
  ```
220
220
 
221
221
  ```bash
package/dist/cli.js CHANGED
@@ -461,7 +461,7 @@ export function codexReadinessLines(status) {
461
461
  }
462
462
  return [
463
463
  'Codex needs a one-time login for the companion\'s own home:',
464
- ' Windows PowerShell: New-Item -ItemType Directory -Force "$HOME\\.product-console-companion\\codex-home" | Out-Null; $env:CODEX_HOME="$HOME\\.product-console-companion\\codex-home"; codex login; Remove-Item Env:CODEX_HOME',
464
+ ' Windows PowerShell: New-Item -ItemType Directory -Force "$HOME\\.product-console-companion\\codex-home" | Out-Null; $env:CODEX_HOME="$HOME\\.product-console-companion\\codex-home"; codex.cmd login; Remove-Item Env:CODEX_HOME',
465
465
  ' macOS/Linux: mkdir -p "$HOME/.product-console-companion/codex-home" && CODEX_HOME="$HOME/.product-console-companion/codex-home" codex login',
466
466
  'This separate login keeps your own ~/.codex instructions out of the companion\'s calls.',
467
467
  ];
@@ -0,0 +1,96 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { dirname, isAbsolute, join, resolve, sep } from 'node:path';
3
+ const defaultFs = { existsSync, readFileSync };
4
+ let cachedCommand = null;
5
+ let cachedPath;
6
+ let cachedPathExt;
7
+ function envValue(env, name) {
8
+ const key = Object.keys(env).find((candidate) => candidate.toLowerCase() === name.toLowerCase());
9
+ return key ? env[key] : undefined;
10
+ }
11
+ function windowsPathEntries(env) {
12
+ return (envValue(env, 'PATH') ?? '')
13
+ .split(';')
14
+ .map((entry) => entry.trim().replace(/^"|"$/g, ''))
15
+ .filter(Boolean);
16
+ }
17
+ function windowsExtensions(env) {
18
+ const pathExt = envValue(env, 'PATHEXT') || '.COM;.EXE;.BAT;.CMD';
19
+ return pathExt
20
+ .split(';')
21
+ .map((extension) => extension.trim().toLowerCase())
22
+ .filter(Boolean)
23
+ .map((extension) => extension.startsWith('.') ? extension : `.${extension}`);
24
+ }
25
+ function existingCandidate(directory, extension, fs) {
26
+ const lowerCaseCandidate = join(directory, `codex${extension.toLowerCase()}`);
27
+ if (fs.existsSync(lowerCaseCandidate))
28
+ return resolve(lowerCaseCandidate);
29
+ const originalCaseCandidate = join(directory, `codex${extension}`);
30
+ return fs.existsSync(originalCaseCandidate) ? resolve(originalCaseCandidate) : null;
31
+ }
32
+ function parsedShimScript(shimPath, fs) {
33
+ let contents;
34
+ try {
35
+ contents = fs.readFileSync(shimPath, 'utf8');
36
+ }
37
+ catch {
38
+ return null;
39
+ }
40
+ const shimDirectory = dirname(shimPath);
41
+ for (const match of contents.matchAll(/"([^"\r\n]*codex\.js)"/gi)) {
42
+ let target = match[1].replace(/%(?:~dp0|dp0)%?/gi, shimDirectory);
43
+ // Tests inject win32 while running on Linux CI; normalizing both separator forms keeps the
44
+ // resolver testable with a real temporary filesystem and is a no-op for native Windows paths.
45
+ target = target.replace(/[\\/]/g, sep);
46
+ if (!isAbsolute(target))
47
+ target = resolve(shimDirectory, target);
48
+ if (fs.existsSync(target))
49
+ return resolve(target);
50
+ }
51
+ return null;
52
+ }
53
+ function scriptForShim(shimPath, fs) {
54
+ const parsed = parsedShimScript(shimPath, fs);
55
+ if (parsed)
56
+ return parsed;
57
+ const conventional = join(dirname(shimPath), 'node_modules', '@openai', 'codex', 'bin', 'codex.js');
58
+ return fs.existsSync(conventional) ? resolve(conventional) : null;
59
+ }
60
+ function resolveUncached(platform, env, fs) {
61
+ if (platform !== 'win32')
62
+ return { command: 'codex', prefixArgs: [] };
63
+ const extensions = windowsExtensions(env);
64
+ for (const directory of windowsPathEntries(env)) {
65
+ for (const extension of extensions) {
66
+ if (extension !== '.cmd' && extension !== '.exe')
67
+ continue;
68
+ const candidate = existingCandidate(directory, extension, fs);
69
+ if (!candidate)
70
+ continue;
71
+ if (extension === '.exe')
72
+ return { command: candidate, prefixArgs: [] };
73
+ const script = scriptForShim(candidate, fs);
74
+ if (script)
75
+ return { command: process.execPath, prefixArgs: [script] };
76
+ }
77
+ }
78
+ return { command: 'codex', prefixArgs: [] };
79
+ }
80
+ /** Cache each production environment. Injected dependencies bypass the cache so Linux CI can
81
+ * exercise the Windows lookup rules without mutating process globals or depending on Windows
82
+ * filesystem semantics. */
83
+ export function resolveCodexCommand(options = {}) {
84
+ const injected = options.platform !== undefined || options.env !== undefined || options.fs !== undefined;
85
+ const path = envValue(process.env, 'PATH');
86
+ const pathExt = envValue(process.env, 'PATHEXT');
87
+ if (!injected && cachedCommand && path === cachedPath && pathExt === cachedPathExt)
88
+ return cachedCommand;
89
+ const command = resolveUncached(options.platform ?? process.platform, options.env ?? process.env, options.fs ?? defaultFs);
90
+ if (!injected) {
91
+ cachedCommand = command;
92
+ cachedPath = path;
93
+ cachedPathExt = pathExt;
94
+ }
95
+ return command;
96
+ }
@@ -9,6 +9,7 @@ import { basename, dirname, isAbsolute, join, relative, resolve } from 'node:pat
9
9
  import { createInterface } from 'node:readline';
10
10
  import { StringDecoder } from 'node:string_decoder';
11
11
  import { assertSingleLineArguments } from './cli-arguments.js';
12
+ import { resolveCodexCommand } from './codex-binary.js';
12
13
  import { gitErrorMessage } from './sandbox.js';
13
14
  import { stripIntroducedNulls, toCodexStrictSchema } from './strict-schema.js';
14
15
  const WALL_CLOCK_TIMEOUT_MS = 10 * 60 * 1000;
@@ -347,7 +348,8 @@ removeDirectory = rm) {
347
348
  let firstUnparseableLineFirst = '';
348
349
  let firstUnparseableLineLast = '';
349
350
  const typeCounts = new Map();
350
- const child = spawn('codex', args, {
351
+ const { command, prefixArgs } = resolveCodexCommand();
352
+ const child = spawn(command, [...prefixArgs, ...args], {
351
353
  cwd: invocationCwd,
352
354
  env: buildCodexEnvironment(codexHome),
353
355
  windowsHide: true,
@@ -522,7 +524,8 @@ export async function checkCodexLogin(codexHome) {
522
524
  throw error;
523
525
  }
524
526
  return new Promise((resolvePromise) => {
525
- const child = spawn('codex', ['login', 'status'], {
527
+ const { command, prefixArgs } = resolveCodexCommand();
528
+ const child = spawn(command, [...prefixArgs, 'login', 'status'], {
526
529
  env: buildCodexEnvironment(validatedHome),
527
530
  windowsHide: true,
528
531
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@withtruing/companion",
3
- "version": "0.13.2",
3
+ "version": "0.13.3",
4
4
  "description": "The Truing companion: reads your code and runs AI on your own computer, for Truing.",
5
5
  "type": "module",
6
6
  "license": "MIT",