/* eslint-disable custom-rules/no-process-exit */import { feature } from 'bun:bundle'import chalk from 'chalk'import {type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,logEvent,} from 'src/services/analytics/index.js'import { getCwd } from 'src/utils/cwd.js'import { checkForReleaseNotes } from 'src/utils/releaseNotes.js'import { setCwd } from 'src/utils/Shell.js'import { initSinks } from 'src/utils/sinks.js'import {getIsNonInteractiveSession,getProjectRoot,getSessionId,setOriginalCwd,setProjectRoot,switchSession,} from './bootstrap/state.js'import { getCommands } from './commands.js'import { initSessionMemory } from './services/SessionMemory/sessionMemory.js'import { asSessionId } from './types/ids.js'import { isAgentSwarmsEnabled } from './utils/agentSwarmsEnabled.js'import { checkAndRestoreTerminalBackup } from './utils/appleTerminalBackup.js'import { prefetchApiKeyFromApiKeyHelperIfSafe } from './utils/auth.js'import { clearMemoryFileCaches } from './utils/claudemd.js'import { getCurrentProjectConfig, getGlobalConfig } from './utils/config.js'import { logForDiagnosticsNoPII } from './utils/diagLogs.js'import { env } from './utils/env.js'import { envDynamic } from './utils/envDynamic.js'import { isBareMode, isEnvTruthy } from './utils/envUtils.js'import { errorMessage } from './utils/errors.js'import { findCanonicalGitRoot, findGitRoot, getIsGit } from './utils/git.js'import { initializeFileChangedWatcher } from './utils/hooks/fileChangedWatcher.js'import {captureHooksConfigSnapshot,updateHooksConfigSnapshot,} from './utils/hooks/hooksConfigSnapshot.js'import { hasWorktreeCreateHook } from './utils/hooks.js'import { checkAndRestoreITerm2Backup } from './utils/iTermBackup.js'import { logError } from './utils/log.js'import { getRecentActivity } from './utils/logoV2Utils.js'import { lockCurrentVersion } from './utils/nativeInstaller/index.js'import type { PermissionMode } from './utils/permissions/PermissionMode.js'import { getPlanSlug } from './utils/plans.js'import { saveWorktreeState } from './utils/sessionStorage.js'import { profileCheckpoint } from './utils/startupProfiler.js'import {createTmuxSessionForWorktree,createWorktreeForSession,generateTmuxSessionName,worktreeBranchName,} from './utils/worktree.js'export async function setup(cwd: string,permissionMode: PermissionMode,allowDangerouslySkipPermissions: boolean,worktreeEnabled: boolean,worktreeName: string | undefined,tmuxEnabled: boolean,customSessionId?: string | null,worktreePRNumber?: number,messagingSocketPath?: string,): Promise<void> {logForDiagnosticsNoPII('info', 'setup_started')// Check for Node.js version < 18const nodeVersion = process.version.match(/^v(\d+)\./)?.[1]if (!nodeVersion || parseInt(nodeVersion) < 18) {// biome-ignore lint/suspicious/noConsole:: intentional console outputconsole.error(chalk.bold.red('Error: Claude Code requires Node.js version 18 or higher.',),)process.exit(1)}// Set custom session ID if providedif (customSessionId) {switchSession(asSessionId(customSessionId))}// --bare / SIMPLE: skip UDS messaging server and teammate snapshot.// Scripted calls don't receive injected messages and don't use swarm teammates.// Explicit --messaging-socket-path is the escape hatch (per #23222 gate pattern).if (!isBareMode() || messagingSocketPath !== undefined) {// Start UDS messaging server (Mac/Linux only).// Enabled by default for ants — creates a socket in tmpdir if no// --messaging-socket-path is passed. Awaited so the server is bound// and $CLAUDE_CODE_MESSAGING_SOCKET is exported before any hook// (SessionStart in particular) can spawn and snapshot process.env.if (feature('UDS_INBOX')) {const m = await import('./utils/udsMessaging.js')await m.startUdsMessaging(messagingSocketPath ?? m.getDefaultUdsSocketPath(),{ isExplicit: messagingSocketPath !== undefined },)}}// Teammate snapshot — SIMPLE-only gate (no escape hatch, swarm not used in bare)if (!isBareMode() && isAgentSwarmsEnabled()) {const { captureTeammateModeSnapshot } = await import('./utils/swarm/backends/teammateModeSnapshot.js')captureTeammateModeSnapshot()}// Terminal backup restoration — interactive only. Print mode doesn't// interact with terminal settings; the next interactive session will// detect and restore any interrupted setup.if (!getIsNonInteractiveSession()) {// iTerm2 backup check only when swarms enabledif (isAgentSwarmsEnabled()) {const restoredIterm2Backup = await checkAndRestoreITerm2Backup()if (restoredIterm2Backup.status === 'restored') {// biome-ignore lint/suspicious/noConsole:: intentional console outputconsole.log(chalk.yellow('Detected an interrupted iTerm2 setup. Your original settings have been restored. You may need to restart iTerm2 for the changes to take effect.',),)} else if (restoredIterm2Backup.status === 'failed') {// biome-ignore lint/suspicious/noConsole:: intentional console outputconsole.error(chalk.red(`Failed to restore iTerm2 settings. Please manually restore your original settings with: defaults import com.googlecode.iterm2 ${restoredIterm2Backup.backupPath}.`,),)}}// Check and restore Terminal.app backup if setup was interruptedtry {const restoredTerminalBackup = await checkAndRestoreTerminalBackup()if (restoredTerminalBackup.status === 'restored') {// biome-ignore lint/suspicious/noConsole:: intentional console outputconsole.log(chalk.yellow('Detected an interrupted Terminal.app setup. Your original settings have been restored. You may need to restart Terminal.app for the changes to take effect.',),)} else if (restoredTerminalBackup.status === 'failed') {// biome-ignore lint/suspicious/noConsole:: intentional console outputconsole.error(chalk.red(`Failed to restore Terminal.app settings. Please manually restore your original settings with: defaults import com.apple.Terminal ${restoredTerminalBackup.backupPath}.`,),)}} catch (error) {// Log but don't crash if Terminal.app backup restoration failslogError(error)}}// IMPORTANT: setCwd() must be called before any other code that depends on the cwdsetCwd(cwd)// Capture hooks configuration snapshot to avoid hidden hook modifications.// IMPORTANT: Must be called AFTER setCwd() so hooks are loaded from the correct directoryconst hooksStart = Date.now()captureHooksConfigSnapshot()logForDiagnosticsNoPII('info', 'setup_hooks_captured', {duration_ms: Date.now() - hooksStart,})// Initialize FileChanged hook watcher — sync, reads hook config snapshotinitializeFileChangedWatcher(cwd)// Handle worktree creation if requested// IMPORTANT: this must be called befiore getCommands(), otherwise /eject won't be available.if (worktreeEnabled) {// Mirrors bridgeMain.ts: hook-configured sessions can proceed without git// so createWorktreeForSession() can delegate to the hook (non-git VCS).const hasHook = hasWorktreeCreateHook()const inGit = await getIsGit()if (!hasHook && !inGit) {process.stderr.write(chalk.red(`Error: Can only use --worktree in a git repository, but ${chalk.bold(cwd)} is not a git repository. ` +`Configure a WorktreeCreate hook in settings.json to use --worktree with other VCS systems.\n`,),)process.exit(1)}const slug = worktreePRNumber? `pr-${worktreePRNumber}`: (worktreeName ?? getPlanSlug())// Git preamble runs whenever we're in a git repo — even if a hook is// configured — so --tmux keeps working for git users who also have a// WorktreeCreate hook. Only hook-only (non-git) mode skips it.let tmuxSessionName: string | undefinedif (inGit) {// Resolve to main repo root (handles being invoked from within a worktree).// findCanonicalGitRoot is sync/filesystem-only/memoized; the underlying// findGitRoot cache was already warmed by getIsGit() above, so this is ~free.const mainRepoRoot = findCanonicalGitRoot(getCwd())if (!mainRepoRoot) {process.stderr.write(chalk.red(`Error: Could not determine the main git repository root.\n`,),)process.exit(1)}// If we're inside a worktree, switch to the main repo for worktree creationif (mainRepoRoot !== (findGitRoot(getCwd()) ?? getCwd())) {logForDiagnosticsNoPII('info', 'worktree_resolved_to_main_repo')process.chdir(mainRepoRoot)setCwd(mainRepoRoot)}tmuxSessionName = tmuxEnabled? generateTmuxSessionName(mainRepoRoot, worktreeBranchName(slug)): undefined} else {// Non-git hook mode: no canonical root to resolve, so name the tmux// session from cwd — generateTmuxSessionName only basenames the path.tmuxSessionName = tmuxEnabled? generateTmuxSessionName(getCwd(), worktreeBranchName(slug)): undefined}let worktreeSession: Awaited<ReturnType<typeof createWorktreeForSession>>try {worktreeSession = await createWorktreeForSession(getSessionId(),slug,tmuxSessionName,worktreePRNumber ? { prNumber: worktreePRNumber } : undefined,)} catch (error) {process.stderr.write(chalk.red(`Error creating worktree: ${errorMessage(error)}\n`),)process.exit(1)}logEvent('tengu_worktree_created', { tmux_enabled: tmuxEnabled })// Create tmux session for the worktree if enabledif (tmuxEnabled && tmuxSessionName) {const tmuxResult = await createTmuxSessionForWorktree(tmuxSessionName,worktreeSession.worktreePath,)if (tmuxResult.created) {// biome-ignore lint/suspicious/noConsole:: intentional console outputconsole.log(chalk.green(`Created tmux session: ${chalk.bold(tmuxSessionName)}\nTo attach: ${chalk.bold(`tmux attach -t ${tmuxSessionName}`)}`,),)} else {// biome-ignore lint/suspicious/noConsole:: intentional console outputconsole.error(chalk.yellow(`Warning: Failed to create tmux session: ${tmuxResult.error}`,),)}}process.chdir(worktreeSession.worktreePath)setCwd(worktreeSession.worktreePath)setOriginalCwd(getCwd())// --worktree means the worktree IS the session's project, so skills/hooks/// cron/etc. should resolve here. (EnterWorktreeTool mid-session does NOT// touch projectRoot — that's a throwaway worktree, project stays stable.)setProjectRoot(getCwd())saveWorktreeState(worktreeSession)// Clear memory files cache since originalCwd has changedclearMemoryFileCaches()// Settings cache was populated in init() (via applySafeConfigEnvironmentVariables)// and again at captureHooksConfigSnapshot() above, both from the original dir's// .claude/settings.json. Re-read from the worktree and re-capture hooks.updateHooksConfigSnapshot()}// Background jobs - only critical registrations that must happen before first querylogForDiagnosticsNoPII('info', 'setup_background_jobs_starting')// Bundled skills/plugins are registered in main.tsx before the parallel// getCommands() kick — see comment there. Moved out of setup() because// the await points above (startUdsMessaging, ~20ms) meant getCommands()// raced ahead and memoized an empty bundledSkills list.if (!isBareMode()) {initSessionMemory() // Synchronous - registers hook, gate check happens lazilyif (feature('CONTEXT_COLLAPSE')) {/* eslint-disable @typescript-eslint/no-require-imports */;(require('./services/contextCollapse/index.js') as typeof import('./services/contextCollapse/index.js')).initContextCollapse()/* eslint-enable @typescript-eslint/no-require-imports */}}void lockCurrentVersion() // Lock current version to prevent deletion by other processeslogForDiagnosticsNoPII('info', 'setup_background_jobs_launched')profileCheckpoint('setup_before_prefetch')// Pre-fetch promises - only items needed before renderlogForDiagnosticsNoPII('info', 'setup_prefetch_starting')// When CLAUDE_CODE_SYNC_PLUGIN_INSTALL is set, skip all plugin prefetch.// The sync install path in print.ts calls refreshPluginState() after// installing, which reloads commands, hooks, and agents. Prefetching here// races with the install (concurrent copyPluginToVersionedCache / cachePlugin// on the same directories), and the hot-reload handler fires clearPluginCache()// mid-install when policySettings arrives.const skipPluginPrefetch =(getIsNonInteractiveSession() &&isEnvTruthy(process.env.CLAUDE_CODE_SYNC_PLUGIN_INSTALL)) ||// --bare: loadPluginHooks → loadAllPlugins is filesystem work that's// wasted when executeHooks early-returns under --bare anyway.isBareMode()if (!skipPluginPrefetch) {void getCommands(getProjectRoot())}void import('./utils/plugins/loadPluginHooks.js').then(m => {if (!skipPluginPrefetch) {void m.loadPluginHooks() // Pre-load plugin hooks (consumed by processSessionStartHooks before render)m.setupPluginHookHotReload() // Set up hot reload for plugin hooks when settings change}})// --bare: skip attribution hook install + repo classification +// session-file-access analytics + team memory watcher. These are background// bookkeeping for commit attribution + usage metrics — scripted calls don't// commit code, and the 49ms attribution hook stat check (measured) is pure// overhead. NOT an early-return: the --dangerously-skip-permissions safety// gate, tengu_started beacon, and apiKeyHelper prefetch below must still run.if (!isBareMode()) {if (process.env.USER_TYPE === 'ant') {// Prime repo classification cache for auto-undercover mode. Default is// undercover ON until proven internal; if this resolves to internal, clear// the prompt cache so the next turn picks up the OFF state.void import('./utils/commitAttribution.js').then(async m => {if (await m.isInternalModelRepo()) {const { clearSystemPromptSections } = await import('./constants/systemPromptSections.js')clearSystemPromptSections()}})}if (feature('COMMIT_ATTRIBUTION')) {// Dynamic import to enable dead code elimination (module contains excluded strings).// Defer to next tick so the git subprocess spawn runs after first render// rather than during the setup() microtask window.setImmediate(() => {void import('./utils/attributionHooks.js').then(({ registerAttributionHooks }) => {registerAttributionHooks() // Register attribution tracking hooks (ant-only feature)},)})}void import('./utils/sessionFileAccessHooks.js').then(m =>m.registerSessionFileAccessHooks(),) // Register session file access analytics hooksif (feature('TEAMMEM')) {void import('./services/teamMemorySync/watcher.js').then(m =>m.startTeamMemoryWatcher(),) // Start team memory sync watcher}}initSinks() // Attach error log + analytics sinks and drain queued events// Session-success-rate denominator. Emit immediately after the analytics// sink is attached — before any parsing, fetching, or I/O that could throw.// inc-3694 (P0 CHANGELOG crash) threw at checkForReleaseNotes below; every// event after this point was dead. This beacon is the earliest reliable// "process started" signal for release health monitoring.logEvent('tengu_started', {})void prefetchApiKeyFromApiKeyHelperIfSafe(getIsNonInteractiveSession()) // Prefetch safely - only executes if trust already confirmedprofileCheckpoint('setup_after_prefetch')// Pre-fetch data for Logo v2 - await to ensure it's ready before logo renders.// --bare / SIMPLE: skip — release notes are interactive-UI display data,// and getRecentActivity() reads up to 10 session JSONL files.if (!isBareMode()) {const { hasReleaseNotes } = await checkForReleaseNotes(getGlobalConfig().lastReleaseNotesSeen,)if (hasReleaseNotes) {await getRecentActivity()}}// If permission mode is set to bypass, verify we're in a safe environmentif (permissionMode === 'bypassPermissions' ||allowDangerouslySkipPermissions) {// Check if running as root/sudo on Unix-like systems// Allow root if in a sandbox (e.g., TPU devspaces that require root)if (process.platform !== 'win32' &&typeof process.getuid === 'function' &&process.getuid() === 0 &&process.env.IS_SANDBOX !== '1' &&!isEnvTruthy(process.env.CLAUDE_CODE_BUBBLEWRAP)) {// biome-ignore lint/suspicious/noConsole:: intentional console outputconsole.error(`--dangerously-skip-permissions cannot be used with root/sudo privileges for security reasons`,)process.exit(1)}if (process.env.USER_TYPE === 'ant' &&// Skip for Desktop's local agent mode — same trust model as CCR/BYOC// (trusted Anthropic-managed launcher intentionally pre-approving everything).// Precedent: permissionSetup.ts:861, applySettingsChange.ts:55 (PR #19116)process.env.CLAUDE_CODE_ENTRYPOINT !== 'local-agent' &&// Same for CCD (Claude Code in Desktop) — apps#29127 passes the flag// unconditionally to unlock mid-session bypass switchingprocess.env.CLAUDE_CODE_ENTRYPOINT !== 'claude-desktop') {// Only await if permission mode is set to bypassconst [isDocker, hasInternet] = await Promise.all([envDynamic.getIsDocker(),env.hasInternetAccess(),])const isBubblewrap = envDynamic.getIsBubblewrapSandbox()const isSandbox = process.env.IS_SANDBOX === '1'const isSandboxed = isDocker || isBubblewrap || isSandboxif (!isSandboxed || hasInternet) {// biome-ignore lint/suspicious/noConsole:: intentional console outputconsole.error(`--dangerously-skip-permissions can only be used in Docker/sandbox containers with no internet access but got Docker: ${isDocker}, Bubblewrap: ${isBubblewrap}, IS_SANDBOX: ${isSandbox}, hasInternet: ${hasInternet}`,)process.exit(1)}}}if (process.env.NODE_ENV === 'test') {return}// Log tengu_exit event from the last session?const projectConfig = getCurrentProjectConfig()if (projectConfig.lastCost !== undefined &&projectConfig.lastDuration !== undefined) {logEvent('tengu_exit', {last_session_cost: projectConfig.lastCost,last_session_api_duration: projectConfig.lastAPIDuration,last_session_tool_duration: projectConfig.lastToolDuration,last_session_duration: projectConfig.lastDuration,last_session_lines_added: projectConfig.lastLinesAdded,last_session_lines_removed: projectConfig.lastLinesRemoved,last_session_total_input_tokens: projectConfig.lastTotalInputTokens,last_session_total_output_tokens: projectConfig.lastTotalOutputTokens,last_session_total_cache_creation_input_tokens:projectConfig.lastTotalCacheCreationInputTokens,last_session_total_cache_read_input_tokens:projectConfig.lastTotalCacheReadInputTokens,last_session_fps_average: projectConfig.lastFpsAverage,last_session_fps_low_1_pct: projectConfig.lastFpsLow1Pct,last_session_id:projectConfig.lastSessionId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,...projectConfig.lastSessionMetrics,})// Note: We intentionally don't clear these values after logging.// They're needed for cost restoration when resuming sessions.// The values will be overwritten when the next session exits.}}
此处可能存在不合适展示的内容,页面不予展示。您可通过相关编辑功能自查并修改。
如您确认内容无涉及 不当用语 / 纯广告导流 / 暴力 / 低俗色情 / 侵权 / 盗版 / 虚假 / 无价值内容或违法国家有关法律法规的内容,可点击提交进行申诉,我们将尽快为您处理。