/**
 * Extension loader - loads TypeScript extension modules using jiti.
 */
import { createJiti } from 'jiti';
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 { CONFIG_DIR_NAME, getAgentDir } from '../../config.js';
import { createRuntimeExtensionRuntime } from '../../contracts/extension-runtime.js';
import { createEventBus } from './event-bus.js';
const require = createRequire(import.meta.url);
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 appIndex = path.resolve(__dirname, '../..', 'app', 'index.js');
    const extensionsIndex = path.resolve(__dirname, '../..', 'extensions.js');
    const aiIndex = path.resolve(__dirname, '../..', 'ai', 'index.js');
    const aiProvidersIndex = path.resolve(__dirname, '../..', 'ai', 'providers', 'index.js');
    const shellIndex = path.resolve(__dirname, '../..', 'shell', 'index.js');
    const typeboxEntry = require.resolve('@sinclair/typebox');
    const typeboxRoot = typeboxEntry.replace(/[\\/]build[\\/]cjs[\\/]index\.js$/, '');
    _aliases = {
        shortcutxl: packageIndex,
        'shortcutxl/app': appIndex,
        'shortcutxl/extensions': extensionsIndex,
        'shortcutxl/ai': aiIndex,
        'shortcutxl/ai/providers': aiProvidersIndex,
        'shortcutxl/shell': shellIndex,
        '@sinclair/typebox': typeboxRoot
    };
    return _aliases;
}
const UNICODE_SPACES = /[\u00A0\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);
}
export function createExtensionRuntime() {
    return createRuntimeExtensionRuntime();
}
/**
 * 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();
        },
        registerCommand(name, options) {
            extension.commands.set(name, { name, ...options });
        },
        appendEntry(customType, data) {
            runtime.appendEntry(customType, data);
        },
        registerPostTurnHandler(handler) {
            extension.postTurnHandlers.push(handler);
        },
        registerStopHook(hook) {
            extension.stopHooks.push(hook);
        }
    };
    return api;
}
async function loadExtensionModule(extensionPath) {
    const jiti = createJiti(import.meta.url, {
        alias: getAliases(),
        moduleCache: false
    });
    const extensionModule = await jiti.import(extensionPath, { default: true });
    const factory = extensionModule;
    return typeof factory !== 'function' ? undefined : factory;
}
/**
 * Create an Extension object with empty collections.
 */
function createExtension(extensionPath, resolvedPath, visibility = 'visible') {
    return {
        path: extensionPath,
        resolvedPath,
        visibility,
        handlers: new Map(),
        tools: new Map(),
        commands: new Map(),
        postTurnHandlers: [],
        stopHooks: []
    };
}
async function loadExtension(extensionPath, cwd, eventBus, runtime) {
    const resolvedPath = resolvePath(extensionPath, cwd);
    try {
        const factory = await loadExtensionModule(resolvedPath);
        if (!factory) {
            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);
        return { extension, error: null };
    }
    catch (err) {
        const message = err instanceof Error ? err.message : String(err);
        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>', options) {
    const extension = createExtension(extensionPath, extensionPath, options?.visibility);
    const api = createExtensionAPI(extension, runtime, cwd, eventBus);
    await factory(api);
    return extension;
}
/**
 * Load extensions from paths.
 */
export async function loadExtensions(paths, cwd, eventBus) {
    const extensions = [];
    const errors = [];
    const resolvedEventBus = eventBus ?? createEventBus();
    const runtime = createExtensionRuntime();
    for (const extPath of paths) {
        const { extension, error } = await loadExtension(extPath, cwd, resolvedEventBus, runtime);
        if (error) {
            errors.push({ path: extPath, error });
            continue;
        }
        if (extension) {
            extensions.push(extension);
        }
    }
    return {
        extensions,
        errors,
        runtime
    };
}
function readShortcutManifest(packageJsonPath) {
    try {
        const content = fs.readFileSync(packageJsonPath, 'utf-8');
        const pkg = JSON.parse(content);
        if (pkg.shortcut && typeof pkg.shortcut === 'object') {
            return pkg.shortcut;
        }
        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 "shortcut.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 "shortcut" field first.
    const packageJsonPath = path.join(dir, 'package.json');
    if (fs.existsSync(packageJsonPath)) {
        const manifest = readShortcutManifest(packageJsonPath);
        if (manifest?.extensions?.length) {
            const entries = [];
            for (const extPath of manifest.extensions) {
                const resolvedExtPath = path.resolve(dir, extPath);
                if (fs.existsSync(resolvedExtPath)) {
                    entries.push(resolvedExtPath);
                }
            }
            if (entries.length > 0) {
                return entries;
            }
        }
    }
    // 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 "shortcut" 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/.shortcut/extensions/
    const localExtDir = path.join(cwd, CONFIG_DIR_NAME, 'extensions');
    addPaths(discoverExtensionsInDir(localExtDir));
    // 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