import { getAgentDir, getSettingsListTheme, type ExtensionAPI } from "@earendil-works/pi-coding-agent"; import { Container, type SettingItem, SettingsList, Text } from "@earendil-works/pi-tui"; import { getEnabledPackageSource, mergeManagedExtensions, mergePackageSelections, readPiSettings, readSelectorState, resolvePoolDir, scanExtensionPool, scanInstalledPackageExtensions, updateManagedHistory, writeJsonAtomic, } from "./core.js"; import { join } from "node:path"; const STATE_FILENAME = "extension-selector.json"; export default function extensionSelector(pi: ExtensionAPI) { pi.registerCommand("extensions", { description: "Enable or disable managed Pi extensions", getArgumentCompletions: (prefix: string) => { // Completion cannot await disk I/O, so profile names are shown after the command opens. const common = ["default"]; const filtered = common.filter((name) => name.startsWith(prefix)); return filtered.length ? filtered.map((name) => ({ value: name, label: name })) : null; }, handler: async (args, ctx) => { if (ctx.mode !== "tui") { if (ctx.hasUI) ctx.ui.notify("/extensions requires TUI mode", "error"); return; } const agentDir = getAgentDir(); const statePath = join(agentDir, STATE_FILENAME); try { const state = await readSelectorState(statePath); const requestedProfile = args.trim(); const profileName = requestedProfile || state.activeProfile; if (!state.profiles[profileName]) { const create = await ctx.ui.confirm("Create extension profile?", `Profile \"${profileName}\" does not exist.`); if (!create) return; state.profiles[profileName] = { enabled: [] }; } const settingsPath = join(agentDir, "settings.json"); const settings = await readPiSettings(settingsPath); const poolDir = resolvePoolDir(agentDir, state.poolDir); const poolRecords = await scanExtensionPool(poolDir); const packageRecords = await scanInstalledPackageExtensions(agentDir, settings); const records = [ ...packageRecords.map((record) => ({ ...record, kind: "package" as const })), ...poolRecords.map((record) => ({ ...record, kind: "pool" as const })), ]; if (records.length === 0) { ctx.ui.notify(`No installed package extensions or pool extensions found`, "warning"); return; } const enabled = new Set(state.profiles[profileName].enabled); for (const record of packageRecords) { if (!(record.source in state.packageDefaults)) { const configured = settings.packages?.find( (entry) => (typeof entry === "string" ? entry : entry.source) === record.source, ); if (configured) state.packageDefaults[record.source] = getEnabledPackageSource(configured); if (record.enabled) enabled.add(record.id); } } const before = new Set(enabled); await ctx.ui.custom((tui, theme, _keybindings, done) => { const items: SettingItem[] = records.map((record) => ({ id: record.id, label: record.kind === "package" ? `${record.label} [package]` : `${record.label} [pool]`, description: record.description ?? (record.kind === "pool" ? record.path : record.source), currentValue: enabled.has(record.id) ? "enabled" : "disabled", values: ["enabled", "disabled"], })); const container = new Container(); container.addChild( new Text( [ theme.fg("accent", theme.bold(`Extension profile: ${profileName}`)), theme.fg("dim", `${packageRecords.length} installed packages ยท ${poolRecords.length} pool extensions`), theme.fg("dim", "Toggle entries, then press Escape to save and reload."), "", ].join("\n"), 1, 0, ), ); const list = new SettingsList( items, Math.min(items.length + 2, 18), getSettingsListTheme(), (id, value) => { if (value === "enabled") enabled.add(id); else enabled.delete(id); }, () => done(undefined), { enableSearch: true }, ); container.addChild(list); return { render: (width: number) => container.render(width), invalidate: () => container.invalidate(), handleInput: (data: string) => { list.handleInput(data); tui.requestRender(); }, }; }); const changed = profileName !== state.activeProfile || enabled.size !== before.size || [...enabled].some((id) => !before.has(id)); if (!changed) { ctx.ui.notify("Extension selection unchanged", "info"); return; } state.activeProfile = profileName; state.profiles[profileName] = { enabled: records.filter((record) => enabled.has(record.id)).map((record) => record.id), }; state.managedPaths = updateManagedHistory(state, poolRecords); const enabledPaths = poolRecords.filter((record) => enabled.has(record.id)).map((record) => record.path); const withLocalExtensions = mergeManagedExtensions(settings, agentDir, state.managedPaths, enabledPaths); const nextSettings = mergePackageSelections( withLocalExtensions, packageRecords, enabled, state.packageDefaults, ); await writeJsonAtomic(statePath, state); await writeJsonAtomic(settingsPath, nextSettings); ctx.ui.notify(`Applying extension profile \"${profileName}\"`, "info"); await ctx.reload(); return; } catch (error) { const message = error instanceof Error ? error.message : String(error); ctx.ui.notify(`Extension selector: ${message}`, "error"); } }, }); }