212 lines
8.6 KiB
TypeScript
212 lines
8.6 KiB
TypeScript
import { profileCheckpoint } from '../utils/startupProfiler.js'
|
|
import '../bootstrap/state.js'
|
|
import '../utils/config.js'
|
|
import memoize from 'lodash-es/memoize.js'
|
|
import { getIsNonInteractiveSession } from 'src/bootstrap/state.js'
|
|
import { shutdownLspServerManager } from '../services/lsp/manager.js'
|
|
import { populateOAuthAccountInfoIfNeeded } from '../services/oauth/client.js'
|
|
import {
|
|
initializePolicyLimitsLoadingPromise,
|
|
isPolicyLimitsEligible,
|
|
} from '../services/policyLimits/index.js'
|
|
import {
|
|
initializeRemoteManagedSettingsLoadingPromise,
|
|
isEligibleForRemoteManagedSettings,
|
|
waitForRemoteManagedSettingsToLoad,
|
|
} from '../services/remoteManagedSettings/index.js'
|
|
import { preconnectAnthropicApi } from '../utils/apiPreconnect.js'
|
|
import { applyExtraCACertsFromConfig } from '../utils/caCertsConfig.js'
|
|
import { registerCleanup } from '../utils/cleanupRegistry.js'
|
|
import { enableConfigs, recordFirstStartTime } from '../utils/config.js'
|
|
import { logForDebugging } from '../utils/debug.js'
|
|
import { detectCurrentRepository } from '../utils/detectRepository.js'
|
|
import { logForDiagnosticsNoPII } from '../utils/diagLogs.js'
|
|
import { initJetBrainsDetection } from '../utils/envDynamic.js'
|
|
import { isEnvTruthy } from '../utils/envUtils.js'
|
|
import { ConfigParseError, errorMessage } from '../utils/errors.js'
|
|
// showInvalidConfigDialog is dynamically imported in the error path to avoid loading React at init
|
|
import {
|
|
gracefulShutdownSync,
|
|
setupGracefulShutdown,
|
|
} from '../utils/gracefulShutdown.js'
|
|
import {
|
|
applyConfigEnvironmentVariables,
|
|
applySafeConfigEnvironmentVariables,
|
|
} from '../utils/managedEnv.js'
|
|
import { configureGlobalMTLS } from '../utils/mtls.js'
|
|
import {
|
|
ensureScratchpadDir,
|
|
isScratchpadEnabled,
|
|
} from '../utils/permissions/filesystem.js'
|
|
import { configureGlobalAgents } from '../utils/proxy.js'
|
|
import { setShellIfWindows } from '../utils/windowsPaths.js'
|
|
|
|
export const init = memoize(async (): Promise<void> => {
|
|
const initStartTime = Date.now()
|
|
logForDiagnosticsNoPII('info', 'init_started')
|
|
profileCheckpoint('init_function_start')
|
|
|
|
// Validate configs are valid and enable configuration system
|
|
try {
|
|
const configsStart = Date.now()
|
|
enableConfigs()
|
|
logForDiagnosticsNoPII('info', 'init_configs_enabled', {
|
|
duration_ms: Date.now() - configsStart,
|
|
})
|
|
profileCheckpoint('init_configs_enabled')
|
|
|
|
// Apply only safe environment variables before trust dialog
|
|
// Full environment variables are applied after trust is established
|
|
const envVarsStart = Date.now()
|
|
applySafeConfigEnvironmentVariables()
|
|
|
|
// Apply NODE_EXTRA_CA_CERTS from settings.json to process.env early,
|
|
// before any TLS connections. Bun caches the TLS cert store at boot
|
|
// via BoringSSL, so this must happen before the first TLS handshake.
|
|
applyExtraCACertsFromConfig()
|
|
|
|
logForDiagnosticsNoPII('info', 'init_safe_env_vars_applied', {
|
|
duration_ms: Date.now() - envVarsStart,
|
|
})
|
|
profileCheckpoint('init_safe_env_vars_applied')
|
|
|
|
// Make sure things get flushed on exit
|
|
setupGracefulShutdown()
|
|
profileCheckpoint('init_after_graceful_shutdown')
|
|
|
|
// Telemetry/log export is disabled in this build. Keep the startup
|
|
// checkpoint so callers depending on the init timeline still see it.
|
|
profileCheckpoint('init_after_1p_event_logging')
|
|
|
|
// Populate OAuth account info if it is not already cached in config. This is needed since the
|
|
// OAuth account info may not be populated when logging in through the VSCode extension.
|
|
void populateOAuthAccountInfoIfNeeded()
|
|
profileCheckpoint('init_after_oauth_populate')
|
|
|
|
// Initialize JetBrains IDE detection asynchronously (populates cache for later sync access)
|
|
void initJetBrainsDetection()
|
|
profileCheckpoint('init_after_jetbrains_detection')
|
|
|
|
// Detect GitHub repository asynchronously (populates cache for gitDiff PR linking)
|
|
void detectCurrentRepository()
|
|
|
|
// Initialize the loading promise early so that other systems (like plugin hooks)
|
|
// can await remote settings loading. The promise includes a timeout to prevent
|
|
// deadlocks if loadRemoteManagedSettings() is never called (e.g., Agent SDK tests).
|
|
if (isEligibleForRemoteManagedSettings()) {
|
|
initializeRemoteManagedSettingsLoadingPromise()
|
|
}
|
|
if (isPolicyLimitsEligible()) {
|
|
initializePolicyLimitsLoadingPromise()
|
|
}
|
|
profileCheckpoint('init_after_remote_settings_check')
|
|
|
|
// Record the first start time
|
|
recordFirstStartTime()
|
|
|
|
// Configure global mTLS settings
|
|
const mtlsStart = Date.now()
|
|
logForDebugging('[init] configureGlobalMTLS starting')
|
|
configureGlobalMTLS()
|
|
logForDiagnosticsNoPII('info', 'init_mtls_configured', {
|
|
duration_ms: Date.now() - mtlsStart,
|
|
})
|
|
logForDebugging('[init] configureGlobalMTLS complete')
|
|
|
|
// Configure global HTTP agents (proxy and/or mTLS)
|
|
const proxyStart = Date.now()
|
|
logForDebugging('[init] configureGlobalAgents starting')
|
|
configureGlobalAgents()
|
|
logForDiagnosticsNoPII('info', 'init_proxy_configured', {
|
|
duration_ms: Date.now() - proxyStart,
|
|
})
|
|
logForDebugging('[init] configureGlobalAgents complete')
|
|
profileCheckpoint('init_network_configured')
|
|
|
|
// Preconnect to the Anthropic API — overlap TCP+TLS handshake
|
|
// (~100-200ms) with the ~100ms of action-handler work before the API
|
|
// request. After CA certs + proxy agents are configured so the warmed
|
|
// connection uses the right transport. Fire-and-forget; skipped for
|
|
// proxy/mTLS/unix/cloud-provider where the SDK's dispatcher wouldn't
|
|
// reuse the global pool.
|
|
preconnectAnthropicApi()
|
|
|
|
// CCR upstreamproxy: start the local CONNECT relay so agent subprocesses
|
|
// can reach org-configured upstreams with credential injection. Gated on
|
|
// CLAUDE_CODE_REMOTE + GrowthBook; fail-open on any error. Lazy import so
|
|
// non-CCR startups don't pay the module load. The getUpstreamProxyEnv
|
|
// function is registered with subprocessEnv.ts so subprocess spawning can
|
|
// inject proxy vars without a static import of the upstreamproxy module.
|
|
if (isEnvTruthy(process.env.CLAUDE_CODE_REMOTE)) {
|
|
try {
|
|
const { initUpstreamProxy, getUpstreamProxyEnv } = await import(
|
|
'../upstreamproxy/upstreamproxy.js'
|
|
)
|
|
const { registerUpstreamProxyEnvFn } = await import(
|
|
'../utils/subprocessEnv.js'
|
|
)
|
|
registerUpstreamProxyEnvFn(getUpstreamProxyEnv)
|
|
await initUpstreamProxy()
|
|
} catch (err) {
|
|
logForDebugging(
|
|
`[init] upstreamproxy init failed: ${err instanceof Error ? err.message : String(err)}; continuing without proxy`,
|
|
{ level: 'warn' },
|
|
)
|
|
}
|
|
}
|
|
|
|
// Set up git-bash if relevant
|
|
setShellIfWindows()
|
|
|
|
// Register LSP manager cleanup (initialization happens in main.tsx after --plugin-dir is processed)
|
|
registerCleanup(shutdownLspServerManager)
|
|
|
|
// gh-32730: teams created by subagents (or main agent without
|
|
// explicit TeamDelete) were left on disk forever. Register cleanup
|
|
// for all teams created this session. Lazy import: swarm code is
|
|
// behind feature gate and most sessions never create teams.
|
|
registerCleanup(async () => {
|
|
const { cleanupSessionTeams } = await import(
|
|
'../utils/swarm/teamHelpers.js'
|
|
)
|
|
await cleanupSessionTeams()
|
|
})
|
|
|
|
// Initialize scratchpad directory if enabled
|
|
if (isScratchpadEnabled()) {
|
|
const scratchpadStart = Date.now()
|
|
await ensureScratchpadDir()
|
|
logForDiagnosticsNoPII('info', 'init_scratchpad_created', {
|
|
duration_ms: Date.now() - scratchpadStart,
|
|
})
|
|
}
|
|
|
|
logForDiagnosticsNoPII('info', 'init_completed', {
|
|
duration_ms: Date.now() - initStartTime,
|
|
})
|
|
profileCheckpoint('init_function_end')
|
|
} catch (error) {
|
|
if (error instanceof ConfigParseError) {
|
|
// Skip the interactive Ink dialog when we can't safely render it.
|
|
// The dialog breaks JSON consumers (e.g. desktop marketplace plugin
|
|
// manager running `plugin marketplace list --json` in a VM sandbox).
|
|
if (getIsNonInteractiveSession()) {
|
|
process.stderr.write(
|
|
`Configuration error in ${error.filePath}: ${error.message}\n`,
|
|
)
|
|
gracefulShutdownSync(1)
|
|
return
|
|
}
|
|
|
|
// Show the invalid config dialog with the error object and wait for it to complete
|
|
return import('../components/InvalidConfigDialog.js').then(m =>
|
|
m.showInvalidConfigDialog({ error }),
|
|
)
|
|
// Dialog itself handles process.exit, so we don't need additional cleanup here
|
|
} else {
|
|
// For non-config errors, rethrow them
|
|
throw error
|
|
}
|
|
}
|
|
})
|