Built-in design generation now runs as an agentic MCP tool-loop (reusing the agent-rs BuiltInProvider), gated behind OPENPENCIL_DESIGN_AGENT_LOOP / the Settings experimental toggle; the orchestrator stays the default. - design-agent system prompt + in-process design toolset (parity-locked with the MCP surface) + flag-gated Intent::Design routing - spawn_agents execution as sequential sub-loops + live creation-mode badges (per-agent glow + 'N/M designing...' header) - new MCP tools: get_guidelines, ToolSearch, get_screenshot, get_editor_state, export_nodes, spawn_agents; style-guide local audit - #27 AI panel restyle: rounded tool cards + green check-rings, gray user bubbles, model-pill bottom toolbar, header, empty-state pills, the PARALLEL AGENTS (agent_team_size) 1x-6x chip dropdown - multi-chat tabs: ChatSessions model (Deref-to-active) + tab row UI (switch / close / + / Cmd+T) with each run bound to its tab Large checkpoint commit spanning the working tree (Rust shell crates).
119 lines
4.6 KiB
TypeScript
119 lines
4.6 KiB
TypeScript
import { execSync } from 'node:child_process';
|
|
import { existsSync } from 'node:fs';
|
|
import { homedir, platform } from 'node:os';
|
|
import { join } from 'node:path';
|
|
import { serverLog } from './server-logger';
|
|
import { posixUserBinDirs, probeViaLoginShell } from './cli-resolver-helpers';
|
|
|
|
const isWindows = platform() === 'win32';
|
|
|
|
/** Windows npm global installs may create .cmd or .ps1 wrappers — try both */
|
|
function winNpmCandidates(dir: string, name: string): string[] {
|
|
return [join(dir, `${name}.cmd`), join(dir, `${name}.ps1`)];
|
|
}
|
|
|
|
/** On Windows, `where` may return an extensionless shell script — prefer .cmd/.ps1/.exe */
|
|
function resolveWinExtension(binPath: string): string {
|
|
if (!isWindows) return binPath;
|
|
if (/\.(cmd|ps1|exe)$/i.test(binPath)) return binPath;
|
|
for (const ext of ['.cmd', '.ps1', '.exe']) {
|
|
if (existsSync(binPath + ext)) return binPath + ext;
|
|
}
|
|
return binPath;
|
|
}
|
|
|
|
/**
|
|
* Resolve the absolute path to the standalone `claude` binary.
|
|
*
|
|
* When Nitro bundles @anthropic-ai/claude-agent-sdk, the SDK's internal
|
|
* `import.meta.url`-based resolution to find its own `cli.js` breaks.
|
|
* Instead we locate the standalone native binary and pass it via
|
|
* `pathToClaudeCodeExecutable` — the SDK detects non-.js paths as native
|
|
* binaries and spawns them directly (no `node` wrapper needed).
|
|
*/
|
|
export function resolveClaudeCli(): string | undefined {
|
|
serverLog.info(
|
|
`[resolve-claude-cli] platform=${platform()}, isWindows=${isWindows}, SHELL=${process.env.SHELL ?? 'unset'}`,
|
|
);
|
|
serverLog.info(`[resolve-claude-cli] PATH=${(process.env.PATH ?? '').slice(0, 400)}`);
|
|
|
|
// 1. Try PATH lookup
|
|
try {
|
|
const cmd = isWindows ? 'where claude 2>nul' : 'which claude 2>/dev/null';
|
|
serverLog.info(`[resolve-claude-cli] PATH lookup: ${cmd}`);
|
|
const raw = execSync(cmd, {
|
|
encoding: 'utf-8',
|
|
timeout: 3000,
|
|
}).trim();
|
|
const p = raw.split(/\r?\n/)[0]; // `where` on Windows may return multiple lines
|
|
serverLog.info(
|
|
`[resolve-claude-cli] PATH lookup result: "${p}" (exists=${p ? existsSync(p) : false})`,
|
|
);
|
|
if (p && existsSync(p)) return resolveWinExtension(p);
|
|
} catch (err) {
|
|
serverLog.info(
|
|
`[resolve-claude-cli] PATH lookup failed: ${err instanceof Error ? err.message : err}`,
|
|
);
|
|
}
|
|
|
|
// 2. On macOS/Linux, ask the user's login shell — Electron does not
|
|
// inherit the user's shell config, so nvm/pnpm/bun/mise/asdf shims are
|
|
// invisible to a bare `which`. This is the single most common cause of
|
|
// "CLI installed but OpenPencil can't find it" reports on Mac.
|
|
if (!isWindows) {
|
|
const viaShell = probeViaLoginShell('claude', 'resolve-claude-cli');
|
|
if (viaShell) return viaShell;
|
|
}
|
|
|
|
// 3. Try `npm prefix -g` to find actual npm global bin directory
|
|
// On Windows, must use `npm.cmd` since Electron spawns cmd.exe
|
|
if (isWindows) {
|
|
try {
|
|
serverLog.info('[resolve-claude-cli] trying npm.cmd prefix -g');
|
|
const prefix = execSync('npm.cmd prefix -g', {
|
|
encoding: 'utf-8',
|
|
timeout: 5000,
|
|
}).trim();
|
|
serverLog.info(`[resolve-claude-cli] npm global prefix: "${prefix}"`);
|
|
if (prefix) {
|
|
for (const bin of winNpmCandidates(prefix, 'claude')) {
|
|
serverLog.info(
|
|
`[resolve-claude-cli] checking npm global bin: "${bin}" (exists=${existsSync(bin)})`,
|
|
);
|
|
if (existsSync(bin)) return bin;
|
|
}
|
|
}
|
|
} catch (err) {
|
|
serverLog.info(
|
|
`[resolve-claude-cli] npm prefix -g failed: ${err instanceof Error ? err.message : err}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
// 4. Common install locations
|
|
const candidates = isWindows
|
|
? [
|
|
// npm global (.cmd + .ps1)
|
|
...winNpmCandidates(join(process.env.APPDATA || '', 'npm'), 'claude'),
|
|
// nvm-windows / fnm
|
|
...winNpmCandidates(join(process.env.NVM_SYMLINK || ''), 'claude'),
|
|
...winNpmCandidates(join(process.env.FNM_MULTISHELL_PATH || ''), 'claude'),
|
|
// Native .exe install locations
|
|
join(process.env.LOCALAPPDATA || '', 'Programs', 'claude-code', 'claude.exe'),
|
|
join(process.env.LOCALAPPDATA || '', 'Microsoft', 'WinGet', 'Links', 'claude.exe'),
|
|
join(homedir(), '.claude', 'local', 'claude.exe'),
|
|
join(homedir(), 'AppData', 'Local', 'Programs', 'claude-code', 'claude.exe'),
|
|
]
|
|
: posixUserBinDirs().map((dir) => join(dir, 'claude'));
|
|
for (const c of candidates) {
|
|
const exists = c ? existsSync(c) : false;
|
|
serverLog.info(`[resolve-claude-cli] candidate: "${c}" (exists=${exists})`);
|
|
if (c && exists) return c;
|
|
}
|
|
|
|
serverLog.warn(
|
|
'[resolve-claude-cli] no claude binary found after PATH, login-shell probe, and candidate scan',
|
|
);
|
|
return undefined;
|
|
}
|