init claude-code
This commit is contained in:
@@ -0,0 +1,464 @@
|
||||
import { getIsNonInteractiveSession } from '../../../bootstrap/state.js'
|
||||
import { logForDebugging } from '../../../utils/debug.js'
|
||||
import { getPlatform } from '../../../utils/platform.js'
|
||||
import {
|
||||
isInITerm2,
|
||||
isInsideTmux,
|
||||
isInsideTmuxSync,
|
||||
isIt2CliAvailable,
|
||||
isTmuxAvailable,
|
||||
} from './detection.js'
|
||||
import { createInProcessBackend } from './InProcessBackend.js'
|
||||
import { getPreferTmuxOverIterm2 } from './it2Setup.js'
|
||||
import { createPaneBackendExecutor } from './PaneBackendExecutor.js'
|
||||
import { getTeammateModeFromSnapshot } from './teammateModeSnapshot.js'
|
||||
import type {
|
||||
BackendDetectionResult,
|
||||
PaneBackend,
|
||||
PaneBackendType,
|
||||
TeammateExecutor,
|
||||
} from './types.js'
|
||||
|
||||
/**
|
||||
* Cached backend detection result.
|
||||
* Once detected, the backend selection is fixed for the lifetime of the process.
|
||||
*/
|
||||
let cachedBackend: PaneBackend | null = null
|
||||
|
||||
/**
|
||||
* Cached detection result with additional metadata.
|
||||
*/
|
||||
let cachedDetectionResult: BackendDetectionResult | null = null
|
||||
|
||||
/**
|
||||
* Flag to track if backends have been registered.
|
||||
*/
|
||||
let backendsRegistered = false
|
||||
|
||||
/**
|
||||
* Cached in-process backend instance.
|
||||
*/
|
||||
let cachedInProcessBackend: TeammateExecutor | null = null
|
||||
|
||||
/**
|
||||
* Cached pane backend executor instance.
|
||||
* Wraps the detected PaneBackend to provide TeammateExecutor interface.
|
||||
*/
|
||||
let cachedPaneBackendExecutor: TeammateExecutor | null = null
|
||||
|
||||
/**
|
||||
* Tracks whether spawn fell back to in-process mode because no pane backend
|
||||
* was available (e.g., iTerm2 without it2 or tmux installed). Once set,
|
||||
* isInProcessEnabled() returns true so UI (banner, teams menu) reflects reality.
|
||||
*/
|
||||
let inProcessFallbackActive = false
|
||||
|
||||
/**
|
||||
* Placeholder for TmuxBackend - will be replaced with actual implementation.
|
||||
* This allows the registry to compile before the backend implementations exist.
|
||||
*/
|
||||
let TmuxBackendClass: (new () => PaneBackend) | null = null
|
||||
|
||||
/**
|
||||
* Placeholder for ITermBackend - will be replaced with actual implementation.
|
||||
* This allows the registry to compile before the backend implementations exist.
|
||||
*/
|
||||
let ITermBackendClass: (new () => PaneBackend) | null = null
|
||||
|
||||
/**
|
||||
* Ensures backend classes are dynamically imported so getBackendByType() can
|
||||
* construct them. Unlike detectAndGetBackend(), this never spawns subprocesses
|
||||
* and never throws — it's the lightweight option when you only need class
|
||||
* registration (e.g., killing a pane by its stored backendType).
|
||||
*/
|
||||
export async function ensureBackendsRegistered(): Promise<void> {
|
||||
if (backendsRegistered) return
|
||||
await import('./TmuxBackend.js')
|
||||
await import('./ITermBackend.js')
|
||||
backendsRegistered = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the TmuxBackend class with the registry.
|
||||
* Called by TmuxBackend.ts to avoid circular dependencies.
|
||||
*/
|
||||
export function registerTmuxBackend(backendClass: new () => PaneBackend): void {
|
||||
TmuxBackendClass = backendClass
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the ITermBackend class with the registry.
|
||||
* Called by ITermBackend.ts to avoid circular dependencies.
|
||||
*/
|
||||
export function registerITermBackend(
|
||||
backendClass: new () => PaneBackend,
|
||||
): void {
|
||||
logForDebugging(
|
||||
`[registry] registerITermBackend called, class=${backendClass?.name || 'undefined'}`,
|
||||
)
|
||||
ITermBackendClass = backendClass
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a TmuxBackend instance.
|
||||
* Throws if TmuxBackend hasn't been registered.
|
||||
*/
|
||||
function createTmuxBackend(): PaneBackend {
|
||||
if (!TmuxBackendClass) {
|
||||
throw new Error(
|
||||
'TmuxBackend not registered. Import TmuxBackend.ts before using the registry.',
|
||||
)
|
||||
}
|
||||
return new TmuxBackendClass()
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an ITermBackend instance.
|
||||
* Throws if ITermBackend hasn't been registered.
|
||||
*/
|
||||
function createITermBackend(): PaneBackend {
|
||||
if (!ITermBackendClass) {
|
||||
throw new Error(
|
||||
'ITermBackend not registered. Import ITermBackend.ts before using the registry.',
|
||||
)
|
||||
}
|
||||
return new ITermBackendClass()
|
||||
}
|
||||
|
||||
/**
|
||||
* Detection priority flow:
|
||||
* 1. If inside tmux, always use tmux (even in iTerm2)
|
||||
* 2. If in iTerm2 with it2 available, use iTerm2 backend
|
||||
* 3. If in iTerm2 without it2, return result indicating setup needed
|
||||
* 4. If tmux available, use tmux (creates external session)
|
||||
* 5. Otherwise, throw error with instructions
|
||||
*/
|
||||
export async function detectAndGetBackend(): Promise<BackendDetectionResult> {
|
||||
// Ensure backends are registered before detection
|
||||
await ensureBackendsRegistered()
|
||||
|
||||
// Return cached result if available
|
||||
if (cachedDetectionResult) {
|
||||
logForDebugging(
|
||||
`[BackendRegistry] Using cached backend: ${cachedDetectionResult.backend.type}`,
|
||||
)
|
||||
return cachedDetectionResult
|
||||
}
|
||||
|
||||
logForDebugging('[BackendRegistry] Starting backend detection...')
|
||||
|
||||
// Check all environment conditions upfront for logging
|
||||
const insideTmux = await isInsideTmux()
|
||||
const inITerm2 = isInITerm2()
|
||||
|
||||
logForDebugging(
|
||||
`[BackendRegistry] Environment: insideTmux=${insideTmux}, inITerm2=${inITerm2}`,
|
||||
)
|
||||
|
||||
// Priority 1: If inside tmux, always use tmux
|
||||
if (insideTmux) {
|
||||
logForDebugging(
|
||||
'[BackendRegistry] Selected: tmux (running inside tmux session)',
|
||||
)
|
||||
const backend = createTmuxBackend()
|
||||
cachedBackend = backend
|
||||
cachedDetectionResult = {
|
||||
backend,
|
||||
isNative: true,
|
||||
needsIt2Setup: false,
|
||||
}
|
||||
return cachedDetectionResult
|
||||
}
|
||||
|
||||
// Priority 2: If in iTerm2, try to use native panes
|
||||
if (inITerm2) {
|
||||
// Check if user previously chose to prefer tmux over iTerm2
|
||||
const preferTmux = getPreferTmuxOverIterm2()
|
||||
if (preferTmux) {
|
||||
logForDebugging(
|
||||
'[BackendRegistry] User prefers tmux over iTerm2, skipping iTerm2 detection',
|
||||
)
|
||||
} else {
|
||||
const it2Available = await isIt2CliAvailable()
|
||||
logForDebugging(
|
||||
`[BackendRegistry] iTerm2 detected, it2 CLI available: ${it2Available}`,
|
||||
)
|
||||
|
||||
if (it2Available) {
|
||||
logForDebugging(
|
||||
'[BackendRegistry] Selected: iterm2 (native iTerm2 with it2 CLI)',
|
||||
)
|
||||
const backend = createITermBackend()
|
||||
cachedBackend = backend
|
||||
cachedDetectionResult = {
|
||||
backend,
|
||||
isNative: true,
|
||||
needsIt2Setup: false,
|
||||
}
|
||||
return cachedDetectionResult
|
||||
}
|
||||
}
|
||||
|
||||
// In iTerm2 but it2 not available - check if tmux can be used as fallback
|
||||
const tmuxAvailable = await isTmuxAvailable()
|
||||
logForDebugging(
|
||||
`[BackendRegistry] it2 not available, tmux available: ${tmuxAvailable}`,
|
||||
)
|
||||
|
||||
if (tmuxAvailable) {
|
||||
logForDebugging(
|
||||
'[BackendRegistry] Selected: tmux (fallback in iTerm2, it2 setup recommended)',
|
||||
)
|
||||
// Return tmux as fallback. Only signal it2 setup if the user hasn't already
|
||||
// chosen to prefer tmux - otherwise they'd be re-prompted on every spawn.
|
||||
const backend = createTmuxBackend()
|
||||
cachedBackend = backend
|
||||
cachedDetectionResult = {
|
||||
backend,
|
||||
isNative: false,
|
||||
needsIt2Setup: !preferTmux,
|
||||
}
|
||||
return cachedDetectionResult
|
||||
}
|
||||
|
||||
// In iTerm2 with no it2 and no tmux - it2 setup is required
|
||||
logForDebugging(
|
||||
'[BackendRegistry] ERROR: iTerm2 detected but no it2 CLI and no tmux',
|
||||
)
|
||||
throw new Error(
|
||||
'iTerm2 detected but it2 CLI not installed. Install it2 with: pip install it2',
|
||||
)
|
||||
}
|
||||
|
||||
// Priority 3: Fall back to tmux external session
|
||||
const tmuxAvailable = await isTmuxAvailable()
|
||||
logForDebugging(
|
||||
`[BackendRegistry] Not in tmux or iTerm2, tmux available: ${tmuxAvailable}`,
|
||||
)
|
||||
|
||||
if (tmuxAvailable) {
|
||||
logForDebugging('[BackendRegistry] Selected: tmux (external session mode)')
|
||||
const backend = createTmuxBackend()
|
||||
cachedBackend = backend
|
||||
cachedDetectionResult = {
|
||||
backend,
|
||||
isNative: false,
|
||||
needsIt2Setup: false,
|
||||
}
|
||||
return cachedDetectionResult
|
||||
}
|
||||
|
||||
// No backend available - tmux is not installed
|
||||
logForDebugging('[BackendRegistry] ERROR: No pane backend available')
|
||||
throw new Error(getTmuxInstallInstructions())
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns platform-specific tmux installation instructions.
|
||||
*/
|
||||
function getTmuxInstallInstructions(): string {
|
||||
const platform = getPlatform()
|
||||
|
||||
switch (platform) {
|
||||
case 'macos':
|
||||
return `To use agent swarms, install tmux:
|
||||
brew install tmux
|
||||
Then start a tmux session with: tmux new-session -s claude`
|
||||
|
||||
case 'linux':
|
||||
case 'wsl':
|
||||
return `To use agent swarms, install tmux:
|
||||
sudo apt install tmux # Ubuntu/Debian
|
||||
sudo dnf install tmux # Fedora/RHEL
|
||||
Then start a tmux session with: tmux new-session -s claude`
|
||||
|
||||
case 'windows':
|
||||
return `To use agent swarms, you need tmux which requires WSL (Windows Subsystem for Linux).
|
||||
Install WSL first, then inside WSL run:
|
||||
sudo apt install tmux
|
||||
Then start a tmux session with: tmux new-session -s claude`
|
||||
|
||||
default:
|
||||
return `To use agent swarms, install tmux using your system's package manager.
|
||||
Then start a tmux session with: tmux new-session -s claude`
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a backend by explicit type selection.
|
||||
* Useful for testing or when the user has a preference.
|
||||
*
|
||||
* @param type - The backend type to get
|
||||
* @returns The requested backend instance
|
||||
* @throws If the requested backend type is not available
|
||||
*/
|
||||
export function getBackendByType(type: PaneBackendType): PaneBackend {
|
||||
switch (type) {
|
||||
case 'tmux':
|
||||
return createTmuxBackend()
|
||||
case 'iterm2':
|
||||
return createITermBackend()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the currently cached backend, if any.
|
||||
* Returns null if no backend has been detected yet.
|
||||
*/
|
||||
export function getCachedBackend(): PaneBackend | null {
|
||||
return cachedBackend
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the cached backend detection result, if any.
|
||||
* Returns null if detection hasn't run yet.
|
||||
* Use `isNative` to check if teammates are visible in native panes.
|
||||
*/
|
||||
export function getCachedDetectionResult(): BackendDetectionResult | null {
|
||||
return cachedDetectionResult
|
||||
}
|
||||
|
||||
/**
|
||||
* Records that spawn fell back to in-process mode because no pane backend
|
||||
* was available. After this, isInProcessEnabled() returns true and subsequent
|
||||
* spawns short-circuit to in-process (the environment won't change mid-session).
|
||||
*/
|
||||
export function markInProcessFallback(): void {
|
||||
logForDebugging('[BackendRegistry] Marking in-process fallback as active')
|
||||
inProcessFallbackActive = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the teammate mode for this session.
|
||||
* Returns the session snapshot captured at startup, ignoring runtime config changes.
|
||||
*/
|
||||
function getTeammateMode(): 'auto' | 'tmux' | 'in-process' {
|
||||
return getTeammateModeFromSnapshot()
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if in-process teammate execution is enabled.
|
||||
*
|
||||
* Logic:
|
||||
* - If teammateMode is 'in-process', always enabled
|
||||
* - If teammateMode is 'tmux', always disabled (use pane backend)
|
||||
* - If teammateMode is 'auto' (default), check environment:
|
||||
* - If inside tmux, use pane backend (return false)
|
||||
* - If inside iTerm2, use pane backend (return false) - detectAndGetBackend()
|
||||
* will pick ITermBackend if it2 is available, or fall back to tmux
|
||||
* - Otherwise, use in-process (return true)
|
||||
*/
|
||||
export function isInProcessEnabled(): boolean {
|
||||
// Force in-process mode for non-interactive sessions (-p mode)
|
||||
// since tmux-based teammates don't make sense without a terminal UI
|
||||
if (getIsNonInteractiveSession()) {
|
||||
logForDebugging(
|
||||
'[BackendRegistry] isInProcessEnabled: true (non-interactive session)',
|
||||
)
|
||||
return true
|
||||
}
|
||||
|
||||
const mode = getTeammateMode()
|
||||
|
||||
let enabled: boolean
|
||||
if (mode === 'in-process') {
|
||||
enabled = true
|
||||
} else if (mode === 'tmux') {
|
||||
enabled = false
|
||||
} else {
|
||||
// 'auto' mode - if a prior spawn fell back to in-process because no pane
|
||||
// backend was available, stay in-process (scoped to auto mode only so a
|
||||
// mid-session Settings change to explicit 'tmux' still takes effect).
|
||||
if (inProcessFallbackActive) {
|
||||
logForDebugging(
|
||||
'[BackendRegistry] isInProcessEnabled: true (fallback after pane backend unavailable)',
|
||||
)
|
||||
return true
|
||||
}
|
||||
// Check if a pane backend environment is available
|
||||
// If inside tmux or iTerm2, use pane backend; otherwise use in-process
|
||||
const insideTmux = isInsideTmuxSync()
|
||||
const inITerm2 = isInITerm2()
|
||||
enabled = !insideTmux && !inITerm2
|
||||
}
|
||||
|
||||
logForDebugging(
|
||||
`[BackendRegistry] isInProcessEnabled: ${enabled} (mode=${mode}, insideTmux=${isInsideTmuxSync()}, inITerm2=${isInITerm2()})`,
|
||||
)
|
||||
return enabled
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the resolved teammate executor mode for this session.
|
||||
* Unlike getTeammateModeFromSnapshot which may return 'auto', this returns
|
||||
* what 'auto' actually resolves to given the current environment.
|
||||
*/
|
||||
export function getResolvedTeammateMode(): 'in-process' | 'tmux' {
|
||||
return isInProcessEnabled() ? 'in-process' : 'tmux'
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the InProcessBackend instance.
|
||||
* Creates and caches the instance on first call.
|
||||
*/
|
||||
export function getInProcessBackend(): TeammateExecutor {
|
||||
if (!cachedInProcessBackend) {
|
||||
cachedInProcessBackend = createInProcessBackend()
|
||||
}
|
||||
return cachedInProcessBackend
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a TeammateExecutor for spawning teammates.
|
||||
*
|
||||
* Returns either:
|
||||
* - InProcessBackend when preferInProcess is true and in-process mode is enabled
|
||||
* - PaneBackendExecutor wrapping the detected pane backend otherwise
|
||||
*
|
||||
* This provides a unified TeammateExecutor interface regardless of execution mode,
|
||||
* allowing callers to spawn and manage teammates without knowing the backend details.
|
||||
*
|
||||
* @param preferInProcess - If true and in-process is enabled, returns InProcessBackend.
|
||||
* Otherwise returns PaneBackendExecutor.
|
||||
* @returns TeammateExecutor instance
|
||||
*/
|
||||
export async function getTeammateExecutor(
|
||||
preferInProcess: boolean = false,
|
||||
): Promise<TeammateExecutor> {
|
||||
if (preferInProcess && isInProcessEnabled()) {
|
||||
logForDebugging('[BackendRegistry] Using in-process executor')
|
||||
return getInProcessBackend()
|
||||
}
|
||||
|
||||
// Return pane backend executor
|
||||
logForDebugging('[BackendRegistry] Using pane backend executor')
|
||||
return getPaneBackendExecutor()
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the PaneBackendExecutor instance.
|
||||
* Creates and caches the instance on first call, detecting the appropriate pane backend.
|
||||
*/
|
||||
async function getPaneBackendExecutor(): Promise<TeammateExecutor> {
|
||||
if (!cachedPaneBackendExecutor) {
|
||||
const detection = await detectAndGetBackend()
|
||||
cachedPaneBackendExecutor = createPaneBackendExecutor(detection.backend)
|
||||
logForDebugging(
|
||||
`[BackendRegistry] Created PaneBackendExecutor wrapping ${detection.backend.type}`,
|
||||
)
|
||||
}
|
||||
return cachedPaneBackendExecutor
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the backend detection cache.
|
||||
* Used for testing to allow re-detection.
|
||||
*/
|
||||
export function resetBackendDetection(): void {
|
||||
cachedBackend = null
|
||||
cachedDetectionResult = null
|
||||
cachedInProcessBackend = null
|
||||
cachedPaneBackendExecutor = null
|
||||
backendsRegistered = false
|
||||
inProcessFallbackActive = false
|
||||
}
|
||||
Reference in New Issue
Block a user