terminfo.dev 0.7.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 +4 -1
- package/src/index.ts +107 -85
- package/src/report.tsx +113 -0
- package/src/submit.ts +41 -53
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "terminfo.dev",
|
|
3
|
-
"version": "
|
|
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",
|
|
@@ -32,6 +32,9 @@
|
|
|
32
32
|
"publishConfig": {
|
|
33
33
|
"access": "public"
|
|
34
34
|
},
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"commander": "^14.0.0"
|
|
37
|
+
},
|
|
35
38
|
"engines": {
|
|
36
39
|
"node": ">=23.6.0"
|
|
37
40
|
}
|
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,60 +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
|
-
/** OSC 8 hyperlink
|
|
46
|
+
/** OSC 8 hyperlink */
|
|
48
47
|
function link(url: string, text: string): string {
|
|
49
48
|
return `\x1b]8;;${url}\x07${text}\x1b]8;;\x07`
|
|
50
49
|
}
|
|
51
50
|
|
|
52
|
-
interface
|
|
53
|
-
terminal:
|
|
54
|
-
terminalVersion: string
|
|
55
|
-
os: string
|
|
56
|
-
osVersion: string
|
|
57
|
-
source: "community"
|
|
58
|
-
generated: string
|
|
51
|
+
interface ProbeResults {
|
|
52
|
+
terminal: ReturnType<typeof detectTerminal>
|
|
59
53
|
results: Record<string, boolean>
|
|
60
54
|
notes: Record<string, string>
|
|
61
55
|
responses: Record<string, string>
|
|
56
|
+
passed: number
|
|
57
|
+
total: number
|
|
62
58
|
}
|
|
63
59
|
|
|
64
|
-
async function
|
|
65
|
-
const args = process.argv.slice(2)
|
|
66
|
-
const jsonMode = args.includes("--json")
|
|
67
|
-
const submitMode = args.includes("--submit")
|
|
68
|
-
|
|
69
|
-
// Detect terminal
|
|
60
|
+
async function runProbes(): Promise<ProbeResults> {
|
|
70
61
|
const terminal = detectTerminal()
|
|
71
|
-
|
|
72
|
-
if (!jsonMode) {
|
|
73
|
-
console.log(`\x1b[1mterminfo.dev\x1b[0m — can your terminal do that?\n`)
|
|
74
|
-
console.log(` Terminal: \x1b[1m${terminal.name}\x1b[0m${terminal.version ? ` ${terminal.version}` : ""}`)
|
|
75
|
-
console.log(` Platform: ${terminal.os}${terminal.osVersion ? ` ${terminal.osVersion}` : ""}`)
|
|
76
|
-
console.log(` Probes: ${ALL_PROBES.length} features across ${new Set(ALL_PROBES.map(p => p.id.split(".")[0])).size} categories`)
|
|
77
|
-
console.log(` Website: https://terminfo.dev`)
|
|
78
|
-
console.log(``)
|
|
79
|
-
console.log(`\x1b[2mResults are compared against ${ALL_PROBES.length} terminal features from the`)
|
|
80
|
-
console.log(`ECMA-48, VT100/VT510, xterm, and Kitty specifications.`)
|
|
81
|
-
console.log(`Run with --submit to contribute your results to the database.\x1b[0m\n`)
|
|
82
|
-
console.log(`Running probes...`)
|
|
83
|
-
}
|
|
84
|
-
|
|
85
62
|
const results: Record<string, boolean> = {}
|
|
86
63
|
const notes: Record<string, string> = {}
|
|
87
64
|
const responses: Record<string, string> = {}
|
|
88
65
|
let passed = 0
|
|
89
|
-
let failed = 0
|
|
90
66
|
|
|
91
67
|
await withRawMode(async () => {
|
|
92
|
-
|
|
93
|
-
process.stdout.write("\x1b[?1049h") // alt screen
|
|
94
|
-
process.stdout.write("\x1b[2J\x1b[H") // clear + home
|
|
95
|
-
|
|
68
|
+
process.stdout.write("\x1b[?1049h\x1b[2J\x1b[H")
|
|
96
69
|
for (const probe of ALL_PROBES) {
|
|
97
|
-
// Clear screen before each probe to prevent leaking output
|
|
98
70
|
process.stdout.write("\x1b[2J\x1b[H")
|
|
99
71
|
try {
|
|
100
72
|
const result = await probe.run()
|
|
@@ -102,43 +74,34 @@ async function main() {
|
|
|
102
74
|
if (result.note) notes[probe.id] = result.note
|
|
103
75
|
if (result.response) responses[probe.id] = result.response
|
|
104
76
|
if (result.pass) passed++
|
|
105
|
-
else failed++
|
|
106
77
|
} catch (err) {
|
|
107
78
|
results[probe.id] = false
|
|
108
79
|
notes[probe.id] = `error: ${err instanceof Error ? err.message : String(err)}`
|
|
109
|
-
failed++
|
|
110
80
|
}
|
|
111
81
|
}
|
|
112
|
-
|
|
113
|
-
// Exit alt screen while still in raw mode
|
|
114
82
|
process.stdout.write("\x1b[?1049l")
|
|
115
83
|
})
|
|
116
84
|
|
|
117
|
-
|
|
118
|
-
|
|
85
|
+
return { terminal, results, notes, responses, passed, total: ALL_PROBES.length }
|
|
86
|
+
}
|
|
119
87
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
notes,
|
|
129
|
-
responses,
|
|
130
|
-
}
|
|
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
|
+
}
|
|
131
96
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
97
|
+
function printResults(data: ProbeResults) {
|
|
98
|
+
const { passed, total } = data
|
|
99
|
+
const pct = Math.round((passed / total) * 100)
|
|
100
|
+
const slugs = loadFeatureSlugs()
|
|
136
101
|
|
|
137
|
-
|
|
138
|
-
console.log(
|
|
102
|
+
printHeader(data.terminal)
|
|
103
|
+
console.log(` Score: \x1b[1m${passed}/${total} (${pct}%)\x1b[0m\n`)
|
|
139
104
|
|
|
140
|
-
// Show categories with OSC 8 hyperlinks
|
|
141
|
-
const slugs = loadFeatureSlugs()
|
|
142
105
|
const categories = new Map<string, Array<{ id: string; name: string; pass: boolean; note?: string }>>()
|
|
143
106
|
for (const probe of ALL_PROBES) {
|
|
144
107
|
const cat = probe.id.split(".")[0]!
|
|
@@ -146,13 +109,13 @@ async function main() {
|
|
|
146
109
|
categories.get(cat)!.push({
|
|
147
110
|
id: probe.id,
|
|
148
111
|
name: probe.name,
|
|
149
|
-
pass: results[probe.id] ?? false,
|
|
150
|
-
note: notes[probe.id],
|
|
112
|
+
pass: data.results[probe.id] ?? false,
|
|
113
|
+
note: data.notes[probe.id],
|
|
151
114
|
})
|
|
152
115
|
}
|
|
153
116
|
|
|
154
117
|
for (const [cat, probes] of categories) {
|
|
155
|
-
const catPassed = probes.filter(
|
|
118
|
+
const catPassed = probes.filter(p => p.pass).length
|
|
156
119
|
const color = catPassed === probes.length ? "\x1b[32m" : catPassed > 0 ? "\x1b[33m" : "\x1b[31m"
|
|
157
120
|
const catLink = link(`https://terminfo.dev/${cat}`, cat)
|
|
158
121
|
console.log(`${color}${catLink}\x1b[0m (${catPassed}/${probes.length})`)
|
|
@@ -160,25 +123,84 @@ async function main() {
|
|
|
160
123
|
const icon = p.pass ? "\x1b[32m✓\x1b[0m" : "\x1b[31m✗\x1b[0m"
|
|
161
124
|
const note = p.note ? ` \x1b[2m— ${p.note}\x1b[0m` : ""
|
|
162
125
|
const slug = slugs[p.id] ?? p.id.replaceAll(".", "-")
|
|
163
|
-
const
|
|
164
|
-
const featureLink = link(`https://terminfo.dev/${
|
|
126
|
+
const fCat = p.id.split(".")[0]!
|
|
127
|
+
const featureLink = link(`https://terminfo.dev/${fCat}/${slug}`, p.name)
|
|
165
128
|
console.log(` ${icon} ${featureLink}${note}`)
|
|
166
129
|
}
|
|
167
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)
|
|
163
|
+
})
|
|
164
|
+
|
|
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)
|
|
168
171
|
|
|
169
|
-
if (submitMode) {
|
|
170
172
|
console.log(`\nSubmitting results to terminfo.dev...`)
|
|
171
|
-
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
|
+
})
|
|
172
183
|
if (url) {
|
|
173
|
-
console.log(`\x1b[32m✓ Issue created:\x1b[0m ${url}`)
|
|
184
|
+
console.log(`\x1b[32m✓ Issue created:\x1b[0m ${link(url, url)}`)
|
|
174
185
|
}
|
|
175
|
-
}
|
|
176
|
-
console.log(`\n\x1b[2mSubmit results to terminfo.dev: npx terminfo --submit\x1b[0m`)
|
|
177
|
-
console.log(`\x1b[2mJSON output: npx terminfo --json\x1b[0m`)
|
|
178
|
-
}
|
|
179
|
-
}
|
|
186
|
+
})
|
|
180
187
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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`)
|
|
184
204
|
})
|
|
205
|
+
|
|
206
|
+
program.parse()
|
package/src/report.tsx
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Silvery-rendered CLI report for terminfo probe results.
|
|
3
|
+
*/
|
|
4
|
+
import React from "react"
|
|
5
|
+
import { Box, Text } from "silvery"
|
|
6
|
+
import { renderString } from "silvery"
|
|
7
|
+
|
|
8
|
+
interface ProbeResult {
|
|
9
|
+
id: string
|
|
10
|
+
name: string
|
|
11
|
+
pass: boolean
|
|
12
|
+
note?: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface ReportProps {
|
|
16
|
+
terminal: string
|
|
17
|
+
terminalVersion: string
|
|
18
|
+
os: string
|
|
19
|
+
osVersion: string
|
|
20
|
+
probeCount: number
|
|
21
|
+
categoryCount: number
|
|
22
|
+
passed: number
|
|
23
|
+
total: number
|
|
24
|
+
categories: Map<string, ProbeResult[]>
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** OSC 8 hyperlink wrapper */
|
|
28
|
+
function osc8(url: string, text: string): string {
|
|
29
|
+
return `\x1b]8;;${url}\x07${text}\x1b]8;;\x07`
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function featureUrl(id: string, slug: string): string {
|
|
33
|
+
const cat = id.split(".")[0]!
|
|
34
|
+
return `https://terminfo.dev/${cat}/${slug}`
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function Header({ terminal, terminalVersion, os, osVersion, probeCount, categoryCount, passed, total }: ReportProps) {
|
|
38
|
+
const pct = Math.round((passed / total) * 100)
|
|
39
|
+
return (
|
|
40
|
+
<Box flexDirection="column" marginBottom={1}>
|
|
41
|
+
<Text bold>{osc8("https://terminfo.dev", "terminfo.dev")}</Text>
|
|
42
|
+
<Text dimColor>Can your terminal do that?</Text>
|
|
43
|
+
<Text> </Text>
|
|
44
|
+
<Box flexDirection="row" gap={2}>
|
|
45
|
+
<Box flexDirection="column" width={12}>
|
|
46
|
+
<Text dimColor>Terminal</Text>
|
|
47
|
+
<Text dimColor>Platform</Text>
|
|
48
|
+
<Text dimColor>Probes</Text>
|
|
49
|
+
<Text dimColor>Score</Text>
|
|
50
|
+
</Box>
|
|
51
|
+
<Box flexDirection="column">
|
|
52
|
+
<Text bold>{terminal}{terminalVersion ? ` ${terminalVersion}` : ""}</Text>
|
|
53
|
+
<Text>{os} {osVersion}</Text>
|
|
54
|
+
<Text>{probeCount} features, {categoryCount} categories</Text>
|
|
55
|
+
<Text bold color={pct === 100 ? "green" : pct >= 90 ? "yellow" : "red"}>{passed}/{total} ({pct}%)</Text>
|
|
56
|
+
</Box>
|
|
57
|
+
</Box>
|
|
58
|
+
</Box>
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function CategorySection({ name, probes, slugs }: { name: string; probes: ProbeResult[]; slugs: Record<string, string> }) {
|
|
63
|
+
const catPassed = probes.filter(p => p.pass).length
|
|
64
|
+
const allPassed = catPassed === probes.length
|
|
65
|
+
const catUrl = `https://terminfo.dev/${name}`
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<Box flexDirection="column">
|
|
69
|
+
<Text color={allPassed ? "green" : catPassed > 0 ? "yellow" : "red"}>
|
|
70
|
+
{osc8(catUrl, name)} ({catPassed}/{probes.length})
|
|
71
|
+
</Text>
|
|
72
|
+
{probes.map(p => {
|
|
73
|
+
const slug = slugs[p.id] ?? p.id.replaceAll(".", "-")
|
|
74
|
+
const url = featureUrl(p.id, slug)
|
|
75
|
+
const icon = p.pass ? "✓" : "✗"
|
|
76
|
+
return (
|
|
77
|
+
<Box key={p.id} flexDirection="row" paddingLeft={2}>
|
|
78
|
+
<Text color={p.pass ? "green" : "red"}>{icon} </Text>
|
|
79
|
+
<Text>{osc8(url, p.name)}</Text>
|
|
80
|
+
{p.note && <Text dimColor> — {p.note}</Text>}
|
|
81
|
+
</Box>
|
|
82
|
+
)
|
|
83
|
+
})}
|
|
84
|
+
</Box>
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function Footer({ submitMode }: { submitMode: boolean }) {
|
|
89
|
+
if (submitMode) return null
|
|
90
|
+
return (
|
|
91
|
+
<Box flexDirection="column" marginTop={1}>
|
|
92
|
+
<Text dimColor>Submit: npx terminfo.dev --submit</Text>
|
|
93
|
+
<Text dimColor>JSON: npx terminfo.dev --json</Text>
|
|
94
|
+
</Box>
|
|
95
|
+
)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function Report(props: ReportProps & { slugs: Record<string, string>; submitMode: boolean }) {
|
|
99
|
+
return (
|
|
100
|
+
<Box flexDirection="column">
|
|
101
|
+
<Header {...props} />
|
|
102
|
+
{[...props.categories.entries()].map(([name, probes]) => (
|
|
103
|
+
<CategorySection key={name} name={name} probes={probes} slugs={props.slugs} />
|
|
104
|
+
))}
|
|
105
|
+
<Footer submitMode={props.submitMode} />
|
|
106
|
+
</Box>
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export async function renderReport(props: ReportProps & { slugs: Record<string, string>; submitMode: boolean }): Promise<string> {
|
|
111
|
+
const width = process.stdout.columns ?? 80
|
|
112
|
+
return renderString(<Report {...props} />, { width })
|
|
113
|
+
}
|
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
|
}
|