import { z } from 'zod';
import { resolve, join } from 'node:path';
import { existsSync } from 'node:fs';
import { getServiceUrls } from '@agentuity/server';
import { createCommand } from '../../types';
import { startBunDevServer } from '../build/vite/bun-dev-server';
import { startViteAssetServer } from '../build/vite/vite-asset-server';
import * as tui from '../../tui';
import { getCommand } from '../../command-prefix';
import { generateEndpoint } from './api';
import { APIClient, getAPIBaseURL, getAppBaseURL, getGravityDevModeURL } from '../../api';
import { download } from './download';
import { createDevmodeSyncService } from './sync';
import { getDevmodeDeploymentId } from '../build/ids';
import { getDefaultConfigDir, saveConfig, loadProjectSDKKey, getAuth } from '../../config';
import { typecheck } from '../build/typecheck';
import { validateGravityRequiresUpgrade } from '../../runtime';
import { isTTY, hasLoggedInBefore } from '../../auth';
import { prepareDevLock, releaseLockSync } from './dev-lock';
import { checkAndUpgradeDependencies } from '../../utils/dependency-checker';
import { initProcessManager } from './process-manager';
import { detectVersionMismatch, formatVersionMismatchWarning } from '../../utils/version-mismatch';
import { ErrorCode } from '../../errors';
const DEFAULT_PORT = 3500;
const MIN_PORT = 1024;
const MAX_PORT = 65535;
/**
 * Kill any lingering gravity processes from previous dev sessions.
 * This is a defensive measure to clean up orphaned processes.
 *
 * When a projectId is provided, only kills gravity processes for that specific
 * project. Otherwise falls back to killing all gravity processes (used during
 * startup before project info is available).
 */
async function killLingeringGravityProcesses(logger, projectId) {
    // Only attempt on Unix-like systems (macOS, Linux)
    if (process.platform === 'win32') {
        return;
    }
    try {
        // Scope the pkill pattern to the specific project when possible,
        // avoiding killing gravity processes from other dev sessions.
        const pattern = projectId ? `gravity.*--project-id.*${projectId}` : 'gravity.*--endpoint-id';
        const result = Bun.spawnSync(['pkill', '-f', pattern], {
            stdout: 'ignore',
            stderr: 'ignore',
        });
        // Exit code 0 = processes killed, 1 = no matching processes, other = error
        if (result.exitCode === 0) {
            logger.debug('Killed lingering gravity processes%s from previous session', projectId ? ` (project ${projectId})` : '');
            // Brief pause to let processes fully terminate
            await new Promise((resolve) => setTimeout(resolve, 100));
        }
        else if (result.exitCode === 1) {
            logger.debug('no lingering gravity processes found');
        }
    }
    catch {
        // pkill not available or failed - not critical, continue
    }
}
/**
 * Kill the Bun backend subprocess if one is running.
 *
 * @param forceKill - If true, sends SIGKILL instead of SIGTERM. Used in
 *   process.on('exit') handlers where there's no time for graceful shutdown.
 *   Also kills the entire process tree to prevent orphaned child processes.
 */
function killBunSubprocess(logger, forceKill = false) {
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const globalAny = globalThis;
    const bunSubprocess = globalAny.__AGENTUITY_BUN_SUBPROCESS__;
    if (!bunSubprocess)
        return;
    const signal = forceKill ? 'SIGKILL' : 'SIGTERM';
    const pid = bunSubprocess.pid;
    // Kill the entire process tree if we have a PID (guard against dangerous PIDs)
    if (pid && pid > 1) {
        try {
            process.kill(-pid, signal);
            logger.debug('Sent %s to Bun subprocess process group -%d', signal, pid);
        }
        catch {
            // Process group kill failed, fall back to direct kill
            try {
                bunSubprocess.kill(signal);
                logger.debug('Sent %s to Bun subprocess pid %d (direct)', signal, pid);
            }
            catch (err) {
                logger.debug('Error killing Bun subprocess: %s', err);
            }
        }
    }
    else {
        try {
            bunSubprocess.kill(signal);
            logger.debug('Bun subprocess killed with %s', signal);
        }
        catch (err) {
            logger.debug('Error killing Bun subprocess: %s', err);
        }
    }
    globalAny.__AGENTUITY_BUN_SUBPROCESS__ = undefined;
}
const getDefaultPort = () => {
    const envPort = process.env.PORT;
    if (!envPort) {
        return DEFAULT_PORT;
    }
    const trimmed = envPort.trim();
    if (!trimmed || !/^\d+$/.test(trimmed)) {
        return DEFAULT_PORT;
    }
    const parsed = Number(trimmed);
    if (!Number.isInteger(parsed) || parsed < MIN_PORT || parsed > MAX_PORT) {
        return DEFAULT_PORT;
    }
    return parsed;
};
const shouldDisableInteractive = (interactive) => {
    if (!interactive) {
        return true;
    }
    return process.env.TERM_PROGRAM === 'vscode';
};
export const command = createCommand({
    name: 'dev',
    description: 'Build and run the development server',
    tags: ['mutating', 'slow', 'requires-project'],
    idempotent: true,
    examples: [
        { command: getCommand('dev'), description: 'Start development server' },
        { command: getCommand('dev --port 8080'), description: 'Specify custom port' },
        { command: getCommand('dev --local'), description: 'Run in local mode' },
        { command: getCommand('dev --no-public'), description: 'Disable public URL' },
    ],
    schema: {
        options: z.object({
            local: z.boolean().optional().describe('Turn on local services (instead of cloud)'),
            interactive: z.boolean().default(true).optional().describe('Turn on interactive mode'),
            public: z
                .boolean()
                .optional()
                .default(!process.env.CI)
                .describe('Turn on or off the public url'),
            port: z
                .number()
                .min(MIN_PORT)
                .max(MAX_PORT)
                .default(getDefaultPort())
                .describe('The TCP port to start the dev server (also reads from PORT env)'),
            inspect: z.boolean().optional().describe('Enable bun debugger on available port'),
            inspectWait: z
                .boolean()
                .optional()
                .describe('Enable bun debugger and wait for connection before executing'),
            inspectBrk: z
                .boolean()
                .optional()
                .describe('Enable bun debugger with breakpoint at first line'),
            noTypecheck: z
                .boolean()
                .optional()
                .describe('Skip TypeScript type checking on startup and restarts'),
            resume: z.string().optional().describe('Resume a paused Hub session by ID'),
        }),
    },
    optional: { project: true },
    async handler(ctx) {
        const { opts, logger, projectDir } = ctx;
        let { config, project } = ctx;
        // Get auth state - we handle auth ourselves based on project state
        let auth = await getAuth();
        const rootDir = resolve(projectDir);
        const appTs = join(rootDir, 'app.ts');
        const srcDir = join(rootDir, 'src');
        // Verify required files exist
        const mustHaves = [join(rootDir, 'package.json'), appTs, srcDir];
        const missing = [];
        const interactive = !shouldDisableInteractive(opts.interactive);
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
        let originalExit = globalThis.AGENTUITY_PROCESS_EXIT;
        if (!originalExit) {
            originalExit = process.exit.bind(process);
        }
        for (const filename of mustHaves) {
            if (!existsSync(filename)) {
                missing.push(filename);
            }
        }
        if (missing.length) {
            tui.error(`${rootDir} does not appear to be a valid Agentuity project`);
            for (const filename of missing) {
                tui.bullet(`Missing ${filename}`);
            }
            originalExit(1);
        }
        // Handle authentication state based on project registration
        if (project) {
            // Registered project (has agentuity.json) - check if user needs to login
            const isValidAuth = auth && auth.expires > new Date();
            if (!isValidAuth) {
                if (isTTY()) {
                    const hasProfile = await hasLoggedInBefore();
                    const message = hasProfile
                        ? 'Your session has expired or you are not logged in.'
                        : 'This project is registered with Agentuity Cloud but you are not logged in.';
                    tui.warning(message);
                    tui.newline();
                    const shouldLogin = await tui.confirm(hasProfile
                        ? 'Would you like to login now?'
                        : 'Would you like to login or create an account?', true);
                    if (shouldLogin) {
                        tui.newline();
                        // Run login flow inline
                        const { loginCommand } = await import('../auth/login');
                        // Ensure apiClient is available for login handler
                        const loginCtx = ctx;
                        if (!loginCtx.apiClient) {
                            loginCtx.apiClient = new APIClient(getAPIBaseURL(config), logger, config);
                        }
                        if (loginCommand.handler) {
                            await loginCommand.handler(loginCtx);
                        }
                        // Refresh auth state after login
                        const freshAuth = await getAuth();
                        if (!freshAuth || freshAuth.expires <= new Date()) {
                            tui.fatal('Login was not completed successfully.', ErrorCode.AUTH_FAILED);
                        }
                        auth = freshAuth;
                        tui.newline();
                        tui.success('Login successful! Continuing with dev server...');
                        tui.newline();
                    }
                    else {
                        // User chose not to login - show warning about disabled features
                        tui.newline();
                        tui.showLoggedOutMessage(getAppBaseURL(config), hasProfile);
                    }
                }
                else {
                    // Non-TTY: fatal error with instruction
                    logger.fatal(`Authentication required for this project.\n` +
                        `Run "${getCommand('auth login')}" to login to Agentuity`, ErrorCode.AUTH_REQUIRED);
                }
            }
            // After auth is established, verify project access
            if (auth && config) {
                const { reconcileProject } = await import('../project/reconcile');
                const apiClient = new APIClient(getAPIBaseURL(config), logger, auth.apiKey, config);
                const result = await reconcileProject({
                    dir: rootDir,
                    auth,
                    apiClient,
                    config,
                    logger,
                    interactive: isTTY(),
                });
                if (result.status === 'error') {
                    tui.fatal(result.message, ErrorCode.PROJECT_NOT_FOUND);
                }
                else if (result.status === 'imported' && result.project) {
                    // Project was re-imported to user's org
                    project = result.project;
                    tui.newline();
                }
                else if (result.status === 'skipped') {
                    // User declined import - can't continue with cloud features
                    tui.warning('Continuing in local-only mode.');
                    project = undefined;
                }
            }
        }
        else {
            // No agentuity.json - check if this is a valid project that needs importing
            if (auth && config) {
                const { reconcileProject } = await import('../project/reconcile');
                const apiClient = new APIClient(getAPIBaseURL(config), logger, auth.apiKey, config);
                const result = await reconcileProject({
                    dir: rootDir,
                    auth,
                    apiClient,
                    config,
                    logger,
                    interactive: isTTY(),
                });
                if (result.status === 'error') {
                    // Not a valid project - show local-only warning
                    tui.showLocalOnlyWarning();
                }
                else if (result.status === 'imported' && result.project) {
                    // Project was imported - reload project config
                    project = result.project;
                    tui.newline();
                }
                else if (result.status === 'skipped') {
                    // User declined import - continue in local-only mode
                    tui.showLocalOnlyWarning();
                }
            }
            else {
                // Not authenticated - local-only mode
                tui.showLocalOnlyWarning();
            }
        }
        // Prepare dev lock: cleans up stale processes from previous sessions
        // and creates a new lockfile for this session
        const devLock = await prepareDevLock(rootDir, opts.port, logger);
        // Kill any lingering gravity processes from previous dev sessions.
        // Scoped to this project to avoid killing gravity from other dev sessions.
        await killLingeringGravityProcesses(logger, project?.projectId);
        // Check and upgrade @agentuity/* dependencies if needed
        const upgradeResult = await checkAndUpgradeDependencies(rootDir, logger);
        if (upgradeResult.failed.length > 0) {
            devLock.release();
            tui.fatal(`Failed to upgrade dependencies: ${upgradeResult.failed.join(', ')}`, ErrorCode.BUILD_FAILED);
        }
        // Check for version mismatches (v1 vs v2 SDK packages)
        const versionMismatch = detectVersionMismatch(rootDir, logger);
        if (versionMismatch.hasV1Packages || versionMismatch.hasMajorMismatches) {
            tui.newline();
            tui.warning(formatVersionMismatchWarning(versionMismatch));
            tui.newline();
        }
        try {
            // Setup devmode and gravity (if using public URL)
            const useMockService = process.env.DEVMODE_SYNC_SERVICE_MOCK === 'true';
            // Create apiClient with fresh auth API key (important after inline login)
            const apiClient = auth
                ? new APIClient(getAPIBaseURL(config), logger, auth.apiKey, config)
                : null;
            const syncService = apiClient
                ? createDevmodeSyncService({
                    logger,
                    apiClient,
                    mock: useMockService,
                })
                : null;
            // Track previous metadata for sync diffing
            let previousMetadata;
            let devmode;
            let gravityBin;
            let gravityURL;
            let appURL;
            let savedPrivateKey = config?.devmode?.privateKey
                ? Buffer.from(config.devmode.privateKey, 'base64').toString('utf-8')
                : undefined;
            if (auth && project && opts.public) {
                // Generate devmode endpoint for public URL
                const endpoint = await tui.spinner({
                    message: 'Connecting to Gravity',
                    callback: () => {
                        return generateEndpoint(apiClient, project.projectId, config?.devmode?.hostname, savedPrivateKey);
                    },
                    clearOnSuccess: true,
                });
                if (endpoint.privateKey) {
                    savedPrivateKey = endpoint.privateKey;
                }
                const _config = { ...config };
                _config.devmode = {
                    hostname: endpoint.hostname,
                    privateKey: savedPrivateKey
                        ? Buffer.from(savedPrivateKey).toString('base64')
                        : undefined,
                };
                await saveConfig(_config);
                config = _config;
                devmode = endpoint;
                gravityURL = getGravityDevModeURL(project.region, config);
                appURL = `${getAppBaseURL(config)}/r/${project.projectId}`;
                // Download gravity client
                const configDir = getDefaultConfigDir();
                const gravityDir = join(configDir, 'gravity');
                let mustCheck = true;
                if (config?.gravity?.version &&
                    existsSync(join(gravityDir, config.gravity.version, 'gravity')) &&
                    config?.gravity?.checked &&
                    !validateGravityRequiresUpgrade(config.gravity.version)) {
                    if (Date.now() - config.gravity.checked < 3.6e6) {
                        mustCheck = false;
                        gravityBin = join(gravityDir, config.gravity.version, 'gravity');
                    }
                }
                if (mustCheck) {
                    const res = await download(gravityDir);
                    gravityBin = res.filename;
                    const _config = { ...config };
                    _config.gravity = {
                        checked: Date.now(),
                        version: res.version,
                    };
                    await saveConfig(_config);
                    config = _config;
                }
            }
            // Get workbench info from createApp() in app.ts (v2 approach)
            const { getWorkbenchConfig, loadRuntimeConfig } = await import('../build/vite/config-loader');
            const runtimeConfig = await loadRuntimeConfig(rootDir, logger);
            const workbenchConfigData = getWorkbenchConfig(true, runtimeConfig); // dev mode
            const workbench = {
                hasWorkbench: workbenchConfigData.enabled,
                config: workbenchConfigData.enabled
                    ? { route: workbenchConfigData.route, headers: workbenchConfigData.headers }
                    : null,
            };
            const deploymentId = getDevmodeDeploymentId(project?.projectId ?? '', devmode?.id ?? '');
            // Calculate URLs for banner
            const padding = 12;
            const workbenchUrl = auth && project?.projectId
                ? `${getAppBaseURL(config)}/w/${project.projectId}`
                : `http://127.0.0.1:${opts.port}${workbench.config?.route ?? '/workbench'}`;
            const devmodebody = tui.muted(tui.padRight('Local:', padding)) +
                tui.link(`http://127.0.0.1:${opts.port}`) +
                '\n' +
                tui.muted(tui.padRight('Public:', padding)) +
                (devmode?.hostname ? tui.link(`https://${devmode.hostname}`) : tui.warn('Disabled')) +
                '\n' +
                tui.muted(tui.padRight('Workbench:', padding)) +
                (workbench.hasWorkbench ? tui.link(workbenchUrl) : tui.warn('Disabled')) +
                '\n' +
                tui.muted(tui.padRight('Dashboard:', padding)) +
                (appURL ? tui.link(appURL) : tui.warn('Disabled')) +
                '\n' +
                (interactive
                    ? '\n' + tui.muted('Press ') + tui.bold('h') + tui.muted(' for keyboard shortcuts')
                    : '');
            tui.banner('⨺ Agentuity DevMode', devmodebody, {
                padding: 2,
                topSpacer: false,
                bottomSpacer: false,
                centerTitle: false,
            });
            // Detect user route mount paths for Vite proxy configuration
            // This is a quick AST scan of app.ts — runs before Vite starts
            let routePaths = ['/api']; // Default fallback
            try {
                const { detectExplicitRouter } = await import('../build/app-router-detector');
                const detection = await detectExplicitRouter(rootDir, logger);
                if (detection.detected && detection.mounts.length > 0) {
                    routePaths = detection.mounts.map((m) => m.path);
                    logger.debug('Detected route mount paths: %s', routePaths.join(', '));
                }
            }
            catch (err) {
                logger.debug('Route detection failed, using default /api: %s', err);
            }
            // Pick internal ports (neither is user-facing — the front-door proxy is)
            const bunBackendPort = opts.port + 1;
            const viteInternalPort = opts.port + 2;
            // No-bundle dev mode guard: ensure stale bundled app artifact cannot be executed.
            // We keep other .agentuity artifacts (metadata/workbench files) intact.
            try {
                const staleBundlePath = join(rootDir, '.agentuity', 'app.js');
                if (existsSync(staleBundlePath)) {
                    await Bun.file(staleBundlePath).delete();
                    logger.debug('Removed stale dev bundle artifact: %s', staleBundlePath);
                }
            }
            catch (err) {
                logger.debug('Failed to remove stale dev bundle artifact: %s', err);
            }
            // Debug trace: locate unexpected legacy credential warnings.
            // Enable with AGENTUITY_TRACE_CREDENTIAL_WARNINGS=true.
            if (process.env.AGENTUITY_TRACE_CREDENTIAL_WARNINGS === 'true') {
                const originalConsoleError = console.error.bind(console);
                console.error = (...args) => {
                    try {
                        const first = typeof args[0] === 'string' ? args[0] : '';
                        if (first.includes('No credentials found for this AI provider')) {
                            const stack = new Error('Credential warning trace').stack;
                            originalConsoleError('[TRACE] Credential warning origin stack:');
                            if (stack)
                                originalConsoleError(stack);
                        }
                    }
                    catch {
                        // ignore tracing errors
                    }
                    originalConsoleError(...args);
                };
            }
            // --- State for long-running processes ---
            // Declared early so signal handlers can reference them before
            // servers are started.
            let viteServer = null;
            let frontDoorServer = null;
            let gravityProcess = null;
            let gravityHeartbeatInterval = null;
            let stdinListenerRegistered = false;
            let stdinDataHandler = null;
            let shutdownRequested = false;
            // Initialize process manager to track all servers/processes
            const procManager = initProcessManager(logger);
            // Resolve the actual Vite port BEFORE starting Bun so that env vars
            // like AGENTUITY_BASE_URL contain the correct port. The runtime's
            // CORS trusted-origins are built once at startup, so a late update
            // would leave the backend with stale origins.
            const { findAvailablePort } = await import('../build/vite/vite-asset-server');
            const vitePort = await findAvailablePort(viteInternalPort, '127.0.0.1');
            if (vitePort !== viteInternalPort) {
                logger.info(`Port ${viteInternalPort} is in use, using port ${vitePort} for Vite dev server`);
            }
            // Separate guard flags:
            // - cleanupStarted: prevents double-entry into cleanup()
            // - shutdownRequested: breaks the main wait loop (set inside cleanup
            //   AFTER the cleanupStarted guard passes, so the loop resolves only
            //   once cleanup has actually started running)
            let cleanupStarted = false;
            /**
             * Centralized cleanup function for all resources.
             * Uses the process manager for tracked servers/processes.
             */
            const cleanup = async (exitAfter = false, exitCode = 0, silent = false) => {
                if (cleanupStarted)
                    return;
                cleanupStarted = true;
                shutdownRequested = true;
                if (!silent) {
                    tui.info('Shutting down...');
                }
                // Stop gravity heartbeat interval first
                if (gravityHeartbeatInterval) {
                    clearInterval(gravityHeartbeatInterval);
                    gravityHeartbeatInterval = null;
                }
                // Use process manager for tracked cleanup
                await procManager.cleanup('shutdown');
                // Additional cleanup for non-tracked resources
                await devLock.release();
                await killLingeringGravityProcesses(logger, project?.projectId);
                if (exitAfter) {
                    if (stdinListenerRegistered && process.stdin.isTTY) {
                        try {
                            if (stdinDataHandler) {
                                process.stdin.removeListener('data', stdinDataHandler);
                                stdinDataHandler = null;
                            }
                            process.stdin.setRawMode(false);
                            process.stdin.pause();
                            process.stdin.unref();
                        }
                        catch {
                            // Ignore
                        }
                    }
                    originalExit(exitCode);
                }
            };
            // Signal handlers
            let exitingFromSignal = false;
            const safeExit = (code, reason) => {
                if (exitingFromSignal)
                    return;
                exitingFromSignal = true;
                if (reason)
                    logger.debug('DevMode terminating (%d): %s', code, reason);
                cleanup(true, code).catch(() => originalExit(1));
            };
            process.on('SIGINT', () => safeExit(0, 'SIGINT'));
            process.on('SIGTERM', () => safeExit(0, 'SIGTERM'));
            process.on('SIGHUP', () => safeExit(0, 'SIGHUP'));
            process.on('uncaughtException', (err) => {
                tui.error(`Uncaught exception: ${err instanceof Error ? (err.stack ?? err.message) : String(err)}`);
                void safeExit(1, 'uncaughtException');
            });
            process.on('unhandledRejection', (reason) => {
                logger.warn('Unhandled promise rejection: %s', reason instanceof Error ? (reason.stack ?? reason.message) : String(reason));
            });
            process.on('exit', () => {
                // Last-resort synchronous cleanup. Only runs aggressive SIGKILL
                // if the async cleanup() hasn't already handled everything.
                // This prevents the race where both cleanup paths try to kill
                // the same processes.
                procManager.forceKillAllSync();
                // SIGKILL the Bun subprocess tree as a final safety net.
                // forceKill=true ensures we use SIGKILL (no time for graceful
                // shutdown in an exit handler) and target the process group.
                killBunSubprocess(logger, true);
                releaseLockSync(rootDir);
            });
            // ================================================================
            // Step 0b: Early environment setup
            // ================================================================
            // Load SDK key and set gateway env vars BEFORE agent discovery.
            // Agent discovery imports eval files, which may import LLM SDKs at
            // module scope. Those SDKs check for API keys (e.g. GROQ_API_KEY)
            // at import time, so the gateway env patching must happen first.
            if (!process.env.AGENTUITY_SDK_KEY) {
                const sdkKey = await loadProjectSDKKey(logger, rootDir);
                if (sdkKey) {
                    process.env.AGENTUITY_SDK_KEY = sdkKey;
                }
                else if (project) {
                    tui.warning('AGENTUITY_SDK_KEY not found in .env file. Numerous features will be unavailable.');
                    tui.bullet(`Run "${getCommand('cloud env pull')}" to sync your SDK key, or add AGENTUITY_SDK_KEY to your .env file.`);
                }
            }
            process.env.NODE_ENV = 'development';
            process.env.AGENTUITY_ENV = 'development';
            if (project) {
                const earlyServiceUrls = getServiceUrls(project.region);
                if (!process.env.AGENTUITY_TRANSPORT_URL) {
                    process.env.AGENTUITY_TRANSPORT_URL = earlyServiceUrls.catalyst;
                }
            }
            // Apply gateway env patching so LLM SDK API keys are set before
            // agent discovery imports eval files that may reference them.
            {
                const sdkKey = process.env.AGENTUITY_SDK_KEY;
                const gatewayUrl = process.env.AGENTUITY_AIGATEWAY_URL ||
                    process.env.AGENTUITY_TRANSPORT_URL ||
                    (sdkKey ? 'https://catalyst.agentuity.cloud' : '');
                const gatewayConfigs = [
                    {
                        apiKeyEnv: 'ANTHROPIC_API_KEY',
                        baseUrlEnv: 'ANTHROPIC_BASE_URL',
                        provider: 'anthropic',
                    },
                    { apiKeyEnv: 'GROQ_API_KEY', baseUrlEnv: 'GROQ_BASE_URL', provider: 'groq' },
                    { apiKeyEnv: 'OPENAI_API_KEY', baseUrlEnv: 'OPENAI_BASE_URL', provider: 'openai' },
                ];
                for (const cfg of gatewayConfigs) {
                    const currentKey = process.env[cfg.apiKeyEnv];
                    if (currentKey && currentKey !== sdkKey)
                        continue;
                    if (gatewayUrl && sdkKey) {
                        process.env[cfg.apiKeyEnv] = sdkKey;
                        process.env[cfg.baseUrlEnv] = `${gatewayUrl}/gateway/${cfg.provider}`;
                        logger.debug('Enabled Agentuity AI Gateway for %s', cfg.provider);
                    }
                }
            }
            // ================================================================
            // Step 1: Prepare dev server (once)
            // ================================================================
            await tui.spinner({
                message: 'Preparing dev server',
                callback: async () => {
                    // Typecheck (skip with --no-typecheck)
                    if (!opts.noTypecheck) {
                        const typeResult = await typecheck(rootDir);
                        if (!typeResult.success) {
                            // Non-fatal in dev: log errors and continue
                            console.log('');
                            console.log(typeResult.output);
                            console.log('');
                        }
                    }
                    // Generate workbench files if enabled
                    if (workbenchConfigData.enabled) {
                        const { generateWorkbenchFiles } = await import('../build/vite/workbench-generator');
                        await generateWorkbenchFiles(rootDir, project?.projectId ?? '', workbenchConfigData, logger);
                    }
                    // Discover agents and routes in parallel
                    const srcDir = join(rootDir, 'src');
                    const { discoverAgents } = await import('../build/vite/agent-discovery');
                    const { discoverRoutes } = await import('../build/vite/route-discovery');
                    const [agentMetadata, { routes }] = await Promise.all([
                        discoverAgents(srcDir, project?.projectId ?? '', deploymentId, logger),
                        discoverRoutes(srcDir, project?.projectId ?? '', deploymentId, logger),
                    ]);
                    // Generate metadata file
                    const { generateMetadata, writeMetadataFile } = await import('../build/vite/metadata-generator');
                    const promises = [];
                    // Generate prompt files (non-blocking)
                    promises.push(import('../build/vite/prompt-generator')
                        .then(({ generatePromptFiles }) => generatePromptFiles(srcDir, logger))
                        .catch((err) => logger.warn('Failed to generate prompt files: %s', err.message)));
                    const metadata = await generateMetadata({
                        rootDir,
                        projectId: project?.projectId ?? '',
                        orgId: project?.orgId ?? '',
                        deploymentId,
                        agents: agentMetadata,
                        routes,
                        dev: true,
                        logger,
                    });
                    writeMetadataFile(rootDir, metadata, true, logger);
                    // Sync metadata with backend
                    if (syncService && project?.projectId) {
                        promises.push(syncService.sync(metadata, previousMetadata, project.projectId, deploymentId));
                        previousMetadata = metadata;
                    }
                    await Promise.all(promises);
                },
                clearOnSuccess: true,
            });
            // ================================================================
            // Step 2: Set remaining environment variables
            // ================================================================
            // Note: AGENTUITY_SDK_KEY, NODE_ENV, AGENTUITY_ENV, and
            // AGENTUITY_TRANSPORT_URL are already set in Step 0b (before
            // agent discovery) to support gateway env patching.
            //
            // vitePort is pre-resolved (via findAvailablePort) before Bun
            // starts, so env vars like AGENTUITY_BASE_URL are correct when
            // the runtime builds its CORS trusted-origin set.
            process.env.AGENTUITY_SDK_DEV_MODE = 'true';
            process.env.AGENTUITY_RUNTIME = 'yes';
            process.env.AGENTUITY_PROJECT_DIR = rootDir;
            if (project?.region) {
                process.env.AGENTUITY_REGION = project.region;
            }
            process.env.PORT = String(bunBackendPort);
            process.env.AGENTUITY_PORT = String(bunBackendPort);
            process.env.AGENTUITY_BASE_URL =
                process.env.AGENTUITY_BASE_URL || `http://localhost:${vitePort}`;
            process.env.AGENTUITY_NO_BUNDLE = 'true';
            if (opts.resume) {
                process.env.AGENTUITY_CODER_RESUME_SESSION = opts.resume;
            }
            if (project) {
                const serviceUrls = getServiceUrls(project.region);
                process.env.AGENTUITY_TRANSPORT_URL = serviceUrls.catalyst;
                process.env.AGENTUITY_CATALYST_URL = serviceUrls.catalyst;
                process.env.AGENTUITY_VECTOR_URL = serviceUrls.vector;
                process.env.AGENTUITY_KEYVALUE_URL = serviceUrls.keyvalue;
                process.env.AGENTUITY_SANDBOX_URL = serviceUrls.sandbox;
                process.env.AGENTUITY_STREAM_URL = serviceUrls.stream;
                process.env.AGENTUITY_CLOUD_ORG_ID = project.orgId;
                process.env.AGENTUITY_CLOUD_PROJECT_ID = project.projectId;
                process.env.AGENTUITY_CLOUD_DEPLOYMENT_ID = deploymentId;
            }
            if (devmode?.hostname) {
                process.env.AGENTUITY_DEVMODE_URL = `https://${devmode.hostname}`;
            }
            else {
                process.env.AGENTUITY_DEVMODE_URL = `http://localhost:${vitePort}`;
            }
            // ================================================================
            // Step 3: Start Bun backend with --hot (handles its own HMR)
            // ================================================================
            try {
                await startBunDevServer({
                    rootDir,
                    port: bunBackendPort,
                    logger,
                    vitePort,
                    inspect: opts.inspect,
                    inspectWait: opts.inspectWait,
                    inspectBrk: opts.inspectBrk,
                    // Register the subprocess BEFORE the readiness wait so a SIGINT
                    // during startup can clean it up via procManager. Without this,
                    // the only safety net is the synchronous process.on('exit')
                    // handler, which is fragile and runs too late on some signals.
                    onSpawn: (proc) => {
                        procManager.registerProcess({
                            id: 'bun-backend',
                            process: proc,
                            description: 'Bun backend server (--hot)',
                            port: bunBackendPort,
                            critical: true,
                        });
                    },
                });
            }
            catch (error) {
                tui.error(`Failed to start Bun backend server: ${error}`);
                await cleanup(true, 1, true);
                return;
            }
            // ================================================================
            // Step 4: Start Vite asset server (frontend + API proxy)
            // ================================================================
            // Vite starts AFTER the Bun backend so that its proxy is ready
            // to forward API requests immediately — no ECONNREFUSED race.
            try {
                logger.debug('Starting Vite dev server (internal port %d)...', viteInternalPort);
                const viteResult = await startViteAssetServer({
                    rootDir,
                    logger,
                    workbenchPath: workbench.config?.route,
                    port: vitePort,
                    backendPort: bunBackendPort,
                    routePaths,
                    liveHostname: devmode?.hostname,
                });
                viteServer = viteResult.server;
                // Verify Vite used the port we pre-resolved (should always match
                // since strictPort:true is set and we already confirmed availability).
                if (viteResult.port !== vitePort) {
                    logger.warn(`Vite started on port ${viteResult.port} instead of expected ${vitePort} — env vars may be incorrect`);
                }
                // Register Vite server with process manager.
                // We wrap close() to first force-drop keep-alive HTTP/HMR sockets via
                // httpServer.closeAllConnections() (Node 18.2+). Without this, idle
                // keep-alive connections keep the listener bound until they time out,
                // which leaves the Vite port reserved for several seconds after the
                // CLI exits. We also extend the close timeout (Vite's chokidar +
                // HMR teardown can exceed 1s under load).
                const viteForCleanup = viteServer;
                procManager.registerServer({
                    id: 'vite',
                    server: {
                        close: async () => {
                            try {
                                const http = viteForCleanup.httpServer;
                                http?.closeIdleConnections?.();
                                http?.closeAllConnections?.();
                            }
                            catch {
                                // Best effort — these methods are runtime-dependent.
                            }
                            await viteForCleanup.close();
                        },
                    },
                    description: 'Vite dev server (frontend assets)',
                    port: vitePort,
                    closeTimeoutMs: 3000,
                });
                // Update dev lock with actual Vite port
                await devLock.updatePorts({ vite: vitePort });
                logger.debug(`Vite dev server running on port ${vitePort} (internal, proxying backend on port ${bunBackendPort})`);
            }
            catch (error) {
                tui.error(`Failed to start Vite dev server: ${error}`);
                await cleanup(true, 1, true);
                return;
            }
            // ================================================================
            // Step 5: Start front-door TCP proxy (user-facing port)
            // ================================================================
            // Routes WebSocket upgrades (for /api/*, /_agentuity/*) directly to Bun
            // and everything else (HTTP, HMR WebSocket) to Vite.
            // This works around Bun's broken node:http upgrade socket implementation.
            try {
                const { startWsProxy } = await import('../build/vite/ws-proxy');
                frontDoorServer = await startWsProxy({
                    port: opts.port,
                    vitePort,
                    backendPort: bunBackendPort,
                    routePaths,
                    logger,
                });
                // Register front-door proxy with process manager. Use closeAll()
                // (returns a Promise) so cleanup actually waits for the listener
                // to release the user-facing port instead of fire-and-forgetting.
                // closeAll() also destroys live piped sockets (HMR WS, backend WS)
                // so the listener doesn't sit waiting for clients to disconnect.
                procManager.registerServer({
                    id: 'front-door-proxy',
                    server: {
                        close: () => frontDoorServer?.closeAll() ?? Promise.resolve(),
                    },
                    description: 'Front-door TCP proxy (WS routing)',
                    port: opts.port,
                });
                logger.debug(`Front-door proxy on port ${opts.port} (Vite:${vitePort}, Bun:${bunBackendPort})`);
            }
            catch (error) {
                tui.error(`Failed to start front-door proxy: ${error}`);
                await cleanup(true, 1, true);
                return;
            }
            // ================================================================
            // Step 6: Start gravity tunnel (if public URL enabled)
            // ================================================================
            if (gravityBin && gravityURL && devmode && project) {
                const privateKeyPEM = devmode.privateKey ?? savedPrivateKey;
                if (!privateKeyPEM) {
                    tui.error('No private key available for gravity connection. Please re-run to generate a new key.');
                    await cleanup(true, 1, true);
                    return;
                }
                // Gravity must target the user-facing front-door proxy port.
                // The front-door proxy (ws-proxy) is the only server that correctly
                // routes public WebSocket upgrades to /api/* through to the Bun backend.
                // We read the actual bound port from the front-door server to avoid any
                // mismatch with opts.port (e.g. if the port was overridden or shifted).
                const frontDoorPort = frontDoorServer?.address()?.port ??
                    opts.port;
                try {
                    gravityProcess = Bun.spawn([
                        gravityBin,
                        '--endpoint-id',
                        devmode.id,
                        '--port',
                        frontDoorPort.toString(),
                        '--url',
                        gravityURL,
                        '--log-level',
                        process.env.AGENTUITY_GRAVITY_LOG_LEVEL ?? 'error',
                        '--org-id',
                        project.orgId,
                        '--project-id',
                        project.projectId,
                        '--private-key',
                        Buffer.from(privateKeyPEM).toString('base64'),
                        '--health-check',
                    ], {
                        cwd: rootDir,
                        stdout: 'pipe',
                        stderr: 'pipe',
                        // Make the child a process-group leader so process.kill(-pid, signal)
                        // from procManager.killProcessTree() actually reaches the whole tree
                        // (otherwise it fails with EPERM and falls back to a direct PID kill
                        // that leaves any grandchildren running). We intentionally do NOT call
                        // .unref() — we still want the parent to track the child's lifecycle
                        // and drive cleanup on Ctrl-C / shutdown.
                        detached: true,
                        // Pass a clean env without PORT to prevent the inherited
                        // PORT (set to bunBackendPort) from leaking into gravity.
                        env: {
                            ...process.env,
                            PORT: undefined,
                        },
                    });
                    logger.debug('Gravity tunnel targeting front-door proxy on port %d', frontDoorPort);
                    const gravityPid = gravityProcess.pid;
                    if (gravityPid) {
                        await devLock.registerChild({
                            pid: gravityPid,
                            type: 'gravity',
                            description: 'Gravity public URL tunnel',
                        });
                        // Register with process manager
                        procManager.registerProcess({
                            id: 'gravity',
                            process: gravityProcess,
                            description: 'Gravity public URL tunnel',
                            critical: false,
                        });
                    }
                    // Log gravity output and detect heartbeat port
                    (async () => {
                        try {
                            if (gravityProcess?.stdout) {
                                for await (const chunk of gravityProcess.stdout) {
                                    const text = new TextDecoder().decode(chunk);
                                    const trimmed = text.trim();
                                    const match = trimmed.match(/^HEARTBEAT_PORT=(\d+)$/m);
                                    if (match?.[1]) {
                                        const heartbeatPort = parseInt(match[1], 10);
                                        logger.debug('Gravity heartbeat port: %d', heartbeatPort);
                                        if (!gravityHeartbeatInterval) {
                                            const sendHeartbeat = async () => {
                                                try {
                                                    await fetch(`http://127.0.0.1:${heartbeatPort}/heartbeat`, {
                                                        method: 'POST',
                                                        signal: AbortSignal.timeout(2000),
                                                    });
                                                }
                                                catch {
                                                    // Ignore heartbeat failures
                                                }
                                            };
                                            sendHeartbeat();
                                            gravityHeartbeatInterval = setInterval(sendHeartbeat, 5000);
                                        }
                                    }
                                    else if (trimmed) {
                                        logger.debug('[gravity] %s', trimmed);
                                    }
                                }
                            }
                        }
                        catch (err) {
                            logger.error('Error reading gravity stdout: %s', err);
                        }
                    })();
                    (async () => {
                        try {
                            if (gravityProcess?.stderr) {
                                for await (const chunk of gravityProcess.stderr) {
                                    logger.warn('[gravity] %s', new TextDecoder().decode(chunk).trim());
                                }
                            }
                        }
                        catch (err) {
                            logger.error('Error reading gravity stderr: %s', err);
                        }
                    })();
                }
                catch (error) {
                    tui.error(`Failed to start gravity tunnel: ${error}`);
                    await cleanup(true, 1, true);
                    return;
                }
            }
            // ================================================================
            // Step 7: Keyboard shortcuts + wait for shutdown
            // ================================================================
            if (interactive && process.stdin.isTTY && process.stdout.isTTY) {
                stdinListenerRegistered = true;
                process.stdin.setRawMode(true);
                process.stdin.resume();
                process.stdin.setEncoding('utf8');
                const showHelp = () => {
                    console.log('\n' + tui.bold('Keyboard Shortcuts:'));
                    console.log(tui.muted('  h') + ' - show this help');
                    console.log(tui.muted('  c') + ' - clear console');
                    console.log(tui.muted('  q') + ' - quit\n');
                };
                stdinDataHandler = (data) => {
                    const key = data.toString();
                    if (key === '\u0003' || key === 'q') {
                        if (stdinDataHandler) {
                            process.stdin.removeListener('data', stdinDataHandler);
                            stdinDataHandler = null;
                        }
                        cleanup(true, 0).catch(() => originalExit(1));
                        return;
                    }
                    switch (key) {
                        case 'h':
                            showHelp();
                            break;
                        case 'c':
                            console.clear();
                            tui.banner('⨺ Agentuity DevMode', devmodebody, {
                                padding: 2,
                                topSpacer: false,
                                bottomSpacer: false,
                                centerTitle: false,
                            });
                            break;
                        default:
                            process.stdout.write(data);
                            break;
                    }
                };
                process.stdin.on('data', stdinDataHandler);
            }
            logger.info('DevMode ready 🚀');
            // Block until shutdown — bun --hot handles backend HMR,
            // Vite handles frontend HMR. Nothing to restart.
            await new Promise((resolve) => {
                const check = setInterval(() => {
                    if (shutdownRequested) {
                        clearInterval(check);
                        resolve();
                    }
                }, 200);
            });
        }
        finally {
            /* brute force clean up */
            await devLock.release();
            await killLingeringGravityProcesses(logger, project?.projectId);
            releaseLockSync(rootDir);
        }
    },
});
//# sourceMappingURL=index.js.map