From 39396d45346fc1d808cd041e181eb2e7b4e71fca Mon Sep 17 00:00:00 2001 From: Dhruv Sharma Date: Tue, 31 Mar 2026 04:02:25 +0530 Subject: [PATCH] add cli error formatter and plugin store --- packages/cli/src/lib/cli-errors.ts | 34 ++++++ packages/cli/src/lib/plugin-store.ts | 158 +++++++++++++++++++++++++++ 2 files changed, 192 insertions(+) create mode 100644 packages/cli/src/lib/cli-errors.ts create mode 100644 packages/cli/src/lib/plugin-store.ts diff --git a/packages/cli/src/lib/cli-errors.ts b/packages/cli/src/lib/cli-errors.ts new file mode 100644 index 000000000..40cddb406 --- /dev/null +++ b/packages/cli/src/lib/cli-errors.ts @@ -0,0 +1,34 @@ +export interface CommandErrorOptions { + cmd: string; + args?: string[]; + action?: string; + installHints?: string[]; +} + +function formatHints(hints: string[] | undefined): string { + if (!hints || hints.length === 0) return ""; + return `\nInstall hint${hints.length === 1 ? "" : "s"}:\n ${hints.join("\n ")}`; +} + +export function formatCommandError(err: unknown, options: CommandErrorOptions): Error { + const command = [options.cmd, ...(options.args ?? [])].join(" ").trim(); + const action = options.action ?? "run the command"; + const code = + err && typeof err === "object" && "code" in err ? String((err as { code?: unknown }).code) : undefined; + const message = err instanceof Error ? err.message : String(err); + + if (code === "ENOENT") { + return new Error( + `${options.cmd} is not installed or not on PATH, so AO could not ${action}.${formatHints(options.installHints)}`, + ); + } + + if (code === "EACCES") { + return new Error( + `${options.cmd} exists but AO could not execute it due to a permission error while trying to ${action}. ` + + `Check that the binary is executable and accessible to this user.${formatHints(options.installHints)}`, + ); + } + + return new Error(`Failed to ${action}: ${command}${message ? `\n${message}` : ""}`); +} diff --git a/packages/cli/src/lib/plugin-store.ts b/packages/cli/src/lib/plugin-store.ts new file mode 100644 index 000000000..6b7684cb1 --- /dev/null +++ b/packages/cli/src/lib/plugin-store.ts @@ -0,0 +1,158 @@ +import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; +import { createRequire } from "node:module"; +import { homedir } from "node:os"; +import { join } from "node:path"; +import { pathToFileURL } from "node:url"; +import { exec } from "./shell.js"; +import { formatCommandError } from "./cli-errors.js"; + +interface PluginStoreManifest { + name: string; + private: boolean; + type: "module"; +} + +const STORE_MANIFEST: PluginStoreManifest = { + name: "ao-plugin-store", + private: true, + type: "module", +}; + +function getPluginStorePackageJsonPath(): string { + return join(getPluginStoreRoot(), "package.json"); +} + +function getInstalledPackageJsonPath(packageName: string): string { + return join(getPluginStoreRoot(), "node_modules", ...packageName.split("/"), "package.json"); +} + +function getStoreRequire(): NodeRequire { + const packageJsonPath = getPluginStorePackageJsonPath(); + ensurePluginStore(); + return createRequire(packageJsonPath); +} + +function isPackageSpecifier(specifier: string): boolean { + return !( + specifier.startsWith("file:") || + specifier.startsWith("node:") || + specifier.startsWith("/") || + specifier.startsWith("./") || + specifier.startsWith("../") + ); +} + +async function runNpmInStore(args: string[]): Promise { + const storeRoot = ensurePluginStore(); + try { + await exec("npm", args, { cwd: storeRoot }); + } catch (err) { + throw formatCommandError(err, { + cmd: "npm", + args, + action: "manage AO marketplace plugins", + installHints: ["Install Node.js/npm from https://nodejs.org/ and re-run the command."], + }); + } +} + +export function getPluginStoreRoot(): string { + return join(homedir(), ".agent-orchestrator", "plugins"); +} + +export function ensurePluginStore(): string { + const rootDir = getPluginStoreRoot(); + mkdirSync(rootDir, { recursive: true }); + + const packageJsonPath = getPluginStorePackageJsonPath(); + if (!existsSync(packageJsonPath)) { + writeFileSync(packageJsonPath, `${JSON.stringify(STORE_MANIFEST, null, 2)}\n`, "utf-8"); + } + + return rootDir; +} + +export function readInstalledPackageVersion(packageName: string): string | null { + const packageJsonPath = getInstalledPackageJsonPath(packageName); + if (!existsSync(packageJsonPath)) return null; + + try { + const parsed = JSON.parse(readFileSync(packageJsonPath, "utf-8")) as { version?: unknown }; + return typeof parsed.version === "string" && parsed.version.length > 0 ? parsed.version : null; + } catch { + return null; + } +} + +export async function installPackageIntoStore( + packageName: string, + version?: string, +): Promise { + const requested = version ? `${packageName}@${version}` : packageName; + await runNpmInStore(["install", "--save-exact", requested]); + + const installedVersion = readInstalledPackageVersion(packageName); + if (!installedVersion) { + throw new Error(`Package ${packageName} was installed into the AO plugin store but no version was resolved afterwards.`); + } + + return installedVersion; +} + +export async function uninstallPackageFromStore(packageName: string): Promise { + if (!readInstalledPackageVersion(packageName)) { + return false; + } + + await runNpmInStore(["uninstall", packageName]); + return readInstalledPackageVersion(packageName) === null; +} + +export function tryResolveInstalledPluginSpecifier(packageName: string): string | null { + try { + const resolvedPath = getStoreRequire().resolve(packageName); + return pathToFileURL(resolvedPath).href; + } catch { + return null; + } +} + +export function resolveInstalledPluginSpecifier(packageName: string): string { + const specifier = tryResolveInstalledPluginSpecifier(packageName); + if (!specifier) { + throw new Error( + `Package ${packageName} is not installed in the AO plugin store (${getPluginStoreRoot()}).`, + ); + } + return specifier; +} + +export async function importPluginModuleFromSource(specifier: string): Promise { + if (isPackageSpecifier(specifier)) { + const storeSpecifier = tryResolveInstalledPluginSpecifier(specifier); + if (storeSpecifier) { + return import(storeSpecifier); + } + } + + return import(specifier); +} + +export async function getLatestPublishedPackageVersion(packageName: string): Promise { + try { + const { stdout } = await exec("npm", ["view", packageName, "version", "--json"]); + const parsed = JSON.parse(stdout) as unknown; + if (typeof parsed === "string" && parsed.length > 0) { + return parsed; + } + } catch (err) { + throw formatCommandError(err, { + cmd: "npm", + args: ["view", packageName, "version", "--json"], + action: `resolve the latest published version for ${packageName}`, + installHints: ["Install Node.js/npm from https://nodejs.org/ and re-run the command."], + }); + } + + throw new Error(`npm did not return a usable version for ${packageName}.`); +}