From 1a3323a26152cc352cb849c09a70c6a8386143ba Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 1 Jan 2026 22:55:15 +0100 Subject: [PATCH] fix(cli): improve skill install failure output --- src/agents/skills-install.ts | 51 ++++++++++++++++++++++++++++++---- src/commands/onboard-skills.ts | 25 +++++++++++++++-- 2 files changed, 68 insertions(+), 8 deletions(-) diff --git a/src/agents/skills-install.ts b/src/agents/skills-install.ts index bce676389..7f0eb78aa 100644 --- a/src/agents/skills-install.ts +++ b/src/agents/skills-install.ts @@ -29,6 +29,42 @@ export type SkillInstallResult = { code: number | null; }; +function summarizeInstallOutput(text: string): string | undefined { + const raw = text.trim(); + if (!raw) return undefined; + const lines = raw + .split("\n") + .map((line) => line.trim()) + .filter(Boolean); + if (lines.length === 0) return undefined; + + const preferred = + lines.find((line) => /^error\b/i.test(line)) ?? + lines.find((line) => /\b(err!|error:|failed)\b/i.test(line)) ?? + lines.at(-1); + + if (!preferred) return undefined; + const normalized = preferred.replace(/\s+/g, " ").trim(); + const maxLen = 200; + return normalized.length > maxLen + ? `${normalized.slice(0, maxLen - 1)}…` + : normalized; +} + +function formatInstallFailureMessage(result: { + code: number | null; + stdout: string; + stderr: string; +}): string { + const code = + typeof result.code === "number" ? `exit ${result.code}` : "unknown exit"; + const summary = + summarizeInstallOutput(result.stderr) ?? + summarizeInstallOutput(result.stdout); + if (!summary) return `Install failed (${code})`; + return `Install failed (${code}): ${summary}`; +} + function resolveInstallId(spec: SkillInstallSpec, index: number): string { return (spec.id ?? `${spec.kind}-${index}`).trim(); } @@ -91,7 +127,9 @@ function buildInstallCommand( } } -async function resolveBrewBinDir(timeoutMs: number): Promise { +async function resolveBrewBinDir( + timeoutMs: number, +): Promise { if (!hasBinary("brew")) return undefined; const prefixResult = await runCommandWithTimeout(["brew", "--prefix"], { timeoutMs: Math.min(timeoutMs, 30_000), @@ -204,9 +242,12 @@ export async function installSkill( if (spec.kind === "go" && !hasBinary("go")) { if (hasBinary("brew")) { - const brewResult = await runCommandWithTimeout(["brew", "install", "go"], { - timeoutMs, - }); + const brewResult = await runCommandWithTimeout( + ["brew", "install", "go"], + { + timeoutMs, + }, + ); if (brewResult.code !== 0) { return { ok: false, @@ -252,7 +293,7 @@ export async function installSkill( const success = result.code === 0; return { ok: success, - message: success ? "Installed" : "Install failed", + message: success ? "Installed" : formatInstallFailureMessage(result), stdout: result.stdout.trim(), stderr: result.stderr.trim(), code: result.code, diff --git a/src/commands/onboard-skills.ts b/src/commands/onboard-skills.ts index bb14ac731..754610513 100644 --- a/src/commands/onboard-skills.ts +++ b/src/commands/onboard-skills.ts @@ -13,6 +13,15 @@ import type { ClawdisConfig } from "../config/config.js"; import type { RuntimeEnv } from "../runtime.js"; import { guardCancel, resolveNodeManagerOptions } from "./onboard-helpers.js"; +function summarizeInstallFailure(message: string): string | undefined { + const cleaned = message + .replace(/^Install failed(?:\s*\([^)]*\))?\s*:?\s*/i, "") + .trim(); + if (!cleaned) return undefined; + const maxLen = 140; + return cleaned.length > maxLen ? `${cleaned.slice(0, maxLen - 1)}…` : cleaned; +} + function upsertSkillEntry( cfg: ClawdisConfig, skillKey: string, @@ -108,9 +117,19 @@ export async function setupSkills( installId, config: next, }); - spin.stop(result.ok ? `Installed ${name}` : `Install failed: ${name}`); - if (!result.ok && result.stderr) { - runtime.log(result.stderr.trim()); + if (result.ok) { + spin.stop(`Installed ${name}`); + } else { + const code = result.code == null ? "" : ` (exit ${result.code})`; + const detail = summarizeInstallFailure(result.message); + spin.stop( + `Install failed: ${name}${code}${detail ? ` — ${detail}` : ""}`, + ); + if (result.stderr) runtime.log(result.stderr.trim()); + else if (result.stdout) runtime.log(result.stdout.trim()); + runtime.log( + "Tip: run `clawdis doctor` to review skills + requirements.", + ); } } }