@chatpanel/bridge 0.2.13 → 0.2.15
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 +4 -1
- package/src/engines/claude.js +163 -29
- package/src/env.js +151 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chatpanel/bridge",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.15",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Local bridge that exposes the AI coding agents installed on your machine — Claude Code (CLI), Codex (CLI), and Gemini CLI — to the ChatPanel Chrome extension over a localhost SSE endpoint. Bring your own agent.",
|
|
6
6
|
"keywords": [
|
|
@@ -37,6 +37,9 @@
|
|
|
37
37
|
"build:bin": "bash scripts/build-binaries.sh"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {},
|
|
40
|
+
"optionalDependencies": {
|
|
41
|
+
"@anthropic-ai/claude-agent-sdk": "^0.1.0"
|
|
42
|
+
},
|
|
40
43
|
"publishConfig": {
|
|
41
44
|
"registry": "https://registry.npmjs.org/",
|
|
42
45
|
"access": "public"
|
package/src/engines/claude.js
CHANGED
|
@@ -1,40 +1,54 @@
|
|
|
1
|
-
// Claude Code engine — drives the Claude Code CLI (`claude --print`)
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
// `claude` on PATH (npm install, native installer, or `npx`), and there's nothing
|
|
5
|
-
// to resolve inside a compiled binary (the old "/$bunfs/root/cli.js" failure).
|
|
1
|
+
// Claude Code engine — drives the Claude Code CLI (`claude --print`) the SAME
|
|
2
|
+
// way the Codex engine drives `codex exec`, but with cross-platform launching so
|
|
3
|
+
// it works no matter where `claude` lives:
|
|
6
4
|
//
|
|
7
|
-
//
|
|
8
|
-
// (
|
|
9
|
-
//
|
|
10
|
-
//
|
|
5
|
+
// • macOS / Linux / WSL-native Node — spawn the native `claude` on PATH.
|
|
6
|
+
// • Windows (native install) — run the package's cli.js with our own Node/Bun
|
|
7
|
+
// (npm's claude.cmd/.ps1 shims aren't directly spawnable → "spawn …ENOENT").
|
|
8
|
+
// • Windows host + claude only in WSL — cross the boundary via `wsl.exe`.
|
|
9
|
+
// • Last resort — the in-process Claude Agent SDK (bundled cli.js), if present.
|
|
10
|
+
//
|
|
11
|
+
// Resolution lives in env.js (resolveClaude); set CHATPANEL_CLAUDE_PATH to force
|
|
12
|
+
// a specific executable. It uses your *local* Claude Code login. By default the
|
|
13
|
+
// agent can READ your code but cannot write/run shell unless the agent's
|
|
14
|
+
// permissionMode is 'acceptEdits'/'bypassPermissions' in ChatPanel Settings.
|
|
11
15
|
|
|
12
16
|
import { spawn } from 'node:child_process';
|
|
13
17
|
import os from 'node:os';
|
|
14
18
|
import path from 'node:path';
|
|
15
|
-
import {
|
|
19
|
+
import { resolveClaude, toWslPath, isCompiledBinary } from '../env.js';
|
|
16
20
|
|
|
17
21
|
const TIMEOUT_MS = Number(process.env.CHATPANEL_CLAUDE_TIMEOUT_MS) || 180_000;
|
|
18
22
|
// Read-only tools allowed without approval in headless mode; writes/shell are
|
|
19
23
|
// gated behind the agent's permission mode.
|
|
20
24
|
const READONLY_TOOLS = ['Read', 'Grep', 'Glob', 'WebFetch', 'WebSearch', 'TodoWrite', 'Task'];
|
|
21
25
|
|
|
22
|
-
let
|
|
26
|
+
let lastReason = 'Claude Code not found.';
|
|
23
27
|
let lastProbe = 0;
|
|
28
|
+
let cachedOk = false;
|
|
24
29
|
export async function available() {
|
|
25
|
-
// Availability = "
|
|
26
|
-
// "does `claude --version` exit 0" (which fails when it just needs login).
|
|
27
|
-
if (!
|
|
30
|
+
// Availability = "can we launch claude somehow" (native / cli.js / WSL / SDK),
|
|
31
|
+
// NOT "does `claude --version` exit 0" (which fails when it just needs login).
|
|
32
|
+
if (!cachedOk && Date.now() - lastProbe > 4000) {
|
|
28
33
|
lastProbe = Date.now();
|
|
29
34
|
try {
|
|
30
|
-
|
|
35
|
+
const spec = resolveClaude();
|
|
36
|
+
if (spec) {
|
|
37
|
+
cachedOk = true;
|
|
38
|
+
} else if (!isCompiledBinary() && (await loadSdk())) {
|
|
39
|
+
cachedOk = true;
|
|
40
|
+
} else {
|
|
41
|
+
cachedOk = false;
|
|
42
|
+
lastReason =
|
|
43
|
+
process.platform === 'win32'
|
|
44
|
+
? 'Claude Code not found on Windows PATH or in WSL. Install it (npm i -g @anthropic-ai/claude-code) and run `claude` once to log in — in Windows or in your WSL distro.'
|
|
45
|
+
: 'Claude Code not found on PATH. Install it (npm i -g @anthropic-ai/claude-code) and run `claude` once to log in.';
|
|
46
|
+
}
|
|
31
47
|
} catch {
|
|
32
|
-
|
|
48
|
+
cachedOk = false;
|
|
33
49
|
}
|
|
34
50
|
}
|
|
35
|
-
return
|
|
36
|
-
? { ok: true }
|
|
37
|
-
: { ok: false, reason: 'Claude Code not found on PATH. Install it (npm i -g @anthropic-ai/claude-code) and run `claude` once to log in.' };
|
|
51
|
+
return cachedOk ? { ok: true } : { ok: false, reason: lastReason };
|
|
38
52
|
}
|
|
39
53
|
|
|
40
54
|
// The bridge is stateless, so we replay the conversation as a single prompt.
|
|
@@ -51,15 +65,51 @@ function buildPrompt(messages) {
|
|
|
51
65
|
return prompt;
|
|
52
66
|
}
|
|
53
67
|
|
|
54
|
-
//
|
|
55
|
-
// `
|
|
56
|
-
//
|
|
68
|
+
// Turn a launch spec + the claude CLI args into a concrete [bin, argv, options]
|
|
69
|
+
// for spawn(). `cwd` is the resolved working dir (Windows path on win32), or null
|
|
70
|
+
// to use the home directory.
|
|
71
|
+
function buildSpawn(spec, args, cwd) {
|
|
72
|
+
if (spec.kind === 'wsl') {
|
|
73
|
+
// Run claude inside WSL's login shell so nvm/etc. PATH resolves it. The
|
|
74
|
+
// `'exec claude "$@"'` + 'chatpanel' ($0) trick passes our args through as a
|
|
75
|
+
// proper argv array — no manual quoting, even for multi-line system prompts.
|
|
76
|
+
const pre = [];
|
|
77
|
+
if (cwd) {
|
|
78
|
+
const wslCwd = toWslPath(cwd);
|
|
79
|
+
if (wslCwd) pre.push('--cd', wslCwd); // else: run in WSL home
|
|
80
|
+
}
|
|
81
|
+
const argv = [...pre, '-e', 'bash', '-lic', 'exec claude "$@"', 'chatpanel', ...args];
|
|
82
|
+
return ['wsl.exe', argv, { stdio: ['pipe', 'pipe', 'pipe'], env: process.env, windowsHide: true }];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const spawnCwd = cwd || os.homedir();
|
|
86
|
+
const opts = { cwd: spawnCwd, stdio: ['pipe', 'pipe', 'pipe'], env: process.env, windowsHide: true };
|
|
87
|
+
if (spec.kind === 'script') {
|
|
88
|
+
// Run cli.js with the interpreter already running the bridge (node/bun).
|
|
89
|
+
return [process.execPath, [spec.script, ...args], opts];
|
|
90
|
+
}
|
|
91
|
+
if (spec.kind === 'cmd') {
|
|
92
|
+
// Launch the .cmd/.bat shim via cmd.exe with a real argv (shell:false). Node
|
|
93
|
+
// applies cmd.exe-aware quoting here, so args are passed safely — unlike
|
|
94
|
+
// spawn(..., { shell: true }), which concatenates (DEP0190).
|
|
95
|
+
return ['cmd.exe', ['/d', '/s', '/c', spec.bin, ...args], opts];
|
|
96
|
+
}
|
|
97
|
+
// kind === 'native' — a directly executable file (mac/linux binary or .exe).
|
|
98
|
+
return [spec.bin, args, opts];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Spawn claude (however it resolves) and stream its stream-json output via
|
|
102
|
+
// `emit`. Resolves with { streamedAny, resultText } once it closes 0. Returns
|
|
103
|
+
// null (no spawn) when claude can't be resolved, so the caller can fall back.
|
|
57
104
|
function runClaude({ prompt, args, cwd, emit }) {
|
|
58
|
-
const
|
|
105
|
+
const spec = resolveClaude();
|
|
106
|
+
if (!spec) return null;
|
|
107
|
+
const [bin, argv, opts] = buildSpawn(spec, args, cwd);
|
|
108
|
+
|
|
59
109
|
return new Promise((resolve, reject) => {
|
|
60
110
|
let child;
|
|
61
111
|
try {
|
|
62
|
-
child = spawn(bin,
|
|
112
|
+
child = spawn(bin, argv, opts);
|
|
63
113
|
} catch (e) {
|
|
64
114
|
return reject(new Error(`Failed to start claude: ${e.message}`));
|
|
65
115
|
}
|
|
@@ -95,7 +145,7 @@ function runClaude({ prompt, args, cwd, emit }) {
|
|
|
95
145
|
child.stderr.on('data', (d) => (stderr += d.toString()));
|
|
96
146
|
child.on('error', (e) => {
|
|
97
147
|
clearTimeout(timer);
|
|
98
|
-
reject(e);
|
|
148
|
+
reject(new Error(`Failed to start claude (${bin}): ${e.message}`));
|
|
99
149
|
});
|
|
100
150
|
child.on('close', (code) => {
|
|
101
151
|
clearTimeout(timer);
|
|
@@ -140,7 +190,8 @@ function handleMessage(msg, emit, alreadyStreamed) {
|
|
|
140
190
|
|
|
141
191
|
export async function chat({ messages, system, options }, emit) {
|
|
142
192
|
const permissionMode = options.permissionMode || 'default';
|
|
143
|
-
|
|
193
|
+
// Explicit project dir, else null → CLI runs in home (or WSL home).
|
|
194
|
+
const cwd = options.workingDir ? path.resolve(options.workingDir) : null;
|
|
144
195
|
|
|
145
196
|
const args = ['--print', '--output-format', 'stream-json', '--include-partial-messages', '--verbose'];
|
|
146
197
|
|
|
@@ -158,7 +209,9 @@ export async function chat({ messages, system, options }, emit) {
|
|
|
158
209
|
// "Use my local skills & config" off → run clean.
|
|
159
210
|
if (options.useLocalConfig === false) args.push('--setting-sources', '');
|
|
160
211
|
|
|
161
|
-
const
|
|
212
|
+
const run = runClaude({ prompt: buildPrompt(messages), args, cwd, emit });
|
|
213
|
+
if (run === null) return sdkChat({ messages, system, options }, emit); // no CLI → SDK
|
|
214
|
+
const { streamedAny, resultText } = await run;
|
|
162
215
|
emit({ type: 'done', text: streamedAny ? '' : resultText });
|
|
163
216
|
}
|
|
164
217
|
|
|
@@ -175,14 +228,16 @@ export async function complete({ prompt, system, model }) {
|
|
|
175
228
|
'--system-prompt', system || "Continue the user's text briefly. Reply with only the continuation.",
|
|
176
229
|
];
|
|
177
230
|
let text = '';
|
|
178
|
-
const
|
|
231
|
+
const run = runClaude({
|
|
179
232
|
prompt,
|
|
180
233
|
args,
|
|
181
|
-
cwd:
|
|
234
|
+
cwd: null,
|
|
182
235
|
emit: (e) => {
|
|
183
236
|
if (e.type === 'delta') text += e.text;
|
|
184
237
|
},
|
|
185
238
|
});
|
|
239
|
+
if (run === null) return sdkComplete({ prompt, system, model }); // no CLI → SDK
|
|
240
|
+
const { resultText } = await run;
|
|
186
241
|
return (text || resultText || '').trim();
|
|
187
242
|
}
|
|
188
243
|
|
|
@@ -194,3 +249,82 @@ function toolSummary(block) {
|
|
|
194
249
|
if (i.url) return i.url;
|
|
195
250
|
return '';
|
|
196
251
|
}
|
|
252
|
+
|
|
253
|
+
// ---------------------------------------------------------------------------
|
|
254
|
+
// Fallback: in-process Claude Agent SDK. Only reached when no native/WSL claude
|
|
255
|
+
// CLI is resolvable (and we're not a compiled binary, where its bundled cli.js
|
|
256
|
+
// is unreachable). The SDK ships its own cli.js and uses your ~/.claude login.
|
|
257
|
+
|
|
258
|
+
let sdkPromise = null;
|
|
259
|
+
function loadSdk() {
|
|
260
|
+
// Optional dependency — absent in lean/compiled installs; import().catch makes
|
|
261
|
+
// that a graceful "no fallback available" rather than a crash.
|
|
262
|
+
if (!sdkPromise) sdkPromise = import('@anthropic-ai/claude-agent-sdk').catch(() => null);
|
|
263
|
+
return sdkPromise;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
async function sdkChat({ messages, system, options }, emit) {
|
|
267
|
+
const sdk = await loadSdk();
|
|
268
|
+
if (!sdk) throw new Error(lastReason);
|
|
269
|
+
const { query } = sdk;
|
|
270
|
+
|
|
271
|
+
const permissionMode = options.permissionMode || 'default';
|
|
272
|
+
const cwd = options.workingDir ? path.resolve(options.workingDir) : os.homedir();
|
|
273
|
+
const writesAllowed = permissionMode === 'acceptEdits' || permissionMode === 'bypassPermissions';
|
|
274
|
+
const readonly = new Set(READONLY_TOOLS);
|
|
275
|
+
const canUseTool = async (toolName) =>
|
|
276
|
+
readonly.has(toolName) || writesAllowed
|
|
277
|
+
? { behavior: 'allow', updatedInput: undefined }
|
|
278
|
+
: { behavior: 'deny', message: `${toolName} blocked — set this agent's permission mode in ChatPanel to enable it.` };
|
|
279
|
+
|
|
280
|
+
let streamedAny = false;
|
|
281
|
+
let resultText = '';
|
|
282
|
+
const iterator = query({
|
|
283
|
+
prompt: buildPrompt(messages),
|
|
284
|
+
options: {
|
|
285
|
+
cwd,
|
|
286
|
+
permissionMode,
|
|
287
|
+
includePartialMessages: true,
|
|
288
|
+
canUseTool,
|
|
289
|
+
settingSources: options.useLocalConfig === false ? [] : ['user', 'project'],
|
|
290
|
+
systemPrompt: system
|
|
291
|
+
? { type: 'preset', preset: 'claude_code', append: system }
|
|
292
|
+
: { type: 'preset', preset: 'claude_code' },
|
|
293
|
+
...(options.model ? { model: options.model } : {}),
|
|
294
|
+
...(process.env.CHATPANEL_MAX_TURNS ? { maxTurns: Number(process.env.CHATPANEL_MAX_TURNS) } : {}),
|
|
295
|
+
},
|
|
296
|
+
});
|
|
297
|
+
for await (const message of iterator) {
|
|
298
|
+
const r = handleMessage(message, emit, streamedAny);
|
|
299
|
+
if (r.streamed) streamedAny = true;
|
|
300
|
+
if (r.result != null) resultText = r.result;
|
|
301
|
+
}
|
|
302
|
+
emit({ type: 'done', text: streamedAny ? '' : resultText });
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
async function sdkComplete({ prompt, system, model }) {
|
|
306
|
+
const sdk = await loadSdk();
|
|
307
|
+
if (!sdk) throw new Error(lastReason);
|
|
308
|
+
const { query } = sdk;
|
|
309
|
+
let text = '';
|
|
310
|
+
const iterator = query({
|
|
311
|
+
prompt,
|
|
312
|
+
options: {
|
|
313
|
+
cwd: os.homedir(),
|
|
314
|
+
permissionMode: 'default',
|
|
315
|
+
allowedTools: [],
|
|
316
|
+
maxTurns: 1,
|
|
317
|
+
settingSources: [],
|
|
318
|
+
systemPrompt: system || "Continue the user's text briefly. Reply with only the continuation.",
|
|
319
|
+
model: model || 'haiku',
|
|
320
|
+
},
|
|
321
|
+
});
|
|
322
|
+
for await (const message of iterator) {
|
|
323
|
+
if (message.type === 'assistant') {
|
|
324
|
+
for (const block of message.message.content) if (block.type === 'text') text += block.text;
|
|
325
|
+
} else if (message.type === 'result' && message.subtype === 'success' && !text) {
|
|
326
|
+
text = message.result || '';
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
return text.trim();
|
|
330
|
+
}
|
package/src/env.js
CHANGED
|
@@ -8,12 +8,13 @@
|
|
|
8
8
|
import os from 'node:os';
|
|
9
9
|
import path from 'node:path';
|
|
10
10
|
import { spawnSync } from 'node:child_process';
|
|
11
|
-
import { readdirSync, existsSync } from 'node:fs';
|
|
11
|
+
import { readdirSync, readFileSync, existsSync } from 'node:fs';
|
|
12
12
|
|
|
13
13
|
let enriched = false;
|
|
14
14
|
|
|
15
|
-
// The agent CLIs the bridge shells out to
|
|
16
|
-
|
|
15
|
+
// The agent CLIs the bridge shells out to. Claude has its own richer resolution
|
|
16
|
+
// (resolveClaude: native / cli.js / WSL / SDK) below.
|
|
17
|
+
const AGENT_CLIS = ['codex', 'gemini', 'claude'];
|
|
17
18
|
|
|
18
19
|
// Is `name` executable somewhere on the current PATH?
|
|
19
20
|
function onPath(name) {
|
|
@@ -64,6 +65,153 @@ function shellWhich(name) {
|
|
|
64
65
|
}
|
|
65
66
|
}
|
|
66
67
|
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
// Claude Code launcher resolution.
|
|
70
|
+
//
|
|
71
|
+
// The Codex/Gemini engines can assume `spawn('codex', …)` runs a directly
|
|
72
|
+
// executable file on the current OS's PATH. Claude needs more care:
|
|
73
|
+
// • On Windows, npm installs `claude.cmd` / `claude.ps1` / an extensionless
|
|
74
|
+
// bash shim — NONE of which Node's spawn() can execute directly (that's the
|
|
75
|
+
// "spawn C:\… ENOENT"). The runnable thing is the package's `cli.js`, which
|
|
76
|
+
// we run with our own Node/Bun.
|
|
77
|
+
// • A very common setup is "Windows host, `claude` only installed inside WSL."
|
|
78
|
+
// A Windows process can't see WSL's filesystem or PATH, so we cross the
|
|
79
|
+
// boundary explicitly via `wsl.exe`.
|
|
80
|
+
// On macOS/Linux (and WSL-native Node) none of this applies: we return the same
|
|
81
|
+
// native binary the old code spawned, so behavior there is unchanged.
|
|
82
|
+
//
|
|
83
|
+
// Returns one of:
|
|
84
|
+
// { kind: 'native', bin } → spawn(bin, args) (directly exec'able)
|
|
85
|
+
// { kind: 'script', script } → spawn(process.execPath, [script, ...args])
|
|
86
|
+
// { kind: 'cmd', bin } → spawn('cmd.exe', ['/c', bin, ...args])
|
|
87
|
+
// { kind: 'wsl' } → spawn('wsl.exe', [wsl prefix, ...args])
|
|
88
|
+
// null → not found (caller may fall back to SDK)
|
|
89
|
+
//
|
|
90
|
+
// We never use spawn's `shell: true` — with an args array it concatenates rather
|
|
91
|
+
// than escapes (Node DEP0190 / a real injection surface). The 'script' and 'cmd'
|
|
92
|
+
// kinds run the shim safely with a proper argv instead.
|
|
93
|
+
export function resolveClaude() {
|
|
94
|
+
const override = process.env.CHATPANEL_CLAUDE_PATH;
|
|
95
|
+
if (override) {
|
|
96
|
+
const ext = path.extname(override).toLowerCase();
|
|
97
|
+
if (!isCompiledBinary() && /^\.(c?js|mjs)$/.test(ext)) return { kind: 'script', script: override };
|
|
98
|
+
if (process.platform === 'win32' && (ext === '.cmd' || ext === '.bat')) return { kind: 'cmd', bin: override };
|
|
99
|
+
return { kind: 'native', bin: override };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (process.platform === 'win32') {
|
|
103
|
+
const win = findClaudeWindows();
|
|
104
|
+
if (win) return win;
|
|
105
|
+
if (claudeInWsl()) return { kind: 'wsl' };
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// macOS / Linux / WSL-native: same resolution the engine used before.
|
|
110
|
+
const bin = findAgentBin('claude');
|
|
111
|
+
return bin ? { kind: 'native', bin } : null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Locate a runnable Claude Code on Windows. Prefer the package's cli.js (run
|
|
115
|
+
// with our own Node/Bun — clean arg passing, no cmd.exe quoting), then a real
|
|
116
|
+
// .exe, then a .cmd/.bat shim launched safely via cmd.exe.
|
|
117
|
+
function findClaudeWindows() {
|
|
118
|
+
const dirs = (process.env.PATH || '').split(path.delimiter);
|
|
119
|
+
for (const d of dirs) {
|
|
120
|
+
if (!d) continue;
|
|
121
|
+
const hasShim = ['claude', 'claude.cmd', 'claude.exe', 'claude.ps1', 'claude.bat'].some((n) =>
|
|
122
|
+
existsSync(path.join(d, n)),
|
|
123
|
+
);
|
|
124
|
+
if (!hasShim) continue;
|
|
125
|
+
// Running cli.js with our own interpreter only works under a real Node/Bun,
|
|
126
|
+
// not inside a compiled single-file binary (which is not a JS interpreter).
|
|
127
|
+
if (!isCompiledBinary()) {
|
|
128
|
+
const js = claudeCliJs(d) || shimTarget(d);
|
|
129
|
+
if (js) return { kind: 'script', script: js };
|
|
130
|
+
}
|
|
131
|
+
if (existsSync(path.join(d, 'claude.exe'))) return { kind: 'native', bin: path.join(d, 'claude.exe') };
|
|
132
|
+
if (existsSync(path.join(d, 'claude.cmd'))) return { kind: 'cmd', bin: path.join(d, 'claude.cmd') };
|
|
133
|
+
if (existsSync(path.join(d, 'claude.bat'))) return { kind: 'cmd', bin: path.join(d, 'claude.bat') };
|
|
134
|
+
}
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// The npm shim usually sits next to (or one level up from) the claude-code
|
|
139
|
+
// package — quick static guesses before parsing the shim itself.
|
|
140
|
+
function claudeCliJs(dir) {
|
|
141
|
+
const rels = [
|
|
142
|
+
['node_modules', '@anthropic-ai', 'claude-code', 'cli.js'],
|
|
143
|
+
['..', 'node_modules', '@anthropic-ai', 'claude-code', 'cli.js'],
|
|
144
|
+
['..', 'lib', 'node_modules', '@anthropic-ai', 'claude-code', 'cli.js'],
|
|
145
|
+
];
|
|
146
|
+
for (const r of rels) {
|
|
147
|
+
const c = path.join(dir, ...r);
|
|
148
|
+
if (existsSync(c)) return c;
|
|
149
|
+
}
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Robust fallback: every npm/pnpm/yarn/volta shim literally names the JS entry it
|
|
154
|
+
// runs, relative to the shim dir (`%dp0%\…\cli.js` in .cmd, `$basedir/…/cli.js`
|
|
155
|
+
// in the sh/.ps1 shims). Extract that so any install layout resolves to a real
|
|
156
|
+
// cli.js we can run with our own interpreter.
|
|
157
|
+
function shimTarget(dir) {
|
|
158
|
+
for (const shim of ['claude.cmd', 'claude', 'claude.ps1']) {
|
|
159
|
+
let txt;
|
|
160
|
+
try {
|
|
161
|
+
txt = readFileSync(path.join(dir, shim), 'utf8');
|
|
162
|
+
} catch {
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
const m = txt.match(/(?:%~?dp0%|\$basedir|\$\{basedir\})[\\/]+([^"'\s]+\.[cm]?js)/i);
|
|
166
|
+
if (m) {
|
|
167
|
+
const abs = path.join(dir, m[1].replace(/[\\/]+/g, path.sep));
|
|
168
|
+
if (existsSync(abs)) return abs;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Is `claude` reachable inside the default WSL distro's login shell? Cached;
|
|
175
|
+
// re-probed (throttled) while not found so it self-heals once WSL/claude appear.
|
|
176
|
+
let wslClaude = null;
|
|
177
|
+
let wslProbe = 0;
|
|
178
|
+
function claudeInWsl() {
|
|
179
|
+
if (wslClaude === null || (!wslClaude && Date.now() - wslProbe > 4000)) {
|
|
180
|
+
wslProbe = Date.now();
|
|
181
|
+
try {
|
|
182
|
+
const r = spawnSync('wsl.exe', ['-e', 'bash', '-lic', 'command -v claude'], {
|
|
183
|
+
encoding: 'utf8',
|
|
184
|
+
timeout: 8000,
|
|
185
|
+
windowsHide: true,
|
|
186
|
+
});
|
|
187
|
+
wslClaude = r.status === 0 && /\S/.test(stripBom(r.stdout || ''));
|
|
188
|
+
} catch {
|
|
189
|
+
wslClaude = false;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return wslClaude;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Translate a Windows path to its WSL (/mnt/c/…) equivalent. Returns null on
|
|
196
|
+
// failure so the caller can just run in WSL's home instead.
|
|
197
|
+
export function toWslPath(winPath) {
|
|
198
|
+
try {
|
|
199
|
+
const r = spawnSync('wsl.exe', ['-e', 'wslpath', '-a', winPath], {
|
|
200
|
+
encoding: 'utf8',
|
|
201
|
+
timeout: 5000,
|
|
202
|
+
windowsHide: true,
|
|
203
|
+
});
|
|
204
|
+
const out = stripBom(r.stdout || '').trim();
|
|
205
|
+
return out.startsWith('/') ? out : null;
|
|
206
|
+
} catch {
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function stripBom(s) {
|
|
212
|
+
return s.replace(/^/, '').trim();
|
|
213
|
+
}
|
|
214
|
+
|
|
67
215
|
// Version managers install CLIs under versioned bin dirs that a lazy-loaded
|
|
68
216
|
// shell (nvm/fnm) doesn't export into a non-interactive service PATH. Add them.
|
|
69
217
|
function versionManagerBins(home) {
|