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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "terminfo.dev",
3
- "version": "1.2.0",
3
+ "version": "1.4.0",
4
4
  "description": "Test your terminal's feature support and contribute to terminfo.dev",
5
5
  "keywords": [
6
6
  "ansi",
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, DA2 response parsing, and fallback heuristics.
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; versionEnv?: 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
- return { name, version: "", os, osVersion }
58
+ name = n
59
+ break
44
60
  }
45
61
  }
46
62
 
47
63
  // Check $TERM_PROGRAM
48
- const termProgram = process.env.TERM_PROGRAM
49
- if (termProgram) {
50
- const name = TERM_PROGRAM_MAP[termProgram] ?? termProgram.toLowerCase()
51
- const version = process.env.TERM_PROGRAM_VERSION ?? ""
52
- return { name, version, os, osVersion }
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
- // Check $TERMINAL_EMULATOR (set by some Linux terminals)
56
- const termEmu = process.env.TERMINAL_EMULATOR
57
- if (termEmu) {
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
- // Fallback: use $TERM
62
- const term = process.env.TERM ?? "unknown"
63
- return { name: term, version: "", os, osVersion }
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
- return "macos"
70
- case "linux":
71
- return "linux"
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