jetic-cli 0.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.
Files changed (47) hide show
  1. package/dist/commands/config.d.ts +3 -0
  2. package/dist/commands/config.d.ts.map +1 -0
  3. package/dist/commands/config.js +36 -0
  4. package/dist/commands/config.js.map +1 -0
  5. package/dist/commands/dev.d.ts +3 -0
  6. package/dist/commands/dev.d.ts.map +1 -0
  7. package/dist/commands/dev.js +478 -0
  8. package/dist/commands/dev.js.map +1 -0
  9. package/dist/commands/init.d.ts +3 -0
  10. package/dist/commands/init.d.ts.map +1 -0
  11. package/dist/commands/init.js +91 -0
  12. package/dist/commands/init.js.map +1 -0
  13. package/dist/commands/inspect.d.ts +3 -0
  14. package/dist/commands/inspect.d.ts.map +1 -0
  15. package/dist/commands/inspect.js +73 -0
  16. package/dist/commands/inspect.js.map +1 -0
  17. package/dist/commands/memory.d.ts +3 -0
  18. package/dist/commands/memory.d.ts.map +1 -0
  19. package/dist/commands/memory.js +96 -0
  20. package/dist/commands/memory.js.map +1 -0
  21. package/dist/commands/scan.d.ts +3 -0
  22. package/dist/commands/scan.d.ts.map +1 -0
  23. package/dist/commands/scan.js +54 -0
  24. package/dist/commands/scan.js.map +1 -0
  25. package/dist/commands/simulate-workflow.d.ts +46 -0
  26. package/dist/commands/simulate-workflow.d.ts.map +1 -0
  27. package/dist/commands/simulate-workflow.js +671 -0
  28. package/dist/commands/simulate-workflow.js.map +1 -0
  29. package/dist/commands/simulate.d.ts +3 -0
  30. package/dist/commands/simulate.d.ts.map +1 -0
  31. package/dist/commands/simulate.js +482 -0
  32. package/dist/commands/simulate.js.map +1 -0
  33. package/dist/index.d.ts +3 -0
  34. package/dist/index.d.ts.map +1 -0
  35. package/dist/index.js +25 -0
  36. package/dist/index.js.map +1 -0
  37. package/package.json +30 -0
  38. package/src/commands/config.ts +36 -0
  39. package/src/commands/dev.ts +451 -0
  40. package/src/commands/init.ts +64 -0
  41. package/src/commands/inspect.ts +47 -0
  42. package/src/commands/memory.ts +71 -0
  43. package/src/commands/scan.ts +22 -0
  44. package/src/commands/simulate-workflow.ts +794 -0
  45. package/src/commands/simulate.ts +512 -0
  46. package/src/index.ts +26 -0
  47. package/tsconfig.json +8 -0
@@ -0,0 +1,512 @@
1
+ import { Command } from 'commander';
2
+ import { loadConfig, readJsonSync } from '@jetic/core';
3
+ import { BehavioralModel, Environment } from '@jetic/model';
4
+ import { EndpointSimulator, SimulationResult } from '@jetic/simulator';
5
+ import * as path from 'path';
6
+ import * as readline from 'readline';
7
+ import { simulateWorkflowCommand } from './simulate-workflow';
8
+
9
+ // ─── ANSI Helpers ─────────────────────────────────────────────────────────────
10
+
11
+ const c = {
12
+ reset: '\x1b[0m',
13
+ bold: '\x1b[1m',
14
+ dim: '\x1b[2m',
15
+ italic: '\x1b[3m',
16
+ cyan: '\x1b[36m',
17
+ green: '\x1b[32m',
18
+ red: '\x1b[31m',
19
+ yellow: '\x1b[33m',
20
+ magenta: '\x1b[35m',
21
+ white: '\x1b[37m',
22
+ bgRed: '\x1b[41m',
23
+ bgGreen: '\x1b[42m',
24
+ bgCyan: '\x1b[46m',
25
+ black: '\x1b[30m',
26
+ };
27
+
28
+ const TICK = `${c.green}✓${c.reset}`;
29
+ const CROSS = `${c.red}✗${c.reset}`;
30
+ const SKIP = `${c.yellow}⊘${c.reset}`;
31
+ const SEPARATOR = `${c.dim}──────────────────────────────────────────────────${c.reset}`;
32
+
33
+ // ─── Spinner ──────────────────────────────────────────────────────────────────
34
+
35
+ class Spinner {
36
+ private frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
37
+ private idx = 0;
38
+ private interval: NodeJS.Timeout | null = null;
39
+ private message: string = '';
40
+
41
+ start(message: string) {
42
+ this.message = message;
43
+ this.idx = 0;
44
+ this.interval = setInterval(() => {
45
+ const frame = this.frames[this.idx % this.frames.length];
46
+ process.stdout.write(`\r ${c.cyan}${frame}${c.reset} ${this.message}`);
47
+ this.idx++;
48
+ }, 80);
49
+ }
50
+
51
+ update(message: string) {
52
+ this.message = message;
53
+ }
54
+
55
+ stop(finalMessage?: string) {
56
+ if (this.interval) {
57
+ clearInterval(this.interval);
58
+ this.interval = null;
59
+ }
60
+ if (finalMessage) {
61
+ process.stdout.write(`\r${finalMessage}\x1b[K\n`);
62
+ } else {
63
+ process.stdout.write(`\r\x1b[K`);
64
+ }
65
+ }
66
+ }
67
+
68
+ // ─── Interactive Environment Selector ─────────────────────────────────────────
69
+
70
+ async function selectEnvironment(environments: Environment[]): Promise<Environment> {
71
+ if (environments.length === 0) {
72
+ console.log(` ${c.yellow}⚠${c.reset} No environments defined in model.json`);
73
+ console.log(` ${c.dim}Using default: http://localhost:3000${c.reset}\n`);
74
+ return { name: 'default', baseUrl: 'http://localhost:3000' };
75
+ }
76
+
77
+ if (environments.length === 1) {
78
+ const env = environments[0];
79
+ console.log(` ${c.cyan}🌍${c.reset} Environment: ${c.bold}${env.name}${c.reset} ${c.dim}→ ${env.baseUrl}${c.reset}\n`);
80
+ return env;
81
+ }
82
+
83
+ return new Promise<Environment>((resolve) => {
84
+ let selectedIndex = 0;
85
+
86
+ const renderOptions = () => {
87
+ // Move cursor up to overwrite previous render
88
+ if (selectedIndex >= 0) {
89
+ process.stdout.write(`\x1b[${environments.length}A`);
90
+ }
91
+
92
+ for (let i = 0; i < environments.length; i++) {
93
+ const env = environments[i];
94
+ const isSelected = i === selectedIndex;
95
+ const radio = isSelected ? `${c.cyan}◉${c.reset}` : `${c.dim}○${c.reset}`;
96
+ const name = isSelected ? `${c.bold}${c.cyan}${env.name}${c.reset}` : `${c.dim}${env.name}${c.reset}`;
97
+ const url = `${c.dim}→ ${env.baseUrl}${c.reset}`;
98
+ process.stdout.write(`\r ${radio} ${name.padEnd(isSelected ? 30 : 20)} ${url}\x1b[K\n`);
99
+ }
100
+ };
101
+
102
+ console.log(`\n ${c.cyan}🌍${c.reset} Select environment:\n`);
103
+
104
+ // Initial render
105
+ for (let i = 0; i < environments.length; i++) {
106
+ const env = environments[i];
107
+ const isSelected = i === selectedIndex;
108
+ const radio = isSelected ? `${c.cyan}◉${c.reset}` : `${c.dim}○${c.reset}`;
109
+ const name = isSelected ? `${c.bold}${c.cyan}${env.name}${c.reset}` : `${c.dim}${env.name}${c.reset}`;
110
+ const url = `${c.dim}→ ${env.baseUrl}${c.reset}`;
111
+ console.log(` ${radio} ${name.padEnd(isSelected ? 30 : 20)} ${url}`);
112
+ }
113
+
114
+ console.log(`\n ${c.dim}Press ↑/↓ to navigate, Enter to select${c.reset}`);
115
+
116
+ // Enable raw mode for key capture
117
+ if (process.stdin.isTTY) {
118
+ process.stdin.setRawMode(true);
119
+ }
120
+ process.stdin.resume();
121
+ process.stdin.setEncoding('utf8');
122
+
123
+ const onKeyPress = (key: string) => {
124
+ // Ctrl+C
125
+ if (key === '\u0003') {
126
+ process.stdin.setRawMode(false);
127
+ process.stdin.removeListener('data', onKeyPress);
128
+ process.exit(0);
129
+ }
130
+
131
+ // Arrow keys are escape sequences
132
+ if (key === '\u001b[A') {
133
+ // Up arrow
134
+ selectedIndex = Math.max(0, selectedIndex - 1);
135
+ // Move back up past the hint line and options
136
+ process.stdout.write(`\x1b[${environments.length + 2}A`);
137
+ // Re-render
138
+ for (let i = 0; i < environments.length; i++) {
139
+ const env = environments[i];
140
+ const isSelected = i === selectedIndex;
141
+ const radio = isSelected ? `${c.cyan}◉${c.reset}` : `${c.dim}○${c.reset}`;
142
+ const name = isSelected ? `${c.bold}${c.cyan}${env.name}${c.reset}` : `${c.dim}${env.name}${c.reset}`;
143
+ const url = `${c.dim}→ ${env.baseUrl}${c.reset}`;
144
+ process.stdout.write(` ${radio} ${name} ${url}\x1b[K\n`);
145
+ }
146
+ process.stdout.write(`\n ${c.dim}Press ↑/↓ to navigate, Enter to select${c.reset}\n`);
147
+ } else if (key === '\u001b[B') {
148
+ // Down arrow
149
+ selectedIndex = Math.min(environments.length - 1, selectedIndex + 1);
150
+ process.stdout.write(`\x1b[${environments.length + 2}A`);
151
+ for (let i = 0; i < environments.length; i++) {
152
+ const env = environments[i];
153
+ const isSelected = i === selectedIndex;
154
+ const radio = isSelected ? `${c.cyan}◉${c.reset}` : `${c.dim}○${c.reset}`;
155
+ const name = isSelected ? `${c.bold}${c.cyan}${env.name}${c.reset}` : `${c.dim}${env.name}${c.reset}`;
156
+ const url = `${c.dim}→ ${env.baseUrl}${c.reset}`;
157
+ process.stdout.write(` ${radio} ${name} ${url}\x1b[K\n`);
158
+ }
159
+ process.stdout.write(`\n ${c.dim}Press ↑/↓ to navigate, Enter to select${c.reset}\n`);
160
+ } else if (key === '\r' || key === '\n') {
161
+ // Enter
162
+ if (process.stdin.isTTY) {
163
+ process.stdin.setRawMode(false);
164
+ }
165
+ process.stdin.removeListener('data', onKeyPress);
166
+ process.stdin.pause();
167
+
168
+ const selected = environments[selectedIndex];
169
+ // Clear the hint line and show selection
170
+ process.stdout.write(`\r\x1b[K`);
171
+ console.log(`\n ${c.green}✓${c.reset} Selected: ${c.bold}${selected.name}${c.reset} ${c.dim}→ ${selected.baseUrl}${c.reset}\n`);
172
+ resolve(selected);
173
+ }
174
+ };
175
+
176
+ process.stdin.on('data', onKeyPress);
177
+ });
178
+ }
179
+
180
+ // ─── Result Rendering ─────────────────────────────────────────────────────────
181
+
182
+ function renderSingleResult(result: SimulationResult, verbose: boolean = true): void {
183
+ const methodColor = getMethodColor(result.method);
184
+ const methodStr = `${methodColor}${result.method.padEnd(6)}${c.reset}`;
185
+
186
+ if (result.skipped) {
187
+ console.log(` ${SKIP} ${methodStr} ${result.path} ${c.dim}(skipped: ${result.skipReason})${c.reset}`);
188
+ return;
189
+ }
190
+
191
+ if (result.error) {
192
+ console.log(` ${CROSS} ${methodStr} ${result.path} ${c.red}ERROR${c.reset}`);
193
+ console.log(` ${c.dim} └─ ${result.error}${c.reset}`);
194
+ return;
195
+ }
196
+
197
+ const statusStr = formatStatus(result.responseStatus);
198
+ const timeStr = `${c.dim}${result.responseTimeMs}ms${c.reset}`;
199
+
200
+ if (!verbose) {
201
+ const icon = result.passed ? TICK : CROSS;
202
+ console.log(` ${icon} ${methodStr} ${result.path.padEnd(25)} ${statusStr} ${timeStr}`);
203
+ return;
204
+ }
205
+
206
+ // ── Verbose single-endpoint output ──────────────────────────────────────
207
+ console.log(` ${c.cyan}📡${c.reset} ${c.bold}${result.method} ${result.path}${c.reset}`);
208
+
209
+ // Request body
210
+ if (result.requestBody && Object.keys(result.requestBody).length > 0) {
211
+ console.log(` ${c.dim}├─${c.reset} ${c.dim}Generating request data...${c.reset}`);
212
+ const bodyStr = JSON.stringify(result.requestBody, null, 2);
213
+ const lines = bodyStr.split('\n');
214
+ for (const line of lines) {
215
+ console.log(` ${c.dim}│${c.reset} ${c.yellow}${line}${c.reset}`);
216
+ }
217
+ }
218
+
219
+ // Headers
220
+ const headerKeys = Object.keys(result.requestHeaders);
221
+ if (headerKeys.length > 0) {
222
+ const headerStr = headerKeys.map((k) => {
223
+ let val = result.requestHeaders[k];
224
+ // Truncate long values (like auth tokens)
225
+ if (val.length > 40) val = val.substring(0, 37) + '...';
226
+ return `${k}: ${val}`;
227
+ }).join(', ');
228
+ console.log(` ${c.dim}├─${c.reset} Headers: ${c.dim}${headerStr}${c.reset}`);
229
+ }
230
+
231
+ // Response status
232
+ console.log(` ${c.dim}├─${c.reset} Response: ${statusStr} ${c.dim}(${result.responseTimeMs}ms)${c.reset}`);
233
+
234
+ // ── Response Body ───────────────────────────────────────────────────────
235
+ console.log(` ${c.dim}├─${c.reset} ${c.bold}Response Body:${c.reset}`);
236
+ if (result.responseBody !== null && result.responseBody !== undefined) {
237
+ const bodyStr = typeof result.responseBody === 'string'
238
+ ? result.responseBody
239
+ : JSON.stringify(result.responseBody, null, 2);
240
+ const bodyLines = bodyStr.split('\n');
241
+ for (const line of bodyLines) {
242
+ console.log(` ${c.dim}│${c.reset} ${c.cyan}${line}${c.reset}`);
243
+ }
244
+ } else {
245
+ console.log(` ${c.dim}│${c.reset} ${c.dim}(empty response)${c.reset}`);
246
+ }
247
+
248
+ // ── Schema Validation ──────────────────────────────────────────────────
249
+ if (result.validation.fieldValidations.length > 0) {
250
+ console.log(` ${c.dim}├─${c.reset} ${c.bold}Schema Validation:${c.reset} ${c.dim}(${result.validation.passedFields}/${result.validation.totalFields} fields match)${c.reset}`);
251
+
252
+ for (const field of result.validation.fieldValidations) {
253
+ if (field.passed) {
254
+ // Field matches
255
+ const valuePreview = formatValuePreview(field.actualValue);
256
+ const resolvedHint = field.resolvedPath
257
+ ? ` ${c.dim}(resolved via ${c.italic}${field.resolvedPath}${c.reset}${c.dim})${c.reset}`
258
+ : '';
259
+ console.log(` ${c.dim}│${c.reset} ${TICK} ${c.bold}${field.field}${c.reset}${resolvedHint}`);
260
+ console.log(` ${c.dim}│${c.reset} ${c.dim}Type:${c.reset} ${c.green}${field.actualType}${c.reset} ${c.dim}(expected ${field.expectedType})${c.reset}`);
261
+ console.log(` ${c.dim}│${c.reset} ${c.dim}Value:${c.reset} ${c.cyan}${valuePreview}${c.reset}`);
262
+ } else {
263
+ // Field does NOT match
264
+ const valuePreview = formatValuePreview(field.actualValue);
265
+ console.log(` ${c.dim}│${c.reset} ${CROSS} ${c.bold}${c.red}${field.field}${c.reset} ${c.red}← MISMATCH${c.reset}`);
266
+ console.log(` ${c.dim}│${c.reset} ${c.dim}Expected type:${c.reset} ${c.green}${field.expectedType}${c.reset}`);
267
+ console.log(` ${c.dim}│${c.reset} ${c.dim}Actual type:${c.reset} ${c.red}${field.actualType}${c.reset}`);
268
+ console.log(` ${c.dim}│${c.reset} ${c.dim}Actual value:${c.reset} ${c.red}${valuePreview}${c.reset}`);
269
+ if (field.actualType === 'undefined') {
270
+ console.log(` ${c.dim}│${c.reset} ${c.yellow}⚠ Field is missing from the response${c.reset}`);
271
+ } else if (field.actualType === 'null') {
272
+ console.log(` ${c.dim}│${c.reset} ${c.yellow}⚠ Field is null, expected ${field.expectedType}${c.reset}`);
273
+ }
274
+ }
275
+ }
276
+ } else {
277
+ console.log(` ${c.dim}├─${c.reset} ${c.dim}Schema Validation: no schema defined in model for this response${c.reset}`);
278
+ }
279
+
280
+ // ── Final Verdict ──────────────────────────────────────────────────────
281
+ const verdictIcon = result.passed ? TICK : CROSS;
282
+ const verdictText = result.passed
283
+ ? `${c.green}${c.bold}PASSED${c.reset}`
284
+ : `${c.red}${c.bold}FAILED${c.reset}`;
285
+
286
+ const reasons: string[] = [];
287
+ // if (!result.validation.statusPassed) {
288
+ // reasons.push(`status ${result.responseStatus} ≠ ${result.validation.expectedStatus}`);
289
+ //}
290
+ if (result.validation.failedFields > 0) {
291
+ reasons.push(`${result.validation.failedFields} field(s) mismatched`);
292
+ }
293
+
294
+ const reasonStr = reasons.length > 0
295
+ ? ` ${c.dim}(${reasons.join(', ')})${c.reset}`
296
+ : '';
297
+ const fieldSummary = result.validation.totalFields > 0
298
+ ? ` ${c.dim}• ${result.validation.passedFields}/${result.validation.totalFields} fields${c.reset}`
299
+ : '';
300
+ console.log(` ${c.dim}└─${c.reset} ${verdictIcon} ${verdictText}${fieldSummary}${reasonStr}`);
301
+ console.log('');
302
+ }
303
+
304
+ /**
305
+ * Format a value for display preview — truncate long strings/objects.
306
+ */
307
+ function formatValuePreview(value: any): string {
308
+ if (value === undefined) return 'undefined';
309
+ if (value === null) return 'null';
310
+ if (typeof value === 'string') {
311
+ return value.length > 60 ? `"${value.substring(0, 57)}..."` : `"${value}"`;
312
+ }
313
+ if (typeof value === 'object') {
314
+ const str = JSON.stringify(value);
315
+ return str.length > 80 ? str.substring(0, 77) + '...' : str;
316
+ }
317
+ return String(value);
318
+ }
319
+
320
+
321
+ function renderSummary(summary: {
322
+ passed: number;
323
+ failed: number;
324
+ skipped: number;
325
+ totalTimeMs: number;
326
+ results: SimulationResult[];
327
+ }): void {
328
+ console.log('');
329
+ // console.log(SEPARATOR);
330
+
331
+ const elapsed = (summary.totalTimeMs / 1000).toFixed(1);
332
+ const allPassed = summary.failed === 0;
333
+ const icon = allPassed ? `${c.green}✅${c.reset}` : `${c.red}❌${c.reset}`;
334
+ const label = allPassed ? `${c.green}Simulation Complete!${c.reset}` : `${c.red}Simulation Complete (with failures)${c.reset}`;
335
+
336
+ console.log(` ${icon} ${c.bold}${label}${c.reset} ${c.dim}(${elapsed}s)${c.reset}`);
337
+ console.log('');
338
+
339
+ // Individual results (compact)
340
+ for (const result of summary.results) {
341
+ renderSingleResult(result, false);
342
+ }
343
+
344
+ console.log('');
345
+ // console.log(SEPARATOR);
346
+
347
+ const parts: string[] = [];
348
+ if (summary.passed > 0) parts.push(`${c.green}${summary.passed} passed${c.reset}`);
349
+ if (summary.failed > 0) parts.push(`${c.red}${summary.failed} failed${c.reset}`);
350
+ if (summary.skipped > 0) parts.push(`${c.yellow}${summary.skipped} skipped${c.reset}`);
351
+
352
+ console.log(` ${parts.join(` ${c.dim}│${c.reset} `)}`);
353
+ // console.log(SEPARATOR);
354
+ console.log('');
355
+ }
356
+
357
+ function getMethodColor(method: string): string {
358
+ switch (method.toUpperCase()) {
359
+ case 'GET': return c.green;
360
+ case 'POST': return c.yellow;
361
+ case 'PUT': return c.cyan;
362
+ case 'PATCH': return c.magenta;
363
+ case 'DELETE': return c.red;
364
+ default: return c.white;
365
+ }
366
+ }
367
+
368
+ function formatStatus(status: number): string {
369
+ if (status >= 200 && status < 300) return `${c.green}${status}${c.reset}`;
370
+ if (status >= 300 && status < 400) return `${c.yellow}${status}${c.reset}`;
371
+ if (status >= 400 && status < 500) return `${c.red}${status}${c.reset}`;
372
+ if (status >= 500) return `${c.bgRed}${c.white} ${status} ${c.reset}`;
373
+ return `${c.dim}${status}${c.reset}`;
374
+ }
375
+
376
+ // ─── Progress Bar ─────────────────────────────────────────────────────────────
377
+
378
+ function buildProgressBar(current: number, total: number): string {
379
+ const width = 24;
380
+ const filled = Math.round((current / total) * width);
381
+ const empty = width - filled;
382
+ return `${c.magenta}${'━'.repeat(filled)}${c.dim}${'░'.repeat(empty)}${c.reset}`;
383
+ }
384
+
385
+ // ─── Load Model ───────────────────────────────────────────────────────────────
386
+
387
+ function loadModel(): BehavioralModel | null {
388
+ const config = loadConfig();
389
+ const modelPath = path.join(config.jeticDir, 'model.json');
390
+ return readJsonSync<BehavioralModel>(modelPath);
391
+ }
392
+
393
+ // ─── Commands ─────────────────────────────────────────────────────────────────
394
+
395
+ export const simulateCommand = new Command('simulate')
396
+ .description('Simulate API endpoints against a live server');
397
+
398
+ simulateCommand.addCommand(simulateWorkflowCommand);
399
+
400
+ simulateCommand
401
+ .command('endpoint [method] [endpointPath]')
402
+ .description('Simulate a specific endpoint or all endpoints')
403
+ .option('--all', 'Simulate all endpoints')
404
+ .option('--verbose', 'Show detailed request/response info', false)
405
+ .action(async (method: string | undefined, endpointPath: string | undefined, options: { all?: boolean; verbose?: boolean }) => {
406
+ // Banner
407
+ console.log('');
408
+ console.log(` ${c.bgCyan}${c.black}${c.bold} JETIC ${c.reset} ${c.cyan}${c.bold}Endpoint Simulator${c.reset}`);
409
+ // console.log(` ${SEPARATOR}`);
410
+ console.log('');
411
+
412
+ // Load model
413
+ const model = loadModel();
414
+ if (!model) {
415
+ console.log(` ${c.red}✗${c.reset} No behavioral model found.`);
416
+ console.log(` ${c.dim}Run \`jetic scan\` first to generate the model.${c.reset}\n`);
417
+ process.exit(1);
418
+ }
419
+
420
+ console.log(` ${c.dim}Model v${model.version} • ${model.endpoints.length} endpoints${c.reset}\n`);
421
+
422
+ // Environment selection
423
+ const environments = model.environments || [];
424
+ const selectedEnv = await selectEnvironment(environments);
425
+
426
+ // ── Check Backend Health ───────────────────────────────────────────
427
+ const checkSpinner = new Spinner();
428
+ checkSpinner.start(`Checking if backend is active at ${selectedEnv.baseUrl}...`);
429
+ try {
430
+ await fetch(selectedEnv.baseUrl);
431
+ checkSpinner.stop(` ${c.green}✓${c.reset} Backend is active`);
432
+ console.log('');
433
+ } catch (e) {
434
+ checkSpinner.stop();
435
+ console.log(` ${c.red}✗${c.reset} Backend is unreachable at ${c.bold}${selectedEnv.baseUrl}${c.reset}`);
436
+ console.log(` ${c.yellow}⚠${c.reset} Please run/initialize your backend project and try again.\n`);
437
+ process.exit(1);
438
+ }
439
+
440
+ // Create simulator
441
+ const simulator = new EndpointSimulator(model, selectedEnv);
442
+ const spinner = new Spinner();
443
+
444
+ if (options.all) {
445
+ // ── Simulate ALL endpoints ──────────────────────────────────────
446
+ console.log(` ${c.magenta}🚀${c.reset} Simulating ${c.bold}${model.endpoints.length}${c.reset} endpoints...\n`);
447
+
448
+ const results: SimulationResult[] = [];
449
+ const startTime = Date.now();
450
+
451
+ for (let i = 0; i < model.endpoints.length; i++) {
452
+ const ep = model.endpoints[i];
453
+ const label = `${ep.method} ${ep.path}`;
454
+ const progress = buildProgressBar(i, model.endpoints.length);
455
+
456
+ spinner.start(`${progress} ${c.dim}${i + 1}/${model.endpoints.length}${c.reset} ${c.dim}⏳ ${label}${c.reset}`);
457
+
458
+ const result = await simulator.simulateEndpoint(ep);
459
+ results.push(result);
460
+
461
+ const icon = result.skipped ? SKIP : (result.passed ? TICK : CROSS);
462
+ const statusStr = result.skipped
463
+ ? `${c.dim}skipped${c.reset}`
464
+ : formatStatus(result.responseStatus);
465
+ const timeStr = result.skipped ? '' : `${c.dim}${result.responseTimeMs}ms${c.reset}`;
466
+
467
+ spinner.stop(` ${buildProgressBar(i + 1, model.endpoints.length)} ${c.dim}${i + 1}/${model.endpoints.length}${c.reset} ${icon} ${label} ${statusStr} ${timeStr}`);
468
+ }
469
+
470
+ const totalTimeMs = Date.now() - startTime;
471
+
472
+ renderSummary({
473
+ passed: results.filter((r) => r.passed).length,
474
+ failed: results.filter((r) => !r.passed && !r.skipped).length,
475
+ skipped: results.filter((r) => r.skipped).length,
476
+ totalTimeMs,
477
+ results,
478
+ });
479
+
480
+ } else if (method && endpointPath) {
481
+ // ── Simulate SINGLE endpoint ────────────────────────────────────
482
+ const ep = model.endpoints.find(
483
+ (e) => e.method.toUpperCase() === method.toUpperCase() && e.path === endpointPath
484
+ );
485
+
486
+ if (!ep) {
487
+ console.log(` ${c.red}✗${c.reset} Endpoint ${c.bold}${method.toUpperCase()} ${endpointPath}${c.reset} not found in model.\n`);
488
+ console.log(` ${c.dim}Available endpoints:${c.reset}`);
489
+ for (const e of model.endpoints) {
490
+ console.log(` ${c.dim}•${c.reset} ${getMethodColor(e.method)}${e.method}${c.reset} ${e.path}`);
491
+ }
492
+ console.log('');
493
+ process.exit(1);
494
+ }
495
+
496
+ spinner.start(`Simulating ${c.bold}${ep.method} ${ep.path}${c.reset}...`);
497
+
498
+ const result = await simulator.simulateEndpoint(ep);
499
+
500
+ spinner.stop();
501
+ console.log('');
502
+ renderSingleResult(result, true);
503
+
504
+ } else {
505
+ console.log(` ${c.red}✗${c.reset} Please specify an endpoint or use ${c.bold}--all${c.reset}\n`);
506
+ console.log(` ${c.dim}Usage:${c.reset}`);
507
+ console.log(` ${c.cyan}jetic simulate endpoint POST /register${c.reset}`);
508
+ console.log(` ${c.cyan}jetic simulate endpoint --all${c.reset}`);
509
+ console.log('');
510
+ process.exit(1);
511
+ }
512
+ });
package/src/index.ts ADDED
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from 'commander';
3
+ import { initCommand } from './commands/init';
4
+ import { scanCommand } from './commands/scan';
5
+ import { inspectCommand } from './commands/inspect';
6
+ import { configCommand } from './commands/config';
7
+ import { memoryCommand } from './commands/memory';
8
+ import { simulateCommand } from './commands/simulate';
9
+ import { devCommand } from './commands/dev';
10
+
11
+ const program = new Command();
12
+
13
+ program
14
+ .name('jetic')
15
+ .description('AI-Native API Behavior Testing')
16
+ .version('0.1.0');
17
+
18
+ program.addCommand(initCommand);
19
+ program.addCommand(scanCommand);
20
+ program.addCommand(inspectCommand);
21
+ program.addCommand(configCommand);
22
+ program.addCommand(memoryCommand);
23
+ program.addCommand(simulateCommand);
24
+ program.addCommand(devCommand);
25
+
26
+ program.parse(process.argv);
package/tsconfig.json ADDED
@@ -0,0 +1,8 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "outDir": "./dist",
5
+ "rootDir": "./src"
6
+ },
7
+ "include": ["src/**/*"]
8
+ }