From ddf36667f9b7cfe8672738983ba01e68be27607b Mon Sep 17 00:00:00 2001 From: "Pawla Abdul (Bot)" Date: Sun, 12 Apr 2026 16:38:19 +0000 Subject: [PATCH] Auto-reinstall adapter packages missing from disk on reload and boot Adapters installed with the old --no-save flag are not tracked in package.json and get pruned when another adapter is installed. This adds ensurePackageOnDisk() which detects missing packages and reinstalls them from npm before reload or server boot attempts to import them, fixing ENOENT errors for previously-pruned adapters. Fixes FAR-47 Co-Authored-By: Paperclip --- server/src/adapters/plugin-loader.ts | 35 ++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/server/src/adapters/plugin-loader.ts b/server/src/adapters/plugin-loader.ts index a9f70463..63872979 100644 --- a/server/src/adapters/plugin-loader.ts +++ b/server/src/adapters/plugin-loader.ts @@ -9,11 +9,15 @@ * adapter-utils, never registry.ts. */ +import { execFile } from "node:child_process"; import fs from "node:fs"; import path from "node:path"; +import { promisify } from "node:util"; import type { ServerAdapterModule } from "./types.js"; import { logger } from "../middleware/logger.js"; +const execFileAsync = promisify(execFile); + import { listAdapterPlugins, getAdapterPluginsDir, @@ -187,8 +191,37 @@ export async function loadExternalAdapterPackage( return adapterModule; } +/** + * Ensure an npm-sourced adapter package exists on disk. + * If the package directory is missing (e.g. pruned by a prior --no-save install), + * reinstall it from the registry so reload/boot can proceed. + */ +async function ensurePackageOnDisk(record: AdapterPluginRecord): Promise { + if (record.localPath) return; // local-path adapters are managed externally + + const packageDir = resolvePackageDir(record); + const pkgJsonPath = path.join(packageDir, "package.json"); + if (fs.existsSync(pkgJsonPath)) return; // already on disk + + const pluginsDir = getAdapterPluginsDir(); + const spec = record.version + ? `${record.packageName}@${record.version}` + : record.packageName; + + logger.info( + { packageName: record.packageName, type: record.type, spec }, + "Adapter package missing from disk — reinstalling from npm", + ); + + await execFileAsync("npm", ["install", spec], { + cwd: pluginsDir, + timeout: 120_000, + }); +} + async function loadFromRecord(record: AdapterPluginRecord): Promise { try { + await ensurePackageOnDisk(record); return await loadExternalAdapterPackage(record.packageName, record.localPath); } catch (err) { logger.warn( @@ -209,6 +242,8 @@ export async function reloadExternalAdapter( const record = getAdapterPluginByType(type); if (!record) return null; + await ensurePackageOnDisk(record); + const packageDir = resolvePackageDir(record); const entryPoint = resolvePackageEntryPoint(packageDir); const modulePath = path.resolve(packageDir, entryPoint);