220 lines
7.7 KiB
JavaScript
220 lines
7.7 KiB
JavaScript
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 <name>\n`);
|
|
}
|
|
});
|
|
|
|
plugin
|
|
.command("install <name>")
|
|
.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 <name>")
|
|
.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 <name>")
|
|
.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 <name>")
|
|
.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`);
|
|
});
|
|
}
|