terminfo.dev 1.1.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "terminfo.dev",
3
- "version": "1.1.0",
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",
package/src/index.ts CHANGED
@@ -25,10 +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
- const candidates = [
29
- join(__dirname, "..", "..", "features.json"),
30
- join(__dirname, "..", "..", "..", "features.json"),
31
- ]
28
+ const candidates = [join(__dirname, "..", "..", "features.json"), join(__dirname, "..", "..", "..", "features.json")]
32
29
  for (const path of candidates) {
33
30
  try {
34
31
  const raw = JSON.parse(readFileSync(path, "utf-8"))
@@ -90,7 +87,9 @@ function printHeader(terminal: ReturnType<typeof detectTerminal>) {
90
87
  console.log(`\x1b[1m${siteLink}\x1b[0m — can your terminal do that?\n`)
91
88
  console.log(` Terminal: \x1b[1m${terminal.name}\x1b[0m${terminal.version ? ` ${terminal.version}` : ""}`)
92
89
  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`)
90
+ console.log(
91
+ ` Probes: ${ALL_PROBES.length} features across ${new Set(ALL_PROBES.map((p) => p.id.split(".")[0])).size} categories`,
92
+ )
94
93
  console.log(` Website: ${link("https://terminfo.dev", "https://terminfo.dev")}`)
95
94
  }
96
95
 
@@ -115,7 +114,7 @@ function printResults(data: ProbeResults) {
115
114
  }
116
115
 
117
116
  for (const [cat, probes] of categories) {
118
- const catPassed = probes.filter(p => p.pass).length
117
+ const catPassed = probes.filter((p) => p.pass).length
119
118
  const color = catPassed === probes.length ? "\x1b[32m" : catPassed > 0 ? "\x1b[33m" : "\x1b[31m"
120
119
  const catLink = link(`https://terminfo.dev/${cat}`, cat)
121
120
  console.log(`${color}${catLink}\x1b[0m (${catPassed}/${probes.length})`)
@@ -145,21 +144,28 @@ program
145
144
  const data = await runProbes()
146
145
 
147
146
  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))
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
+ )
159
164
  return
160
165
  }
161
166
 
162
167
  printResults(data)
168
+ console.log(`\n\x1b[2mContribute these results: \x1b[0m\x1b[1mnpx terminfo.dev submit\x1b[0m`)
163
169
  })
164
170
 
165
171
  program
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>{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>
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({ name, probes, slugs }: { name: string; probes: ProbeResult[]; slugs: Record<string, string> }) {
63
- const catPassed = probes.filter(p => p.pass).length
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: npx terminfo.dev --json</Text>
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(props: ReportProps & { slugs: Record<string, string>; submitMode: boolean }): Promise<string> {
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
@@ -6,6 +6,7 @@ import { writeFileSync, unlinkSync } from "node:fs"
6
6
  import { tmpdir } from "node:os"
7
7
  import { join } from "node:path"
8
8
  import { execFileSync } from "node:child_process"
9
+ import { createInterface } from "node:readline"
9
10
 
10
11
  const REPO = "beorn/terminfo.dev"
11
12
 
@@ -20,7 +21,42 @@ interface SubmitData {
20
21
  generated: string
21
22
  }
22
23
 
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
+
23
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
+
24
60
  const passed = Object.values(data.results).filter(Boolean).length
25
61
  const total = Object.keys(data.results).length
26
62
  const pct = Math.round((passed / total) * 100)
@@ -51,7 +87,7 @@ ${JSON.stringify(data, null, 2)}
51
87
  </details>
52
88
 
53
89
  ---
54
- *Submitted via \`npx terminfo.dev\`*`
90
+ *Submitted via \`npx terminfo.dev submit\`*`
55
91
 
56
92
  if (!hasGhCli()) {
57
93
  const filename = `terminfo-${data.terminal}-${data.os}-${Date.now()}.json`
@@ -61,7 +97,6 @@ ${JSON.stringify(data, null, 2)}
61
97
  return null
62
98
  }
63
99
 
64
- // Write body to temp file to avoid shell escaping issues
65
100
  const bodyFile = join(tmpdir(), `terminfo-submit-${Date.now()}.md`)
66
101
  try {
67
102
  writeFileSync(bodyFile, body)
@@ -90,6 +125,25 @@ function hasGhCli(): boolean {
90
125
  }
91
126
  }
92
127
 
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
+ }
146
+
93
147
  function formatSummary(data: SubmitData): string {
94
148
  const categories = new Map<string, { pass: number; fail: number; failList: string[] }>()
95
149
  for (const [id, pass] of Object.entries(data.results)) {