"use strict";
// src/design/DesignMdExtractor.ts
// v1.15.0 — TypeScript wrapper around the bundled `design-md-extractor`
// Claude Code Skill (Alan Nicolas / @oalanicolas, MIT).
//
// What this layer adds on top of run.cjs:
//   1. URL safety via SecurityDownloadGuard.validateUrl (consistent with
//      the rest of OpenLife's network surface).
//   2. Provider resolution via ModelTierRouter — picks whichever CLI is
//      installed (claude/codex/gemini) instead of assuming `claude` in PATH.
//   3. Output normalization — drops artifacts under
//      `~/.openlife/design-extract/<slug>/` or, with --catalog, also
//      mirrors them into `design/profiles/<slug>/` so DesignMdRegistry
//      picks them up without a second import step.
//   4. 24-hour result cache keyed by URL hash, so successive
//      `openlife design extract https://stripe.com` calls are instant.
//
// Network calls are funneled through the skill's existing axios layer;
// we don't open new sockets here.
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._internals = exports.DesignMdExtractor = void 0;
const crypto = __importStar(require("crypto"));
const child_process_1 = require("child_process");
const fs = __importStar(require("fs"));
const os = __importStar(require("os"));
const path = __importStar(require("path"));
/**
 * v1.15.0 — Lightweight URL validation for read-only HTML extraction.
 *
 * Distinct from SecurityDownloadGuard.validateUrl (which whitelists a
 * tight set of trusted package sources). The extractor only reads HTML
 * + CSS — no code execution, no file writes from the network. So we
 * accept any well-formed public https:// URL but block private IPs
 * and known SSRF vectors.
 */
function validateExtractUrl(url) {
    let parsed;
    try {
        parsed = new URL(url);
    }
    catch {
        return { ok: false, reason: 'malformed_url' };
    }
    if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') {
        return { ok: false, reason: `unsupported_protocol_${parsed.protocol}` };
    }
    // Strip IPv6 brackets that the WHATWG URL parser preserves ([::1] → ::1).
    const host = parsed.hostname.toLowerCase().replace(/^\[|\]$/g, '');
    // SSRF guards: block localhost, link-local, RFC1918, and metadata endpoints.
    const blocklist = [
        /^localhost$/,
        /^127\./,
        /^0\./,
        /^10\./,
        /^192\.168\./,
        /^172\.(1[6-9]|2[0-9]|3[01])\./,
        /^169\.254\./, // link-local + cloud metadata
        /^::1?$/, // IPv6 loopback (bracket-stripped)
        /^::ffff:127\./i, // IPv4-mapped loopback
        /^fe80::/i,
        /^fc00::/i,
        /^fd00::/i,
        /\.local$/,
        /\.internal$/,
    ];
    for (const re of blocklist) {
        if (re.test(host))
            return { ok: false, reason: `blocked_host_${host}` };
    }
    return { ok: true };
}
/** Resolve `~/.openlife/design-extract` honoring OPENLIFE_STATE_DIR. */
function defaultExtractDir() {
    const explicit = process.env.OPENLIFE_STATE_DIR;
    if (explicit && explicit.trim())
        return path.join(explicit, 'design-extract');
    return path.join(os.homedir(), '.openlife', 'design-extract');
}
/** Slugify URL into a filesystem-safe directory name. */
function slugFromUrl(url) {
    try {
        const u = new URL(url);
        const host = u.hostname.replace(/^www\./, '');
        const path = u.pathname.replace(/^\/+|\/+$/g, '').replace(/[^a-zA-Z0-9_-]/g, '-');
        return path ? `${host}-${path}`.slice(0, 80) : host;
    }
    catch {
        // Last-resort hash if URL is malformed.
        return `url-${crypto.createHash('sha1').update(url).digest('hex').slice(0, 12)}`;
    }
}
/** Locate the bundled skill regardless of install layout. */
function resolveSkillRoot() {
    const candidates = [
        // From repo checkout (cwd is openlife-core-main)
        path.join(process.cwd(), '.catalog', 'skills', 'design-md-extractor'),
        // From compiled dist when shipped in tarball
        path.join(__dirname, '..', '..', '.catalog', 'skills', 'design-md-extractor'),
        path.join(__dirname, '..', '..', '..', '.catalog', 'skills', 'design-md-extractor'),
    ];
    for (const c of candidates) {
        const runCjs = path.join(c, 'run.cjs');
        if (fs.existsSync(runCjs))
            return c;
    }
    return null;
}
/**
 * Pick a provider for the extraction step. Strategy:
 *   1. Caller override wins.
 *   2. If anthropic CLI is router-preferred AND `claude` is on PATH → claude-cli.
 *   3. Else if OPENROUTER_API_KEY is set → openrouter.
 *   4. Else return null (caller surfaces actionable error).
 */
function resolveProvider(opts) {
    if (opts.provider)
        return { provider: opts.provider, reason: 'user-override' };
    // Router-preferred CLI first.
    try {
        // Lazy require so we don't pay this cost when caller overrides provider.
        // eslint-disable-next-line @typescript-eslint/no-var-requires
        const { ModelTierRouter } = require('../orchestrator/router/ModelTierRouter');
        const decision = ModelTierRouter.pick('chat.smart');
        if (decision?.provider === 'anthropic') {
            return { provider: 'claude-cli', reason: 'router-preferred-anthropic' };
        }
    }
    catch { /* router not configured — fall through */ }
    if ((process.env.OPENROUTER_API_KEY || '').trim()) {
        return { provider: 'openrouter', reason: 'openrouter-key-set' };
    }
    return null;
}
/**
 * Cache key derived from URL hash. The skill itself has phase-level
 * caching inside its working dir; this is a coarse outer cache that
 * skips the whole run when a recent successful result exists.
 */
function cacheStatePath(slug) {
    return path.join(defaultExtractDir(), slug, '.last-extract.json');
}
function readCache(slug) {
    const p = cacheStatePath(slug);
    if (!fs.existsSync(p))
        return null;
    try {
        const rec = JSON.parse(fs.readFileSync(p, 'utf-8'));
        const ageMs = Date.now() - Date.parse(rec.extractedAt);
        if (!Number.isFinite(ageMs))
            return null;
        if (ageMs > 24 * 60 * 60 * 1000)
            return null; // 24h TTL
        if (rec.exitCode !== 0)
            return null;
        return rec;
    }
    catch {
        return null;
    }
}
function writeCache(rec) {
    const p = cacheStatePath(rec.slug);
    try {
        fs.mkdirSync(path.dirname(p), { recursive: true });
        fs.writeFileSync(p, JSON.stringify(rec, null, 2), 'utf-8');
    }
    catch { /* non-fatal — caching is best-effort */ }
}
/** Copy a single brand's artifacts into design/profiles/<slug>/. */
function publishToCatalog(slug, outDir, rootDir) {
    const dest = path.join(rootDir, 'design', 'profiles', slug);
    fs.mkdirSync(dest, { recursive: true });
    const artifactNames = ['DESIGN.md', 'tokens.json', 'preview.html', 'quality-score.json', 'render-contract.json', 'style-fingerprint.json'];
    for (const name of artifactNames) {
        const src = path.join(outDir, name);
        if (!fs.existsSync(src))
            continue;
        try {
            fs.copyFileSync(src, path.join(dest, name));
        }
        catch { /* skip */ }
    }
    // Write meta.json so DesignMdRegistry picks it up.
    const designPath = path.join(dest, 'DESIGN.md');
    if (fs.existsSync(designPath)) {
        const content = fs.readFileSync(designPath, 'utf-8');
        const checksum = crypto.createHash('sha256').update(content).digest('hex');
        const meta = {
            id: slug,
            source: 'design-md-extractor',
            title: slug,
            importedAt: new Date().toISOString(),
            checksum,
        };
        // Surface archetype/quality if available.
        try {
            const fp = JSON.parse(fs.readFileSync(path.join(dest, 'style-fingerprint.json'), 'utf-8'));
            if (typeof fp?.archetype === 'string')
                meta.archetype = fp.archetype;
        }
        catch { /* missing or malformed — skip */ }
        try {
            const q = JSON.parse(fs.readFileSync(path.join(dest, 'quality-score.json'), 'utf-8'));
            if (typeof q?.letter === 'string')
                meta.qualityScore = q.letter;
            else if (typeof q?.grade === 'string')
                meta.qualityScore = q.grade;
        }
        catch { /* skip */ }
        fs.writeFileSync(path.join(dest, 'meta.json'), JSON.stringify(meta, null, 2), 'utf-8');
    }
    return dest;
}
class DesignMdExtractor {
    rootDir;
    constructor(rootDir = process.cwd()) {
        this.rootDir = rootDir;
    }
    async extract(url, opts = {}) {
        const slug = slugFromUrl(url);
        const outDir = opts.out || path.join(defaultExtractDir(), slug);
        // 1. URL safety — read-only HTML extraction has a different threat
        //    model than package download. We accept any public http(s) URL
        //    but block SSRF vectors (localhost, RFC1918, cloud metadata).
        const guardCheck = validateExtractUrl(url);
        if (!guardCheck.ok) {
            return {
                ok: false,
                slug,
                outDir,
                exitCode: -1,
                provider: 'unresolved',
                cached: false,
                catalogged: false,
                errors: [`url_blocked: ${guardCheck.reason}`],
            };
        }
        // 2. Cache short-circuit (unless caller forces no-cache).
        if (!opts.noCache) {
            const cached = readCache(slug);
            if (cached && cached.url === url) {
                const catalogPath = opts.catalog ? publishToCatalog(slug, cached.outDir, this.rootDir) : null;
                return {
                    ok: true,
                    slug,
                    outDir: cached.outDir,
                    exitCode: 0,
                    provider: `${cached.provider} (cached)`,
                    cached: true,
                    catalogged: !!catalogPath,
                    designPath: path.join(cached.outDir, 'DESIGN.md'),
                    tokensPath: path.join(cached.outDir, 'tokens.json'),
                    previewPath: path.join(cached.outDir, 'preview.html'),
                    qualityScorePath: path.join(cached.outDir, 'quality-score.json'),
                };
            }
        }
        // 3. Resolve provider.
        const providerDecision = resolveProvider(opts);
        if (!providerDecision) {
            return {
                ok: false,
                slug,
                outDir,
                exitCode: -2,
                provider: 'unresolved',
                cached: false,
                catalogged: false,
                errors: [
                    'no_provider_available: install `claude` (anthropic CLI) OR set OPENROUTER_API_KEY',
                    'run `openlife provider setup` to configure',
                ],
            };
        }
        // 4. Locate the bundled skill.
        const skillRoot = resolveSkillRoot();
        if (!skillRoot) {
            return {
                ok: false,
                slug,
                outDir,
                exitCode: -3,
                provider: providerDecision.provider,
                cached: false,
                catalogged: false,
                errors: ['skill_not_found: .catalog/skills/design-md-extractor/ missing from this distribution'],
            };
        }
        const runCjs = path.join(skillRoot, 'run.cjs');
        // 5. Spawn run.cjs with the resolved flags.
        fs.mkdirSync(outDir, { recursive: true });
        const args = [runCjs, '--url', url, '--out', outDir, '--provider', providerDecision.provider];
        if (opts.model && providerDecision.provider === 'openrouter')
            args.push('--model', opts.model);
        if (opts.comparePath)
            args.push('--compare', opts.comparePath);
        if (opts.contentGate === false)
            args.push('--no-content-gate');
        const exitCode = await new Promise((resolve) => {
            const child = (0, child_process_1.spawn)(process.execPath, args, {
                stdio: ['ignore', 'inherit', 'inherit'],
                env: process.env,
                cwd: skillRoot,
            });
            const timeoutMs = opts.timeoutMs ?? 180_000;
            const timer = setTimeout(() => {
                try {
                    child.kill('SIGTERM');
                }
                catch { /* best effort */ }
                resolve(-99);
            }, timeoutMs);
            child.on('exit', (code) => {
                clearTimeout(timer);
                resolve(typeof code === 'number' ? code : -1);
            });
            child.on('error', () => {
                clearTimeout(timer);
                resolve(-1);
            });
        });
        // 6. Bookkeeping + optional catalog publish.
        const ok = exitCode === 0;
        if (ok) {
            writeCache({
                url,
                slug,
                outDir,
                provider: providerDecision.provider,
                extractedAt: new Date().toISOString(),
                exitCode,
            });
        }
        const catalogPath = ok && opts.catalog ? publishToCatalog(slug, outDir, this.rootDir) : null;
        return {
            ok,
            slug,
            outDir,
            exitCode,
            provider: providerDecision.provider,
            cached: false,
            catalogged: !!catalogPath,
            designPath: path.join(outDir, 'DESIGN.md'),
            tokensPath: path.join(outDir, 'tokens.json'),
            previewPath: path.join(outDir, 'preview.html'),
            qualityScorePath: path.join(outDir, 'quality-score.json'),
            errors: ok ? undefined : [`skill_exit_${exitCode}: see ${outDir} for partial outputs`],
        };
    }
}
exports.DesignMdExtractor = DesignMdExtractor;
/** Exposed for tests. */
exports._internals = { slugFromUrl, resolveProvider, defaultExtractDir, resolveSkillRoot, validateExtractUrl };
