hyperlinker
hyperlinker is a dependency-free CommonJS function that wraps visible text in an OSC 8 terminal hyperlink escape sequence. A supporting terminal displays the short label but opens the separate URI when the user clicks it. An optional object adds hidden OSC 8 parameters such as a shared link id. The package only formats a string: it does not detect terminal support, validate destinations, sanitize control characters, print output, or create HTML links.
The implementation is short enough to audit, but it leaves the two hard parts, capability fallback and safe handling of untrusted terminal data, entirely to you. For new CLIs, terminal-link is the more complete default; keep hyperlinker for trusted legacy output.
Use it if
- You maintain CommonJS CLI output that already depends on hyperlinker's exact BEL-terminated OSC 8 sequence
- You need one tiny function and will perform support detection, fallback rendering, and input validation yourself
- Your visible text, URI, and parameters are trusted application constants rather than user-controlled data
- You target terminal emulators known to support OSC 8 and do not need browser bundling or TypeScript declarations
- You want a usable fallback automatically: the README says unsupported terminals will likely show only the label, so the destination disappears unless you add supports-hyperlinks and print it yourself
- Any link component is untrusted: index.js concatenates text, URI, parameter keys, and values without escaping BEL, ESC, semicolons, colons, or other terminal control characters
- You ship browser code: package.json maps browser builds to browser.js, whose only behavior is throwing an unsupported error
- You need TypeScript or ESM-first packaging: version 1.0.0 has no declarations or export map and exposes a single CommonJS function
- You want current maintenance signals: npm 1.0.0 was published in November 2017, the last default-branch commits are from that month, and the repository's last push was in July 2020
Setup reality
npm install hyperlinker is the whole package installation: there are no dependencies, native binaries, peers, credentials, or configuration files, and its engines field allows Node 4 or newer. Call the exported CommonJS function with text and URI; it returns a string containing ESC ] 8, semicolon-separated fields, a BEL terminator, your visible text, and a closing sequence. Nothing is printed until you write or log that string. The main first-run surprise is that capability detection is deliberately out of scope. In unsupported terminals, redirected output, CI logs, and many log collectors, escape bytes may be ignored, stripped, or shown literally, while the human-readable output does not include the URL. Pair it with supports-hyperlinks and make the fallback contain the destination. The function performs no type checks, URL parsing, protocol allowlist, encoding, or control-character escaping. Treat text, URI, parameter names, and values as terminal-control input: a BEL or ESC from untrusted data can end the hyperlink sequence and inject further terminal instructions. Parameter pairs use key=value joined by colons, so delimiters inside keys or values are also ambiguous. The package always uses BEL, offers no switch to the ST terminator, and does not expose a stream helper. It ships no TypeScript declarations. Bundlers choose browser.js through the browser field, and that stub throws on every call rather than returning plain text. OSC 8 behavior is controlled by each terminal, not Node, so test the actual terminals, multiplexers, CI capture, and pager combinations your users run.
Patterns
Create a terminal hyperlinkcreate-link
const hyperlinker = require('hyperlinker');
const label = hyperlinker('Open documentation', 'https://example.com/docs');
console.log(label);The function returns a control-sequence string; console.log is what writes it. Unsupported terminals may show only Open documentation and hide the URL.
Provide a fallback when stdout lacks hyperlink supportdetect-support
const supportsHyperlinks = require('supports-hyperlinks');
const hyperlinker = require('hyperlinker');
function stdoutLink(text, uri) {
return supportsHyperlinks.stdout
? hyperlinker(text, uri)
: `${text} (${uri})`;
}
console.log(stdoutLink('Build report', reportUrl));Detection is not included in hyperlinker. Keep the URI visible in fallback text so redirected logs remain useful.
Detect stderr separatelylink-on-stderr
function stderrLink(text, uri) {
return supportsHyperlinks.stderr
? hyperlinker(text, uri)
: `${text}: ${uri}`;
}
process.stderr.write(stderrLink('Error details', errorUrl) + '\n');stdout and stderr can have different TTY and redirection states. Do not reuse stdout detection for diagnostics written to stderr.
Link to a local file safelylink-local-file
const { pathToFileURL } = require('url');
const uri = pathToFileURL('/tmp/build report.html').href;
console.log(hyperlinker('Open local report', uri));pathToFileURL handles spaces, platform path syntax, and percent encoding more reliably than concatenating file:// yourself. Terminal policy may still block local-file links.
Group separated link fragments with an idreuse-link-id
const first = hyperlinker('issue', issueUrl, { id: 'issue-42' });
const second = hyperlinker('#42', issueUrl, { id: 'issue-42' });
console.log(first, 'was fixed in', second);The OSC 8 id parameter can associate fragments visually, but the README warns that parameter behavior is not widely supported. Use trusted delimiter-free ids.
Apply color inside the clickable labelcolor-link-text
const chalk = require('chalk');
const label = hyperlinker(
chalk.cyan('project homepage'),
'https://example.com'
);
console.log(label);Color control sequences become part of the visible-text region. Test the combination in target terminals because nested terminal controls can interact with link rendering.
Allow only expected URL protocolsvalidate-uri
function safeUri(value) {
const url = new URL(value);
if (!['https:', 'http:'].includes(url.protocol)) {
throw new Error('Unsupported link protocol');
}
return url.href;
}
console.log(hyperlinker('Release', safeUri(candidateUrl)));hyperlinker accepts any string and does not validate schemes. Apply a protocol allowlist before link creation when destinations are not constants.
Reject terminal control bytesreject-control-characters
const CONTROL = /[\u0000-\u001F\u007F-\u009F]/u;
function trustedField(value, field) {
const text = String(value);
if (CONTROL.test(text)) throw new Error(`${field} contains control bytes`);
return text;
}
const output = hyperlinker(
trustedField(label, 'label'),
trustedField(uri, 'uri')
);The package concatenates inputs without escaping. BEL and ESC are especially dangerous because they can terminate or start terminal control sequences.
Write a link as part of a status linewrite-without-extra-space
process.stdout.write('Report: ');
process.stdout.write(hyperlinker('open', reportUrl));
process.stdout.write('\n');The returned value contains no trailing newline. process.stdout.write gives exact control over spacing where console.log argument separators would be unwanted.
Make escape bytes visible in testsinspect-output
const actual = hyperlinker('docs', 'https://example.com');
console.log(JSON.stringify(actual));
// "\u001b]8;;https://example.com\u0007docs\u001b]8;;\u0007"Snapshot an escaped representation instead of raw OSC bytes so test reporters and terminals do not interpret the value as a real link.
Assert exact OSC 8 formattingassert-exact-sequence
const expected =
'\u001B]8;id=docs;https://example.com\u0007' +
'Docs' +
'\u001B]8;;\u0007';
assert.equal(hyperlinker('Docs', 'https://example.com', { id: 'docs' }), expected);Version 1.0.0 uses BEL as the terminator and colon-joins multiple parameters. Exact tests catch accidental control-byte changes.
Disable links when output is redirectedplain-text-for-pipes
const interactive = process.stdout.isTTY && supportsHyperlinks.stdout;
const output = interactive
? hyperlinker('artifact', artifactUrl)
: `artifact: ${artifactUrl}`;
process.stdout.write(output + '\n');Plain output is safer for files, pipes, pagers, and machine-readable logs. Capability detection can be truthy in environments where your downstream consumer still strips OSC sequences.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| terminal-link | npm | Use a current hyperlink helper with built-in support detection and a configurable fallback |
| ansi-escapes | npm | Use a maintained collection when hyperlinks are one of several terminal-control operations your CLI needs |
| supports-hyperlinks | npm | Use detection plus a short local OSC 8 formatter when you want explicit fallback and sanitization policy without another link wrapper |