mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-21 07:45:52 +00:00
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 from637c908dropped 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 from637c908dropped 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>
This commit is contained in:
344
src/cli/bg.ts
344
src/cli/bg.ts
@@ -1,7 +1,337 @@
|
||||
// Auto-generated stub — replace with real implementation
|
||||
export {};
|
||||
export const psHandler: (args: string[]) => Promise<void> = (async () => {}) as (args: string[]) => Promise<void>;
|
||||
export const logsHandler: (sessionId: string | undefined) => Promise<void> = (async () => {}) as (sessionId: string | undefined) => Promise<void>;
|
||||
export const attachHandler: (sessionId: string | undefined) => Promise<void> = (async () => {}) as (sessionId: string | undefined) => Promise<void>;
|
||||
export const killHandler: (sessionId: string | undefined) => Promise<void> = (async () => {}) as (sessionId: string | undefined) => Promise<void>;
|
||||
export const handleBgFlag: (args: string[]) => Promise<void> = (async () => {}) as (args: string[]) => Promise<void>;
|
||||
import { readdir, readFile, unlink } from 'fs/promises'
|
||||
import { join } from 'path'
|
||||
import { randomUUID } from 'crypto'
|
||||
import { getClaudeConfigHomeDir } from '../utils/envUtils.js'
|
||||
import { isProcessRunning } from '../utils/genericProcessUtils.js'
|
||||
import { jsonParse } from '../utils/slowOperations.js'
|
||||
import { selectEngine } from './bg/engines/index.js'
|
||||
import type { SessionEntry } from './bg/engine.js'
|
||||
|
||||
export type { SessionEntry } from './bg/engine.js'
|
||||
|
||||
function getSessionsDir(): string {
|
||||
return join(getClaudeConfigHomeDir(), 'sessions')
|
||||
}
|
||||
|
||||
export async function listLiveSessions(): Promise<SessionEntry[]> {
|
||||
const dir = getSessionsDir()
|
||||
let files: string[]
|
||||
try {
|
||||
files = await readdir(dir)
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
|
||||
const sessions: SessionEntry[] = []
|
||||
for (const file of files) {
|
||||
if (!/^\d+\.json$/.test(file)) continue
|
||||
const pid = parseInt(file.slice(0, -5), 10)
|
||||
|
||||
if (!isProcessRunning(pid)) {
|
||||
void unlink(join(dir, file)).catch(() => {})
|
||||
continue
|
||||
}
|
||||
|
||||
try {
|
||||
const raw = await readFile(join(dir, file), 'utf-8')
|
||||
const entry = jsonParse(raw) as SessionEntry
|
||||
sessions.push(entry)
|
||||
} catch {
|
||||
// Corrupt file — skip
|
||||
}
|
||||
}
|
||||
|
||||
return sessions
|
||||
}
|
||||
|
||||
export function findSession(
|
||||
sessions: SessionEntry[],
|
||||
target: string,
|
||||
): SessionEntry | undefined {
|
||||
const asNum = parseInt(target, 10)
|
||||
return sessions.find(
|
||||
s =>
|
||||
s.sessionId === target ||
|
||||
s.pid === asNum ||
|
||||
(s.name && s.name === target),
|
||||
)
|
||||
}
|
||||
|
||||
function formatTime(ts: number): string {
|
||||
return new Date(ts).toLocaleString()
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the engine type for an existing session.
|
||||
* Backward-compatible: sessions without an `engine` field are inferred
|
||||
* from the presence of `tmuxSessionName`.
|
||||
*/
|
||||
function resolveSessionEngine(session: SessionEntry): 'tmux' | 'detached' {
|
||||
if (session.engine) return session.engine
|
||||
return session.tmuxSessionName ? 'tmux' : 'detached'
|
||||
}
|
||||
|
||||
/**
|
||||
* `claude daemon status` / `claude ps` — list live sessions.
|
||||
*/
|
||||
export async function psHandler(_args: string[]): Promise<void> {
|
||||
const sessions = await listLiveSessions()
|
||||
|
||||
if (sessions.length === 0) {
|
||||
console.log('No active sessions.')
|
||||
return
|
||||
}
|
||||
|
||||
console.log(
|
||||
`${sessions.length} active session${sessions.length > 1 ? 's' : ''}:\n`,
|
||||
)
|
||||
|
||||
for (const s of sessions) {
|
||||
const engineType = resolveSessionEngine(s)
|
||||
const parts: string[] = [
|
||||
` PID: ${s.pid}`,
|
||||
` Kind: ${s.kind}`,
|
||||
` Engine: ${engineType}`,
|
||||
` Session: ${s.sessionId}`,
|
||||
` CWD: ${s.cwd}`,
|
||||
]
|
||||
|
||||
if (s.name) parts.push(` Name: ${s.name}`)
|
||||
if (s.startedAt) parts.push(` Started: ${formatTime(s.startedAt)}`)
|
||||
if (s.status) parts.push(` Status: ${s.status}`)
|
||||
if (s.waitingFor) parts.push(` Waiting for: ${s.waitingFor}`)
|
||||
if (s.bridgeSessionId) parts.push(` Bridge: ${s.bridgeSessionId}`)
|
||||
if (s.tmuxSessionName) parts.push(` Tmux: ${s.tmuxSessionName}`)
|
||||
if (s.logPath) parts.push(` Log: ${s.logPath}`)
|
||||
|
||||
console.log(parts.join('\n'))
|
||||
console.log()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* `claude daemon logs <target>` — show logs for a session.
|
||||
*/
|
||||
export async function logsHandler(target: string | undefined): Promise<void> {
|
||||
const sessions = await listLiveSessions()
|
||||
|
||||
if (!target) {
|
||||
if (sessions.length === 0) {
|
||||
console.log('No active sessions.')
|
||||
return
|
||||
}
|
||||
if (sessions.length === 1) {
|
||||
target = sessions[0]!.sessionId
|
||||
} else {
|
||||
console.log('Multiple sessions active. Specify one:')
|
||||
for (const s of sessions) {
|
||||
const label = s.name ? `${s.name} (${s.sessionId})` : s.sessionId
|
||||
console.log(` ${label} PID=${s.pid}`)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
const session = findSession(sessions, target)
|
||||
if (!session) {
|
||||
console.error(`Session not found: ${target}`)
|
||||
process.exitCode = 1
|
||||
return
|
||||
}
|
||||
|
||||
if (!session.logPath) {
|
||||
console.log(`No log path recorded for session ${session.sessionId}`)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const content = await readFile(session.logPath, 'utf-8')
|
||||
process.stdout.write(content)
|
||||
} catch (e) {
|
||||
console.error(`Failed to read log file: ${session.logPath}`)
|
||||
console.error(e instanceof Error ? e.message : String(e))
|
||||
process.exitCode = 1
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* `claude daemon attach <target>` — attach to a background session.
|
||||
*
|
||||
* Engine-aware: tmux sessions use tmux attach, detached sessions use log tail.
|
||||
*/
|
||||
export async function attachHandler(target: string | undefined): Promise<void> {
|
||||
const sessions = await listLiveSessions()
|
||||
|
||||
if (!target) {
|
||||
// Find bg sessions (tmux or detached)
|
||||
const bgSessions = sessions.filter(
|
||||
s => s.tmuxSessionName || s.engine === 'detached',
|
||||
)
|
||||
if (bgSessions.length === 0) {
|
||||
console.log(
|
||||
'No background sessions to attach to. Start one with `claude daemon bg`.',
|
||||
)
|
||||
return
|
||||
}
|
||||
if (bgSessions.length === 1) {
|
||||
target = bgSessions[0]!.sessionId
|
||||
} else {
|
||||
console.log('Multiple background sessions. Specify one:')
|
||||
for (const s of bgSessions) {
|
||||
const label = s.name ? `${s.name} (${s.sessionId})` : s.sessionId
|
||||
const engineType = resolveSessionEngine(s)
|
||||
console.log(` ${label} PID=${s.pid} engine=${engineType}`)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
const session = findSession(sessions, target)
|
||||
if (!session) {
|
||||
console.error(`Session not found: ${target}`)
|
||||
process.exitCode = 1
|
||||
return
|
||||
}
|
||||
|
||||
const engineType = resolveSessionEngine(session)
|
||||
|
||||
try {
|
||||
if (engineType === 'tmux') {
|
||||
const { TmuxEngine } = await import('./bg/engines/tmux.js')
|
||||
const tmux = new TmuxEngine()
|
||||
if (!(await tmux.available())) {
|
||||
console.error('tmux is no longer available. Cannot attach to tmux session.')
|
||||
process.exitCode = 1
|
||||
return
|
||||
}
|
||||
await tmux.attach(session)
|
||||
} else {
|
||||
const { DetachedEngine } = await import('./bg/engines/detached.js')
|
||||
const detached = new DetachedEngine()
|
||||
await detached.attach(session)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e instanceof Error ? e.message : String(e))
|
||||
process.exitCode = 1
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* `claude daemon kill <target>` — kill a session.
|
||||
*/
|
||||
export async function killHandler(target: string | undefined): Promise<void> {
|
||||
const sessions = await listLiveSessions()
|
||||
|
||||
if (!target) {
|
||||
if (sessions.length === 0) {
|
||||
console.log('No active sessions to kill.')
|
||||
return
|
||||
}
|
||||
console.log('Specify a session to kill:')
|
||||
for (const s of sessions) {
|
||||
const label = s.name ? `${s.name} (${s.sessionId})` : s.sessionId
|
||||
console.log(` ${label} PID=${s.pid}`)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const session = findSession(sessions, target)
|
||||
if (!session) {
|
||||
console.error(`Session not found: ${target}`)
|
||||
process.exitCode = 1
|
||||
return
|
||||
}
|
||||
|
||||
console.log(`Killing session ${session.sessionId} (PID: ${session.pid})...`)
|
||||
|
||||
try {
|
||||
process.kill(session.pid, 'SIGTERM')
|
||||
} catch {
|
||||
console.log('Session already exited.')
|
||||
return
|
||||
}
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 2000))
|
||||
|
||||
if (isProcessRunning(session.pid)) {
|
||||
try {
|
||||
process.kill(session.pid, 'SIGKILL')
|
||||
console.log('Session force-killed.')
|
||||
} catch {
|
||||
console.log('Session exited during grace period.')
|
||||
}
|
||||
} else {
|
||||
console.log('Session stopped.')
|
||||
}
|
||||
|
||||
const pidFile = join(getSessionsDir(), `${session.pid}.json`)
|
||||
void unlink(pidFile).catch(() => {})
|
||||
}
|
||||
|
||||
/**
|
||||
* `claude daemon bg [args]` — start a background session.
|
||||
*
|
||||
* Cross-platform: uses TmuxEngine on macOS/Linux when tmux is available,
|
||||
* falls back to DetachedEngine on Windows or when tmux is absent.
|
||||
*/
|
||||
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(),
|
||||
'sessions',
|
||||
'logs',
|
||||
`${sessionName}.log`,
|
||||
)
|
||||
|
||||
try {
|
||||
const result = await engine.start({
|
||||
sessionName,
|
||||
args: filteredArgs,
|
||||
env: { ...process.env },
|
||||
logPath,
|
||||
cwd: process.cwd(),
|
||||
})
|
||||
|
||||
console.log(`Background session started: ${result.sessionName}`)
|
||||
console.log(` Engine: ${result.engineUsed}`)
|
||||
console.log(` Log: ${result.logPath}`)
|
||||
console.log()
|
||||
console.log(`Use \`claude daemon attach ${result.sessionName}\` to reconnect.`)
|
||||
console.log(`Use \`claude daemon status\` to check status.`)
|
||||
console.log(`Use \`claude daemon kill ${result.sessionName}\` to stop.`)
|
||||
} catch (e) {
|
||||
console.error(e instanceof Error ? e.message : String(e))
|
||||
process.exitCode = 1
|
||||
}
|
||||
}
|
||||
|
||||
// Legacy export alias — kept for backward compatibility with cli.tsx
|
||||
export const handleBgFlag = handleBgStart
|
||||
|
||||
Reference in New Issue
Block a user