import { existsSync, realpathSync } from "node:fs"; import { createRequire } from "node:module"; import { dirname, join } from "node:path"; import { pathToFileURL } from "node:url"; import type { ExtensionAPI } from "@mariozechner/pi-coding-agent"; import { clearExtensionsUpdateCache, loadExtensionsRegistry, warmExtensionsUpdateCache } from "./extensions-registry.js"; import { updateAllPackageSources, updatePackageSource } from "./package-actions.js"; import { setExtensionEnabled } from "./extension-enabled-toggle.js"; import type { ExtensionsRegistry } from "./types.js"; import { showExtensionsManager } from "./ui/extensions-manager.js"; const STARTUP_EXTENSIONS_PATCH_KEY = "__piExtensionsMenuHideStartupExtensionsPatched"; function addCandidateVariants(candidates: Set, basePath: string): void { const current = existsSync(basePath) ? realpathSync(basePath) : basePath; let dir = dirname(current); for (let i = 0; i < 5; i += 1) { candidates.add(join(dir, "modes", "interactive", "interactive-mode.js")); candidates.add(join(dir, "dist", "modes", "interactive", "interactive-mode.js")); const parent = dirname(dir); if (parent === dir) break; dir = parent; } } function getInteractiveModeCandidates(): string[] { const candidates = new Set(); const argvEntry = process.argv[1]; if (argvEntry) { addCandidateVariants(candidates, argvEntry); } try { const require = createRequire(import.meta.url); const piEntry = require.resolve("@mariozechner/pi-coding-agent"); addCandidateVariants(candidates, piEntry); } catch { // Ignore. } return [...candidates].filter((candidate) => existsSync(candidate)); } async function patchInteractiveModeToHideStartupExtensionsSection(): Promise { for (const interactiveModePath of getInteractiveModeCandidates()) { try { const module = await import(pathToFileURL(interactiveModePath).href); const prototype = (module as { InteractiveMode?: { prototype?: Record } }).InteractiveMode?.prototype as (Record & { showLoadedResources?: (options?: { extensions?: unknown[]; force?: boolean; showDiagnosticsWhenQuiet?: boolean }) => void; }) | undefined; if (!prototype?.showLoadedResources || prototype[STARTUP_EXTENSIONS_PATCH_KEY]) { continue; } const original = prototype.showLoadedResources; prototype.showLoadedResources = function (options?: { extensions?: unknown[]; force?: boolean; showDiagnosticsWhenQuiet?: boolean }) { const nextOptions = { ...(options ?? {}), extensions: [] }; return original.call(this, nextOptions); }; prototype[STARTUP_EXTENSIONS_PATCH_KEY] = true; return; } catch { // Try next candidate. } } } await patchInteractiveModeToHideStartupExtensionsSection(); const EMPTY_REGISTRY: ExtensionsRegistry = { extensions: [], allExtensions: [], byName: new Map(), }; export default function extensionsMenuExtension(pi: ExtensionAPI) { let registry: ExtensionsRegistry = EMPTY_REGISTRY; let currentCwd: string | undefined; async function refreshRegistry(cwd: string): Promise { registry = await loadExtensionsRegistry(cwd); currentCwd = cwd; return registry; } async function ensureRegistry(cwd: string): Promise { if (currentCwd === cwd && registry.allExtensions.length > 0) return registry; return await refreshRegistry(cwd); } pi.registerCommand("extensions", { description: "Browse, enable, disable, and update extensions", handler: async (_args, ctx) => { if (!ctx.hasUI) { ctx.ui.notify("/extensions requires interactive mode", "warning"); return; } try { await ensureRegistry(ctx.cwd); void warmExtensionsUpdateCache(ctx.cwd).then(async (changed) => { if (!changed || currentCwd !== ctx.cwd) return; registry = await loadExtensionsRegistry(ctx.cwd); }); } catch (error) { console.error("extensions-menu: failed to load extensions registry", error); ctx.ui.notify("Failed to load extensions list", "error"); return; } await showExtensionsManager(ctx, registry, { onRefresh: async () => await refreshRegistry(ctx.cwd), onToggle: async (extension, enabled) => { await setExtensionEnabled(ctx.cwd, extension, enabled); }, onUpdateOne: async (extension, signal) => { if (extension.origin !== "package") throw new Error("Only package extensions can be updated."); await updatePackageSource(ctx.cwd, extension.source, undefined, signal); clearExtensionsUpdateCache(ctx.cwd); }, onUpdateAll: async (signal) => { await updateAllPackageSources(ctx.cwd, undefined, signal); clearExtensionsUpdateCache(ctx.cwd); }, }); }, }); pi.on("session_start", async (_event, ctx) => { try { await ensureRegistry(ctx.cwd); void warmExtensionsUpdateCache(ctx.cwd).then(async (changed) => { if (!changed || currentCwd !== ctx.cwd) return; registry = await loadExtensionsRegistry(ctx.cwd); }); } catch (error) { registry = EMPTY_REGISTRY; console.error("extensions-menu: failed to load registry on session start", error); } }); }