"use strict";
// src/cli/ScaffoldCommand.ts
// v1.16.0 — `openlife scaffold ...` Commander subcommand group.
//
// Surface:
//   openlife scaffold list                          # all templates (bundled+user)
//   openlife scaffold info <id>                     # manifest detail
//   openlife scaffold <name> [--template=<id>]      # apply template to ./<name>
//   openlife scaffold custom <name> --from=<path>   # use a non-registered template
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.registerScaffoldCommands = registerScaffoldCommands;
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
function emitJson(obj) {
    process.stdout.write(`${JSON.stringify(obj, null, 2)}\n`);
}
async function listCmd(opts) {
    const { TemplateRegistry } = await Promise.resolve().then(() => __importStar(require('../orchestrator/scaffold/TemplateRegistry')));
    const reg = new TemplateRegistry();
    const templates = reg.list();
    if (opts.json) {
        emitJson({ ok: true, templates });
        return;
    }
    if (templates.length === 0) {
        process.stdout.write('No scaffold templates available.\n');
        return;
    }
    process.stdout.write(`# ${templates.length} template(s)\n\n`);
    for (const t of templates) {
        const stackBits = [t.stack?.framework, t.stack?.database, t.stack?.auth, t.stack?.billing].filter(Boolean).join(' + ');
        const tag = t.source === 'user' ? ' [user]' : '';
        process.stdout.write(`  ${t.id.padEnd(34)} ${stackBits}${tag}\n`);
        process.stdout.write(`  ${' '.repeat(34)} ${t.description}\n\n`);
    }
}
async function infoCmd(id, opts) {
    const { TemplateRegistry } = await Promise.resolve().then(() => __importStar(require('../orchestrator/scaffold/TemplateRegistry')));
    const reg = new TemplateRegistry();
    const t = reg.get(id);
    if (!t) {
        emitJson({ ok: false, error: `template_not_found: ${id}` });
        process.exitCode = 1;
        return;
    }
    if (opts.json) {
        emitJson({ ok: true, template: t });
        return;
    }
    process.stdout.write(`# ${t.name}  (id: ${t.id})\n\n`);
    process.stdout.write(`${t.description}\n\n`);
    process.stdout.write('## Stack\n');
    for (const [k, v] of Object.entries(t.stack || {})) {
        if (v)
            process.stdout.write(`  ${k.padEnd(15)} ${v}\n`);
    }
    if (t.required_env && t.required_env.length) {
        process.stdout.write('\n## Required env\n');
        for (const e of t.required_env)
            process.stdout.write(`  - ${e}\n`);
    }
    if (t.post_install_steps && t.post_install_steps.length) {
        process.stdout.write('\n## Post-install steps\n');
        for (const s of t.post_install_steps)
            process.stdout.write(`  - ${s}\n`);
    }
    if (t.required_node_version) {
        process.stdout.write(`\nRequires Node ${t.required_node_version}\n`);
    }
    process.stdout.write(`\nSource: ${t.source} (${t.rootDir})\n`);
}
async function applyCmd(name, opts) {
    const { ScaffoldEngine } = await Promise.resolve().then(() => __importStar(require('../orchestrator/scaffold/ScaffoldEngine')));
    const engine = new ScaffoldEngine();
    if (!opts.template) {
        emitJson({ ok: false, error: 'template_required: pass --template=<id>. List with `openlife scaffold list`.' });
        process.exitCode = 1;
        return;
    }
    const targetDir = path.resolve(process.cwd(), name);
    const extraVars = {};
    for (const v of opts.var || []) {
        const idx = v.indexOf('=');
        if (idx > 0)
            extraVars[v.slice(0, idx)] = v.slice(idx + 1);
    }
    const result = engine.apply(opts.template, targetDir, {
        project_name: name,
        description: opts.description,
        force: opts.force,
        vars: extraVars,
        dryRun: opts.dryRun,
    });
    if (opts.json) {
        emitJson(result);
        if (!result.ok)
            process.exitCode = 1;
        return;
    }
    if (!result.ok) {
        process.stderr.write(`❌ Scaffold failed:\n`);
        for (const e of result.errors)
            process.stderr.write(`  - ${e}\n`);
        process.exitCode = 1;
        return;
    }
    process.stdout.write(`\n✓ Scaffolded ${result.filesCopied} file(s) into ${targetDir}\n`);
    process.stdout.write(`  Template: ${result.manifest.name}\n`);
    process.stdout.write(`  Substituted: ${result.filesSubstituted} file(s)\n`);
    if (result.warnings.length) {
        process.stdout.write(`\n⚠ Warnings:\n`);
        for (const w of result.warnings)
            process.stdout.write(`  - ${w}\n`);
    }
    if (result.postInstallSteps.length) {
        process.stdout.write(`\nNext steps:\n`);
        for (const s of result.postInstallSteps)
            process.stdout.write(`  ${s}\n`);
    }
}
async function customCmd(name, opts) {
    if (!opts.from) {
        emitJson({ ok: false, error: 'from_required: pass --from=<local-path-or-git-url>.' });
        process.exitCode = 1;
        return;
    }
    // v1.18.1 — Accept either a local path OR a git URL. Git URLs get
    // cloned into ~/.openlife/templates/cache/<hash>/ first.
    let fromPath;
    if (isGitUrl(opts.from)) {
        const cloned = await cloneGitTemplate(opts.from);
        if (!cloned.ok) {
            emitJson({ ok: false, error: cloned.error });
            process.exitCode = 1;
            return;
        }
        fromPath = cloned.path;
    }
    else {
        fromPath = path.resolve(opts.from);
        if (!fs.existsSync(fromPath)) {
            emitJson({ ok: false, error: `from_path_not_found: ${fromPath}` });
            process.exitCode = 1;
            return;
        }
    }
    // Treat the path as a one-off template — copy its manifest + content
    // into ~/.openlife/templates/<id> first, then apply.
    const yaml = await Promise.resolve().then(() => __importStar(require('js-yaml')));
    const manifestPath = path.join(fromPath, 'manifest.yaml');
    if (!fs.existsSync(manifestPath)) {
        emitJson({ ok: false, error: `manifest.yaml_missing: ${manifestPath}` });
        process.exitCode = 1;
        return;
    }
    const manifest = yaml.load(fs.readFileSync(manifestPath, 'utf-8'));
    if (!manifest?.id) {
        emitJson({ ok: false, error: 'manifest_id_missing' });
        process.exitCode = 1;
        return;
    }
    const { TemplateRegistry } = await Promise.resolve().then(() => __importStar(require('../orchestrator/scaffold/TemplateRegistry')));
    const userDir = path.join(TemplateRegistry._userTemplatesDir(), manifest.id);
    fs.mkdirSync(userDir, { recursive: true });
    // Mirror the directory (shallow, no node_modules).
    copyDirShallow(fromPath, userDir, ['node_modules', '.git', '.next', 'dist', 'build']);
    // Now apply via standard path.
    await applyCmd(name, {
        template: manifest.id,
        force: opts.force,
        var: opts.var,
    });
}
/**
 * v1.18.1 — Detect a git-cloneable URL.
 *
 * Accepts https://github.com/... and https://gitlab.com/... style URLs
 * + the github/owner/repo shorthand.
 *
 * SSH (git@) URLs are rejected for v1.18.1 — they bypass the SSRF guard
 * model and require key management we don't ship. v1.18.2 may add them.
 */
function isGitUrl(value) {
    if (value.startsWith('git@'))
        return false; // explicit reject for now
    if (value.startsWith('https://') || value.startsWith('http://')) {
        // Looks like an http URL → treat as git if it ends in .git OR matches
        // the common code-hosting domains.
        if (value.endsWith('.git'))
            return true;
        return /github\.com|gitlab\.com|bitbucket\.org|codeberg\.org/.test(value);
    }
    // GitHub shorthand: owner/repo
    return /^[a-zA-Z0-9_-]+\/[a-zA-Z0-9._-]+$/.test(value);
}
/**
 * v1.18.1 — Clone a git URL into the OpenLife template cache.
 *
 * Steps:
 *   1. Validate URL via SecurityDownloadGuard.validateUrl
 *      (uses the existing whitelist + SSRF guards from v1.13).
 *   2. Compute a stable hash so repeated clones of the same URL go to the
 *      same dir (idempotent).
 *   3. `git clone --depth=1` (no history → faster + smaller).
 *
 * Returns `{ ok, path?, error? }`.
 */
async function cloneGitTemplate(url) {
    // Normalize shorthand to https.
    let cloneUrl = url;
    if (!/^https?:\/\//.test(url) && !url.startsWith('git@')) {
        cloneUrl = `https://github.com/${url}.git`;
    }
    else if (!cloneUrl.endsWith('.git') && cloneUrl.startsWith('https://')) {
        cloneUrl = `${cloneUrl}.git`;
    }
    // Hash the canonical URL → stable cache dir.
    const crypto = await Promise.resolve().then(() => __importStar(require('crypto')));
    const hash = crypto.createHash('sha256').update(cloneUrl).digest('hex').slice(0, 16);
    const { TemplateRegistry } = await Promise.resolve().then(() => __importStar(require('../orchestrator/scaffold/TemplateRegistry')));
    const cacheRoot = path.join(TemplateRegistry._userTemplatesDir(), '.git-cache');
    fs.mkdirSync(cacheRoot, { recursive: true });
    const targetDir = path.join(cacheRoot, hash);
    // SecurityDownloadGuard validateUrl — reuses the v1.13 SSRF + blocklist guards.
    try {
        // eslint-disable-next-line @typescript-eslint/no-var-requires
        const { SecurityDownloadGuard } = require('../orchestrator/SecurityDownloadGuard');
        if (typeof SecurityDownloadGuard?.validateUrl === 'function') {
            const validation = SecurityDownloadGuard.validateUrl(cloneUrl);
            if (validation && validation.ok === false) {
                return { ok: false, path: '', error: `url_rejected: ${validation.reason || 'failed SecurityDownloadGuard'}` };
            }
        }
    }
    catch { /* guard absent — proceed with explicit allowlist check */ }
    // Explicit allowlist as a belt-and-suspenders backstop.
    if (!/github\.com|gitlab\.com|bitbucket\.org|codeberg\.org/.test(cloneUrl)) {
        return { ok: false, path: '', error: `url_not_allowlisted: only github.com / gitlab.com / bitbucket.org / codeberg.org allowed in v1.18.1` };
    }
    // If already cloned, reuse (idempotent for re-runs).
    if (fs.existsSync(path.join(targetDir, 'manifest.yaml'))) {
        return { ok: true, path: targetDir };
    }
    // Remove any stale partial clone.
    if (fs.existsSync(targetDir)) {
        fs.rmSync(targetDir, { recursive: true, force: true });
    }
    // Spawn git clone --depth=1 — shallow, no history.
    return new Promise((resolve) => {
        // eslint-disable-next-line @typescript-eslint/no-var-requires
        const cp = require('child_process');
        const child = cp.spawn('git', ['clone', '--depth=1', '--quiet', cloneUrl, targetDir], {
            stdio: ['ignore', 'pipe', 'pipe'],
        });
        let stderr = '';
        child.stderr.on('data', (c) => { stderr += String(c); });
        child.on('exit', (code) => {
            if (code === 0) {
                // Validate the clone has a manifest.yaml.
                if (!fs.existsSync(path.join(targetDir, 'manifest.yaml'))) {
                    // Clean up and return error.
                    try {
                        fs.rmSync(targetDir, { recursive: true, force: true });
                    }
                    catch { /* */ }
                    resolve({ ok: false, path: '', error: 'manifest_missing_in_clone: the repo has no manifest.yaml at root' });
                    return;
                }
                resolve({ ok: true, path: targetDir });
            }
            else {
                try {
                    fs.rmSync(targetDir, { recursive: true, force: true });
                }
                catch { /* */ }
                resolve({ ok: false, path: '', error: `git_clone_failed: ${stderr.slice(0, 300)}` });
            }
        });
        child.on('error', (err) => {
            resolve({ ok: false, path: '', error: `git_spawn_failed: ${err.message}` });
        });
    });
}
function copyDirShallow(src, dst, exclude) {
    for (const ent of fs.readdirSync(src, { withFileTypes: true })) {
        if (exclude.includes(ent.name))
            continue;
        const sp = path.join(src, ent.name);
        const dp = path.join(dst, ent.name);
        if (ent.isDirectory()) {
            fs.mkdirSync(dp, { recursive: true });
            copyDirShallow(sp, dp, exclude);
        }
        else if (ent.isFile()) {
            fs.copyFileSync(sp, dp);
        }
    }
}
function registerScaffoldCommands(program) {
    const s = program
        .command('scaffold')
        .description('v1.16.0 — Scaffold a SaaS project from a bundled or user-defined template');
    s.command('list')
        .description('List available templates (bundled + ~/.openlife/templates/)')
        .option('--json', 'Emit JSON instead of human-readable.')
        .action((opts) => { void listCmd(opts); });
    s.command('info <id>')
        .description('Show full template manifest (stack, env, post-install steps)')
        .option('--json', 'Emit JSON instead of human-readable.')
        .action((id, opts) => { void infoCmd(id, opts); });
    s.command('custom <name>')
        .description('v1.18.1 — Apply a one-off template from a local path OR a git URL (github.com / gitlab.com / bitbucket.org / codeberg.org).')
        .option('--from <path-or-url>', 'Local directory OR git URL containing manifest.yaml + scaffold content')
        .option('--force', 'Allow writing into a non-empty target directory')
        .option('--var <key=value>', 'Extra substitution variable (repeatable)', (v, prev = []) => prev.concat(v), [])
        .action((name, opts) => { void customCmd(name, opts); });
    // The bare `scaffold <name>` form must be last so it doesn't shadow the
    // subcommands above.
    s.command('apply <name>', { isDefault: true })
        .description('Scaffold <name>/ from --template=<id>')
        .option('--template <id>', 'Template id (run `openlife scaffold list`)')
        .option('--description <text>', 'One-line project description')
        .option('--force', 'Allow writing into a non-empty target directory')
        .option('--var <key=value>', 'Extra substitution variable (repeatable)', (v, prev = []) => prev.concat(v), [])
        .option('--dry-run', 'Report what would be copied without writing')
        .option('--json', 'Emit JSON result')
        .action((name, opts) => { void applyCmd(name, opts); });
}
