ai_api / src /lib /plugins /scanner.ts
Yogesh
initial deploy
cd8bd0a
Raw
History Blame Contribute Delete
3.17 kB
/**
* Plugin scanner — discovers plugins from the filesystem.
*
* Scans ~/.omniroute/plugins/ for subdirectories containing plugin.json manifests.
* Returns validated manifests with directory paths.
*
* @module plugins/scanner
*/
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;
}
/**
* Get the default plugin directory: ~/.omniroute/plugins/
*/
export function getDefaultPluginDir(): string {
const home = process.env.HOME || process.env.USERPROFILE || "/tmp";
return join(home, ".omniroute", "plugins");
}
/**
* Scan a directory for plugin subdirectories containing plugin.json.
* Skips hidden directories (.xxx) and non-directories.
*/
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);
// Verify entry point exists
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 };
}