import { spawnSync } from "node:child_process"; import { existsSync, mkdirSync, writeFileSync } from "node:fs"; import { join } from "node:path"; import { t } from "../i18n.mjs"; import { emit } from "../output.mjs"; import { discoverPlugins } from "../plugins.mjs"; // Run npm with an explicit argument array and no shell. Passing args this way // (instead of string-interpolating into `execSync`) prevents a malicious plugin // name like `foo; rm -rf ~` or `` foo`id` `` from being interpreted by the shell. function runNpm(args) { const res = spawnSync("npm", args, { stdio: "inherit", shell: false }); if (res.error) throw res.error; if (typeof res.status === "number" && res.status !== 0) { throw new Error(`npm exited with code ${res.status}`); } } const TEMPLATE_INDEX = `export const meta = { name: "PLUGIN_NAME", version: "0.1.0", description: "OmniRoute plugin", omnirouteApi: ">=4.0.0", }; export function register(program, ctx) { program .command("PLUGIN_NAME") .description(meta.description) .action(async (opts, cmd) => { const gOpts = cmd.optsWithGlobals(); // ctx provides: ctx.apiFetch, ctx.emit, ctx.t, ctx.withSpinner, ctx.baseUrl, ctx.apiKey const res = await ctx.apiFetch("/api/health", { baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey }); const data = await res.json(); ctx.emit(data, gOpts); }); } `; export function registerPlugin(program) { const plugin = program .command("plugin") .description(t("plugin.description") || "Manage CLI plugins (omniroute-cmd-*)"); plugin .command("list") .description(t("plugin.list") || "List installed plugins") .action(async (opts, cmd) => { const plugins = await discoverPlugins(); emit( plugins.map((p) => ({ name: p.name, version: p.version, description: p.description })), cmd.optsWithGlobals() ); if (plugins.length === 0) { process.stdout.write("No plugins installed.\n"); process.stdout.write(`Install: omniroute plugin install \n`); } }); plugin .command("install ") .description(t("plugin.install") || "Install a plugin") .option("-y, --yes", "Skip confirmation prompt") .action(async (name, opts) => { const isLocal = name.startsWith("./") || name.startsWith("/") || name.startsWith("../"); const pkgName = isLocal ? name : `omniroute-cmd-${name}`; if (!opts.yes) { process.stderr.write( `⚠ WARNING: Plugins run with the same privileges as omniroute CLI.\n` + ` Only install plugins from sources you trust.\n` + ` Installing: ${pkgName}\n` + ` Pass --yes to skip this prompt.\n` ); // In non-interactive mode, require explicit --yes if (!process.stdin.isTTY) { process.stderr.write("Non-interactive mode: use --yes to confirm.\n"); process.exit(1); } } try { runNpm(["install", "-g", pkgName]); process.stdout.write(`\n✓ Installed: ${pkgName}\n`); } catch { process.stderr.write(`✗ Failed to install ${pkgName}\n`); process.exit(1); } }); plugin .command("remove ") .alias("uninstall") .description(t("plugin.remove") || "Remove a plugin") .option("-y, --yes", "Skip confirmation") .action(async (name, opts) => { const pkgName = name.startsWith("omniroute-cmd-") ? name : `omniroute-cmd-${name}`; if (!opts.yes) { process.stderr.write(`Removing: ${pkgName} — pass --yes to confirm.\n`); if (!process.stdin.isTTY) { process.exit(1); } } try { runNpm(["uninstall", "-g", pkgName]); process.stdout.write(`✓ Removed: ${pkgName}\n`); } catch { process.stderr.write(`✗ Failed to remove ${pkgName}\n`); process.exit(1); } }); plugin .command("info ") .description(t("plugin.info") || "Show plugin details") .action(async (name, opts, cmd) => { const plugins = await discoverPlugins(); const p = plugins.find((x) => x.name === name || x.name === `omniroute-cmd-${name}`); if (!p) { process.stderr.write(`Plugin '${name}' not found.\n`); process.exit(1); } emit(p.pkg, cmd.optsWithGlobals()); }); plugin .command("search [query]") .description(t("plugin.search") || "Search npm for available plugins") .action(async (query) => { const q = query ? `omniroute-cmd-${query}` : "omniroute-cmd"; const url = `https://registry.npmjs.org/-/v1/search?text=${encodeURIComponent(q)}&size=50`; try { const res = await fetch(url); if (!res.ok) throw new Error(`npm registry returned ${res.status}`); const data = await res.json(); const rows = (data.objects || []).map((o) => ({ name: o.package.name, version: o.package.version, description: o.package.description, })); if (rows.length === 0) { process.stdout.write(`No plugins found for '${query || "omniroute-cmd"}'.\n`); } else { rows.forEach((r) => process.stdout.write(` ${r.name}@${r.version} ${r.description || ""}\n`) ); } } catch (err) { process.stderr.write(`Search failed: ${err.message}\n`); process.exit(1); } }); plugin .command("update [name]") .description(t("plugin.update") || "Update installed plugin(s)") .action(async (name) => { try { if (name) { const pkg = `omniroute-cmd-${name}`; runNpm(["update", "-g", pkg]); process.stdout.write(`✓ Updated: ${pkg}\n`); return; } // No name → update every installed plugin. Enumerate them explicitly // instead of relying on a shell glob (`omniroute-cmd-*`), which never // expands without a shell and would otherwise update nothing. const plugins = await discoverPlugins(); const names = plugins .map((p) => p.name) .filter((n) => typeof n === "string" && n.startsWith("omniroute-cmd-")); if (names.length === 0) { process.stdout.write("No plugins installed to update.\n"); return; } runNpm(["update", "-g", ...names]); process.stdout.write(`✓ Updated: ${names.join(", ")}\n`); } catch { process.stderr.write(`✗ Update failed\n`); process.exit(1); } }); plugin .command("scaffold ") .description(t("plugin.scaffold") || "Scaffold a new plugin boilerplate") .action(async (name) => { const safeName = name.replace(/[^a-z0-9-]/g, "-"); const dir = join(process.cwd(), `omniroute-cmd-${safeName}`); if (existsSync(dir)) { process.stderr.write(`Directory already exists: ${dir}\n`); process.exit(1); } mkdirSync(dir, { recursive: true }); writeFileSync( join(dir, "package.json"), JSON.stringify( { name: `omniroute-cmd-${safeName}`, version: "0.1.0", type: "module", main: "index.mjs", description: `OmniRoute CLI plugin: ${safeName}`, engines: { omniroute: ">=4.0.0" }, keywords: ["omniroute-plugin", "omniroute-cmd"], }, null, 2 ) + "\n" ); writeFileSync(join(dir, "index.mjs"), TEMPLATE_INDEX.replace(/PLUGIN_NAME/g, safeName)); writeFileSync( join(dir, "README.md"), `# omniroute-cmd-${safeName}\n\nAn OmniRoute CLI plugin.\n\n## Install\n\n\`\`\`bash\nomniroute plugin install ${safeName}\n\`\`\`\n` ); process.stdout.write(`✓ Scaffolded: ${dir}\n`); process.stdout.write(` Run: cd ${dir} && omniroute plugin install .\n`); }); }