From 4035f4794eda038d12d7f59eabb7f43dd4284c38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Beteg=C3=B3n?= Date: Mon, 6 Apr 2026 14:14:31 +0200 Subject: [PATCH 1/2] refactor(init): use mdKvTable and renderMarkdown for wizard summary Replace manual spacing with mdKvTable for key-value output and markdown lists for changed files, matching the formatting patterns used by other commands. Drop clack note() in favor of log.message() to avoid forced dim styling on the summary content. Colorize file action icons. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/lib/init/formatters.ts | 53 +++++++++++++++++--------------- test/lib/init/formatters.test.ts | 38 +++++++++++------------ 2 files changed, 47 insertions(+), 44 deletions(-) diff --git a/src/lib/init/formatters.ts b/src/lib/init/formatters.ts index c7cbfe005..0b7c9742b 100644 --- a/src/lib/init/formatters.ts +++ b/src/lib/init/formatters.ts @@ -4,8 +4,9 @@ * Format wizard results and errors for terminal display using clack. */ -import { cancel, log, note, outro } from "@clack/prompts"; -import { terminalLink } from "../formatters/colors.js"; +import { cancel, log, outro } from "@clack/prompts"; +import { green, red, terminalLink, yellow } from "../formatters/colors.js"; +import { mdKvTable, renderMarkdown } from "../formatters/markdown.js"; import { featureLabel } from "./clack-utils.js"; import { EXIT_DEPENDENCY_INSTALL_FAILED, @@ -16,56 +17,60 @@ import type { WizardOutput, WorkflowRunResult } from "./types.js"; function fileActionIcon(action: string): string { if (action === "create") { - return "+"; + return green("+"); } if (action === "delete") { - return "-"; + return red("-"); } - return "~"; + return yellow("~"); } -function buildSummaryLines(output: WizardOutput): string[] { - const lines: string[] = []; +function buildSummary(output: WizardOutput): string { + const sections: string[] = []; + const kvRows: [string, string][] = []; if (output.platform) { - lines.push(`Platform: ${output.platform}`); + kvRows.push(["Platform", output.platform]); } if (output.projectDir) { - lines.push(`Directory: ${output.projectDir}`); + kvRows.push(["Directory", output.projectDir]); } - if (output.features?.length) { - lines.push(`Features: ${output.features.map(featureLabel).join(", ")}`); + kvRows.push(["Features", output.features.map(featureLabel).join(", ")]); } - if (output.commands?.length) { - lines.push(`Commands: ${output.commands.join("; ")}`); + kvRows.push(["Commands", output.commands.join("; ")]); } if (output.sentryProjectUrl) { - lines.push(`Project: ${terminalLink(output.sentryProjectUrl)}`); + kvRows.push(["Project", output.sentryProjectUrl]); } if (output.docsUrl) { - lines.push(`Docs: ${terminalLink(output.docsUrl)}`); + kvRows.push(["Docs", output.docsUrl]); + } + + if (kvRows.length > 0) { + sections.push(mdKvTable(kvRows)); } const changedFiles = output.changedFiles; if (changedFiles?.length) { - lines.push(""); - lines.push("Changed files:"); - for (const f of changedFiles) { - lines.push(` ${fileActionIcon(f.action)} ${f.path}`); - } + sections.push( + "### Changed files\n\n" + + changedFiles + .map((f) => `- ${fileActionIcon(f.action)} ${f.path}`) + .join("\n") + ); } - return lines; + return sections.join("\n\n"); } export function formatResult(result: WorkflowRunResult): void { const output: WizardOutput = result.result ?? {}; - const lines = buildSummaryLines(output); + const md = buildSummary(output); - if (lines.length > 0) { - note(lines.join("\n"), "Setup complete"); + if (md.length > 0) { + log.message(renderMarkdown(md)); } if (output.warnings?.length) { diff --git a/test/lib/init/formatters.test.ts b/test/lib/init/formatters.test.ts index a4977732c..295cefe9b 100644 --- a/test/lib/init/formatters.test.ts +++ b/test/lib/init/formatters.test.ts @@ -12,7 +12,7 @@ import * as clack from "@clack/prompts"; import { formatError, formatResult } from "../../../src/lib/init/formatters.js"; // Spy on clack functions to capture arguments without replacing them -let noteSpy: ReturnType; +let logMessageSpy: ReturnType; let outroSpy: ReturnType; let cancelSpy: ReturnType; let logInfoSpy: ReturnType; @@ -24,7 +24,7 @@ const noop = () => { }; beforeEach(() => { - noteSpy = spyOn(clack, "note").mockImplementation(noop); + logMessageSpy = spyOn(clack.log, "message").mockImplementation(noop); outroSpy = spyOn(clack, "outro").mockImplementation(noop); cancelSpy = spyOn(clack, "cancel").mockImplementation(noop); logInfoSpy = spyOn(clack.log, "info").mockImplementation(noop); @@ -33,7 +33,7 @@ beforeEach(() => { }); afterEach(() => { - noteSpy.mockRestore(); + logMessageSpy.mockRestore(); outroSpy.mockRestore(); cancelSpy.mockRestore(); logInfoSpy.mockRestore(); @@ -60,25 +60,23 @@ describe("formatResult", () => { }, }); - expect(noteSpy).toHaveBeenCalledTimes(1); - const noteContent: string = noteSpy.mock.calls[0][0]; - - expect(noteContent).toContain("Next.js"); - expect(noteContent).toContain("/app"); - expect(noteContent).toContain("Error Monitoring"); - expect(noteContent).toContain("Performance Monitoring"); - expect(noteContent).toContain("npm install @sentry/nextjs"); - expect(noteContent).toContain("+ sentry.client.config.ts"); - expect(noteContent).toContain("~ next.config.js"); - expect(noteContent).toContain("- old-sentry.js"); - - expect(noteSpy.mock.calls[0][1]).toBe("Setup complete"); + expect(logMessageSpy).toHaveBeenCalledTimes(1); + const content: string = logMessageSpy.mock.calls[0][0]; + + expect(content).toContain("Next.js"); + expect(content).toContain("/app"); + expect(content).toContain("Error Monitoring"); + expect(content).toContain("Performance Monitoring"); + expect(content).toContain("npm install @sentry/nextjs"); + expect(content).toContain("sentry.client.config.ts"); + expect(content).toContain("next.config.js"); + expect(content).toContain("old-sentry.js"); }); - test("skips note when result has no summary fields", () => { + test("skips summary when result has no summary fields", () => { formatResult({ status: "success" }); - expect(noteSpy).not.toHaveBeenCalled(); + expect(logMessageSpy).not.toHaveBeenCalled(); expect(outroSpy).toHaveBeenCalled(); }); @@ -98,8 +96,8 @@ describe("formatResult", () => { test("unwraps nested result property", () => { formatResult({ status: "success", result: { platform: "React" } }); - const noteContent: string = noteSpy.mock.calls[0][0]; - expect(noteContent).toContain("React"); + const content: string = logMessageSpy.mock.calls[0][0]; + expect(content).toContain("React"); }); }); From 43f26019d2cb0169f42a52260c82d67f7a1be322 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Beteg=C3=B3n?= Date: Mon, 6 Apr 2026 14:47:26 +0200 Subject: [PATCH 2/2] fix(init): use colorTag instead of raw ANSI for file action icons Raw chalk/color helpers embed ANSI escape codes into markdown source, which can break marked's lexer on unmatched brackets. Use colorTag() to produce // tags that the custom markdown renderer handles correctly in both TTY and plain output modes. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/lib/init/formatters.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/lib/init/formatters.ts b/src/lib/init/formatters.ts index 0b7c9742b..7e0eb771b 100644 --- a/src/lib/init/formatters.ts +++ b/src/lib/init/formatters.ts @@ -5,8 +5,8 @@ */ import { cancel, log, outro } from "@clack/prompts"; -import { green, red, terminalLink, yellow } from "../formatters/colors.js"; -import { mdKvTable, renderMarkdown } from "../formatters/markdown.js"; +import { terminalLink } from "../formatters/colors.js"; +import { colorTag, mdKvTable, renderMarkdown } from "../formatters/markdown.js"; import { featureLabel } from "./clack-utils.js"; import { EXIT_DEPENDENCY_INSTALL_FAILED, @@ -17,12 +17,12 @@ import type { WizardOutput, WorkflowRunResult } from "./types.js"; function fileActionIcon(action: string): string { if (action === "create") { - return green("+"); + return colorTag("green", "+"); } if (action === "delete") { - return red("-"); + return colorTag("red", "-"); } - return yellow("~"); + return colorTag("yellow", "~"); } function buildSummary(output: WizardOutput): string {