terminfo.dev 1.2.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/detect.ts +76 -26
- package/src/index.ts +14 -0
- package/src/submit.ts +22 -0
package/package.json
CHANGED
package/src/detect.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Terminal detection — identify the running terminal emulator.
|
|
3
3
|
*
|
|
4
|
-
* Uses environment variables,
|
|
4
|
+
* Uses environment variables, macOS bundle metadata, and fallback heuristics.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { release } from "node:os"
|
|
8
|
+
import { execFileSync } from "node:child_process"
|
|
8
9
|
|
|
9
10
|
export interface TerminalInfo {
|
|
10
11
|
name: string
|
|
@@ -14,7 +15,7 @@ export interface TerminalInfo {
|
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
/** Known terminal detection via environment variables */
|
|
17
|
-
const ENV_DETECTORS: Array<{ env: string; name: string
|
|
18
|
+
const ENV_DETECTORS: Array<{ env: string; name: string }> = [
|
|
18
19
|
{ env: "GHOSTTY_RESOURCES_DIR", name: "ghostty" },
|
|
19
20
|
{ env: "KITTY_WINDOW_ID", name: "kitty" },
|
|
20
21
|
{ env: "WEZTERM_EXECUTABLE", name: "wezterm" },
|
|
@@ -33,46 +34,97 @@ const TERM_PROGRAM_MAP: Record<string, string> = {
|
|
|
33
34
|
WarpTerminal: "warp",
|
|
34
35
|
}
|
|
35
36
|
|
|
37
|
+
/** Known macOS bundle IDs for version lookup */
|
|
38
|
+
const BUNDLE_IDS: Record<string, string> = {
|
|
39
|
+
ghostty: "com.mitchellh.ghostty",
|
|
40
|
+
kitty: "net.kovidgoyal.kitty",
|
|
41
|
+
iterm2: "com.googlecode.iterm2",
|
|
42
|
+
"terminal-app": "com.apple.Terminal",
|
|
43
|
+
wezterm: "org.wezfurlong.wezterm",
|
|
44
|
+
alacritty: "org.alacritty",
|
|
45
|
+
warp: "dev.warp.Warp-Stable",
|
|
46
|
+
}
|
|
47
|
+
|
|
36
48
|
export function detectTerminal(): TerminalInfo {
|
|
37
49
|
const os = detectOS()
|
|
38
50
|
const osVersion = detectOSVersion()
|
|
39
51
|
|
|
52
|
+
let name = "unknown"
|
|
53
|
+
let version = ""
|
|
54
|
+
|
|
40
55
|
// Check specific env vars first
|
|
41
|
-
for (const { env, name } of ENV_DETECTORS) {
|
|
56
|
+
for (const { env, name: n } of ENV_DETECTORS) {
|
|
42
57
|
if (process.env[env]) {
|
|
43
|
-
|
|
58
|
+
name = n
|
|
59
|
+
break
|
|
44
60
|
}
|
|
45
61
|
}
|
|
46
62
|
|
|
47
63
|
// Check $TERM_PROGRAM
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
64
|
+
if (name === "unknown") {
|
|
65
|
+
const termProgram = process.env.TERM_PROGRAM
|
|
66
|
+
if (termProgram) {
|
|
67
|
+
name = TERM_PROGRAM_MAP[termProgram] ?? termProgram.toLowerCase()
|
|
68
|
+
version = process.env.TERM_PROGRAM_VERSION ?? ""
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Check $TERMINAL_EMULATOR (Linux)
|
|
73
|
+
if (name === "unknown") {
|
|
74
|
+
const termEmu = process.env.TERMINAL_EMULATOR
|
|
75
|
+
if (termEmu) name = termEmu.toLowerCase()
|
|
53
76
|
}
|
|
54
77
|
|
|
55
|
-
//
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
return { name: termEmu.toLowerCase(), version: "", os, osVersion }
|
|
78
|
+
// Fallback: $TERM
|
|
79
|
+
if (name === "unknown") {
|
|
80
|
+
name = process.env.TERM ?? "unknown"
|
|
59
81
|
}
|
|
60
82
|
|
|
61
|
-
//
|
|
62
|
-
|
|
63
|
-
|
|
83
|
+
// On macOS, get version from app bundle if we don't have it yet
|
|
84
|
+
if (!version && os === "macos") {
|
|
85
|
+
version = getMacOSAppVersion(name)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return { name, version, os, osVersion }
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Get app version from macOS bundle metadata.
|
|
93
|
+
* Uses $__CFBundleIdentifier → mdfind → PlistBuddy.
|
|
94
|
+
*/
|
|
95
|
+
function getMacOSAppVersion(terminalName: string): string {
|
|
96
|
+
try {
|
|
97
|
+
// Try __CFBundleIdentifier first (set by the running app)
|
|
98
|
+
let bundleId = process.env.__CFBundleIdentifier
|
|
99
|
+
if (!bundleId) bundleId = BUNDLE_IDS[terminalName]
|
|
100
|
+
if (!bundleId) return ""
|
|
101
|
+
|
|
102
|
+
// Find app path from bundle ID
|
|
103
|
+
const appPath = execFileSync("mdfind", [`kMDItemCFBundleIdentifier == '${bundleId}'`], {
|
|
104
|
+
encoding: "utf-8",
|
|
105
|
+
timeout: 3000,
|
|
106
|
+
}).trim().split("\n")[0]
|
|
107
|
+
|
|
108
|
+
if (!appPath) return ""
|
|
109
|
+
|
|
110
|
+
// Read version from Info.plist
|
|
111
|
+
const version = execFileSync("/usr/libexec/PlistBuddy", [
|
|
112
|
+
"-c", "Print :CFBundleShortVersionString",
|
|
113
|
+
`${appPath}/Contents/Info.plist`,
|
|
114
|
+
], { encoding: "utf-8", timeout: 2000 }).trim()
|
|
115
|
+
|
|
116
|
+
return version
|
|
117
|
+
} catch {
|
|
118
|
+
return ""
|
|
119
|
+
}
|
|
64
120
|
}
|
|
65
121
|
|
|
66
122
|
function detectOS(): string {
|
|
67
123
|
switch (process.platform) {
|
|
68
|
-
case "darwin":
|
|
69
|
-
|
|
70
|
-
case "
|
|
71
|
-
|
|
72
|
-
case "win32":
|
|
73
|
-
return "windows"
|
|
74
|
-
default:
|
|
75
|
-
return process.platform
|
|
124
|
+
case "darwin": return "macos"
|
|
125
|
+
case "linux": return "linux"
|
|
126
|
+
case "win32": return "windows"
|
|
127
|
+
default: return process.platform
|
|
76
128
|
}
|
|
77
129
|
}
|
|
78
130
|
|
|
@@ -86,8 +138,6 @@ function detectOSVersion(): string {
|
|
|
86
138
|
|
|
87
139
|
/**
|
|
88
140
|
* Query terminal identity via DA2 (Secondary Device Attributes).
|
|
89
|
-
* Sends CSI > 0 c and parses the response.
|
|
90
|
-
*
|
|
91
141
|
* Must be called with raw mode enabled on stdin.
|
|
92
142
|
*/
|
|
93
143
|
export async function queryDA2(
|
package/src/index.ts
CHANGED
|
@@ -79,6 +79,18 @@ async function runProbes(): Promise<ProbeResults> {
|
|
|
79
79
|
process.stdout.write("\x1b[?1049l")
|
|
80
80
|
})
|
|
81
81
|
|
|
82
|
+
// Drain any late-arriving escape sequence responses before output
|
|
83
|
+
await new Promise<void>((resolve) => {
|
|
84
|
+
process.stdin.resume()
|
|
85
|
+
const timer = setTimeout(() => {
|
|
86
|
+
process.stdin.pause()
|
|
87
|
+
process.stdin.removeAllListeners("readable")
|
|
88
|
+
resolve()
|
|
89
|
+
}, 100)
|
|
90
|
+
process.stdin.on("readable", () => { while (process.stdin.read() !== null) {} })
|
|
91
|
+
timer.unref()
|
|
92
|
+
})
|
|
93
|
+
|
|
82
94
|
return { terminal, results, notes, responses, passed, total: ALL_PROBES.length }
|
|
83
95
|
}
|
|
84
96
|
|
|
@@ -185,6 +197,8 @@ program
|
|
|
185
197
|
notes: data.notes,
|
|
186
198
|
responses: data.responses,
|
|
187
199
|
generated: new Date().toISOString(),
|
|
200
|
+
cliVersion: "1.3.0",
|
|
201
|
+
probeCount: ALL_PROBES.length,
|
|
188
202
|
})
|
|
189
203
|
if (url) {
|
|
190
204
|
console.log(`\x1b[32m✓ Issue created:\x1b[0m ${link(url, url)}`)
|
package/src/submit.ts
CHANGED
|
@@ -19,9 +19,29 @@ interface SubmitData {
|
|
|
19
19
|
notes: Record<string, string>
|
|
20
20
|
responses: Record<string, string>
|
|
21
21
|
generated: string
|
|
22
|
+
cliVersion?: string
|
|
23
|
+
probeCount?: number
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Drain any leftover bytes from stdin (e.g., late-arriving escape sequence responses) */
|
|
27
|
+
async function drainStdin(): Promise<void> {
|
|
28
|
+
return new Promise((resolve) => {
|
|
29
|
+
if (!process.stdin.readable) { resolve(); return }
|
|
30
|
+
process.stdin.resume()
|
|
31
|
+
const timer = setTimeout(() => {
|
|
32
|
+
process.stdin.pause()
|
|
33
|
+
process.stdin.removeAllListeners("readable")
|
|
34
|
+
resolve()
|
|
35
|
+
}, 200)
|
|
36
|
+
process.stdin.on("readable", () => {
|
|
37
|
+
while (process.stdin.read() !== null) {} // discard
|
|
38
|
+
})
|
|
39
|
+
timer.unref()
|
|
40
|
+
})
|
|
22
41
|
}
|
|
23
42
|
|
|
24
43
|
async function prompt(question: string, defaultValue?: string): Promise<string> {
|
|
44
|
+
await drainStdin()
|
|
25
45
|
const rl = createInterface({ input: process.stdin, output: process.stdout })
|
|
26
46
|
const suffix = defaultValue ? ` [${defaultValue}]` : ""
|
|
27
47
|
return new Promise((resolve) => {
|
|
@@ -71,6 +91,8 @@ export async function submitResults(data: SubmitData): Promise<string | null> {
|
|
|
71
91
|
| Version | ${data.terminalVersion || "unknown"} |
|
|
72
92
|
| OS | ${data.os} ${data.osVersion || ""} |
|
|
73
93
|
| Score | ${passed}/${total} (${pct}%) |
|
|
94
|
+
| CLI Version | ${data.cliVersion ?? "unknown"} |
|
|
95
|
+
| Probes | ${data.probeCount ?? total} |
|
|
74
96
|
| Generated | ${data.generated} |
|
|
75
97
|
|
|
76
98
|
### Summary
|