Skip to content

Commit 479c304

Browse files
committed
add terminal link as cli module so we can more easily patch it
1 parent cf9460a commit 479c304

File tree

5 files changed

+376
-124
lines changed

5 files changed

+376
-124
lines changed

packages/cli-v3/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@
9191
"@opentelemetry/semantic-conventions": "1.25.1",
9292
"@trigger.dev/build": "workspace:4.0.0-v4-beta.11",
9393
"@trigger.dev/core": "workspace:4.0.0-v4-beta.11",
94+
"ansi-escapes": "^7.0.0",
9495
"c12": "^1.11.1",
9596
"chalk": "^5.2.0",
9697
"chokidar": "^3.6.0",
@@ -103,6 +104,7 @@
103104
"evt": "^2.4.13",
104105
"fast-npm-meta": "^0.2.2",
105106
"gradient-string": "^2.0.2",
107+
"has-flag": "^5.0.1",
106108
"import-in-the-middle": "1.11.0",
107109
"import-meta-resolve": "^4.1.0",
108110
"jsonc-parser": "3.2.1",
@@ -123,6 +125,7 @@
123125
"socket.io-client": "4.7.5",
124126
"source-map-support": "0.5.21",
125127
"std-env": "^3.7.0",
128+
"supports-color": "^10.0.0",
126129
"terminal-link": "^3.0.0",
127130
"tiny-invariant": "^1.2.0",
128131
"tinyexec": "^0.3.1",

packages/cli-v3/src/utilities/cliOutput.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { log } from "@clack/prompts";
22
import chalk from "chalk";
3-
import terminalLink, { Options as TerminalLinkOptions } from "terminal-link";
3+
import { terminalLink, TerminalLinkOptions } from "./terminalLink.js";
44
import { hasTTY } from "std-env";
55

66
export const isInteractive = hasTTY;
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
import { createSupportsColor } from "supports-color";
2+
import hasFlag from "has-flag";
3+
4+
function parseVersion(versionString = ""): { major: number; minor: number; patch: number } {
5+
if (/^\d{3,4}$/.test(versionString)) {
6+
// Env var doesn't always use dots. example: 4601 => 46.1.0
7+
const match = /(\d{1,2})(\d{2})/.exec(versionString) ?? [];
8+
return {
9+
major: 0,
10+
minor: Number.parseInt(match[1] ?? "0", 10),
11+
patch: Number.parseInt(match[2] ?? "0", 10),
12+
};
13+
}
14+
15+
const versions = (versionString ?? "").split(".").map((n) => Number.parseInt(n, 10));
16+
return {
17+
major: versions[0] ?? 0,
18+
minor: versions[1] ?? 0,
19+
patch: versions[2] ?? 0,
20+
};
21+
}
22+
23+
/**
24+
Creates a supports hyperlinks check for a given stream.
25+
26+
@param stream - Optional stream to check for hyperlink support.
27+
@returns boolean indicating whether hyperlinks are supported.
28+
*/
29+
export function createSupportsHyperlinks(stream: NodeJS.WriteStream): boolean {
30+
const {
31+
CI,
32+
FORCE_HYPERLINK,
33+
NETLIFY,
34+
TEAMCITY_VERSION,
35+
TERM_PROGRAM,
36+
TERM_PROGRAM_VERSION,
37+
VTE_VERSION,
38+
TERM,
39+
} = process.env;
40+
41+
if (FORCE_HYPERLINK) {
42+
return !(FORCE_HYPERLINK.length > 0 && Number.parseInt(FORCE_HYPERLINK, 10) === 0);
43+
}
44+
45+
if (
46+
hasFlag("no-hyperlink") ||
47+
hasFlag("no-hyperlinks") ||
48+
hasFlag("hyperlink=false") ||
49+
hasFlag("hyperlink=never")
50+
) {
51+
return false;
52+
}
53+
54+
if (hasFlag("hyperlink=true") || hasFlag("hyperlink=always")) {
55+
return true;
56+
}
57+
58+
// Netlify does not run a TTY, it does not need `supportsColor` check
59+
if (NETLIFY) {
60+
return true;
61+
}
62+
63+
// If they specify no colors, they probably don't want hyperlinks.
64+
if (!createSupportsColor(stream)) {
65+
return false;
66+
}
67+
68+
if (stream && !stream.isTTY) {
69+
return false;
70+
}
71+
72+
// Windows Terminal
73+
if ("WT_SESSION" in process.env) {
74+
return true;
75+
}
76+
77+
if (process.platform === "win32") {
78+
return false;
79+
}
80+
81+
if (CI) {
82+
return false;
83+
}
84+
85+
if (TEAMCITY_VERSION) {
86+
return false;
87+
}
88+
89+
if (TERM_PROGRAM) {
90+
const version = parseVersion(TERM_PROGRAM_VERSION);
91+
92+
switch (TERM_PROGRAM) {
93+
case "iTerm.app": {
94+
if (version.major === 3) {
95+
return version.minor >= 1;
96+
}
97+
98+
return version.major > 3;
99+
}
100+
101+
case "WezTerm": {
102+
return version.major >= 20_200_620;
103+
}
104+
105+
case "vscode": {
106+
// eslint-disable-next-line no-mixed-operators
107+
return version.major > 1 || (version.major === 1 && version.minor >= 72);
108+
}
109+
110+
case "ghostty": {
111+
return true;
112+
}
113+
// No default
114+
}
115+
}
116+
117+
if (VTE_VERSION) {
118+
// 0.50.0 was supposed to support hyperlinks, but throws a segfault
119+
if (VTE_VERSION === "0.50.0") {
120+
return false;
121+
}
122+
123+
const version = parseVersion(VTE_VERSION);
124+
return version.major > 0 || version.minor >= 50;
125+
}
126+
127+
switch (TERM) {
128+
case "alacritty": {
129+
// Support added in v0.11 (2022-10-13)
130+
return true;
131+
}
132+
// No default
133+
}
134+
135+
return false;
136+
}
137+
138+
/** Object containing hyperlink support status for stdout and stderr. */
139+
const supportsHyperlinks = {
140+
/** Whether stdout supports hyperlinks. */
141+
stdout: createSupportsHyperlinks(process.stdout),
142+
/** Whether stderr supports hyperlinks. */
143+
stderr: createSupportsHyperlinks(process.stderr),
144+
};
145+
146+
export default supportsHyperlinks;
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import ansiEscapes from "ansi-escapes";
2+
import supportsHyperlinks from "./supportsHyperlinks.js";
3+
4+
export type TerminalLinkOptions = {
5+
/**
6+
Override the default fallback. If false, the fallback will be disabled.
7+
@default `${text} (${url})`
8+
*/
9+
readonly fallback?: ((text: string, url: string) => string) | boolean;
10+
};
11+
12+
/**
13+
Create a clickable link in the terminal's stdout.
14+
15+
[Supported terminals.](https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda)
16+
For unsupported terminals, the link will be printed in parens after the text: `My website (https://sindresorhus.com)`,
17+
unless the fallback is disabled by setting the `fallback` option to `false`.
18+
19+
@param text - Text to linkify.
20+
@param url - URL to link to.
21+
22+
@example
23+
```
24+
import terminalLink from 'terminal-link';
25+
26+
const link = terminalLink('My Website', 'https://sindresorhus.com');
27+
console.log(link);
28+
```
29+
*/
30+
function terminalLink(
31+
text: string,
32+
url: string,
33+
{ target = "stdout", ...options }: { target?: "stdout" | "stderr" } & TerminalLinkOptions = {}
34+
) {
35+
if (!supportsHyperlinks[target]) {
36+
// If the fallback has been explicitly disabled, don't modify the text itself.
37+
if (options.fallback === false) {
38+
return text;
39+
}
40+
41+
return typeof options.fallback === "function"
42+
? options.fallback(text, url)
43+
: `${text} (\u200B${url}\u200B)`;
44+
}
45+
46+
return ansiEscapes.link(text, url);
47+
}
48+
/**
49+
Check whether the terminal supports links.
50+
51+
Prefer just using the default fallback or the `fallback` option whenever possible.
52+
*/
53+
terminalLink.isSupported = supportsHyperlinks.stdout;
54+
terminalLink.stderr = terminalLinkStderr;
55+
56+
/**
57+
Create a clickable link in the terminal's stderr.
58+
59+
[Supported terminals.](https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda)
60+
For unsupported terminals, the link will be printed in parens after the text: `My website (https://sindresorhus.com)`.
61+
62+
@param text - Text to linkify.
63+
@param url - URL to link to.
64+
65+
@example
66+
```
67+
import terminalLink from 'terminal-link';
68+
69+
const link = terminalLink.stderr('My Website', 'https://sindresorhus.com');
70+
console.error(link);
71+
```
72+
*/
73+
function terminalLinkStderr(text: string, url: string, options: TerminalLinkOptions = {}) {
74+
return terminalLink(text, url, { target: "stderr", ...options });
75+
}
76+
77+
/**
78+
Check whether the terminal's stderr supports links.
79+
80+
Prefer just using the default fallback or the `fallback` option whenever possible.
81+
*/
82+
terminalLinkStderr.isSupported = supportsHyperlinks.stderr;
83+
84+
export { terminalLink };

0 commit comments

Comments
 (0)