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,671 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.simulateWorkflowCommand = void 0;
37
+ const path = __importStar(require("path"));
38
+ const fs = __importStar(require("fs"));
39
+ const commander_1 = require("commander");
40
+ const core_1 = require("@jetic/core");
41
+ const memory_1 = require("@jetic/memory");
42
+ const zod_1 = require("zod");
43
+ const faker_1 = require("@faker-js/faker");
44
+ // ─── ANSI Helpers ──────────────────────────────────────────────────────────────
45
+ const c = {
46
+ reset: '\x1b[0m',
47
+ bold: '\x1b[1m',
48
+ dim: '\x1b[2m',
49
+ italic: '\x1b[3m',
50
+ cyan: '\x1b[36m',
51
+ green: '\x1b[32m',
52
+ red: '\x1b[31m',
53
+ yellow: '\x1b[33m',
54
+ magenta: '\x1b[35m',
55
+ blue: '\x1b[34m',
56
+ white: '\x1b[37m',
57
+ bgCyan: '\x1b[46m',
58
+ black: '\x1b[30m',
59
+ };
60
+ const TICK = `${c.green}✓${c.reset}`;
61
+ const CROSS = `${c.red}✗${c.reset}`;
62
+ const SKIP = `${c.yellow}⊘${c.reset}`;
63
+ const ARROW = `${c.cyan}→${c.reset}`;
64
+ const CHAIN = `${c.dim}│${c.reset}`;
65
+ const SEP = `${c.dim}──────────────────────────────────────────────────${c.reset}`;
66
+ // ─── Spinner ──────────────────────────────────────────────────────────────────
67
+ class Spinner {
68
+ frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
69
+ idx = 0;
70
+ interval = null;
71
+ start(message) {
72
+ this.idx = 0;
73
+ this.interval = setInterval(() => {
74
+ const frame = this.frames[this.idx % this.frames.length];
75
+ process.stdout.write(`\r ${c.cyan}${frame}${c.reset} ${message}`);
76
+ this.idx++;
77
+ }, 80);
78
+ }
79
+ stop(finalMessage) {
80
+ if (this.interval) {
81
+ clearInterval(this.interval);
82
+ this.interval = null;
83
+ }
84
+ if (finalMessage)
85
+ process.stdout.write(`\r${finalMessage}\x1b[K\n`);
86
+ else
87
+ process.stdout.write(`\r\x1b[K`);
88
+ }
89
+ }
90
+ // ─── AI workflow generator ────────────────────────────────────────────────────
91
+ async function generateWorkflow(model, config, workflowName) {
92
+ if (!config.ai) {
93
+ throw new Error('AI is not configured. Run `jetic config ai` first.');
94
+ }
95
+ const { provider, model: aiModel, apiKeyEnvVar } = config.ai;
96
+ const apiKey = process.env[apiKeyEnvVar];
97
+ if (!apiKey)
98
+ throw new Error(`Missing API key in env var: ${apiKeyEnvVar}`);
99
+ if (provider !== 'openai' && provider !== 'openrouter') {
100
+ throw new Error(`Unsupported AI provider: ${provider}`);
101
+ }
102
+ // Build a concise endpoint summary for the prompt
103
+ const endpointSummary = model.endpoints
104
+ .map((ep) => {
105
+ const mw = ep.middleware.map((m) => m.name).join(', ');
106
+ const auth = mw ? ` [auth: ${mw}]` : '';
107
+ const resp = ep.responses?.['200']?.schema
108
+ ? ' → ' + Object.keys(ep.responses['200'].schema).slice(0, 5).join(', ')
109
+ : '';
110
+ const body = ep.requestBody?.fields
111
+ ? ' body:' + Object.keys(ep.requestBody.fields).slice(0, 5).join(',')
112
+ : '';
113
+ return `${ep.method} ${ep.path}${auth}${body}${resp}`;
114
+ })
115
+ .join('\n');
116
+ const prompt = `You are an expert API test workflow designer.
117
+ Given this API's endpoints, design a realistic end-to-end workflow that tests the most important user journey.
118
+
119
+ Project: ${model.project.name} (${model.project.framework})
120
+ Workflow goal: "${workflowName}"
121
+
122
+ Endpoints:
123
+ ${endpointSummary}
124
+
125
+ Rules:
126
+ 1. Order steps logically (auth first, then CRUD operations, then cleanup/logout).
127
+ 2. For each step, specify which response fields to "capture" into memory (e.g., accessToken, refreshToken, userId, workspaceId, examId etc.)
128
+ 3. For each step, specify which memory keys to "inject" — headers or body fields needed by this request.
129
+ - Use prefix "header:" for HTTP headers (e.g., "header:Authorization" → "Bearer {token}")
130
+ - Use prefix "body:" for body fields injection
131
+ - Memory keys are in format "scope:key" e.g. "workflow:accessToken"
132
+ 4. The memory key format for capture is "scope:key" → "response.field.path" (dot-notation).
133
+ Bearer tokens should be stored with the full "Bearer " prefix if needed.
134
+ 5. For body fields that must be generated (email, password, name), use realistic placeholder values.
135
+ 6. Mark "expectStatus": 201 for creation routes, 200 for others, 204 for deletes.
136
+ 7. Include between 5-12 steps covering the full lifecycle.
137
+
138
+ Return a JSON object matching this schema exactly.`;
139
+ const importDynamic = new Function('modulePath', 'return import(modulePath)');
140
+ const { generateObject } = await importDynamic('ai');
141
+ let aiModelObj;
142
+ if (provider === 'openai') {
143
+ const { createOpenAI } = await importDynamic('@ai-sdk/openai');
144
+ aiModelObj = createOpenAI({ apiKey })(aiModel);
145
+ }
146
+ else {
147
+ const { createOpenRouter } = await importDynamic('@openrouter/ai-sdk-provider');
148
+ aiModelObj = createOpenRouter({ apiKey })(aiModel);
149
+ }
150
+ const StepSchema = zod_1.z.object({
151
+ name: zod_1.z.string(),
152
+ method: zod_1.z.string(),
153
+ path: zod_1.z.string(),
154
+ description: zod_1.z.string().optional(),
155
+ inject: zod_1.z.record(zod_1.z.string(), zod_1.z.string()).optional(),
156
+ capture: zod_1.z.record(zod_1.z.string(), zod_1.z.string()).optional(),
157
+ expectStatus: zod_1.z.number().optional(),
158
+ body: zod_1.z.record(zod_1.z.string(), zod_1.z.any()).optional(),
159
+ });
160
+ const WorkflowSchema = zod_1.z.object({
161
+ name: zod_1.z.string(),
162
+ description: zod_1.z.string().optional(),
163
+ steps: zod_1.z.array(StepSchema),
164
+ });
165
+ const { object } = await generateObject({
166
+ model: aiModelObj,
167
+ mode: 'json',
168
+ maxTokens: 2000,
169
+ schema: WorkflowSchema,
170
+ prompt,
171
+ });
172
+ return {
173
+ name: object.name,
174
+ description: object.description,
175
+ generatedAt: new Date().toISOString(),
176
+ steps: object.steps,
177
+ };
178
+ }
179
+ // ─── Deep-get value from object using dot-notation ────────────────────────────
180
+ function deepGet(obj, dotPath) {
181
+ const parts = dotPath.replace(/\[(\d+)\]/g, '.$1').split('.');
182
+ let current = obj;
183
+ for (const part of parts) {
184
+ if (current == null)
185
+ return undefined;
186
+ current = current[part];
187
+ }
188
+ return current;
189
+ }
190
+ // ─── Shared template string resolver ────────────────────────────────────────
191
+ // Resolves all {{faker.*}} and {{scope:key}} placeholders in a single string.
192
+ async function resolveTemplateString(value) {
193
+ const templateRe = /\{\{([^}]+)\}\}/g;
194
+ let match;
195
+ const replacements = [];
196
+ while ((match = templateRe.exec(value)) !== null) {
197
+ const expr = match[1].trim();
198
+ // ── scope:key → read from JeticMemory ───────────────────────────
199
+ if (expr.includes(':') && !expr.startsWith('faker.')) {
200
+ const colonIdx = expr.indexOf(':');
201
+ const scope = expr.slice(0, colonIdx);
202
+ const key = expr.slice(colonIdx + 1);
203
+ const mem = new memory_1.JeticMemory({ scope });
204
+ const memVal = await mem.get(key);
205
+ replacements.push({ placeholder: match[0], resolved: memVal != null ? String(memVal) : '' });
206
+ continue;
207
+ }
208
+ // ── faker.x.y → call faker dynamically ──────────────────────────
209
+ if (expr.startsWith('faker.')) {
210
+ const parts = expr.split('.');
211
+ try {
212
+ let fn = faker_1.faker;
213
+ for (const part of parts.slice(1))
214
+ fn = fn[part];
215
+ const generated = typeof fn === 'function' ? fn() : fn;
216
+ replacements.push({ placeholder: match[0], resolved: String(generated) });
217
+ }
218
+ catch {
219
+ replacements.push({ placeholder: match[0], resolved: expr });
220
+ }
221
+ continue;
222
+ }
223
+ // Unknown — leave as-is
224
+ replacements.push({ placeholder: match[0], resolved: match[0] });
225
+ }
226
+ let result = value;
227
+ for (const { placeholder, resolved } of replacements) {
228
+ result = result.replace(placeholder, resolved);
229
+ }
230
+ return result;
231
+ }
232
+ // ─── Resolve {{faker.*}} and {{scope:*}} templates in body values ─────────────
233
+ async function resolveBodyTemplates(body) {
234
+ const resolved = {};
235
+ for (const [key, value] of Object.entries(body)) {
236
+ resolved[key] = typeof value === 'string'
237
+ ? await resolveTemplateString(value)
238
+ : value;
239
+ }
240
+ return resolved;
241
+ }
242
+ // ─── Resolve memory injections into request headers/body ─────────────────────
243
+ // inject values support two forms:
244
+ // 1. Plain memory key: "workflow:accessToken" (legacy, direct lookup)
245
+ // 2. Template string: "Bearer {{workflow:accessToken}}" (resolved via resolveTemplateString)
246
+ async function resolveInjections(inject, memory, allMemory) {
247
+ const headers = {};
248
+ const body = {};
249
+ if (!inject)
250
+ return { headers, body };
251
+ for (const [target, memKeyOrTemplate] of Object.entries(inject)) {
252
+ let strValue;
253
+ if (memKeyOrTemplate.includes('{{')) {
254
+ // Template mode: resolve {{...}} placeholders (supports Bearer prefix etc.)
255
+ strValue = await resolveTemplateString(memKeyOrTemplate);
256
+ }
257
+ else {
258
+ // Legacy mode: treat as a direct "scope:key" memory reference
259
+ const [scope, key] = memKeyOrTemplate.includes(':')
260
+ ? memKeyOrTemplate.split(':', 2)
261
+ : ['workflow', memKeyOrTemplate];
262
+ const scopedMemory = new memory_1.JeticMemory({ scope });
263
+ const value = await scopedMemory.get(key);
264
+ if (value === null)
265
+ continue;
266
+ strValue = typeof value === 'string' ? value : JSON.stringify(value);
267
+ }
268
+ if (target.startsWith('body:')) {
269
+ body[target.slice(5)] = strValue;
270
+ }
271
+ else if (target.startsWith('header:')) {
272
+ headers[target.slice(7)] = strValue;
273
+ }
274
+ else {
275
+ // Default: treat as header
276
+ headers[target] = strValue;
277
+ }
278
+ }
279
+ return { headers, body };
280
+ }
281
+ // ─── Capture response values into Jetic memory ───────────────────────────────
282
+ async function captureToMemory(capture, responseBody) {
283
+ const captured = [];
284
+ if (!capture || !responseBody)
285
+ return captured;
286
+ for (const [memKey, responsePath] of Object.entries(capture)) {
287
+ const value = deepGet(responseBody, responsePath);
288
+ if (value === undefined || value === null)
289
+ continue;
290
+ const [scope, key] = memKey.includes(':') ? memKey.split(':', 2) : ['workflow', memKey];
291
+ const memory = new memory_1.JeticMemory({ scope });
292
+ await memory.set(key, value);
293
+ captured.push(`${scope}:${key} ← ${responsePath}`);
294
+ }
295
+ return captured;
296
+ }
297
+ // ─── Capture resolved REQUEST BODY fields into Jetic memory (pre-call) ────────
298
+ async function captureInputToMemory(captureInput, resolvedBody) {
299
+ const captured = [];
300
+ if (!captureInput)
301
+ return captured;
302
+ for (const [memKey, bodyField] of Object.entries(captureInput)) {
303
+ const value = resolvedBody[bodyField];
304
+ if (value === undefined || value === null)
305
+ continue;
306
+ const [scope, key] = memKey.includes(':') ? memKey.split(':', 2) : ['workflow', memKey];
307
+ const memory = new memory_1.JeticMemory({ scope });
308
+ await memory.set(key, value);
309
+ captured.push(`${scope}:${key} ← request.${bodyField}`);
310
+ }
311
+ return captured;
312
+ }
313
+ // ─── Replace path params with values from memory or body ─────────────────────
314
+ async function resolvePathParams(urlPath, body) {
315
+ // e.g. /api/workspaces/:id → try to find :id in memory or body
316
+ return urlPath.replace(/:([a-zA-Z_][a-zA-Z0-9_]*)/g, (_, param) => {
317
+ // Check body first
318
+ if (body[param] !== undefined)
319
+ return String(body[param]);
320
+ // Fallback to common memory keys
321
+ return `:${param}`; // leave as-is if not found
322
+ });
323
+ }
324
+ async function executeStep(step, baseUrl, defaultBody) {
325
+ const startTime = Date.now();
326
+ // Resolve memory injections
327
+ const memory = new memory_1.JeticMemory({ scope: 'workflow' });
328
+ const { headers, body: injectedBody } = await resolveInjections(step.inject, memory, memory);
329
+ // Resolve {{faker.*}} / {{workflow:*}} templates in the step body
330
+ const resolvedStepBody = await resolveBodyTemplates(step.body || {});
331
+ // Merge body: step.body overrides defaults, injectedBody adds to body
332
+ const requestBody = { ...defaultBody, ...injectedBody, ...resolvedStepBody };
333
+ // Capture resolved request body fields into memory BEFORE the HTTP call
334
+ // so subsequent steps can reference faker-generated values via {{workflow:key}}
335
+ const inputCaptured = await captureInputToMemory(step.captureInput, requestBody);
336
+ // Resolve path params
337
+ const resolvedPath = await resolvePathParams(step.path, requestBody);
338
+ const url = `${baseUrl.replace(/\/$/, '')}${resolvedPath}`;
339
+ const expectedStatus = step.expectStatus ?? 200;
340
+ try {
341
+ const fetchOptions = {
342
+ method: step.method.toUpperCase(),
343
+ headers: {
344
+ 'Content-Type': 'application/json',
345
+ ...headers,
346
+ },
347
+ };
348
+ // Only add body for non-GET/HEAD requests
349
+ if (!['GET', 'HEAD'].includes(step.method.toUpperCase()) && Object.keys(requestBody).length > 0) {
350
+ fetchOptions.body = JSON.stringify(requestBody);
351
+ }
352
+ // Add query params for GET requests
353
+ let finalUrl = url;
354
+ if (['GET', 'HEAD'].includes(step.method.toUpperCase()) && Object.keys(requestBody).length > 0) {
355
+ const params = new URLSearchParams();
356
+ for (const [k, v] of Object.entries(requestBody)) {
357
+ if (v !== undefined && v !== null)
358
+ params.set(k, String(v));
359
+ }
360
+ finalUrl = `${url}?${params.toString()}`;
361
+ }
362
+ const response = await fetch(finalUrl, fetchOptions);
363
+ const durationMs = Date.now() - startTime;
364
+ let responseBody = null;
365
+ const contentType = response.headers.get('content-type') || '';
366
+ if (contentType.includes('application/json')) {
367
+ try {
368
+ responseBody = await response.json();
369
+ }
370
+ catch {
371
+ responseBody = null;
372
+ }
373
+ }
374
+ else {
375
+ responseBody = await response.text();
376
+ }
377
+ const passed = response.status === expectedStatus || (response.status >= 200 && response.status < 300 && expectedStatus >= 200 && expectedStatus < 300);
378
+ // Capture response fields into memory
379
+ const captured = passed
380
+ ? await captureToMemory(step.capture, responseBody)
381
+ : [];
382
+ return {
383
+ step,
384
+ status: response.status,
385
+ responseBody,
386
+ durationMs,
387
+ passed,
388
+ captured: [...inputCaptured, ...captured],
389
+ injected: headers,
390
+ };
391
+ }
392
+ catch (err) {
393
+ return {
394
+ step,
395
+ status: 0,
396
+ responseBody: null,
397
+ durationMs: Date.now() - startTime,
398
+ passed: false,
399
+ captured: inputCaptured,
400
+ error: err.message || String(err),
401
+ injected: headers,
402
+ };
403
+ }
404
+ }
405
+ // ─── Rendering helpers ────────────────────────────────────────────────────────
406
+ function formatStatus(status) {
407
+ if (status === 0)
408
+ return `${c.dim}NO RESPONSE${c.reset}`;
409
+ if (status >= 200 && status < 300)
410
+ return `${c.green}${status}${c.reset}`;
411
+ if (status >= 300 && status < 400)
412
+ return `${c.yellow}${status}${c.reset}`;
413
+ if (status >= 400 && status < 500)
414
+ return `${c.red}${status}${c.reset}`;
415
+ return `${c.red}${c.bold}${status}${c.reset}`;
416
+ }
417
+ function getMethodColor(method) {
418
+ switch (method.toUpperCase()) {
419
+ case 'GET': return c.green;
420
+ case 'POST': return c.yellow;
421
+ case 'PUT': return c.cyan;
422
+ case 'PATCH': return c.magenta;
423
+ case 'DELETE': return c.red;
424
+ default: return c.white;
425
+ }
426
+ }
427
+ function renderStepResult(result, index, total) {
428
+ const icon = result.error ? CROSS : (result.passed ? TICK : CROSS);
429
+ const mc = getMethodColor(result.step.method);
430
+ const method = `${mc}${result.step.method.padEnd(6)}${c.reset}`;
431
+ const statusStr = formatStatus(result.status);
432
+ const timeStr = `${c.dim}${result.durationMs}ms${c.reset}`;
433
+ console.log(` ${icon} ${c.bold}Step ${index + 1}/${total}${c.reset} ${method} ${c.bold}${result.step.path}${c.reset} ${statusStr} ${timeStr}`);
434
+ if (result.step.name) {
435
+ console.log(` ${CHAIN} ${c.italic}${c.dim}${result.step.name}${c.reset}`);
436
+ }
437
+ // Show injected headers
438
+ if (Object.keys(result.injected).length > 0) {
439
+ for (const [k, v] of Object.entries(result.injected)) {
440
+ const preview = v.length > 50 ? v.substring(0, 47) + '...' : v;
441
+ console.log(` ${CHAIN} ${c.dim}📥 inject ${k}: ${preview}${c.reset}`);
442
+ }
443
+ }
444
+ // Show captured memory
445
+ if (result.captured.length > 0) {
446
+ for (const cap of result.captured) {
447
+ console.log(` ${CHAIN} ${c.green}💾 captured ${cap}${c.reset}`);
448
+ }
449
+ }
450
+ // Show error
451
+ if (result.error) {
452
+ console.log(` ${CHAIN} ${c.red}Error: ${result.error}${c.reset}`);
453
+ }
454
+ // Show response body preview on failure
455
+ if (!result.passed && result.responseBody && !result.error) {
456
+ const preview = typeof result.responseBody === 'string'
457
+ ? result.responseBody
458
+ : JSON.stringify(result.responseBody, null, 2);
459
+ console.log(` ${CHAIN} ${c.red}Response: ${preview}${c.reset}`);
460
+ }
461
+ console.log('');
462
+ }
463
+ function renderWorkflowHeader(workflow) {
464
+ console.log('');
465
+ console.log(` ${c.bold}${workflow.name}${c.reset}`);
466
+ if (workflow.description) {
467
+ console.log(` ${c.dim}${workflow.description}${c.reset}`);
468
+ }
469
+ console.log('');
470
+ console.log(` ${c.dim}${workflow.steps.length} steps${c.reset}`);
471
+ // Print the workflow graph
472
+ for (let i = 0; i < workflow.steps.length; i++) {
473
+ const step = workflow.steps[i];
474
+ const mc = getMethodColor(step.method);
475
+ const label = `${mc}${step.method}${c.reset} ${step.path}`;
476
+ if (i === 0) {
477
+ console.log(` ${c.cyan}┌─${c.reset} ${c.bold}${label}${c.reset} ${c.dim}${step.name}${c.reset}`);
478
+ }
479
+ else if (i === workflow.steps.length - 1) {
480
+ console.log(` ${c.cyan}└─${c.reset} ${label} ${c.dim}${step.name}${c.reset}`);
481
+ }
482
+ else {
483
+ console.log(` ${c.cyan}├─${c.reset} ${label} ${c.dim}${step.name}${c.reset}`);
484
+ }
485
+ }
486
+ console.log('');
487
+ }
488
+ function renderWorkflowSummary(results, totalMs) {
489
+ const passed = results.filter((r) => r.passed).length;
490
+ const failed = results.filter((r) => !r.passed).length;
491
+ const elapsed = (totalMs / 1000).toFixed(2);
492
+ //console.log(SEP);
493
+ const allPassed = failed === 0;
494
+ const icon = allPassed ? `${c.green}✅${c.reset}` : `${c.red}❌${c.reset}`;
495
+ const label = allPassed
496
+ ? `${c.green}${c.bold}Workflow Complete!${c.reset}`
497
+ : `${c.red}${c.bold}Workflow Failed${c.reset}`;
498
+ console.log(` ${icon} ${label} ${c.dim}${elapsed}s${c.reset}`);
499
+ console.log('');
500
+ const parts = [];
501
+ if (passed > 0)
502
+ parts.push(`${c.green}${passed} passed${c.reset}`);
503
+ if (failed > 0)
504
+ parts.push(`${c.red}${failed} failed${c.reset}`);
505
+ console.log(` ${parts.join(` ${c.dim}│${c.reset} `)}`);
506
+ // console.log(SEP);
507
+ console.log('');
508
+ }
509
+ // ─── Environment selector (simple for workflow) ───────────────────────────────
510
+ async function pickEnvironment(model) {
511
+ const envs = model.environments || [];
512
+ if (envs.length === 0) {
513
+ console.log(` ${c.yellow}⚠${c.reset} No environments in model.json. Using ${c.bold}http://localhost:3000${c.reset}\n`);
514
+ return 'http://localhost:3000';
515
+ }
516
+ if (envs.length === 1) {
517
+ const env = envs[0];
518
+ console.log(` ${c.cyan}🌍${c.reset} Environment: ${c.bold}${env.name}${c.reset} ${c.dim}${env.baseUrl}${c.reset}\n`);
519
+ return env.baseUrl;
520
+ }
521
+ // Default to first (non-interactive for now; extend with selector if desired)
522
+ const env = envs[0];
523
+ console.log(` ${c.cyan}🌍${c.reset} Using environment: ${c.bold}${env.name}${c.reset} ${c.dim}${env.baseUrl}${c.reset}`);
524
+ console.log(` ${c.dim}(Pass --env <name> to choose a different environment)${c.reset}\n`);
525
+ return env.baseUrl;
526
+ }
527
+ // ─── Command: jetic simulate workflow ────────────────────────────────────────
528
+ exports.simulateWorkflowCommand = new commander_1.Command('workflow')
529
+ .description('Generate and execute a full end-to-end workflow test from model.json')
530
+ .option('--goal <text>', 'Describe the workflow goal', 'Full user journey')
531
+ .option('--env <name>', 'Environment name to use from model.json')
532
+ .option('--workflow <file>', 'Use an existing workflow.json instead of generating')
533
+ .option('--generate-only', 'Only generate workflow.json without executing')
534
+ .option('--clear-memory', 'Clear Jetic memory before running', false)
535
+ .action(async (options) => {
536
+ console.log('');
537
+ console.log(` ${c.bgCyan}${c.black}${c.bold} JETIC ${c.reset} ${c.cyan}${c.bold}Workflow Runner${c.reset}`);
538
+ // console.log(` ${SEP}`);
539
+ console.log('');
540
+ // ── Load config & model ────────────────────────────────────────────
541
+ const config = (0, core_1.loadConfig)();
542
+ const modelPath = path.join(config.jeticDir, 'model.json');
543
+ const model = (0, core_1.readJsonSync)(modelPath);
544
+ if (!model) {
545
+ console.error(` ${c.red}✗${c.reset} No model.json found. Run ${c.bold}jetic scan${c.reset} first.\n`);
546
+ process.exit(1);
547
+ }
548
+ console.log(` ${c.dim}Model: ${model.project.name} • ${model.endpoints.length} endpoints${c.reset}\n`);
549
+ // ── Optionally clear memory ────────────────────────────────────────
550
+ if (options.clearMemory) {
551
+ memory_1.JeticMemory.clearAllMemory();
552
+ console.log(` ${c.yellow}🧹${c.reset} Jetic memory cleared\n`);
553
+ }
554
+ // ── Load or generate workflow ──────────────────────────────────────
555
+ let workflow;
556
+ const workflowPath = options.workflow
557
+ ? path.resolve(options.workflow)
558
+ : path.join(config.jeticDir, 'workflow.json');
559
+ if (options.workflow && fs.existsSync(workflowPath)) {
560
+ // Use provided workflow file
561
+ try {
562
+ workflow = JSON.parse(fs.readFileSync(workflowPath, 'utf-8'));
563
+ console.log(` ${TICK} Loaded workflow: ${c.bold}${workflow.name}${c.reset} ${c.dim}(${workflowPath})${c.reset}\n`);
564
+ }
565
+ catch {
566
+ console.error(` ${c.red}✗${c.reset} Failed to parse workflow file: ${workflowPath}\n`);
567
+ process.exit(1);
568
+ }
569
+ }
570
+ else {
571
+ // Generate with AI
572
+ const spinner = new Spinner();
573
+ spinner.start(`${c.magenta}🤖 AI generating workflow for: "${options.goal}"...${c.reset}`);
574
+ try {
575
+ workflow = await generateWorkflow(model, config, options.goal);
576
+ spinner.stop(` ${TICK} Workflow generated: ${c.bold}${workflow.name}${c.reset} ${c.dim}(${workflow.steps.length} steps)${c.reset}`);
577
+ // Save workflow.json
578
+ fs.mkdirSync(path.dirname(workflowPath), { recursive: true });
579
+ fs.writeFileSync(workflowPath, JSON.stringify(workflow, null, 2), 'utf-8');
580
+ console.log(` ${c.dim} Saved to ${workflowPath}${c.reset}\n`);
581
+ }
582
+ catch (err) {
583
+ spinner.stop(` ${c.red}✗${c.reset} Failed to generate workflow`);
584
+ console.error(`\n ${c.red}${err.message}${c.reset}\n`);
585
+ console.error(` ${c.dim}Make sure AI is configured: jetic config ai${c.reset}\n`);
586
+ process.exit(1);
587
+ }
588
+ }
589
+ // ── Generate-only mode ─────────────────────────────────────────────
590
+ if (options.generateOnly) {
591
+ renderWorkflowHeader(workflow);
592
+ console.log(` ${c.green}✓${c.reset} Workflow saved. Run without ${c.bold}--generate-only${c.reset} to execute.\n`);
593
+ process.exit(0);
594
+ }
595
+ // ── Pick environment ───────────────────────────────────────────────
596
+ let baseUrl;
597
+ const envs = model.environments || [];
598
+ if (options.env) {
599
+ const found = envs.find((e) => e.name === options.env);
600
+ if (!found) {
601
+ console.error(` ${c.red}✗${c.reset} Environment "${options.env}" not found in model.json\n`);
602
+ console.error(` ${c.dim}Available: ${envs.map((e) => e.name).join(', ')}${c.reset}\n`);
603
+ process.exit(1);
604
+ }
605
+ baseUrl = found.baseUrl;
606
+ console.log(` ${c.cyan}🌍${c.reset} Environment: ${c.bold}${found.name}${c.reset} ${c.dim}${found.baseUrl}${c.reset}\n`);
607
+ }
608
+ else {
609
+ baseUrl = await pickEnvironment(model);
610
+ }
611
+ // ── Check Backend Health ───────────────────────────────────────────
612
+ const checkSpinner = new Spinner();
613
+ checkSpinner.start(`Checking if backend is active at ${baseUrl}...`);
614
+ try {
615
+ await fetch(baseUrl);
616
+ checkSpinner.stop(` ${c.green}✓${c.reset} Backend is active`);
617
+ console.log('');
618
+ }
619
+ catch (e) {
620
+ checkSpinner.stop();
621
+ console.log(` ${c.red}✗${c.reset} Backend is unreachable at ${c.bold}${baseUrl}${c.reset}`);
622
+ console.log(` ${c.yellow}⚠${c.reset} Please run/initialize your backend project and try again.\n`);
623
+ process.exit(1);
624
+ }
625
+ // ── Render workflow graph ──────────────────────────────────────────
626
+ renderWorkflowHeader(workflow);
627
+ console.log(` ${c.magenta}🚀${c.reset} Executing workflow steps...\n`);
628
+ // ── Execute steps ──────────────────────────────────────────────────
629
+ const results = [];
630
+ const totalStart = Date.now();
631
+ let stopOnFailure = false;
632
+ for (let i = 0; i < workflow.steps.length; i++) {
633
+ const step = workflow.steps[i];
634
+ const stepLabel = `${c.dim}${i + 1}/${workflow.steps.length}${c.reset} ${getMethodColor(step.method)}${step.method}${c.reset} ${step.path}`;
635
+ const spinner = new Spinner();
636
+ spinner.start(`${stepLabel} ${c.dim}${step.name}${c.reset}...`);
637
+ const result = await executeStep(step, baseUrl, {});
638
+ results.push(result);
639
+ const icon = result.passed ? TICK : CROSS;
640
+ spinner.stop(` ${icon} ${stepLabel} ${formatStatus(result.status)} ${c.dim}${result.durationMs}ms${c.reset}`);
641
+ renderStepResult(result, i, workflow.steps.length);
642
+ // If a critical step fails (auth steps), stop early
643
+ if (!result.passed && (step.path.includes('/login') ||
644
+ step.path.includes('/register') ||
645
+ step.path.includes('/auth'))) {
646
+ console.log(` ${c.red}${c.bold}⚠ Auth step failed — stopping workflow to prevent cascading failures.${c.reset}\n`);
647
+ stopOnFailure = true;
648
+ break;
649
+ }
650
+ }
651
+ // ── Summary ────────────────────────────────────────────────────────
652
+ renderWorkflowSummary(results, Date.now() - totalStart);
653
+ // Show memory state
654
+ const memoryState = memory_1.JeticMemory.getAllMemory();
655
+ const memKeys = Object.entries(memoryState).flatMap(([scope, keys]) => Object.keys(keys).map((k) => `${scope}:${k}`));
656
+ if (memKeys.length > 0) {
657
+ console.log(` ${c.cyan}💾${c.reset} ${c.bold}Jetic Memory${c.reset} ${c.dim}(captured during run)${c.reset}`);
658
+ for (const key of memKeys) {
659
+ const [scope, k] = key.split(':', 2);
660
+ const val = memoryState[scope][k];
661
+ const preview = typeof val === 'string' && val.length > 60
662
+ ? val.substring(0, 57) + '...'
663
+ : String(val);
664
+ console.log(` ${c.dim} ${key}${c.reset} = ${c.cyan}${preview}${c.reset}`);
665
+ }
666
+ console.log('');
667
+ }
668
+ const failed = results.filter((r) => !r.passed).length;
669
+ process.exit(failed > 0 ? 1 : 0);
670
+ });
671
+ //# sourceMappingURL=simulate-workflow.js.map