/**
 * commands/create-docs.mjs — `apt-tools create-docs <project-dir>` (C49).
 *
 * Deterministic CLI surface for the apt:create-docs skill. Like its siblings
 * (apt-tools bootstrap + apt-tools personas), this command never calls an
 * LLM — it composes the three deterministic helpers (features-reader,
 * structure-planner, nav-renderer) and emits a JSON envelope the SKILL.md
 * body / apt-docs-author agent consume.
 *
 * The LLM work (per-feature prose generation, D-04 variant-vs-shared
 * rubric decisions, section-level diff-and-confirm per D-05) happens in the
 * apt-docs-author agent body, NOT here.
 *
 * Subcommand menu:
 *
 *   apt-tools create-docs plan <project-dir> [--output-dir <path>]
 *     → load features + personas + product-context, emit the full
 *       StructurePlan (D-02 tree) + inputs the agent consumes. No files
 *       are written.
 *
 *   apt-tools create-docs render-nav <project-dir> [--output-dir <path>] [--print]
 *     → regenerate `<output>/README.md` from the current plan. D-07
 *       navigation index. Writes unless --print (then emits the rendered
 *       markdown on the envelope for inspection).
 *
 *   apt-tools create-docs read <project-dir> [--output-dir <path>]
 *     → scan the existing output tree (if present) and report which
 *       persona/area/feature files exist. Used by the agent's D-05
 *       diff-and-confirm path.
 *
 *   apt-tools create-docs run <project-dir>
 *       [--output-dir <path>]
 *       [--force]
 *     → one-shot orchestration envelope: plan + read + (sentinel) next-steps
 *       list. This is the command the SKILL.md body invokes at the start
 *       of every run; the agent consumes the envelope and writes feature
 *       pages itself.
 *
 * Return shape: always `{ status, command, ... }`. Failures (e.g., no
 * features registry) exit 0 with `status: 'needs-scan'` so the SKILL.md
 * body can surface the next-step suggestion — we do NOT exit 1 for user-
 * correctable states (that would break auto-mode branches).
 */
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
import { isAbsolute, join, relative, resolve } from 'node:path';
import { readFeaturesRegistry, readRegistryLastScanned } from '../docs/features-reader.mjs';
import { renderNavIndex } from '../docs/nav-renderer.mjs';
import { planStructure } from '../docs/structure-planner.mjs';
import { getFlagValue, resolveProjectDir } from '../util/common-flags.mjs';
import { atomicWriteText } from '../util/io.mjs';
import { err, exitWith, ok } from '../util/result.mjs';
const DEFAULT_OUTPUT_DIR_REL = 'docs/product';
/**
 * Guard against path-traversal via --output-dir (SEC-001). Resolves the
 * requested output directory relative to projectDir and rejects anything
 * that would escape projectDir (absolute paths or `../../`-style traversals).
 *
 * Returns `{ safe: true, absDir }` on success, or `{ safe: false, reason }`.
 *
 * @param {string} projectDir  Resolved absolute project root.
 * @param {string} outputDirRaw  The raw value from the --output-dir flag.
 */
function checkOutputDirContainment(projectDir, outputDirRaw) {
    // Absolute paths are rejected outright — they can name any location on disk.
    if (isAbsolute(outputDirRaw)) {
        return {
            safe: false,
            reason: `--output-dir must be a relative path inside the project; got absolute path: ${JSON.stringify(outputDirRaw)}`,
        };
    }
    const absDir = resolve(projectDir, outputDirRaw);
    const rel = relative(projectDir, absDir);
    // relative() returns a path starting with '..' when absDir is outside projectDir.
    if (rel.startsWith('..') || rel === '') {
        return {
            safe: false,
            reason: `--output-dir must resolve inside the project root; ${JSON.stringify(outputDirRaw)} escapes to ${JSON.stringify(absDir)}`,
        };
    }
    return { safe: true, absDir };
}
/**
 * @param {string | undefined} subcommand
 * @param {string[] | undefined} extraArgs
 */
export function cmdCreateDocs(subcommand, extraArgs) {
    const args = extraArgs ?? [];
    if (!subcommand) {
        return err('Usage: apt-tools create-docs <plan|render-nav|read|run> <project-dir> [flags]\n' +
            'Flags: --output-dir <path>, --force, --print');
    }
    switch (subcommand) {
        case 'plan':
            return cmdPlan(args);
        case 'render-nav':
            return cmdRenderNav(args);
        case 'read':
            return cmdRead(args);
        case 'run':
            return cmdRun(args);
        default:
            return err(`apt-tools create-docs: unknown subcommand "${subcommand}". ` +
                'Known: plan, render-nav, read, run');
    }
}
/**
 * @param {string[]} args
 * @param {string} flag
 */
function hasFlag(args, flag) {
    return args.includes(flag);
}
/**
 * Load the personas envelope defensively. The framework uses JSON-only
 * persistence (C48 D-01); we mirror that contract and treat a missing file
 * as "no personas configured" rather than as an error.
 *
 * @param {string} projectDir
 * @returns {{ personas: unknown[], source: string | null }}
 */
function loadPersonas(projectDir) {
    const path = join(projectDir, '.aperant', 'personas.json');
    if (!existsSync(path))
        return { personas: [], source: null };
    try {
        const parsed = JSON.parse(readFileSync(path, 'utf-8'));
        if (parsed && Array.isArray(parsed.personas)) {
            return { personas: parsed.personas, source: path };
        }
    }
    catch {
        /* fall through → empty */
    }
    return { personas: [], source: path };
}
/**
 * Load the derived product-context sidecar. Optional input — used for the
 * H1 productName on the README. Missing is fine.
 *
 * @param {string} projectDir
 * @returns {{ productName: string | null, targetAudience: string | null, path: string | null }}
 */
function loadProductContext(projectDir) {
    const path = join(projectDir, '.aperant', 'product-context.json');
    if (!existsSync(path))
        return { productName: null, targetAudience: null, path: null };
    try {
        const parsed = JSON.parse(readFileSync(path, 'utf-8'));
        return {
            productName: typeof parsed?.productName === 'string' ? parsed.productName : null,
            targetAudience: typeof parsed?.targetAudience === 'string' ? parsed.targetAudience : null,
            path,
        };
    }
    catch {
        return { productName: null, targetAudience: null, path };
    }
}
/**
 * Compose the planning envelope from on-disk inputs. Shared by plan/run/
 * render-nav so a missing-features state emits the same envelope everywhere.
 *
 * @param {string} projectDir
 * @param {string} outputDirRelative
 */
function composePlanEnvelope(projectDir, outputDirRelative) {
    const registry = readFeaturesRegistry(projectDir);
    const { personas, source: personasSource } = loadPersonas(projectDir);
    const productContext = loadProductContext(projectDir);
    const registryLastScanned = readRegistryLastScanned(projectDir);
    const plan = planStructure({
        personas,
        areas: registry.areas,
        outputDirRelative,
    });
    const status = registry.status === 'missing' || registry.status === 'empty' ? 'needs-scan' : 'ok';
    return {
        status,
        registry_status: registry.status,
        registry_dir: registry.registryDir,
        registry_last_scanned: registryLastScanned,
        personas_source: personasSource,
        personas_count: plan.personas.length,
        product_name: productContext.productName,
        target_audience: productContext.targetAudience,
        product_context_source: productContext.path,
        parse_errors: registry.parse_errors,
        totals: {
            ...plan.totals,
            registry_totals: registry.totals,
        },
        plan,
    };
}
/**
 * plan — emit the StructurePlan + inputs. No file writes.
 *
 * @param {string[]} args
 */
function cmdPlan(args) {
    const projectDir = resolveProjectDir(args);
    const outputDirRaw = getFlagValue(args, '--output-dir') ?? DEFAULT_OUTPUT_DIR_REL;
    const containment = checkOutputDirContainment(projectDir, outputDirRaw);
    if (!containment.safe) {
        return exitWith({
            status: 'error',
            command: 'create-docs-plan',
            error_code: 'unsafe_output_dir',
            reason: containment.reason,
        }, 1);
    }
    const outputDirRelative = outputDirRaw;
    const envelope = composePlanEnvelope(projectDir, outputDirRelative);
    return ok({
        command: 'create-docs-plan',
        project_dir: projectDir,
        output_dir_relative: outputDirRelative,
        ...envelope,
    });
}
/**
 * render-nav — regenerate the README.md navigation index. Writes unless
 * --print is passed.
 *
 * @param {string[]} args
 */
function cmdRenderNav(args) {
    const projectDir = resolveProjectDir(args);
    const outputDirRaw = getFlagValue(args, '--output-dir') ?? DEFAULT_OUTPUT_DIR_REL;
    const containment = checkOutputDirContainment(projectDir, outputDirRaw);
    if (!containment.safe) {
        return exitWith({
            status: 'error',
            command: 'create-docs-render-nav',
            error_code: 'unsafe_output_dir',
            reason: containment.reason,
        }, 1);
    }
    const outputDirRelative = outputDirRaw;
    const print = hasFlag(args, '--print');
    const envelope = composePlanEnvelope(projectDir, outputDirRelative);
    const md = renderNavIndex({
        plan: envelope.plan,
        registryLastScanned: envelope.registry_last_scanned,
        productName: envelope.product_name,
    });
    const absPath = join(projectDir, outputDirRelative, 'README.md');
    if (print) {
        return ok({
            command: 'create-docs-render-nav',
            project_dir: projectDir,
            output_dir_relative: outputDirRelative,
            nav_index_path: absPath,
            written: false,
            content: md,
        });
    }
    atomicWriteText(absPath, md);
    return ok({
        command: 'create-docs-render-nav',
        project_dir: projectDir,
        output_dir_relative: outputDirRelative,
        nav_index_path: absPath,
        written: true,
        bytes: Buffer.byteLength(md, 'utf-8'),
    });
}
/**
 * read — emit a summary of the existing output tree. Used by the agent's
 * D-05 diff-and-confirm path so it knows which files already exist and
 * should be diff'd vs overwritten.
 *
 * @param {string[]} args
 */
function cmdRead(args) {
    const projectDir = resolveProjectDir(args);
    const outputDirRaw = getFlagValue(args, '--output-dir') ?? DEFAULT_OUTPUT_DIR_REL;
    const containment = checkOutputDirContainment(projectDir, outputDirRaw);
    if (!containment.safe) {
        return exitWith({
            status: 'error',
            command: 'create-docs-read',
            error_code: 'unsafe_output_dir',
            reason: containment.reason,
        }, 1);
    }
    const outputDirRelative = outputDirRaw;
    const absBase = join(projectDir, outputDirRelative);
    if (!existsSync(absBase)) {
        return ok({
            command: 'create-docs-read',
            project_dir: projectDir,
            output_dir_relative: outputDirRelative,
            output_dir_exists: false,
            files: [],
        });
    }
    const files = listMarkdownFilesRecursive(absBase)
        .map((absPath) => ({
        path: absPath,
        relative: absPath.slice(absBase.length + 1),
        bytes: safeSize(absPath),
    }))
        .sort((a, b) => a.relative.localeCompare(b.relative));
    return ok({
        command: 'create-docs-read',
        project_dir: projectDir,
        output_dir_relative: outputDirRelative,
        output_dir_exists: true,
        files,
    });
}
/**
 * run — one-shot planning + read envelope. The apt-docs-author agent
 * consumes this then writes feature pages itself. We intentionally do NOT
 * write feature pages here — they require LLM synthesis.
 *
 * @param {string[]} args
 */
function cmdRun(args) {
    const projectDir = resolveProjectDir(args);
    const outputDirRaw = getFlagValue(args, '--output-dir') ?? DEFAULT_OUTPUT_DIR_REL;
    const containment = checkOutputDirContainment(projectDir, outputDirRaw);
    if (!containment.safe) {
        return exitWith({
            status: 'error',
            command: 'create-docs-run',
            error_code: 'unsafe_output_dir',
            reason: containment.reason,
        }, 1);
    }
    const outputDirRelative = outputDirRaw;
    const envelope = composePlanEnvelope(projectDir, outputDirRelative);
    const absBase = join(projectDir, outputDirRelative);
    const existing = existsSync(absBase) ? listMarkdownFilesRecursive(absBase) : [];
    return ok({
        command: 'create-docs-run',
        project_dir: projectDir,
        output_dir_relative: outputDirRelative,
        ...envelope,
        existing_output: {
            output_dir_exists: existsSync(absBase),
            file_count: existing.length,
        },
        next_steps: envelope.status === 'needs-scan'
            ? [
                'features registry is missing or empty — run /apt:scan first to populate .aperant/features/',
            ]
            : envelope.personas_count === 0
                ? ['no personas configured — run /apt:personas to draft a roster before generating docs']
                : [
                    'invoke the apt-docs-author agent to generate per-feature markdown (D-09: one feature → one commit)',
                    `after the agent finishes, run 'apt-tools create-docs render-nav ${projectDir}' to refresh the navigation index (D-07)`,
                ],
    });
}
/**
 * Recursive enumerate of `.md` files under a directory. Used by the
 * `read` + `run` subcommands to surface the existing output tree.
 *
 * @param {string} root
 * @returns {string[]}
 */
function listMarkdownFilesRecursive(root) {
    /** @type {string[]} */
    const out = [];
    const stack = [root];
    while (stack.length > 0) {
        const dir = stack.pop() ?? '';
        let entries;
        try {
            entries = readdirSync(dir, { withFileTypes: true });
        }
        catch {
            continue;
        }
        for (const e of entries) {
            const full = join(dir, e.name);
            if (e.isDirectory()) {
                stack.push(full);
            }
            else if (e.isFile() && e.name.endsWith('.md')) {
                out.push(full);
            }
        }
    }
    return out;
}
/**
 * @param {string} path
 * @returns {number}
 */
function safeSize(path) {
    try {
        return statSync(path).size;
    }
    catch {
        return 0;
    }
}
//# sourceMappingURL=create-docs.mjs.map