| |
| |
| |
| |
| |
| |
| |
|
|
|
|
| import { readdir, stat, readFile } from "fs/promises";
|
| import { join } from "path";
|
| import { logger } from "../../../open-sse/utils/logger.ts";
|
| import { safeValidateManifest, type PluginManifestWithDefaults } from "./manifest";
|
|
|
| const log = logger("PLUGIN_SCANNER");
|
|
|
| export interface DiscoveredPlugin {
|
| name: string;
|
| manifest: PluginManifestWithDefaults;
|
| pluginDir: string;
|
| entryPoint: string;
|
| }
|
|
|
| |
| |
|
|
| export function getDefaultPluginDir(): string {
|
| const home = process.env.HOME || process.env.USERPROFILE || "/tmp";
|
| return join(home, ".omniroute", "plugins");
|
| }
|
|
|
| |
| |
| |
|
|
| export async function scanPluginDir(
|
| dir: string
|
| ): Promise<{ plugins: DiscoveredPlugin[]; errors: Array<{ name: string; error: string }> }> {
|
| const plugins: DiscoveredPlugin[] = [];
|
| const errors: Array<{ name: string; error: string }> = [];
|
|
|
| let entries: string[];
|
| try {
|
| const dirEntries = await readdir(dir, { withFileTypes: true });
|
| entries = dirEntries
|
| .filter((e) => e.isDirectory() && !e.name.startsWith("."))
|
| .map((e) => e.name);
|
| } catch (err: any) {
|
| if (err.code === "ENOENT") {
|
| log.info("scanner.dir_not_found", { dir });
|
| return { plugins: [], errors: [] };
|
| }
|
| throw err;
|
| }
|
|
|
| for (const entry of entries) {
|
| const pluginDir = join(dir, entry);
|
| const manifestPath = join(pluginDir, "plugin.json");
|
|
|
| try {
|
| const manifestStat = await stat(manifestPath);
|
| if (!manifestStat.isFile()) {
|
| errors.push({ name: entry, error: "plugin.json is not a file" });
|
| continue;
|
| }
|
| } catch {
|
| errors.push({ name: entry, error: "no plugin.json found" });
|
| continue;
|
| }
|
|
|
| try {
|
| const raw = await readFile(manifestPath, "utf-8");
|
| const parsed = JSON.parse(raw);
|
| const result = safeValidateManifest(parsed);
|
|
|
| if (!result.success) {
|
| const failResult = result as { success: false; errors: string[] };
|
| errors.push({ name: entry, error: `invalid manifest: ${failResult.errors.join("; ")}` });
|
| continue;
|
| }
|
|
|
| const manifest = result.data;
|
| const entryPoint = join(pluginDir, manifest.main);
|
|
|
|
|
| try {
|
| await stat(entryPoint);
|
| } catch {
|
| errors.push({
|
| name: entry,
|
| error: `entry point not found: ${manifest.main}`,
|
| });
|
| continue;
|
| }
|
|
|
| plugins.push({
|
| name: manifest.name,
|
| manifest,
|
| pluginDir,
|
| entryPoint,
|
| });
|
|
|
| log.info("scanner.discovered", { name: manifest.name, version: manifest.version });
|
| } catch (err: any) {
|
| errors.push({ name: entry, error: `failed to read manifest: ${err.message}` });
|
| }
|
| }
|
|
|
| return { plugins, errors };
|
| }
|
|
|