@zenera/faker 1.1.20 → 1.1.22

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/dist/logger.js ADDED
@@ -0,0 +1,273 @@
1
+ import { bold, cyan, dim, green, red, yellow } from '@zenera/cli/lib';
2
+ import { mkdirSync } from 'node:fs';
3
+ import { writeFile } from 'node:fs/promises';
4
+ import { join, resolve } from 'node:path';
5
+ // ---------------------------------------------------------------------------
6
+ // Formatting Helpers
7
+ // ---------------------------------------------------------------------------
8
+ export function formatTimestamp(d) {
9
+ const pad = (n, w = 2) => String(n).padStart(w, '0');
10
+ const yyyy = d.getFullYear();
11
+ const mm = pad(d.getMonth() + 1);
12
+ const dd = pad(d.getDate());
13
+ const hh = pad(d.getHours());
14
+ const min = pad(d.getMinutes());
15
+ const ss = pad(d.getSeconds());
16
+ const ms = pad(d.getMilliseconds(), 3);
17
+ return `${yyyy}-${mm}-${dd} ${hh}:${min}:${ss}.${ms}`;
18
+ }
19
+ export function statusText(status) {
20
+ switch (status) {
21
+ case 200:
22
+ return 'OK';
23
+ case 201:
24
+ return 'Created';
25
+ case 202:
26
+ return 'Accepted';
27
+ case 204:
28
+ return 'No Content';
29
+ case 400:
30
+ return 'Bad Request';
31
+ case 401:
32
+ return 'Unauthorized';
33
+ case 403:
34
+ return 'Forbidden';
35
+ case 404:
36
+ return 'Not Found';
37
+ case 405:
38
+ return 'Method Not Allowed';
39
+ case 413:
40
+ return 'Payload Too Large';
41
+ case 500:
42
+ return 'Internal Server Error';
43
+ case 501:
44
+ return 'Not Implemented';
45
+ case 502:
46
+ return 'Bad Gateway';
47
+ case 504:
48
+ return 'Gateway Timeout';
49
+ default:
50
+ return status < 400 ? 'Success' : status < 500 ? 'Client Error' : 'Server Error';
51
+ }
52
+ }
53
+ export function colorForStatus(status) {
54
+ if (status < 300)
55
+ return green;
56
+ if (status < 400)
57
+ return cyan;
58
+ if (status < 500)
59
+ return yellow;
60
+ return red;
61
+ }
62
+ export function colorForErrorKind(kind) {
63
+ if (kind.startsWith('CLIENT_ERROR'))
64
+ return yellow;
65
+ if (kind === 'SUCCESS')
66
+ return green;
67
+ return (s) => red(bold(s));
68
+ }
69
+ // ---------------------------------------------------------------------------
70
+ // Markdown Request / Response Dump File
71
+ // ---------------------------------------------------------------------------
72
+ /**
73
+ * Saves a markdown file with all input information and output data for a request.
74
+ * Returns the absolute path of the generated markdown file.
75
+ */
76
+ export async function saveRequestDump(dir, data) {
77
+ mkdirSync(dir, { recursive: true });
78
+ const safePath = data.pathname.replace(/[^a-zA-Z0-9_-]/g, '_').replace(/^_+|_+$/g, '') || 'root';
79
+ const isoPrefix = data.timestamp.toISOString().replace(/[:.]/g, '-');
80
+ const filename = `${isoPrefix}-${data.method.toUpperCase()}-${safePath}-${data.id}.md`;
81
+ const fullPath = join(dir, filename);
82
+ const lines = [
83
+ `# Request: ${data.method.toUpperCase()} ${data.pathname}${data.search}`,
84
+ '',
85
+ `- **Request ID**: \`${data.id}\``,
86
+ `- **Timestamp**: ${data.timestamp.toISOString()}`,
87
+ `- **Status**: ${data.status} ${statusText(data.status)}`,
88
+ `- **Duration**: ${data.durationMs}ms`,
89
+ `- **Operation ID**: ${data.operationId ? `\`${data.operationId}\`` : '_None_'}`,
90
+ `- **Route Pattern**: ${data.routePath ? `\`${data.routePath}\`` : '_None_'}`,
91
+ `- **Cache**: ${data.cacheStatus ?? 'n/a'}`,
92
+ ];
93
+ if (data.errorKind) {
94
+ lines.push(`- **Error Kind**: \`${data.errorKind}\``);
95
+ }
96
+ if (data.regenerated) {
97
+ lines.push(`- **Regenerated**: Yes (attempts: ${data.regenAttempts ?? 1})`);
98
+ }
99
+ if (data.regenLimitHit) {
100
+ lines.push(`- **Regeneration Limit**: Hit (limit: ${data.regenLimit})`);
101
+ }
102
+ if (data.note) {
103
+ lines.push(`- **Note**: ${data.note}`);
104
+ }
105
+ lines.push('', '---', '', '## Request Info', '');
106
+ lines.push('### URL & Method', '```http', `${data.method.toUpperCase()} ${data.url}`, '```', '');
107
+ lines.push('### Path Parameters');
108
+ if (data.pathParams && Object.keys(data.pathParams).length > 0) {
109
+ lines.push('```json', JSON.stringify(data.pathParams, null, 2), '```');
110
+ }
111
+ else {
112
+ lines.push('_None_');
113
+ }
114
+ lines.push('');
115
+ lines.push('### Query Parameters');
116
+ if (data.query && Object.keys(data.query).length > 0) {
117
+ lines.push('```json', JSON.stringify(data.query, null, 2), '```');
118
+ }
119
+ else {
120
+ lines.push('_None_');
121
+ }
122
+ lines.push('');
123
+ lines.push('### Request Headers');
124
+ if (data.requestHeaders && Object.keys(data.requestHeaders).length > 0) {
125
+ lines.push('```json', JSON.stringify(data.requestHeaders, null, 2), '```');
126
+ }
127
+ else {
128
+ lines.push('_None_');
129
+ }
130
+ lines.push('');
131
+ lines.push('### Request Body');
132
+ if (data.requestBody !== undefined) {
133
+ lines.push('```json', typeof data.requestBody === 'string'
134
+ ? data.requestBody
135
+ : JSON.stringify(data.requestBody, null, 2), '```');
136
+ }
137
+ else {
138
+ lines.push('_Empty_');
139
+ }
140
+ lines.push('');
141
+ lines.push('---', '', '## Response Data', '');
142
+ lines.push('### Status', '```http', `HTTP/1.1 ${data.status} ${statusText(data.status)}`, '```', '');
143
+ lines.push('### Response Headers');
144
+ if (data.responseHeaders && Object.keys(data.responseHeaders).length > 0) {
145
+ lines.push('```json', JSON.stringify(data.responseHeaders, null, 2), '```');
146
+ }
147
+ else {
148
+ lines.push('_None_');
149
+ }
150
+ lines.push('');
151
+ lines.push('### Response Body');
152
+ if (data.responseBody !== undefined) {
153
+ lines.push('```json', typeof data.responseBody === 'string'
154
+ ? data.responseBody
155
+ : JSON.stringify(data.responseBody, null, 2), '```');
156
+ }
157
+ else {
158
+ lines.push('_Empty_');
159
+ }
160
+ lines.push('');
161
+ if (data.errorMessage || data.stderr || (data.issues && data.issues.length > 0)) {
162
+ lines.push('---', '', '## Error & Diagnostics', '');
163
+ if (data.errorKind) {
164
+ lines.push(`### Classification`, `\`${data.errorKind}\``, '');
165
+ }
166
+ if (data.errorMessage) {
167
+ lines.push(`### Error Message`, `\`${data.errorMessage}\``, '');
168
+ }
169
+ if (data.issues && data.issues.length > 0) {
170
+ lines.push('### Validation Issues', '```json', JSON.stringify(data.issues, null, 2), '```', '');
171
+ }
172
+ if (data.stderr) {
173
+ lines.push('### Stderr / Traceback', '```', data.stderr, '```', '');
174
+ }
175
+ }
176
+ await writeFile(fullPath, lines.join('\n'), 'utf8');
177
+ return resolve(fullPath);
178
+ }
179
+ // ---------------------------------------------------------------------------
180
+ // Log Line Formatter
181
+ // ---------------------------------------------------------------------------
182
+ export function formatLogLine(data, dumpPath) {
183
+ const ts = dim(`[${formatTimestamp(data.timestamp)}]`);
184
+ const method = cyan(bold(data.method.toUpperCase()));
185
+ const target = `${data.pathname}${data.search}`;
186
+ const statusCol = colorForStatus(data.status);
187
+ const status = statusCol(String(data.status));
188
+ const duration = dim(`${data.durationMs}ms`);
189
+ let tag = '';
190
+ if (data.errorKind) {
191
+ const tagCol = colorForErrorKind(data.errorKind);
192
+ tag = ` ${tagCol(`[${data.errorKind}]`)}`;
193
+ }
194
+ else if (data.status < 400) {
195
+ tag = ` ${green('[SUCCESS]')}`;
196
+ }
197
+ const note = data.note ? ` ${dim(`· ${data.note}`)}` : '';
198
+ const errDetail = data.errorMessage ? ` ${dim(`(${data.errorMessage})`)}` : '';
199
+ const firstLine = `${ts} ${method} ${target} ${status} ${duration}${tag}${note}${errDetail}`;
200
+ const lines = [firstLine];
201
+ if (data.stderr && data.stderr.trim()) {
202
+ const lastStderr = data.stderr.trim().split('\n').slice(-4).join('\n ');
203
+ lines.push(` ${dim('stderr:')} ${red(lastStderr)}`);
204
+ }
205
+ if (dumpPath) {
206
+ const fileUrl = `file://${resolve(dumpPath)}`;
207
+ lines.push(` ${dim('↳ dump:')} ${cyan(fileUrl)}`);
208
+ }
209
+ return lines.join('\n');
210
+ }
211
+ // ---------------------------------------------------------------------------
212
+ // Special Log Messages
213
+ // ---------------------------------------------------------------------------
214
+ export function formatRegenLimitHit(urlPath, limit, operationId) {
215
+ const ts = dim(`[${formatTimestamp(new Date())}]`);
216
+ const op = operationId ? ` (${operationId})` : '';
217
+ return `${ts} ${red(bold('[REGENERATION_LIMIT_HIT]'))} Maximum regeneration limit (${limit}) reached for URL "${urlPath}"${op}. Halting further regeneration attempts.`;
218
+ }
219
+ export function formatRegenerating(urlPath, attempt, limit, operationId, reason) {
220
+ const ts = dim(`[${formatTimestamp(new Date())}]`);
221
+ const op = operationId ? ` (${operationId})` : '';
222
+ const r = reason ? ` [${reason}]` : '';
223
+ return `${ts} ${yellow(bold('[REGENERATING]'))} Error in generator for "${urlPath}"${op}${r}. Attempting regeneration (${attempt}/${limit})...`;
224
+ }
225
+ export function formatRegenerated(urlPath, attempt, durationMs, operationId) {
226
+ const ts = dim(`[${formatTimestamp(new Date())}]`);
227
+ const op = operationId ? ` (${operationId})` : '';
228
+ return `${ts} ${green(bold('[REGENERATED]'))} Successfully regenerated generator for "${urlPath}"${op} in attempt ${attempt} (${durationMs}ms).`;
229
+ }
230
+ export class RequestHistory {
231
+ #successful = new Map();
232
+ #maxExamples;
233
+ constructor(maxExamples = 5) {
234
+ this.#maxExamples = maxExamples;
235
+ }
236
+ recordSuccess(operationKey, input, output) {
237
+ let list = this.#successful.get(operationKey);
238
+ if (!list) {
239
+ list = [];
240
+ this.#successful.set(operationKey, list);
241
+ }
242
+ list.push({ input, output });
243
+ if (list.length > this.#maxExamples) {
244
+ list.shift();
245
+ }
246
+ }
247
+ getSuccessful(operationKey, count = 2) {
248
+ const list = this.#successful.get(operationKey);
249
+ if (!list || list.length === 0) {
250
+ return [];
251
+ }
252
+ return list.slice(-count);
253
+ }
254
+ }
255
+ export class RegenerationLimiter {
256
+ #counts = new Map();
257
+ limit;
258
+ constructor(limit = 3) {
259
+ this.limit = limit;
260
+ }
261
+ countFor(url) {
262
+ return this.#counts.get(url) ?? 0;
263
+ }
264
+ isLimitHit(url) {
265
+ return (this.#counts.get(url) ?? 0) >= this.limit;
266
+ }
267
+ increment(url) {
268
+ const next = (this.#counts.get(url) ?? 0) + 1;
269
+ this.#counts.set(url, next);
270
+ return next;
271
+ }
272
+ }
273
+ //# sourceMappingURL=logger.js.map
package/dist/page.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ import { type Operation } from './spec.ts';
2
+ export declare function indexPage(operations: readonly Operation[]): string;
3
+ //# sourceMappingURL=page.d.ts.map