/**
 * studiograph serve command
 *
 * Starts the Studiograph HTTP server, hosting:
 *   - Graph API (read-only REST)
 *   - Agent chat (POST /api/chat, GET /api/chat/stream)
 *   - Installed app plugins (from .studiograph/apps/)
 *
 * Environment variables:
 *   API_KEYS         — semicolon-separated bearer tokens (e.g. "key1;key2")
 *   PORT             — HTTP port (default 8000)
 *   HOST             — Bind address (default 0.0.0.0)
 */
import { Command } from 'commander';
import { join } from 'path';
import { homedir } from 'os';
import { randomUUID } from 'crypto';
import { exec, spawnSync } from 'child_process';
import { existsSync } from 'fs';
import { Workspace } from '../../core/workspace.js';
import { writeServerInfo, deleteServerInfo } from '../../mcp/server-discovery.js';
import { WorkspaceManager } from '../../core/workspace-manager.js';
import { loadUserConfig, resolveApiKey, propagateApiKeyToEnv } from '../../core/user-config.js';
import { promptLLMSetupIfNeeded } from '../setup-wizard.js';
import { runPreflight } from '../../utils/preflight.js';
import { createServer } from '../../server/index.js';
import { createUnhandledRejectionHandler } from '../../server/process-guards.js';
import { box } from 'consola/utils';
import { header, dimText } from '../colors.js';
import { SqliteEncryptedStore, loadMasterKey, loadPreviousMasterKey, setSecretStore, } from '../../core/secrets/index.js';
import { tryResolve } from '../../core/secrets/read-flip.js';
import { migrateAuthDbPhase8 } from '../../core/secrets/auth-db-migration.js';
import { migrateR2Structural } from '../../core/secrets/r2-structural-migration.js';
import { installConsolePatch } from '../../utils/log.js';
import { incrementalReindexWorkspace } from '../../services/vector-service.js';
import { ensurePlaywrightChromium } from '../../services/playwright-bootstrap.js';
export const serveCommand = new Command('serve')
    .description('Start the Studiograph HTTP server')
    .option('-p, --port <port>', 'Port to listen on', '8000')
    .option('-H, --host <host>', 'Host to bind to', '0.0.0.0')
    .option('--verbose', 'Show detailed startup info (tools, etc.)')
    .action(async (opts) => {
    if (opts.verbose)
        process.env.STUDIOGRAPH_VERBOSE = '1';
    // On Railway (and other container environments), the process starts in a
    // default directory, not the workspace volume. Chdir to /workspace if it
    // exists and the current directory isn't already a workspace.
    if (process.env.RAILWAY_ENVIRONMENT && existsSync('/workspace')) {
        process.chdir('/workspace');
    }
    // Ensure git operations work regardless of file ownership. The workspace
    // volume may have files owned by a different uid than the running process
    // (e.g. after container restart or data import), which triggers git's
    // "dubious ownership" refusal. Without this, the embedded git HTTP server
    // returns empty refs for cloned folders. Docker entrypoint does this
    // too, but Railway bypasses the entrypoint via `npx` start command.
    try {
        spawnSync('git', ['config', '--global', '--add', 'safe.directory', '*'], { stdio: 'ignore' });
    }
    catch { /* non-fatal */ }
    const workspace = new Workspace();
    if (!workspace.isWorkspace()) {
        console.error('Not a Studiograph workspace. Run `studiograph init` first.');
        process.exit(1);
    }
    const ready = runPreflight();
    if (!ready) {
        process.exit(1);
        return;
    }
    // Phase 11: install the production console patch as soon as we know
    // the master key is loaded. Patches console.log/info/warn/error/debug
    // so any direct console call (third-party, yet-to-migrate, or
    // unexpected) flows through sanitize() before reaching stdout in
    // production. No-op outside production unless RAILWAY_ENVIRONMENT
    // is set. Honours STUDIOGRAPH_LOG_RAW=1 as an escape hatch.
    //
    // Installed BEFORE the secrets-store messages below so the redactor
    // is live by the time we log the master-key source. The master-key
    // source itself is a friendly label ('env'/'file'/'first-run'), not
    // a secret, so the line passes through unredacted in practice.
    //
    // The encrypted secrets store + Phase 8 auth.db migration + first-run
    // importer all run unconditionally. The `STUDIOGRAPH_NEW_STORE` env
    // flag that previously gated this block was removed once Phase 6
    // shipped stably in production; setting the var is now a no-op.
    let authCryptoOptions;
    try {
        const loaded = loadMasterKey();
        const previous = loadPreviousMasterKey();
        installConsolePatch();
        const sgRoot = process.env.RAILWAY_ENVIRONMENT && existsSync('/workspace')
            ? '/workspace'
            : join(homedir(), '.studiograph');
        const dbPath = join(sgRoot, 'secrets.db');
        const store = new SqliteEncryptedStore({
            dbPath,
            masterKey: loaded.key,
            previousMasterKey: previous,
        });
        setSecretStore(store);
        console.log(`[secrets] store ready at ${dbPath} (master key from ${loaded.source})`);
        // Stash the master key so AuthService (constructed later in
        // createServer) can use it for per-user OAuth + settings encryption.
        authCryptoOptions = { masterKey: loaded.key };
        // Phase 8: auth.db column upgrades. Runs BEFORE the importer
        // (defensive ordering — even though today's importer doesn't
        // touch user-secret keys, a future importer could mirror
        // auth.db values into the store and would need the new schema
        // already in place). Idempotent — re-running after success is
        // a no-op via meta.auth_db_phase8_migrated_at. Failure is fatal:
        // a half-rebuilt oauth_tokens would corrupt OAuth flows for
        // every MCP connector, so refusing to start is safer than
        // serving with mixed plaintext/ciphertext rows.
        try {
            const authDbPath = join(process.cwd(), '.studiograph', 'auth.db');
            const migrationResult = migrateAuthDbPhase8({
                authDbPath,
                masterKey: loaded.key,
            });
            if (!migrationResult.skipped) {
                console.log(`[secrets] Encrypted per-user secrets (one-time migration): ` +
                    `${migrationResult.oauthRowsMigrated} oauth_tokens row(s), ` +
                    `${migrationResult.prefsRowsMigrated} user_preferences row(s). ` +
                    `Backup at ${migrationResult.backupPath}.`);
            }
        }
        catch (err) {
            console.error('[secrets] auth.db Phase 8 migration failed; refusing to start:', err instanceof Error ? err.message : err);
            process.exit(1);
        }
        // Phase 12 decommission (2026-05-12): the first-run importer that
        // copied legacy ~/.studiograph/user.json + connectors/*.json +
        // workspace.json fields into the store ran successfully on every
        // existing workspace. With no migration ramp left to support,
        // operators write values via the Settings UI or `studiograph
        // secrets set` CLI directly.
        // Post-Phase-12 one-shot: copy the four R2 structural fields
        // (account_id, bucket, public_url, endpoint) from
        // workspace.json:r2_config.* into the store on first encounter.
        // The credentials (access_key_id / secret_access_key) were already
        // store-resident; this fills the structural gap so the Settings UI
        // can fully configure R2 without anyone touching the file. Marker
        // gates re-runs. Non-fatal — see migrateR2Structural's doc.
        try {
            const workspaceJsonPath = join(process.cwd(), '.studiograph', 'workspace.json');
            const r2Result = await migrateR2Structural({ workspaceJsonPath, store });
            if (!r2Result.skipped && r2Result.written.length > 0) {
                console.log(`[secrets] Imported R2 structural fields from workspace.json: ${r2Result.written.join(', ')}`);
            }
        }
        catch (err) {
            console.warn('[secrets] R2 structural migration failed (non-fatal):', err instanceof Error ? err.message : err);
        }
    }
    catch (err) {
        // Hard error — the store is required to boot. A misconfigured
        // master key or unreadable secrets.db should surface immediately
        // rather than silently fall back.
        console.error('[secrets] failed to initialize store:', err instanceof Error ? err.message : err);
        process.exit(1);
    }
    const config = await workspace.loadConfig();
    // Configure each folder repo to accept pushes to the checked-out branch.
    // By default, git refuses `git push` into a non-bare repo's checked-out branch
    // because it desyncs the working tree. `updateInstead` accepts pushes that
    // fast-forward AND keeps the working tree in sync, which is exactly what we
    // want for folder data pushed via `studiograph push --remote --data` → the
    // embedded git HTTP server. Without this, the git receive-pack rejects with
    // "branch is currently checked out."
    for (const repo of config.repos) {
        const repoGit = join(process.cwd(), repo.path, '.git');
        if (!existsSync(repoGit))
            continue;
        try {
            spawnSync('git', ['-C', join(process.cwd(), repo.path), 'config', 'receive.denyCurrentBranch', 'updateInstead'], { stdio: 'ignore' });
        }
        catch { /* non-fatal */ }
    }
    // Prompt for model setup if not configured (non-blocking — server works without it, but chat won't)
    await promptLLMSetupIfNeeded();
    // Propagate API key from user config to env (orchestrator reads provider env vars)
    const userConfig = loadUserConfig();
    propagateApiKeyToEnv(userConfig, config);
    const llmApiKey = resolveApiKey(userConfig, config);
    // Build GitUser from workspace config
    const gitUser = {
        id: 'serve',
        name: config.team_name || 'Studiograph Server',
        email: 'noreply@studiograph.local',
    };
    const workspaceManager = new WorkspaceManager(process.cwd(), config, gitUser);
    // Boot-time backstop for the MCP pre-signed asset upload flow: warn if
    // the bucket has no `pending/` lifecycle rule (orphans would accumulate
    // indefinitely). Fire-and-forget — never fatal; the check downgrades
    // SDK errors to WARN itself.
    void import('../../services/asset-lifecycle-check.js').then(({ checkAssetLifecycleRule }) => checkAssetLifecycleRule(workspaceManager)).catch(() => { });
    // Auto-index on boot. Replaces the auto-index that used to live in
    // `studiograph start` (deleted in Phase C). Non-fatal — a failed index
    // shouldn't keep the workspace from serving.
    const autoOnBoot = tryResolve('index.auto_on_boot') ?? true;
    if (autoOnBoot) {
        try {
            // First-time index renders a single-line progress bar in TTY
            // sessions. Voyage batches at 128/request, so each callback
            // tick is a real chunk of remote work — no need to throttle the
            // re-render. In non-TTY environments (CI, Railway, piped),
            // suppress the bar entirely so the log stays line-oriented.
            const isTTY = !!process.stdout.isTTY;
            let progressStarted = false;
            const renderProgress = (done, total) => {
                if (!isTTY || total === 0)
                    return;
                if (!progressStarted) {
                    process.stdout.write('[index] first-run indexing — this can take a few minutes the first time\n');
                    progressStarted = true;
                }
                const width = 28;
                const ratio = Math.min(1, done / total);
                const filled = Math.round(ratio * width);
                const bar = '█'.repeat(filled) + '░'.repeat(width - filled);
                const pct = Math.round(ratio * 100).toString().padStart(3);
                process.stdout.write(`\r[index] ${bar} ${pct}% (${done}/${total})`);
            };
            const result = await incrementalReindexWorkspace(workspaceManager, config.repos, process.cwd(), {
                log: (msg) => console.warn(`[index] ${msg}`),
                onFirstTimeProgress: renderProgress,
            });
            if (progressStarted) {
                // Close out the progress line so the next log starts on a fresh line.
                process.stdout.write('\n');
            }
            if (result.fresh) {
                console.log(`[index] first-run index complete: ${result.total} entit${result.total === 1 ? 'y' : 'ies'}`);
            }
            else if (result.total > 0) {
                console.log(`[index] incremental update: ${result.total} entit${result.total === 1 ? 'y' : 'ies'} reindexed`);
            }
        }
        catch (err) {
            console.warn('[index] auto-index on boot failed (non-fatal):', err instanceof Error ? err.message : err);
        }
    }
    const port = parseInt(process.env.PORT || opts.port, 10);
    const host = process.env.HOST || opts.host;
    // Random per-boot id used by the stdio MCP proxy to confirm it is talking
    // to the server that owns this workspace before forwarding tool calls.
    const serverId = randomUUID();
    const server = await createServer({
        workspaceManager,
        workspacePath: process.cwd(),
        workspaceConfig: config,
        gitUser,
        port,
        host,
        authCryptoOptions,
        serverId,
    });
    // Try to listen, auto-incrementing port if in use
    let actualPort = port;
    const maxRetries = 10;
    for (let attempt = 0; attempt < maxRetries; attempt++) {
        try {
            await server.listen({ port: actualPort, host });
            break;
        }
        catch (err) {
            if (err.code === 'EADDRINUSE' && attempt < maxRetries - 1) {
                actualPort++;
                continue;
            }
            console.error(`\nFailed to start server: ${err.message}`);
            process.exit(1);
        }
    }
    // Publish the discovery file now that the listen port is known. Always use
    // the loopback url regardless of bind host — the stdio proxy only ever
    // sends a Bearer token to a loopback address. Removed after server.close()
    // in the shutdown path below.
    try {
        writeServerInfo(process.cwd(), {
            url: `http://127.0.0.1:${actualPort}`,
            pid: process.pid,
            workspacePath: process.cwd(),
            serverId,
        });
    }
    catch (err) {
        // Non-fatal, but loud: without server.json the stdio MCP proxy can't
        // discover this server and falls back to writing in-process, which can
        // clobber unsaved edits in an open live editor. (Same observable state as
        // running a server build that predates the proxy — the documented rollout
        // case.) If this fires while the server is otherwise healthy, .studiograph
        // is likely read-only/full and needs attention.
        console.warn('[serve] WARNING: failed to write .studiograph/server.json — the stdio MCP ' +
            'proxy will not discover this server and may write locally, risking live-edit ' +
            'data loss. Check .studiograph permissions/disk:', err instanceof Error ? err.message : err);
    }
    // The asset caption/extract pass now runs as a live in-process drain
    // (AssetIndexService) constructed inside createServer — it captions new
    // uploads while the server runs AND does the boot recovery sweep that used
    // to live here. Removed from serve.ts so it isn't boot-only.
    const folders = config.repos.length;
    const hasChat = !!llmApiKey;
    const url = `http://localhost:${actualPort}`;
    console.log(box([
        `URL:          ${header(url)}`,
        `Folders:      ${folders}`,
        `Chat:         ${hasChat ? 'enabled' : dimText('disabled (run studiograph config to add an AI model)')}`,
    ].join('\n'), {
        title: `Studiograph — ${config.team_name}`,
        style: { borderColor: 'cyan', padding: 1 },
    }));
    if (actualPort !== port) {
        console.log(dimText(`  Port ${port} was in use, using ${actualPort} instead.\n`));
    }
    // Open browser (skip if STUDIOGRAPH_NO_OPEN=1 or on known remote environments)
    if (!process.env.STUDIOGRAPH_NO_OPEN && !process.env.RAILWAY_ENVIRONMENT) {
        const openUrl = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
        exec(`${openUrl} ${url}`);
    }
    console.log(dimText('Press Ctrl+C to stop the server.\n'));
    // Best-effort, non-blocking: ensure Playwright's Chromium runtime is present
    // so inspect_artifact / artifact export work on a server deploy. Opt-in via
    // STUDIOGRAPH_INSTALL_PLAYWRIGHT=1; a no-op otherwise. Runs after listen so it
    // never delays serving.
    ensurePlaywrightChromium();
    // Graceful shutdown. server.close() runs the onClose hook which
    // awaits sessionManager.shutdown() — flushing pending commits BEFORE
    // tearing down the timer. The 10s force-exit budget lets larger flushes
    // complete; SessionManager also persists to disk on every markDirty
    // so the next boot's recoverOnBoot() catches anything that doesn't
    // make it out before the timeout fires.
    let shuttingDown = false;
    const shutdown = async (signal, exitCode) => {
        if (shuttingDown)
            return;
        shuttingDown = true;
        console.log(`[serve] ${signal} received — shutting down gracefully`);
        const forceTimer = setTimeout(() => {
            console.log('[serve] forcing exit after timeout');
            process.exit(exitCode);
        }, 10000);
        forceTimer.unref();
        try {
            await server.close();
            console.log(`[serve] shutdown complete`);
        }
        catch (err) {
            console.error('[serve] shutdown error:', err);
        }
        finally {
            // Always remove the discovery file, even if server.close() threw — a
            // stale server.json is otherwise left pointing at a dead port. (The
            // probe's pid-alive + serverId checks already make a stale file safe,
            // but cleaning it up keeps the common case tidy.) Runs after close()
            // so the file never points at a still-listening-but-closing server.
            deleteServerInfo(process.cwd());
        }
        process.exit(exitCode);
    };
    process.once('SIGTERM', () => shutdown('SIGTERM', 0));
    process.once('SIGINT', () => shutdown('SIGINT', 0));
    // Crash handlers — last-chance flush. Pending sessions are already
    // persisted on every markDirty/flush, so recoverOnBoot() recovers
    // anything that doesn't make it out here.
    process.on('uncaughtException', (err) => {
        console.error('[serve] uncaughtException:', err);
        shutdown('uncaughtException', 1);
    });
    // Rejections do NOT exit on the first failure: an isolated unhandled
    // rejection is almost always one request's bug (the next.224 ETag outage),
    // and exiting costs every user the workspace. Log-and-continue, with a
    // storm breaker that restores fail-fast when rejections indicate systemic
    // breakage. Policy + rationale: src/server/process-guards.ts.
    process.on('unhandledRejection', createUnhandledRejectionHandler({
        onFatal: () => shutdown('unhandledRejection storm', 1),
    }));
});
//# sourceMappingURL=serve.js.map