aipeek 0.2.3 → 0.2.4
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 -4
- package/src/core/check.ts +34 -0
- package/src/core/compact.ts +338 -0
- package/src/core/detail.ts +252 -0
- package/src/core/diff.ts +38 -0
- package/src/core/emit.ts +186 -0
- package/src/core/util.ts +16 -0
- package/src/index.ts +5 -0
- package/src/server/cli.ts +70 -0
- package/src/server/plugin.ts +536 -0
package/src/core/emit.ts
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import type { CheckResult, CompactState, DiffResult, RawState } from './types'
|
|
2
|
+
import pc from 'picocolors'
|
|
3
|
+
import { nameOf } from './compact'
|
|
4
|
+
import { compactUrl, truncate } from './util'
|
|
5
|
+
|
|
6
|
+
// --- Full emit (for ?full) ---
|
|
7
|
+
|
|
8
|
+
const SECTIONS = ['ui', 'console', 'network', 'errors', 'state'] as const
|
|
9
|
+
const COUNTED_SECTIONS: Record<string, keyof CompactState['counts']> = {
|
|
10
|
+
console: 'console',
|
|
11
|
+
network: 'network',
|
|
12
|
+
errors: 'errors',
|
|
13
|
+
state: 'state',
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function emit(state: CompactState): string {
|
|
17
|
+
const sections: string[] = []
|
|
18
|
+
|
|
19
|
+
for (const key of SECTIONS) {
|
|
20
|
+
if (state[key]) {
|
|
21
|
+
const countKey = COUNTED_SECTIONS[key]
|
|
22
|
+
const count = countKey ? state.counts?.[countKey] ?? 0 : 0
|
|
23
|
+
const attr = count ? ` count="${count}"` : ''
|
|
24
|
+
sections.push(`<${key}${attr}>\n${state[key]}\n</${key}>`)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (!sections.length) {
|
|
29
|
+
sections.push('<empty/>')
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return `<aipeek url="${state.url}">\n\n${sections.join('\n\n')}\n\n</aipeek>\n\ndetail: GET /__aipeek/{section}/{index}?full`
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// --- Summary emit (default, high-density) ---
|
|
36
|
+
|
|
37
|
+
export function emitSummary(raw: RawState): string {
|
|
38
|
+
const consoleErrors = raw.console.filter(l => l.level === 'error')
|
|
39
|
+
const consoleWarns = raw.console.filter(l => l.level === 'warn')
|
|
40
|
+
const failedReqs = raw.network.filter(r => r.status >= 400 || r.failed)
|
|
41
|
+
const hasIssues = consoleErrors.length > 0 || raw.errors.length > 0 || failedReqs.length > 0
|
|
42
|
+
|
|
43
|
+
const lines: string[] = []
|
|
44
|
+
|
|
45
|
+
// ui — always 1-line summary
|
|
46
|
+
if (raw.ui.trim()) {
|
|
47
|
+
lines.push(`ui: ${summarizeUI(raw.ui)}`)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// console — 1-line if clean, expand errors/warns if dirty
|
|
51
|
+
if (raw.console.length) {
|
|
52
|
+
if (consoleErrors.length || consoleWarns.length) {
|
|
53
|
+
const parts: string[] = []
|
|
54
|
+
for (const e of consoleErrors) parts.push(` [error] ${truncate(e.text, 150)}`)
|
|
55
|
+
for (const w of consoleWarns) parts.push(` [warn] ${truncate(w.text, 150)}`)
|
|
56
|
+
const rest = raw.console.length - consoleErrors.length - consoleWarns.length
|
|
57
|
+
if (rest > 0)
|
|
58
|
+
parts.push(` … ${rest} more`)
|
|
59
|
+
lines.push(`console (${raw.console.length}):`)
|
|
60
|
+
lines.push(...parts)
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
lines.push(`console: ${raw.console.length} logs`)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// network — 1-line if clean, expand failures if dirty
|
|
68
|
+
if (raw.network.length) {
|
|
69
|
+
if (failedReqs.length) {
|
|
70
|
+
lines.push(`network (${raw.network.length}):`)
|
|
71
|
+
for (const r of failedReqs) {
|
|
72
|
+
const body = r.responseBody ? ` "${truncate(r.responseBody, 80)}"` : ''
|
|
73
|
+
lines.push(` ${r.method} ${compactUrl(r.url)} ${r.status}${body}`)
|
|
74
|
+
}
|
|
75
|
+
const ok = raw.network.length - failedReqs.length
|
|
76
|
+
if (ok > 0)
|
|
77
|
+
lines.push(` … ${ok} ok`)
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
lines.push(`network: ${raw.network.length} ok`)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// errors — always expand (these are uncaught exceptions)
|
|
85
|
+
if (raw.errors.length) {
|
|
86
|
+
lines.push(`errors (${raw.errors.length}):`)
|
|
87
|
+
for (const e of raw.errors) lines.push(` ${truncate(e.message, 150)}`)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// state — 1-line: store names + key counts
|
|
91
|
+
const storeNames = Object.keys(raw.state)
|
|
92
|
+
if (storeNames.length) {
|
|
93
|
+
const parts = storeNames.map((n) => {
|
|
94
|
+
const v = raw.state[n]
|
|
95
|
+
const keys = typeof v === 'object' && v !== null ? Object.keys(v).length : 0
|
|
96
|
+
return keys > 0 ? `${n}(${keys})` : n
|
|
97
|
+
})
|
|
98
|
+
lines.push(`state: ${parts.join(', ')}`)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (!lines.length)
|
|
102
|
+
return '<aipeek>empty</aipeek>'
|
|
103
|
+
|
|
104
|
+
const status = hasIssues
|
|
105
|
+
? `${consoleErrors.length + raw.errors.length + failedReqs.length} issues`
|
|
106
|
+
: 'ok'
|
|
107
|
+
|
|
108
|
+
return `<aipeek url="${raw.url}" status="${status}">\n${lines.join('\n')}\n</aipeek>${hasIssues ? '\n\ndetail: GET /__aipeek/{section}/{index}' : ''}`
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function summarizeUI(tree: string): string {
|
|
112
|
+
// extract top-level component names (depth 0-1)
|
|
113
|
+
const components: string[] = []
|
|
114
|
+
const counts = new Map<string, number>()
|
|
115
|
+
|
|
116
|
+
for (const line of tree.split('\n')) {
|
|
117
|
+
const trimmed = line.trimStart()
|
|
118
|
+
if (!trimmed)
|
|
119
|
+
continue
|
|
120
|
+
const indent = line.length - trimmed.length
|
|
121
|
+
if (indent > 2)
|
|
122
|
+
continue // only depth 0-1
|
|
123
|
+
|
|
124
|
+
const name = nameOf(trimmed)
|
|
125
|
+
if (!name)
|
|
126
|
+
continue
|
|
127
|
+
|
|
128
|
+
// extract state markers
|
|
129
|
+
const focused = trimmed.includes('[focused]')
|
|
130
|
+
const generating = trimmed.includes('[generating]')
|
|
131
|
+
const loading = trimmed.includes('[loading]')
|
|
132
|
+
|
|
133
|
+
const existing = counts.get(name) || 0
|
|
134
|
+
if (existing > 0) {
|
|
135
|
+
counts.set(name, existing + 1)
|
|
136
|
+
continue
|
|
137
|
+
}
|
|
138
|
+
counts.set(name, 1)
|
|
139
|
+
|
|
140
|
+
let label = name
|
|
141
|
+
if (focused)
|
|
142
|
+
label += '[focused]'
|
|
143
|
+
if (generating)
|
|
144
|
+
label += '[generating]'
|
|
145
|
+
if (loading)
|
|
146
|
+
label += '[loading]'
|
|
147
|
+
components.push(label)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// apply counts
|
|
151
|
+
const result = components.map((c) => {
|
|
152
|
+
const name = c.split('[')[0]
|
|
153
|
+
const count = counts.get(name) || 1
|
|
154
|
+
return count > 1 ? `${c}(×${count})` : c
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
return result.join(', ') || 'empty'
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// --- Check emit ---
|
|
161
|
+
|
|
162
|
+
export function emitCheck(result: CheckResult): string {
|
|
163
|
+
const lines = result.assertions.map(a =>
|
|
164
|
+
a.pass
|
|
165
|
+
? `✓ ${a.name}`
|
|
166
|
+
: `✗ ${a.name}${a.detail ? `: ${a.detail}` : ''}`,
|
|
167
|
+
)
|
|
168
|
+
return `<aipeek-check pass="${result.pass}">\n${lines.join('\n')}\n</aipeek-check>`
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// --- Diff emit (terminal, colored) ---
|
|
172
|
+
|
|
173
|
+
export function emitDiff(diff: DiffResult): string {
|
|
174
|
+
const issues: string[] = []
|
|
175
|
+
for (const e of diff.newErrors) issues.push(pc.red(` [error] ${e.text}`))
|
|
176
|
+
for (const e of diff.newExceptions) issues.push(pc.red(` [exception] ${e.message}`))
|
|
177
|
+
for (const r of diff.newFailedRequests) issues.push(pc.yellow(` [network] ${r.method} ${r.url} ${r.status}`))
|
|
178
|
+
if (diff.uiGone)
|
|
179
|
+
issues.push(pc.magenta(' [ui] component tree disappeared'))
|
|
180
|
+
|
|
181
|
+
if (!issues.length)
|
|
182
|
+
return ''
|
|
183
|
+
|
|
184
|
+
const count = issues.length
|
|
185
|
+
return `${pc.bold('[aipeek]')} ${pc.red(`✗ ${count} issue${count > 1 ? 's' : ''} after HMR`)}\n${issues.join('\n')}`
|
|
186
|
+
}
|
package/src/core/util.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export function truncate(s: string, max: number): string {
|
|
2
|
+
return s.length > max ? `${s.slice(0, max)}…` : s
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
// pathname only; pass `search` to append a truncated query string
|
|
6
|
+
export function compactUrl(url: string, search?: number): string {
|
|
7
|
+
try {
|
|
8
|
+
const u = new URL(url)
|
|
9
|
+
if (search && u.search)
|
|
10
|
+
return `${u.pathname}?${truncate(u.search.slice(1), search)}`
|
|
11
|
+
return u.pathname
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return truncate(url, 80)
|
|
15
|
+
}
|
|
16
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { check } from './core/check'
|
|
2
|
+
export { diffState } from './core/diff'
|
|
3
|
+
export { emitCheck, emitDiff, emitSummary } from './core/emit'
|
|
4
|
+
export type { CheckResult, CompactState, DiffResult, RawState } from './core/types'
|
|
5
|
+
export { aipeekPlugin } from './server/plugin'
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/* eslint-disable no-console -- this is a CLI; stdout is its output channel */
|
|
2
|
+
import process from 'node:process'
|
|
3
|
+
import pc from 'picocolors'
|
|
4
|
+
|
|
5
|
+
const args = process.argv.slice(2)
|
|
6
|
+
const flags = Object.fromEntries(
|
|
7
|
+
args.filter(a => a.startsWith('--')).map((a) => {
|
|
8
|
+
const [k, v] = a.slice(2).split('=')
|
|
9
|
+
return [k, v ?? 'true']
|
|
10
|
+
}),
|
|
11
|
+
)
|
|
12
|
+
const positional = args.filter(a => !a.startsWith('--'))
|
|
13
|
+
|
|
14
|
+
async function main() {
|
|
15
|
+
if (flags.help) {
|
|
16
|
+
console.log(`
|
|
17
|
+
${pc.bold('aipeek')} — runtime snapshot from Vite dev server
|
|
18
|
+
|
|
19
|
+
${pc.dim('Usage:')}
|
|
20
|
+
aipeek Full summary
|
|
21
|
+
aipeek check Health check (pass/fail)
|
|
22
|
+
aipeek console Console logs
|
|
23
|
+
aipeek network/0 Network request detail
|
|
24
|
+
aipeek errors/1?full Error detail (untruncated)
|
|
25
|
+
|
|
26
|
+
${pc.dim('Options:')}
|
|
27
|
+
--port=<port> Dev server port (default: 5173)
|
|
28
|
+
--full Untruncated output
|
|
29
|
+
--help Show this help
|
|
30
|
+
|
|
31
|
+
${pc.dim('Setup:')}
|
|
32
|
+
Add aipeekPlugin() to your vite.config.ts plugins array.
|
|
33
|
+
`)
|
|
34
|
+
process.exit(0)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const port = flags.port || '5173'
|
|
38
|
+
const sub = positional[0] || ''
|
|
39
|
+
const full = flags.full ? '?full' : ''
|
|
40
|
+
const path = sub ? `/${sub}` : ''
|
|
41
|
+
const endpoint = `http://localhost:${port}/__aipeek${path}${full}`
|
|
42
|
+
|
|
43
|
+
const resp = await fetch(endpoint)
|
|
44
|
+
if (!resp.ok && resp.status !== 417) {
|
|
45
|
+
const text = await resp.text()
|
|
46
|
+
console.error(pc.red(`Error ${resp.status}: ${text}`))
|
|
47
|
+
process.exit(1)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const text = await resp.text()
|
|
51
|
+
// color check results — assertions are one per line: "✓ name" / "✗ name: detail"
|
|
52
|
+
if (sub === 'check') {
|
|
53
|
+
const colored = text.split('\n').map((line) => {
|
|
54
|
+
if (line.startsWith('✓'))
|
|
55
|
+
return pc.green(line)
|
|
56
|
+
if (line.startsWith('✗'))
|
|
57
|
+
return pc.red(line)
|
|
58
|
+
return line
|
|
59
|
+
}).join('\n')
|
|
60
|
+
console.log(colored)
|
|
61
|
+
process.exit(resp.status === 417 ? 1 : 0)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
console.log(text)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
main().catch((err) => {
|
|
68
|
+
console.error(pc.red(`Error: ${err.message}`))
|
|
69
|
+
process.exit(1)
|
|
70
|
+
})
|