Files
claude-code/src/utils/cliLaunch.ts
claude-code-best c8d08d235b Feat/integrate lint preview (#285)
* feat: 适配 zed acp 协议

* docs: 完善 acp 文档

* feat: integrate feature branches + daemon/job 命令层级化 + 跨平台后台引擎

Cherry-picked from origin/lint/preview (637c908), excluding lint-only changes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: correct detectMimeFromBase64 to decode raw bytes from base64

Cherry-picked from origin/lint/preview (ee36954).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: daemon 子进程 spawn 跨平台修复 + CliLaunchSpec 集中化重构

Cherry-picked from origin/lint/preview (c5f52cd), excluding lint-only formatting changes.

- 新建 src/utils/cliLaunch.ts: 集中化 CLI 子进程启动层
- 修复 --daemon-worker=kind 等号格式解析
- 修复 daemon/bg fast path 缺少 setShellIfWindows()
- 修复 checkPathExists 用 existsSync 替代 execSync('dir')
- 7 个 spawn 站点迁移到 CliLaunchSpec

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: merge tsconfig.base.json into tsconfig.json with full compiler options

The cherry-pick from 637c908 dropped jsx/strict/etc settings when removing
tsconfig.base.json. This commit restores them in a single tsconfig.json.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: merge tsconfig.base.json into tsconfig.json with full compiler options

The cherry-pick from 637c908 dropped jsx/strict/etc settings when removing
tsconfig.base.json. This commit restores them in a single tsconfig.json.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-16 20:59:29 +08:00

181 lines
5.9 KiB
TypeScript

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
}