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 +1 -1
- package/src/index.ts +67 -4
- package/src/serve.ts +210 -0
package/package.json
CHANGED
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
|
|
252
|
-
console.log(` \x1b[1msubmit\x1b[0m
|
|
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
|
|
256
|
-
console.log(` \x1b[1m--help\x1b[0m
|
|
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
|
+
}
|