terminfo.dev 3.0.1 → 3.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "terminfo.dev",
3
- "version": "3.0.1",
3
+ "version": "3.1.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
@@ -238,6 +238,67 @@ program
238
238
  }
239
239
  })
240
240
 
241
+ program
242
+ .command("serve")
243
+ .description("Start daemon — accepts remote probe requests from other sessions")
244
+ .option("-p, --port <port>", "Port to listen on (default: auto)", parseInt)
245
+ .action(async (opts) => {
246
+ const { startDaemon } = await import("./serve.ts")
247
+ await startDaemon(opts.port ?? 0)
248
+ })
249
+
250
+ program
251
+ .command("test-all")
252
+ .description("Run probes on all running daemons (started with 'serve')")
253
+ .action(async () => {
254
+ const { listDaemons } = await import("./serve.ts")
255
+ const daemons = listDaemons()
256
+
257
+ if (daemons.length === 0) {
258
+ console.log(`\x1b[33mNo daemons found.\x1b[0m`)
259
+ console.log(`Start a daemon in each terminal: \x1b[1mnpx terminfo.dev serve\x1b[0m`)
260
+ return
261
+ }
262
+
263
+ console.log(`\x1b[1mterminfo.dev\x1b[0m — testing ${daemons.length} terminal(s)\n`)
264
+
265
+ for (const d of daemons) {
266
+ const label = `${d.terminal}${d.terminalVersion ? ` ${d.terminalVersion}` : ""}`
267
+ process.stdout.write(` ${label.padEnd(25)} `)
268
+
269
+ try {
270
+ const res = await fetch(`http://127.0.0.1:${d.port}/probe`, { signal: AbortSignal.timeout(120000) })
271
+ if (!res.ok) {
272
+ console.log(`\x1b[31m✗ HTTP ${res.status}\x1b[0m`)
273
+ continue
274
+ }
275
+ const data = await res.json() as any
276
+ const passed = Object.values(data.results).filter((v: any) => v).length
277
+ const total = Object.keys(data.results).length
278
+ const pct = Math.round(passed / total * 100)
279
+ const color = pct >= 98 ? "\x1b[32m" : pct >= 90 ? "\x1b[33m" : "\x1b[31m"
280
+ console.log(`${color}${passed}/${total} (${pct}%)\x1b[0m`)
281
+
282
+ // Save results
283
+ const { mkdirSync, writeFileSync } = await import("node:fs")
284
+ const dir = "docs/data/results/app"
285
+ mkdirSync(dir, { recursive: true })
286
+ const name = data.terminal.replace(/[^a-z0-9-]/g, "-")
287
+ const ver = (data.terminalVersion || "unknown").replace(/[^a-z0-9.-]/g, "-")
288
+ writeFileSync(`${dir}/${name}-${ver}-${data.os}.json`, JSON.stringify(data, null, 2))
289
+ } catch (err) {
290
+ const msg = err instanceof Error ? err.message : String(err)
291
+ if (msg.includes("ECONNREFUSED")) {
292
+ console.log(`\x1b[31m✗ not running (stale daemon file)\x1b[0m`)
293
+ } else {
294
+ console.log(`\x1b[31m✗ ${msg}\x1b[0m`)
295
+ }
296
+ }
297
+ }
298
+
299
+ console.log(`\nResults saved to docs/data/results/app/`)
300
+ })
301
+
241
302
  // Default action: show terminal info + help
242
303
  program.action(() => {
243
304
  const terminal = detectTerminal()
@@ -248,12 +309,14 @@ program.action(() => {
248
309
  console.log(`submitted to the community database at terminfo.dev.\x1b[0m`)
249
310
  console.log(``)
250
311
  console.log(`Commands:`)
251
- console.log(` \x1b[1mprobe\x1b[0m Run all probes and display results`)
252
- console.log(` \x1b[1msubmit\x1b[0m Run probes and submit to terminfo.dev`)
312
+ console.log(` \x1b[1mprobe\x1b[0m Run all probes and display results`)
313
+ console.log(` \x1b[1msubmit\x1b[0m Run probes and submit to terminfo.dev`)
314
+ console.log(` \x1b[1mserve\x1b[0m Start daemon for remote testing`)
315
+ console.log(` \x1b[1mtest-all\x1b[0m Run probes on all daemons`)
253
316
  console.log(``)
254
317
  console.log(`Options:`)
255
- console.log(` \x1b[1m--json\x1b[0m Output results as JSON (with probe command)`)
256
- console.log(` \x1b[1m--help\x1b[0m Show this help`)
318
+ console.log(` \x1b[1m--json\x1b[0m Output results as JSON (with probe command)`)
319
+ console.log(` \x1b[1m--help\x1b[0m Show this help`)
257
320
  })
258
321
 
259
322
  program.parse()
package/src/serve.ts ADDED
@@ -0,0 +1,210 @@
1
+ /**
2
+ * Daemon mode — run inside a terminal, accept remote probe commands.
3
+ *
4
+ * Usage: npx terminfo.dev serve
5
+ *
6
+ * Starts an HTTP server that accepts probe requests. Run this in each
7
+ * terminal you want to test, then use `terminfo.dev test-all` or
8
+ * curl to run probes remotely.
9
+ *
10
+ * Discovery: writes terminal info + port to ~/.terminfo-dev/daemons/
11
+ * so clients can find all running daemons automatically.
12
+ */
13
+
14
+ import { createServer, type IncomingMessage, type ServerResponse } from "node:http"
15
+ import { mkdirSync, writeFileSync, unlinkSync, readdirSync, readFileSync } from "node:fs"
16
+ import { join } from "node:path"
17
+ import { homedir } from "node:os"
18
+ import { detectTerminal } from "./detect.ts"
19
+ import { ALL_PROBES } from "./probes/index.ts"
20
+ import { withRawMode, drainStdin } from "./tty.ts"
21
+
22
+ const DAEMON_DIR = join(homedir(), ".terminfo-dev", "daemons")
23
+
24
+ interface DaemonInfo {
25
+ pid: number
26
+ port: number
27
+ terminal: string
28
+ terminalVersion: string
29
+ os: string
30
+ osVersion: string
31
+ started: string
32
+ }
33
+
34
+ function register(info: DaemonInfo): string {
35
+ mkdirSync(DAEMON_DIR, { recursive: true })
36
+ const filename = `${info.terminal}-${info.pid}.json`
37
+ const filepath = join(DAEMON_DIR, filename)
38
+ writeFileSync(filepath, JSON.stringify(info, null, 2))
39
+ return filepath
40
+ }
41
+
42
+ function unregister(filepath: string) {
43
+ try { unlinkSync(filepath) } catch {}
44
+ }
45
+
46
+ export function listDaemons(): DaemonInfo[] {
47
+ try {
48
+ const files = readdirSync(DAEMON_DIR).filter(f => f.endsWith(".json"))
49
+ const daemons: DaemonInfo[] = []
50
+ for (const f of files) {
51
+ try {
52
+ const data = JSON.parse(readFileSync(join(DAEMON_DIR, f), "utf-8"))
53
+ daemons.push(data)
54
+ } catch {}
55
+ }
56
+ return daemons
57
+ } catch {
58
+ return []
59
+ }
60
+ }
61
+
62
+ export async function startDaemon(port = 0): Promise<void> {
63
+ const terminal = detectTerminal()
64
+
65
+ // Run all probes once synchronously, then serve results
66
+ // For live probing, we need the server to run probes on-demand in the terminal context
67
+ const server = createServer(async (req: IncomingMessage, res: ServerResponse) => {
68
+ res.setHeader("Content-Type", "application/json")
69
+ res.setHeader("Access-Control-Allow-Origin", "*")
70
+
71
+ const url = new URL(req.url ?? "/", `http://localhost`)
72
+
73
+ if (url.pathname === "/info") {
74
+ res.end(JSON.stringify({
75
+ terminal: terminal.name,
76
+ terminalVersion: terminal.version,
77
+ os: terminal.os,
78
+ osVersion: terminal.osVersion,
79
+ probeCount: ALL_PROBES.length,
80
+ }))
81
+ return
82
+ }
83
+
84
+ if (url.pathname === "/probe") {
85
+ // Run all probes in this terminal
86
+ console.log(`\x1b[2m[${new Date().toISOString()}] Running ${ALL_PROBES.length} probes...\x1b[0m`)
87
+
88
+ const results: Record<string, boolean> = {}
89
+ const notes: Record<string, string> = {}
90
+ const responses: Record<string, string> = {}
91
+
92
+ await withRawMode(async () => {
93
+ for (const probe of ALL_PROBES) {
94
+ process.stdout.write("\x1b[0m\x1b[2J\x1b[H")
95
+ try {
96
+ const result = await probe.run()
97
+ results[probe.id] = result.pass
98
+ if (result.note) notes[probe.id] = result.note
99
+ if (result.response) responses[probe.id] = result.response
100
+ } catch (err) {
101
+ results[probe.id] = false
102
+ notes[probe.id] = `error: ${err instanceof Error ? err.message : String(err)}`
103
+ }
104
+ }
105
+ process.stdout.write("\x1b[0m\x1b[2J\x1b[H")
106
+ await drainStdin(1000)
107
+ })
108
+
109
+ // Reset terminal after probes
110
+ process.stdout.write("\x1bc")
111
+
112
+ const passed = Object.values(results).filter(v => v).length
113
+ const total = Object.keys(results).length
114
+ console.log(`\x1b[32m✓\x1b[0m ${passed}/${total} (${Math.round(passed / total * 100)}%)`)
115
+
116
+ res.end(JSON.stringify({
117
+ terminal: terminal.name,
118
+ terminalVersion: terminal.version,
119
+ os: terminal.os,
120
+ osVersion: terminal.osVersion,
121
+ source: "daemon",
122
+ generated: new Date().toISOString(),
123
+ results,
124
+ notes,
125
+ responses,
126
+ }))
127
+ return
128
+ }
129
+
130
+ if (url.pathname === "/probe/single") {
131
+ const probeId = url.searchParams.get("id")
132
+ if (!probeId) {
133
+ res.statusCode = 400
134
+ res.end(JSON.stringify({ error: "Missing ?id= parameter" }))
135
+ return
136
+ }
137
+ const probe = ALL_PROBES.find(p => p.id === probeId)
138
+ if (!probe) {
139
+ res.statusCode = 404
140
+ res.end(JSON.stringify({ error: `Unknown probe: ${probeId}` }))
141
+ return
142
+ }
143
+
144
+ await withRawMode(async () => {
145
+ process.stdout.write("\x1b[0m\x1b[2J\x1b[H")
146
+ try {
147
+ const result = await probe.run()
148
+ process.stdout.write("\x1b[0m\x1b[2J\x1b[H")
149
+ await drainStdin(500)
150
+ res.end(JSON.stringify({ id: probeId, ...result }))
151
+ } catch (err) {
152
+ process.stdout.write("\x1b[0m\x1b[2J\x1b[H")
153
+ await drainStdin(500)
154
+ res.end(JSON.stringify({ id: probeId, pass: false, note: String(err) }))
155
+ }
156
+ })
157
+ process.stdout.write("\x1bc")
158
+ return
159
+ }
160
+
161
+ // Default: show help
162
+ res.end(JSON.stringify({
163
+ endpoints: {
164
+ "/info": "Terminal info",
165
+ "/probe": "Run all probes",
166
+ "/probe/single?id=sgr.bold": "Run single probe",
167
+ },
168
+ terminal: terminal.name,
169
+ version: terminal.version,
170
+ }))
171
+ })
172
+
173
+ server.listen(port, "127.0.0.1", () => {
174
+ const addr = server.address()
175
+ if (!addr || typeof addr === "string") return
176
+ const actualPort = addr.port
177
+
178
+ const info: DaemonInfo = {
179
+ pid: process.pid,
180
+ port: actualPort,
181
+ terminal: terminal.name,
182
+ terminalVersion: terminal.version,
183
+ os: terminal.os,
184
+ osVersion: terminal.osVersion,
185
+ started: new Date().toISOString(),
186
+ }
187
+
188
+ const filepath = register(info)
189
+
190
+ console.log(`\x1b[1mterminfo.dev\x1b[0m daemon running\n`)
191
+ console.log(` Terminal: \x1b[1m${terminal.name}\x1b[0m ${terminal.version}`)
192
+ console.log(` Port: \x1b[1m${actualPort}\x1b[0m`)
193
+ console.log(` Probes: ${ALL_PROBES.length}`)
194
+ console.log(``)
195
+ console.log(` Test: curl http://localhost:${actualPort}/probe`)
196
+ console.log(` Info: curl http://localhost:${actualPort}/info`)
197
+ console.log(` Single: curl http://localhost:${actualPort}/probe/single?id=sgr.bold`)
198
+ console.log(``)
199
+ console.log(`\x1b[2mPress Ctrl+C to stop.\x1b[0m`)
200
+
201
+ // Clean up on exit
202
+ const cleanup = () => {
203
+ unregister(filepath)
204
+ server.close()
205
+ process.exit(0)
206
+ }
207
+ process.on("SIGINT", cleanup)
208
+ process.on("SIGTERM", cleanup)
209
+ })
210
+ }