mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-15 12:55:51 +00:00
fix: daemon 子进程 spawn 跨平台修复 + CliLaunchSpec 集中化重构
- 新建 src/utils/cliLaunch.ts: 集中化 CLI 子进程启动层
- buildCliLaunch(): 标准化启动规范(execArgv snapshot + bundled mode 检测)
- spawnCli(): 统一 spawn(自动 windowsHide)
- quoteCliLaunch(): tmux shell 引用
- 修复 --daemon-worker=kind 等号格式解析(cli.tsx)
- 修复 daemon/bg fast path 缺少 setShellIfWindows()(Windows git-bash 发现)
- 修复 checkPathExists 用 execSync('dir') 改为 existsSync(消除 cmd.exe 弹窗)
- 修复 CLAUDE_CODE_GIT_BASH_PATH env 传播给子进程
- 7 个 spawn 站点迁移到 CliLaunchSpec
- BgEngine 接口新增 supportsInteractiveInput capability
- daemon bg 无 -p 时 detached 引擎给出清晰错误提示
This commit is contained in:
@@ -12,6 +12,7 @@ import {
|
||||
logEventAsync,
|
||||
} from '../services/analytics/index.js'
|
||||
import { isInBundledMode } from '../utils/bundledMode.js'
|
||||
import { getBootstrapArgs, getScriptPath } from '../utils/cliLaunch.js'
|
||||
import { logForDebugging } from '../utils/debug.js'
|
||||
import { rcLog } from './rcDebugLog.js'
|
||||
import { logForDiagnosticsNoPII } from '../utils/diagLogs.js'
|
||||
@@ -111,17 +112,15 @@ function pollSleepDetectionThresholdMs(backoff: BackoffConfig): number {
|
||||
|
||||
/**
|
||||
* Returns the args that must precede CLI flags when spawning a child claude
|
||||
* process. In compiled binaries, process.execPath is the claude binary itself
|
||||
* and args go directly to it. In npm installs (node running cli.js),
|
||||
* process.execPath is the node runtime — the child spawn must pass the script
|
||||
* path as the first arg, otherwise node interprets --sdk-url as a node option
|
||||
* and exits with "bad option: --sdk-url". See anthropics/claude-code#28334.
|
||||
* process. Delegates to the centralized cliLaunch module which handles
|
||||
* bundled-vs-script mode, execArgv sanitization, and the Bun execArgv leak
|
||||
* quirk. See anthropics/claude-code#28334.
|
||||
*/
|
||||
function spawnScriptArgs(): string[] {
|
||||
if (isInBundledMode() || !process.argv[1]) {
|
||||
return []
|
||||
}
|
||||
return [process.argv[1]]
|
||||
const bootstrap = [...getBootstrapArgs()]
|
||||
const script = getScriptPath()
|
||||
if (script) bootstrap.push(script)
|
||||
return bootstrap
|
||||
}
|
||||
|
||||
/** Attempt to spawn a session; returns error string if spawn throws. */
|
||||
|
||||
@@ -279,6 +279,32 @@ export async function killHandler(target: string | undefined): Promise<void> {
|
||||
export async function handleBgStart(args: string[]): Promise<void> {
|
||||
const engine = await selectEngine()
|
||||
|
||||
// Strip --bg/--background from args (for backward-compat shortcut)
|
||||
const filteredArgs = args.filter(a => a !== '--bg' && a !== '--background')
|
||||
|
||||
// Engines without interactive TTY input (e.g. detached) require -p/--print
|
||||
// or piped input. Tmux provides a virtual terminal so it works without -p.
|
||||
if (
|
||||
!engine.supportsInteractiveInput &&
|
||||
!filteredArgs.some(a => a === '-p' || a === '--print' || a === '--pipe')
|
||||
) {
|
||||
console.error(
|
||||
'Error: Background sessions with detached engine require -p/--print flag.\n' +
|
||||
'The detached engine has no terminal for interactive input.\n\n' +
|
||||
'Usage:\n' +
|
||||
' claude daemon bg -p "your prompt here"\n' +
|
||||
' echo "prompt" | claude daemon bg --pipe',
|
||||
)
|
||||
if (process.platform !== 'win32') {
|
||||
console.error(
|
||||
'\nAlternatively, install tmux for interactive background sessions:\n' +
|
||||
` ${process.platform === 'darwin' ? 'brew install tmux' : 'sudo apt install tmux'}`,
|
||||
)
|
||||
}
|
||||
process.exitCode = 1
|
||||
return
|
||||
}
|
||||
|
||||
const sessionName = `claude-bg-${randomUUID().slice(0, 8)}`
|
||||
const logPath = join(
|
||||
getClaudeConfigHomeDir(),
|
||||
@@ -287,9 +313,6 @@ export async function handleBgStart(args: string[]): Promise<void> {
|
||||
`${sessionName}.log`,
|
||||
)
|
||||
|
||||
// Strip --bg/--background from args (for backward-compat shortcut)
|
||||
const filteredArgs = args.filter(a => a !== '--bg' && a !== '--background')
|
||||
|
||||
try {
|
||||
const result = await engine.start({
|
||||
sessionName,
|
||||
|
||||
@@ -41,6 +41,8 @@ export interface BgStartResult {
|
||||
|
||||
export interface BgEngine {
|
||||
readonly name: 'tmux' | 'detached'
|
||||
/** Whether the engine provides a TTY for interactive REPL input. */
|
||||
readonly supportsInteractiveInput: boolean
|
||||
available(): Promise<boolean>
|
||||
start(opts: BgStartOptions): Promise<BgStartResult>
|
||||
attach(session: SessionEntry): Promise<void>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { spawn } from 'child_process'
|
||||
import { openSync, closeSync, mkdirSync } from 'fs'
|
||||
import { closeSync, mkdirSync, openSync } from 'fs'
|
||||
import { dirname } from 'path'
|
||||
import { buildCliLaunch, spawnCli } from '../../../utils/cliLaunch.js'
|
||||
import type {
|
||||
BgEngine,
|
||||
BgStartOptions,
|
||||
@@ -11,6 +11,7 @@ import { tailLog } from '../tail.js'
|
||||
|
||||
export class DetachedEngine implements BgEngine {
|
||||
readonly name = 'detached' as const
|
||||
readonly supportsInteractiveInput = false
|
||||
|
||||
async available(): Promise<boolean> {
|
||||
return true
|
||||
@@ -20,17 +21,19 @@ export class DetachedEngine implements BgEngine {
|
||||
mkdirSync(dirname(opts.logPath), { recursive: true })
|
||||
|
||||
const logFd = openSync(opts.logPath, 'a')
|
||||
const entrypoint = process.argv[1]!
|
||||
|
||||
const child = spawn(process.execPath, [entrypoint, ...opts.args], {
|
||||
detached: true,
|
||||
stdio: ['ignore', logFd, logFd],
|
||||
const launch = buildCliLaunch(opts.args, {
|
||||
env: {
|
||||
...opts.env,
|
||||
CLAUDE_CODE_SESSION_KIND: 'bg',
|
||||
CLAUDE_CODE_SESSION_NAME: opts.sessionName,
|
||||
CLAUDE_CODE_SESSION_LOG: opts.logPath,
|
||||
} as Record<string, string>,
|
||||
} as NodeJS.ProcessEnv,
|
||||
})
|
||||
|
||||
const child = spawnCli(launch, {
|
||||
detached: true,
|
||||
stdio: ['ignore', logFd, logFd],
|
||||
cwd: opts.cwd,
|
||||
})
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { spawnSync } from 'child_process'
|
||||
import { execFileNoThrow } from '../../../utils/execFileNoThrow.js'
|
||||
import { quote } from '../../../utils/bash/shellQuote.js'
|
||||
import { buildCliLaunch, quoteCliLaunch } from '../../../utils/cliLaunch.js'
|
||||
import type {
|
||||
BgEngine,
|
||||
BgStartOptions,
|
||||
@@ -10,6 +10,7 @@ import type {
|
||||
|
||||
export class TmuxEngine implements BgEngine {
|
||||
readonly name = 'tmux' as const
|
||||
readonly supportsInteractiveInput = true
|
||||
|
||||
async available(): Promise<boolean> {
|
||||
const { code } = await execFileNoThrow('tmux', ['-V'], { useCwd: false })
|
||||
@@ -17,21 +18,22 @@ export class TmuxEngine implements BgEngine {
|
||||
}
|
||||
|
||||
async start(opts: BgStartOptions): Promise<BgStartResult> {
|
||||
const entrypoint = process.argv[1]!
|
||||
const cmd = quote([process.execPath, entrypoint, ...opts.args])
|
||||
const launch = buildCliLaunch(opts.args, {
|
||||
env: {
|
||||
...opts.env,
|
||||
CLAUDE_CODE_SESSION_KIND: 'bg',
|
||||
CLAUDE_CODE_SESSION_NAME: opts.sessionName,
|
||||
CLAUDE_CODE_SESSION_LOG: opts.logPath,
|
||||
CLAUDE_CODE_TMUX_SESSION: opts.sessionName,
|
||||
} as NodeJS.ProcessEnv,
|
||||
})
|
||||
|
||||
const tmuxEnv: Record<string, string | undefined> = {
|
||||
...opts.env,
|
||||
CLAUDE_CODE_SESSION_KIND: 'bg',
|
||||
CLAUDE_CODE_SESSION_NAME: opts.sessionName,
|
||||
CLAUDE_CODE_SESSION_LOG: opts.logPath,
|
||||
CLAUDE_CODE_TMUX_SESSION: opts.sessionName,
|
||||
}
|
||||
const cmd = quoteCliLaunch(launch)
|
||||
|
||||
const result = spawnSync(
|
||||
'tmux',
|
||||
['new-session', '-d', '-s', opts.sessionName, cmd],
|
||||
{ stdio: 'inherit', env: tmuxEnv },
|
||||
{ stdio: 'inherit', env: launch.env },
|
||||
)
|
||||
|
||||
if (result.status !== 0) {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { spawn } from 'child_process';
|
||||
import * as React from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { resolve } from 'path';
|
||||
@@ -8,6 +7,7 @@ import { ListItem } from '../../components/design-system/ListItem.js';
|
||||
import { useRegisterOverlay } from '../../context/overlayContext.js';
|
||||
import { useKeybindings } from '../../keybindings/useKeybinding.js';
|
||||
import { findGitRoot } from '../../utils/git.js';
|
||||
import { buildCliLaunch, spawnCli } from '../../utils/cliLaunch.js';
|
||||
import { getKairosActive, setKairosActive } from '../../bootstrap/state.js';
|
||||
import type { LocalJSXCommandContext } from '../../commands.js';
|
||||
import type { LocalJSXCommandOnDone } from '../../types/command.js';
|
||||
@@ -65,9 +65,9 @@ export function NewInstallWizard({ defaultDir, onInstalled, onCancel, onError }:
|
||||
const dir = defaultDir || resolve('.');
|
||||
|
||||
try {
|
||||
const execArgs = [...process.execArgv, process.argv[1]!, 'daemon', 'start', `--dir=${dir}`];
|
||||
const launch = buildCliLaunch(['daemon', 'start', `--dir=${dir}`]);
|
||||
|
||||
const child = spawn(process.execPath, execArgs, {
|
||||
const child = spawnCli(launch, {
|
||||
cwd: dir,
|
||||
stdio: 'ignore',
|
||||
detached: true,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { spawn, type ChildProcess } from 'child_process';
|
||||
import { type ChildProcess } from 'child_process';
|
||||
import { resolve } from 'path';
|
||||
import * as React from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
@@ -10,6 +10,7 @@ import { ListItem } from '../../components/design-system/ListItem.js';
|
||||
import { useRegisterOverlay } from '../../context/overlayContext.js';
|
||||
import { Box, Text } from '@anthropic/ink';
|
||||
import { useKeybindings } from '../../keybindings/useKeybinding.js';
|
||||
import { buildCliLaunch, spawnCli } from '../../utils/cliLaunch.js';
|
||||
import type { ToolUseContext } from '../../Tool.js';
|
||||
import type { LocalJSXCommandContext, LocalJSXCommandOnDone } from '../../types/command.js';
|
||||
import { errorMessage } from '../../utils/errors.js';
|
||||
@@ -202,9 +203,9 @@ async function checkPrerequisites(): Promise<string | null> {
|
||||
function startDaemon(): void {
|
||||
const dir = resolve('.');
|
||||
|
||||
const execArgs = [...process.execArgv, process.argv[1]!, 'daemon', 'start', `--dir=${dir}`];
|
||||
const launch = buildCliLaunch(['daemon', 'start', `--dir=${dir}`]);
|
||||
|
||||
const child = spawn(process.execPath, execArgs, {
|
||||
const child = spawnCli(launch, {
|
||||
cwd: dir,
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
detached: false,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { spawn, type ChildProcess } from 'child_process'
|
||||
import { type ChildProcess } from 'child_process'
|
||||
import { resolve } from 'path'
|
||||
import { buildCliLaunch, spawnCli } from '../utils/cliLaunch.js'
|
||||
import { errorMessage } from '../utils/errors.js'
|
||||
import {
|
||||
writeDaemonState,
|
||||
@@ -335,17 +336,11 @@ function spawnWorker(
|
||||
CLAUDE_CODE_SESSION_KIND: 'daemon-worker',
|
||||
}
|
||||
|
||||
// Build the worker command: reuse the same entrypoint with --daemon-worker flag
|
||||
const execArgs = [
|
||||
...process.execArgv,
|
||||
process.argv[1]!,
|
||||
`--daemon-worker=${worker.kind}`,
|
||||
]
|
||||
|
||||
console.log(`[daemon] spawning worker '${worker.kind}'`)
|
||||
|
||||
const child = spawn(process.execPath, execArgs, {
|
||||
env,
|
||||
const launch = buildCliLaunch([`--daemon-worker=${worker.kind}`], { env })
|
||||
|
||||
const child = spawnCli(launch, {
|
||||
cwd: dir,
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
})
|
||||
|
||||
@@ -121,9 +121,10 @@ async function main(): Promise<void> {
|
||||
// perf-sensitive. No enableConfigs(), no analytics sinks at this layer —
|
||||
// workers are lean. If a worker kind needs configs/auth (assistant will),
|
||||
// it calls them inside its run() fn.
|
||||
if (feature('DAEMON') && args[0] === '--daemon-worker') {
|
||||
if (feature('DAEMON') && (args[0] === '--daemon-worker' || args[0]?.startsWith('--daemon-worker='))) {
|
||||
const kind = args[0] === '--daemon-worker' ? args[1] : args[0].split('=')[1];
|
||||
const { runDaemonWorker } = await import('../daemon/workerRegistry.js');
|
||||
await runDaemonWorker(args[1]);
|
||||
await runDaemonWorker(kind);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -184,6 +185,8 @@ async function main(): Promise<void> {
|
||||
profileCheckpoint('cli_daemon_path');
|
||||
const { enableConfigs } = await import('../utils/config.js');
|
||||
enableConfigs();
|
||||
const { setShellIfWindows } = await import('../utils/windowsPaths.js');
|
||||
setShellIfWindows();
|
||||
const { initSinks } = await import('../utils/sinks.js');
|
||||
initSinks();
|
||||
const { daemonMain } = await import('../daemon/main.js');
|
||||
@@ -196,6 +199,8 @@ async function main(): Promise<void> {
|
||||
profileCheckpoint('cli_daemon_path');
|
||||
const { enableConfigs } = await import('../utils/config.js');
|
||||
enableConfigs();
|
||||
const { setShellIfWindows } = await import('../utils/windowsPaths.js');
|
||||
setShellIfWindows();
|
||||
const bg = await import('../cli/bg.js');
|
||||
await bg.handleBgStart(args.filter(a => a !== '--bg' && a !== '--background'));
|
||||
return;
|
||||
@@ -211,6 +216,8 @@ async function main(): Promise<void> {
|
||||
profileCheckpoint('cli_daemon_path');
|
||||
const { enableConfigs } = await import('../utils/config.js');
|
||||
enableConfigs();
|
||||
const { setShellIfWindows } = await import('../utils/windowsPaths.js');
|
||||
setShellIfWindows();
|
||||
const { initSinks } = await import('../utils/sinks.js');
|
||||
initSinks();
|
||||
const { daemonMain } = await import('../daemon/main.js');
|
||||
|
||||
180
src/utils/cliLaunch.ts
Normal file
180
src/utils/cliLaunch.ts
Normal file
@@ -0,0 +1,180 @@
|
||||
import { type ChildProcess, spawn, type SpawnOptions } from 'child_process'
|
||||
import { isInBundledMode } from './bundledMode.js'
|
||||
import { quote } from './bash/shellQuote.js'
|
||||
|
||||
/**
|
||||
* CliLaunchSpec — normalized descriptor for spawning a child CLI process.
|
||||
*
|
||||
* Every site that re-execs the CLI (daemon workers, bg sessions, bridge
|
||||
* sessions, assistant/RCS daemon launchers) should use this instead of
|
||||
* manually assembling `[...process.execArgv, process.argv[1]!, ...]`.
|
||||
*
|
||||
* Centralizing the bootstrap contract prevents the class of bugs where
|
||||
* individual spawn sites forget execArgv, windowsHide, or env propagation.
|
||||
*/
|
||||
export interface CliLaunchSpec {
|
||||
/** Runtime binary path (e.g. bun, node). */
|
||||
execPath: string
|
||||
/** Full argument list including bootstrap args and CLI args. */
|
||||
args: string[]
|
||||
/** Environment for the child process. */
|
||||
env: NodeJS.ProcessEnv
|
||||
/** Whether to hide the console window on Windows. */
|
||||
windowsHide: boolean
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Frozen bootstrap snapshot — computed once at module load time.
|
||||
//
|
||||
// Bun quirk (https://github.com/oven-sh/bun/issues/11673): in single-file
|
||||
// executables, app arguments from process.argv can leak into process.execArgv.
|
||||
// We snapshot and filter once, so every child gets a clean, stable set of
|
||||
// runtime flags regardless of when buildCliLaunch is called.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Filter out leaked application arguments from process.execArgv.
|
||||
* Only keep known runtime flags: -d (defines), --feature, --inspect variants.
|
||||
*/
|
||||
function sanitizeExecArgv(raw: readonly string[]): string[] {
|
||||
const result: string[] = []
|
||||
for (let i = 0; i < raw.length; i++) {
|
||||
const arg = raw[i]!
|
||||
// Bun define flags: -d KEY:VALUE or -dKEY:VALUE
|
||||
if (arg === '-d' || arg.startsWith('-d ') || arg.startsWith('-d\t')) {
|
||||
result.push(arg)
|
||||
if (arg === '-d' && i + 1 < raw.length) {
|
||||
result.push(raw[++i]!)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if (arg.startsWith('-d') && arg.includes(':')) {
|
||||
result.push(arg)
|
||||
continue
|
||||
}
|
||||
// Bun feature flags: --feature NAME
|
||||
if (arg === '--feature') {
|
||||
result.push(arg)
|
||||
if (i + 1 < raw.length) {
|
||||
result.push(raw[++i]!)
|
||||
}
|
||||
continue
|
||||
}
|
||||
// Node/Bun inspect flags
|
||||
if (/^--inspect(-brk)?(=|$)/.test(arg)) {
|
||||
result.push(arg)
|
||||
continue
|
||||
}
|
||||
// Keep other known runtime flags (e.g. --conditions, --experimental-*)
|
||||
if (arg.startsWith('--') && !arg.includes('=') && i + 1 < raw.length) {
|
||||
// Unknown two-part flag — skip conservatively in bundled mode only
|
||||
if (isInBundledMode()) continue
|
||||
result.push(arg)
|
||||
result.push(raw[++i]!)
|
||||
continue
|
||||
}
|
||||
if (arg.startsWith('-') && !isInBundledMode()) {
|
||||
result.push(arg)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
const BOOTSTRAP_ARGS: readonly string[] = Object.freeze(
|
||||
sanitizeExecArgv(process.execArgv),
|
||||
)
|
||||
const SCRIPT_PATH: string | undefined = process.argv[1]
|
||||
const EXEC_PATH: string = process.execPath
|
||||
const IS_WINDOWS = process.platform === 'win32'
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Public API
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Build a normalized launch spec for spawning a child CLI process.
|
||||
*
|
||||
* @param cliArgs Arguments to pass to the CLI entrypoint (e.g. ['daemon', 'start'])
|
||||
* @param opts.env Override environment (defaults to process.env)
|
||||
*/
|
||||
export function buildCliLaunch(
|
||||
cliArgs: string[],
|
||||
opts?: { env?: NodeJS.ProcessEnv },
|
||||
): CliLaunchSpec {
|
||||
const baseEnv = opts?.env ?? process.env
|
||||
|
||||
// In bundled mode the execPath IS the CLI binary — no script path needed.
|
||||
// In script mode (dev / npm) we need the script path between runtime flags
|
||||
// and CLI args so the runtime knows which file to execute.
|
||||
const args: string[] =
|
||||
isInBundledMode() || !SCRIPT_PATH
|
||||
? [...BOOTSTRAP_ARGS, ...cliArgs]
|
||||
: [...BOOTSTRAP_ARGS, SCRIPT_PATH, ...cliArgs]
|
||||
|
||||
// Ensure Windows children can discover git-bash without shelling out
|
||||
const env: NodeJS.ProcessEnv = { ...baseEnv }
|
||||
if (IS_WINDOWS) {
|
||||
if (
|
||||
process.env.CLAUDE_CODE_GIT_BASH_PATH &&
|
||||
!env.CLAUDE_CODE_GIT_BASH_PATH
|
||||
) {
|
||||
env.CLAUDE_CODE_GIT_BASH_PATH = process.env.CLAUDE_CODE_GIT_BASH_PATH
|
||||
}
|
||||
if (process.env.SHELL && !env.SHELL) {
|
||||
env.SHELL = process.env.SHELL
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
execPath: EXEC_PATH,
|
||||
args,
|
||||
env,
|
||||
windowsHide: IS_WINDOWS,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Spawn a child CLI process from a launch spec.
|
||||
*
|
||||
* Callers provide transport-level options (stdio, detached, cwd) while the
|
||||
* spec handles bootstrap concerns (execPath, args, env, windowsHide).
|
||||
*
|
||||
* Windows note: `detached: true` on Windows creates a new console window
|
||||
* (unlike Unix where it only creates a new process group). Node.js uses
|
||||
* `windowsHide` to pass CREATE_NO_WINDOW, but Bun may not implement it.
|
||||
* As a fallback, we always set both `windowsHide: true` and keep
|
||||
* `detached` as-is — the child needs `detached` to outlive the parent.
|
||||
*/
|
||||
export function spawnCli(
|
||||
spec: CliLaunchSpec,
|
||||
spawnOpts: Omit<SpawnOptions, 'windowsHide'>,
|
||||
): ChildProcess {
|
||||
return spawn(spec.execPath, spec.args, {
|
||||
...spawnOpts,
|
||||
env: { ...spec.env, ...(spawnOpts.env as NodeJS.ProcessEnv) },
|
||||
windowsHide: spec.windowsHide,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Quote a launch spec into a single shell command string (for tmux).
|
||||
*/
|
||||
export function quoteCliLaunch(spec: CliLaunchSpec): string {
|
||||
return quote([spec.execPath, ...spec.args])
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the frozen bootstrap args snapshot.
|
||||
* Useful for call sites that need the raw args (e.g. bridgeMain deps).
|
||||
*/
|
||||
export function getBootstrapArgs(): readonly string[] {
|
||||
return BOOTSTRAP_ARGS
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the script path (process.argv[1] at startup).
|
||||
* Returns undefined in bundled mode.
|
||||
*/
|
||||
export function getScriptPath(): string | undefined {
|
||||
return SCRIPT_PATH
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { existsSync } from 'fs'
|
||||
import memoize from 'lodash-es/memoize.js'
|
||||
import * as path from 'path'
|
||||
import * as pathWin32 from 'path/win32'
|
||||
@@ -8,17 +9,13 @@ import { memoizeWithLRU } from './memoize.js'
|
||||
import { getPlatform } from './platform.js'
|
||||
|
||||
/**
|
||||
* Check if a file or directory exists on Windows using the dir command
|
||||
* @param path - The path to check
|
||||
* @returns true if the path exists, false otherwise
|
||||
* Check if a file or directory exists on Windows.
|
||||
* Uses fs.existsSync instead of `dir` shell command to avoid spawning
|
||||
* cmd.exe — which can cause brief console window flashes in detached
|
||||
* or windowsHide child processes.
|
||||
*/
|
||||
function checkPathExists(path: string): boolean {
|
||||
try {
|
||||
execSync_DEPRECATED(`dir "${path}"`, { stdio: 'pipe' })
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
function checkPathExists(filePath: string): boolean {
|
||||
return existsSync(filePath)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -88,6 +85,8 @@ export function setShellIfWindows(): void {
|
||||
if (getPlatform() === 'windows') {
|
||||
const gitBashPath = findGitBashPath()
|
||||
process.env.SHELL = gitBashPath
|
||||
// Propagate to child processes so they skip filesystem probing
|
||||
process.env.CLAUDE_CODE_GIT_BASH_PATH = gitBashPath
|
||||
logForDebugging(`Using bash path: "${gitBashPath}"`)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user