fix: auto-install dependencies for local plugins during onboard
When selecting 'Use local plugin path' during onboard, the plugin's node_modules may not exist (especially in pnpm workspace setups). This fix: - Detects the appropriate package manager (pnpm/npm) by checking for lockfiles and workspace configs - Automatically runs 'pnpm install' or 'npm install' if node_modules is missing or empty - Provides clear feedback during installation - Handles errors gracefully with informative messages This resolves the 'Cannot find module ox/Hash' error when onboarding the XMTP extension from a local development path. Fixes: XMTP extension loading failure during onboard
This commit is contained in:
parent
3b0c80ce24
commit
047070a5cf
@ -8,6 +8,7 @@ import { recordPluginInstall } from "../../plugins/installs.js";
|
||||
import { enablePluginInConfig } from "../../plugins/enable.js";
|
||||
import { loadClawdbotPlugins } from "../../plugins/loader.js";
|
||||
import { installPluginFromNpmSpec } from "../../plugins/install.js";
|
||||
import { runCommandWithTimeout } from "../../process/exec.js";
|
||||
import type { RuntimeEnv } from "../../runtime.js";
|
||||
import type { WizardPrompter } from "../../wizard/prompts.js";
|
||||
|
||||
@ -18,6 +19,101 @@ type InstallResult = {
|
||||
installed: boolean;
|
||||
};
|
||||
|
||||
type EnsureLocalDepsResult = {
|
||||
ok: boolean;
|
||||
error?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Detect which package manager to use for a plugin directory.
|
||||
* Prefers pnpm if pnpm-lock.yaml exists, then npm if package-lock.json exists,
|
||||
* otherwise defaults to pnpm (as the clawdbot monorepo uses pnpm).
|
||||
*/
|
||||
function detectPackageManager(pluginDir: string): "pnpm" | "npm" {
|
||||
if (fs.existsSync(path.join(pluginDir, "pnpm-lock.yaml"))) return "pnpm";
|
||||
if (fs.existsSync(path.join(pluginDir, "pnpm-workspace.yaml"))) return "pnpm";
|
||||
if (fs.existsSync(path.join(pluginDir, "package-lock.json"))) return "npm";
|
||||
// Check parent directories for pnpm workspace (monorepo setup)
|
||||
let cursor = path.dirname(pluginDir);
|
||||
for (let i = 0; i < 5; i++) {
|
||||
if (fs.existsSync(path.join(cursor, "pnpm-workspace.yaml"))) return "pnpm";
|
||||
const parent = path.dirname(cursor);
|
||||
if (parent === cursor) break;
|
||||
cursor = parent;
|
||||
}
|
||||
return "pnpm";
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure dependencies are installed for a local plugin.
|
||||
* This is necessary because local plugins in development may not have
|
||||
* node_modules populated, especially when using pnpm workspaces.
|
||||
*/
|
||||
async function ensureLocalPluginDependencies(params: {
|
||||
pluginDir: string;
|
||||
runtime: RuntimeEnv;
|
||||
timeoutMs?: number;
|
||||
}): Promise<EnsureLocalDepsResult> {
|
||||
const { pluginDir, runtime, timeoutMs = 120_000 } = params;
|
||||
|
||||
// Check if package.json exists
|
||||
const packageJsonPath = path.join(pluginDir, "package.json");
|
||||
if (!fs.existsSync(packageJsonPath)) {
|
||||
return { ok: true }; // No package.json, nothing to install
|
||||
}
|
||||
|
||||
// Check if dependencies are needed
|
||||
let packageJson: {
|
||||
dependencies?: Record<string, string>;
|
||||
devDependencies?: Record<string, string>;
|
||||
};
|
||||
try {
|
||||
packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
|
||||
} catch {
|
||||
return { ok: true }; // Can't parse, skip dep check
|
||||
}
|
||||
|
||||
const hasDeps =
|
||||
Object.keys(packageJson.dependencies ?? {}).length > 0 ||
|
||||
Object.keys(packageJson.devDependencies ?? {}).length > 0;
|
||||
|
||||
if (!hasDeps) {
|
||||
return { ok: true }; // No dependencies to install
|
||||
}
|
||||
|
||||
// Check if node_modules exists and has content
|
||||
const nodeModulesPath = path.join(pluginDir, "node_modules");
|
||||
const hasNodeModules =
|
||||
fs.existsSync(nodeModulesPath) && fs.readdirSync(nodeModulesPath).length > 0;
|
||||
|
||||
if (hasNodeModules) {
|
||||
return { ok: true }; // Dependencies already installed
|
||||
}
|
||||
|
||||
// Install dependencies
|
||||
const pm = detectPackageManager(pluginDir);
|
||||
runtime.log?.(`Installing dependencies for local plugin (${pm})...`);
|
||||
|
||||
const installCmd = pm === "pnpm" ? ["pnpm", "install"] : ["npm", "install", "--omit=dev"];
|
||||
|
||||
try {
|
||||
const result = await runCommandWithTimeout(installCmd, {
|
||||
timeoutMs,
|
||||
cwd: pluginDir,
|
||||
});
|
||||
|
||||
if (result.code !== 0) {
|
||||
const errorMsg = result.stderr.trim() || result.stdout.trim() || "unknown error";
|
||||
return { ok: false, error: `${pm} install failed: ${errorMsg}` };
|
||||
}
|
||||
|
||||
runtime.log?.(`Dependencies installed successfully.`);
|
||||
return { ok: true };
|
||||
} catch (err) {
|
||||
return { ok: false, error: `${pm} install failed: ${String(err)}` };
|
||||
}
|
||||
}
|
||||
|
||||
function hasGitWorkspace(workspaceDir?: string): boolean {
|
||||
const candidates = new Set<string>();
|
||||
candidates.add(path.join(process.cwd(), ".git"));
|
||||
@ -141,6 +237,17 @@ export async function ensureOnboardingPluginInstalled(params: {
|
||||
}
|
||||
|
||||
if (choice === "local" && localPath) {
|
||||
// Ensure dependencies are installed before loading the plugin
|
||||
const depsResult = await ensureLocalPluginDependencies({
|
||||
pluginDir: localPath,
|
||||
runtime,
|
||||
});
|
||||
|
||||
if (!depsResult.ok) {
|
||||
await prompter.note(`Failed to install dependencies: ${depsResult.error}`, "Plugin setup");
|
||||
return { cfg: next, installed: false };
|
||||
}
|
||||
|
||||
next = addPluginLoadPath(next, localPath);
|
||||
next = enablePluginInConfig(next, entry.id).config;
|
||||
return { cfg: next, installed: true };
|
||||
@ -177,6 +284,17 @@ export async function ensureOnboardingPluginInstalled(params: {
|
||||
initialValue: true,
|
||||
});
|
||||
if (fallback) {
|
||||
// Ensure dependencies are installed before loading the plugin
|
||||
const depsResult = await ensureLocalPluginDependencies({
|
||||
pluginDir: localPath,
|
||||
runtime,
|
||||
});
|
||||
|
||||
if (!depsResult.ok) {
|
||||
await prompter.note(`Failed to install dependencies: ${depsResult.error}`, "Plugin setup");
|
||||
return { cfg: next, installed: false };
|
||||
}
|
||||
|
||||
next = addPluginLoadPath(next, localPath);
|
||||
next = enablePluginInConfig(next, entry.id).config;
|
||||
return { cfg: next, installed: true };
|
||||
|
||||
Loading…
Reference in New Issue
Block a user