/**
 * runtime-detect.mjs — pure project-shape capability detection.
 *
 * Extracted from packages/core/src/prompts/prompt-loader.ts:572-595 so the
 * framework CLI is self-contained (no @aperant/core dep). Same heuristics:
 * read package.json's dependencies + devDependencies, return a capability
 * record the resolver can score against.
 *
 * Pattern: pure data in → pure data out. No filesystem walking, no exec.
 * Use loadRuntimeCapabilities(projectDir) for the cwd-rooted variant.
 *
 * v0.8.6 — `loadRuntimeCapabilities` now ALSO walks `apps/*` by default
 * (configurable via the second arg `{monorepo_globs}`) and OR-merges
 * each match's `package.json` capabilities with the root. The default
 * `['apps/*']` matches the canonical Aperant monorepo layout; adopters
 * whose monorepo lives elsewhere (e.g. `packages/apps/*`) pass a
 * custom `monorepo_globs`. Setting `monorepo_globs: []` disables the
 * walk entirely (root-only escape hatch).
 */
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
import { join, resolve } from 'node:path';
/**
 * @typedef {Object} RuntimeCapabilities
 * @property {boolean} is_electron
 * @property {boolean} is_tauri
 * @property {boolean} is_nextjs
 * @property {boolean} is_nuxt
 * @property {boolean} is_web_frontend
 * @property {boolean} is_expo
 * @property {boolean} is_react_native
 * @property {boolean} has_node
 */
/** A capability record with every flag defaulted to false. */
function emptyCapabilities() {
    return {
        is_electron: false,
        is_tauri: false,
        is_nextjs: false,
        is_nuxt: false,
        is_web_frontend: false,
        is_expo: false,
        is_react_native: false,
        has_node: false,
    };
}
/**
 * Detect runtime capabilities from a parsed package.json object.
 *
 * @param {Record<string, unknown> | null} packageJson
 * @returns {RuntimeCapabilities}
 */
export function detectFromPackageJson(packageJson) {
    const capabilities = emptyCapabilities();
    if (packageJson === null || typeof packageJson !== 'object')
        return capabilities;
    const allDeps = {
        ...(packageJson.dependencies ?? {}) /** @type {Record<string,string>} */,
        ...(packageJson.devDependencies ?? {}) /** @type {Record<string,string>} */,
    };
    const deps = new Set(Object.keys(allDeps).map((k) => k.toLowerCase()));
    if (deps.has('electron') || [...deps].some((d) => d.startsWith('@electron'))) {
        capabilities.is_electron = true;
    }
    if (deps.has('@tauri-apps/api') || deps.has('tauri')) {
        capabilities.is_tauri = true;
    }
    if (deps.has('expo'))
        capabilities.is_expo = true;
    if (deps.has('react-native'))
        capabilities.is_react_native = true;
    // Web frontends — react/vue/svelte/angular/solid → frontend. BUT: an
    // Electron app commonly bundles React as its renderer; in that case the
    // runtime is "electron", not "web_frontend". Electron + react-native +
    // expo + tauri all win over the web flag.
    const isNativeContainer = capabilities.is_electron ||
        capabilities.is_tauri ||
        capabilities.is_react_native ||
        capabilities.is_expo;
    const webFrameworks = ['react', 'vue', 'svelte', 'angular', 'solid'];
    if (!isNativeContainer && webFrameworks.some((f) => deps.has(f))) {
        capabilities.is_web_frontend = true;
    }
    if (deps.has('next')) {
        capabilities.is_nextjs = true;
        capabilities.is_web_frontend = true;
    }
    if (deps.has('nuxt')) {
        capabilities.is_nuxt = true;
        capabilities.is_web_frontend = true;
    }
    if (deps.has('vite') && !isNativeContainer) {
        capabilities.is_web_frontend = true;
    }
    // Node-ish? Anything with a package.json + an entry point counts.
    if (typeof packageJson.main === 'string' || typeof packageJson.module === 'string') {
        capabilities.has_node = true;
    }
    if (!capabilities.has_node && packageJson.scripts && typeof packageJson.scripts === 'object') {
        capabilities.has_node = true;
    }
    return capabilities;
}
/**
 * Load and detect capabilities from a project directory's package.json.
 * v0.8.6 — also walks `monorepo_globs` (default `['apps/*']`) and
 * OR-merges each match's capabilities with the root.
 *
 * @param {string} projectDir
 * @param {{monorepo_globs?: string[]}} [opts]
 * @returns {RuntimeCapabilities}
 */
export function loadRuntimeCapabilities(projectDir, opts) {
    const root = resolve(projectDir);
    const rootPkgPath = join(root, 'package.json');
    let rootCaps = emptyCapabilities();
    if (existsSync(rootPkgPath)) {
        try {
            rootCaps = detectFromPackageJson(JSON.parse(readFileSync(rootPkgPath, 'utf-8')));
        }
        catch {
            /* fall through with empty caps */
        }
    }
    // Default to the canonical Aperant monorepo layout. Passing `[]`
    // disables the walk entirely (root-only escape hatch per spec ID-05).
    const globs = opts && Array.isArray(opts.monorepo_globs) ? opts.monorepo_globs : ['apps/*'];
    if (globs.length === 0)
        return rootCaps;
    let merged = rootCaps;
    for (const glob of globs) {
        const matches = expandSimpleGlob(root, glob);
        for (const dir of matches) {
            const childPkgPath = join(dir, 'package.json');
            if (!existsSync(childPkgPath))
                continue;
            try {
                const childCaps = detectFromPackageJson(JSON.parse(readFileSync(childPkgPath, 'utf-8')));
                merged = orMergeCapabilities(merged, childCaps);
            }
            catch {
                /* skip malformed package.json */
            }
        }
    }
    return merged;
}
/**
 * Expand a simple `prefix/*` glob against the root. Supports ONLY the
 * `<dir>/*` shape (matches each direct subdirectory of `<dir>`). Other
 * glob syntax is intentionally rejected — v1 keeps the dep surface
 * small. Fails closed on malformed patterns (returns []).
 *
 * @param {string} root
 * @param {string} glob
 * @returns {string[]}
 */
function expandSimpleGlob(root, glob) {
    if (typeof glob !== 'string' || !glob.endsWith('/*'))
        return [];
    const prefix = glob.slice(0, -2);
    if (prefix.length === 0 || prefix.includes('*'))
        return [];
    const dir = join(root, prefix);
    if (!existsSync(dir))
        return [];
    let entries;
    try {
        entries = readdirSync(dir);
    }
    catch {
        return [];
    }
    const matches = [];
    for (const name of entries) {
        const sub = join(dir, name);
        try {
            if (statSync(sub).isDirectory())
                matches.push(sub);
        }
        catch {
            /* skip unreadable entries */
        }
    }
    return matches;
}
/**
 * Boolean OR-merge of two capability records. Any flag true in either
 * input is true in the output. The "Electron wins over web" rule from
 * `detectFromPackageJson` is preserved when this is called repeatedly —
 * each child's flags are honest about that child's own shape, and the
 * OR-merge just unions them.
 *
 * @param {RuntimeCapabilities} a
 * @param {RuntimeCapabilities} b
 * @returns {RuntimeCapabilities}
 */
function orMergeCapabilities(a, b) {
    return {
        is_electron: a.is_electron || b.is_electron,
        is_tauri: a.is_tauri || b.is_tauri,
        is_nextjs: a.is_nextjs || b.is_nextjs,
        is_nuxt: a.is_nuxt || b.is_nuxt,
        is_web_frontend: a.is_web_frontend || b.is_web_frontend,
        is_expo: a.is_expo || b.is_expo,
        is_react_native: a.is_react_native || b.is_react_native,
        has_node: a.has_node || b.has_node,
    };
}
//# sourceMappingURL=runtime-detect.mjs.map