/**
 * olam create — Provision a new isolated development world.
 *
 * Delegates to WorldManager.createWorld, same as the MCP server tool.
 */
import { spawnSync } from 'node:child_process';
import { existsSync } from 'node:fs';
import { dirname, resolve } from 'node:path';
import ora from 'ora';
import pc from 'picocolors';
import { AuthPreflightError, TaskDispatchError } from '@olam/core/src/world/manager.js';
import { getDevboxFreshness, formatFreshnessWarning, } from '@olam/core/src/world/devbox-freshness.js';
import { inferRepos, PICKER_CONFIDENCE_THRESHOLD, } from '@olam/core/src/world/infer-repos.js';
import { decideWorkspaceMatch, } from '@olam/core/src/world/match-workspace.js';
import { loadContext } from '../context.js';
import { printError, printInfo, printHeader, printWarning } from '../output.js';
import { probeHostCp, gatherProbeFailureDiagnostics, callHostCpProxy, openHostCpUrl } from './host-cp.js';
import { readMemorySecretOrNull } from '../lib/memory-secret.js';
import { registerAgentMemoryMcp } from '../lib/world-mcp-register.js';
import { buildIfStale } from '../lib/build-if-stale.js';
import { resolveBundleSource, BundleSourceNotFoundError } from '../lib/bundle-source.js';
import { isDevMode } from '../install-root.js';
// Phase B1 — mirror of AGENTMEMORY_HOST_INTERNAL_URL in
// packages/adapters/src/docker/container.ts. Hardcoded here for the
// post-spawn MCP registration call so the host knows what URL was
// injected into the world's env. Phase C makes this configurable for
// cloud mode.
const AGENTMEMORY_LOCAL_URL = 'http://host.docker.internal:3111';
const HOST_CP_URL = 'http://127.0.0.1:19000';
/** Read the host-cp token from ~/.olam/host-cp.token for use in create. */
async function readHostCpTokenForCreate() {
    try {
        const { default: fs } = await import('node:fs');
        const { default: os } = await import('node:os');
        const { default: path } = await import('node:path');
        const tp = path.join(process.env.OLAM_HOME ?? path.join(os.homedir(), '.olam'), 'host-cp.token');
        if (!fs.existsSync(tp))
            return null;
        return fs.readFileSync(tp, 'utf-8').trim();
    }
    catch {
        return null;
    }
}
export function registerCreate(program) {
    program
        .command('create')
        .description('Create a new development world')
        .option('--name <name>', 'World name (required unless --from-prompt is set; auto-derived in that case)')
        .option('--repos <repos...>', 'Repos to include (names from .olam/config.yaml; wins over --workspace)')
        .option('--workspace <name>', 'Named workspace from the host catalog (~/.olam/workspaces/<name>.yaml)')
        .option('--task <task>', 'Initial task to dispatch')
        .option('--branch <branch>', 'Override default branch name')
        .option('--plan <file>', 'Path to a plan file to inject')
        .option('--no-auth', 'Skip auto-injecting host credentials')
        // Phase F-2-D (D4): host CP integration. Default: emit a one-line
        // hint if host CP is not running. --no-host-cp suppresses the hint
        // entirely. Real auto-start ships in a future iteration; for now
        // the hint keeps the operator's existing workflow unblocked while
        // surfacing the host CP UI option.
        .option('--no-host-cp', 'Suppress the host CP "you might want to start it" hint')
        // Phase C9 (olam-dogfood-vision): opt-in flag spawns a parallel
        // claude-lane-codex-review session alongside claude-main, prompted
        // to monitor + critique main's work. Lives on the same world; sees
        // the same workspace + thoughts. Auto-spawned at world creation
        // time; the operator can destroy it via `olam lanes destroy <world>
        // codex-review` if it gets noisy.
        .option('--auto-codex-review', 'Spawn a parallel codex-review lane that critiques main as it works')
        // Phase D6 (olam-dogfood-vision): open Host CP UI in browser on
        // successful create. Default-on for interactive sessions; suppress
        // with --no-open. Headless / CI / piped-output runs auto-skip.
        .option('--no-open', 'Suppress auto-opening the Host CP UI in the browser on success')
        // M5 dogfood follow-up (B1): when the devbox image is older than
        // the source files baked into it, fresh worlds run stale code.
        // --rebuild-base auto-invokes the build script; --no-freshness-check
        // skips the warning entirely (CI/automation).
        .option('--rebuild-base', 'Rebuild olam-devbox:latest before creating (slow)')
        .option('--no-freshness-check', 'Skip the devbox image freshness check')
        // CLI parity for the MCP `olam_create_from_prompt` tool. The prompt
        // is dispatched as the world's task AND drives the workspace
        // inference (same heuristic the MCP tool uses). When passed:
        //   - --name is auto-derived from the prompt (slugified, capped at 40 chars)
        //   - --workspace + --repos are inferred against the host-cp catalog
        //   - On low-confidence inference or N-way ambiguity, the CLI prints
        //     the picker payload and exits 1 so the operator can re-run with
        //     an explicit --workspace / --repos.
        // Mutually exclusive with --task (the prompt IS the task) and
        // overrides --name. --workspace/--repos still win if explicitly set.
        .option('--from-prompt <prompt>', 'NL prompt → infer workspace + dispatch (CLI parity for olam_create_from_prompt MCP tool)')
        .option('--keep-after-merge', "Disable auto-destroy when the world's PR merges (useful for inspection/debugging)")
        // Phase B B3 (olam-session-stability): opt out of B1's strip step
        // so the operator's in-progress edits make it into the world's
        // worktree. Default OFF: world starts with a clean tree (the
        // baseline-diff snapshot still runs so the reaper can scope claude's
        // footprint). Use this when you've explicitly staged or modified
        // files in your local checkout and want claude to continue from
        // there.
        .option('--carry-uncommitted', "Preserve operator's uncommitted edits in the world's worktree")
        .option('--allow-bootstrap-failure', 'Treat bootstrap step failures as warnings instead of destroying the world (dogfood escape hatch for cross-repo seed coupling)')
        // C4 (Phase C — fresh-install self-bootstrap): operator override of
        // the default devbox image. `OLAM_DEVBOX_IMAGE=<ref>` env var is the
        // alternate input path. Refs outside the default allowlist
        // (ghcr.io/pleri/*) require --allow-custom-registry per Decision #19.
        // Full digest-pinned-pull integration with image-digests.json
        // arrives in Phase B; this flag pair locks the surface now and
        // validates input early.
        .option('--devbox-image <ref>', 'Override the default devbox image (full registry/name:tag or @sha256: ref)')
        .option('--allow-custom-registry', 'Allow --devbox-image refs outside ghcr.io/pleri/* (logs a warning)')
        .option('--runbook <name>', 'Named runbook profile from ~/.olam/config.json')
        .action(async (opts) => {
        // C4 — early validation of the devbox-image override (if any).
        // Runs BEFORE any docker / world-provisioning work so a rejected
        // ref never produces partial state.
        const { resolveDevboxImageOverride, decideAllowlist } = await import('../registry-allowlist.js');
        const overrideRef = resolveDevboxImageOverride(opts.devboxImage);
        if (overrideRef) {
            const decision = decideAllowlist({
                imageRef: overrideRef,
                allowCustomRegistry: opts.allowCustomRegistry === true,
            });
            if (!decision.accepted) {
                process.stderr.write(decision.stderrLine + '\n');
                process.exitCode = 1;
                return;
            }
            if (decision.stderrLine) {
                process.stderr.write(decision.stderrLine + '\n');
            }
            // C4b — protocol-version handshake on the override. Bootstrap
            // already enforces this for the 3 default images; user-supplied
            // overrides must satisfy the same contract or `olam create`
            // would happily spin a container that's incompatible with the
            // rest of the stack.
            //
            // Best-effort pull first (tolerate failure for locally-built
            // images that aren't in any registry). Then inspect the LABEL.
            // Failed inspect or no-overlap → exit EXIT_PROTOCOL_MISMATCH.
            spawnSync('docker', ['pull', overrideRef], { stdio: 'pipe' }); // ignore errors
            const { inspectImageProtocolVersions, checkProtocolOverlap } = await import('../protocol-version.js');
            const inspect = inspectImageProtocolVersions(overrideRef);
            if (inspect.inspectFailed) {
                printError(`Devbox image '${overrideRef}' is not present locally and could not be pulled. ` +
                    `Run \`docker pull ${overrideRef}\` to verify the ref, then re-run.`);
                const { EXIT_PROTOCOL_MISMATCH } = await import('../exit-codes.js');
                process.exitCode = EXIT_PROTOCOL_MISMATCH;
                return;
            }
            const handshake = checkProtocolOverlap(inspect.versions);
            if (!handshake.compatible) {
                printError(handshake.remedy);
                const { EXIT_PROTOCOL_MISMATCH } = await import('../exit-codes.js');
                process.exitCode = EXIT_PROTOCOL_MISMATCH;
                return;
            }
        }
        // CLI parity for the MCP tool: when --from-prompt is set, treat
        // it as the world's task AND use it to infer workspace + repos.
        // Mutually exclusive with --task; explicit --workspace / --repos
        // still win (operator override).
        let resolvedName = opts.name;
        let resolvedWorkspace = opts.workspace;
        let resolvedRepos = opts.repos;
        let resolvedTask = opts.task;
        if (opts.fromPrompt) {
            if (opts.task) {
                printError('--from-prompt and --task are mutually exclusive (the prompt IS the task).');
                process.exitCode = 1;
                return;
            }
            resolvedTask = opts.fromPrompt;
            if (!resolvedName) {
                resolvedName = defaultNameFromPrompt(opts.fromPrompt);
            }
            // Infer when caller didn't already pin workspace + repos.
            if (!resolvedWorkspace && !resolvedRepos) {
                const inferenceSpinner = ora('Inferring workspace from prompt…').start();
                try {
                    const catalog = await fetchHostCpWorkspaces();
                    const catalogRepos = Array.from(new Set(catalog.flatMap((w) => w.projects)));
                    const inferred = inferRepos({
                        prompt: opts.fromPrompt,
                        catalog: catalogRepos,
                    });
                    if (!inferred.proceed) {
                        inferenceSpinner.fail('Inference confidence too low');
                        const reason = inferred.repos.length === 0
                            ? 'no repo names extracted from prompt'
                            : `inference confidence ${inferred.confidence.toFixed(2)} below ${PICKER_CONFIDENCE_THRESHOLD}`;
                        printError(`Picker needed: ${reason}`);
                        if (catalogRepos.length > 0) {
                            console.log(pc.dim(`  available repos: ${catalogRepos.join(', ')}`));
                        }
                        console.log(pc.dim('  rerun with explicit --workspace <name> or --repos <a> <b> <c>'));
                        process.exitCode = 1;
                        return;
                    }
                    const exactMatches = await fetchHostCpExactMatches(inferred.repos);
                    const decision = decideWorkspaceMatch({
                        inferredRepos: inferred.repos,
                        exactMatches,
                        fullCatalog: catalog,
                    });
                    if (decision.result === 'exact-1') {
                        resolvedWorkspace = decision.workspace.name;
                    }
                    else if (decision.result === 'subset-1') {
                        resolvedWorkspace = decision.workspace.name;
                    }
                    else if (decision.result === 'exact-N' || decision.result === 'subset-N') {
                        inferenceSpinner.fail(`Multiple workspaces match (${decision.result})`);
                        const cands = decision.result === 'exact-N'
                            ? decision.candidates.map((w) => w.name)
                            : decision.candidates.map((c) => c.workspace.name);
                        printError(`Picker needed: ${cands.join(', ')}`);
                        console.log(pc.dim('  rerun with explicit --workspace <name>'));
                        process.exitCode = 1;
                        return;
                    }
                    else {
                        // create-new — no existing workspace covers the inferred
                        // repo set. Pass repos through; WorldManager creates one.
                        resolvedRepos = [...inferred.repos];
                    }
                    inferenceSpinner.succeed(resolvedWorkspace
                        ? `Inferred workspace: ${resolvedWorkspace} (confidence ${inferred.confidence.toFixed(2)})`
                        : `Inferred new workspace from repos: ${inferred.repos.join(', ')}`);
                }
                catch (err) {
                    inferenceSpinner.fail('Inference failed');
                    printError(err instanceof Error ? err.message : String(err));
                    process.exitCode = 1;
                    return;
                }
            }
        }
        if (!resolvedName) {
            printError('--name is required (omit only when --from-prompt is set).');
            process.exitCode = 1;
            return;
        }
        const { ctx, error } = await loadContext();
        if (!ctx) {
            printError(error?.message ?? 'Olam is not configured. Run `olam init` first.');
            process.exitCode = 1;
            return;
        }
        // M5 dogfood follow-up (B1): devbox staleness check. Resolves
        // repo root by walking up from cwd until we hit a directory
        // containing `packages/`. Falls back to cwd if not found.
        // Phase B6: --rebuild-base routes through resolveBuildScript so
        // a published-CLI run gets the named remedy instead of a silent
        // bash-not-found stderr.
        if (opts.freshnessCheck) {
            const repoRoot = resolveRepoRoot(process.cwd());
            const freshness = getDevboxFreshness({ repoRoot });
            if (freshness.isStale) {
                if (opts.rebuildBase) {
                    const { resolveBuildScript, MissingBuildScriptError } = await import('../install-root.js');
                    let buildScript;
                    try {
                        buildScript = resolveBuildScript({
                            scriptRelPath: 'packages/adapters/src/docker/build-devbox.sh',
                        });
                    }
                    catch (err) {
                        if (err instanceof MissingBuildScriptError) {
                            printError(err.message);
                            process.exitCode = 1;
                            return;
                        }
                        throw err;
                    }
                    const spinner = ora('Rebuilding olam-devbox:latest…').start();
                    const rebuild = spawnSync('bash', [buildScript], { cwd: repoRoot, stdio: 'inherit' });
                    if (rebuild.status !== 0) {
                        spinner.fail('Devbox rebuild failed');
                        process.exitCode = 1;
                        return;
                    }
                    spinner.succeed('Devbox image rebuilt');
                }
                else {
                    const warning = formatFreshnessWarning(freshness);
                    if (warning)
                        printWarning(warning);
                }
            }
        }
        // C2 (olam-world-bundle-freshness): resolve the agent-stream bundle
        // source. Handles both source-mode (OLAM_DEV=1 + packages/ sibling →
        // auto-build if stale) and install-mode (CLI's own dist/agent-stream/).
        let agentStreamDistPath;
        try {
            const bundleSource = resolveBundleSource();
            if (bundleSource.mode === 'source') {
                // B3 path: auto-build when dist is stale.
                const bundleRepoRoot = resolveRepoRoot(process.cwd());
                const buildResult = buildIfStale(bundleRepoRoot);
                if (!buildResult.ok) {
                    printError('Agent-stream bundle build failed. Not creating world with a stale bundle.\n' +
                        'Fix the build error and re-run `olam create`.');
                    process.exitCode = 1;
                    return;
                }
                if (buildResult.built) {
                    printInfo('Bundle', buildResult.message);
                }
            }
            // Both modes: pass the resolved path so the docker provider bind-mounts
            // the correct dist (source-mode: repo dist; install-mode: CLI dist).
            agentStreamDistPath = bundleSource.path;
        }
        catch (err) {
            if (err instanceof BundleSourceNotFoundError) {
                // Graceful degradation: warn but do not block world create.
                // The world will use the image-baked bundle as fallback.
                printWarning(`agent-stream bundle path unresolved; world will use image-baked bundle.\n` +
                    `  ${err.message}`);
            }
            else {
                throw err;
            }
        }
        const spinner = ora('Creating world...').start();
        try {
            const world = await ctx.worldManager.createWorld({
                name: resolvedName,
                repos: resolvedRepos,
                ...(resolvedWorkspace ? { workspace: resolvedWorkspace } : {}),
                task: resolvedTask,
                branchName: opts.branch,
                planFile: opts.plan,
                noAuth: !opts.auth,
                ...(agentStreamDistPath ? { agentStreamDistPath } : {}),
                // ADV-B3-LOW2: strict-boolean coercion. `?? false` would pass through
                // truthy non-booleans (e.g. if the flag's shape ever grows to
                // `--carry-uncommitted [mode]` where commander returns a string).
                // `=== true` ensures only the literal boolean true reaches the
                // manager, matching the manager's strict `if (!opts.carryUncommitted)`.
                carryUncommitted: opts.carryUncommitted === true,
                allowBootstrapFailure: opts.allowBootstrapFailure === true,
                ...(opts.runbook ? { runbookName: opts.runbook } : {}),
            });
            spinner.succeed('World created');
            // Phase B3 — register the agentmemory MCP server inside the
            // freshly-spawned world's claude config so the in-world claude
            // session reaches the host agent-memory pool. Gracefully skipped
            // when the host memory service was absent at spawn time (B1's
            // env injection only runs when the secret file exists). Spec:
            // docs/plans/olam-agent-memory-distributed/phase-b-tasks.md B3
            try {
                const memorySecret = readMemorySecretOrNull() ?? '';
                const containerName = `olam-${world.id}-devbox`;
                const result = registerAgentMemoryMcp({
                    containerName,
                    agentmemoryUrl: AGENTMEMORY_LOCAL_URL,
                    agentmemorySecret: memorySecret,
                });
                switch (result.outcome) {
                    case 'registered':
                        printInfo('Agent memory', `MCP registered in-world (--scope ${result.scope})`);
                        break;
                    case 'already-registered':
                        printInfo('Agent memory', `MCP already registered (idempotent skip)`);
                        break;
                    case 'skipped-no-env':
                        // Silent on the common path — no memory service or --skip-memory at bootstrap.
                        break;
                    case 'failed':
                        printWarning(`Agent memory MCP registration failed (rc=${result.rc}): ${result.detail}. ` +
                            `World is still usable; the in-world @agentmemory/mcp shim falls back to local InMemoryKV.`);
                        break;
                }
            }
            catch (err) {
                // Defensive: registration is best-effort. Never block world create.
                printWarning(`Agent memory MCP registration threw: ${err instanceof Error ? err.message : String(err)}. ` +
                    `World is still usable.`);
            }
            // --keep-after-merge: persist into the registry immediately so
            // the host-cp polling loop sees it even if host-cp starts later.
            if (opts.keepAfterMerge) {
                const { WorldRegistry } = await import('@olam/core/src/world/registry.js');
                const reg2 = new WorldRegistry();
                try {
                    reg2.update(world.id, { autoDestroyOnMerge: false });
                }
                finally {
                    reg2.close();
                }
            }
            printHeader(world.name);
            printInfo('ID', world.id);
            printInfo('Status', world.status);
            printInfo('Repos', world.repos.join(', ') || '(all configured)');
            printInfo('Branch', world.branch);
            printInfo('Port Offset', String(world.portOffset));
            printInfo('Provider', world.computeProvider);
            printInfo('Workspace', world.workspacePath);
            if (world.dashboardUrl) {
                printInfo('Dashboard', world.dashboardUrl);
            }
            if (world.appPortUrls && world.appPortUrls.length > 0) {
                world.appPortUrls.forEach((ap, i) => {
                    printInfo(i === 0 ? 'Apps' : '', `${ap.repoName}  ${ap.url}`);
                });
            }
            if (resolvedTask) {
                printInfo('Task', resolvedTask);
            }
            if (world.credentialsInjected?.claude) {
                console.log(`\n${pc.green('Credentials injected. World is ready for dispatch.')}`);
            }
            else if (world.dashboardUrl) {
                console.log(`\n${pc.yellow('Authenticate at')} ${world.dashboardUrl}`);
            }
            // host-cp reliability fix: use HTTP probe instead of docker-only detection.
            // Supports both bare-node and container deployments.
            if (opts.hostCp !== false) {
                const probeResult = await probeHostCp().catch(() => null);
                if (!probeResult) {
                    // Fail-loud: the world IS created but auto-registration failed.
                    const diag = await gatherProbeFailureDiagnostics().catch(() => ({
                        bootstrapStatus: 'probe error',
                        containerStatus: 'probe error',
                    }));
                    process.stderr.write([
                        '',
                        pc.red('Host CP probe failed.'),
                        `  tried: http://127.0.0.1:19000/api/bootstrap → ${diag.bootstrapStatus}`,
                        `  tried: docker container "olam-host-cp" → ${diag.containerStatus}`,
                        '',
                        pc.yellow('World was created but the SPA inbox will not show it until you:'),
                        `  ${pc.cyan('olam host-cp start')}            (start the host CP)`,
                        `  ${pc.cyan(`olam host-cp register --world ${world.id}`)}   (register manually)`,
                        `  OR pass ${pc.dim('--no-host-cp')} on next create to suppress this check.`,
                        '',
                    ].join('\n'));
                    process.exitCode = 1;
                    return;
                }
                // Auto-register the new world so it appears in the host CP UI
                // immediately. Per-world CP base port = 19080 + offset.
                const cpPort = 19080 + world.portOffset;
                const hostCpUrl = probeResult.url;
                const reg = await (async () => {
                    const token = await readHostCpTokenForCreate();
                    if (!token)
                        return { ok: false, status: 0, error: 'no token' };
                    const url = `${hostCpUrl}/api/admin/registry`;
                    try {
                        const res = await fetch(url, {
                            method: 'POST',
                            headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
                            body: JSON.stringify({ id: world.id, port: cpPort }),
                        });
                        if (!res.ok) {
                            const text = await res.text().catch(() => '');
                            return { ok: false, status: res.status, error: text || `HTTP ${res.status}` };
                        }
                        return { ok: true, status: res.status };
                    }
                    catch (err) {
                        return { ok: false, status: 0, error: err instanceof Error ? err.message : String(err) };
                    }
                })();
                if (!reg.ok) {
                    process.stderr.write([
                        '',
                        pc.red('Host CP registry POST failed.'),
                        `  url: ${hostCpUrl}/api/admin/registry`,
                        `  status: ${reg.status}`,
                        `  error: ${reg.error}`,
                        '',
                        pc.yellow('World was created but not registered. Run manually:'),
                        `  ${pc.cyan(`olam host-cp register --world ${world.id}`)}`,
                        '',
                    ].join('\n'));
                    process.exitCode = 1;
                    return;
                }
                printInfo('Host CP', `registered → :${cpPort} (mode: ${probeResult.mode})`);
                // Propagate the friendly name from the CLI's --name flag.
                try {
                    const token = await readHostCpTokenForCreate();
                    if (token && world.name && world.name !== world.id) {
                        await fetch(`${hostCpUrl}/api/worlds/${encodeURIComponent(world.id)}`, {
                            method: 'PATCH',
                            headers: {
                                Authorization: `Bearer ${token}`,
                                'Content-Type': 'application/json',
                            },
                            body: JSON.stringify({ name: world.name }),
                        });
                    }
                    // --keep-after-merge: record auto_destroy_on_merge=false.
                    if (opts.keepAfterMerge && token) {
                        await fetch(`${hostCpUrl}/api/admin/world-pr`, {
                            method: 'POST',
                            headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
                            body: JSON.stringify({ worldId: world.id, prUrl: null, autoDestroyOnMerge: false }),
                            signal: AbortSignal.timeout(3000),
                        });
                        printInfo('Keep-after-merge', 'auto-destroy disabled for this world');
                    }
                }
                catch {
                    // Silent — the world is registered; the name is just UI sugar.
                }
                // Phase C9: spawn the auto-codex-review lane.
                if (opts.autoCodexReview && reg.ok) {
                    const reviewTask = 'You are the codex-review sub-lane. Watch the main session ' +
                        '(claude-main) as it works on the world\'s primary task. ' +
                        'Periodically read its recent thought_nodes (filter by ' +
                        'session_id=claude-main) and surface concrete concerns: ' +
                        'security risks, plumbing bugs, premature abstractions. ' +
                        'Adversarial-reviewer voice; constructive but blunt.';
                    try {
                        const result = await callHostCpProxy('POST', world.id, '/lanes/create', { name: 'codex-review', task: reviewTask });
                        if (result.ok) {
                            printInfo('Auto-review', 'spawned claude-lane-codex-review');
                        }
                        else {
                            printWarning(`Auto-review spawn failed (HTTP ${result.status}): ${result.error}. Retry with \`olam lanes create ${world.id} --name codex-review --task '...'\`.`);
                        }
                    }
                    catch (err) {
                        printWarning(`Auto-review spawn errored: ${err instanceof Error ? err.message : String(err)}`);
                    }
                }
                const worldUrl = `${hostCpUrl}/world/${encodeURIComponent(world.id)}`;
                console.log(`\n${pc.cyan('Host CP UI:')} ${worldUrl}`);
                // Phase D6: auto-open in browser unless --no-open.
                if (opts.open !== false && reg.ok) {
                    const openResult = openHostCpUrl(worldUrl);
                    if (openResult.opened) {
                        printInfo('Browser', 'opened');
                    }
                    else if (openResult.reason) {
                        printInfo('Browser', `not opened (${openResult.reason})`);
                    }
                }
            }
        }
        catch (err) {
            // TaskDispatchError fires AFTER the world is fully created
            // but BEFORE we return from createWorld. The world exists,
            // is running, and is usable — only the auto-dispatch failed.
            // Surface a remedy the operator can paste verbatim so they
            // recover in one command instead of re-creating the world.
            // Closes friction #18 (silent --task no-op).
            if (err instanceof TaskDispatchError) {
                spinner.fail('Auto-dispatch failed (world exists but task was not sent)');
                printError(`Task auto-dispatch failed (${err.kind}): ${err.message}`);
                // World ID is on err's enclosing scope (resolvedName) but
                // not on TaskDispatchError itself (the throw point doesn't
                // know it). Surface the name-derived remedy with the
                // operator-friendly note that they can also use the world
                // ID once `olam list` shows it.
                console.log(`  ${pc.yellow('Remedy:')} ${pc.cyan(`olam dispatch ${resolvedName} '<your task>'`)}`);
                console.log(`  ${pc.dim(`(or use 'olam list' to find the world ID if the name is ambiguous)`)}`);
                if (err.detail) {
                    console.log(`  ${pc.dim(`detail: ${err.detail}`)}`);
                }
                process.exitCode = 1;
                return;
            }
            spinner.fail('Failed to create world');
            if (err instanceof AuthPreflightError) {
                printError(err.message);
                if (err.remedy)
                    console.log(`  ${pc.dim(err.remedy)}`);
            }
            else {
                printError(err instanceof Error ? err.message : String(err));
            }
            process.exitCode = 1;
        }
    });
}
/**
 * Walk up from `start` until we find a directory containing `packages/`.
 * Used by the B1 freshness check to locate paths relative to the repo
 * root regardless of which subdirectory the operator invoked the CLI
 * from. Falls back to `start` if no marker is found before /.
 */
function resolveRepoRoot(start) {
    let cur = start;
    while (true) {
        // Repo root signature: top-level package.json + packages/ dir.
        if (existsSync(resolve(cur, 'packages')) && existsSync(resolve(cur, 'package.json'))) {
            return cur;
        }
        const parent = dirname(cur);
        if (parent === cur)
            return start; // walked to filesystem root
        cur = parent;
    }
}
/**
 * Slugify an NL prompt into a default world name. Lowercase, kebab-
 * case, max 40 chars. Mirrors the MCP tool's `defaultNameFromPrompt`
 * helper at packages/mcp-server/src/tools/create-from-prompt.ts so
 * --from-prompt produces the same slug shape regardless of entry
 * point.
 */
function defaultNameFromPrompt(prompt) {
    const slug = prompt
        .toLowerCase()
        .replace(/[^a-z0-9\s-]/g, '')
        .trim()
        .replace(/\s+/g, '-')
        .slice(0, 40);
    return slug || 'unnamed';
}
/**
 * Read host-cp's startup token. Defensive — returns null on every
 * failure (file missing, permissions, etc.) so --from-prompt can
 * still fall through to a "host-cp not running" picker error rather
 * than throwing.
 */
async function readHostCpToken() {
    try {
        const { default: fs } = await import('node:fs');
        const { default: os } = await import('node:os');
        const { default: path } = await import('node:path');
        const tp = path.join(os.homedir(), '.olam', 'host-cp.token');
        if (!fs.existsSync(tp))
            return null;
        const raw = fs.readFileSync(tp, 'utf-8').trim();
        return raw.length > 0 ? raw : null;
    }
    catch {
        return null;
    }
}
/**
 * Fetch the operator's workspace catalog from host-cp's
 * GET /api/workspaces. Empty array on any failure (host-cp not
 * running, network, parse) — the caller's inference will then run
 * with empty catalog and degrade to a low-confidence picker rather
 * than crash.
 */
async function fetchHostCpWorkspaces() {
    try {
        const token = await readHostCpToken();
        if (!token)
            return [];
        const res = await fetch(`${HOST_CP_URL}/api/workspaces`, {
            headers: { Authorization: `Bearer ${token}` },
        });
        if (!res.ok)
            return [];
        const data = (await res.json());
        const arr = Array.isArray(data) ? data : (data.workspaces ?? []);
        return arr
            .filter((w) => typeof w.name === 'string' && Array.isArray(w.projects))
            .map((w) => ({ name: w.name, projects: w.projects ?? [] }));
    }
    catch {
        return [];
    }
}
/**
 * Fetch host-cp's POST /api/workspaces/match exact-match candidates
 * for a given inferred repo set. Empty array on any failure.
 */
async function fetchHostCpExactMatches(projects) {
    try {
        const token = await readHostCpToken();
        if (!token)
            return [];
        const res = await fetch(`${HOST_CP_URL}/api/workspaces/match`, {
            method: 'POST',
            headers: {
                Authorization: `Bearer ${token}`,
                'Content-Type': 'application/json',
            },
            body: JSON.stringify({ projects }),
        });
        if (!res.ok)
            return [];
        const data = (await res.json());
        return (data.matches ?? [])
            .filter((w) => typeof w.name === 'string' && Array.isArray(w.projects))
            .map((w) => ({ name: w.name, projects: w.projects ?? [] }));
    }
    catch {
        return [];
    }
}
//# sourceMappingURL=create.js.map