/** * Symlinks whitelisted shared dependencies from the assistant's own * `node_modules/` into `/node_modules/` so user plugins can * resolve bare imports like: * * import { z } from "zod"; * * A plugin's own declared dependencies are installed at install time (see * `../cli/lib/install-plugin-dependencies.ts`). The whitelisted deps here are * the ones a plugin may import *without* declaring — `zod` is de-facto plugin * SDK surface the config-validation idiom leans on — so their bare imports * resolve Node-style walking up from the plugin directory, where the * assistant's own copies are not on the path. This module bridges that gap by * linking the real package directories, not re-export shims: a plugin gets the * actual zod the assistant uses, subpaths and internal resolution included. * * ## Why symlinks (not generated re-export shims) * * Re-export shims re-bind each export from `globalThis`, which works for * the `@vellumai/plugin-api` surface (a small set of identifiers the * assistant controls) but is fragile for real npm packages: zod ships 238 * exports including reserved words (`enum`, `function`, `instanceof`, * `void`, `default`) that need alias-form codegen, and any export added in * a zod update silently breaks until the shim is regenerated. A symlink to * the real package has none of these problems — it IS the package. * * ## Existing real installs are respected * * If `/node_modules/` already exists (a real package, a * prior symlink, or anything else), it is left untouched. We never clobber * user-managed files from daemon boot. * * ## Compiled-binary edge case * * `bun --compile` inlines the assistant's code graph into the binary; the * `node_modules/` directory may not exist on disk. When a package can't be * resolved to a real directory, it is skipped with a log line — plugins * importing it will fail individually with a clear module-not-found error. * * ## Whitelist policy * * Only deps that are (a) already direct dependencies of the assistant, * (b) pure-JS with no native bindings or lifecycle scripts, and (c) broadly * useful to plugins belong here. Each addition widens the de-facto plugin * SDK: plugins will pin to the assistant's copy and its version, so treat * the list like public API surface. `zod` is the founding member — the * plugin config-validation idiom depends on it, and the assistant pins an * exact version. * * Called from `loadUserPlugins` alongside `ensurePluginApiShim`, before any * user plugin is dynamic-imported. Never throws — failures are logged * per-dep and the daemon must never block startup. */ import { existsSync, readFileSync } from "node:fs"; import { mkdir, symlink } from "node:fs/promises"; import { dirname, join } from "node:path"; import { getLogger } from "../util/logger.js"; import { getWorkspaceDir } from "../util/platform.js"; const log = getLogger("shared-dep-links"); /** Package names to symlink into the workspace for plugin resolution. */ const SHARED_DEPS: readonly string[] = Object.freeze(["zod"]); /** * Resolve a package to its on-disk directory by walking up from the entry * file to the nearest `package.json` with a matching `name` field. Returns * `null` when the package can't be resolved or isn't on disk (e.g. inside a * `bun --compile` binary). */ function resolvePackageDir(name: string): string | null { let entryPath: string; try { entryPath = require.resolve(name); } catch { return null; } let dir = dirname(entryPath); for (let depth = 0; depth < 8; depth++) { const pkgPath = join(dir, "package.json"); if (existsSync(pkgPath)) { try { const pkg = JSON.parse(readFileSync(pkgPath, "utf8")) as { name?: string; }; if (pkg.name === name) { return dir; } } catch { // corrupt package.json — keep walking } } const parent = dirname(dir); if (parent === dir) { break; } // reached filesystem root dir = parent; } return null; } /** * Symlink every whitelisted shared dep into `/node_modules/`. * Idempotent; per-dep failures are logged and do not abort the remaining * deps. Never throws. */ export async function ensureSharedDepLinks(): Promise { const workspaceDir = getWorkspaceDir(); const nodeModulesDir = join(workspaceDir, "node_modules"); for (const name of SHARED_DEPS) { try { const sourceDir = resolvePackageDir(name); if (!sourceDir) { log.warn( { dep: name }, "shared-dep link skipped — package not resolvable on disk (compiled binary?)", ); continue; } const linkPath = join(nodeModulesDir, ...name.split("/")); // Don't clobber anything already there — a real install, a prior // symlink, or even a stale link all qualify. The user can clear it // manually if they want a refresh. if (existsSync(linkPath)) { log.debug( { dep: name, linkPath }, "shared-dep link skipped — already exists in workspace node_modules", ); continue; } await mkdir(dirname(linkPath), { recursive: true }); await symlink(sourceDir, linkPath, "dir"); log.info( { dep: name, source: sourceDir, link: linkPath }, "shared-dep symlinked into workspace node_modules", ); } catch (err) { log.warn( { err, dep: name }, "shared-dep link failed — plugins importing it will fail individually", ); } } }