From 18e1c3455c63524bd8c15f23c06f169fd3cb674e Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 17 Aug 2026 05:15:24 +0000 Subject: [PATCH] Print a summary listing which tools failed Listr shows per-task titles, but a failing task's title can stay generic or get overwritten by the tool's own error output, making it hard to tell which tool actually failed. After the run finishes, print an explicit "Errors (N):" summary naming each failed tool alongside its error message. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01XhinaznDRvEKj1P1qsFdPr --- src/cli.test.ts | 20 ++++++++++++++++++++ src/cli.ts | 20 ++++++++++++++++---- 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/src/cli.test.ts b/src/cli.test.ts index 0f878f0..3391165 100644 --- a/src/cli.test.ts +++ b/src/cli.test.ts @@ -197,4 +197,24 @@ describe('run', () => { expect(calls).toHaveLength(0); }); + + it('logs a summary naming every failed tool', async () => { + const logs: string[] = []; + const executor = async (cmd: string) => { + if (cmd === 'claude') throw new Error('[unauthenticated] Error'); + }; + + await run(['claude', 'codex'], executor, allInstalled, undefined, (msg) => logs.push(msg)); + + expect(logs.join('\n')).toContain('claude: Error: [unauthenticated] Error'); + }); + + it('does not log a summary when everything succeeds', async () => { + const logs: string[] = []; + const executor = async () => {}; + + await run(['claude'], executor, allInstalled, undefined, (msg) => logs.push(msg)); + + expect(logs).toHaveLength(0); + }); }); diff --git a/src/cli.ts b/src/cli.ts index e4b31a3..e23319c 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -14,6 +14,7 @@ interface Tool { export type Executor = (command: string, args: string[]) => Promise; export type Checker = (command: string) => Promise; export type Versioner = (command: string) => Promise; +export type Logger = (message: string) => void; export const AI_TOOLS: Tool[] = [ { name: 'claude', command: 'claude', args: ['update'], versioned: true }, @@ -74,6 +75,7 @@ export async function run( executor: Executor = async (cmd, args) => { await execa(cmd, args); }, checker: Checker = defaultChecker, versioner: Versioner = getVersion, + logger: Logger = (message) => console.error(message), ): Promise { const { selectedAITools, includeSkills } = selectTools(targetArgs); @@ -83,7 +85,7 @@ export async function run( ); const allTools = checks.filter((c) => c.installed).map((c) => c.tool); - let hasFailures = false; + const failures: { name: string; message: string }[] = []; const runner = new Listr( allTools.map((tool) => ({ @@ -93,9 +95,10 @@ export async function run( try { await executor(tool.command, tool.args); } catch (err) { - hasFailures = true; const e = err as ExecaError; - throw new Error(e.stderr || e.shortMessage || String(e)); + const message = e.stderr || e.shortMessage || String(e); + failures.push({ name: tool.name, message }); + throw new Error(message); } if (before) { const after = await versioner(tool.command); @@ -107,7 +110,16 @@ export async function run( ); await runner.run().catch(() => {}); - return !hasFailures; + + if (failures.length > 0) { + logger(''); + logger(`Errors (${failures.length}):`); + for (const failure of failures) { + logger(` - ${failure.name}: ${failure.message}`); + } + } + + return failures.length === 0; } if (process.argv[1] && realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url))) {