aipeek 0.2.2 → 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,252 @@
1
+ import type { ErrorEntry, LogEntry, NetworkRequest, RawState } from './types'
2
+ import { compactUI } from './compact'
3
+ import { truncate } from './util'
4
+
5
+ export function detail(raw: RawState, section: string, index: string | undefined, full: boolean): string | null {
6
+ switch (section) {
7
+ case 'ui': return full ? (raw.ui || null) : (compactUI(raw.ui) || null)
8
+ case 'console': return detailConsole(raw.console, index, full)
9
+ case 'network': return detailNetwork(raw.network, index, full)
10
+ case 'errors': return detailError(raw.errors, index, full)
11
+ case 'state': return detailState(raw.state, index, full)
12
+ default: return null
13
+ }
14
+ }
15
+
16
+ // --- Console ---
17
+
18
+ function detailConsole(logs: LogEntry[], index: string | undefined, full: boolean): string | null {
19
+ if (index === undefined) {
20
+ if (!logs.length)
21
+ return '(empty)'
22
+ return logs.map((log, i) => `[${i}] [${log.level}] ${truncate(log.text, 120)}`).join('\n')
23
+ }
24
+ const i = Number.parseInt(index)
25
+ if (Number.isNaN(i) || i < 0 || i >= logs.length)
26
+ return null
27
+ const log = logs[i]
28
+
29
+ if (full) {
30
+ const parts = [`[${log.level}] ${log.text}`]
31
+ if (log.timestamp)
32
+ parts.push(`timestamp: ${new Date(log.timestamp).toISOString()}`)
33
+ if (log.source)
34
+ parts.push(`source: ${log.source}`)
35
+ return parts.join('\n')
36
+ }
37
+
38
+ return `[${log.level}] ${truncate(log.text, 200)}`
39
+ }
40
+
41
+ // --- Network ---
42
+
43
+ function detailNetwork(requests: NetworkRequest[], index: string | undefined, full: boolean): string | null {
44
+ if (index === undefined) {
45
+ if (!requests.length)
46
+ return '(empty)'
47
+ return requests.map((req, i) => `[${i}] ${req.method} ${req.status} ${truncate(req.url, 80)} ${req.duration}ms${req.failed ? ' FAILED' : ''}`).join('\n')
48
+ }
49
+ const i = Number.parseInt(index)
50
+ if (Number.isNaN(i) || i < 0 || i >= requests.length)
51
+ return null
52
+ const req = requests[i]
53
+
54
+ const lines = [
55
+ `${req.method} ${req.url}`,
56
+ `status: ${req.status}`,
57
+ `duration: ${req.duration}ms`,
58
+ `type: ${req.resourceType}`,
59
+ ]
60
+ if (req.failed)
61
+ lines.push(`failed: ${req.failureText || 'true'}`)
62
+
63
+ if (full) {
64
+ if (req.requestHeaders && Object.keys(req.requestHeaders).length) {
65
+ lines.push('request-headers:')
66
+ for (const [k, v] of Object.entries(req.requestHeaders)) lines.push(` ${k}: ${v}`)
67
+ }
68
+ if (req.requestBody)
69
+ lines.push(`request-body:\n${req.requestBody}`)
70
+ if (req.responseHeaders && Object.keys(req.responseHeaders).length) {
71
+ lines.push('response-headers:')
72
+ for (const [k, v] of Object.entries(req.responseHeaders)) lines.push(` ${k}: ${v}`)
73
+ }
74
+ if (req.responseBody)
75
+ lines.push(`response-body:\n${req.responseBody}`)
76
+ }
77
+ else {
78
+ if (req.requestBody) {
79
+ lines.push(`request-body: ${byteSize(req.requestBody)}`)
80
+ if (req.requestSample) {
81
+ const schema = jsonSchema(req.requestSample)
82
+ if (schema)
83
+ lines.push(schema)
84
+ }
85
+ }
86
+ if (req.responseBody) {
87
+ if (req.status >= 400) {
88
+ lines.push(`response-body: ${byteSize(req.responseBody)} "${truncate(req.responseBody, 100)}"`)
89
+ }
90
+ else {
91
+ if (req.responseSample) {
92
+ lines.push(`response-body: ${byteSize(req.responseBody)}`)
93
+ const schema = jsonSchema(req.responseSample)
94
+ if (schema)
95
+ lines.push(schema)
96
+ }
97
+ else {
98
+ lines.push(`response-body: ${byteSize(req.responseBody)} ${truncate(req.responseBody, 100)}`)
99
+ }
100
+ }
101
+ }
102
+ }
103
+
104
+ return lines.join('\n')
105
+ }
106
+
107
+ // --- Errors ---
108
+
109
+ function detailError(errors: ErrorEntry[], index: string | undefined, full: boolean): string | null {
110
+ if (index === undefined) {
111
+ if (!errors.length)
112
+ return '(empty)'
113
+ return errors.map((err, i) => `[${i}] ${truncate(err.message, 120)}`).join('\n')
114
+ }
115
+ const i = Number.parseInt(index)
116
+ if (Number.isNaN(i) || i < 0 || i >= errors.length)
117
+ return null
118
+ const err = errors[i]
119
+
120
+ if (full) {
121
+ const lines = [err.message]
122
+ if (err.stack)
123
+ lines.push(err.stack)
124
+ if (err.source)
125
+ lines.push(`source: ${err.source}`)
126
+ if (err.line != null)
127
+ lines.push(`location: ${err.source || ''}:${err.line}:${err.column ?? 0}`)
128
+ return lines.join('\n')
129
+ }
130
+
131
+ const lines = [err.message]
132
+ if (err.stack) {
133
+ const appFrames = err.stack.split('\n')
134
+ .map(l => l.trim())
135
+ .filter(l => l.startsWith('at ') && !l.includes('node_modules'))
136
+ .slice(0, 3)
137
+ if (appFrames.length)
138
+ lines.push(...appFrames)
139
+ const totalApp = err.stack.split('\n').filter(l => l.trim().startsWith('at ') && !l.includes('node_modules')).length
140
+ if (totalApp > 3)
141
+ lines.push(` ... ${totalApp - 3} more app frames`)
142
+ }
143
+ if (err.line != null)
144
+ lines.push(`location: ${err.source || ''}:${err.line}:${err.column ?? 0}`)
145
+ return lines.join('\n')
146
+ }
147
+
148
+ // --- State ---
149
+
150
+ function detailState(state: Record<string, unknown>, name: string | undefined, full: boolean): string | null {
151
+ if (!name) {
152
+ const keys = Object.keys(state)
153
+ if (!keys.length)
154
+ return '(empty)'
155
+ return keys.map(k => `${k}: ${formatSummaryValue(state[k])}`).join('\n')
156
+ }
157
+ if (!(name in state))
158
+ return null
159
+ const value = state[name]
160
+ if (full) {
161
+ try {
162
+ return JSON.stringify(value, null, 2)
163
+ }
164
+ catch {
165
+ return String(value)
166
+ }
167
+ }
168
+ if (typeof value !== 'object' || value === null)
169
+ return `${name}: ${typeof value}`
170
+ const lines: string[] = []
171
+ for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
172
+ lines.push(`${k}: ${formatSummaryValue(v)}`)
173
+ }
174
+ return lines.join('\n')
175
+ }
176
+
177
+ function isArraySentinel(v: string): boolean {
178
+ if (!v.startsWith('Array(') || !v.endsWith(')'))
179
+ return false
180
+ const digits = v.slice(6, -1)
181
+ return digits.length > 0 && [...digits].every(c => c >= '0' && c <= '9')
182
+ }
183
+
184
+ function formatSummaryValue(v: unknown): string {
185
+ if (v === null || v === undefined)
186
+ return String(v)
187
+ if (typeof v === 'string') {
188
+ if (isArraySentinel(v)) // "Array(N)" from boundedSnapshot — leave untruncated
189
+ return v
190
+ return v.length > 80 ? `${v.slice(0, 80)}…` : v
191
+ }
192
+ if (typeof v === 'number' || typeof v === 'boolean')
193
+ return String(v)
194
+ if (typeof v === 'object') {
195
+ const s = JSON.stringify(v)
196
+ return s.length > 80 ? `${s.slice(0, 80)}…` : s
197
+ }
198
+ return String(v)
199
+ }
200
+
201
+ // --- JSON schema fallback ---
202
+
203
+ function jsonSchema(sample: string): string | null {
204
+ try {
205
+ return schemaOf(JSON.parse(sample), 0)
206
+ }
207
+ catch {
208
+ return null
209
+ }
210
+ }
211
+
212
+ function schemaOf(v: unknown, d: number): string {
213
+ if (v === null)
214
+ return 'null'
215
+ if (typeof v === 'string')
216
+ return 'string'
217
+ if (typeof v === 'number')
218
+ return 'number'
219
+ if (typeof v === 'boolean')
220
+ return 'boolean'
221
+ if (Array.isArray(v)) {
222
+ if (!v.length)
223
+ return '[]'
224
+ if (d >= 3)
225
+ return '[…]'
226
+ return `${schemaOf(v[0], d + 1)}[]`
227
+ }
228
+ if (typeof v === 'object') {
229
+ if (d >= 3)
230
+ return '{…}'
231
+ const entries = Object.entries(v as Record<string, unknown>)
232
+ if (!entries.length)
233
+ return '{}'
234
+ const max = d === 0 ? 12 : 6
235
+ const fields = entries.slice(0, max).map(([k, val]) => `${k}: ${schemaOf(val, d + 1)}`)
236
+ if (entries.length > max)
237
+ fields.push(`… ${entries.length - max} more`)
238
+ return `{ ${fields.join(', ')} }`
239
+ }
240
+ return typeof v
241
+ }
242
+
243
+ // --- Utils ---
244
+
245
+ function byteSize(s: string): string {
246
+ const bytes = new TextEncoder().encode(s).length
247
+ if (bytes < 1024)
248
+ return `${bytes}B`
249
+ if (bytes < 1024 * 1024)
250
+ return `${(bytes / 1024).toFixed(1)}KB`
251
+ return `${(bytes / (1024 * 1024)).toFixed(1)}MB`
252
+ }
@@ -0,0 +1,38 @@
1
+ import type { DiffResult, RawState } from './types'
2
+
3
+ export function diffState(prev: RawState | null, curr: RawState): DiffResult {
4
+ if (!prev) {
5
+ return {
6
+ newErrors: curr.console.filter(l => l.level === 'error'),
7
+ newExceptions: [...curr.errors],
8
+ newFailedRequests: curr.network.filter(r => r.status >= 400 || r.failed),
9
+ uiGone: false,
10
+ clean: curr.console.filter(l => l.level === 'error').length === 0
11
+ && curr.errors.length === 0
12
+ && curr.network.filter(r => r.status >= 400 || r.failed).length === 0,
13
+ }
14
+ }
15
+
16
+ const prevErrorTexts = new Set(prev.console.filter(l => l.level === 'error').map(l => l.text))
17
+ const newErrors = curr.console.filter(l => l.level === 'error' && !prevErrorTexts.has(l.text))
18
+
19
+ const prevExceptionMsgs = new Set(prev.errors.map(e => e.message))
20
+ const newExceptions = curr.errors.filter(e => !prevExceptionMsgs.has(e.message))
21
+
22
+ const prevFailedKeys = new Set(
23
+ prev.network.filter(r => r.status >= 400 || r.failed).map(r => `${r.method}|${r.url}|${r.status}`),
24
+ )
25
+ const newFailedRequests = curr.network
26
+ .filter(r => r.status >= 400 || r.failed)
27
+ .filter(r => !prevFailedKeys.has(`${r.method}|${r.url}|${r.status}`))
28
+
29
+ const uiGone = prev.ui.trim().length > 0 && curr.ui.trim().length === 0
30
+
31
+ return {
32
+ newErrors,
33
+ newExceptions,
34
+ newFailedRequests,
35
+ uiGone,
36
+ clean: newErrors.length === 0 && newExceptions.length === 0 && newFailedRequests.length === 0 && !uiGone,
37
+ }
38
+ }
@@ -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,75 @@
1
+ export interface LogEntry {
2
+ level: 'error' | 'warn' | 'info' | 'debug' | 'log'
3
+ text: string
4
+ timestamp?: number
5
+ source?: string
6
+ }
7
+
8
+ export interface NetworkRequest {
9
+ method: string
10
+ url: string
11
+ status: number
12
+ duration: number
13
+ resourceType: string
14
+ requestHeaders?: Record<string, string>
15
+ responseHeaders?: Record<string, string>
16
+ requestBody?: string
17
+ requestSample?: string
18
+ responseBody?: string
19
+ responseSample?: string
20
+ failed?: boolean
21
+ failureText?: string
22
+ }
23
+
24
+ export interface ErrorEntry {
25
+ message: string
26
+ stack?: string
27
+ source?: string
28
+ line?: number
29
+ column?: number
30
+ }
31
+
32
+ export interface RawState {
33
+ url: string
34
+ ui: string
35
+ console: LogEntry[]
36
+ network: NetworkRequest[]
37
+ errors: ErrorEntry[]
38
+ state: Record<string, unknown>
39
+ timestamp: number
40
+ }
41
+
42
+ export interface CompactState {
43
+ url: string
44
+ ui: string
45
+ console: string
46
+ network: string
47
+ errors: string
48
+ state: string
49
+ timestamp: number
50
+ counts: {
51
+ console: number
52
+ network: number
53
+ errors: number
54
+ state: number
55
+ }
56
+ }
57
+
58
+ export interface Assertion {
59
+ name: string
60
+ pass: boolean
61
+ detail?: string
62
+ }
63
+
64
+ export interface CheckResult {
65
+ pass: boolean
66
+ assertions: Assertion[]
67
+ }
68
+
69
+ export interface DiffResult {
70
+ newErrors: LogEntry[]
71
+ newExceptions: ErrorEntry[]
72
+ newFailedRequests: NetworkRequest[]
73
+ uiGone: boolean
74
+ clean: boolean
75
+ }
@@ -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
+ })