Download src/setup.ts from chenbhao/codev: direct link, hf CLI and curl.
- Browser
- Download file 21 kB
-
https://huggingface.co/chenbhao/codev/resolve/e91c8197c9918ff9e5df3140e8c36282bb30e9a2/src/setup.ts
- Command line
-
hf download hf://chenbhao/codev@e91c8197c9918ff9e5df3140e8c36282bb30e9a2/src/setup.ts
-
curl -L -o setup.ts https://huggingface.co/chenbhao/codev/resolve/e91c8197c9918ff9e5df3140e8c36282bb30e9a2/src/setup.ts
21 kB
| /* 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 < 18 | |
| const nodeVersion = process.version.match(/^v(\d+)\./)?.[1] | |
| if (!nodeVersion || parseInt(nodeVersion) < 18) { | |
| // biome-ignore lint/suspicious/noConsole:: intentional console output | |
| console.error( | |
| chalk.bold.red( | |
| 'Error: Claude Code requires Node.js version 18 or higher.', | |
| ), | |
| ) | |
| process.exit(1) | |
| } | |
| // Set custom session ID if provided | |
| if (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 enabled | |
| if (isAgentSwarmsEnabled()) { | |
| const restoredIterm2Backup = await checkAndRestoreITerm2Backup() | |
| if (restoredIterm2Backup.status === 'restored') { | |
| // biome-ignore lint/suspicious/noConsole:: intentional console output | |
| console.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 output | |
| console.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 interrupted | |
| try { | |
| const restoredTerminalBackup = await checkAndRestoreTerminalBackup() | |
| if (restoredTerminalBackup.status === 'restored') { | |
| // biome-ignore lint/suspicious/noConsole:: intentional console output | |
| console.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 output | |
| console.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 fails | |
| logError(error) | |
| } | |
| } | |
| // IMPORTANT: setCwd() must be called before any other code that depends on the cwd | |
| setCwd(cwd) | |
| // Capture hooks configuration snapshot to avoid hidden hook modifications. | |
| // IMPORTANT: Must be called AFTER setCwd() so hooks are loaded from the correct directory | |
| const hooksStart = Date.now() | |
| captureHooksConfigSnapshot() | |
| logForDiagnosticsNoPII('info', 'setup_hooks_captured', { | |
| duration_ms: Date.now() - hooksStart, | |
| }) | |
| // Initialize FileChanged hook watcher — sync, reads hook config snapshot | |
| initializeFileChangedWatcher(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 | undefined | |
| if (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 creation | |
| if (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 enabled | |
| if (tmuxEnabled && tmuxSessionName) { | |
| const tmuxResult = await createTmuxSessionForWorktree( | |
| tmuxSessionName, | |
| worktreeSession.worktreePath, | |
| ) | |
| if (tmuxResult.created) { | |
| // biome-ignore lint/suspicious/noConsole:: intentional console output | |
| console.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 output | |
| console.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 changed | |
| clearMemoryFileCaches() | |
| // 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 query | |
| logForDiagnosticsNoPII('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 lazily | |
| if (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 processes | |
| logForDiagnosticsNoPII('info', 'setup_background_jobs_launched') | |
| profileCheckpoint('setup_before_prefetch') | |
| // Pre-fetch promises - only items needed before render | |
| logForDiagnosticsNoPII('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 hooks | |
| if (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 confirmed | |
| profileCheckpoint('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() | |
| } | |
| } | |
| // Start background fetch of OpenRouter models if configured | |
| // This is non-blocking and runs in the background | |
| const { getConfiguredAuthProvider } = await import('./utils/auth.js') | |
| if (getConfiguredAuthProvider() === 'openrouter') { | |
| const { startOpenRouterModelsFetch } = await import('./utils/model/openRouterModels.js') | |
| startOpenRouterModelsFetch() | |
| } | |
| // If permission mode is set to bypass, verify we're in a safe environment | |
| if ( | |
| 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 output | |
| console.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 switching | |
| process.env.CLAUDE_CODE_ENTRYPOINT !== 'claude-desktop' | |
| ) { | |
| // Only await if permission mode is set to bypass | |
| const [isDocker, hasInternet] = await Promise.all([ | |
| envDynamic.getIsDocker(), | |
| env.hasInternetAccess(), | |
| ]) | |
| const isBubblewrap = envDynamic.getIsBubblewrapSandbox() | |
| const isSandbox = process.env.IS_SANDBOX === '1' | |
| const isSandboxed = isDocker || isBubblewrap || isSandbox | |
| if (!isSandboxed || hasInternet) { | |
| // biome-ignore lint/suspicious/noConsole:: intentional console output | |
| console.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. | |
| } | |
| } | |