terminfo.dev 1.0.0 → 1.2.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 +130 -80
- package/src/report.tsx +28 -9
- package/src/submit.ts +95 -53
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "terminfo.dev",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.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,11 +25,7 @@ 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
|
-
|
|
29
|
-
const candidates = [
|
|
30
|
-
join(__dirname, "..", "..", "features.json"), // repo: cli/src/ -> features.json
|
|
31
|
-
join(__dirname, "..", "..", "..", "features.json"), // npm: node_modules/terminfo.dev/src/ -> features.json
|
|
32
|
-
]
|
|
28
|
+
const candidates = [join(__dirname, "..", "..", "features.json"), join(__dirname, "..", "..", "..", "features.json")]
|
|
33
29
|
for (const path of candidates) {
|
|
34
30
|
try {
|
|
35
31
|
const raw = JSON.parse(readFileSync(path, "utf-8"))
|
|
@@ -41,42 +37,33 @@ function loadFeatureSlugs(): Record<string, string> {
|
|
|
41
37
|
return slugs
|
|
42
38
|
} catch {}
|
|
43
39
|
}
|
|
44
|
-
return {}
|
|
40
|
+
return {}
|
|
45
41
|
}
|
|
46
42
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
43
|
+
/** OSC 8 hyperlink */
|
|
44
|
+
function link(url: string, text: string): string {
|
|
45
|
+
return `\x1b]8;;${url}\x07${text}\x1b]8;;\x07`
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
interface ProbeResults {
|
|
49
|
+
terminal: ReturnType<typeof detectTerminal>
|
|
54
50
|
results: Record<string, boolean>
|
|
55
51
|
notes: Record<string, string>
|
|
56
52
|
responses: Record<string, string>
|
|
53
|
+
passed: number
|
|
54
|
+
total: number
|
|
57
55
|
}
|
|
58
56
|
|
|
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
|
|
57
|
+
async function runProbes(): Promise<ProbeResults> {
|
|
65
58
|
const terminal = detectTerminal()
|
|
66
|
-
|
|
67
59
|
const results: Record<string, boolean> = {}
|
|
68
60
|
const notes: Record<string, string> = {}
|
|
69
61
|
const responses: Record<string, string> = {}
|
|
70
62
|
let passed = 0
|
|
71
|
-
let failed = 0
|
|
72
63
|
|
|
73
64
|
await withRawMode(async () => {
|
|
74
|
-
|
|
75
|
-
process.stdout.write("\x1b[?1049h") // alt screen
|
|
76
|
-
process.stdout.write("\x1b[2J\x1b[H") // clear + home
|
|
77
|
-
|
|
65
|
+
process.stdout.write("\x1b[?1049h\x1b[2J\x1b[H")
|
|
78
66
|
for (const probe of ALL_PROBES) {
|
|
79
|
-
// Clear screen before each probe to prevent leaking output
|
|
80
67
|
process.stdout.write("\x1b[2J\x1b[H")
|
|
81
68
|
try {
|
|
82
69
|
const result = await probe.run()
|
|
@@ -84,40 +71,36 @@ async function main() {
|
|
|
84
71
|
if (result.note) notes[probe.id] = result.note
|
|
85
72
|
if (result.response) responses[probe.id] = result.response
|
|
86
73
|
if (result.pass) passed++
|
|
87
|
-
else failed++
|
|
88
74
|
} catch (err) {
|
|
89
75
|
results[probe.id] = false
|
|
90
76
|
notes[probe.id] = `error: ${err instanceof Error ? err.message : String(err)}`
|
|
91
|
-
failed++
|
|
92
77
|
}
|
|
93
78
|
}
|
|
94
|
-
|
|
95
|
-
// Exit alt screen while still in raw mode
|
|
96
79
|
process.stdout.write("\x1b[?1049l")
|
|
97
80
|
})
|
|
98
81
|
|
|
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
|
-
}
|
|
82
|
+
return { terminal, results, notes, responses, passed, total: ALL_PROBES.length }
|
|
83
|
+
}
|
|
113
84
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
}
|
|
85
|
+
function printHeader(terminal: ReturnType<typeof detectTerminal>) {
|
|
86
|
+
const siteLink = link("https://terminfo.dev", "terminfo.dev")
|
|
87
|
+
console.log(`\x1b[1m${siteLink}\x1b[0m — can your terminal do that?\n`)
|
|
88
|
+
console.log(` Terminal: \x1b[1m${terminal.name}\x1b[0m${terminal.version ? ` ${terminal.version}` : ""}`)
|
|
89
|
+
console.log(` Platform: ${terminal.os} ${terminal.osVersion}`)
|
|
90
|
+
console.log(
|
|
91
|
+
` Probes: ${ALL_PROBES.length} features across ${new Set(ALL_PROBES.map((p) => p.id.split(".")[0])).size} categories`,
|
|
92
|
+
)
|
|
93
|
+
console.log(` Website: ${link("https://terminfo.dev", "https://terminfo.dev")}`)
|
|
94
|
+
}
|
|
118
95
|
|
|
119
|
-
|
|
96
|
+
function printResults(data: ProbeResults) {
|
|
97
|
+
const { passed, total } = data
|
|
98
|
+
const pct = Math.round((passed / total) * 100)
|
|
120
99
|
const slugs = loadFeatureSlugs()
|
|
100
|
+
|
|
101
|
+
printHeader(data.terminal)
|
|
102
|
+
console.log(` Score: \x1b[1m${passed}/${total} (${pct}%)\x1b[0m\n`)
|
|
103
|
+
|
|
121
104
|
const categories = new Map<string, Array<{ id: string; name: string; pass: boolean; note?: string }>>()
|
|
122
105
|
for (const probe of ALL_PROBES) {
|
|
123
106
|
const cat = probe.id.split(".")[0]!
|
|
@@ -125,38 +108,105 @@ async function main() {
|
|
|
125
108
|
categories.get(cat)!.push({
|
|
126
109
|
id: probe.id,
|
|
127
110
|
name: probe.name,
|
|
128
|
-
pass: results[probe.id] ?? false,
|
|
129
|
-
note: notes[probe.id],
|
|
111
|
+
pass: data.results[probe.id] ?? false,
|
|
112
|
+
note: data.notes[probe.id],
|
|
130
113
|
})
|
|
131
114
|
}
|
|
132
115
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
116
|
+
for (const [cat, probes] of categories) {
|
|
117
|
+
const catPassed = probes.filter((p) => p.pass).length
|
|
118
|
+
const color = catPassed === probes.length ? "\x1b[32m" : catPassed > 0 ? "\x1b[33m" : "\x1b[31m"
|
|
119
|
+
const catLink = link(`https://terminfo.dev/${cat}`, cat)
|
|
120
|
+
console.log(`${color}${catLink}\x1b[0m (${catPassed}/${probes.length})`)
|
|
121
|
+
for (const p of probes) {
|
|
122
|
+
const icon = p.pass ? "\x1b[32m✓\x1b[0m" : "\x1b[31m✗\x1b[0m"
|
|
123
|
+
const note = p.note ? ` \x1b[2m— ${p.note}\x1b[0m` : ""
|
|
124
|
+
const slug = slugs[p.id] ?? p.id.replaceAll(".", "-")
|
|
125
|
+
const fCat = p.id.split(".")[0]!
|
|
126
|
+
const featureLink = link(`https://terminfo.dev/${fCat}/${slug}`, p.name)
|
|
127
|
+
console.log(` ${icon} ${featureLink}${note}`)
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ── CLI ──
|
|
133
|
+
|
|
134
|
+
const program = new Command()
|
|
135
|
+
.name("terminfo.dev")
|
|
136
|
+
.description("Can your terminal do that? — test terminal feature support and contribute to terminfo.dev")
|
|
137
|
+
.version("1.1.0")
|
|
138
|
+
|
|
139
|
+
program
|
|
140
|
+
.command("probe")
|
|
141
|
+
.description("Run all probes against your terminal and display results")
|
|
142
|
+
.option("--json", "Output results as JSON")
|
|
143
|
+
.action(async (opts) => {
|
|
144
|
+
const data = await runProbes()
|
|
145
|
+
|
|
146
|
+
if (opts.json) {
|
|
147
|
+
console.log(
|
|
148
|
+
JSON.stringify(
|
|
149
|
+
{
|
|
150
|
+
terminal: data.terminal.name,
|
|
151
|
+
terminalVersion: data.terminal.version,
|
|
152
|
+
os: data.terminal.os,
|
|
153
|
+
osVersion: data.terminal.osVersion,
|
|
154
|
+
source: "community",
|
|
155
|
+
generated: new Date().toISOString(),
|
|
156
|
+
results: data.results,
|
|
157
|
+
notes: data.notes,
|
|
158
|
+
responses: data.responses,
|
|
159
|
+
},
|
|
160
|
+
null,
|
|
161
|
+
2,
|
|
162
|
+
),
|
|
163
|
+
)
|
|
164
|
+
return
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
printResults(data)
|
|
168
|
+
console.log(`\n\x1b[2mContribute these results: \x1b[0m\x1b[1mnpx terminfo.dev submit\x1b[0m`)
|
|
147
169
|
})
|
|
148
|
-
console.log(output)
|
|
149
170
|
|
|
150
|
-
|
|
171
|
+
program
|
|
172
|
+
.command("submit")
|
|
173
|
+
.description("Run all probes and submit results to terminfo.dev via GitHub issue")
|
|
174
|
+
.action(async () => {
|
|
175
|
+
const data = await runProbes()
|
|
176
|
+
printResults(data)
|
|
177
|
+
|
|
151
178
|
console.log(`\nSubmitting results to terminfo.dev...`)
|
|
152
|
-
const url = await submitResults(
|
|
179
|
+
const url = await submitResults({
|
|
180
|
+
terminal: data.terminal.name,
|
|
181
|
+
terminalVersion: data.terminal.version,
|
|
182
|
+
os: data.terminal.os,
|
|
183
|
+
osVersion: data.terminal.osVersion,
|
|
184
|
+
results: data.results,
|
|
185
|
+
notes: data.notes,
|
|
186
|
+
responses: data.responses,
|
|
187
|
+
generated: new Date().toISOString(),
|
|
188
|
+
})
|
|
153
189
|
if (url) {
|
|
154
|
-
console.log(`\x1b[32m✓ Issue created:\x1b[0m ${url}`)
|
|
190
|
+
console.log(`\x1b[32m✓ Issue created:\x1b[0m ${link(url, url)}`)
|
|
155
191
|
}
|
|
156
|
-
}
|
|
157
|
-
}
|
|
192
|
+
})
|
|
158
193
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
194
|
+
// Default action: show terminal info + help
|
|
195
|
+
program.action(() => {
|
|
196
|
+
const terminal = detectTerminal()
|
|
197
|
+
printHeader(terminal)
|
|
198
|
+
console.log(``)
|
|
199
|
+
console.log(`\x1b[2mTest your terminal against ${ALL_PROBES.length} features from the ECMA-48,`)
|
|
200
|
+
console.log(`VT100/VT510, xterm, and Kitty specifications. Results can be`)
|
|
201
|
+
console.log(`submitted to the community database at terminfo.dev.\x1b[0m`)
|
|
202
|
+
console.log(``)
|
|
203
|
+
console.log(`Commands:`)
|
|
204
|
+
console.log(` \x1b[1mprobe\x1b[0m Run all probes and display results`)
|
|
205
|
+
console.log(` \x1b[1msubmit\x1b[0m Run probes and submit to terminfo.dev`)
|
|
206
|
+
console.log(``)
|
|
207
|
+
console.log(`Options:`)
|
|
208
|
+
console.log(` \x1b[1m--json\x1b[0m Output results as JSON (with probe command)`)
|
|
209
|
+
console.log(` \x1b[1m--help\x1b[0m Show this help`)
|
|
162
210
|
})
|
|
211
|
+
|
|
212
|
+
program.parse()
|
package/src/report.tsx
CHANGED
|
@@ -49,18 +49,35 @@ function Header({ terminal, terminalVersion, os, osVersion, probeCount, category
|
|
|
49
49
|
<Text dimColor>Score</Text>
|
|
50
50
|
</Box>
|
|
51
51
|
<Box flexDirection="column">
|
|
52
|
-
<Text bold>
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
52
|
+
<Text bold>
|
|
53
|
+
{terminal}
|
|
54
|
+
{terminalVersion ? ` ${terminalVersion}` : ""}
|
|
55
|
+
</Text>
|
|
56
|
+
<Text>
|
|
57
|
+
{os} {osVersion}
|
|
58
|
+
</Text>
|
|
59
|
+
<Text>
|
|
60
|
+
{probeCount} features, {categoryCount} categories
|
|
61
|
+
</Text>
|
|
62
|
+
<Text bold color={pct === 100 ? "green" : pct >= 90 ? "yellow" : "red"}>
|
|
63
|
+
{passed}/{total} ({pct}%)
|
|
64
|
+
</Text>
|
|
56
65
|
</Box>
|
|
57
66
|
</Box>
|
|
58
67
|
</Box>
|
|
59
68
|
)
|
|
60
69
|
}
|
|
61
70
|
|
|
62
|
-
function CategorySection({
|
|
63
|
-
|
|
71
|
+
function CategorySection({
|
|
72
|
+
name,
|
|
73
|
+
probes,
|
|
74
|
+
slugs,
|
|
75
|
+
}: {
|
|
76
|
+
name: string
|
|
77
|
+
probes: ProbeResult[]
|
|
78
|
+
slugs: Record<string, string>
|
|
79
|
+
}) {
|
|
80
|
+
const catPassed = probes.filter((p) => p.pass).length
|
|
64
81
|
const allPassed = catPassed === probes.length
|
|
65
82
|
const catUrl = `https://terminfo.dev/${name}`
|
|
66
83
|
|
|
@@ -69,7 +86,7 @@ function CategorySection({ name, probes, slugs }: { name: string; probes: ProbeR
|
|
|
69
86
|
<Text color={allPassed ? "green" : catPassed > 0 ? "yellow" : "red"}>
|
|
70
87
|
{osc8(catUrl, name)} ({catPassed}/{probes.length})
|
|
71
88
|
</Text>
|
|
72
|
-
{probes.map(p => {
|
|
89
|
+
{probes.map((p) => {
|
|
73
90
|
const slug = slugs[p.id] ?? p.id.replaceAll(".", "-")
|
|
74
91
|
const url = featureUrl(p.id, slug)
|
|
75
92
|
const icon = p.pass ? "✓" : "✗"
|
|
@@ -90,7 +107,7 @@ function Footer({ submitMode }: { submitMode: boolean }) {
|
|
|
90
107
|
return (
|
|
91
108
|
<Box flexDirection="column" marginTop={1}>
|
|
92
109
|
<Text dimColor>Submit: npx terminfo.dev --submit</Text>
|
|
93
|
-
<Text dimColor>JSON:
|
|
110
|
+
<Text dimColor>JSON: npx terminfo.dev --json</Text>
|
|
94
111
|
</Box>
|
|
95
112
|
)
|
|
96
113
|
}
|
|
@@ -107,7 +124,9 @@ function Report(props: ReportProps & { slugs: Record<string, string>; submitMode
|
|
|
107
124
|
)
|
|
108
125
|
}
|
|
109
126
|
|
|
110
|
-
export async function renderReport(
|
|
127
|
+
export async function renderReport(
|
|
128
|
+
props: ReportProps & { slugs: Record<string, string>; submitMode: boolean },
|
|
129
|
+
): Promise<string> {
|
|
111
130
|
const width = process.stdout.columns ?? 80
|
|
112
131
|
return renderString(<Report {...props} />, { width })
|
|
113
132
|
}
|
package/src/submit.ts
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
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
|
+
import { createInterface } from "node:readline"
|
|
10
|
+
|
|
9
11
|
const REPO = "beorn/terminfo.dev"
|
|
10
|
-
const ISSUE_LABEL = "community-results"
|
|
11
12
|
|
|
12
|
-
interface
|
|
13
|
+
interface SubmitData {
|
|
13
14
|
terminal: string
|
|
14
15
|
terminalVersion: string
|
|
15
16
|
os: string
|
|
@@ -20,11 +21,42 @@ interface SubmitResult {
|
|
|
20
21
|
generated: string
|
|
21
22
|
}
|
|
22
23
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
24
|
+
async function prompt(question: string, defaultValue?: string): Promise<string> {
|
|
25
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout })
|
|
26
|
+
const suffix = defaultValue ? ` [${defaultValue}]` : ""
|
|
27
|
+
return new Promise((resolve) => {
|
|
28
|
+
rl.question(`${question}${suffix}: `, (answer) => {
|
|
29
|
+
rl.close()
|
|
30
|
+
resolve(answer.trim() || defaultValue || "")
|
|
31
|
+
})
|
|
32
|
+
})
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function submitResults(data: SubmitData): Promise<string | null> {
|
|
36
|
+
// Confirm/fill terminal info
|
|
37
|
+
console.log(`\n\x1b[1mConfirm submission details:\x1b[0m`)
|
|
38
|
+
data.terminal = await prompt(" Terminal name", data.terminal)
|
|
39
|
+
data.terminalVersion = await prompt(" Terminal version", data.terminalVersion || undefined)
|
|
40
|
+
data.os = await prompt(" Operating system", data.os)
|
|
41
|
+
|
|
42
|
+
if (!data.terminalVersion) {
|
|
43
|
+
console.log(`\x1b[33m ⚠ No version specified — results will be less useful\x1b[0m`)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Check for duplicates
|
|
47
|
+
if (hasGhCli()) {
|
|
48
|
+
const existing = checkDuplicate(data.terminal, data.terminalVersion, data.os)
|
|
49
|
+
if (existing) {
|
|
50
|
+
console.log(`\n\x1b[33m⚠ A submission already exists for ${data.terminal}${data.terminalVersion ? ` ${data.terminalVersion}` : ""} on ${data.os}:\x1b[0m`)
|
|
51
|
+
console.log(` ${existing}`)
|
|
52
|
+
const proceed = await prompt(" Submit anyway? (y/N)", "N")
|
|
53
|
+
if (proceed.toLowerCase() !== "y") {
|
|
54
|
+
console.log(`Skipped.`)
|
|
55
|
+
return null
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
28
60
|
const passed = Object.values(data.results).filter(Boolean).length
|
|
29
61
|
const total = Object.keys(data.results).length
|
|
30
62
|
const pct = Math.round((passed / total) * 100)
|
|
@@ -41,10 +73,12 @@ export async function submitResults(data: SubmitResult): Promise<string | null>
|
|
|
41
73
|
| Score | ${passed}/${total} (${pct}%) |
|
|
42
74
|
| Generated | ${data.generated} |
|
|
43
75
|
|
|
44
|
-
###
|
|
76
|
+
### Summary
|
|
77
|
+
|
|
78
|
+
${formatSummary(data)}
|
|
45
79
|
|
|
46
80
|
<details>
|
|
47
|
-
<summary>Full JSON
|
|
81
|
+
<summary>Full JSON</summary>
|
|
48
82
|
|
|
49
83
|
\`\`\`json
|
|
50
84
|
${JSON.stringify(data, null, 2)}
|
|
@@ -52,55 +86,66 @@ ${JSON.stringify(data, null, 2)}
|
|
|
52
86
|
|
|
53
87
|
</details>
|
|
54
88
|
|
|
55
|
-
### Summary
|
|
56
|
-
|
|
57
|
-
${formatSummary(data)}
|
|
58
|
-
|
|
59
89
|
---
|
|
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
|
-
}
|
|
90
|
+
*Submitted via \`npx terminfo.dev submit\`*`
|
|
91
|
+
|
|
92
|
+
if (!hasGhCli()) {
|
|
93
|
+
const filename = `terminfo-${data.terminal}-${data.os}-${Date.now()}.json`
|
|
94
|
+
writeFileSync(filename, JSON.stringify(data, null, 2))
|
|
95
|
+
console.log(`\n\x1b[33mgh CLI not found. Results saved to ${filename}\x1b[0m`)
|
|
96
|
+
console.log(`To submit: https://github.com/${REPO}/issues/new`)
|
|
97
|
+
return null
|
|
77
98
|
}
|
|
78
99
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
100
|
+
const bodyFile = join(tmpdir(), `terminfo-submit-${Date.now()}.md`)
|
|
101
|
+
try {
|
|
102
|
+
writeFileSync(bodyFile, body)
|
|
103
|
+
const result = execFileSync("gh", [
|
|
104
|
+
"issue", "create",
|
|
105
|
+
"--repo", REPO,
|
|
106
|
+
"--title", title,
|
|
107
|
+
"--body-file", bodyFile,
|
|
108
|
+
], { encoding: "utf-8", timeout: 30000 })
|
|
109
|
+
return result.trim()
|
|
110
|
+
} catch (err) {
|
|
111
|
+
console.error(`\x1b[31mFailed to create issue\x1b[0m`)
|
|
112
|
+
console.error(err instanceof Error ? err.message : String(err))
|
|
113
|
+
return null
|
|
114
|
+
} finally {
|
|
115
|
+
try { unlinkSync(bodyFile) } catch {}
|
|
116
|
+
}
|
|
89
117
|
}
|
|
90
118
|
|
|
91
|
-
|
|
119
|
+
function hasGhCli(): boolean {
|
|
92
120
|
try {
|
|
93
|
-
|
|
94
|
-
execSync("gh --version", { stdio: "ignore", timeout: 5000 })
|
|
121
|
+
execFileSync("gh", ["--version"], { stdio: "ignore", timeout: 5000 })
|
|
95
122
|
return true
|
|
96
123
|
} catch {
|
|
97
124
|
return false
|
|
98
125
|
}
|
|
99
126
|
}
|
|
100
127
|
|
|
101
|
-
function
|
|
102
|
-
|
|
128
|
+
function checkDuplicate(terminal: string, version: string, os: string): string | null {
|
|
129
|
+
try {
|
|
130
|
+
const search = `[census] ${terminal}${version ? ` ${version}` : ""} on ${os}`
|
|
131
|
+
const result = execFileSync("gh", [
|
|
132
|
+
"issue", "list",
|
|
133
|
+
"--repo", REPO,
|
|
134
|
+
"--search", search,
|
|
135
|
+
"--state", "all",
|
|
136
|
+
"--limit", "1",
|
|
137
|
+
"--json", "url,title",
|
|
138
|
+
"--jq", ".[0] | .title + \" \" + .url",
|
|
139
|
+
], { encoding: "utf-8", timeout: 10000 })
|
|
140
|
+
const trimmed = result.trim()
|
|
141
|
+
return trimmed || null
|
|
142
|
+
} catch {
|
|
143
|
+
return null
|
|
144
|
+
}
|
|
145
|
+
}
|
|
103
146
|
|
|
147
|
+
function formatSummary(data: SubmitData): string {
|
|
148
|
+
const categories = new Map<string, { pass: number; fail: number; failList: string[] }>()
|
|
104
149
|
for (const [id, pass] of Object.entries(data.results)) {
|
|
105
150
|
const cat = id.split(".")[0]!
|
|
106
151
|
if (!categories.has(cat)) categories.set(cat, { pass: 0, fail: 0, failList: [] })
|
|
@@ -112,14 +157,11 @@ function formatSummary(data: SubmitResult): string {
|
|
|
112
157
|
entry.failList.push(note ? `- \`${id}\`: ${note}` : `- \`${id}\``)
|
|
113
158
|
}
|
|
114
159
|
}
|
|
115
|
-
|
|
116
160
|
const lines: string[] = []
|
|
117
161
|
for (const [cat, { pass, fail, failList }] of categories) {
|
|
118
|
-
const total = pass + fail
|
|
119
162
|
const icon = fail === 0 ? "✅" : "⚠️"
|
|
120
|
-
lines.push(`${icon} **${cat}**: ${pass}/${
|
|
163
|
+
lines.push(`${icon} **${cat}**: ${pass}/${pass + fail}`)
|
|
121
164
|
if (failList.length > 0) lines.push(...failList)
|
|
122
165
|
}
|
|
123
|
-
|
|
124
166
|
return lines.join("\n")
|
|
125
167
|
}
|