@adhdev/daemon-core 0.9.82-rc.317 → 0.9.82-rc.318

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/daemon-core",
3
- "version": "0.9.82-rc.317",
3
+ "version": "0.9.82-rc.318",
4
4
  "description": "ADHDev daemon core — CDP, IDE detection, providers, command execution",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -46,7 +46,7 @@
46
46
  "author": "vilmire",
47
47
  "license": "AGPL-3.0-or-later",
48
48
  "dependencies": {
49
- "@adhdev/mesh-shared": "0.9.82-rc.317",
49
+ "@adhdev/mesh-shared": "0.9.82-rc.318",
50
50
  "@adhdev/session-host-core": "*",
51
51
  "@agentclientprotocol/sdk": "^0.16.1",
52
52
  "ajv": "^8.20.0",
@@ -6,6 +6,42 @@ import * as path from 'path';
6
6
  // need a cmd.exe wrapper).
7
7
  const DIRECT_EXEC_EXT = new Set(['.exe', '.com']);
8
8
 
9
+ // Executable extensions to probe when scanning a directory ourselves, ordered
10
+ // most-directly-launchable first. node-pty's ConPTY backend launches an
11
+ // absolute `.cmd`/`.bat` shim fine (verified) — it only fails to *resolve* a
12
+ // bare command against an incomplete PATH — so once we hand it an absolute
13
+ // path, a `.cmd` shim works just as well as a real `.exe`.
14
+ const WIN_EXEC_EXT = ['.exe', '.com', '.cmd', '.bat'];
15
+
16
+ /**
17
+ * Resolve a bare command against well-known global-bin directories that are
18
+ * frequently NOT on the daemon's inherited PATH, so `where` (which only
19
+ * searches PATH) misses them. A daemon running under one Node install (e.g.
20
+ * nvm) never sees another npm prefix's bin dir — notably npm's Windows default
21
+ * prefix at %APPDATA%\npm, where `npm i -g @openai/codex` lands. Returns an
22
+ * absolute path on the first hit, or null. Mirrors findBinary()'s extraDirs in
23
+ * provider-cli-shared.ts; kept inline here so this lightweight module (loaded
24
+ * by pty-transport) need not pull in the heavier shared module.
25
+ */
26
+ function resolveWin32GlobalBin(trimmed: string): string | null {
27
+ // Only resolve a bare command name — anything with a path separator is the
28
+ // caller's explicit location and must not be re-pointed at a global bin dir.
29
+ if (path.isAbsolute(trimmed) || trimmed.includes('/') || trimmed.includes('\\')) {
30
+ return null;
31
+ }
32
+ const extraDirs: string[] = [];
33
+ if (process.env.APPDATA) extraDirs.push(path.join(process.env.APPDATA, 'npm'));
34
+ try { extraDirs.push(path.dirname(process.execPath)); } catch { /* best-effort */ }
35
+ for (const dir of extraDirs) {
36
+ if (!dir) continue;
37
+ for (const ext of WIN_EXEC_EXT) {
38
+ const full = path.join(dir, trimmed + ext);
39
+ if (existsSync(full)) return full;
40
+ }
41
+ }
42
+ return null;
43
+ }
44
+
9
45
  /**
10
46
  * Resolve a launch command to an absolute executable path on Windows.
11
47
  *
@@ -39,7 +75,16 @@ export function resolveWin32Executable(command: string): string {
39
75
  return direct || matches[0] || command;
40
76
  }
41
77
  } catch {
42
- // `where` not found / non-zero exit — fall through to original command.
78
+ // `where` not found / non-zero exit — fall through to the global-bin scan.
43
79
  }
80
+
81
+ // `where` found nothing on PATH. Before giving up (and letting node-pty crash
82
+ // with "File not found:" on the bare command), search npm's off-PATH global
83
+ // bin dir(s). This is the codex case: `npm i -g @openai/codex` installs to
84
+ // %APPDATA%\npm, which is absent from a nvm-launched daemon's PATH, so a spec
85
+ // binary of "codex" never resolved and the spawn ENOENT'd.
86
+ const globalBin = resolveWin32GlobalBin(trimmed);
87
+ if (globalBin) return globalBin;
88
+
44
89
  return command;
45
90
  }
@@ -12,6 +12,7 @@ import * as os from 'os';
12
12
  import * as path from 'path';
13
13
  import { existsSync } from 'fs';
14
14
  import type { ProviderLoader } from '../providers/provider-loader.js';
15
+ import { findBinary } from '../cli-adapters/provider-cli-shared.js';
15
16
 
16
17
  export interface CLIInfo {
17
18
  id: string;
@@ -57,6 +58,29 @@ function resolveCommandPath(command: string): string | null {
57
58
  return null;
58
59
  }
59
60
 
61
+ /**
62
+ * Resolve a CLI command to an absolute path for install detection.
63
+ * Order: explicit path → PATH (`where`/`which`) → well-known global-bin dirs
64
+ * (e.g. %APPDATA%\npm) via the shared spawn-layer findBinary.
65
+ *
66
+ * The final step keeps detection consistent with the spawn layer: findBinary
67
+ * already searches npm's default Windows prefix at %APPDATA%\npm (where
68
+ * `npm i -g @openai/codex` lands), so a CLI installed under an npm prefix that
69
+ * is NOT on the daemon's inherited PATH is detected as installed instead of
70
+ * being blocked at the launch gate. findBinary returns a bare "<name>.cmd"
71
+ * (non-absolute) when nothing is found, so we only accept an absolute path
72
+ * that actually exists.
73
+ */
74
+ async function resolveDetectionPath(command: string, whichCmd: string): Promise<string | null> {
75
+ const explicitPath = resolveCommandPath(command);
76
+ if (explicitPath) return explicitPath;
77
+ const whichResult = await execAsync(`${whichCmd} ${shellQuote(command)}`);
78
+ if (whichResult) return whichResult.split('\n')[0];
79
+ const resolved = findBinary(command);
80
+ if (path.isAbsolute(resolved) && existsSync(resolved)) return resolved;
81
+ return null;
82
+ }
83
+
60
84
  /** Run a shell command with timeout, returning stdout or null on failure */
61
85
  function execAsync(cmd: string, timeoutMs = 5000): Promise<string | null> {
62
86
  return new Promise((resolve) => {
@@ -97,11 +121,8 @@ export async function detectCLIs(
97
121
  const results = await Promise.all(
98
122
  cliList.map(async (cli): Promise<CLIInfo> => {
99
123
  try {
100
- const explicitPath = resolveCommandPath(cli.command);
101
- const pathResult = explicitPath || await execAsync(`${whichCmd} ${shellQuote(cli.command)}`);
102
- if (!pathResult) return { ...cli, installed: false };
103
-
104
- const firstPath = explicitPath || pathResult.split('\n')[0];
124
+ const firstPath = await resolveDetectionPath(cli.command, whichCmd);
125
+ if (!firstPath) return { ...cli, installed: false };
105
126
 
106
127
  // Get version (parallel with other checks)
107
128
  let version: string | undefined;
@@ -148,10 +169,8 @@ export async function detectCLI(
148
169
  const platform = os.platform();
149
170
  const whichCmd = platform === 'win32' ? 'where' : 'which';
150
171
  try {
151
- const explicitPath = resolveCommandPath(target.command);
152
- const pathResult = explicitPath || await execAsync(`${whichCmd} ${shellQuote(target.command)}`);
153
- if (!pathResult) return null;
154
- const firstPath = explicitPath || pathResult.split('\n')[0];
172
+ const firstPath = await resolveDetectionPath(target.command, whichCmd);
173
+ if (!firstPath) return null;
155
174
  let version: string | undefined;
156
175
  if (options?.includeVersion !== false) {
157
176
  const versionCommands = [