Skip to content

Commit 90a62fe

Browse files
fix(init): make URLs clickable with OSC 8 terminal hyperlinks
Wrap project and docs URLs in terminalLink() so they become clickable in terminals that support OSC 8 (iTerm2, VS Code, Windows Terminal). On unsupported terminals, the escape sequences are silently ignored. Also make the url parameter optional in terminalLink() — when only the display text is given and it is already the full URL, it doubles as the link target.
1 parent a6d62d7 commit 90a62fe

File tree

5 files changed

+24
-8
lines changed

5 files changed

+24
-8
lines changed

src/lib/formatters/colors.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,11 +51,12 @@ export const boldUnderline = (text: string): string =>
5151
* `string-width` treats OSC 8 sequences as zero-width, so column sizing
5252
* in tables is not affected.
5353
*
54-
* @param text - Display text
55-
* @param url - Target URL
54+
* @param text - Display text (also used as the link target when `url` is omitted)
55+
* @param url - Target URL. Defaults to `text`, which is convenient when the
56+
* display text is already the full URL.
5657
* @returns Text wrapped in OSC 8 hyperlink escape sequences
5758
*/
58-
export function terminalLink(text: string, url: string): string {
59+
export function terminalLink(text: string, url: string = text): string {
5960
// OSC 8 ; params ; URI BEL text OSC 8 ; ; BEL
6061
// \x1b] opens the OSC sequence; \x07 (BEL) terminates it.
6162
// Using BEL instead of ST (\x1b\\) for broad terminal compatibility.

src/lib/init/clack-utils.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
*/
66

77
import { cancel, isCancel } from "@clack/prompts";
8+
import { terminalLink } from "../formatters/colors.js";
89
import { SENTRY_DOCS_URL } from "./constants.js";
910

1011
export class WizardCancelledError extends Error {
@@ -17,7 +18,7 @@ export class WizardCancelledError extends Error {
1718
export function abortIfCancelled<T>(value: T | symbol): T {
1819
if (isCancel(value)) {
1920
cancel(
20-
`Setup cancelled. You can visit ${SENTRY_DOCS_URL} to set up manually.`
21+
`Setup cancelled. You can visit ${terminalLink(SENTRY_DOCS_URL)} to set up manually.`
2122
);
2223
throw new WizardCancelledError();
2324
}

src/lib/init/formatters.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
*/
66

77
import { cancel, log, note, outro } from "@clack/prompts";
8+
import { terminalLink } from "../formatters/colors.js";
89
import { featureLabel } from "./clack-utils.js";
910
import {
1011
EXIT_DEPENDENCY_INSTALL_FAILED,
@@ -41,10 +42,13 @@ function buildSummaryLines(output: WizardOutput): string[] {
4142
lines.push(`Commands: ${output.commands.join("; ")}`);
4243
}
4344
if (output.sentryProjectUrl) {
44-
lines.push(`Project: ${output.sentryProjectUrl}`);
45+
lines.push(`Project: ${terminalLink(output.sentryProjectUrl)}`);
4546
}
4647
if (output.docsUrl) {
47-
lines.push(`Docs: ${output.docsUrl}`);
48+
lines.push(`Docs: ${terminalLink(output.docsUrl)}`);
49+
}
50+
if (output.docsUrl) {
51+
lines.push(`Docs: ${terminalLink(output.docsUrl, output.docsUrl)}`);
4852
}
4953

5054
const changedFiles = output.changedFiles;
@@ -103,7 +107,7 @@ export function formatError(result: WorkflowRunResult): void {
103107

104108
const docsUrl = inner?.docsUrl;
105109
if (docsUrl) {
106-
log.info(`Docs: ${docsUrl}`);
110+
log.info(`Docs: ${terminalLink(docsUrl)}`);
107111
}
108112

109113
cancel("Setup failed");

src/lib/init/wizard-runner.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { captureException } from "@sentry/bun";
1313
import { formatBanner } from "../banner.js";
1414
import { CLI_VERSION } from "../constants.js";
1515
import { getAuthToken } from "../db/auth.js";
16+
import { terminalLink } from "../formatters/colors.js";
1617
import {
1718
abortIfCancelled,
1819
STEP_LABELS,
@@ -251,7 +252,7 @@ export async function runWizard(options: WizardOptions): Promise<void> {
251252

252253
log.info(
253254
"This wizard uses AI to analyze your project and configure Sentry." +
254-
`\nFor manual setup: ${SENTRY_DOCS_URL}`
255+
`\nFor manual setup: ${terminalLink(SENTRY_DOCS_URL)}`
255256
);
256257

257258
const tracingOptions = {

test/lib/formatters/colors.test.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,4 +127,13 @@ describe("terminalLink", () => {
127127
const stripped = result.replace(/\x1b\]8;;[^\x07]*\x07/g, "");
128128
expect(stripped).toBe("display");
129129
});
130+
131+
test("uses text as URL when url is omitted", () => {
132+
const result = terminalLink("https://example.com");
133+
expect(result).toContain("]8;;https://example.com");
134+
expect(result).toContain("https://example.com");
135+
expect(result).toBe(
136+
terminalLink("https://example.com", "https://example.com")
137+
);
138+
});
130139
});

0 commit comments

Comments
 (0)