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.
@@ -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
+ }
@@ -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
+ })