intellitester 0.1.12

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 (89) hide show
  1. package/README.md +200 -0
  2. package/dist/chunk-35WJGNDA.cjs +136 -0
  3. package/dist/chunk-35WJGNDA.cjs.map +1 -0
  4. package/dist/chunk-4B54JUOP.js +234 -0
  5. package/dist/chunk-4B54JUOP.js.map +1 -0
  6. package/dist/chunk-5LFSLMQ7.js +2517 -0
  7. package/dist/chunk-5LFSLMQ7.js.map +1 -0
  8. package/dist/chunk-6PYKWWH5.js +63 -0
  9. package/dist/chunk-6PYKWWH5.js.map +1 -0
  10. package/dist/chunk-ARJYJVRM.cjs +302 -0
  11. package/dist/chunk-ARJYJVRM.cjs.map +1 -0
  12. package/dist/chunk-CN6HSJJX.js +133 -0
  13. package/dist/chunk-CN6HSJJX.js.map +1 -0
  14. package/dist/chunk-DE5UFTTG.js +31 -0
  15. package/dist/chunk-DE5UFTTG.js.map +1 -0
  16. package/dist/chunk-ECBA4GJ3.js +287 -0
  17. package/dist/chunk-ECBA4GJ3.js.map +1 -0
  18. package/dist/chunk-OFXNJXMV.cjs +237 -0
  19. package/dist/chunk-OFXNJXMV.cjs.map +1 -0
  20. package/dist/chunk-PAKODOH4.cjs +66 -0
  21. package/dist/chunk-PAKODOH4.cjs.map +1 -0
  22. package/dist/chunk-QMYM2TCH.cjs +36 -0
  23. package/dist/chunk-QMYM2TCH.cjs.map +1 -0
  24. package/dist/chunk-SAVY6D3X.js +125 -0
  25. package/dist/chunk-SAVY6D3X.js.map +1 -0
  26. package/dist/chunk-UUJXCHVT.cjs +128 -0
  27. package/dist/chunk-UUJXCHVT.cjs.map +1 -0
  28. package/dist/chunk-XWGUA67E.cjs +2552 -0
  29. package/dist/chunk-XWGUA67E.cjs.map +1 -0
  30. package/dist/cli/index.cjs +1985 -0
  31. package/dist/cli/index.cjs.map +1 -0
  32. package/dist/cli/index.d.cts +1 -0
  33. package/dist/cli/index.d.ts +1 -0
  34. package/dist/cli/index.js +1957 -0
  35. package/dist/cli/index.js.map +1 -0
  36. package/dist/core/cleanup/index.cjs +45 -0
  37. package/dist/core/cleanup/index.cjs.map +1 -0
  38. package/dist/core/cleanup/index.d.cts +117 -0
  39. package/dist/core/cleanup/index.d.ts +117 -0
  40. package/dist/core/cleanup/index.js +8 -0
  41. package/dist/core/cleanup/index.js.map +1 -0
  42. package/dist/index.cjs +110 -0
  43. package/dist/index.cjs.map +1 -0
  44. package/dist/index.d.cts +852 -0
  45. package/dist/index.d.ts +852 -0
  46. package/dist/index.js +9 -0
  47. package/dist/index.js.map +1 -0
  48. package/dist/integration/index.cjs +22 -0
  49. package/dist/integration/index.cjs.map +1 -0
  50. package/dist/integration/index.d.cts +42 -0
  51. package/dist/integration/index.d.ts +42 -0
  52. package/dist/integration/index.js +20 -0
  53. package/dist/integration/index.js.map +1 -0
  54. package/dist/providers/appwrite/index.cjs +16 -0
  55. package/dist/providers/appwrite/index.cjs.map +1 -0
  56. package/dist/providers/appwrite/index.d.cts +12 -0
  57. package/dist/providers/appwrite/index.d.ts +12 -0
  58. package/dist/providers/appwrite/index.js +3 -0
  59. package/dist/providers/appwrite/index.js.map +1 -0
  60. package/dist/providers/index.cjs +60 -0
  61. package/dist/providers/index.cjs.map +1 -0
  62. package/dist/providers/index.d.cts +13 -0
  63. package/dist/providers/index.d.ts +13 -0
  64. package/dist/providers/index.js +7 -0
  65. package/dist/providers/index.js.map +1 -0
  66. package/dist/providers/mysql/index.cjs +16 -0
  67. package/dist/providers/mysql/index.cjs.map +1 -0
  68. package/dist/providers/mysql/index.d.cts +14 -0
  69. package/dist/providers/mysql/index.d.ts +14 -0
  70. package/dist/providers/mysql/index.js +3 -0
  71. package/dist/providers/mysql/index.js.map +1 -0
  72. package/dist/providers/postgres/index.cjs +16 -0
  73. package/dist/providers/postgres/index.cjs.map +1 -0
  74. package/dist/providers/postgres/index.d.cts +10 -0
  75. package/dist/providers/postgres/index.d.ts +10 -0
  76. package/dist/providers/postgres/index.js +3 -0
  77. package/dist/providers/postgres/index.js.map +1 -0
  78. package/dist/providers/sqlite/index.cjs +16 -0
  79. package/dist/providers/sqlite/index.cjs.map +1 -0
  80. package/dist/providers/sqlite/index.d.cts +11 -0
  81. package/dist/providers/sqlite/index.d.ts +11 -0
  82. package/dist/providers/sqlite/index.js +3 -0
  83. package/dist/providers/sqlite/index.js.map +1 -0
  84. package/dist/types-LONNVTIF.d.cts +56 -0
  85. package/dist/types-l-ZaFKC-.d.ts +56 -0
  86. package/package.json +114 -0
  87. package/schemas/intellitester.config.schema.json +384 -0
  88. package/schemas/test.schema.json +517 -0
  89. package/schemas/workflow.schema.json +227 -0
@@ -0,0 +1,1957 @@
1
+ #!/usr/bin/env node
2
+ import { loadIntellitesterConfig, isPipelineFile, isWorkflowFile, loadTestDefinition, collectMissingEnvVars, loadPipelineDefinition, loadWorkflowDefinition, runWorkflow, runWebTest, startTrackingServer, startWebServer, createTestContext, setupAppwriteTracking, runWorkflowWithContext, killServer, createAIProvider, TestDefinitionSchema } from '../chunk-5LFSLMQ7.js';
3
+ import { loadFailedCleanups, loadCleanupHandlers, executeCleanup, removeFailedCleanup } from '../chunk-ECBA4GJ3.js';
4
+ import '../chunk-DE5UFTTG.js';
5
+ import '../chunk-6PYKWWH5.js';
6
+ import '../chunk-4B54JUOP.js';
7
+ import '../chunk-SAVY6D3X.js';
8
+ import '../chunk-CN6HSJJX.js';
9
+ import dotenv2 from 'dotenv';
10
+ import fs3 from 'fs/promises';
11
+ import * as path4 from 'path';
12
+ import path4__default from 'path';
13
+ import process2 from 'process';
14
+ import { Command } from 'commander';
15
+ import { spawn } from 'child_process';
16
+ import crypto from 'crypto';
17
+ import { chromium, webkit, firefox } from 'playwright';
18
+ import { parse } from 'yaml';
19
+ import { glob } from 'glob';
20
+ import { promises } from 'fs';
21
+ import prompts from 'prompts';
22
+
23
+ var getBrowser = (browser) => {
24
+ switch (browser) {
25
+ case "firefox":
26
+ return firefox;
27
+ case "webkit":
28
+ return webkit;
29
+ default:
30
+ return chromium;
31
+ }
32
+ };
33
+ function buildExecutionOrder(workflows) {
34
+ const workflowMap = /* @__PURE__ */ new Map();
35
+ const workflowIds = [];
36
+ for (let i = 0; i < workflows.length; i++) {
37
+ const workflow = workflows[i];
38
+ const id = workflow.id ?? `workflow_${i}`;
39
+ workflowIds.push(id);
40
+ workflowMap.set(id, { ...workflow, id });
41
+ }
42
+ const adjacencyList = /* @__PURE__ */ new Map();
43
+ const inDegree = /* @__PURE__ */ new Map();
44
+ for (const id of workflowIds) {
45
+ adjacencyList.set(id, []);
46
+ inDegree.set(id, 0);
47
+ }
48
+ for (const id of workflowIds) {
49
+ const workflow = workflowMap.get(id);
50
+ const deps = workflow.depends_on ?? [];
51
+ for (const depId of deps) {
52
+ if (!workflowMap.has(depId)) {
53
+ throw new Error(
54
+ `Workflow "${id}" depends on "${depId}" which does not exist in the pipeline`
55
+ );
56
+ }
57
+ adjacencyList.get(depId).push(id);
58
+ inDegree.set(id, (inDegree.get(id) ?? 0) + 1);
59
+ }
60
+ }
61
+ const queue = [];
62
+ for (const id of workflowIds) {
63
+ if (inDegree.get(id) === 0) {
64
+ queue.push(id);
65
+ }
66
+ }
67
+ const sorted = [];
68
+ while (queue.length > 0) {
69
+ const currentId = queue.shift();
70
+ sorted.push(workflowMap.get(currentId));
71
+ for (const dependentId of adjacencyList.get(currentId) ?? []) {
72
+ const newInDegree = (inDegree.get(dependentId) ?? 1) - 1;
73
+ inDegree.set(dependentId, newInDegree);
74
+ if (newInDegree === 0) {
75
+ queue.push(dependentId);
76
+ }
77
+ }
78
+ }
79
+ if (sorted.length !== workflowIds.length) {
80
+ const remaining = workflowIds.filter((id) => !sorted.some((w) => w.id === id));
81
+ throw new Error(
82
+ `Circular dependency detected in pipeline. Workflows involved: ${remaining.join(", ")}`
83
+ );
84
+ }
85
+ return sorted;
86
+ }
87
+ function inferCleanupConfig(config) {
88
+ if (!config) return void 0;
89
+ if (config.cleanup) {
90
+ return config.cleanup;
91
+ }
92
+ if (config.appwrite?.cleanup) {
93
+ return {
94
+ provider: "appwrite",
95
+ appwrite: {
96
+ endpoint: config.appwrite.endpoint,
97
+ projectId: config.appwrite.projectId,
98
+ apiKey: config.appwrite.apiKey,
99
+ cleanupOnFailure: config.appwrite.cleanupOnFailure
100
+ }
101
+ };
102
+ }
103
+ return void 0;
104
+ }
105
+ async function runPipeline(pipeline, pipelinePath, options = {}) {
106
+ const pipelineDir = path4__default.dirname(pipelinePath);
107
+ const sessionId = crypto.randomUUID();
108
+ const testStartTime = (/* @__PURE__ */ new Date()).toISOString();
109
+ console.log(`
110
+ ${"=".repeat(60)}`);
111
+ console.log(`Pipeline: ${pipeline.name}`);
112
+ console.log(`Session ID: ${sessionId}`);
113
+ console.log(`${"=".repeat(60)}
114
+ `);
115
+ let executionOrder;
116
+ try {
117
+ executionOrder = buildExecutionOrder(pipeline.workflows);
118
+ console.log(
119
+ `Execution order: ${executionOrder.map((w) => w.id ?? w.file).join(" -> ")}
120
+ `
121
+ );
122
+ } catch (error) {
123
+ const message = error instanceof Error ? error.message : String(error);
124
+ console.error(`Failed to build execution order: ${message}`);
125
+ return {
126
+ status: "failed",
127
+ workflows: [],
128
+ sessionId
129
+ };
130
+ }
131
+ let trackingServer = null;
132
+ try {
133
+ trackingServer = await startTrackingServer({ port: 0 });
134
+ console.log(`Tracking server started on port ${trackingServer.port}`);
135
+ } catch (error) {
136
+ console.warn("Failed to start tracking server:", error);
137
+ }
138
+ if (trackingServer) {
139
+ process.env.INTELLITESTER_SESSION_ID = sessionId;
140
+ process.env.INTELLITESTER_TRACK_URL = `http://localhost:${trackingServer.port}`;
141
+ }
142
+ let serverProcess = null;
143
+ if (pipeline.config?.webServer) {
144
+ try {
145
+ serverProcess = await startWebServer({
146
+ ...pipeline.config.webServer,
147
+ cwd: pipelineDir
148
+ });
149
+ } catch (error) {
150
+ console.error("Failed to start web server:", error);
151
+ if (trackingServer) await trackingServer.stop();
152
+ throw error;
153
+ }
154
+ }
155
+ const signalCleanup = async () => {
156
+ console.log("\n\nInterrupted - cleaning up...");
157
+ killServer(serverProcess);
158
+ if (trackingServer) await trackingServer.stop();
159
+ process.exit(1);
160
+ };
161
+ process.on("SIGINT", signalCleanup);
162
+ process.on("SIGTERM", signalCleanup);
163
+ const browserName = options.browser ?? pipeline.config?.web?.browser ?? "chromium";
164
+ const headless = options.headed ? false : pipeline.config?.web?.headless ?? true;
165
+ const browser = await getBrowser(browserName).launch({ headless });
166
+ const browserContext = await browser.newContext();
167
+ const page = await browserContext.newPage();
168
+ page.setDefaultTimeout(3e4);
169
+ const executionContext = {
170
+ variables: /* @__PURE__ */ new Map(),
171
+ lastEmail: null,
172
+ emailClient: null,
173
+ appwriteContext: createTestContext(),
174
+ appwriteConfig: pipeline.config?.appwrite ? {
175
+ endpoint: pipeline.config.appwrite.endpoint,
176
+ projectId: pipeline.config.appwrite.projectId,
177
+ apiKey: pipeline.config.appwrite.apiKey
178
+ } : void 0
179
+ };
180
+ if (pipeline.config?.appwrite) {
181
+ setupAppwriteTracking(page, executionContext);
182
+ }
183
+ const completedIds = /* @__PURE__ */ new Set();
184
+ const failedIds = /* @__PURE__ */ new Set();
185
+ const skippedIds = /* @__PURE__ */ new Set();
186
+ const workflowResults = [];
187
+ let pipelineFailed = false;
188
+ let shouldStopPipeline = false;
189
+ try {
190
+ for (const workflowRef of executionOrder) {
191
+ const workflowId = workflowRef.id ?? workflowRef.file;
192
+ if (shouldStopPipeline) {
193
+ workflowResults.push({
194
+ id: workflowRef.id,
195
+ file: workflowRef.file,
196
+ status: "skipped",
197
+ error: "Pipeline stopped due to previous failure"
198
+ });
199
+ skippedIds.add(workflowId);
200
+ continue;
201
+ }
202
+ const deps = workflowRef.depends_on ?? [];
203
+ const depsFailed = deps.some((id) => failedIds.has(id) || skippedIds.has(id));
204
+ const depsNotMet = deps.some(
205
+ (id) => !completedIds.has(id) && !failedIds.has(id) && !skippedIds.has(id)
206
+ );
207
+ if (depsFailed || depsNotMet) {
208
+ const onFailure = workflowRef.on_failure ?? pipeline.on_failure;
209
+ if (onFailure === "skip") {
210
+ console.log(`
211
+ Skipping workflow "${workflowId}" - dependencies not met`);
212
+ workflowResults.push({
213
+ id: workflowRef.id,
214
+ file: workflowRef.file,
215
+ status: "skipped",
216
+ error: `Dependencies not met: ${deps.filter((d) => failedIds.has(d) || skippedIds.has(d)).join(", ")}`
217
+ });
218
+ skippedIds.add(workflowId);
219
+ continue;
220
+ } else if (onFailure === "fail") {
221
+ console.log(
222
+ `
223
+ Pipeline stopped - workflow "${workflowId}" dependencies failed`
224
+ );
225
+ workflowResults.push({
226
+ id: workflowRef.id,
227
+ file: workflowRef.file,
228
+ status: "failed",
229
+ error: `Dependencies failed: ${deps.filter((d) => failedIds.has(d) || skippedIds.has(d)).join(", ")}`
230
+ });
231
+ failedIds.add(workflowId);
232
+ pipelineFailed = true;
233
+ shouldStopPipeline = true;
234
+ continue;
235
+ }
236
+ console.log(
237
+ `
238
+ Running workflow "${workflowId}" despite dependency failure (on_failure: ignore)`
239
+ );
240
+ }
241
+ const workflowFilePath = path4__default.resolve(pipelineDir, workflowRef.file);
242
+ console.log(`
243
+ ${"=".repeat(40)}`);
244
+ console.log(`Workflow: ${workflowId}`);
245
+ console.log(`File: ${workflowRef.file}`);
246
+ console.log(`${"=".repeat(40)}`);
247
+ try {
248
+ const workflowDefinition = await loadWorkflowDefinition(workflowFilePath);
249
+ if (workflowRef.variables) {
250
+ for (const [key, value] of Object.entries(workflowRef.variables)) {
251
+ const interpolated = value.replace(/\{\{(\w+)\}\}/g, (match, varName) => {
252
+ if (varName === "uuid") {
253
+ return crypto.randomUUID().split("-")[0];
254
+ }
255
+ return executionContext.variables.get(varName) ?? match;
256
+ });
257
+ executionContext.variables.set(key, interpolated);
258
+ }
259
+ }
260
+ const workflowOptions = {
261
+ ...options,
262
+ page,
263
+ executionContext,
264
+ skipCleanup: true,
265
+ // Defer cleanup to pipeline end
266
+ sessionId,
267
+ testStartTime
268
+ };
269
+ const result = await runWorkflowWithContext(
270
+ workflowDefinition,
271
+ workflowFilePath,
272
+ workflowOptions
273
+ );
274
+ if (result.status === "passed") {
275
+ completedIds.add(workflowId);
276
+ workflowResults.push({
277
+ id: workflowRef.id,
278
+ file: workflowRef.file,
279
+ status: "passed",
280
+ workflowResult: result
281
+ });
282
+ } else {
283
+ failedIds.add(workflowId);
284
+ pipelineFailed = true;
285
+ workflowResults.push({
286
+ id: workflowRef.id,
287
+ file: workflowRef.file,
288
+ status: "failed",
289
+ workflowResult: result,
290
+ error: result.tests.find((t) => t.status === "failed")?.error
291
+ });
292
+ const onFailure = workflowRef.on_failure ?? pipeline.on_failure;
293
+ if (onFailure === "fail") {
294
+ console.log(`
295
+ Pipeline stopped due to workflow "${workflowId}" failure`);
296
+ shouldStopPipeline = true;
297
+ }
298
+ }
299
+ } catch (error) {
300
+ const message = error instanceof Error ? error.message : String(error);
301
+ console.error(`Failed to load/run workflow "${workflowId}": ${message}`);
302
+ failedIds.add(workflowId);
303
+ pipelineFailed = true;
304
+ workflowResults.push({
305
+ id: workflowRef.id,
306
+ file: workflowRef.file,
307
+ status: "failed",
308
+ error: message
309
+ });
310
+ const onFailure = workflowRef.on_failure ?? pipeline.on_failure;
311
+ if (onFailure === "fail") {
312
+ console.log(`
313
+ Pipeline stopped due to workflow "${workflowId}" failure`);
314
+ shouldStopPipeline = true;
315
+ }
316
+ }
317
+ }
318
+ if (trackingServer) {
319
+ const serverResources = trackingServer.getResources(sessionId);
320
+ if (serverResources.length > 0) {
321
+ console.log(`
322
+ Collected ${serverResources.length} server-tracked resources`);
323
+ executionContext.appwriteContext.resources.push(...serverResources);
324
+ }
325
+ }
326
+ let cleanupResult;
327
+ const cleanupConfig = inferCleanupConfig(pipeline.config);
328
+ if (cleanupConfig) {
329
+ const shouldCleanup = pipelineFailed ? pipeline.cleanup_on_failure : true;
330
+ if (shouldCleanup) {
331
+ try {
332
+ console.log("\n---");
333
+ console.log("[Cleanup] Starting pipeline cleanup...");
334
+ const { handlers, typeMappings, provider } = await loadCleanupHandlers(
335
+ cleanupConfig,
336
+ process.cwd()
337
+ );
338
+ const genericResources = executionContext.appwriteContext.resources.map(
339
+ (r) => ({ ...r })
340
+ );
341
+ const providerConfig = {
342
+ provider: cleanupConfig.provider || "appwrite"
343
+ };
344
+ if (cleanupConfig.provider === "appwrite" && cleanupConfig.appwrite) {
345
+ const appwriteCleanupConfig = cleanupConfig.appwrite;
346
+ providerConfig.endpoint = appwriteCleanupConfig.endpoint;
347
+ providerConfig.projectId = appwriteCleanupConfig.projectId;
348
+ }
349
+ cleanupResult = await executeCleanup(
350
+ genericResources,
351
+ handlers,
352
+ typeMappings,
353
+ {
354
+ parallel: cleanupConfig.parallel ?? false,
355
+ retries: cleanupConfig.retries ?? 3,
356
+ sessionId,
357
+ testStartTime,
358
+ userId: executionContext.appwriteContext.userId,
359
+ providerConfig,
360
+ cwd: process.cwd(),
361
+ config: cleanupConfig,
362
+ provider
363
+ }
364
+ );
365
+ if (cleanupResult.success) {
366
+ console.log(
367
+ `[Cleanup] Cleanup complete: ${cleanupResult.deleted.length} resources deleted`
368
+ );
369
+ } else {
370
+ console.log(
371
+ `[Cleanup] Cleanup partial: ${cleanupResult.deleted.length} deleted, ${cleanupResult.failed.length} failed`
372
+ );
373
+ for (const failed of cleanupResult.failed) {
374
+ console.log(` - ${failed}`);
375
+ }
376
+ }
377
+ } catch (error) {
378
+ console.error("[Cleanup] Cleanup failed:", error);
379
+ }
380
+ } else {
381
+ console.log("\nSkipping cleanup (cleanup_on_failure is false)");
382
+ }
383
+ }
384
+ const passedCount = workflowResults.filter((w) => w.status === "passed").length;
385
+ const failedCount = workflowResults.filter((w) => w.status === "failed").length;
386
+ const skippedCount = workflowResults.filter((w) => w.status === "skipped").length;
387
+ console.log(`
388
+ ${"=".repeat(60)}`);
389
+ console.log(`Pipeline: ${pipelineFailed ? "FAILED" : "PASSED"}`);
390
+ console.log(
391
+ `Workflows: ${passedCount} passed, ${failedCount} failed, ${skippedCount} skipped`
392
+ );
393
+ console.log(`${"=".repeat(60)}
394
+ `);
395
+ return {
396
+ status: pipelineFailed ? "failed" : "passed",
397
+ workflows: workflowResults,
398
+ sessionId,
399
+ cleanupResult
400
+ };
401
+ } finally {
402
+ process.off("SIGINT", signalCleanup);
403
+ process.off("SIGTERM", signalCleanup);
404
+ await browserContext.close();
405
+ await browser.close();
406
+ killServer(serverProcess);
407
+ if (trackingServer) {
408
+ await trackingServer.stop();
409
+ }
410
+ delete process.env.INTELLITESTER_SESSION_ID;
411
+ delete process.env.INTELLITESTER_TRACK_URL;
412
+ }
413
+ }
414
+
415
+ // src/generator/elementExtractor.ts
416
+ var INTERACTIVE_TAGS = [
417
+ "button",
418
+ "input",
419
+ "textarea",
420
+ "select",
421
+ "a",
422
+ "form",
423
+ "label",
424
+ "option"
425
+ ];
426
+ function extractTemplateContent(content, filePath) {
427
+ const ext = filePath.split(".").pop()?.toLowerCase();
428
+ switch (ext) {
429
+ case "vue": {
430
+ const templateMatch = content.match(/<template[^>]*>([\s\S]*?)<\/template>/);
431
+ return templateMatch ? templateMatch[1] : "";
432
+ }
433
+ case "astro": {
434
+ const frontmatterEnd = content.lastIndexOf("---");
435
+ if (frontmatterEnd > 0) {
436
+ return content.substring(frontmatterEnd + 3);
437
+ }
438
+ return content;
439
+ }
440
+ case "tsx":
441
+ case "jsx": {
442
+ const jsxPatterns = [
443
+ // return ( ... )
444
+ /return\s*\(([\s\S]*?)\);/g,
445
+ // return <...>
446
+ /return\s+(<[\s\S]*?>[\s\S]*?<\/[\w.]+>)/g,
447
+ // return <... />
448
+ /return\s+(<[^>]+\/\s*>)/g
449
+ ];
450
+ const jsxParts = [];
451
+ for (const pattern of jsxPatterns) {
452
+ const matches = content.matchAll(pattern);
453
+ for (const match of matches) {
454
+ jsxParts.push(match[1]);
455
+ }
456
+ }
457
+ return jsxParts.join("\n");
458
+ }
459
+ case "svelte": {
460
+ let template = content;
461
+ template = template.replace(/<script[^>]*>[\s\S]*?<\/script>/g, "");
462
+ template = template.replace(/<style[^>]*>[\s\S]*?<\/style>/g, "");
463
+ return template;
464
+ }
465
+ default:
466
+ return content;
467
+ }
468
+ }
469
+ function extractAttribute(element, attrName) {
470
+ const patterns = [
471
+ new RegExp(`${attrName}\\s*=\\s*"([^"]*)"`, "i"),
472
+ new RegExp(`${attrName}\\s*=\\s*'([^']*)'`, "i"),
473
+ new RegExp(`${attrName}\\s*=\\s*{([^}]*)}`, "i")
474
+ // JSX expressions
475
+ ];
476
+ for (const pattern of patterns) {
477
+ const match = element.match(pattern);
478
+ if (match && match[1]) {
479
+ let value = match[1].trim();
480
+ if (value.startsWith('"') && value.endsWith('"')) {
481
+ value = value.slice(1, -1);
482
+ } else if (value.startsWith("'") && value.endsWith("'")) {
483
+ value = value.slice(1, -1);
484
+ }
485
+ return value;
486
+ }
487
+ }
488
+ return void 0;
489
+ }
490
+ function extractTextContent(element, tag) {
491
+ const regex = new RegExp(`<${tag}[^>]*>([^<]*)</${tag}>`, "i");
492
+ const match = element.match(regex);
493
+ if (match && match[1]) {
494
+ let text = match[1].trim();
495
+ text = text.replace(/\{[^}]*\}/g, "").trim();
496
+ if (text.length > 0 && text.length < 100) {
497
+ return text;
498
+ }
499
+ }
500
+ return void 0;
501
+ }
502
+ function generateDescription(element) {
503
+ const parts = [];
504
+ if (element.tag === "input" && element.type) {
505
+ parts.push(`${element.type} input`);
506
+ } else if (element.tag === "a") {
507
+ parts.push("link");
508
+ } else if (element.tag) {
509
+ parts.push(element.tag);
510
+ }
511
+ if (element.text) {
512
+ parts.push(`"${element.text}"`);
513
+ } else if (element.name) {
514
+ parts.push(`"${element.name}"`);
515
+ } else if (element.placeholder) {
516
+ parts.push(`with placeholder "${element.placeholder}"`);
517
+ } else if (element.testId) {
518
+ parts.push(`(${element.testId})`);
519
+ }
520
+ return parts.join(" ");
521
+ }
522
+ function parseElement(elementStr, file, route) {
523
+ const tagMatch = elementStr.match(/<(\w+)/);
524
+ if (!tagMatch) return null;
525
+ const tag = tagMatch[1].toLowerCase();
526
+ if (!INTERACTIVE_TAGS.includes(tag)) {
527
+ return null;
528
+ }
529
+ const element = {
530
+ tag,
531
+ file,
532
+ route
533
+ };
534
+ element.testId = extractAttribute(elementStr, "data-testid");
535
+ element.role = extractAttribute(elementStr, "role");
536
+ element.name = extractAttribute(elementStr, "aria-label");
537
+ element.type = extractAttribute(elementStr, "type");
538
+ element.placeholder = extractAttribute(elementStr, "placeholder");
539
+ if (!element.name) {
540
+ element.name = extractAttribute(elementStr, "name");
541
+ }
542
+ if (["button", "a", "label", "option"].includes(tag)) {
543
+ element.text = extractTextContent(elementStr, tag);
544
+ }
545
+ element.description = generateDescription(element);
546
+ return element;
547
+ }
548
+ function extractElements(content, filePath, route) {
549
+ const template = extractTemplateContent(content, filePath);
550
+ if (!template.trim()) {
551
+ return [];
552
+ }
553
+ const elements = [];
554
+ const elementPattern = /<(\w+)(?:\s+[^>]*)?(?:\/>|>[\s\S]*?<\/\1>)/g;
555
+ const matches = template.matchAll(elementPattern);
556
+ for (const match of matches) {
557
+ const elementStr = match[0];
558
+ const element = parseElement(elementStr, filePath, route);
559
+ if (element) {
560
+ elements.push(element);
561
+ }
562
+ }
563
+ return elements;
564
+ }
565
+
566
+ // src/generator/sourceScanner.ts
567
+ var DEFAULT_EXTENSIONS = [".vue", ".astro", ".tsx", ".jsx", ".svelte"];
568
+ function fileToRoute(filePath, pagesDir) {
569
+ let route = path4.relative(pagesDir, filePath);
570
+ route = route.replace(/\.(vue|astro|tsx|jsx|svelte)$/, "");
571
+ route = route.replace(/\/index$/, "");
572
+ route = route.replace(/^index$/, "");
573
+ route = route.replace(/\[\.\.\.(\w+)\]/g, "*");
574
+ route = route.replace(/\[(\w+)\]/g, ":$1");
575
+ route = "/" + route;
576
+ route = route.replace(/\/+/g, "/");
577
+ if (route.length > 1) {
578
+ route = route.replace(/\/$/, "");
579
+ }
580
+ return route;
581
+ }
582
+ function getComponentName(filePath) {
583
+ const basename2 = path4.basename(filePath);
584
+ return basename2.replace(/\.(vue|astro|tsx|jsx|svelte)$/, "");
585
+ }
586
+ async function scanDirectory(dir, extensions, cwd) {
587
+ const fullDir = path4.resolve(cwd, dir);
588
+ try {
589
+ await promises.access(fullDir);
590
+ } catch {
591
+ return [];
592
+ }
593
+ const patterns = extensions.map((ext) => `**/*${ext}`);
594
+ const files = [];
595
+ for (const pattern of patterns) {
596
+ const matches = await glob(pattern, {
597
+ cwd: fullDir,
598
+ absolute: true,
599
+ nodir: true
600
+ });
601
+ files.push(...matches);
602
+ }
603
+ return files;
604
+ }
605
+ async function scanProjectSource(config) {
606
+ const cwd = config.cwd ?? process.cwd();
607
+ const extensions = config.extensions ?? DEFAULT_EXTENSIONS;
608
+ const routes = [];
609
+ const components = [];
610
+ const allElements = [];
611
+ if (config.pagesDir) {
612
+ const pageFiles = await scanDirectory(config.pagesDir, extensions, cwd);
613
+ const pagesFullDir = path4.resolve(cwd, config.pagesDir);
614
+ for (const file of pageFiles) {
615
+ const routePath = fileToRoute(file, pagesFullDir);
616
+ const name = getComponentName(file);
617
+ routes.push({
618
+ path: routePath,
619
+ file,
620
+ name
621
+ });
622
+ const content = await promises.readFile(file, "utf-8");
623
+ const elements = extractElements(content, file, routePath);
624
+ components.push({
625
+ name,
626
+ file,
627
+ elements
628
+ });
629
+ allElements.push(...elements);
630
+ }
631
+ }
632
+ if (config.componentsDir) {
633
+ const componentFiles = await scanDirectory(config.componentsDir, extensions, cwd);
634
+ for (const file of componentFiles) {
635
+ if (components.some((c) => c.file === file)) {
636
+ continue;
637
+ }
638
+ const name = getComponentName(file);
639
+ const content = await promises.readFile(file, "utf-8");
640
+ const elements = extractElements(content, file, void 0);
641
+ components.push({
642
+ name,
643
+ file,
644
+ elements
645
+ });
646
+ allElements.push(...elements);
647
+ }
648
+ }
649
+ if (!config.pagesDir && !config.componentsDir) {
650
+ const commonPageDirs = ["src/pages", "pages", "app", "src/app", "src/routes"];
651
+ const commonComponentDirs = ["src/components", "components", "src/lib", "lib"];
652
+ for (const dir of commonPageDirs) {
653
+ const files = await scanDirectory(dir, extensions, cwd);
654
+ if (files.length > 0) {
655
+ const pagesFullDir = path4.resolve(cwd, dir);
656
+ for (const file of files) {
657
+ const routePath = fileToRoute(file, pagesFullDir);
658
+ const name = getComponentName(file);
659
+ routes.push({
660
+ path: routePath,
661
+ file,
662
+ name
663
+ });
664
+ const content = await promises.readFile(file, "utf-8");
665
+ const elements = extractElements(content, file, routePath);
666
+ components.push({
667
+ name,
668
+ file,
669
+ elements
670
+ });
671
+ allElements.push(...elements);
672
+ }
673
+ break;
674
+ }
675
+ }
676
+ for (const dir of commonComponentDirs) {
677
+ const files = await scanDirectory(dir, extensions, cwd);
678
+ if (files.length > 0) {
679
+ for (const file of files) {
680
+ if (components.some((c) => c.file === file)) {
681
+ continue;
682
+ }
683
+ const name = getComponentName(file);
684
+ const content = await promises.readFile(file, "utf-8");
685
+ const elements = extractElements(content, file, void 0);
686
+ components.push({
687
+ name,
688
+ file,
689
+ elements
690
+ });
691
+ allElements.push(...elements);
692
+ }
693
+ break;
694
+ }
695
+ }
696
+ }
697
+ return {
698
+ routes,
699
+ components,
700
+ allElements
701
+ };
702
+ }
703
+ function formatScanResultsForPrompt(result) {
704
+ const lines = [];
705
+ if (result.routes.length > 0) {
706
+ lines.push("## ROUTES");
707
+ lines.push("");
708
+ for (const route of result.routes) {
709
+ lines.push(`- ${route.path}: ${route.name}`);
710
+ }
711
+ lines.push("");
712
+ }
713
+ const elementsByRoute = /* @__PURE__ */ new Map();
714
+ for (const element of result.allElements) {
715
+ const route = element.route ?? "shared";
716
+ if (!elementsByRoute.has(route)) {
717
+ elementsByRoute.set(route, []);
718
+ }
719
+ elementsByRoute.get(route).push(element);
720
+ }
721
+ lines.push("## ELEMENTS");
722
+ lines.push("");
723
+ for (const [route, elements] of elementsByRoute) {
724
+ lines.push(`### ${route === "shared" ? "Shared Components" : route}`);
725
+ lines.push("");
726
+ for (const el of elements) {
727
+ const locators = [];
728
+ if (el.testId) locators.push(`data-testid="${el.testId}"`);
729
+ if (el.text) locators.push(`text="${el.text}"`);
730
+ if (el.role) locators.push(`role="${el.role}"`);
731
+ if (el.name) locators.push(`name="${el.name}"`);
732
+ if (el.placeholder) locators.push(`placeholder="${el.placeholder}"`);
733
+ if (el.type) locators.push(`type="${el.type}"`);
734
+ const locatorStr = locators.length > 0 ? `[${locators.join(", ")}]` : "";
735
+ const description = el.description ? ` - ${el.description}` : "";
736
+ lines.push(`- <${el.tag}>${locatorStr}${description}`);
737
+ }
738
+ lines.push("");
739
+ }
740
+ return lines.join("\n");
741
+ }
742
+
743
+ // src/generator/prompts.ts
744
+ var SYSTEM_PROMPT = `You are a test automation expert that converts natural language test descriptions into YAML test definitions.
745
+
746
+ ## Schema Structure
747
+
748
+ A test definition must have:
749
+ - name: A descriptive test name (non-empty string)
750
+ - platform: One of 'web', 'android', or 'ios'
751
+ - config: Optional configuration object
752
+ - steps: Array of actions (minimum 1 action required)
753
+
754
+ ## Available Actions
755
+
756
+ 1. navigate - Navigate to a URL
757
+ { type: 'navigate', value: string }
758
+
759
+ 2. tap - Click or tap an element
760
+ { type: 'tap', target: Locator }
761
+
762
+ 3. input - Type text into an input field
763
+ { type: 'input', target: Locator, value: string }
764
+
765
+ 4. assert - Assert element exists or contains text
766
+ { type: 'assert', target: Locator, value?: string }
767
+
768
+ 5. wait - Wait for an element or timeout
769
+ { type: 'wait', target?: Locator, timeout?: number }
770
+ Note: Requires either target or timeout
771
+
772
+ 6. scroll - Scroll the page or to an element
773
+ { type: 'scroll', target?: Locator, direction?: 'up'|'down', amount?: number }
774
+
775
+ 7. screenshot - Take a screenshot
776
+ { type: 'screenshot', name?: string }
777
+
778
+ ## Locator Structure
779
+
780
+ A locator must have AT LEAST ONE of these properties:
781
+ - description: Human-readable description for AI healing
782
+ - testId: data-testid attribute value
783
+ - text: Text content to match
784
+ - css: CSS selector
785
+ - xpath: XPath expression
786
+ - role: ARIA role attribute
787
+ - name: Accessible name
788
+
789
+ ## Configuration Options
790
+
791
+ web:
792
+ baseUrl: Base URL for the application
793
+ browser: Browser to use (e.g., 'chromium', 'firefox', 'webkit')
794
+ headless: Run browser in headless mode (boolean)
795
+ timeout: Default timeout in milliseconds
796
+
797
+ android:
798
+ appId: Android application package ID
799
+ device: Device name or ID
800
+
801
+ ios:
802
+ bundleId: iOS bundle identifier
803
+ simulator: Simulator name
804
+
805
+ ## Example 1: Login Test
806
+
807
+ \`\`\`yaml
808
+ name: Login with valid credentials
809
+ platform: web
810
+ config:
811
+ web:
812
+ baseUrl: https://example.com
813
+ headless: true
814
+ steps:
815
+ - type: navigate
816
+ value: /login
817
+ - type: input
818
+ target:
819
+ testId: email-input
820
+ description: Email input field
821
+ value: test@example.com
822
+ - type: input
823
+ target:
824
+ testId: password-input
825
+ description: Password input field
826
+ value: password123
827
+ - type: tap
828
+ target:
829
+ text: Sign In
830
+ role: button
831
+ description: Sign in button
832
+ - type: assert
833
+ target:
834
+ text: Welcome
835
+ description: Welcome message after login
836
+ \`\`\`
837
+
838
+ ## Example 2: Search Test
839
+
840
+ \`\`\`yaml
841
+ name: Search for products
842
+ platform: web
843
+ config:
844
+ web:
845
+ baseUrl: https://shop.example.com
846
+ steps:
847
+ - type: navigate
848
+ value: /
849
+ - type: input
850
+ target:
851
+ css: input[type="search"]
852
+ description: Product search input
853
+ value: laptop
854
+ - type: tap
855
+ target:
856
+ role: button
857
+ name: Search
858
+ description: Search button
859
+ - type: wait
860
+ target:
861
+ css: .search-results
862
+ description: Search results container
863
+ timeout: 5000
864
+ - type: assert
865
+ target:
866
+ text: results found
867
+ description: Results count message
868
+ \`\`\`
869
+
870
+ ## Example 3: Mobile App Test
871
+
872
+ \`\`\`yaml
873
+ name: Add item to cart
874
+ platform: android
875
+ config:
876
+ android:
877
+ appId: com.example.shop
878
+ steps:
879
+ - type: tap
880
+ target:
881
+ testId: category-electronics
882
+ description: Electronics category button
883
+ - type: scroll
884
+ direction: down
885
+ amount: 300
886
+ - type: tap
887
+ target:
888
+ text: Laptop Pro
889
+ description: Product card for Laptop Pro
890
+ - type: tap
891
+ target:
892
+ testId: add-to-cart-button
893
+ description: Add to cart button
894
+ - type: assert
895
+ target:
896
+ text: Added to cart
897
+ description: Success message
898
+ - type: screenshot
899
+ name: cart-confirmation
900
+ \`\`\`
901
+
902
+ ## Important Instructions
903
+
904
+ 1. Output ONLY valid YAML - no markdown code blocks, no explanations
905
+ 2. Every locator MUST have at least one selector property
906
+ 3. Include descriptive locator descriptions for AI healing
907
+ 4. Use multiple locator strategies when possible for resilience
908
+ 5. For wait actions, provide either a target or timeout (or both)
909
+ 6. Use appropriate platform-specific configurations
910
+ 7. Ensure all strings are properly quoted if they contain special characters
911
+ 8. Action steps must be in logical order
912
+
913
+ Generate the test definition now based on the user's description.`;
914
+ function buildPrompt(naturalLanguage, context) {
915
+ const parts = [
916
+ "Generate a test definition for the following scenario:",
917
+ "",
918
+ naturalLanguage
919
+ ];
920
+ if (context) {
921
+ parts.push("", "Additional Context:");
922
+ if (context.platform) {
923
+ parts.push(`- Platform: ${context.platform}`);
924
+ }
925
+ if (context.baseUrl) {
926
+ parts.push(`- Base URL: ${context.baseUrl}`);
927
+ }
928
+ if (context.additionalContext) {
929
+ parts.push(`- ${context.additionalContext}`);
930
+ }
931
+ }
932
+ parts.push("", "Output only valid YAML without code block markers.");
933
+ return parts.join("\n");
934
+ }
935
+ function buildSourceAwareSystemPrompt(scanResult) {
936
+ const parts = [
937
+ "You are a test automation expert that converts natural language test descriptions into YAML test definitions.",
938
+ "",
939
+ "## Schema Structure",
940
+ "",
941
+ "A test definition must have:",
942
+ "- name: A descriptive test name (non-empty string)",
943
+ "- platform: One of 'web', 'android', or 'ios'",
944
+ "- config: Optional configuration object",
945
+ "- steps: Array of actions (minimum 1 action required)",
946
+ "",
947
+ "## Available Actions",
948
+ "",
949
+ "1. navigate - Navigate to a URL",
950
+ " { type: 'navigate', value: string }",
951
+ "",
952
+ "2. tap - Click or tap an element",
953
+ " { type: 'tap', target: Locator }",
954
+ "",
955
+ "3. input - Type text into an input field",
956
+ " { type: 'input', target: Locator, value: string }",
957
+ "",
958
+ "4. assert - Assert element exists or contains text",
959
+ " { type: 'assert', target: Locator, value?: string }",
960
+ "",
961
+ "5. wait - Wait for an element or timeout",
962
+ " { type: 'wait', target?: Locator, timeout?: number }",
963
+ " Note: Requires either target or timeout",
964
+ "",
965
+ "6. scroll - Scroll the page or to an element",
966
+ " { type: 'scroll', target?: Locator, direction?: 'up'|'down', amount?: number }",
967
+ "",
968
+ "7. screenshot - Take a screenshot",
969
+ " { type: 'screenshot', name?: string }",
970
+ "",
971
+ "## Locator Structure",
972
+ "",
973
+ "A locator must have AT LEAST ONE of these properties:",
974
+ "- description: Human-readable description for AI healing",
975
+ "- testId: data-testid attribute value",
976
+ "- text: Text content to match",
977
+ "- css: CSS selector",
978
+ "- xpath: XPath expression",
979
+ "- role: ARIA role attribute",
980
+ "- name: Accessible name",
981
+ "",
982
+ "## PROJECT STRUCTURE",
983
+ "",
984
+ "The following routes and elements were extracted from the project source code.",
985
+ "Use these REAL selectors in your generated tests.",
986
+ "",
987
+ "### Selector Priority (prefer earlier options):",
988
+ "1. text - Most resilient to DOM changes",
989
+ "2. role + name - ARIA-compliant, accessible",
990
+ "3. testId - Explicit but requires dev setup",
991
+ "4. css - Last resort, fragile",
992
+ ""
993
+ ];
994
+ parts.push(formatScanResultsForPrompt(scanResult));
995
+ parts.push(
996
+ "## Configuration Options",
997
+ "",
998
+ "web:",
999
+ " baseUrl: Base URL for the application",
1000
+ " browser: Browser to use (e.g., 'chromium', 'firefox', 'webkit')",
1001
+ " headless: Run browser in headless mode (boolean)",
1002
+ " timeout: Default timeout in milliseconds",
1003
+ "",
1004
+ "android:",
1005
+ " appId: Android application package ID",
1006
+ " device: Device name or ID",
1007
+ "",
1008
+ "ios:",
1009
+ " bundleId: iOS bundle identifier",
1010
+ " simulator: Simulator name",
1011
+ "",
1012
+ "## Example Test Structure",
1013
+ "",
1014
+ "```yaml",
1015
+ "name: Example test name",
1016
+ "platform: web",
1017
+ "config:",
1018
+ " web:",
1019
+ " baseUrl: https://example.com",
1020
+ " headless: true",
1021
+ "steps:",
1022
+ " - type: navigate",
1023
+ " value: /login",
1024
+ " - type: input",
1025
+ " target:",
1026
+ " text: Email",
1027
+ " description: Email input field",
1028
+ " value: test@example.com",
1029
+ " - type: input",
1030
+ " target:",
1031
+ " role: textbox",
1032
+ " name: Password",
1033
+ " description: Password input field",
1034
+ " value: password123",
1035
+ " - type: tap",
1036
+ " target:",
1037
+ " text: Sign In",
1038
+ " role: button",
1039
+ " description: Sign in button",
1040
+ " - type: assert",
1041
+ " target:",
1042
+ " text: Welcome",
1043
+ " description: Welcome message after login",
1044
+ "```",
1045
+ "",
1046
+ "## Important Instructions",
1047
+ "",
1048
+ "1. Output ONLY valid YAML - no markdown code blocks, no explanations",
1049
+ "2. Use REAL selectors from the project structure above whenever possible",
1050
+ "3. Every locator MUST have at least one selector property",
1051
+ "4. Include descriptive locator descriptions for AI healing",
1052
+ "5. Prefer text and role selectors over testId and css for resilience",
1053
+ "6. Use multiple locator strategies when possible for resilience",
1054
+ "7. For wait actions, provide either a target or timeout (or both)",
1055
+ "8. Use appropriate platform-specific configurations",
1056
+ "9. Ensure all strings are properly quoted if they contain special characters",
1057
+ "10. Action steps must be in logical order",
1058
+ "",
1059
+ "Generate the test definition now based on the user's description."
1060
+ );
1061
+ return parts.join("\n");
1062
+ }
1063
+
1064
+ // src/generator/testGenerator.ts
1065
+ function cleanYamlResponse(response) {
1066
+ let cleaned = response.replace(/```ya?ml\n?/gi, "").replace(/```\n?/g, "");
1067
+ cleaned = cleaned.trim();
1068
+ return cleaned;
1069
+ }
1070
+ async function generateTest(naturalLanguage, options) {
1071
+ const provider = createAIProvider(options.aiConfig);
1072
+ let systemPrompt = SYSTEM_PROMPT;
1073
+ if (options.source !== null) {
1074
+ const sourceConfig = options.source ?? {};
1075
+ const scanResult = await scanProjectSource(sourceConfig);
1076
+ if (scanResult.allElements.length > 0) {
1077
+ systemPrompt = buildSourceAwareSystemPrompt(scanResult);
1078
+ }
1079
+ }
1080
+ const context = {
1081
+ baseUrl: options.baseUrl,
1082
+ platform: options.platform,
1083
+ additionalContext: options.additionalContext
1084
+ };
1085
+ const userPrompt = buildPrompt(naturalLanguage, context);
1086
+ const maxRetries = options.maxRetries ?? 3;
1087
+ let lastError;
1088
+ let lastYaml;
1089
+ for (let attempt = 0; attempt < maxRetries; attempt++) {
1090
+ try {
1091
+ let promptWithFeedback = userPrompt;
1092
+ if (attempt > 0 && lastError) {
1093
+ promptWithFeedback = `${userPrompt}
1094
+
1095
+ Previous attempt failed with error: ${lastError.message}
1096
+
1097
+ Please fix the issue and generate valid YAML.`;
1098
+ }
1099
+ const response = await provider.generateCompletion(promptWithFeedback, systemPrompt);
1100
+ const yaml = cleanYamlResponse(response);
1101
+ lastYaml = yaml;
1102
+ const parsed = parse(yaml);
1103
+ const validated = TestDefinitionSchema.parse(parsed);
1104
+ return {
1105
+ success: true,
1106
+ test: validated,
1107
+ yaml,
1108
+ attempts: attempt + 1
1109
+ };
1110
+ } catch (error) {
1111
+ lastError = error instanceof Error ? error : new Error(String(error));
1112
+ if (attempt === maxRetries - 1) {
1113
+ return {
1114
+ success: false,
1115
+ error: `Failed to generate valid test after ${maxRetries} attempts. Last error: ${lastError.message}`,
1116
+ yaml: lastYaml,
1117
+ attempts: maxRetries
1118
+ };
1119
+ }
1120
+ }
1121
+ }
1122
+ return {
1123
+ success: false,
1124
+ error: "Unknown error occurred during test generation",
1125
+ attempts: maxRetries
1126
+ };
1127
+ }
1128
+ function displayMissingEnvVars(missing) {
1129
+ console.log("\n\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510");
1130
+ console.log("\u2502 \u26A0\uFE0F Missing Environment Variables \u2502");
1131
+ console.log("\u251C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524");
1132
+ for (const name of missing) {
1133
+ console.log(`\u2502 \u2022 ${name.padEnd(39)}\u2502`);
1134
+ }
1135
+ console.log("\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n");
1136
+ }
1137
+ async function promptAddToEnv(missing, envPath) {
1138
+ const { shouldAdd } = await prompts({
1139
+ type: "confirm",
1140
+ name: "shouldAdd",
1141
+ message: `Add missing variables to ${path4__default.basename(envPath)}?`,
1142
+ initial: true
1143
+ });
1144
+ if (!shouldAdd) return false;
1145
+ const values = {};
1146
+ for (const name of missing) {
1147
+ const { value } = await prompts({
1148
+ type: "password",
1149
+ // Hide sensitive values
1150
+ name: "value",
1151
+ message: `Enter value for ${name}:`
1152
+ });
1153
+ if (value !== void 0) {
1154
+ values[name] = value;
1155
+ }
1156
+ }
1157
+ const lines = Object.entries(values).map(([key, val]) => `${key}=${val}`).join("\n");
1158
+ let existingContent = "";
1159
+ try {
1160
+ existingContent = await fs3.readFile(envPath, "utf8");
1161
+ } catch {
1162
+ }
1163
+ const newContent = existingContent ? `${existingContent.trimEnd()}
1164
+ ${lines}
1165
+ ` : `${lines}
1166
+ `;
1167
+ await fs3.writeFile(envPath, newContent, "utf8");
1168
+ console.log(`
1169
+ \u2713 Added ${missing.length} variable(s) to ${path4__default.basename(envPath)}
1170
+ `);
1171
+ dotenv2.config({ path: envPath, override: true });
1172
+ return true;
1173
+ }
1174
+ async function validateEnvVars(missing, projectDir) {
1175
+ if (missing.length === 0) return true;
1176
+ displayMissingEnvVars(missing);
1177
+ const envPath = path4__default.join(projectDir, ".env");
1178
+ const added = await promptAddToEnv(missing, envPath);
1179
+ if (!added) {
1180
+ console.log("Cannot continue without required environment variables.");
1181
+ return false;
1182
+ }
1183
+ return true;
1184
+ }
1185
+
1186
+ // src/cli/index.ts
1187
+ dotenv2.config();
1188
+ var CONFIG_FILENAME = "intellitester.config.yaml";
1189
+ var BROWSER_ALIASES = {
1190
+ chrome: "chromium",
1191
+ chromium: "chromium",
1192
+ safari: "webkit",
1193
+ webkit: "webkit",
1194
+ firefox: "firefox",
1195
+ ff: "firefox"
1196
+ };
1197
+ var resolveBrowserName = (input) => {
1198
+ const normalized = input.toLowerCase().trim();
1199
+ const resolved = BROWSER_ALIASES[normalized];
1200
+ if (!resolved) {
1201
+ const valid = Object.keys(BROWSER_ALIASES).join(", ");
1202
+ throw new Error(`Unknown browser "${input}". Valid options: ${valid}`);
1203
+ }
1204
+ return resolved;
1205
+ };
1206
+ var detectPackageManager = async () => {
1207
+ if (await fileExists("pnpm-lock.yaml")) return "pnpm";
1208
+ if (await fileExists("bun.lockb")) return "bun";
1209
+ if (await fileExists("yarn.lock")) return "yarn";
1210
+ return "npm";
1211
+ };
1212
+ var execCommand = async (cmd, args, cwd) => {
1213
+ return new Promise((resolve2, reject) => {
1214
+ console.log(`Running: ${cmd} ${args.join(" ")}`);
1215
+ const child = spawn(cmd, args, {
1216
+ cwd,
1217
+ stdio: "inherit",
1218
+ shell: true
1219
+ });
1220
+ child.on("close", (code) => {
1221
+ if (code === 0) resolve2();
1222
+ else reject(new Error(`Command failed with exit code ${code}`));
1223
+ });
1224
+ child.on("error", reject);
1225
+ });
1226
+ };
1227
+ var buildAndPreview = async (config, cwd) => {
1228
+ const pm = await detectPackageManager();
1229
+ const previewConfig = config?.preview || {};
1230
+ const buildCmd = previewConfig.build?.command || `${pm} run build`;
1231
+ const [buildExec, ...buildArgs] = buildCmd.split(" ");
1232
+ const previewCmd = previewConfig.preview?.command || `${pm} run preview`;
1233
+ const [previewExec, ...previewArgs] = previewCmd.split(" ");
1234
+ const previewUrl = previewConfig.url || config?.webServer?.url || config?.platforms?.web?.baseUrl || "http://localhost:4321";
1235
+ const timeout = previewConfig.timeout || 6e4;
1236
+ console.log("\n\u{1F4E6} Building project...\n");
1237
+ await execCommand(buildExec, buildArgs, cwd);
1238
+ console.log("\n\u2705 Build complete\n");
1239
+ console.log("\n\u{1F680} Starting preview server...\n");
1240
+ const previewProcess = await startPreviewServer(previewExec, previewArgs, cwd, previewUrl, timeout);
1241
+ const cleanup = () => {
1242
+ if (previewProcess && !previewProcess.killed) {
1243
+ console.log("\n\u{1F6D1} Stopping preview server...");
1244
+ previewProcess.kill("SIGTERM");
1245
+ }
1246
+ };
1247
+ process2.on("SIGINT", cleanup);
1248
+ process2.on("SIGTERM", cleanup);
1249
+ return { previewProcess, cleanup };
1250
+ };
1251
+ var startPreviewServer = async (cmd, args, cwd, url, timeout = 6e4) => {
1252
+ return new Promise((resolve2, reject) => {
1253
+ console.log(`Starting preview server: ${cmd} ${args.join(" ")}`);
1254
+ const child = spawn(cmd, args, {
1255
+ cwd,
1256
+ stdio: "pipe",
1257
+ shell: true
1258
+ });
1259
+ let output = "";
1260
+ const startTime = Date.now();
1261
+ const checkServer = async () => {
1262
+ try {
1263
+ const response = await fetch(url, { method: "HEAD" });
1264
+ if (response.ok || response.status < 500) {
1265
+ console.log(`Preview server ready at ${url}`);
1266
+ resolve2(child);
1267
+ return true;
1268
+ }
1269
+ } catch {
1270
+ }
1271
+ return false;
1272
+ };
1273
+ const pollInterval = setInterval(async () => {
1274
+ if (await checkServer()) {
1275
+ clearInterval(pollInterval);
1276
+ } else if (Date.now() - startTime > timeout) {
1277
+ clearInterval(pollInterval);
1278
+ child.kill();
1279
+ reject(new Error(`Preview server failed to start within ${timeout}ms`));
1280
+ }
1281
+ }, 500);
1282
+ child.stdout?.on("data", (data) => {
1283
+ output += data.toString();
1284
+ process2.stdout.write(data);
1285
+ });
1286
+ child.stderr?.on("data", (data) => {
1287
+ output += data.toString();
1288
+ process2.stderr.write(data);
1289
+ });
1290
+ child.on("error", (err) => {
1291
+ clearInterval(pollInterval);
1292
+ reject(err);
1293
+ });
1294
+ child.on("close", (code) => {
1295
+ clearInterval(pollInterval);
1296
+ if (code !== 0 && code !== null) {
1297
+ reject(new Error(`Preview server exited with code ${code}
1298
+ ${output}`));
1299
+ }
1300
+ });
1301
+ });
1302
+ };
1303
+ var logError = (message) => {
1304
+ console.error(`Error: ${message}`);
1305
+ };
1306
+ var findProjectRoot = async (startPath) => {
1307
+ let currentDir = path4__default.isAbsolute(startPath) ? startPath : path4__default.resolve(startPath);
1308
+ try {
1309
+ const stat = await fs3.stat(currentDir);
1310
+ if (stat.isFile()) {
1311
+ currentDir = path4__default.dirname(currentDir);
1312
+ }
1313
+ } catch {
1314
+ currentDir = path4__default.dirname(currentDir);
1315
+ }
1316
+ const root = path4__default.parse(currentDir).root;
1317
+ while (currentDir !== root) {
1318
+ const markers = ["package.json", ".git", CONFIG_FILENAME];
1319
+ for (const marker of markers) {
1320
+ if (await fileExists(path4__default.join(currentDir, marker))) {
1321
+ return currentDir;
1322
+ }
1323
+ }
1324
+ const parentDir = path4__default.dirname(currentDir);
1325
+ if (parentDir === currentDir) break;
1326
+ currentDir = parentDir;
1327
+ }
1328
+ return null;
1329
+ };
1330
+ var loadProjectEnv = async (targetPath) => {
1331
+ const projectRoot = await findProjectRoot(targetPath);
1332
+ if (projectRoot) {
1333
+ const envPath = path4__default.join(projectRoot, ".env");
1334
+ if (await fileExists(envPath)) {
1335
+ dotenv2.config({ path: envPath, override: false });
1336
+ console.log(`Loaded .env from ${projectRoot}`);
1337
+ }
1338
+ }
1339
+ };
1340
+ var fileExists = async (filePath) => {
1341
+ try {
1342
+ await fs3.access(filePath);
1343
+ return true;
1344
+ } catch {
1345
+ return false;
1346
+ }
1347
+ };
1348
+ var collectYamlFiles = async (target) => {
1349
+ const stat = await fs3.stat(target);
1350
+ if (stat.isFile()) return [target];
1351
+ if (!stat.isDirectory()) {
1352
+ throw new Error(`Unsupported target: ${target}`);
1353
+ }
1354
+ const entries = await fs3.readdir(target, { withFileTypes: true });
1355
+ const files = [];
1356
+ for (const entry of entries) {
1357
+ const fullPath = path4__default.join(target, entry.name);
1358
+ if (entry.isDirectory()) {
1359
+ const nested = await collectYamlFiles(fullPath);
1360
+ files.push(...nested);
1361
+ } else if (entry.isFile() && (entry.name.endsWith(".yaml") || entry.name.endsWith(".yml"))) {
1362
+ files.push(fullPath);
1363
+ }
1364
+ }
1365
+ return files;
1366
+ };
1367
+ var discoverTestFiles = async (testsDir = "tests") => {
1368
+ const absoluteDir = path4__default.resolve(testsDir);
1369
+ if (!await fileExists(absoluteDir)) {
1370
+ return { pipelines: [], workflows: [], tests: [] };
1371
+ }
1372
+ const allFiles = await collectYamlFiles(absoluteDir);
1373
+ const pipelines = [];
1374
+ const workflows = [];
1375
+ const tests = [];
1376
+ for (const file of allFiles) {
1377
+ const name = path4__default.basename(file).toLowerCase();
1378
+ if (name.endsWith(".pipeline.yaml") || name.endsWith(".pipeline.yml")) {
1379
+ pipelines.push(file);
1380
+ } else if (name.endsWith(".workflow.yaml") || name.endsWith(".workflow.yml")) {
1381
+ workflows.push(file);
1382
+ } else if (name.endsWith(".test.yaml") || name.endsWith(".test.yml")) {
1383
+ tests.push(file);
1384
+ }
1385
+ }
1386
+ return { pipelines, workflows, tests };
1387
+ };
1388
+ var writeFileIfMissing = async (filePath, contents) => {
1389
+ if (await fileExists(filePath)) return;
1390
+ await fs3.mkdir(path4__default.dirname(filePath), { recursive: true });
1391
+ await fs3.writeFile(filePath, contents, "utf8");
1392
+ };
1393
+ var initCommand = async () => {
1394
+ const configTemplate = `defaults:
1395
+ timeout: 30000
1396
+ screenshots: on-failure
1397
+
1398
+ platforms:
1399
+ web:
1400
+ baseUrl: http://localhost:3000
1401
+ headless: true
1402
+
1403
+ ai:
1404
+ provider: anthropic
1405
+ model: claude-3-5-sonnet-20241022
1406
+ apiKey: \${ANTHROPIC_API_KEY}
1407
+ temperature: 0
1408
+ maxTokens: 4096
1409
+
1410
+ email:
1411
+ provider: inbucket
1412
+ endpoint: http://localhost:9000
1413
+
1414
+ appwrite:
1415
+ endpoint: https://cloud.appwrite.io/v1
1416
+ projectId: your-project-id
1417
+ apiKey: your-api-key
1418
+ `;
1419
+ const sampleTest = `name: Example web smoke test
1420
+ platform: web
1421
+ config:
1422
+ web:
1423
+ baseUrl: http://localhost:3000
1424
+
1425
+ steps:
1426
+ - type: navigate
1427
+ value: /
1428
+
1429
+ - type: assert
1430
+ target:
1431
+ text: "Welcome"
1432
+ `;
1433
+ await writeFileIfMissing(path4__default.resolve(CONFIG_FILENAME), configTemplate);
1434
+ await writeFileIfMissing(path4__default.resolve("tests", "example.web.test.yaml"), sampleTest);
1435
+ console.log("Initialized intellitester.config.yaml and tests/example.web.test.yaml");
1436
+ };
1437
+ var validateCommand = async (target) => {
1438
+ const absoluteTarget = path4__default.resolve(target);
1439
+ const files = await collectYamlFiles(absoluteTarget);
1440
+ if (files.length === 0) {
1441
+ throw new Error(`No YAML files found at ${absoluteTarget}`);
1442
+ }
1443
+ for (const file of files) {
1444
+ await loadTestDefinition(file);
1445
+ console.log(`\u2713 ${path4__default.relative(process2.cwd(), file)} valid`);
1446
+ }
1447
+ };
1448
+ var resolveBaseUrl = (test, configBaseUrl) => test.config?.web?.baseUrl ?? configBaseUrl;
1449
+ var runTestCommand = async (target, options) => {
1450
+ const absoluteTarget = path4__default.resolve(target);
1451
+ await loadProjectEnv(absoluteTarget);
1452
+ const { parse: parse2 } = await import('yaml');
1453
+ const testContent = await fs3.readFile(absoluteTarget, "utf8");
1454
+ const parsedTest = parse2(testContent);
1455
+ const hasConfigFile = await fileExists(CONFIG_FILENAME);
1456
+ let parsedConfig = void 0;
1457
+ if (hasConfigFile) {
1458
+ const configContent = await fs3.readFile(CONFIG_FILENAME, "utf8");
1459
+ parsedConfig = parse2(configContent);
1460
+ }
1461
+ const configMissing = parsedConfig ? collectMissingEnvVars(parsedConfig) : [];
1462
+ const testMissing = collectMissingEnvVars(parsedTest);
1463
+ const allMissing = [.../* @__PURE__ */ new Set([...configMissing, ...testMissing])];
1464
+ if (allMissing.length > 0) {
1465
+ const projectRoot = await findProjectRoot(absoluteTarget);
1466
+ const canContinue = await validateEnvVars(allMissing, projectRoot || process2.cwd());
1467
+ if (!canContinue) {
1468
+ process2.exit(1);
1469
+ }
1470
+ }
1471
+ const test = await loadTestDefinition(absoluteTarget);
1472
+ const config = hasConfigFile ? await loadIntellitesterConfig(CONFIG_FILENAME) : void 0;
1473
+ const baseUrl = resolveBaseUrl(test, config?.platforms?.web?.baseUrl);
1474
+ const headed = options.headed ?? false;
1475
+ const browser = options.browser ?? "chromium";
1476
+ const skipWebServer = options.noServer ?? false;
1477
+ const debug = options.debug ?? false;
1478
+ const interactive = options.interactive ?? false;
1479
+ const modeFlags = [];
1480
+ if (headed) modeFlags.push("headed");
1481
+ if (debug) modeFlags.push("debug mode");
1482
+ if (interactive) modeFlags.push("interactive");
1483
+ console.log(
1484
+ `Running ${path4__default.basename(absoluteTarget)} on web (${browser}${modeFlags.length > 0 ? ", " + modeFlags.join(", ") : ""})`
1485
+ );
1486
+ const result = await runWebTest(test, {
1487
+ baseUrl,
1488
+ headed,
1489
+ browser,
1490
+ defaultTimeoutMs: config?.defaults?.timeout,
1491
+ webServer: !skipWebServer && config?.webServer ? config.webServer : void 0,
1492
+ debug,
1493
+ interactive,
1494
+ aiConfig: interactive ? config?.ai : void 0
1495
+ });
1496
+ for (const step of result.steps) {
1497
+ const label = `[${step.status === "passed" ? "OK" : "FAIL"}] ${step.action.type}`;
1498
+ if (step.error) {
1499
+ console.error(`${label} - ${step.error}`);
1500
+ } else {
1501
+ console.log(label);
1502
+ }
1503
+ }
1504
+ if (result.status === "failed") {
1505
+ process2.exitCode = 1;
1506
+ }
1507
+ };
1508
+ var generateCommand = async (prompt, options) => {
1509
+ const targetPath = options.output ? path4__default.resolve(options.output) : process2.cwd();
1510
+ await loadProjectEnv(targetPath);
1511
+ const hasConfigFile = await fileExists(CONFIG_FILENAME);
1512
+ if (!hasConfigFile) {
1513
+ throw new Error('No intellitester.config.yaml found. Run "intellitester init" first and configure AI settings.');
1514
+ }
1515
+ const { parse: parse2 } = await import('yaml');
1516
+ const configContent = await fs3.readFile(CONFIG_FILENAME, "utf8");
1517
+ const parsedConfig = parse2(configContent);
1518
+ const configMissing = collectMissingEnvVars(parsedConfig);
1519
+ if (configMissing.length > 0) {
1520
+ const projectRoot = await findProjectRoot(CONFIG_FILENAME);
1521
+ const canContinue = await validateEnvVars(configMissing, projectRoot || process2.cwd());
1522
+ if (!canContinue) {
1523
+ process2.exit(1);
1524
+ }
1525
+ }
1526
+ const config = await loadIntellitesterConfig(CONFIG_FILENAME);
1527
+ if (!config.ai) {
1528
+ throw new Error('AI configuration missing in intellitester.config.yaml. Add "ai:" section with provider, model, and apiKey.');
1529
+ }
1530
+ const source = options.noSource ? null : options.pagesDir || options.componentsDir ? {
1531
+ pagesDir: options.pagesDir,
1532
+ componentsDir: options.componentsDir
1533
+ } : void 0;
1534
+ const generateOptions = {
1535
+ aiConfig: config.ai,
1536
+ baseUrl: options.baseUrl,
1537
+ platform: options.platform,
1538
+ source
1539
+ };
1540
+ console.log("Generating test...");
1541
+ const result = await generateTest(prompt, generateOptions);
1542
+ if (!result.success) {
1543
+ throw new Error(result.error || "Failed to generate test");
1544
+ }
1545
+ if (options.output) {
1546
+ await fs3.mkdir(path4__default.dirname(options.output), { recursive: true });
1547
+ await fs3.writeFile(options.output, result.yaml, "utf8");
1548
+ console.log(`\u2713 Test saved to ${options.output}`);
1549
+ } else {
1550
+ console.log("\n--- Generated Test ---\n");
1551
+ console.log(result.yaml);
1552
+ }
1553
+ };
1554
+ var runWorkflowCommand = async (file, options) => {
1555
+ const workflowPath = path4__default.resolve(file);
1556
+ if (!await fileExists(workflowPath)) {
1557
+ logError(`Workflow file not found: ${file}`);
1558
+ process2.exit(1);
1559
+ }
1560
+ await loadProjectEnv(workflowPath);
1561
+ console.log(`Running workflow: ${file}`);
1562
+ const { parse: parse2 } = await import('yaml');
1563
+ const workflowContent = await fs3.readFile(workflowPath, "utf8");
1564
+ const parsedWorkflow = parse2(workflowContent);
1565
+ const hasConfigFile = await fileExists(CONFIG_FILENAME);
1566
+ let parsedConfig = void 0;
1567
+ if (hasConfigFile) {
1568
+ const configContent = await fs3.readFile(CONFIG_FILENAME, "utf8");
1569
+ parsedConfig = parse2(configContent);
1570
+ }
1571
+ const configMissing = parsedConfig ? collectMissingEnvVars(parsedConfig) : [];
1572
+ const workflowMissing = collectMissingEnvVars(parsedWorkflow);
1573
+ const allMissing = [.../* @__PURE__ */ new Set([...configMissing, ...workflowMissing])];
1574
+ if (allMissing.length > 0) {
1575
+ const projectRoot = await findProjectRoot(workflowPath);
1576
+ const canContinue = await validateEnvVars(allMissing, projectRoot || process2.cwd());
1577
+ if (!canContinue) {
1578
+ process2.exit(1);
1579
+ }
1580
+ }
1581
+ const workflow = await loadWorkflowDefinition(workflowPath);
1582
+ const config = hasConfigFile ? await loadIntellitesterConfig(CONFIG_FILENAME) : void 0;
1583
+ const result = await runWorkflow(workflow, workflowPath, {
1584
+ headed: options.visible,
1585
+ browser: options.browser,
1586
+ interactive: options.interactive,
1587
+ debug: options.debug,
1588
+ aiConfig: config?.ai
1589
+ });
1590
+ console.log(`
1591
+ Workflow: ${workflow.name}`);
1592
+ console.log(`Session ID: ${result.sessionId}`);
1593
+ console.log(`Status: ${result.status}
1594
+ `);
1595
+ for (const test of result.tests) {
1596
+ const icon = test.status === "passed" ? "\u2713" : test.status === "failed" ? "\u2717" : "\u25CB";
1597
+ console.log(` ${icon} ${test.file} (${test.status})`);
1598
+ if (test.error) {
1599
+ console.log(` Error: ${test.error}`);
1600
+ }
1601
+ }
1602
+ if (result.cleanupResult) {
1603
+ console.log(`
1604
+ Cleanup: ${result.cleanupResult.deleted.length} resources deleted`);
1605
+ if (result.cleanupResult.failed.length > 0) {
1606
+ console.log(` Failed to delete: ${result.cleanupResult.failed.join(", ")}`);
1607
+ }
1608
+ }
1609
+ process2.exit(result.status === "passed" ? 0 : 1);
1610
+ };
1611
+ var runPipelineCommand = async (file, options) => {
1612
+ const pipelinePath = path4__default.resolve(file);
1613
+ if (!await fileExists(pipelinePath)) {
1614
+ logError(`Pipeline file not found: ${file}`);
1615
+ process2.exit(1);
1616
+ }
1617
+ await loadProjectEnv(pipelinePath);
1618
+ console.log(`Running pipeline: ${file}`);
1619
+ const { parse: parse2 } = await import('yaml');
1620
+ const pipelineContent = await fs3.readFile(pipelinePath, "utf8");
1621
+ const parsedPipeline = parse2(pipelineContent);
1622
+ const hasConfigFile = await fileExists(CONFIG_FILENAME);
1623
+ let parsedConfig = void 0;
1624
+ if (hasConfigFile) {
1625
+ const configContent = await fs3.readFile(CONFIG_FILENAME, "utf8");
1626
+ parsedConfig = parse2(configContent);
1627
+ }
1628
+ const configMissing = parsedConfig ? collectMissingEnvVars(parsedConfig) : [];
1629
+ const pipelineMissing = collectMissingEnvVars(parsedPipeline);
1630
+ const allMissing = [.../* @__PURE__ */ new Set([...configMissing, ...pipelineMissing])];
1631
+ if (allMissing.length > 0) {
1632
+ const projectRoot = await findProjectRoot(pipelinePath);
1633
+ const canContinue = await validateEnvVars(allMissing, projectRoot || process2.cwd());
1634
+ if (!canContinue) {
1635
+ process2.exit(1);
1636
+ }
1637
+ }
1638
+ const pipeline = await loadPipelineDefinition(pipelinePath);
1639
+ hasConfigFile ? await loadIntellitesterConfig(CONFIG_FILENAME) : void 0;
1640
+ const result = await runPipeline(pipeline, pipelinePath, {
1641
+ headed: options.visible,
1642
+ browser: options.browser,
1643
+ interactive: options.interactive,
1644
+ debug: options.debug
1645
+ });
1646
+ console.log(`
1647
+ Pipeline: ${pipeline.name}`);
1648
+ console.log(`Session ID: ${result.sessionId}`);
1649
+ console.log(`Status: ${result.status}
1650
+ `);
1651
+ for (const workflow of result.workflows) {
1652
+ const icon = workflow.status === "passed" ? "\u2713" : workflow.status === "failed" ? "\u2717" : "\u25CB";
1653
+ console.log(` ${icon} ${workflow.file} (${workflow.status})`);
1654
+ if (workflow.error) {
1655
+ console.log(` Error: ${workflow.error}`);
1656
+ }
1657
+ }
1658
+ if (result.cleanupResult) {
1659
+ console.log(`
1660
+ Cleanup: ${result.cleanupResult.deleted.length} resources deleted`);
1661
+ if (result.cleanupResult.failed.length > 0) {
1662
+ console.log(` Failed to delete: ${result.cleanupResult.failed.join(", ")}`);
1663
+ }
1664
+ }
1665
+ process2.exit(result.status === "passed" ? 0 : 1);
1666
+ };
1667
+ var main = async () => {
1668
+ const program = new Command();
1669
+ program.name("intellitester").description("AI-powered cross-platform test automation").version("1.0.0");
1670
+ program.command("init").description("Initialize IntelliTester in current directory").action(async () => {
1671
+ try {
1672
+ await initCommand();
1673
+ } catch (error) {
1674
+ const message = error instanceof Error ? error.message : String(error);
1675
+ logError(message);
1676
+ process2.exitCode = 1;
1677
+ }
1678
+ });
1679
+ program.command("validate").description("Validate test YAML files").argument("[file]", "Test file or directory to validate", "tests").action(async (file) => {
1680
+ try {
1681
+ await validateCommand(file);
1682
+ } catch (error) {
1683
+ const message = error instanceof Error ? error.message : String(error);
1684
+ logError(message);
1685
+ process2.exitCode = 1;
1686
+ }
1687
+ });
1688
+ program.command("run").description("Run test file(s), workflow, or auto-discover tests in tests/ directory").argument("[file]", "Test file, workflow, or pipeline to run (auto-discovers if omitted)").option("--visible", "Run browser in visible mode (not headless)").option("--browser <name>", "Browser to use (chrome, safari, firefox)", "chrome").option("--preview", "Build project and run against preview server").option("--no-server", "Skip auto-starting web server").option("-i, --interactive", "Interactive mode - AI suggests fixes on failure").option("--debug", "Debug mode - verbose logging").action(async (file, options) => {
1689
+ let previewCleanup = null;
1690
+ try {
1691
+ const browser = resolveBrowserName(options.browser || "chrome");
1692
+ if (options.preview) {
1693
+ const hasConfigFile = await fileExists(CONFIG_FILENAME);
1694
+ const config = hasConfigFile ? await loadIntellitesterConfig(CONFIG_FILENAME) : void 0;
1695
+ const { cleanup } = await buildAndPreview(config, process2.cwd());
1696
+ previewCleanup = cleanup;
1697
+ }
1698
+ const runOpts = {
1699
+ visible: options.visible,
1700
+ browser,
1701
+ interactive: options.interactive,
1702
+ debug: options.debug
1703
+ };
1704
+ if (!file) {
1705
+ const discovered = await discoverTestFiles("tests");
1706
+ const total = discovered.pipelines.length + discovered.workflows.length + discovered.tests.length;
1707
+ if (total === 0) {
1708
+ logError("No test files found in tests/ directory. Create .pipeline.yaml, .workflow.yaml, or .test.yaml files.");
1709
+ process2.exit(1);
1710
+ }
1711
+ console.log(`Discovered ${total} test file(s):`);
1712
+ if (discovered.pipelines.length > 0) {
1713
+ console.log(` Pipelines: ${discovered.pipelines.length}`);
1714
+ }
1715
+ if (discovered.workflows.length > 0) {
1716
+ console.log(` Workflows: ${discovered.workflows.length}`);
1717
+ }
1718
+ if (discovered.tests.length > 0) {
1719
+ console.log(` Tests: ${discovered.tests.length}`);
1720
+ }
1721
+ console.log("");
1722
+ let failed = false;
1723
+ for (const pipeline of discovered.pipelines) {
1724
+ try {
1725
+ await runPipelineCommand(pipeline, runOpts);
1726
+ } catch {
1727
+ failed = true;
1728
+ }
1729
+ }
1730
+ for (const workflow of discovered.workflows) {
1731
+ try {
1732
+ await runWorkflowCommand(workflow, runOpts);
1733
+ } catch {
1734
+ failed = true;
1735
+ }
1736
+ }
1737
+ for (const test of discovered.tests) {
1738
+ try {
1739
+ await runTestCommand(test, {
1740
+ headed: options.visible,
1741
+ browser,
1742
+ noServer: !options.server,
1743
+ interactive: options.interactive,
1744
+ debug: options.debug
1745
+ });
1746
+ } catch {
1747
+ failed = true;
1748
+ }
1749
+ }
1750
+ if (failed) {
1751
+ process2.exitCode = 1;
1752
+ }
1753
+ return;
1754
+ }
1755
+ if (isPipelineFile(file)) {
1756
+ await runPipelineCommand(file, runOpts);
1757
+ return;
1758
+ }
1759
+ if (isWorkflowFile(file)) {
1760
+ await runWorkflowCommand(file, runOpts);
1761
+ return;
1762
+ }
1763
+ await runTestCommand(file, {
1764
+ headed: options.visible,
1765
+ browser,
1766
+ noServer: !options.server,
1767
+ interactive: options.interactive,
1768
+ debug: options.debug
1769
+ });
1770
+ } catch (error) {
1771
+ const message = error instanceof Error ? error.message : String(error);
1772
+ logError(message);
1773
+ process2.exitCode = 1;
1774
+ } finally {
1775
+ if (previewCleanup) {
1776
+ previewCleanup();
1777
+ }
1778
+ }
1779
+ });
1780
+ program.command("generate").description("Generate test from natural language").argument("<description>", "Natural language description of the test").option("--output <file>", "Output file path").option("--platform <platform>", "Target platform", "web").option("--baseUrl <url>", "Base URL for the app").option("--pagesDir <dir>", "Pages directory for source scanning").option("--componentsDir <dir>", "Components directory for source scanning").option("--no-source", "Disable source scanning").action(async (description, options) => {
1781
+ try {
1782
+ await generateCommand(description, {
1783
+ output: options.output,
1784
+ platform: options.platform,
1785
+ baseUrl: options.baseUrl,
1786
+ pagesDir: options.pagesDir,
1787
+ componentsDir: options.componentsDir,
1788
+ noSource: !options.source
1789
+ });
1790
+ } catch (error) {
1791
+ const message = error instanceof Error ? error.message : String(error);
1792
+ logError(message);
1793
+ process2.exitCode = 1;
1794
+ }
1795
+ });
1796
+ program.command("cleanup:list").description("List pending failed cleanup operations from previous test runs").action(async () => {
1797
+ try {
1798
+ const failedCleanups = await loadFailedCleanups(process2.cwd());
1799
+ if (failedCleanups.length === 0) {
1800
+ console.log("No failed cleanups found.");
1801
+ return;
1802
+ }
1803
+ console.log(`
1804
+ Found ${failedCleanups.length} failed cleanup(s):
1805
+ `);
1806
+ for (const failed of failedCleanups) {
1807
+ console.log(`Session: ${failed.sessionId}`);
1808
+ console.log(` Timestamp: ${failed.timestamp}`);
1809
+ console.log(` Provider: ${failed.providerConfig.provider}`);
1810
+ console.log(` Resources: ${failed.resources.length}`);
1811
+ for (const resource of failed.resources) {
1812
+ console.log(` - ${resource.type}:${resource.id}`);
1813
+ }
1814
+ console.log(` Errors: ${failed.errors.length}`);
1815
+ for (const error of failed.errors.slice(0, 3)) {
1816
+ console.log(` - ${error}`);
1817
+ }
1818
+ if (failed.errors.length > 3) {
1819
+ console.log(` ... and ${failed.errors.length - 3} more`);
1820
+ }
1821
+ console.log("");
1822
+ }
1823
+ console.log(`Use 'intellitester cleanup:retry' to retry these cleanups.
1824
+ `);
1825
+ } catch (error) {
1826
+ logError(error instanceof Error ? error.message : String(error));
1827
+ process2.exit(1);
1828
+ }
1829
+ });
1830
+ program.command("cleanup:retry").description("Retry failed cleanup operations from previous test runs").action(async () => {
1831
+ try {
1832
+ const hasConfigFile = await fileExists(CONFIG_FILENAME);
1833
+ if (!hasConfigFile) {
1834
+ throw new Error(`No ${CONFIG_FILENAME} found. Cannot retry cleanup without provider configuration.`);
1835
+ }
1836
+ const config = await loadIntellitesterConfig(CONFIG_FILENAME);
1837
+ const failedCleanups = await loadFailedCleanups(process2.cwd());
1838
+ if (failedCleanups.length === 0) {
1839
+ console.log("No failed cleanups to retry.");
1840
+ return;
1841
+ }
1842
+ console.log(`Found ${failedCleanups.length} failed cleanup(s) to retry.`);
1843
+ for (const failed of failedCleanups) {
1844
+ console.log(`
1845
+ Retrying cleanup for session ${failed.sessionId}...`);
1846
+ console.log(` Provider: ${failed.providerConfig.provider}`);
1847
+ console.log(` Resources: ${failed.resources.length}`);
1848
+ const provider = failed.providerConfig.provider;
1849
+ const cleanupConfig = {
1850
+ provider,
1851
+ parallel: false,
1852
+ retries: 3
1853
+ };
1854
+ const configAny = config;
1855
+ if (provider === "appwrite") {
1856
+ if (!config.appwrite?.apiKey) {
1857
+ console.log(` \u2717 Skipping: Appwrite API key not configured in ${CONFIG_FILENAME}`);
1858
+ continue;
1859
+ }
1860
+ cleanupConfig.appwrite = {
1861
+ endpoint: failed.providerConfig.endpoint,
1862
+ projectId: failed.providerConfig.projectId,
1863
+ apiKey: config.appwrite.apiKey
1864
+ };
1865
+ } else if (provider === "postgres") {
1866
+ const pgConfig = configAny.postgres;
1867
+ if (!pgConfig?.connectionString && !pgConfig?.password) {
1868
+ console.log(` \u2717 Skipping: Postgres credentials not configured in ${CONFIG_FILENAME}`);
1869
+ continue;
1870
+ }
1871
+ if (pgConfig.connectionString) {
1872
+ cleanupConfig.postgres = {
1873
+ connectionString: pgConfig.connectionString
1874
+ };
1875
+ } else {
1876
+ const host = failed.providerConfig.host;
1877
+ const port = failed.providerConfig.port;
1878
+ const database = failed.providerConfig.database;
1879
+ const user = failed.providerConfig.user;
1880
+ const password = pgConfig.password;
1881
+ cleanupConfig.postgres = {
1882
+ connectionString: `postgresql://${user}:${password}@${host}:${port}/${database}`
1883
+ };
1884
+ }
1885
+ } else if (provider === "mysql") {
1886
+ const mysqlConfig = configAny.mysql;
1887
+ if (!mysqlConfig?.password) {
1888
+ console.log(` \u2717 Skipping: MySQL password not configured in ${CONFIG_FILENAME}`);
1889
+ continue;
1890
+ }
1891
+ cleanupConfig.mysql = {
1892
+ host: failed.providerConfig.host,
1893
+ port: failed.providerConfig.port,
1894
+ user: failed.providerConfig.user,
1895
+ password: mysqlConfig.password,
1896
+ database: failed.providerConfig.database
1897
+ };
1898
+ } else if (provider === "sqlite") {
1899
+ const sqliteConfig = configAny.sqlite;
1900
+ if (!sqliteConfig?.database && !failed.providerConfig.database) {
1901
+ console.log(` \u2717 Skipping: SQLite database path not configured`);
1902
+ continue;
1903
+ }
1904
+ cleanupConfig.sqlite = {
1905
+ database: failed.providerConfig.database || sqliteConfig?.database
1906
+ };
1907
+ } else {
1908
+ console.log(` \u2717 Skipping: Unknown provider "${provider}"`);
1909
+ continue;
1910
+ }
1911
+ try {
1912
+ const { handlers, typeMappings } = await loadCleanupHandlers(
1913
+ cleanupConfig,
1914
+ process2.cwd()
1915
+ );
1916
+ const result = await executeCleanup(
1917
+ failed.resources,
1918
+ handlers,
1919
+ typeMappings,
1920
+ {
1921
+ parallel: false,
1922
+ retries: 3,
1923
+ cwd: process2.cwd()
1924
+ // Don't save failed cleanups again during retry
1925
+ }
1926
+ );
1927
+ if (result.success) {
1928
+ console.log(` \u2713 Successfully cleaned up ${result.deleted.length} resources`);
1929
+ await removeFailedCleanup(failed.sessionId, process2.cwd());
1930
+ } else {
1931
+ console.log(` \u26A0 Partial cleanup: ${result.deleted.length} deleted, ${result.failed.length} failed`);
1932
+ for (const failedResource of result.failed) {
1933
+ console.log(` \u2717 ${failedResource}`);
1934
+ }
1935
+ }
1936
+ } catch (error) {
1937
+ console.log(` \u2717 Error during cleanup: ${error instanceof Error ? error.message : String(error)}`);
1938
+ }
1939
+ }
1940
+ const remaining = await loadFailedCleanups(process2.cwd());
1941
+ if (remaining.length === 0) {
1942
+ console.log("\n\u2713 All failed cleanups have been resolved.");
1943
+ } else {
1944
+ console.log(`
1945
+ \u26A0 ${remaining.length} failed cleanup(s) still remaining.`);
1946
+ console.log(` Use 'intellitester cleanup:list' to see details.`);
1947
+ }
1948
+ } catch (error) {
1949
+ logError(error instanceof Error ? error.message : String(error));
1950
+ process2.exit(1);
1951
+ }
1952
+ });
1953
+ await program.parseAsync(process2.argv);
1954
+ };
1955
+ main();
1956
+ //# sourceMappingURL=index.js.map
1957
+ //# sourceMappingURL=index.js.map