/**
 * Extension loader - loads TypeScript extension modules using jiti.
 *
 * Uses @mariozechner/jiti fork with virtualModules support for compiled Bun binaries.
 */
import * as fs from "node:fs";
import { createRequire } from "node:module";
import * as os from "node:os";
import * as path from "node:path";
import { fileURLToPath } from "node:url";
import { createJiti } from "@mariozechner/jiti";
import { CONFIG_DIR_NAME, getAgentDir, isBunBinary } from "../../config.js";
import { createEventBus } from "../event-bus.js";
import { execCommand } from "../exec.js";
import { getUntrustedExtensionPaths } from "./project-trust.js";
export { isProjectTrusted, trustProject, getUntrustedExtensionPaths } from "./project-trust.js";
/**
 * Bundled modules for Bun binary virtualModules.
 *
 * In Node.js mode these stay empty: extensions resolve via jiti aliases instead.
 * In Bun binary mode, dynamic imports populate the maps so the compiled binary
 * can serve them as virtual modules to extensions.
 *
 * The dynamic imports are intentional: ESM static imports are always evaluated
 * by the JS engine regardless of conditional code, so converting them to
 * dynamic import() calls lets Node.js skip loading ~16 packages at startup.
 */
let STATIC_BUNDLED_MODULES = {};
let VIRTUAL_MODULES = {};
if (isBunBinary) {
    // Dynamic imports: only executed in compiled Bun binaries.
    // NOTE: These specifiers must match what Bun's bundler sees at compile time.
    // loader.ts exports are NOT re-exported from index.ts, avoiding a circular dependency.
    const [_bundledPiAgentCore, _bundledPiAi, _bundledPiAiOauth, _bundledPiTui, _bundledTypebox, _bundledYaml, _bundledMcpClient, _bundledMcpStdio, _bundledMcpStreamableHttp, _bundledMcpSse, _bundledMcpServer, _bundledMcpServerStdio, _bundledMcpServerSse, _bundledMcpServerStreamableHttp, _bundledMcpTypes, _bundledPiCodingAgent,] = await Promise.all([
        import("@nerf/pi-agent-core"),
        import("@nerf/pi-ai"),
        import("@nerf/pi-ai/oauth"),
        import("@nerf/pi-tui"),
        import("@sinclair/typebox"),
        import("yaml"),
        import("@modelcontextprotocol/sdk/client"),
        import("@modelcontextprotocol/sdk/client/stdio.js"),
        import("@modelcontextprotocol/sdk/client/streamableHttp.js"),
        import("@modelcontextprotocol/sdk/client/sse.js"),
        import("@modelcontextprotocol/sdk/server"),
        import("@modelcontextprotocol/sdk/server/stdio.js"),
        import("@modelcontextprotocol/sdk/server/sse.js"),
        import("@modelcontextprotocol/sdk/server/streamableHttp.js"),
        import("@modelcontextprotocol/sdk/types.js"),
        import("../../index.js"),
    ]);
    STATIC_BUNDLED_MODULES = {
        "@sinclair/typebox": _bundledTypebox,
        "@nerf/pi-agent-core": _bundledPiAgentCore,
        "@nerf/pi-tui": _bundledPiTui,
        "@nerf/pi-ai": _bundledPiAi,
        "@nerf/pi-ai/oauth": _bundledPiAiOauth,
        "@nerf/pi-coding-agent": _bundledPiCodingAgent,
        "yaml": _bundledYaml,
        "@modelcontextprotocol/sdk/client": _bundledMcpClient,
        "@modelcontextprotocol/sdk/client/stdio": _bundledMcpStdio,
        "@modelcontextprotocol/sdk/client/stdio.js": _bundledMcpStdio,
        "@modelcontextprotocol/sdk/client/streamableHttp": _bundledMcpStreamableHttp,
        "@modelcontextprotocol/sdk/client/streamableHttp.js": _bundledMcpStreamableHttp,
        "@modelcontextprotocol/sdk/client/sse": _bundledMcpSse,
        "@modelcontextprotocol/sdk/client/sse.js": _bundledMcpSse,
        "@modelcontextprotocol/sdk/server": _bundledMcpServer,
        "@modelcontextprotocol/sdk/server/stdio": _bundledMcpServerStdio,
        "@modelcontextprotocol/sdk/server/stdio.js": _bundledMcpServerStdio,
        "@modelcontextprotocol/sdk/server/sse": _bundledMcpServerSse,
        "@modelcontextprotocol/sdk/server/sse.js": _bundledMcpServerSse,
        "@modelcontextprotocol/sdk/server/streamableHttp": _bundledMcpServerStreamableHttp,
        "@modelcontextprotocol/sdk/server/streamableHttp.js": _bundledMcpServerStreamableHttp,
        "@modelcontextprotocol/sdk/types": _bundledMcpTypes,
        "@modelcontextprotocol/sdk/types.js": _bundledMcpTypes,
        // Aliases for external PI ecosystem packages that import from the original scope
        "@mariozechner/pi-agent-core": _bundledPiAgentCore,
        "@mariozechner/pi-tui": _bundledPiTui,
        "@mariozechner/pi-ai": _bundledPiAi,
        "@mariozechner/pi-ai/oauth": _bundledPiAiOauth,
        "@mariozechner/pi-coding-agent": _bundledPiCodingAgent,
    };
    VIRTUAL_MODULES = { ...STATIC_BUNDLED_MODULES };
}
const require = createRequire(import.meta.url);
const EXTENSION_TIMING_ENABLED = process.env.NERF_STARTUP_TIMING === "1" || process.env.PI_TIMING === "1";
/**
 * Bundled npm packages whose subpath exports should be auto-resolved for extensions.
 * Each package listed here will have its `exports` field read from package.json,
 * and all subpath exports will be registered as jiti aliases (Node.js mode) so that
 * extensions can import any standard subpath without hitting jiti's CJS double-resolve bug.
 */
const BUNDLED_PACKAGES_WITH_EXPORTS = [
    "@modelcontextprotocol/sdk",
    "yaml",
];
/**
 * Read a package's `exports` field and return alias entries mapping
 * specifiers (e.g. `@modelcontextprotocol/sdk/server`) to resolved file paths.
 *
 * Handles:
 * - Explicit subpath exports: `./client` -> `@pkg/client`
 * - Wildcard exports (`./*`): scans the package's dist directory for actual files
 * - Both `.js`-suffixed and bare specifiers for each subpath
 */
function resolveSubpathExports(packageName) {
    const aliases = {};
    let packageJsonPath;
    try {
        // Resolve the package's root directory via its package.json
        packageJsonPath = require.resolve(`${packageName}/package.json`);
    }
    catch {
        // Package doesn't allow importing package.json via exports — find it manually
        try {
            const anyEntry = require.resolve(packageName);
            // Walk up from the resolved entry to find package.json
            let dir = path.dirname(anyEntry);
            while (dir !== path.dirname(dir)) {
                const candidate = path.join(dir, "package.json");
                if (fs.existsSync(candidate)) {
                    try {
                        const pkg = JSON.parse(fs.readFileSync(candidate, "utf-8"));
                        if (pkg.name === packageName) {
                            packageJsonPath = candidate;
                            break;
                        }
                    }
                    catch {
                        // not valid JSON, keep walking
                    }
                }
                dir = path.dirname(dir);
            }
        }
        catch {
            return aliases;
        }
        if (!packageJsonPath)
            return aliases;
    }
    let pkg;
    try {
        pkg = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
    }
    catch {
        return aliases;
    }
    const exports = pkg.exports;
    if (!exports || typeof exports !== "object")
        return aliases;
    const packageDir = path.dirname(packageJsonPath);
    for (const [subpath, target] of Object.entries(exports)) {
        if (subpath === ".")
            continue; // Root export handled by static imports
        // Handle wildcard exports like "./*"
        if (subpath.includes("*")) {
            resolveWildcardExports(packageName, packageDir, subpath, target, aliases);
            continue;
        }
        // Explicit subpath: "./client" -> "@pkg/client"
        const specifier = `${packageName}/${subpath.replace(/^\.\//, "")}`;
        try {
            const resolved = require.resolve(specifier);
            aliases[specifier] = resolved;
            // Add .js-suffixed variant if the specifier doesn't already end in .js
            if (!specifier.endsWith(".js")) {
                const jsSpecifier = `${specifier}.js`;
                try {
                    const jsResolved = require.resolve(jsSpecifier);
                    aliases[jsSpecifier] = jsResolved;
                }
                catch {
                    // .js variant doesn't resolve — that's fine
                }
            }
            // Add bare variant (without .js) if it ends in .js
            if (specifier.endsWith(".js")) {
                const bareSpecifier = specifier.slice(0, -3);
                try {
                    const bareResolved = require.resolve(bareSpecifier);
                    aliases[bareSpecifier] = bareResolved;
                }
                catch {
                    // bare variant doesn't resolve — that's fine
                }
            }
        }
        catch {
            // Subpath doesn't resolve — skip it
        }
    }
    return aliases;
}
/**
 * Resolve wildcard export patterns (e.g. `./*`) by scanning the package's
 * file structure to find all matching files and generate alias entries.
 */
function resolveWildcardExports(packageName, packageDir, subpathPattern, target, aliases) {
    // Extract the target directory pattern from the export target
    // e.g. { "require": "./dist/cjs/*" } -> "dist/cjs"
    let targetDir = null;
    if (typeof target === "string") {
        targetDir = target.replace(/\/\*$/, "").replace(/^\.\//, "");
    }
    else if (target && typeof target === "object") {
        const targetObj = target;
        // Prefer "require" for CJS compatibility with jiti, fall back to "import"
        const resolved = targetObj.require ?? targetObj.import ?? targetObj.default;
        if (typeof resolved === "string") {
            targetDir = resolved.replace(/\/\*$/, "").replace(/^\.\//, "");
        }
    }
    if (!targetDir)
        return;
    const fullTargetDir = path.join(packageDir, targetDir);
    if (!fs.existsSync(fullTargetDir))
        return;
    // Scan for .js files and generate specifiers
    const subpathPrefix = subpathPattern.replace(/\/?\*$/, "").replace(/^\.\//, "");
    scanDirForExports(packageName, fullTargetDir, subpathPrefix, aliases);
}
/**
 * Recursively scan a directory for .js files and register them as aliases.
 */
function scanDirForExports(packageName, dir, relativePath, aliases) {
    let entries;
    try {
        entries = fs.readdirSync(dir, { withFileTypes: true });
    }
    catch {
        return;
    }
    for (const entry of entries) {
        const entryRelative = relativePath ? `${relativePath}/${entry.name}` : entry.name;
        if (entry.isDirectory()) {
            // Skip examples/test directories — extensions don't need them
            if (entry.name === "examples" || entry.name === "__tests__" || entry.name === "test")
                continue;
            scanDirForExports(packageName, path.join(dir, entry.name), entryRelative, aliases);
        }
        else if (entry.name.endsWith(".js") && !entry.name.endsWith(".d.js")) {
            const filePath = path.join(dir, entry.name);
            const specifier = `${packageName}/${entryRelative}`;
            // Only add if not already covered by an explicit export
            if (!(specifier in aliases)) {
                aliases[specifier] = filePath;
            }
            // Also add bare (no .js) variant
            const bareSpecifier = specifier.replace(/\.js$/, "");
            if (!(bareSpecifier in aliases)) {
                aliases[bareSpecifier] = filePath;
            }
        }
    }
}
function logExtensionTiming(extensionPath, ms, outcome) {
    if (!EXTENSION_TIMING_ENABLED)
        return;
    console.error(`[startup] extension ${outcome}: ${extensionPath} (${ms}ms)`);
}
/**
 * Get aliases for jiti (used in Node.js/development mode).
 * In Bun binary mode, virtualModules is used instead.
 */
let _aliases = null;
function getAliases() {
    if (_aliases)
        return _aliases;
    const __dirname = path.dirname(fileURLToPath(import.meta.url));
    const packageIndex = path.resolve(__dirname, "../..", "index.js");
    const typeboxEntry = require.resolve("@sinclair/typebox");
    const typeboxRoot = typeboxEntry.replace(/[\\/]build[\\/]cjs[\\/]index\.js$/, "");
    const yamlEntry = require.resolve("yaml");
    const yamlRoot = yamlEntry.replace(/[\\/]dist[\\/]index\.js$/, "");
    const packagesRoot = path.resolve(__dirname, "../../../../");
    const resolveWorkspaceOrImport = (workspaceRelativePath, specifier) => {
        const workspacePath = path.join(packagesRoot, workspaceRelativePath);
        if (fs.existsSync(workspacePath)) {
            return workspacePath;
        }
        return fileURLToPath(import.meta.resolve(specifier));
    };
    // Auto-discover subpath exports from bundled npm packages.
    // This ensures extensions can import any standard subpath (e.g. @modelcontextprotocol/sdk/server)
    // without hitting jiti's CJS double-resolve bug.
    const autoDiscovered = {};
    for (const packageName of BUNDLED_PACKAGES_WITH_EXPORTS) {
        const subpathAliases = resolveSubpathExports(packageName);
        Object.assign(autoDiscovered, subpathAliases);
    }
    _aliases = {
        // Auto-discovered subpath exports (lowest priority — overridden by manual entries below)
        ...autoDiscovered,
        // Manual entries for workspace packages and packages needing special resolution
        "@nerf/pi-coding-agent": packageIndex,
        "@nerf/pi-agent-core": resolveWorkspaceOrImport("agent/dist/index.js", "@nerf/pi-agent-core"),
        "@nerf/pi-tui": resolveWorkspaceOrImport("tui/dist/index.js", "@nerf/pi-tui"),
        "@nerf/pi-ai": resolveWorkspaceOrImport("ai/dist/index.js", "@nerf/pi-ai"),
        "@nerf/pi-ai/oauth": resolveWorkspaceOrImport("ai/dist/oauth.js", "@nerf/pi-ai/oauth"),
        "@sinclair/typebox": typeboxRoot,
        "yaml": yamlRoot,
        // Aliases for external PI ecosystem packages that import from the original scope
        "@mariozechner/pi-coding-agent": packageIndex,
        "@mariozechner/pi-agent-core": resolveWorkspaceOrImport("agent/dist/index.js", "@nerf/pi-agent-core"),
        "@mariozechner/pi-tui": resolveWorkspaceOrImport("tui/dist/index.js", "@nerf/pi-tui"),
        "@mariozechner/pi-ai": resolveWorkspaceOrImport("ai/dist/index.js", "@nerf/pi-ai"),
        "@mariozechner/pi-ai/oauth": resolveWorkspaceOrImport("ai/dist/oauth.js", "@nerf/pi-ai/oauth"),
    };
    return _aliases;
}
function getJitiOptions() {
    return isBunBinary ? { virtualModules: VIRTUAL_MODULES, tryNative: false } : { alias: getAliases() };
}
const _moduleImporters = new Map();
function getModuleImporter(parentModuleUrl) {
    let importer = _moduleImporters.get(parentModuleUrl);
    if (!importer) {
        importer = createJiti(parentModuleUrl, {
            moduleCache: true,
            ...getJitiOptions(),
        });
        _moduleImporters.set(parentModuleUrl, importer);
    }
    return importer;
}
export async function importExtensionModule(parentModuleUrl, specifier) {
    const importer = getModuleImporter(parentModuleUrl);
    const resolvedPath = fileURLToPath(new URL(specifier, parentModuleUrl));
    return importer.import(resolvedPath);
}
const UNICODE_SPACES = /[\u00A0\u1680\u2000-\u200A\u202F\u205F\u3000]/g;
function normalizeUnicodeSpaces(str) {
    return str.replace(UNICODE_SPACES, " ");
}
function expandPath(p) {
    const normalized = normalizeUnicodeSpaces(p);
    if (normalized.startsWith("~/")) {
        return path.join(os.homedir(), normalized.slice(2));
    }
    if (normalized.startsWith("~")) {
        return path.join(os.homedir(), normalized.slice(1));
    }
    return normalized;
}
function resolvePath(extPath, cwd) {
    const expanded = expandPath(extPath);
    if (path.isAbsolute(expanded)) {
        return expanded;
    }
    return path.resolve(cwd, expanded);
}
/**
 * Create a runtime with throwing stubs for action methods.
 * Runner.bindCore() replaces these with real implementations.
 */
export function createExtensionRuntime() {
    const notInitialized = () => {
        throw new Error("Extension runtime not initialized. Action methods cannot be called during extension loading.");
    };
    const runtime = {
        sendMessage: notInitialized,
        sendUserMessage: notInitialized,
        retryLastTurn: notInitialized,
        appendEntry: notInitialized,
        setSessionName: notInitialized,
        getSessionName: notInitialized,
        setLabel: notInitialized,
        getActiveTools: notInitialized,
        getAllTools: notInitialized,
        setActiveTools: notInitialized,
        // registerTool() is valid during extension load; refresh is only needed post-bind.
        refreshTools: () => { },
        getCommands: notInitialized,
        setModel: () => Promise.reject(new Error("Extension runtime not initialized")),
        getThinkingLevel: notInitialized,
        setThinkingLevel: notInitialized,
        flagValues: new Map(),
        pendingProviderRegistrations: [],
        // Pre-bind: queue registrations so bindCore() can flush them once the
        // model registry is available. bindCore() replaces both with direct calls.
        registerProvider: (name, config) => {
            runtime.pendingProviderRegistrations.push({ name, config });
        },
        unregisterProvider: (name) => {
            runtime.pendingProviderRegistrations = runtime.pendingProviderRegistrations.filter((r) => r.name !== name);
        },
    };
    return runtime;
}
/**
 * Create the ExtensionAPI for an extension.
 * Registration methods write to the extension object.
 * Action methods delegate to the shared runtime.
 */
function createExtensionAPI(extension, runtime, cwd, eventBus) {
    const api = {
        // Registration methods - write to extension
        on(event, handler) {
            const list = extension.handlers.get(event) ?? [];
            list.push(handler);
            extension.handlers.set(event, list);
        },
        registerTool(tool) {
            extension.tools.set(tool.name, {
                definition: tool,
                extensionPath: extension.path,
            });
            runtime.refreshTools();
        },
        registerSandboxedTool(tool) {
            // Defer WASM loading to first execute() call.
            // At registration time we just wrap the tool definition so the
            // extension system sees a normal ToolDefinition. The actual
            // WasmToolHost + CredentialBoundary are created lazily.
            const originalExecute = tool.execute;
            const wasmPath = tool.wasmPath;
            const allowedSecrets = tool.allowedSecrets ?? [];
            const toolName = tool.name;
            // Create a regular ToolDefinition that delegates to the WASM sandbox
            const adapted = {
                name: tool.name,
                label: tool.label,
                description: tool.description,
                promptSnippet: tool.promptSnippet,
                promptGuidelines: tool.promptGuidelines,
                parameters: tool.parameters,
                renderCall: tool.renderCall,
                renderResult: tool.renderResult,
                async execute(toolCallId, params, signal, onUpdate, ctx) {
                    // If the tool provides its own execute(), use it (it may handle
                    // WASM setup internally via SandboxedToolAdapter)
                    if (originalExecute) {
                        return originalExecute.call(tool, toolCallId, params, signal, onUpdate, ctx);
                    }
                    // Fallback: return error indicating the tool needs an execute implementation
                    return {
                        content: [{ type: "text", text: `Sandboxed tool "${toolName}" has no execute implementation. Use SandboxedToolAdapter to bridge WASM plugins.` }],
                        isError: true,
                        details: undefined,
                    };
                },
            };
            extension.tools.set(adapted.name, {
                definition: adapted,
                extensionPath: extension.path,
            });
            runtime.refreshTools();
        },
        registerCommand(name, options) {
            extension.commands.set(name, { name, ...options });
        },
        registerBeforeInstall(handler) {
            extension.lifecycleHooks.beforeInstall.push(handler);
        },
        registerAfterInstall(handler) {
            extension.lifecycleHooks.afterInstall.push(handler);
        },
        registerBeforeRemove(handler) {
            extension.lifecycleHooks.beforeRemove.push(handler);
        },
        registerAfterRemove(handler) {
            extension.lifecycleHooks.afterRemove.push(handler);
        },
        registerShortcut(shortcut, options) {
            extension.shortcuts.set(shortcut, { shortcut, extensionPath: extension.path, ...options });
        },
        registerFlag(name, options) {
            extension.flags.set(name, { name, extensionPath: extension.path, ...options });
            if (options.default !== undefined && !runtime.flagValues.has(name)) {
                runtime.flagValues.set(name, options.default);
            }
        },
        registerMessageRenderer(customType, renderer) {
            extension.messageRenderers.set(customType, renderer);
        },
        // Flag access - checks extension registered it, reads from runtime
        getFlag(name) {
            if (!extension.flags.has(name))
                return undefined;
            return runtime.flagValues.get(name);
        },
        // Action methods - delegate to shared runtime
        sendMessage(message, options) {
            runtime.sendMessage(message, options);
        },
        sendUserMessage(content, options) {
            runtime.sendUserMessage(content, options);
        },
        retryLastTurn() {
            runtime.retryLastTurn();
        },
        appendEntry(customType, data) {
            runtime.appendEntry(customType, data);
        },
        setSessionName(name) {
            runtime.setSessionName(name);
        },
        getSessionName() {
            return runtime.getSessionName();
        },
        setLabel(entryId, label) {
            runtime.setLabel(entryId, label);
        },
        exec(command, args, options) {
            return execCommand(command, args, options?.cwd ?? cwd, options);
        },
        getActiveTools() {
            return runtime.getActiveTools();
        },
        getAllTools() {
            return runtime.getAllTools();
        },
        setActiveTools(toolNames) {
            runtime.setActiveTools(toolNames);
        },
        getCommands() {
            return runtime.getCommands();
        },
        setModel(model) {
            return runtime.setModel(model);
        },
        getThinkingLevel() {
            return runtime.getThinkingLevel();
        },
        setThinkingLevel(level) {
            runtime.setThinkingLevel(level);
        },
        registerProvider(name, config) {
            runtime.registerProvider(name, config);
        },
        unregisterProvider(name) {
            runtime.unregisterProvider(name);
        },
        events: eventBus,
    };
    return api;
}
/**
 * Heuristic patterns that indicate TypeScript syntax in a source file.
 * Used to detect when a .js file accidentally contains TypeScript code
 * and provide a helpful error message instead of a cryptic parse failure.
 */
const TS_SYNTAX_PATTERNS = [
    // Variable type annotations: const name: string, let count: number
    /\b(?:const|let|var)\s+\w+\s*:\s*(?:string|number|boolean|any|void|never|unknown|object|bigint|symbol|undefined|null)\b/,
    // Parameter type annotations: (api: ExtensionAPI)
    /\(\s*\w+\s*:\s*[A-Z]\w*/,
    // Return type annotations: ): Promise<void> {  or  ): string =>
    /\)\s*:\s*(?:Promise|string|number|boolean|void|any|never|unknown)\b/,
    // Interface declarations
    /\binterface\s+[A-Z]\w*\s*(?:<[^>]*>)?\s*\{/,
    // Type alias declarations
    /\btype\s+[A-Z]\w*\s*(?:<[^>]*>)?\s*=/,
    // Angle-bracket type assertions: <Type>value
    /(?:as\s+\w+(?:<[^>]*>)?)\s*[;,)\]}]/,
    // Generic type parameters on functions: function foo<T>
    /\bfunction\s+\w+\s*<[^>]+>/,
    // Enum declarations
    /\benum\s+[A-Z]\w*\s*\{/,
];
/**
 * Check whether a source string likely contains TypeScript syntax.
 * This is a heuristic — it may produce false positives for unusual JS,
 * but is tuned to catch the most common TS-in-JS mistakes.
 */
export function containsTypeScriptSyntax(source) {
    return TS_SYNTAX_PATTERNS.some((pattern) => pattern.test(source));
}
async function loadExtensionModule(extensionPath) {
    // Pre-compiled extension loading: if the source is .ts and a sibling .js
    // file exists with matching or newer mtime, use native import() to skip
    // jiti JIT compilation entirely.  This is the biggest startup win for
    // bundled extensions that have already been built.
    if (extensionPath.endsWith(".ts")) {
        const jsPath = extensionPath.replace(/\.ts$/, ".js");
        try {
            const [tsStat, jsStat] = [fs.statSync(extensionPath), fs.statSync(jsPath)];
            if (jsStat.mtimeMs >= tsStat.mtimeMs) {
                const module = await import(jsPath);
                const factory = (module.default ?? module);
                return typeof factory !== "function" ? undefined : factory;
            }
        }
        catch {
            // .js file doesn't exist or stat failed — fall through to jiti
        }
    }
    const jiti = createJiti(import.meta.url, {
        moduleCache: false,
        ...getJitiOptions(),
    });
    const module = await jiti.import(extensionPath, { default: true });
    const factory = module;
    return typeof factory !== "function" ? undefined : factory;
}
/**
 * Check whether a module path belongs to a non-extension library that should
 * be silently skipped rather than reported as an error.
 *
 * A directory is a non-extension library when its package.json has a "pi"
 * manifest that declares no extensions (e.g. `"pi": {}`). This is the
 * opt-out convention used by shared libraries like cmux that live inside
 * the extensions/ directory but are not extensions themselves.
 *
 * This serves as a defense-in-depth check: even if the upstream discovery
 * layers fail to filter out the library, the loader itself will not emit
 * a spurious error.
 */
function isNonExtensionLibrary(resolvedPath) {
    // Walk up from the resolved file to find the nearest package.json
    let dir = path.dirname(resolvedPath);
    const root = path.parse(dir).root;
    while (dir !== root) {
        const packageJsonPath = path.join(dir, "package.json");
        if (fs.existsSync(packageJsonPath)) {
            try {
                const content = fs.readFileSync(packageJsonPath, "utf-8");
                const pkg = JSON.parse(content);
                if (pkg.pi && typeof pkg.pi === "object") {
                    // Has a pi manifest — check if it declares any extensions
                    const extensions = pkg.pi.extensions;
                    if (!Array.isArray(extensions) || extensions.length === 0) {
                        return true;
                    }
                }
            }
            catch {
                // Malformed package.json — not a known library
            }
            break;
        }
        dir = path.dirname(dir);
    }
    return false;
}
/**
 * Create an Extension object with empty collections.
 */
function createExtension(extensionPath, resolvedPath) {
    return {
        path: extensionPath,
        resolvedPath,
        handlers: new Map(),
        tools: new Map(),
        messageRenderers: new Map(),
        commands: new Map(),
        flags: new Map(),
        shortcuts: new Map(),
        lifecycleHooks: {
            beforeInstall: [],
            afterInstall: [],
            beforeRemove: [],
            afterRemove: [],
        },
    };
}
async function loadExtension(extensionPath, cwd, eventBus, runtime) {
    const resolvedPath = resolvePath(extensionPath, cwd);
    const start = Date.now();
    try {
        const factory = await loadExtensionModule(resolvedPath);
        if (!factory) {
            // Defense-in-depth: if the module is inside a directory that has
            // explicitly opted out of extension loading via its pi manifest,
            // silently skip it instead of reporting a spurious error.
            if (isNonExtensionLibrary(resolvedPath)) {
                return { extension: null, error: null };
            }
            logExtensionTiming(extensionPath, Date.now() - start, "failed");
            // Check if a .js file contains TypeScript syntax
            if (resolvedPath.endsWith(".js")) {
                try {
                    const source = fs.readFileSync(resolvedPath, "utf-8");
                    if (containsTypeScriptSyntax(source)) {
                        return {
                            extension: null,
                            error: `Extension file "${extensionPath}" appears to contain TypeScript syntax but has a .js extension. Rename it to .ts so the loader can compile it.`,
                        };
                    }
                }
                catch {
                    // Could not read file — fall through to generic error
                }
            }
            return { extension: null, error: `Extension does not export a valid factory function: ${extensionPath}` };
        }
        const extension = createExtension(extensionPath, resolvedPath);
        const api = createExtensionAPI(extension, runtime, cwd, eventBus);
        await factory(api);
        logExtensionTiming(extensionPath, Date.now() - start, "loaded");
        return { extension, error: null };
    }
    catch (err) {
        const message = err instanceof Error ? err.message : String(err);
        logExtensionTiming(extensionPath, Date.now() - start, "failed");
        // Check if a .js file contains TypeScript syntax — the parse error from
        // jiti/Node is often cryptic, so surface a clearer diagnostic.
        if (resolvedPath.endsWith(".js")) {
            try {
                const source = fs.readFileSync(resolvedPath, "utf-8");
                if (containsTypeScriptSyntax(source)) {
                    return {
                        extension: null,
                        error: `Extension file "${extensionPath}" appears to contain TypeScript syntax but has a .js extension. Rename it to .ts so the loader can compile it.`,
                    };
                }
            }
            catch {
                // Could not read file — fall through to generic error
            }
        }
        return { extension: null, error: `Failed to load extension: ${message}` };
    }
}
/**
 * Create an Extension from an inline factory function.
 */
export async function loadExtensionFromFactory(factory, cwd, eventBus, runtime, extensionPath = "<inline>") {
    const extension = createExtension(extensionPath, extensionPath);
    const api = createExtensionAPI(extension, runtime, cwd, eventBus);
    await factory(api);
    return extension;
}
/**
 * Load extensions from paths.
 *
 * Extensions are loaded in parallel to reduce wall-clock time (~30-50% faster
 * than sequential loading for I/O-bound jiti compilation).
 */
export async function loadExtensions(paths, cwd, eventBus) {
    const resolvedEventBus = eventBus ?? createEventBus();
    const runtime = createExtensionRuntime();
    const results = await Promise.all(paths.map((extPath) => loadExtension(extPath, cwd, resolvedEventBus, runtime)));
    const extensions = [];
    const errors = [];
    for (let i = 0; i < results.length; i++) {
        const { extension, error } = results[i];
        if (error) {
            errors.push({ path: paths[i], error });
        }
        else if (extension) {
            extensions.push(extension);
        }
    }
    return {
        extensions,
        errors,
        runtime,
    };
}
function readPiManifest(packageJsonPath) {
    try {
        const content = fs.readFileSync(packageJsonPath, "utf-8");
        const pkg = JSON.parse(content);
        if (pkg.pi && typeof pkg.pi === "object") {
            return pkg.pi;
        }
        return null;
    }
    catch {
        return null;
    }
}
function isExtensionFile(name) {
    return name.endsWith(".ts") || name.endsWith(".js");
}
/**
 * Resolve extension entry points from a directory.
 *
 * Checks for:
 * 1. package.json with "pi.extensions" field -> returns declared paths
 * 2. index.ts or index.js -> returns the index file
 *
 * Returns resolved paths or null if no entry points found.
 */
function resolveExtensionEntries(dir) {
    // Check for package.json with "pi" field first
    const packageJsonPath = path.join(dir, "package.json");
    if (fs.existsSync(packageJsonPath)) {
        const manifest = readPiManifest(packageJsonPath);
        if (manifest) {
            // When a pi manifest exists, it is authoritative — don't fall through
            // to index.ts/index.js auto-detection. This allows library directories
            // (like cmux) to opt out by declaring "pi": {} with no extensions.
            if (!manifest.extensions?.length) {
                return null;
            }
            const entries = [];
            for (const extPath of manifest.extensions) {
                const resolvedExtPath = path.resolve(dir, extPath);
                if (fs.existsSync(resolvedExtPath)) {
                    entries.push(resolvedExtPath);
                }
            }
            return entries.length > 0 ? entries : null;
        }
    }
    // Check for index.ts or index.js
    const indexTs = path.join(dir, "index.ts");
    const indexJs = path.join(dir, "index.js");
    if (fs.existsSync(indexTs)) {
        return [indexTs];
    }
    if (fs.existsSync(indexJs)) {
        return [indexJs];
    }
    return null;
}
/**
 * Discover extensions in a directory.
 *
 * Discovery rules:
 * 1. Direct files: `extensions/*.ts` or `*.js` → load
 * 2. Subdirectory with index: `extensions/* /index.ts` or `index.js` → load
 * 3. Subdirectory with package.json: `extensions/* /package.json` with "pi" field → load what it declares
 *
 * No recursion beyond one level. Complex packages must use package.json manifest.
 */
function discoverExtensionsInDir(dir) {
    if (!fs.existsSync(dir)) {
        return [];
    }
    const discovered = [];
    try {
        const entries = fs.readdirSync(dir, { withFileTypes: true });
        for (const entry of entries) {
            const entryPath = path.join(dir, entry.name);
            // 1. Direct files: *.ts or *.js
            if ((entry.isFile() || entry.isSymbolicLink()) && isExtensionFile(entry.name)) {
                discovered.push(entryPath);
                continue;
            }
            // 2 & 3. Subdirectories
            if (entry.isDirectory() || entry.isSymbolicLink()) {
                const entries = resolveExtensionEntries(entryPath);
                if (entries) {
                    discovered.push(...entries);
                }
            }
        }
    }
    catch {
        return [];
    }
    return discovered;
}
/**
 * Discover and load extensions from standard locations.
 */
export async function discoverAndLoadExtensions(configuredPaths, cwd, agentDir = getAgentDir(), eventBus) {
    const allPaths = [];
    const seen = new Set();
    const addPaths = (paths) => {
        for (const p of paths) {
            const resolved = path.resolve(p);
            if (!seen.has(resolved)) {
                seen.add(resolved);
                allPaths.push(p);
            }
        }
    };
    // 1. Project-local extensions: cwd/{CONFIG_DIR_NAME}/extensions/
    // Only loaded when the project path has been explicitly trusted (TOFU model).
    const localExtDir = path.join(cwd, CONFIG_DIR_NAME, "extensions");
    const localDiscovered = discoverExtensionsInDir(localExtDir);
    if (localDiscovered.length > 0) {
        const untrusted = getUntrustedExtensionPaths(cwd, localDiscovered, agentDir);
        if (untrusted.length > 0) {
            process.stderr.write(`[pi] Skipping ${untrusted.length} project-local extension(s) in ${localExtDir} — project not trusted. Use trustProject() to enable.\n`);
        }
        const trusted = localDiscovered.filter((p) => !untrusted.includes(p));
        addPaths(trusted);
    }
    // 2. Global extensions: agentDir/extensions/
    const globalExtDir = path.join(agentDir, "extensions");
    addPaths(discoverExtensionsInDir(globalExtDir));
    // 3. Explicitly configured paths
    for (const p of configuredPaths) {
        const resolved = resolvePath(p, cwd);
        if (fs.existsSync(resolved) && fs.statSync(resolved).isDirectory()) {
            // Check for package.json with pi manifest or index.ts
            const entries = resolveExtensionEntries(resolved);
            if (entries) {
                addPaths(entries);
                continue;
            }
            // No explicit entries - discover individual files in directory
            addPaths(discoverExtensionsInDir(resolved));
            continue;
        }
        addPaths([resolved]);
    }
    return loadExtensions(allPaths, cwd, eventBus);
}
//# sourceMappingURL=loader.js.map