Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions src/lib/formatters/plain-detect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,14 +75,15 @@ export function isPlainOutput(): boolean {
/**
* Strip ANSI escape sequences from a string.
*
* Handles SGR codes (`\x1b[...m`) and OSC 8 terminal hyperlink sequences
* (`\x1b]8;;url\x07text\x1b]8;;\x07`).
* Handles all CSI sequences (`\x1b[...LETTER` — covers SGR colour codes,
* cursor movement, screen-clear, and other control sequences) and OSC 8
* terminal hyperlink sequences (`\x1b]8;;url\x07text\x1b]8;;\x07`).
*/
export function stripAnsi(text: string): string {
Comment thread
sentry[bot] marked this conversation as resolved.
return (
text
// biome-ignore lint/suspicious/noControlCharactersInRegex: ANSI escape detection requires matching \x1b and \x07
.replace(/\x1b\[[0-9;]*m/g, "")
// biome-ignore lint/suspicious/noControlCharactersInRegex: ANSI escape detection requires matching \x1b
.replace(/\x1b\[[0-9;?]*[A-Za-z]/g, "")
// biome-ignore lint/suspicious/noControlCharactersInRegex: OSC 8 hyperlink sequences use \x1b and \x07
.replace(/\x1b\]8;;[^\x07]*\x07/g, "")
);
Expand Down
5 changes: 1 addition & 4 deletions src/lib/init/feedback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,7 @@ const FEEDBACK_COMMANDS: Record<InitFeedbackOutcome, string> = {
};

const FEEDBACK_COPY: Record<InitFeedbackOutcome, string[]> = {
success: [
"Nice, setup made it through.",
"Tell us what felt great or rough:",
],
success: ["Tell us what felt great or rough:"],
cancelled: [
"Sad to see setup stop. Was something going sideways?",
"Tell us so we can fix it:",
Expand Down
28 changes: 25 additions & 3 deletions src/lib/init/formatters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
*/

import { terminalLink } from "../formatters/colors.js";
import { featureLabel } from "./clack-utils.js";
import { stripAnsi } from "../formatters/plain-detect.js";
import { featureLabel, sortFeatures } from "./clack-utils.js";
import {
EXIT_DEPENDENCY_INSTALL_FAILED,
EXIT_PLATFORM_NOT_DETECTED,
Expand All @@ -33,6 +34,22 @@
* appear.
*/
function buildSummary(output: WizardOutput): WizardSummary | null {
// Resolve blurbs first so the Features row can check the *resolved* length.
// If the agent returns blurbs with wrong IDs they all drop out here, and
// the Features row falls back to showing correctly.
const blurbMap = new Map(
(output.featureBlurbs ?? []).map(({ feature, blurb }) => [
feature,
stripAnsi(blurb),
])
Comment thread
betegon marked this conversation as resolved.
);
const featureBlurbs = sortFeatures(output.features ?? [])
.map((feature) => {
const blurb = blurbMap.get(feature);
return blurb ? { label: featureLabel(feature), blurb } : null;
})
.filter((b): b is { label: string; blurb: string } => b !== null);

const fields: WizardSummary["fields"] = [];

if (output.platform) {
Expand All @@ -41,11 +58,11 @@
if (output.projectDir) {
fields.push({ label: "Directory", value: output.projectDir });
}
if (output.features?.length) {
if (output.features?.length && !featureBlurbs.length) {
fields.push({
label: "Features",
value: output.features.map(featureLabel).join(", "),
});

Check warning on line 65 in src/lib/init/formatters.ts

View check run for this annotation

@sentry/warden / warden: find-bugs

Partial featureBlurbs silently omits un-blurbed features from the setup summary

When the server returns blurbs for only a subset of enabled features, the plain `Features` row is suppressed (because `featureBlurbs.length > 0`), but features without blurbs are absent from the blurbs table too — they vanish from the summary entirely.
}
if (output.commands?.length) {
fields.push({
Expand All @@ -62,13 +79,18 @@

const changedFiles = output.changedFiles ?? [];

if (fields.length === 0 && changedFiles.length === 0) {
if (
fields.length === 0 &&
changedFiles.length === 0 &&
featureBlurbs.length === 0
) {
return null;
}

return {
fields,
...(changedFiles.length > 0 ? { changedFiles } : {}),
...(featureBlurbs.length > 0 ? { featureBlurbs } : {}),
};
}

Expand Down
1 change: 1 addition & 0 deletions src/lib/init/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,7 @@ export type WizardOutput = {
docsUrl?: string;
sentryProjectUrl?: string;
message?: string;
featureBlurbs?: Array<{ feature: string; blurb: string }>;
};

// Interactive payloads
Expand Down
22 changes: 22 additions & 0 deletions src/lib/init/ui/ink-app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1173,6 +1173,28 @@
))}
</Box>
) : null}
{summary.featureBlurbs !== undefined &&
summary.featureBlurbs.length > 0 ? (
<Box flexDirection="column" flexShrink={0} marginTop={1}>
<Text bold color={MUTED}>
Here&apos;s what we set up
</Text>
{summary.featureBlurbs.map(({ label, blurb }) => (
<Box flexDirection="row" flexShrink={0} key={label}>
<Box flexShrink={0} width={22}>
<Text bold color={PRIMARY}>
{label}
</Text>
</Box>
<Box flexShrink={1}>
<Text color={MUTED} wrap="wrap">
{blurb}

Check warning on line 1191 in src/lib/init/ui/ink-app.tsx

View check run for this annotation

@sentry/warden / warden: find-bugs

[NFF-FA8] Incomplete ANSI stripping of server-provided blurb allows terminal escape injection (additional location)

The `blurb` from the remote API is passed through `stripAnsi()`, which only strips CSI sequences and OSC 8 hyperlinks — it leaves general OSC sequences (e.g. `\x1b]0;title\x07` window-title injection, `\x1b]52;c;...\x07` clipboard injection in iTerm2/VTE, `\x1b]1337;...` iTerm2 protocol) unstripped, so a compromised server response can inject arbitrary terminal control sequences.
</Text>
</Box>
</Box>
))}
</Box>
) : null}
Comment thread
cursor[bot] marked this conversation as resolved.
{summary.changedFiles !== undefined && summary.changedFiles.length > 0 ? (
<ChangedFilesTree files={summary.changedFiles} />
) : null}
Expand Down
15 changes: 15 additions & 0 deletions src/lib/init/ui/ink-report.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import chalk from "chalk";
import { renderTextTable } from "../../formatters/text-table.js";
import { buildFileTree, flattenTree } from "./file-tree.js";
import type { WizardSummary } from "./types.js";

Expand Down Expand Up @@ -60,6 +61,20 @@
lines.push(` ${label} ${field.value}`);
}
}
if (summary?.featureBlurbs && summary.featureBlurbs.length > 0) {
lines.push("");
lines.push(` ${chalk.hex(REPORT_MUTED).bold("Here's what we set up")}`);

Check warning on line 66 in src/lib/init/ui/ink-report.ts

View check run for this annotation

@sentry/warden / warden: find-bugs

Incomplete ANSI stripping of server-provided blurb allows terminal escape injection

The `blurb` from the remote API is passed through `stripAnsi()`, which only strips CSI sequences and OSC 8 hyperlinks — it leaves general OSC sequences (e.g. `\x1b]0;title\x07` window-title injection, `\x1b]52;c;...\x07` clipboard injection in iTerm2/VTE, `\x1b]1337;...` iTerm2 protocol) unstripped, so a compromised server response can inject arbitrary terminal control sequences.
const tableRows = summary.featureBlurbs.map(({ label, blurb }) => [
chalk.bold(label),
chalk.hex(REPORT_MUTED)(blurb),
Comment thread
sentry-warden[bot] marked this conversation as resolved.
]);
const table = renderTextTable(["", ""], tableRows, {
shrinkable: [false, true],
});
for (const line of table.trimEnd().split("\n")) {
lines.push(` ${line}`);
}
}
if (summary?.changedFiles && summary.changedFiles.length > 0) {
lines.push("");
lines.push(` ${chalk.hex(REPORT_MUTED).bold("Changed files")}`);
Expand Down
21 changes: 20 additions & 1 deletion src/lib/init/ui/logging-ui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
renderInlineMarkdown,
renderMarkdown,
} from "../../formatters/markdown.js";
import { renderTextTable } from "../../formatters/text-table.js";
import { formatFeedbackHint, type InitFeedbackOutcome } from "../feedback.js";
import { buildFileTree, flattenTree } from "./file-tree.js";
import type {
Expand Down Expand Up @@ -94,7 +95,11 @@ export class LoggingUI implements WizardUI {
}

summary(summary: WizardSummary): void {
if (summary.fields.length === 0 && !summary.changedFiles?.length) {
if (
summary.fields.length === 0 &&
!summary.changedFiles?.length &&
!summary.featureBlurbs?.length
) {
return;
}
// Compact two-column key/value listing — one line per field. The
Expand All @@ -109,6 +114,20 @@ export class LoggingUI implements WizardUI {
const padded = field.label.padEnd(labelWidth);
this.writeLine(this.stdout, ` ${padded} ${field.value}`);
}
if (summary.featureBlurbs && summary.featureBlurbs.length > 0) {
this.writeLine(this.stdout, "");
this.writeLine(this.stdout, " Here's what we set up");
const tableRows = summary.featureBlurbs.map(({ label, blurb }) => [
label,
blurb,
]);
const table = renderTextTable(["", ""], tableRows, {
shrinkable: [false, true],
});
for (const line of table.trimEnd().split("\n")) {
this.writeLine(this.stdout, ` ${line}`);
}
}
if (summary.changedFiles && summary.changedFiles.length > 0) {
this.writeLine(this.stdout, "");
this.writeLine(this.stdout, " Changed files:");
Expand Down
2 changes: 2 additions & 0 deletions src/lib/init/ui/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,8 @@ export type WizardSummary = {
fields: { label: string; value: string }[];
/** Optional list of files the wizard added/edited/removed. */
changedFiles?: { action: string; path: string }[];
/** AI-generated per-feature blurbs personalised to the analysed project. */
featureBlurbs?: { label: string; blurb: string }[];
};

/**
Expand Down
1 change: 0 additions & 1 deletion test/lib/init/feedback.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ describe("formatFeedbackHint", () => {
test("maps init outcomes to copy-paste feedback commands", () => {
expect(formatFeedbackHint("success")).toBe(
[
"Nice, setup made it through.",
"Tell us what felt great or rough:",
'$ sentry cli feedback "sentry init worked well"',
].join("\n")
Expand Down
Loading
Loading