fix: explain missing git during plugin install

This commit is contained in:
Peter Steinberger
2026-05-05 05:13:59 +01:00
parent cf3ce08b91
commit a91c17c426
6 changed files with 88 additions and 4 deletions

View File

@@ -1096,6 +1096,34 @@ describe("plugins cli install", () => {
expect(runtimeErrors.at(-1)).toContain("npm install failed");
});
it("adds a Git PATH hint when npm plugin dependency install cannot spawn git", async () => {
loadConfig.mockReturnValue({} as OpenClawConfig);
installPluginFromNpmSpec.mockResolvedValue({
ok: false,
error: [
"npm install failed:",
"npm error code ENOENT",
"npm error syscall spawn git",
"npm error path git",
].join("\n"),
});
installHooksFromNpmSpec.mockResolvedValue({
ok: false,
error: "package.json missing openclaw.hooks",
});
await expect(
runPluginsCommand(["plugins", "install", "npm:@openclaw/whatsapp"]),
).rejects.toThrow("__exit__:1");
expect(installPluginFromClawHub).not.toHaveBeenCalled();
expect(runtimeErrors.at(-1)).toContain(
"one of this plugin's npm dependencies is fetched from a git URL",
);
expect(runtimeErrors.at(-1)).toContain("winget install --id Git.Git -e");
expect(runtimeErrors.at(-1)).toContain("Also not a valid hook pack");
});
it("does not resolve npm: prefixed bundled plugin ids through bundled installs", async () => {
loadConfig.mockReturnValue({ plugins: { load: { paths: [] } } } as OpenClawConfig);
installPluginFromNpmSpec.mockResolvedValue({

View File

@@ -176,16 +176,36 @@ export function formatPluginInstallWithHookFallbackError(
pluginError: string,
hookError: string,
): string {
const formattedPluginError = formatPluginInstallAttemptError(pluginError);
const formattedHookError = formatPluginInstallAttemptError(hookError);
if (/plugin already exists: .+ \(delete it first\)/.test(pluginError)) {
return `${pluginError}\nUse \`openclaw plugins update <id-or-npm-spec>\` to upgrade the tracked plugin, or rerun install with \`--force\` to replace it.`;
return `${formattedPluginError}\nUse \`openclaw plugins update <id-or-npm-spec>\` to upgrade the tracked plugin, or rerun install with \`--force\` to replace it.`;
}
if (
pluginError.startsWith("Invalid extensions directory:") ||
pluginError === "Invalid path: must stay within extensions directory"
) {
return pluginError;
return formattedPluginError;
}
return `${pluginError}\nAlso not a valid hook pack: ${hookError}`;
return `${formattedPluginError}\nAlso not a valid hook pack: ${formattedHookError}`;
}
const MISSING_GIT_FOR_NPM_DEPENDENCY_HINT =
"Git is required because one of this plugin's npm dependencies is fetched from a git URL, but `git` was not found on PATH. Install Git and rerun the install. On Windows, use `winget install --id Git.Git -e` or add a portable Git `bin` directory to PATH.";
function formatPluginInstallAttemptError(error: string): string {
if (!isMissingGitForNpmDependencyError(error)) {
return error;
}
if (error.includes(MISSING_GIT_FOR_NPM_DEPENDENCY_HINT)) {
return error;
}
return `${error}\n\n${MISSING_GIT_FOR_NPM_DEPENDENCY_HINT}`;
}
function isMissingGitForNpmDependencyError(error: string): boolean {
const normalized = normalizeLowercaseStringOrEmpty(error);
return /\bspawn\s+git\b/u.test(normalized) && /\benoent\b/u.test(normalized);
}
export function logHookPackRestartHint(runtime: RuntimeEnv = defaultRuntime) {