terminfo.dev 1.0.0 → 1.1.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 +2 -2
- package/src/index.ts +122 -78
- package/src/submit.ts +41 -53
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "terminfo.dev",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Test your terminal's feature support and contribute to terminfo.dev",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ansi",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"access": "public"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"
|
|
36
|
+
"commander": "^14.0.0"
|
|
37
37
|
},
|
|
38
38
|
"engines": {
|
|
39
39
|
"node": ">=23.6.0"
|
package/src/index.ts
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
/**
|
|
3
|
-
* terminfo CLI —
|
|
3
|
+
* terminfo.dev CLI — can your terminal do that?
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
* shows results, and optionally submits them to terminfo.dev.
|
|
5
|
+
* Test your terminal's feature support and contribute results to terminfo.dev.
|
|
7
6
|
*
|
|
8
7
|
* @example
|
|
9
8
|
* ```bash
|
|
10
|
-
* npx terminfo
|
|
11
|
-
* npx terminfo
|
|
12
|
-
* npx terminfo
|
|
9
|
+
* npx terminfo.dev # Show terminal info + help
|
|
10
|
+
* npx terminfo.dev probe # Run all probes
|
|
11
|
+
* npx terminfo.dev submit # Run probes + submit results
|
|
13
12
|
* ```
|
|
14
13
|
*/
|
|
15
14
|
|
|
15
|
+
import { Command } from "commander"
|
|
16
16
|
import { readFileSync } from "node:fs"
|
|
17
17
|
import { dirname, join } from "node:path"
|
|
18
18
|
import { fileURLToPath } from "node:url"
|
|
@@ -25,10 +25,9 @@ const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
|
25
25
|
|
|
26
26
|
/** Load feature slugs from features.json for OSC 8 hyperlinks */
|
|
27
27
|
function loadFeatureSlugs(): Record<string, string> {
|
|
28
|
-
// Try repo-local path first, then npm-installed path
|
|
29
28
|
const candidates = [
|
|
30
|
-
join(__dirname, "..", "..", "features.json"),
|
|
31
|
-
join(__dirname, "..", "..", "..", "features.json"),
|
|
29
|
+
join(__dirname, "..", "..", "features.json"),
|
|
30
|
+
join(__dirname, "..", "..", "..", "features.json"),
|
|
32
31
|
]
|
|
33
32
|
for (const path of candidates) {
|
|
34
33
|
try {
|
|
@@ -41,42 +40,33 @@ function loadFeatureSlugs(): Record<string, string> {
|
|
|
41
40
|
return slugs
|
|
42
41
|
} catch {}
|
|
43
42
|
}
|
|
44
|
-
return {}
|
|
43
|
+
return {}
|
|
45
44
|
}
|
|
46
45
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
46
|
+
/** OSC 8 hyperlink */
|
|
47
|
+
function link(url: string, text: string): string {
|
|
48
|
+
return `\x1b]8;;${url}\x07${text}\x1b]8;;\x07`
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
interface ProbeResults {
|
|
52
|
+
terminal: ReturnType<typeof detectTerminal>
|
|
54
53
|
results: Record<string, boolean>
|
|
55
54
|
notes: Record<string, string>
|
|
56
55
|
responses: Record<string, string>
|
|
56
|
+
passed: number
|
|
57
|
+
total: number
|
|
57
58
|
}
|
|
58
59
|
|
|
59
|
-
async function
|
|
60
|
-
const args = process.argv.slice(2)
|
|
61
|
-
const jsonMode = args.includes("--json")
|
|
62
|
-
const submitMode = args.includes("--submit")
|
|
63
|
-
|
|
64
|
-
// Detect terminal
|
|
60
|
+
async function runProbes(): Promise<ProbeResults> {
|
|
65
61
|
const terminal = detectTerminal()
|
|
66
|
-
|
|
67
62
|
const results: Record<string, boolean> = {}
|
|
68
63
|
const notes: Record<string, string> = {}
|
|
69
64
|
const responses: Record<string, string> = {}
|
|
70
65
|
let passed = 0
|
|
71
|
-
let failed = 0
|
|
72
66
|
|
|
73
67
|
await withRawMode(async () => {
|
|
74
|
-
|
|
75
|
-
process.stdout.write("\x1b[?1049h") // alt screen
|
|
76
|
-
process.stdout.write("\x1b[2J\x1b[H") // clear + home
|
|
77
|
-
|
|
68
|
+
process.stdout.write("\x1b[?1049h\x1b[2J\x1b[H")
|
|
78
69
|
for (const probe of ALL_PROBES) {
|
|
79
|
-
// Clear screen before each probe to prevent leaking output
|
|
80
70
|
process.stdout.write("\x1b[2J\x1b[H")
|
|
81
71
|
try {
|
|
82
72
|
const result = await probe.run()
|
|
@@ -84,40 +74,34 @@ async function main() {
|
|
|
84
74
|
if (result.note) notes[probe.id] = result.note
|
|
85
75
|
if (result.response) responses[probe.id] = result.response
|
|
86
76
|
if (result.pass) passed++
|
|
87
|
-
else failed++
|
|
88
77
|
} catch (err) {
|
|
89
78
|
results[probe.id] = false
|
|
90
79
|
notes[probe.id] = `error: ${err instanceof Error ? err.message : String(err)}`
|
|
91
|
-
failed++
|
|
92
80
|
}
|
|
93
81
|
}
|
|
94
|
-
|
|
95
|
-
// Exit alt screen while still in raw mode
|
|
96
82
|
process.stdout.write("\x1b[?1049l")
|
|
97
83
|
})
|
|
98
84
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
const entry: ResultEntry = {
|
|
103
|
-
terminal: terminal.name,
|
|
104
|
-
terminalVersion: terminal.version,
|
|
105
|
-
os: terminal.os,
|
|
106
|
-
osVersion: terminal.osVersion,
|
|
107
|
-
source: "community",
|
|
108
|
-
generated: new Date().toISOString(),
|
|
109
|
-
results,
|
|
110
|
-
notes,
|
|
111
|
-
responses,
|
|
112
|
-
}
|
|
85
|
+
return { terminal, results, notes, responses, passed, total: ALL_PROBES.length }
|
|
86
|
+
}
|
|
113
87
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
}
|
|
88
|
+
function printHeader(terminal: ReturnType<typeof detectTerminal>) {
|
|
89
|
+
const siteLink = link("https://terminfo.dev", "terminfo.dev")
|
|
90
|
+
console.log(`\x1b[1m${siteLink}\x1b[0m — can your terminal do that?\n`)
|
|
91
|
+
console.log(` Terminal: \x1b[1m${terminal.name}\x1b[0m${terminal.version ? ` ${terminal.version}` : ""}`)
|
|
92
|
+
console.log(` Platform: ${terminal.os} ${terminal.osVersion}`)
|
|
93
|
+
console.log(` Probes: ${ALL_PROBES.length} features across ${new Set(ALL_PROBES.map(p => p.id.split(".")[0])).size} categories`)
|
|
94
|
+
console.log(` Website: ${link("https://terminfo.dev", "https://terminfo.dev")}`)
|
|
95
|
+
}
|
|
118
96
|
|
|
119
|
-
|
|
97
|
+
function printResults(data: ProbeResults) {
|
|
98
|
+
const { passed, total } = data
|
|
99
|
+
const pct = Math.round((passed / total) * 100)
|
|
120
100
|
const slugs = loadFeatureSlugs()
|
|
101
|
+
|
|
102
|
+
printHeader(data.terminal)
|
|
103
|
+
console.log(` Score: \x1b[1m${passed}/${total} (${pct}%)\x1b[0m\n`)
|
|
104
|
+
|
|
121
105
|
const categories = new Map<string, Array<{ id: string; name: string; pass: boolean; note?: string }>>()
|
|
122
106
|
for (const probe of ALL_PROBES) {
|
|
123
107
|
const cat = probe.id.split(".")[0]!
|
|
@@ -125,38 +109,98 @@ async function main() {
|
|
|
125
109
|
categories.get(cat)!.push({
|
|
126
110
|
id: probe.id,
|
|
127
111
|
name: probe.name,
|
|
128
|
-
pass: results[probe.id] ?? false,
|
|
129
|
-
note: notes[probe.id],
|
|
112
|
+
pass: data.results[probe.id] ?? false,
|
|
113
|
+
note: data.notes[probe.id],
|
|
130
114
|
})
|
|
131
115
|
}
|
|
132
116
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
117
|
+
for (const [cat, probes] of categories) {
|
|
118
|
+
const catPassed = probes.filter(p => p.pass).length
|
|
119
|
+
const color = catPassed === probes.length ? "\x1b[32m" : catPassed > 0 ? "\x1b[33m" : "\x1b[31m"
|
|
120
|
+
const catLink = link(`https://terminfo.dev/${cat}`, cat)
|
|
121
|
+
console.log(`${color}${catLink}\x1b[0m (${catPassed}/${probes.length})`)
|
|
122
|
+
for (const p of probes) {
|
|
123
|
+
const icon = p.pass ? "\x1b[32m✓\x1b[0m" : "\x1b[31m✗\x1b[0m"
|
|
124
|
+
const note = p.note ? ` \x1b[2m— ${p.note}\x1b[0m` : ""
|
|
125
|
+
const slug = slugs[p.id] ?? p.id.replaceAll(".", "-")
|
|
126
|
+
const fCat = p.id.split(".")[0]!
|
|
127
|
+
const featureLink = link(`https://terminfo.dev/${fCat}/${slug}`, p.name)
|
|
128
|
+
console.log(` ${icon} ${featureLink}${note}`)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ── CLI ──
|
|
134
|
+
|
|
135
|
+
const program = new Command()
|
|
136
|
+
.name("terminfo.dev")
|
|
137
|
+
.description("Can your terminal do that? — test terminal feature support and contribute to terminfo.dev")
|
|
138
|
+
.version("1.1.0")
|
|
139
|
+
|
|
140
|
+
program
|
|
141
|
+
.command("probe")
|
|
142
|
+
.description("Run all probes against your terminal and display results")
|
|
143
|
+
.option("--json", "Output results as JSON")
|
|
144
|
+
.action(async (opts) => {
|
|
145
|
+
const data = await runProbes()
|
|
146
|
+
|
|
147
|
+
if (opts.json) {
|
|
148
|
+
console.log(JSON.stringify({
|
|
149
|
+
terminal: data.terminal.name,
|
|
150
|
+
terminalVersion: data.terminal.version,
|
|
151
|
+
os: data.terminal.os,
|
|
152
|
+
osVersion: data.terminal.osVersion,
|
|
153
|
+
source: "community",
|
|
154
|
+
generated: new Date().toISOString(),
|
|
155
|
+
results: data.results,
|
|
156
|
+
notes: data.notes,
|
|
157
|
+
responses: data.responses,
|
|
158
|
+
}, null, 2))
|
|
159
|
+
return
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
printResults(data)
|
|
147
163
|
})
|
|
148
|
-
console.log(output)
|
|
149
164
|
|
|
150
|
-
|
|
165
|
+
program
|
|
166
|
+
.command("submit")
|
|
167
|
+
.description("Run all probes and submit results to terminfo.dev via GitHub issue")
|
|
168
|
+
.action(async () => {
|
|
169
|
+
const data = await runProbes()
|
|
170
|
+
printResults(data)
|
|
171
|
+
|
|
151
172
|
console.log(`\nSubmitting results to terminfo.dev...`)
|
|
152
|
-
const url = await submitResults(
|
|
173
|
+
const url = await submitResults({
|
|
174
|
+
terminal: data.terminal.name,
|
|
175
|
+
terminalVersion: data.terminal.version,
|
|
176
|
+
os: data.terminal.os,
|
|
177
|
+
osVersion: data.terminal.osVersion,
|
|
178
|
+
results: data.results,
|
|
179
|
+
notes: data.notes,
|
|
180
|
+
responses: data.responses,
|
|
181
|
+
generated: new Date().toISOString(),
|
|
182
|
+
})
|
|
153
183
|
if (url) {
|
|
154
|
-
console.log(`\x1b[32m✓ Issue created:\x1b[0m ${url}`)
|
|
184
|
+
console.log(`\x1b[32m✓ Issue created:\x1b[0m ${link(url, url)}`)
|
|
155
185
|
}
|
|
156
|
-
}
|
|
157
|
-
}
|
|
186
|
+
})
|
|
158
187
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
188
|
+
// Default action: show terminal info + help
|
|
189
|
+
program.action(() => {
|
|
190
|
+
const terminal = detectTerminal()
|
|
191
|
+
printHeader(terminal)
|
|
192
|
+
console.log(``)
|
|
193
|
+
console.log(`\x1b[2mTest your terminal against ${ALL_PROBES.length} features from the ECMA-48,`)
|
|
194
|
+
console.log(`VT100/VT510, xterm, and Kitty specifications. Results can be`)
|
|
195
|
+
console.log(`submitted to the community database at terminfo.dev.\x1b[0m`)
|
|
196
|
+
console.log(``)
|
|
197
|
+
console.log(`Commands:`)
|
|
198
|
+
console.log(` \x1b[1mprobe\x1b[0m Run all probes and display results`)
|
|
199
|
+
console.log(` \x1b[1msubmit\x1b[0m Run probes and submit to terminfo.dev`)
|
|
200
|
+
console.log(``)
|
|
201
|
+
console.log(`Options:`)
|
|
202
|
+
console.log(` \x1b[1m--json\x1b[0m Output results as JSON (with probe command)`)
|
|
203
|
+
console.log(` \x1b[1m--help\x1b[0m Show this help`)
|
|
162
204
|
})
|
|
205
|
+
|
|
206
|
+
program.parse()
|
package/src/submit.ts
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Submit results to terminfo.dev via GitHub issue.
|
|
3
|
-
*
|
|
4
|
-
* Creates an issue on beorn/terminfo.dev with the JSON results
|
|
5
|
-
* attached as a code block. Maintainers review and merge into
|
|
6
|
-
* the results database.
|
|
7
3
|
*/
|
|
8
4
|
|
|
5
|
+
import { writeFileSync, unlinkSync } from "node:fs"
|
|
6
|
+
import { tmpdir } from "node:os"
|
|
7
|
+
import { join } from "node:path"
|
|
8
|
+
import { execFileSync } from "node:child_process"
|
|
9
|
+
|
|
9
10
|
const REPO = "beorn/terminfo.dev"
|
|
10
|
-
const ISSUE_LABEL = "community-results"
|
|
11
11
|
|
|
12
|
-
interface
|
|
12
|
+
interface SubmitData {
|
|
13
13
|
terminal: string
|
|
14
14
|
terminalVersion: string
|
|
15
15
|
os: string
|
|
@@ -20,11 +20,7 @@ interface SubmitResult {
|
|
|
20
20
|
generated: string
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
* Submit results by creating a GitHub issue.
|
|
25
|
-
* Requires `gh` CLI to be installed and authenticated.
|
|
26
|
-
*/
|
|
27
|
-
export async function submitResults(data: SubmitResult): Promise<string | null> {
|
|
23
|
+
export async function submitResults(data: SubmitData): Promise<string | null> {
|
|
28
24
|
const passed = Object.values(data.results).filter(Boolean).length
|
|
29
25
|
const total = Object.keys(data.results).length
|
|
30
26
|
const pct = Math.round((passed / total) * 100)
|
|
@@ -41,10 +37,12 @@ export async function submitResults(data: SubmitResult): Promise<string | null>
|
|
|
41
37
|
| Score | ${passed}/${total} (${pct}%) |
|
|
42
38
|
| Generated | ${data.generated} |
|
|
43
39
|
|
|
44
|
-
###
|
|
40
|
+
### Summary
|
|
41
|
+
|
|
42
|
+
${formatSummary(data)}
|
|
45
43
|
|
|
46
44
|
<details>
|
|
47
|
-
<summary>Full JSON
|
|
45
|
+
<summary>Full JSON</summary>
|
|
48
46
|
|
|
49
47
|
\`\`\`json
|
|
50
48
|
${JSON.stringify(data, null, 2)}
|
|
@@ -52,55 +50,48 @@ ${JSON.stringify(data, null, 2)}
|
|
|
52
50
|
|
|
53
51
|
</details>
|
|
54
52
|
|
|
55
|
-
### Summary
|
|
56
|
-
|
|
57
|
-
${formatSummary(data)}
|
|
58
|
-
|
|
59
53
|
---
|
|
60
|
-
*Submitted via \`npx terminfo\`*`
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
{ encoding: "utf-8", timeout: 30000 },
|
|
69
|
-
)
|
|
70
|
-
const url = result.trim()
|
|
71
|
-
return url
|
|
72
|
-
} catch (err) {
|
|
73
|
-
console.error(`\x1b[31mFailed to create issue via gh CLI\x1b[0m`)
|
|
74
|
-
console.error(err instanceof Error ? err.message : String(err))
|
|
75
|
-
return null
|
|
76
|
-
}
|
|
54
|
+
*Submitted via \`npx terminfo.dev\`*`
|
|
55
|
+
|
|
56
|
+
if (!hasGhCli()) {
|
|
57
|
+
const filename = `terminfo-${data.terminal}-${data.os}-${Date.now()}.json`
|
|
58
|
+
writeFileSync(filename, JSON.stringify(data, null, 2))
|
|
59
|
+
console.log(`\n\x1b[33mgh CLI not found. Results saved to ${filename}\x1b[0m`)
|
|
60
|
+
console.log(`To submit: https://github.com/${REPO}/issues/new`)
|
|
61
|
+
return null
|
|
77
62
|
}
|
|
78
63
|
|
|
79
|
-
//
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
64
|
+
// Write body to temp file to avoid shell escaping issues
|
|
65
|
+
const bodyFile = join(tmpdir(), `terminfo-submit-${Date.now()}.md`)
|
|
66
|
+
try {
|
|
67
|
+
writeFileSync(bodyFile, body)
|
|
68
|
+
const result = execFileSync("gh", [
|
|
69
|
+
"issue", "create",
|
|
70
|
+
"--repo", REPO,
|
|
71
|
+
"--title", title,
|
|
72
|
+
"--body-file", bodyFile,
|
|
73
|
+
], { encoding: "utf-8", timeout: 30000 })
|
|
74
|
+
return result.trim()
|
|
75
|
+
} catch (err) {
|
|
76
|
+
console.error(`\x1b[31mFailed to create issue\x1b[0m`)
|
|
77
|
+
console.error(err instanceof Error ? err.message : String(err))
|
|
78
|
+
return null
|
|
79
|
+
} finally {
|
|
80
|
+
try { unlinkSync(bodyFile) } catch {}
|
|
81
|
+
}
|
|
89
82
|
}
|
|
90
83
|
|
|
91
|
-
|
|
84
|
+
function hasGhCli(): boolean {
|
|
92
85
|
try {
|
|
93
|
-
|
|
94
|
-
execSync("gh --version", { stdio: "ignore", timeout: 5000 })
|
|
86
|
+
execFileSync("gh", ["--version"], { stdio: "ignore", timeout: 5000 })
|
|
95
87
|
return true
|
|
96
88
|
} catch {
|
|
97
89
|
return false
|
|
98
90
|
}
|
|
99
91
|
}
|
|
100
92
|
|
|
101
|
-
function formatSummary(data:
|
|
93
|
+
function formatSummary(data: SubmitData): string {
|
|
102
94
|
const categories = new Map<string, { pass: number; fail: number; failList: string[] }>()
|
|
103
|
-
|
|
104
95
|
for (const [id, pass] of Object.entries(data.results)) {
|
|
105
96
|
const cat = id.split(".")[0]!
|
|
106
97
|
if (!categories.has(cat)) categories.set(cat, { pass: 0, fail: 0, failList: [] })
|
|
@@ -112,14 +103,11 @@ function formatSummary(data: SubmitResult): string {
|
|
|
112
103
|
entry.failList.push(note ? `- \`${id}\`: ${note}` : `- \`${id}\``)
|
|
113
104
|
}
|
|
114
105
|
}
|
|
115
|
-
|
|
116
106
|
const lines: string[] = []
|
|
117
107
|
for (const [cat, { pass, fail, failList }] of categories) {
|
|
118
|
-
const total = pass + fail
|
|
119
108
|
const icon = fail === 0 ? "✅" : "⚠️"
|
|
120
|
-
lines.push(`${icon} **${cat}**: ${pass}/${
|
|
109
|
+
lines.push(`${icon} **${cat}**: ${pass}/${pass + fail}`)
|
|
121
110
|
if (failList.length > 0) lines.push(...failList)
|
|
122
111
|
}
|
|
123
|
-
|
|
124
112
|
return lines.join("\n")
|
|
125
113
|
}
|