mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-20 23:35:51 +00:00
注释代码(已死的、引用不存在符号的注释块): - Onboarding.tsx: 注释化的 preflight if-block(引用不存在的 preflightStep) - ultraplan.tsx: 两处引用不存在符号的注释(ULTRAPLAN_INSTRUCTIONS、getUltraplanModel) - types/hooks.ts: 禁用的 type-fest IsEqual 类型断言块 - types/global.d.ts: 已被真实模块取代的 Ultraplan ambient declares - types/textInputTypes.ts: 注释化的 onMessage interface 成员 legacy shim: - cli/bg.ts: 删除 handleBgFlag 别名 export(同胞 handleBgStart 已被所有调用点使用) Co-Authored-By: glm-5.2 <zai-org@claude-code-best.win>
339 lines
9.5 KiB
TypeScript
339 lines
9.5 KiB
TypeScript
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
|
|
}
|
|
}
|