/** * Plugin CLI command handlers. * * Handles `omp plugin ` subcommands for plugin lifecycle management. */ import { APP_NAME, getProjectDir } from "@oh-my-pi/pi-utils"; import chalk from "chalk"; import { resolveOrDefaultProjectRegistryPath } from "../discovery/helpers"; import { PluginManager, parseSettingValue, validateSetting } from "../extensibility/plugins"; import { getInstalledPluginsRegistryPath, getMarketplacesCacheDir, getMarketplacesRegistryPath, getPluginsCacheDir, MarketplaceManager, } from "../extensibility/plugins/marketplace/index.js"; import { theme } from "../modes/theme/theme"; // ============================================================================= // Types // ============================================================================= export type PluginAction = | "install" | "uninstall" | "list" | "link" | "doctor" | "features" | "config" | "enable" | "disable" | "marketplace" | "discover" | "upgrade"; export interface PluginCommandArgs { action: PluginAction; args: string[]; flags: { json?: boolean; fix?: boolean; force?: boolean; dryRun?: boolean; local?: boolean; enable?: string; disable?: string; set?: string; scope?: "user" | "project"; }; } // ============================================================================= // Argument Parser // ============================================================================= const VALID_ACTIONS: PluginAction[] = [ "install", "uninstall", "list", "link", "doctor", "features", "config", "enable", "disable", "marketplace", "discover", "upgrade", ]; /** * Parse plugin subcommand arguments. * Returns undefined if not a plugin command. */ export function parsePluginArgs(args: string[]): PluginCommandArgs | undefined { if (args.length === 0 || args[0] !== "plugin") { return undefined; } if (args.length < 2) { return { action: "list", args: [], flags: {} }; } const action = args[1]; if (!VALID_ACTIONS.includes(action as PluginAction)) { console.error(chalk.red(`Unknown plugin command: ${action}`)); console.error(`Valid commands: ${VALID_ACTIONS.join(", ")}`); process.exit(1); } const result: PluginCommandArgs = { action: action as PluginAction, args: [], flags: {}, }; // Parse remaining arguments for (let i = 2; i < args.length; i++) { const arg = args[i]; if (arg === "--json") { result.flags.json = true; } else if (arg === "--fix") { result.flags.fix = true; } else if (arg === "--force") { result.flags.force = true; } else if (arg === "--dry-run") { result.flags.dryRun = true; } else if (arg === "-l" || arg === "--local") { result.flags.local = true; } else if (arg === "--enable" && i + 1 < args.length) { result.flags.enable = args[++i]; } else if (arg === "--disable" && i + 1 < args.length) { result.flags.disable = args[++i]; } else if (arg === "--set" && i + 1 < args.length) { result.flags.set = args[++i]; } else if (arg === "--scope" && i + 1 < args.length && !args[i + 1].startsWith("-")) { const s = args[++i]; if (s === "user" || s === "project") { result.flags.scope = s; } else { console.error(chalk.red(`Invalid --scope value: "${s}". Must be "user" or "project".`)); process.exit(1); } } else if (arg === "--scope") { // --scope with no value following console.error(chalk.red(`--scope requires a value: "user" or "project".`)); process.exit(1); } else if (!arg.startsWith("-")) { result.args.push(arg); } } return result; } import { classifyInstallTarget } from "./classify-install-target"; export { classifyInstallTarget } from "./classify-install-target"; // ============================================================================= // Command Handlers // ============================================================================= /** * Run a plugin command. */ export async function runPluginCommand(cmd: PluginCommandArgs): Promise { const manager = new PluginManager(); switch (cmd.action) { case "install": await handleInstall(manager, cmd.args, cmd.flags); break; case "uninstall": await handleUninstall(manager, cmd.args, cmd.flags); break; case "list": await handleList(manager, cmd.flags); break; case "link": await handleLink(manager, cmd.args, cmd.flags); break; case "doctor": await handleDoctor(manager, cmd.flags); break; case "features": await handleFeatures(manager, cmd.args, cmd.flags); break; case "config": await handleConfig(manager, cmd.args, cmd.flags); break; case "enable": await handleEnable(manager, cmd.args, cmd.flags); break; case "disable": await handleDisable(manager, cmd.args, cmd.flags); break; case "marketplace": await handleMarketplace(cmd.args, cmd.flags); break; case "discover": await handleDiscover(cmd.args, cmd.flags); break; case "upgrade": await handleUpgrade(cmd.args, cmd.flags); break; } } // ============================================================================= // Marketplace Handlers // ============================================================================= async function makeMarketplaceManager(): Promise { return new MarketplaceManager({ marketplacesRegistryPath: getMarketplacesRegistryPath(), installedRegistryPath: getInstalledPluginsRegistryPath(), projectInstalledRegistryPath: await resolveOrDefaultProjectRegistryPath(getProjectDir()), marketplacesCacheDir: getMarketplacesCacheDir(), pluginsCacheDir: getPluginsCacheDir(), }); } async function handleMarketplace(args: string[], _flags: PluginCommandArgs["flags"]): Promise { const subcommand = args[0] ?? "list"; const manager = await makeMarketplaceManager(); switch (subcommand) { case "add": { const source = args[1]; if (!source) { console.error(chalk.red(`Usage: ${APP_NAME} plugin marketplace add `)); process.exit(1); } try { await manager.addMarketplace(source); console.log(chalk.green(`${theme.status.success} Added marketplace: ${source}`)); } catch (err) { console.error(chalk.red(`${theme.status.error} Failed to add marketplace: ${err}`)); process.exit(1); } break; } case "remove": case "rm": { const name = args[1]; if (!name) { console.error(chalk.red(`Usage: ${APP_NAME} plugin marketplace remove `)); process.exit(1); } try { await manager.removeMarketplace(name); console.log(chalk.green(`${theme.status.success} Removed marketplace: ${name}`)); } catch (err) { console.error(chalk.red(`${theme.status.error} Failed to remove marketplace: ${err}`)); process.exit(1); } break; } case "update": { try { const name = args[1]; if (name) { await manager.updateMarketplace(name); console.log(chalk.green(`${theme.status.success} Updated marketplace: ${name}`)); } else { const results = await manager.updateAllMarketplaces(); console.log(chalk.green(`${theme.status.success} Updated ${results.length} marketplace(s)`)); } } catch (err) { console.error(chalk.red(`${theme.status.error} Failed to update marketplace: ${err}`)); process.exit(1); } break; } default: { if (subcommand !== "list") { console.error(chalk.red(`Unknown marketplace subcommand: ${subcommand}`)); console.error(chalk.dim("Valid subcommands: add, remove, update, list")); process.exit(1); } try { const marketplaces = await manager.listMarketplaces(); if (marketplaces.length === 0) { console.log(chalk.dim("No marketplaces configured")); console.log(chalk.dim(`\nAdd one with: ${APP_NAME} plugin marketplace add `)); return; } console.log(chalk.bold("Configured Marketplaces:\n")); for (const mp of marketplaces) { console.log(` ${chalk.cyan(mp.name)} ${chalk.dim(mp.sourceUri)}`); } } catch (err) { console.error(chalk.red(`${theme.status.error} Failed to list marketplaces: ${err}`)); process.exit(1); } break; } } } async function handleDiscover(args: string[], _flags: PluginCommandArgs["flags"]): Promise { const marketplace = args[0]; const manager = await makeMarketplaceManager(); try { const plugins = await manager.listAvailablePlugins(marketplace); if (plugins.length === 0) { console.log(chalk.dim(marketplace ? `No plugins found in ${marketplace}` : "No plugins available")); return; } console.log(chalk.bold(`Available Plugins${marketplace ? ` (${marketplace})` : ""}:\n`)); for (const plugin of plugins) { console.log(` ${chalk.cyan(plugin.name)}${plugin.version ? `@${plugin.version}` : ""}`); if (plugin.description) { console.log(chalk.dim(` ${plugin.description}`)); } } } catch (err) { console.error(chalk.red(`${theme.status.error} Failed to discover plugins: ${err}`)); process.exit(1); } } async function handleUpgrade(args: string[], flags: PluginCommandArgs["flags"]): Promise { const manager = await makeMarketplaceManager(); const pluginId = args[0]; try { if (pluginId) { if (flags.scope) { const result = await manager.upgradePlugin(pluginId, flags.scope); console.log(chalk.green(`Upgraded ${pluginId} (${flags.scope}) to ${result.version}`)); } else { const entries = await manager.upgradePluginAcrossScopes(pluginId); for (const entry of entries) { console.log(chalk.green(`Upgraded ${pluginId} (${entry.scope}) to ${entry.version}`)); } } } else { if (flags.scope) { console.error( chalk.yellow( `Warning: --scope is ignored when upgrading all plugins. Use 'omp plugin upgrade --scope ${flags.scope}' to target a specific plugin and scope.`, ), ); } const results = await manager.upgradeAllPlugins(); if (results.length === 0) { console.log("All marketplace plugins are up to date."); } else { for (const r of results) { console.log(chalk.green(` ${r.pluginId} (${r.scope}): ${r.from} -> ${r.to}`)); } } } } catch (err) { console.error(chalk.red(`Failed to upgrade: ${err}`)); process.exit(1); } } async function handleInstall( manager: PluginManager, packages: string[], flags: { json?: boolean; force?: boolean; dryRun?: boolean; scope?: "user" | "project" }, ): Promise { if (packages.length === 0) { console.error(chalk.red(`Usage: ${APP_NAME} plugin install [features] ...`)); console.error(chalk.dim("Examples:")); console.error(chalk.dim(` ${APP_NAME} plugin install @oh-my-pi/exa`)); console.error(chalk.dim(` ${APP_NAME} plugin install name@marketplace`)); process.exit(1); } // Build known marketplace set for classification const mktMgr = await makeMarketplaceManager(); const knownMarketplaces = new Set((await mktMgr.listMarketplaces()).map(m => m.name)); for (const spec of packages) { const target = classifyInstallTarget(spec, knownMarketplaces); if (target.type === "marketplace") { try { const entry = await mktMgr.installPlugin(target.name, target.marketplace, { force: flags.force, scope: flags.scope, }); console.log( chalk.green( `${theme.status.success} Installed ${target.name} from ${target.marketplace} (${entry.version})`, ), ); } catch (err) { console.error(chalk.red(`${theme.status.error} Failed to install ${spec}: ${err}`)); process.exit(1); } continue; } // --scope only applies to marketplace installs; warn when it would be silently no-op'd for npm. if (flags.scope) { console.error( chalk.yellow( `Warning: --scope is only supported for marketplace installs (name@marketplace). Ignoring for ${spec}.`, ), ); } // npm path try { const result = await manager.install(spec, { force: flags.force, dryRun: flags.dryRun }); if (flags.json) { console.log(JSON.stringify(result, null, 2)); } else { if (flags.dryRun) { console.log(chalk.dim(`[dry-run] Would install ${spec}`)); } else { console.log(chalk.green(`${theme.status.success} Installed ${result.name}@${result.version}`)); if (result.enabledFeatures && result.enabledFeatures.length > 0) { console.log(chalk.dim(` Features: ${result.enabledFeatures.join(", ")}`)); } if (result.manifest.description) { console.log(chalk.dim(` ${result.manifest.description}`)); } } } } catch (err) { console.error(chalk.red(`${theme.status.error} Failed to install ${spec}: ${err}`)); process.exit(1); } } } async function handleUninstall( manager: PluginManager, packages: string[], flags: { json?: boolean; scope?: "user" | "project" }, ): Promise { if (packages.length === 0) { console.error(chalk.red(`Usage: ${APP_NAME} plugin uninstall ...`)); process.exit(1); } // For uninstall, check the installed plugins registry directly. // This works even if the marketplace entry was later removed from marketplaces.json. const mktMgr = await makeMarketplaceManager(); const installedPlugins = new Set((await mktMgr.listInstalledPlugins()).map(p => p.id)); for (const name of packages) { if (installedPlugins.has(name)) { // Exact match against installed marketplace plugin IDs (name@marketplace) try { await mktMgr.uninstallPlugin(name, flags.scope); console.log(chalk.green(`${theme.status.success} Uninstalled ${name}`)); } catch (err) { console.error(chalk.red(`${theme.status.error} Failed to uninstall ${name}: ${err}`)); process.exit(1); } continue; } // npm path try { await manager.uninstall(name); if (flags.json) { console.log(JSON.stringify({ uninstalled: name })); } else { console.log(chalk.green(`${theme.status.success} Uninstalled ${name}`)); } } catch (err) { console.error(chalk.red(`${theme.status.error} Failed to uninstall ${name}: ${err}`)); process.exit(1); } } } async function handleList(manager: PluginManager, flags: { json?: boolean }): Promise { const npmPlugins = await manager.list(); const mktMgr = await makeMarketplaceManager(); const mktPlugins = await mktMgr.listInstalledPlugins(); if (flags.json) { console.log(JSON.stringify({ npm: npmPlugins, marketplace: mktPlugins }, null, 2)); return; } if (npmPlugins.length === 0 && mktPlugins.length === 0) { console.log(chalk.dim("No plugins installed")); console.log(chalk.dim(`\nInstall plugins with: ${APP_NAME} plugin install `)); return; } if (npmPlugins.length > 0) { console.log(chalk.bold("npm Plugins:\n")); for (const plugin of npmPlugins) { const status = plugin.enabled ? chalk.green(theme.status.enabled) : chalk.dim(theme.status.disabled); const nameVersion = `${plugin.name}@${plugin.version}`; console.log(`${status} ${nameVersion}`); if (plugin.manifest.description) { console.log(chalk.dim(` ${plugin.manifest.description}`)); } if (plugin.enabledFeatures && plugin.enabledFeatures.length > 0) { console.log(chalk.dim(` Features: ${plugin.enabledFeatures.join(", ")}`)); } if (plugin.manifest.features) { const availableFeatures = Object.keys(plugin.manifest.features); if (availableFeatures.length > 0) { const enabledSet = new Set(plugin.enabledFeatures ?? []); const featureDisplay = availableFeatures .map(f => (enabledSet.has(f) ? chalk.green(f) : chalk.dim(f))) .join(", "); console.log(chalk.dim(` Available: [${featureDisplay}]`)); } } } } if (mktPlugins.length > 0) { if (npmPlugins.length > 0) console.log(); console.log(chalk.bold("Marketplace Plugins:\n")); for (const plugin of mktPlugins) { const entry = plugin.entries[0]; const version = entry?.version ?? "unknown"; const shadowLabel = plugin.shadowedBy ? chalk.dim(" [shadowed]") : ""; const scopeLabel = chalk.dim(` (${plugin.scope})`); console.log(` ${plugin.id} (${version})${scopeLabel}${shadowLabel}`); } } } async function handleLink(manager: PluginManager, paths: string[], flags: { json?: boolean }): Promise { if (paths.length === 0) { console.error(chalk.red(`Usage: ${APP_NAME} plugin link `)); process.exit(1); } try { const result = await manager.link(paths[0]); if (flags.json) { console.log(JSON.stringify(result, null, 2)); } else { console.log(chalk.green(`${theme.status.success} Linked ${result.name} from ${paths[0]}`)); } } catch (err) { console.error(chalk.red(`${theme.status.error} Failed to link: ${err}`)); process.exit(1); } } async function handleDoctor(manager: PluginManager, flags: { json?: boolean; fix?: boolean }): Promise { const checks = await manager.doctor({ fix: flags.fix }); if (flags.json) { console.log(JSON.stringify(checks, null, 2)); return; } console.log(chalk.bold("Plugin Health Check\n")); for (const check of checks) { const icon = check.status === "ok" ? chalk.green(theme.status.success) : check.status === "warning" ? chalk.yellow(theme.status.warning) : chalk.red(theme.status.error); console.log(`${icon} ${check.name}: ${check.message}`); if (check.fixed) { console.log(chalk.dim(` ${theme.nav.cursor} Fixed`)); } } const errors = checks.filter(c => c.status === "error" && !c.fixed).length; const warnings = checks.filter(c => c.status === "warning" && !c.fixed).length; const ok = checks.filter(c => c.status === "ok").length; const fixed = checks.filter(c => c.fixed).length; console.log(""); console.log(`Summary: ${ok} ok, ${warnings} warnings, ${errors} errors${fixed > 0 ? `, ${fixed} fixed` : ""}`); if (errors > 0) { if (!flags.fix) { console.log(chalk.dim("\nRun with --fix to attempt automatic repair")); } process.exit(1); } } async function handleFeatures( manager: PluginManager, args: string[], flags: { json?: boolean; enable?: string; disable?: string; set?: string }, ): Promise { if (args.length === 0) { console.error( chalk.red(`Usage: ${APP_NAME} plugin features [--enable f1,f2] [--disable f1] [--set f1,f2]`), ); process.exit(1); } const pluginName = args[0]; const plugins = await manager.list(); const plugin = plugins.find(p => p.name === pluginName); if (!plugin) { console.error(chalk.red(`Plugin "${pluginName}" not found`)); process.exit(1); } // Handle modifications if (flags.enable || flags.disable || flags.set) { let currentFeatures = new Set((await manager.getEnabledFeatures(pluginName)) ?? []); if (flags.set) { // --set replaces all features currentFeatures = new Set( flags.set .split(",") .map(f => f.trim()) .filter(Boolean), ); } else { if (flags.enable) { for (const f of flags.enable .split(",") .map(f => f.trim()) .filter(Boolean)) { currentFeatures.add(f); } } if (flags.disable) { for (const f of flags.disable .split(",") .map(f => f.trim()) .filter(Boolean)) { currentFeatures.delete(f); } } } await manager.setEnabledFeatures(pluginName, [...currentFeatures]); console.log(chalk.green(`${theme.status.success} Updated features for ${pluginName}`)); } // Display current state const updatedFeatures = await manager.getEnabledFeatures(pluginName); if (flags.json) { console.log( JSON.stringify( { plugin: pluginName, enabledFeatures: updatedFeatures, availableFeatures: plugin.manifest.features ? Object.keys(plugin.manifest.features) : [], }, null, 2, ), ); return; } console.log(chalk.bold(`Features for ${pluginName}:\n`)); if (!plugin.manifest.features || Object.keys(plugin.manifest.features).length === 0) { console.log(chalk.dim(" No optional features available")); return; } const enabledSet = new Set(updatedFeatures ?? []); for (const [name, feat] of Object.entries(plugin.manifest.features)) { const enabled = enabledSet.has(name); const icon = enabled ? chalk.green(theme.status.enabled) : chalk.dim(theme.status.disabled); const defaultLabel = feat.default ? chalk.dim(" (default)") : ""; console.log(`${icon} ${name}${defaultLabel}`); if (feat.description) { console.log(chalk.dim(` ${feat.description}`)); } } } async function handleConfig( manager: PluginManager, args: string[], flags: { json?: boolean; local?: boolean }, ): Promise { if (args.length === 0) { console.error( chalk.red(`Usage: ${APP_NAME} plugin config [key] [value]`), ); process.exit(1); } const [subcommand, pluginName, key, ...valueArgs] = args; // Special case: validate doesn't need a plugin name if (subcommand === "validate") { await handleConfigValidate(manager, flags); return; } if (!pluginName) { console.error(chalk.red("Plugin name required")); process.exit(1); } const plugins = await manager.list(); const plugin = plugins.find(p => p.name === pluginName); if (!plugin) { console.error(chalk.red(`Plugin "${pluginName}" not found`)); process.exit(1); } switch (subcommand) { case "list": { const settings = await manager.getPluginSettings(pluginName); const schema = plugin.manifest.settings || {}; if (flags.json) { console.log(JSON.stringify({ settings, schema }, null, 2)); return; } console.log(chalk.bold(`Settings for ${pluginName}:\n`)); if (Object.keys(schema).length === 0) { console.log(chalk.dim(" No settings defined")); return; } for (const [k, s] of Object.entries(schema)) { const value = settings[k] ?? s.default; const displayValue = s.secret && value ? "********" : String(value ?? chalk.dim("(not set)")); console.log(` ${k}: ${displayValue}`); if (s.description) { console.log(chalk.dim(` ${s.description}`)); } if (s.env) { console.log(chalk.dim(` env: ${s.env}`)); } } break; } case "get": { if (!key) { console.error(chalk.red("Key required")); process.exit(1); } const settings = await manager.getPluginSettings(pluginName); const schema = plugin.manifest.settings?.[key]; const value = settings[key] ?? schema?.default; if (flags.json) { console.log(JSON.stringify({ [key]: value })); } else { const displayValue = schema?.secret && value ? "********" : String(value ?? "(not set)"); console.log(displayValue); } break; } case "set": { if (!key) { console.error(chalk.red("Key required")); process.exit(1); } const valueStr = valueArgs.join(" "); const schema = plugin.manifest.settings?.[key]; // Parse value according to type let value: unknown = valueStr; if (schema) { value = parseSettingValue(valueStr, schema); // Validate const validation = validateSetting(value, schema); if (!validation.valid) { console.error(chalk.red(validation.error!)); process.exit(1); } } await manager.setPluginSetting(pluginName, key, value); console.log(chalk.green(`${theme.status.success} Set ${key}`)); break; } case "delete": { if (!key) { console.error(chalk.red("Key required")); process.exit(1); } await manager.deletePluginSetting(pluginName, key); console.log(chalk.green(`${theme.status.success} Deleted ${key}`)); break; } default: console.error(chalk.red(`Unknown config subcommand: ${subcommand}`)); console.error(chalk.dim("Valid subcommands: list, get, set, delete, validate")); process.exit(1); } } async function handleConfigValidate(manager: PluginManager, flags: { json?: boolean }): Promise { const plugins = await manager.list(); const results: Array<{ plugin: string; key: string; error: string }> = []; for (const plugin of plugins) { const settings = await manager.getPluginSettings(plugin.name); const schema = plugin.manifest.settings || {}; for (const [key, s] of Object.entries(schema)) { const value = settings[key]; if (value !== undefined) { const validation = validateSetting(value, s); if (!validation.valid) { results.push({ plugin: plugin.name, key, error: validation.error! }); } } } } if (flags.json) { console.log(JSON.stringify({ valid: results.length === 0, errors: results }, null, 2)); return; } if (results.length === 0) { console.log(chalk.green(`${theme.status.success} All settings valid`)); } else { for (const { plugin, key, error } of results) { console.log(chalk.red(`${theme.status.error} ${plugin}.${key}: ${error}`)); } process.exit(1); } } async function handleEnable( manager: PluginManager, plugins: string[], flags: { json?: boolean; scope?: "user" | "project" }, ): Promise { return handleSetEnabled(manager, plugins, flags, true); } async function handleDisable( manager: PluginManager, plugins: string[], flags: { json?: boolean; scope?: "user" | "project" }, ): Promise { return handleSetEnabled(manager, plugins, flags, false); } async function handleSetEnabled( manager: PluginManager, plugins: string[], flags: { json?: boolean; scope?: "user" | "project" }, enabled: boolean, ): Promise { const action = enabled ? "enable" : "disable"; const pastTense = enabled ? "Enabled" : "Disabled"; const jsonKey = enabled ? "enabled" : "disabled"; if (plugins.length === 0) { console.error(chalk.red(`Usage: ${APP_NAME} plugin ${action} ...`)); process.exit(1); } const mktMgr = await makeMarketplaceManager(); const installedPlugins = new Set((await mktMgr.listInstalledPlugins()).map(p => p.id)); for (const name of plugins) { if (installedPlugins.has(name)) { try { await mktMgr.setPluginEnabled(name, enabled, flags.scope); if (flags.json) { console.log(JSON.stringify({ [jsonKey]: name })); } else { console.log(chalk.green(`${theme.status.success} ${pastTense} ${name}`)); } } catch (err) { console.error(chalk.red(`${theme.status.error} Failed to ${action} ${name}: ${err}`)); process.exit(1); } continue; } try { await manager.setEnabled(name, enabled); if (flags.json) { console.log(JSON.stringify({ [jsonKey]: name })); } else { console.log(chalk.green(`${theme.status.success} ${pastTense} ${name}`)); } } catch (err) { console.error(chalk.red(`${theme.status.error} Failed to ${action} ${name}: ${err}`)); process.exit(1); } } } // ============================================================================= // Help // ============================================================================= export function printPluginHelp(): void { console.log(`${chalk.bold(`${APP_NAME} plugin`)} - Plugin lifecycle management ${chalk.bold("Commands:")} install [features] Install plugins from npm uninstall Remove plugins list Show installed plugins link Link local plugin for development doctor Check plugin health features View/modify enabled features config [key] [val] Manage plugin settings enable Enable a disabled plugin disable Disable plugin without uninstalling marketplace Manage marketplace sources (add, remove, update, list) discover [marketplace] Browse available marketplace plugins ${chalk.bold("Feature Syntax:")} pkg Install with default features pkg[feat1,feat2] Install with specific features pkg[*] Install with all features pkg[] Install with no optional features ${chalk.bold("Config Subcommands:")} config list List all settings config get Get a setting value config set Set a setting value config delete Delete a setting config validate Validate all plugin settings ${chalk.bold("Options:")} --json Output as JSON --fix Attempt automatic fixes (doctor) --force Overwrite without prompting (install) --scope Install scope: user (default) or project (install name@marketplace) --dry-run Preview changes without applying (install) -l, --local Use project-local overrides ${chalk.bold("Examples:")} ${APP_NAME} plugin install @oh-my-pi/exa[search] ${APP_NAME} plugin list --json ${APP_NAME} plugin features my-plugin --enable search,web ${APP_NAME} plugin config set my-plugin apiKey sk-xxx ${APP_NAME} plugin doctor --fix ${APP_NAME} plugin install --scope project name@marketplace `); }