UNPKG

donobu

Version:

Create browser automations with an LLM agent and replay them as Playwright scripts.

349 lines 15.4 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.PluginLoader = void 0; const promises_1 = __importDefault(require("node:fs/promises")); const node_path_1 = __importDefault(require("node:path")); const node_url_1 = require("node:url"); const playwright = __importStar(require("playwright")); const Logger_1 = require("../utils/Logger"); const MiscUtils_1 = require("../utils/MiscUtils"); async function getDefaultDependencies() { let sharpModule; try { sharpModule = (await import('sharp')).default; } catch { // sharp is an optional peer dependency — not required unless a plugin needs it. } return { donobu: await import('../main.js'), playwright: playwright, sharp: sharpModule, }; } /** * Walk up from this file's directory to find the nearest `node_modules` * ancestor, then return the `@donobu` scope path within it. * * When installed via npm, this file lives at * `<project>/node_modules/donobu/dist/managers/PluginLoader.js`, so * walking up finds `<project>/node_modules/@donobu/`. * * Returns `undefined` when not inside a `node_modules` tree (e.g. during * development in the monorepo). */ function findNodeModulesDonobuScope() { let dir = __dirname; while (dir !== node_path_1.default.dirname(dir)) { if (node_path_1.default.basename(dir) === 'node_modules') { return node_path_1.default.join(dir, '@donobu'); } dir = node_path_1.default.dirname(dir); } return undefined; } /** * Resolve the file PluginLoader should dynamic-import for a plugin at * `pkgDir`, given its (possibly undefined) `package.json` `exports` * field. Three-tier cascade — see PluginLoader.resolvePlugin for the * semantic rationale for each tier. */ function resolvePluginEntryPath(pkgDir, exportsField) { if (exportsField && typeof exportsField === 'object') { const exp = exportsField; // 1. Subpath export: exports["./plugin"].import const subpath = exp['./plugin']; if (subpath && typeof subpath === 'object') { const subpathImport = subpath.import; if (typeof subpathImport === 'string') { return node_path_1.default.resolve(pkgDir, subpathImport); } } // 2. Flat export: exports.import const flatImport = exp.import; if (typeof flatImport === 'string') { return node_path_1.default.resolve(pkgDir, flatImport); } } // 3. Convention fallback. return node_path_1.default.join(pkgDir, 'index.mjs'); } /** * Loads Donobu plugins from two sources: * * 1. **App-data plugins directory** — the `plugins/` folder inside the * Donobu Studio data directory (e.g. `~/Library/Application Support/ * Donobu Studio/plugins/`). Plugins here are standalone `index.mjs` * files, typically installed via `install-donobu-plugin`. * * 2. **`node_modules`** — `@donobu/*` packages installed via npm that * declare `"donobu-plugin": true` in their `package.json`. This is * the primary discovery path for SDK users who `npm install` plugins * alongside the `donobu` package. * * When a plugin is found in both sources, the node_modules copy wins — * an explicit project-level dependency should override global Studio * state so tests run against the version the project declares. */ class PluginLoader { constructor(pluginsPath, pluginDependencies, nodeModulesScopePath) { this.pluginsPath = pluginsPath; this.pluginDependencies = pluginDependencies; this.nodeModulesScopePath = nodeModulesScopePath; } /** * Creates a PluginLoader with optional configuration parameters. * If parameters are not provided, defaults will be used. */ static async create(pluginsPath, pluginDependencies) { const finalPluginsPath = pluginsPath ?? node_path_1.default.join(MiscUtils_1.MiscUtils.baseWorkingDirectory(), 'plugins'); const finalDependencies = pluginDependencies ?? (await getDefaultDependencies()); return new PluginLoader(finalPluginsPath, finalDependencies, findNodeModulesDonobuScope()); } /** * Discovers and loads all plugins from both the app-data directory and * `node_modules`. All plugin types are collected and returned. */ async loadAllPlugins() { const dirPlugins = await this.discoverDirectoryPlugins(); const nmPlugins = await this.discoverNodeModulesPlugins(); // Deduplicate: node_modules plugins take precedence over app-data. // A project that explicitly depends on a plugin version (via the // install CLI's `--into ./node_modules` or a regular npm install) // should get that version, not whatever global Studio state // happens to carry. App-data is the fallback for plugins the project // hasn't declared. const seen = new Set(nmPlugins.map((p) => p.name)); const allPlugins = [...nmPlugins]; for (const p of dirPlugins) { if (!seen.has(p.name)) { allPlugins.push(p); seen.add(p.name); } } const tools = []; const persistencePlugins = new Map(); const gptClientPlugins = []; const targetRuntimePlugins = []; for (const plugin of allPlugins) { Logger_1.appLogger.info(`↳ Loading plugin "${plugin.name}" from ${plugin.entryPoint}`); try { const mod = await import((0, node_url_1.pathToFileURL)(plugin.entryPoint).href); if (!mod) { continue; } // Tool plugins if (typeof mod.loadCustomTools === 'function') { const pluginTools = await mod.loadCustomTools(this.pluginDependencies); tools.push(...pluginTools); Logger_1.appLogger.info(` ✔ Loaded ${pluginTools.length} tool(s) from plugin "${plugin.name}".`); pluginTools.forEach((t) => { Logger_1.appLogger.info(` ↳ Loaded tool "${t.name}"`); }); } // Persistence plugins if (typeof mod.loadPersistencePlugin === 'function') { const result = await mod.loadPersistencePlugin(this.pluginDependencies); if (result) { persistencePlugins.set(result.key, result.plugin); Logger_1.appLogger.info(` ✔ Loaded persistence plugin "${result.key}" from "${plugin.name}".`); } } // GPT client plugins if (typeof mod.loadGptClientPlugins === 'function') { const gptPlugins = await mod.loadGptClientPlugins(this.pluginDependencies); for (const gptPlugin of gptPlugins) { gptClientPlugins.push(gptPlugin); Logger_1.appLogger.info(` ✔ Loaded GPT client plugin "${gptPlugin.type}" from "${plugin.name}".`); } } // Target runtime plugins if (typeof mod.loadTargetRuntimePlugins === 'function') { const targetPlugins = await mod .loadTargetRuntimePlugins(this.pluginDependencies); for (const targetPlugin of targetPlugins) { targetRuntimePlugins.push(targetPlugin); Logger_1.appLogger.info(` ✔ Loaded target runtime plugin "${targetPlugin.type}" from "${plugin.name}".`); } } } catch (err) { Logger_1.appLogger.error(`Plugin "${plugin.name}" - failed to load entry point "${node_path_1.default.basename(plugin.entryPoint)}":`, err); /* swallow – other plugins should still load */ } } Logger_1.appLogger.info(`Loaded ${tools.length} custom tool(s) from plugins.`); return { tools, persistencePlugins, gptClientPlugins, targetRuntimePlugins, }; } // --------------------------------------------------------------------------- // Plugin resolution // --------------------------------------------------------------------------- /** * Determine which file PluginLoader should dynamic-import for a plugin * living at `pkgDir`, using a three-tier cascade: * * 1. `exports["./plugin"].import` — preferred for dual-use packages * (those that also expose an SDK surface on the default entry). * Points at a bundle built from plugin hooks only, with no * value-level `donobu` imports, so it loads cleanly even from the * app-data plugins dir where no sibling `donobu` module exists. * Example: `@donobu/donobu-mobile`. * 2. `exports.import` — the old flat shape used by plugin-only * packages (no SDK surface, no value-level `donobu` imports). * Examples: `tools-captcha`, `tools-mfa`, `donobu-aws`, * `donobu-google`. * 3. `<pkgDir>/index.mjs` — convention fallback for hand-dropped * plugins that ship no `package.json` at all. * * `requirePackageJson` toggles whether the caller demands a * package.json with `"donobu-plugin": true` at the package root * (node_modules discovery) or is happy with any directory that has an * `index.mjs` (app-data discovery — supports hand-dropped legacy * plugins). */ async resolvePlugin(pkgDir, requirePackageJson) { let pkgJson = null; try { const raw = await promises_1.default.readFile(node_path_1.default.join(pkgDir, 'package.json'), 'utf-8'); pkgJson = JSON.parse(raw); } catch { if (requirePackageJson) { return null; } // No package.json is fine for the app-data path; the dir is // treated as a bare `index.mjs` plugin drop (legacy pattern). } if (requirePackageJson && !pkgJson?.['donobu-plugin']) { return null; } const entryPoint = resolvePluginEntryPath(pkgDir, pkgJson?.exports); const name = pkgJson?.name ?? node_path_1.default.basename(pkgDir); return { name, entryPoint }; } // --------------------------------------------------------------------------- // Discovery: app-data plugins directory // --------------------------------------------------------------------------- /** * Scan the app-data plugins directory for standalone plugin bundles. * Each plugin is a directory containing a `package.json` with * `"donobu-plugin": true`. Scoped packages (`@scope/name`) are supported * one level deep. */ async discoverDirectoryPlugins() { Logger_1.appLogger.info(`Scanning "${this.pluginsPath}" for plugins...`); let dirEntries; try { dirEntries = await promises_1.default.readdir(this.pluginsPath, { withFileTypes: true }); } catch { dirEntries = []; } const plugins = []; for (const entry of dirEntries.filter((d) => d.isDirectory())) { if (entry.name.startsWith('@')) { try { const scopeDir = node_path_1.default.join(this.pluginsPath, entry.name); const scopeEntries = await promises_1.default.readdir(scopeDir, { withFileTypes: true, }); for (const sub of scopeEntries.filter((d) => d.isDirectory())) { const resolved = await this.resolvePlugin(node_path_1.default.join(scopeDir, sub.name), false); if (resolved) { plugins.push(resolved); } } } catch { /* ignore unreadable scope dirs */ } } else { const resolved = await this.resolvePlugin(node_path_1.default.join(this.pluginsPath, entry.name), false); if (resolved) { plugins.push(resolved); } } } return plugins; } // --------------------------------------------------------------------------- // Discovery: node_modules/@donobu/* // --------------------------------------------------------------------------- /** * Scan `node_modules/@donobu/` for npm-installed packages that declare * `"donobu-plugin": true` in their `package.json`. Delegates the actual * entry-point resolution to `resolvePlugin` so app-data and * node_modules installs follow the same rules. */ async discoverNodeModulesPlugins() { if (!this.nodeModulesScopePath) { return []; } let entries; try { entries = await promises_1.default.readdir(this.nodeModulesScopePath, { withFileTypes: true, }); } catch { return []; } const plugins = []; for (const entry of entries.filter((d) => d.isDirectory())) { const pkgDir = node_path_1.default.join(this.nodeModulesScopePath, entry.name); const resolved = await this.resolvePlugin(pkgDir, true); if (resolved) { plugins.push(resolved); Logger_1.appLogger.info(`Found npm plugin "${resolved.name}" at ${pkgDir}`); } } return plugins; } } exports.PluginLoader = PluginLoader; //# sourceMappingURL=PluginLoader.js.map