@atezca/core 1.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.
package/dist/cli.mjs ADDED
@@ -0,0 +1,1539 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import * as fs4 from "fs";
5
+ import * as path3 from "path";
6
+ import chalk3 from "chalk";
7
+
8
+ // src/index.ts
9
+ var AtezcaState = class {
10
+ config = null;
11
+ commands = [];
12
+ setConfig(config) {
13
+ this.config = config;
14
+ }
15
+ getConfig() {
16
+ return this.config;
17
+ }
18
+ addCommand(command) {
19
+ this.commands.push(command);
20
+ }
21
+ getCommands() {
22
+ return [...this.commands];
23
+ }
24
+ clear() {
25
+ this.config = null;
26
+ this.commands = [];
27
+ }
28
+ };
29
+ var ATEZCA_STATE_KEY = /* @__PURE__ */ Symbol.for("__ATEZCA_STATE__");
30
+ if (!globalThis[ATEZCA_STATE_KEY]) {
31
+ globalThis[ATEZCA_STATE_KEY] = new AtezcaState();
32
+ }
33
+ var state = globalThis[ATEZCA_STATE_KEY];
34
+ function getState() {
35
+ return {
36
+ config: state.getConfig(),
37
+ commands: state.getCommands()
38
+ };
39
+ }
40
+
41
+ // src/runner/test-runner.ts
42
+ import chalk2 from "chalk";
43
+
44
+ // src/config/config-loader.ts
45
+ import * as fs from "fs";
46
+ import * as path from "path";
47
+ import { config as loadEnv } from "dotenv";
48
+ import { z } from "zod";
49
+ var ConfigSchema = z.object({
50
+ aiProvider: z.enum(["claude", "gemini"]).default("claude"),
51
+ anthropicApiKey: z.string().optional(),
52
+ googleApiKey: z.string().optional(),
53
+ cache: z.object({
54
+ enabled: z.boolean().default(true),
55
+ expiryDays: z.number().positive().default(30),
56
+ filePath: z.string().default(".atezca-cache.json")
57
+ }),
58
+ browser: z.object({
59
+ type: z.enum(["chromium", "firefox", "webkit"]).default("chromium"),
60
+ headless: z.boolean().default(true)
61
+ }),
62
+ execution: z.object({
63
+ timeout: z.number().positive().default(3e4),
64
+ retries: z.number().nonnegative().default(3),
65
+ outputDir: z.string().default("generated-tests")
66
+ })
67
+ }).refine(
68
+ (data) => {
69
+ if (data.aiProvider === "claude" && !data.anthropicApiKey) {
70
+ return false;
71
+ }
72
+ if (data.aiProvider === "gemini" && !data.googleApiKey) {
73
+ return false;
74
+ }
75
+ return true;
76
+ },
77
+ {
78
+ message: "API key required for selected AI provider"
79
+ }
80
+ );
81
+ var ConfigLoader = class {
82
+ config = null;
83
+ constructor() {
84
+ this.load();
85
+ }
86
+ /**
87
+ * Load configuration
88
+ */
89
+ load() {
90
+ loadEnv();
91
+ const configFile = this.findConfigFile();
92
+ let fileConfig = {};
93
+ if (configFile) {
94
+ try {
95
+ const content = fs.readFileSync(configFile, "utf-8");
96
+ fileConfig = JSON.parse(content);
97
+ } catch (error) {
98
+ console.warn(`Warning: Failed to parse ${configFile}`);
99
+ }
100
+ }
101
+ const rawConfig = {
102
+ aiProvider: process.env.ATEZCA_AI_PROVIDER || fileConfig.aiProvider || "claude",
103
+ anthropicApiKey: process.env.ANTHROPIC_API_KEY || fileConfig.anthropicApiKey || "",
104
+ googleApiKey: process.env.GOOGLE_API_KEY || fileConfig.googleApiKey || "",
105
+ cache: {
106
+ enabled: this.parseBoolean(process.env.ATEZCA_CACHE_ENABLED) ?? fileConfig.cacheEnabled ?? true,
107
+ expiryDays: parseInt(process.env.ATEZCA_CACHE_EXPIRY_DAYS ?? "") || fileConfig.cacheExpiryDays || 30,
108
+ filePath: process.env.ATEZCA_CACHE_FILE || fileConfig.cacheFile || ".atezca-cache.json"
109
+ },
110
+ browser: {
111
+ type: process.env.ATEZCA_BROWSER || fileConfig.browser || "chromium",
112
+ headless: this.parseBoolean(process.env.ATEZCA_HEADLESS) ?? fileConfig.headless ?? true
113
+ },
114
+ execution: {
115
+ timeout: parseInt(process.env.ATEZCA_TIMEOUT ?? "") || fileConfig.timeout || 3e4,
116
+ retries: parseInt(process.env.ATEZCA_RETRIES ?? "") || fileConfig.retries || 3,
117
+ outputDir: process.env.ATEZCA_OUTPUT_DIR || fileConfig.outputDir || "generated-tests"
118
+ }
119
+ };
120
+ try {
121
+ this.config = ConfigSchema.parse(rawConfig);
122
+ } catch (error) {
123
+ if (error instanceof z.ZodError) {
124
+ const messages = error.errors.map((e) => `${e.path.join(".")}: ${e.message}`).join("\n");
125
+ throw new Error(`Configuration validation failed:
126
+ ${messages}`);
127
+ }
128
+ throw error;
129
+ }
130
+ }
131
+ /**
132
+ * Find config file in current or parent directories
133
+ */
134
+ findConfigFile() {
135
+ const filenames = [".atezcarc", ".atezcarc.json"];
136
+ let dir = process.cwd();
137
+ while (true) {
138
+ for (const filename of filenames) {
139
+ const filepath = path.join(dir, filename);
140
+ if (fs.existsSync(filepath)) {
141
+ return filepath;
142
+ }
143
+ }
144
+ const parent = path.dirname(dir);
145
+ if (parent === dir) break;
146
+ dir = parent;
147
+ }
148
+ return null;
149
+ }
150
+ /**
151
+ * Parse boolean from string
152
+ */
153
+ parseBoolean(value) {
154
+ if (value === void 0) return void 0;
155
+ return value.toLowerCase() === "true" || value === "1";
156
+ }
157
+ /**
158
+ * Get configuration
159
+ */
160
+ getConfig() {
161
+ if (!this.config) {
162
+ throw new Error("Configuration not loaded");
163
+ }
164
+ return this.config;
165
+ }
166
+ /**
167
+ * Create default config file
168
+ */
169
+ static createDefaultConfig(filepath = ".atezcarc") {
170
+ const defaultConfig = {
171
+ aiProvider: "claude",
172
+ anthropicApiKey: process.env.ANTHROPIC_API_KEY || "",
173
+ googleApiKey: process.env.GOOGLE_API_KEY || "",
174
+ cacheEnabled: true,
175
+ cacheExpiryDays: 30,
176
+ cacheFile: ".atezca-cache.json",
177
+ browser: "chromium",
178
+ headless: true,
179
+ timeout: 3e4,
180
+ retries: 3,
181
+ outputDir: "generated-tests"
182
+ };
183
+ fs.writeFileSync(filepath, JSON.stringify(defaultConfig, null, 2), "utf-8");
184
+ }
185
+ };
186
+ var globalConfig = null;
187
+ function getConfig() {
188
+ if (!globalConfig) {
189
+ globalConfig = new ConfigLoader();
190
+ }
191
+ return globalConfig.getConfig();
192
+ }
193
+
194
+ // src/interpreter/interpreter.ts
195
+ import { z as z2 } from "zod";
196
+
197
+ // src/interpreter/claude-client.ts
198
+ import Anthropic from "@anthropic-ai/sdk";
199
+ var ClaudeClient = class {
200
+ client;
201
+ model;
202
+ constructor(apiKey, model = "claude-3-5-sonnet-20241022") {
203
+ if (!apiKey || !apiKey.startsWith("sk-ant-")) {
204
+ throw new Error('Invalid Anthropic API key. Must start with "sk-ant-"');
205
+ }
206
+ this.client = new Anthropic({
207
+ apiKey
208
+ });
209
+ this.model = model;
210
+ }
211
+ /**
212
+ * Send a message to Claude and get response
213
+ */
214
+ async sendMessage(systemPrompt, userPrompt) {
215
+ console.log(`\u{1F916} Using model: ${this.model}`);
216
+ try {
217
+ const message = await this.client.messages.create({
218
+ model: this.model,
219
+ max_tokens: 1024,
220
+ temperature: 0,
221
+ // Deterministic responses
222
+ system: systemPrompt,
223
+ messages: [
224
+ {
225
+ role: "user",
226
+ content: userPrompt
227
+ }
228
+ ]
229
+ });
230
+ const content = message.content[0];
231
+ if (content.type !== "text") {
232
+ throw new Error("Expected text response from Claude");
233
+ }
234
+ return content.text;
235
+ } catch (error) {
236
+ if (error instanceof Anthropic.APIError) {
237
+ throw new Error(`Claude API error: ${error.message}`);
238
+ }
239
+ throw error;
240
+ }
241
+ }
242
+ /**
243
+ * Test API key validity
244
+ */
245
+ async testConnection() {
246
+ try {
247
+ await this.sendMessage("You are a test assistant.", 'Respond with "OK"');
248
+ return true;
249
+ } catch {
250
+ return false;
251
+ }
252
+ }
253
+ };
254
+
255
+ // src/interpreter/gemini-client.ts
256
+ import { GoogleGenerativeAI } from "@google/generative-ai";
257
+ var GeminiClient = class {
258
+ genAI;
259
+ model;
260
+ constructor(apiKey, model = "gemini-3-pro-preview") {
261
+ if (!apiKey || !apiKey.startsWith("AIza")) {
262
+ throw new Error('Invalid Google API key. Must start with "AIza"');
263
+ }
264
+ this.genAI = new GoogleGenerativeAI(apiKey);
265
+ this.model = model;
266
+ }
267
+ /**
268
+ * Send a message to Gemini and get response
269
+ */
270
+ async sendMessage(systemPrompt, userPrompt) {
271
+ console.log(`\u{1F916} Using model: ${this.model}`);
272
+ try {
273
+ const model = this.genAI.getGenerativeModel({
274
+ model: this.model,
275
+ generationConfig: {
276
+ temperature: 0,
277
+ // Deterministic responses
278
+ maxOutputTokens: 1024
279
+ }
280
+ });
281
+ const fullPrompt = `${systemPrompt}
282
+
283
+ User Request:
284
+ ${userPrompt}`;
285
+ const result = await model.generateContent(fullPrompt);
286
+ const response = await result.response;
287
+ return response.text();
288
+ } catch (error) {
289
+ if (error instanceof Error) {
290
+ throw new Error(`Gemini API error: ${error.message}`);
291
+ }
292
+ throw error;
293
+ }
294
+ }
295
+ /**
296
+ * Test API key validity
297
+ */
298
+ async testConnection() {
299
+ try {
300
+ await this.sendMessage("You are a test assistant.", 'Respond with "OK"');
301
+ return true;
302
+ } catch {
303
+ return false;
304
+ }
305
+ }
306
+ };
307
+
308
+ // src/interpreter/prompt.ts
309
+ function getSystemPrompt() {
310
+ return `You are an expert E2E test automation assistant. Your role is to interpret natural language test commands and convert them into structured Playwright actions.
311
+
312
+ CRITICAL RULES:
313
+ 1. Always respond with valid JSON only - no markdown, no code blocks, no explanations
314
+ 2. Use robust selectors in order of preference: role > aria-label > data-testid > text content > CSS
315
+ 3. Be specific and deterministic - avoid ambiguous selectors
316
+ 4. For interact actions with buttons/links, prefer getByRole over other selectors
317
+ 5. For form inputs, use getByLabel or getByPlaceholder
318
+ 6. Always include timeout values for wait actions
319
+
320
+ OUTPUT FORMAT (JSON only):
321
+ {
322
+ "actions": [
323
+ {
324
+ "action": "click" | "type" | "navigate" | "wait" | "expect" | "select" | "hover" | "press",
325
+ "selector": "button:has-text('Login')" or "role=button[name='Login']",
326
+ "value": "text to type or URL to navigate",
327
+ "condition": "visible|hidden|attached|detached",
328
+ "assertionType": "visible|hidden|text|value|enabled|disabled",
329
+ "expected": "expected value for assertions",
330
+ "timeout": 30000
331
+ }
332
+ ]
333
+ }
334
+
335
+ EXAMPLES:
336
+
337
+ Input: { actionType: "interact", description: "click button 'Login'" }
338
+ Output: {"actions":[{"action":"click","selector":"role=button[name='Login']"}]}
339
+
340
+ Input: { actionType: "interact", description: "type 'john@example.com' in email field" }
341
+ Output: {"actions":[{"action":"type","selector":"role=textbox[name='Email' i]","value":"john@example.com"}]}
342
+
343
+ Input: { actionType: "navigate", description: "go to login page" }
344
+ Output: {"actions":[{"action":"navigate","value":"/login"}]}
345
+
346
+ Input: { actionType: "wait", description: "until submit button is visible" }
347
+ Output: {"actions":[{"action":"wait","selector":"role=button[name='Submit']","condition":"visible","timeout":30000}]}
348
+
349
+ Input: { actionType: "expect", description: "show success message" }
350
+ Output: {"actions":[{"action":"expect","selector":"text=/success|successfully/i","assertionType":"visible"}]}
351
+
352
+ Input: { actionType: "interact", description: "select 'Premium' from plan dropdown" }
353
+ Output: {"actions":[{"action":"select","selector":"role=combobox[name='Plan']","value":"Premium"}]}
354
+
355
+ IMPORTANT:
356
+ - For "expect" actions, use appropriate assertionType (visible, text, value, etc.)
357
+ - For "wait" actions, always include timeout
358
+ - For text matching, use case-insensitive regex when appropriate: /text/i
359
+ - Respond ONLY with JSON, no other text`;
360
+ }
361
+ function getUserPrompt(command) {
362
+ return JSON.stringify({
363
+ actionType: command.actionType,
364
+ description: command.description
365
+ });
366
+ }
367
+ function getUserPromptWithContext(command, context) {
368
+ const elementsInfo = context.interactiveElements.length > 0 ? `
369
+
370
+ Available interactive elements on the page:
371
+ ${context.interactiveElements.map((el) => `- ${el.role}: "${el.name}" (${el.tag})`).join("\n")}` : "";
372
+ const accessibilityInfo = context.accessibilityTree && context.accessibilityTree !== "Unable to capture" ? `
373
+
374
+ Page structure:
375
+ ${context.accessibilityTree}` : "";
376
+ return `CURRENT PAGE CONTEXT:
377
+ - URL: ${context.url}
378
+ - Title: ${context.title}${elementsInfo}${accessibilityInfo}
379
+
380
+ USER COMMAND:
381
+ ${JSON.stringify({
382
+ actionType: command.actionType,
383
+ description: command.description
384
+ })}
385
+
386
+ Based on the current page context above, generate the appropriate Playwright actions to fulfill the user's command. Use the available elements to create accurate selectors.`;
387
+ }
388
+
389
+ // src/interpreter/interpreter.ts
390
+ var PlaywrightActionSchema = z2.object({
391
+ action: z2.enum(["click", "type", "navigate", "wait", "expect", "select", "hover", "press"]),
392
+ selector: z2.string().optional(),
393
+ value: z2.string().optional(),
394
+ condition: z2.string().optional(),
395
+ assertionType: z2.enum(["visible", "hidden", "text", "value", "enabled", "disabled"]).optional(),
396
+ expected: z2.string().optional(),
397
+ timeout: z2.number().optional()
398
+ });
399
+ var ClaudeResponseSchema = z2.object({
400
+ actions: z2.array(PlaywrightActionSchema)
401
+ });
402
+ var Interpreter = class {
403
+ client;
404
+ provider;
405
+ constructor(provider, apiKey) {
406
+ this.provider = provider;
407
+ switch (provider) {
408
+ case "claude":
409
+ this.client = new ClaudeClient(apiKey);
410
+ break;
411
+ case "gemini":
412
+ this.client = new GeminiClient(apiKey);
413
+ break;
414
+ default:
415
+ AI;
416
+ throw new Error(`Unsupported AI provider: ${provider}`);
417
+ }
418
+ }
419
+ /**
420
+ * Interpret a test command into Playwright actions
421
+ */
422
+ async interpret(command, pageContext) {
423
+ const waitMatch = command.description.match(/^wait\s+(\d+)ms$/i);
424
+ if (waitMatch && command.actionType === "wait") {
425
+ const milliseconds = parseInt(waitMatch[1], 10);
426
+ return [{
427
+ action: "wait",
428
+ timeout: milliseconds,
429
+ selector: "body",
430
+ // Dummy selector, will just wait
431
+ condition: "attached"
432
+ }];
433
+ }
434
+ const systemPrompt = getSystemPrompt();
435
+ const userPrompt = pageContext ? getUserPromptWithContext(command, pageContext) : getUserPrompt(command);
436
+ const responseText = await this.client.sendMessage(systemPrompt, userPrompt);
437
+ let response;
438
+ try {
439
+ const cleanedResponse = responseText.replace(/```json\n?/g, "").replace(/```\n?/g, "").trim();
440
+ response = JSON.parse(cleanedResponse);
441
+ } catch (error) {
442
+ throw new Error(`Failed to parse ${this.provider} response: ${responseText}`);
443
+ }
444
+ const validated = ClaudeResponseSchema.parse(response);
445
+ return validated.actions;
446
+ }
447
+ /**
448
+ * Test if the interpreter is working
449
+ */
450
+ async test() {
451
+ return this.client.testConnection();
452
+ }
453
+ };
454
+ function createInterpretationResult(command, actions, cached = false) {
455
+ return {
456
+ command,
457
+ actions,
458
+ cached,
459
+ timestamp: Date.now()
460
+ };
461
+ }
462
+
463
+ // src/cache/cache-manager.ts
464
+ import * as fs2 from "fs";
465
+ import * as crypto from "crypto";
466
+ var CacheManager = class {
467
+ cacheFilePath;
468
+ expiryDays;
469
+ cache;
470
+ constructor(cacheFilePath = ".atezca-cache.json", expiryDays = 30) {
471
+ this.cacheFilePath = cacheFilePath;
472
+ this.expiryDays = expiryDays;
473
+ this.cache = /* @__PURE__ */ new Map();
474
+ this.load();
475
+ }
476
+ /**
477
+ * Generate cache key from command
478
+ */
479
+ generateKey(command) {
480
+ const data = `${command.actionType}:${command.description}`;
481
+ return crypto.createHash("sha256").update(data).digest("hex");
482
+ }
483
+ /**
484
+ * Check if cache entry is expired
485
+ */
486
+ isExpired(entry) {
487
+ return Date.now() > entry.expiresAt;
488
+ }
489
+ /**
490
+ * Load cache from file
491
+ */
492
+ load() {
493
+ try {
494
+ if (fs2.existsSync(this.cacheFilePath)) {
495
+ const data = fs2.readFileSync(this.cacheFilePath, "utf-8");
496
+ const entries = JSON.parse(data);
497
+ for (const entry of entries) {
498
+ if (!this.isExpired(entry)) {
499
+ this.cache.set(entry.key, entry);
500
+ }
501
+ }
502
+ }
503
+ } catch (error) {
504
+ console.warn("Failed to load cache, starting with empty cache");
505
+ this.cache.clear();
506
+ }
507
+ }
508
+ /**
509
+ * Save cache to file
510
+ */
511
+ save() {
512
+ try {
513
+ const entries = Array.from(this.cache.values());
514
+ const data = JSON.stringify(entries, null, 2);
515
+ fs2.writeFileSync(this.cacheFilePath, data, "utf-8");
516
+ } catch (error) {
517
+ console.warn("Failed to save cache:", error);
518
+ }
519
+ }
520
+ /**
521
+ * Get cached actions for a command
522
+ */
523
+ get(command) {
524
+ const key = this.generateKey(command);
525
+ const entry = this.cache.get(key);
526
+ if (!entry) {
527
+ return null;
528
+ }
529
+ if (this.isExpired(entry)) {
530
+ this.cache.delete(key);
531
+ this.save();
532
+ return null;
533
+ }
534
+ return entry.actions;
535
+ }
536
+ /**
537
+ * Store actions in cache
538
+ */
539
+ set(command, actions) {
540
+ const key = this.generateKey(command);
541
+ const now = Date.now();
542
+ const expiryMs = this.expiryDays * 24 * 60 * 60 * 1e3;
543
+ const entry = {
544
+ key,
545
+ actions,
546
+ cachedAt: now,
547
+ expiresAt: now + expiryMs
548
+ };
549
+ this.cache.set(key, entry);
550
+ this.save();
551
+ }
552
+ /**
553
+ * Check if command is cached
554
+ */
555
+ has(command) {
556
+ return this.get(command) !== null;
557
+ }
558
+ /**
559
+ * Clear all cache
560
+ */
561
+ clear() {
562
+ this.cache.clear();
563
+ if (fs2.existsSync(this.cacheFilePath)) {
564
+ fs2.unlinkSync(this.cacheFilePath);
565
+ }
566
+ }
567
+ /**
568
+ * Clear expired entries
569
+ */
570
+ clearExpired() {
571
+ const keysToDelete = [];
572
+ for (const [key, entry] of this.cache.entries()) {
573
+ if (this.isExpired(entry)) {
574
+ keysToDelete.push(key);
575
+ }
576
+ }
577
+ for (const key of keysToDelete) {
578
+ this.cache.delete(key);
579
+ }
580
+ if (keysToDelete.length > 0) {
581
+ this.save();
582
+ }
583
+ }
584
+ /**
585
+ * Get cache statistics
586
+ */
587
+ getStats() {
588
+ const entries = Array.from(this.cache.values());
589
+ if (entries.length === 0) {
590
+ return { size: 0, oldestEntry: null, newestEntry: null };
591
+ }
592
+ const timestamps = entries.map((e) => e.cachedAt);
593
+ return {
594
+ size: entries.length,
595
+ oldestEntry: Math.min(...timestamps),
596
+ newestEntry: Math.max(...timestamps)
597
+ };
598
+ }
599
+ };
600
+
601
+ // src/executor/playwright-executor.ts
602
+ import { chromium, firefox, webkit } from "playwright";
603
+ var PlaywrightExecutor = class {
604
+ browser = null;
605
+ context = null;
606
+ page = null;
607
+ config;
608
+ actionLog = [];
609
+ constructor(config) {
610
+ this.config = config;
611
+ }
612
+ /**
613
+ * Initialize browser and page
614
+ */
615
+ async initialize() {
616
+ const browserType = this.config.browser ?? "chromium";
617
+ const headless = this.config.headless ?? true;
618
+ switch (browserType) {
619
+ case "chromium":
620
+ this.browser = await chromium.launch({ headless });
621
+ break;
622
+ case "firefox":
623
+ this.browser = await firefox.launch({ headless });
624
+ break;
625
+ case "webkit":
626
+ this.browser = await webkit.launch({ headless });
627
+ break;
628
+ default:
629
+ throw new Error(`Unsupported browser: ${browserType}`);
630
+ }
631
+ this.context = await this.browser.newContext({
632
+ ignoreHTTPSErrors: this.config.disableSSL ?? false
633
+ });
634
+ this.page = await this.context.newPage();
635
+ this.page.setDefaultTimeout(this.config.timeout ?? 3e4);
636
+ await this.page.goto(this.config.url);
637
+ }
638
+ /**
639
+ * Execute a single action with retry logic
640
+ */
641
+ async executeAction(action) {
642
+ if (!this.page) {
643
+ throw new Error("Executor not initialized. Call initialize() first.");
644
+ }
645
+ const startTime = Date.now();
646
+ const retries = this.config.retries ?? 3;
647
+ let lastError = null;
648
+ for (let attempt = 0; attempt < retries; attempt++) {
649
+ try {
650
+ await this.performAction(action);
651
+ const duration2 = Date.now() - startTime;
652
+ this.actionLog.push({ action, result: "success", duration: duration2 });
653
+ return;
654
+ } catch (error) {
655
+ lastError = error;
656
+ if (attempt < retries - 1) {
657
+ await this.page.waitForTimeout(Math.pow(2, attempt) * 1e3);
658
+ }
659
+ }
660
+ }
661
+ const duration = Date.now() - startTime;
662
+ this.actionLog.push({
663
+ action,
664
+ result: `failed: ${lastError?.message}`,
665
+ duration
666
+ });
667
+ throw lastError;
668
+ }
669
+ /**
670
+ * Perform the actual action
671
+ */
672
+ async performAction(action) {
673
+ if (!this.page) {
674
+ throw new Error("Page not initialized");
675
+ }
676
+ const timeout = action.timeout ?? this.config.timeout ?? 3e4;
677
+ switch (action.action) {
678
+ case "click":
679
+ if (!action.selector) throw new Error("Click action requires selector");
680
+ await this.page.locator(action.selector).click({ timeout });
681
+ break;
682
+ case "type":
683
+ if (!action.selector) throw new Error("Type action requires selector");
684
+ if (!action.value) throw new Error("Type action requires value");
685
+ await this.page.locator(action.selector).fill(action.value, { timeout });
686
+ break;
687
+ case "navigate":
688
+ if (!action.value) throw new Error("Navigate action requires value");
689
+ const url = action.value.startsWith("http") ? action.value : new URL(action.value, this.config.url).toString();
690
+ await this.page.goto(url, { timeout });
691
+ break;
692
+ case "wait":
693
+ if (action.condition === "attached" && action.selector === "body") {
694
+ await this.page.waitForTimeout(timeout);
695
+ } else {
696
+ if (!action.selector) throw new Error("Wait action requires selector");
697
+ const state2 = action.condition ?? "visible";
698
+ await this.page.locator(action.selector).waitFor({
699
+ state: state2,
700
+ timeout
701
+ });
702
+ }
703
+ break;
704
+ case "expect":
705
+ if (!action.selector) throw new Error("Expect action requires selector");
706
+ await this.performAssertion(action, timeout);
707
+ break;
708
+ case "select":
709
+ if (!action.selector) throw new Error("Select action requires selector");
710
+ if (!action.value) throw new Error("Select action requires value");
711
+ await this.page.locator(action.selector).selectOption(action.value, { timeout });
712
+ break;
713
+ case "hover":
714
+ if (!action.selector) throw new Error("Hover action requires selector");
715
+ await this.page.locator(action.selector).hover({ timeout });
716
+ break;
717
+ case "press":
718
+ if (!action.value) throw new Error("Press action requires value (key name)");
719
+ await this.page.keyboard.press(action.value);
720
+ break;
721
+ default:
722
+ throw new Error(`Unsupported action: ${action.action}`);
723
+ }
724
+ }
725
+ /**
726
+ * Perform assertion
727
+ */
728
+ async performAssertion(action, timeout) {
729
+ if (!this.page || !action.selector) return;
730
+ const locator = this.page.locator(action.selector);
731
+ switch (action.assertionType) {
732
+ case "visible":
733
+ await locator.waitFor({ state: "visible", timeout });
734
+ break;
735
+ case "hidden":
736
+ await locator.waitFor({ state: "hidden", timeout });
737
+ break;
738
+ case "text":
739
+ if (!action.expected) throw new Error("Text assertion requires expected value");
740
+ await locator.waitFor({ state: "visible", timeout });
741
+ const text = await locator.textContent();
742
+ if (!text?.includes(action.expected)) {
743
+ throw new Error(`Expected text "${action.expected}" but got "${text}"`);
744
+ }
745
+ break;
746
+ case "value":
747
+ if (!action.expected) throw new Error("Value assertion requires expected value");
748
+ await locator.waitFor({ state: "visible", timeout });
749
+ const value = await locator.inputValue();
750
+ if (value !== action.expected) {
751
+ throw new Error(`Expected value "${action.expected}" but got "${value}"`);
752
+ }
753
+ break;
754
+ case "enabled":
755
+ await locator.waitFor({ state: "visible", timeout });
756
+ if (await locator.isDisabled()) {
757
+ throw new Error("Expected element to be enabled but it is disabled");
758
+ }
759
+ break;
760
+ case "disabled":
761
+ await locator.waitFor({ state: "visible", timeout });
762
+ if (await locator.isEnabled()) {
763
+ throw new Error("Expected element to be disabled but it is enabled");
764
+ }
765
+ break;
766
+ default:
767
+ await locator.waitFor({ state: "visible", timeout });
768
+ }
769
+ }
770
+ /**
771
+ * Execute multiple actions in sequence
772
+ */
773
+ async executeActions(actions) {
774
+ for (const action of actions) {
775
+ await this.executeAction(action);
776
+ }
777
+ }
778
+ /**
779
+ * Get action execution log
780
+ */
781
+ getActionLog() {
782
+ return [...this.actionLog];
783
+ }
784
+ /**
785
+ * Take screenshot
786
+ */
787
+ async screenshot(path4) {
788
+ if (!this.page) {
789
+ throw new Error("Page not initialized");
790
+ }
791
+ await this.page.screenshot({ path: path4 });
792
+ }
793
+ /**
794
+ * Close browser
795
+ */
796
+ async close() {
797
+ if (this.context) {
798
+ await this.context.close();
799
+ }
800
+ if (this.browser) {
801
+ await this.browser.close();
802
+ }
803
+ this.browser = null;
804
+ this.context = null;
805
+ this.page = null;
806
+ }
807
+ /**
808
+ * Get current page (for advanced usage)
809
+ */
810
+ getPage() {
811
+ return this.page;
812
+ }
813
+ /**
814
+ * Capture current page context for AI interpretation
815
+ */
816
+ async getPageContext() {
817
+ if (!this.page) {
818
+ throw new Error("Page not initialized");
819
+ }
820
+ try {
821
+ const url = this.page.url();
822
+ const title = await this.page.title();
823
+ const snapshot = await this.page.accessibility.snapshot();
824
+ const accessibilityTree = this.simplifyAccessibilityTree(snapshot);
825
+ const interactiveElements = await this.page.evaluate(() => {
826
+ const elements = [];
827
+ document.querySelectorAll('button, [role="button"], input[type="button"], input[type="submit"]').forEach((el) => {
828
+ const text = el.textContent?.trim() || el.value || "";
829
+ if (text) {
830
+ elements.push({
831
+ role: "button",
832
+ name: text,
833
+ tag: el.tagName.toLowerCase()
834
+ });
835
+ }
836
+ });
837
+ document.querySelectorAll("a[href]").forEach((el) => {
838
+ const text = el.textContent?.trim() || "";
839
+ if (text) {
840
+ elements.push({
841
+ role: "link",
842
+ name: text,
843
+ tag: "a"
844
+ });
845
+ }
846
+ });
847
+ document.querySelectorAll('input:not([type="hidden"]), textarea, select').forEach((el) => {
848
+ const input = el;
849
+ const label = document.querySelector(`label[for="${input.id}"]`)?.textContent?.trim() || input.placeholder || input.name || input.id;
850
+ if (label) {
851
+ elements.push({
852
+ role: "input",
853
+ name: label,
854
+ tag: input.tagName.toLowerCase()
855
+ });
856
+ }
857
+ });
858
+ return elements.slice(0, 50);
859
+ });
860
+ return {
861
+ url,
862
+ title,
863
+ accessibilityTree,
864
+ interactiveElements
865
+ };
866
+ } catch (error) {
867
+ return {
868
+ url: this.page.url(),
869
+ title: await this.page.title().catch(() => "Unknown"),
870
+ accessibilityTree: "Unable to capture",
871
+ interactiveElements: []
872
+ };
873
+ }
874
+ }
875
+ /**
876
+ * Simplify accessibility tree for AI consumption
877
+ */
878
+ simplifyAccessibilityTree(node, depth = 0, maxDepth = 3) {
879
+ if (!node || depth > maxDepth) return "";
880
+ let result = "";
881
+ const indent = " ".repeat(depth);
882
+ if (node.role) {
883
+ const name = node.name ? ` "${node.name}"` : "";
884
+ result += `${indent}- ${node.role}${name}
885
+ `;
886
+ }
887
+ if (node.children && depth < maxDepth) {
888
+ for (const child of node.children.slice(0, 10)) {
889
+ result += this.simplifyAccessibilityTree(child, depth + 1, maxDepth);
890
+ }
891
+ }
892
+ return result;
893
+ }
894
+ };
895
+
896
+ // src/generator/code-generator.ts
897
+ import * as fs3 from "fs";
898
+ import * as path2 from "path";
899
+ var CodeGenerator = class {
900
+ config;
901
+ constructor(config) {
902
+ this.config = config;
903
+ }
904
+ /**
905
+ * Generate Playwright test code from actions
906
+ */
907
+ generateCode(interpretations) {
908
+ const lines = [];
909
+ lines.push("import { test, expect } from '@playwright/test';");
910
+ lines.push("");
911
+ if (this.config.disableSSL) {
912
+ lines.push("// Note: SSL certificate validation is disabled (ignoreHTTPSErrors: true)");
913
+ lines.push("// This is configured in playwright.config.ts");
914
+ lines.push("");
915
+ }
916
+ lines.push("test.describe('Generated E2E Test', () => {");
917
+ lines.push(" test.beforeEach(async ({ page }) => {");
918
+ lines.push(` // Navigate to base URL`);
919
+ lines.push(` await page.goto('${this.config.url}');`);
920
+ lines.push(" });");
921
+ lines.push("");
922
+ lines.push(" test('should execute test actions', async ({ page }) => {");
923
+ for (const interpretation of interpretations) {
924
+ const { command, actions } = interpretation;
925
+ lines.push("");
926
+ lines.push(` // ${command.actionType}: ${command.description}`);
927
+ for (const action of actions) {
928
+ const code = this.generateActionCode(action);
929
+ lines.push(` ${code}`);
930
+ }
931
+ }
932
+ lines.push(" });");
933
+ lines.push("});");
934
+ lines.push("");
935
+ return lines.join("\n");
936
+ }
937
+ /**
938
+ * Generate code for a single action
939
+ */
940
+ generateActionCode(action) {
941
+ const timeout = action.timeout ?? this.config.timeout ?? 3e4;
942
+ switch (action.action) {
943
+ case "click":
944
+ return `await page.locator('${this.escapeString(action.selector)}').click({ timeout: ${timeout} });`;
945
+ case "type":
946
+ return `await page.locator('${this.escapeString(action.selector)}').fill('${this.escapeString(action.value)}', { timeout: ${timeout} });`;
947
+ case "navigate": {
948
+ const url = action.value.startsWith("http") ? action.value : `\${baseUrl}${action.value}`;
949
+ return `await page.goto('${this.escapeString(url)}', { timeout: ${timeout} });`;
950
+ }
951
+ case "wait": {
952
+ if (action.condition === "attached" && action.selector === "body") {
953
+ return `await page.waitForTimeout(${timeout});`;
954
+ }
955
+ const state2 = action.condition ?? "visible";
956
+ return `await page.locator('${this.escapeString(action.selector)}').waitFor({ state: '${state2}', timeout: ${timeout} });`;
957
+ }
958
+ case "expect":
959
+ return this.generateAssertionCode(action, timeout);
960
+ case "select":
961
+ return `await page.locator('${this.escapeString(action.selector)}').selectOption('${this.escapeString(action.value)}', { timeout: ${timeout} });`;
962
+ case "hover":
963
+ return `await page.locator('${this.escapeString(action.selector)}').hover({ timeout: ${timeout} });`;
964
+ case "press":
965
+ return `await page.keyboard.press('${this.escapeString(action.value)}');`;
966
+ default:
967
+ return `// Unsupported action: ${action.action}`;
968
+ }
969
+ }
970
+ /**
971
+ * Generate assertion code
972
+ */
973
+ generateAssertionCode(action, timeout) {
974
+ const locator = `page.locator('${this.escapeString(action.selector)}')`;
975
+ switch (action.assertionType) {
976
+ case "visible":
977
+ return `await expect(${locator}).toBeVisible({ timeout: ${timeout} });`;
978
+ case "hidden":
979
+ return `await expect(${locator}).toBeHidden({ timeout: ${timeout} });`;
980
+ case "text":
981
+ return `await expect(${locator}).toContainText('${this.escapeString(action.expected)}', { timeout: ${timeout} });`;
982
+ case "value":
983
+ return `await expect(${locator}).toHaveValue('${this.escapeString(action.expected)}', { timeout: ${timeout} });`;
984
+ case "enabled":
985
+ return `await expect(${locator}).toBeEnabled({ timeout: ${timeout} });`;
986
+ case "disabled":
987
+ return `await expect(${locator}).toBeDisabled({ timeout: ${timeout} });`;
988
+ default:
989
+ return `await expect(${locator}).toBeVisible({ timeout: ${timeout} });`;
990
+ }
991
+ }
992
+ /**
993
+ * Escape string for code generation
994
+ */
995
+ escapeString(str) {
996
+ return str.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r");
997
+ }
998
+ /**
999
+ * Generate and save test file
1000
+ */
1001
+ async saveTestFile(interpretations, filename) {
1002
+ const code = this.generateCode(interpretations);
1003
+ const outputDir = this.config.outputDir ?? "generated-tests";
1004
+ if (!fs3.existsSync(outputDir)) {
1005
+ fs3.mkdirSync(outputDir, { recursive: true });
1006
+ }
1007
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
1008
+ const finalFilename = filename ?? `test-${timestamp}.spec.ts`;
1009
+ const filePath = path2.join(outputDir, finalFilename);
1010
+ fs3.writeFileSync(filePath, code, "utf-8");
1011
+ return filePath;
1012
+ }
1013
+ /**
1014
+ * Generate Playwright config file
1015
+ */
1016
+ generatePlaywrightConfig() {
1017
+ const useConfig = [
1018
+ ` baseURL: '${this.config.url}',`,
1019
+ ` trace: 'on-first-retry',`,
1020
+ ` screenshot: 'only-on-failure',`
1021
+ ];
1022
+ if (this.config.disableSSL) {
1023
+ useConfig.push(` ignoreHTTPSErrors: true, // SSL certificate errors disabled`);
1024
+ }
1025
+ return `import { defineConfig, devices } from '@playwright/test';
1026
+
1027
+ export default defineConfig({
1028
+ testDir: './generated-tests',
1029
+ fullyParallel: true,
1030
+ forbidOnly: !!process.env.CI,
1031
+ retries: process.env.CI ? 2 : 0,
1032
+ workers: process.env.CI ? 1 : undefined,
1033
+ reporter: 'html',
1034
+ use: {
1035
+ ${useConfig.join("\n")}
1036
+ },
1037
+ projects: [
1038
+ {
1039
+ name: 'chromium',
1040
+ use: { ...devices['Desktop Chrome'] },
1041
+ },
1042
+ ],
1043
+ });
1044
+ `;
1045
+ }
1046
+ /**
1047
+ * Save Playwright config file
1048
+ */
1049
+ async savePlaywrightConfig() {
1050
+ const config = this.generatePlaywrightConfig();
1051
+ const filePath = "playwright.config.ts";
1052
+ if (!fs3.existsSync(filePath)) {
1053
+ fs3.writeFileSync(filePath, config, "utf-8");
1054
+ }
1055
+ return filePath;
1056
+ }
1057
+ };
1058
+
1059
+ // src/utils/browser-installer.ts
1060
+ import { execSync } from "child_process";
1061
+ import chalk from "chalk";
1062
+ function isMissingBrowserError(error) {
1063
+ const message = error.message;
1064
+ return message.includes("Executable doesn't exist at") || message.includes("Looks like Playwright") || message.includes("npx playwright install");
1065
+ }
1066
+ function extractBrowserFromError(error) {
1067
+ const message = error.message;
1068
+ if (message.includes("chromium")) return "chromium";
1069
+ if (message.includes("firefox")) return "firefox";
1070
+ if (message.includes("webkit")) return "webkit";
1071
+ return null;
1072
+ }
1073
+ async function installPlaywrightBrowser(browser) {
1074
+ try {
1075
+ console.log(chalk.yellow(`
1076
+ \u26A0\uFE0F Browser ${browser} not found. Installing automatically...
1077
+ `));
1078
+ execSync(`npx playwright install ${browser}`, {
1079
+ stdio: "inherit",
1080
+ cwd: process.cwd()
1081
+ });
1082
+ console.log(chalk.green(`
1083
+ \u2713 Browser ${browser} installed successfully!
1084
+ `));
1085
+ return true;
1086
+ } catch (error) {
1087
+ console.error(chalk.red(`
1088
+ \u2717 Failed to install browser: ${error.message}
1089
+ `));
1090
+ return false;
1091
+ }
1092
+ }
1093
+ async function handleBrowserInstallation(error, browserType) {
1094
+ if (!isMissingBrowserError(error)) {
1095
+ return false;
1096
+ }
1097
+ const browser = extractBrowserFromError(error) || browserType;
1098
+ return await installPlaywrightBrowser(browser);
1099
+ }
1100
+
1101
+ // src/runner/test-runner.ts
1102
+ var TestRunner = class {
1103
+ config;
1104
+ interpreter;
1105
+ cache;
1106
+ executor = null;
1107
+ generator;
1108
+ interpretations = [];
1109
+ constructor(config) {
1110
+ this.config = config;
1111
+ const globalConfig2 = getConfig();
1112
+ const aiProvider = config.aiProvider || globalConfig2.aiProvider;
1113
+ let apiKey = config.apiKey;
1114
+ if (!apiKey) {
1115
+ apiKey = aiProvider === "claude" ? globalConfig2.anthropicApiKey : globalConfig2.googleApiKey;
1116
+ }
1117
+ if (!apiKey) {
1118
+ throw new Error(`API key not found for provider: ${aiProvider}`);
1119
+ }
1120
+ this.interpreter = new Interpreter(aiProvider, apiKey);
1121
+ this.cache = new CacheManager(
1122
+ globalConfig2.cache.filePath,
1123
+ globalConfig2.cache.expiryDays
1124
+ );
1125
+ this.generator = new CodeGenerator(config);
1126
+ }
1127
+ /**
1128
+ * Process a single command (interpret and cache) with retry logic
1129
+ */
1130
+ async processCommand(command) {
1131
+ const cacheEnabled = this.config.cacheEnabled ?? true;
1132
+ const aiProvider = this.config.aiProvider || getConfig().aiProvider;
1133
+ if (cacheEnabled) {
1134
+ const cached = this.cache.get(command);
1135
+ if (cached) {
1136
+ console.log(chalk2.gray(` \u2713 Using cached interpretation`));
1137
+ return createInterpretationResult(command, cached, true);
1138
+ }
1139
+ }
1140
+ const displayProvider = aiProvider.toUpperCase();
1141
+ const maxInterpretationRetries = 3;
1142
+ let lastError = null;
1143
+ for (let attempt = 0; attempt < maxInterpretationRetries; attempt++) {
1144
+ try {
1145
+ if (attempt === 0) {
1146
+ console.log(chalk2.blue(` \u2192 Interpreting with ${displayProvider}...`));
1147
+ } else {
1148
+ console.log(chalk2.yellow(` \u27F3 Interpretation retry ${attempt}/${maxInterpretationRetries - 1}...`));
1149
+ }
1150
+ const actions = await this.interpreter.interpret(command);
1151
+ if (cacheEnabled) {
1152
+ this.cache.set(command, actions);
1153
+ }
1154
+ return createInterpretationResult(command, actions, false);
1155
+ } catch (error) {
1156
+ lastError = error;
1157
+ console.log(chalk2.yellow(` \u26A0 Interpretation failed: ${lastError.message}`));
1158
+ if (attempt < maxInterpretationRetries - 1) {
1159
+ await new Promise((resolve2) => setTimeout(resolve2, 1e3 * (attempt + 1)));
1160
+ }
1161
+ }
1162
+ }
1163
+ throw new Error(`Failed to interpret after ${maxInterpretationRetries} attempts: ${lastError?.message}`);
1164
+ }
1165
+ /**
1166
+ * Interpret command with retry logic and page context
1167
+ */
1168
+ async interpretWithRetry(command, pageContext) {
1169
+ const aiProvider = this.config.aiProvider || getConfig().aiProvider;
1170
+ const displayProvider = aiProvider.toUpperCase();
1171
+ const maxInterpretationRetries = 3;
1172
+ let lastError = null;
1173
+ for (let attempt = 0; attempt < maxInterpretationRetries; attempt++) {
1174
+ try {
1175
+ if (attempt > 0) {
1176
+ console.log(chalk2.yellow(` \u27F3 AI retry ${attempt}/${maxInterpretationRetries - 1} (${displayProvider})...`));
1177
+ }
1178
+ const actions = await this.interpreter.interpret(command, pageContext);
1179
+ return actions;
1180
+ } catch (error) {
1181
+ lastError = error;
1182
+ console.log(chalk2.yellow(` \u26A0 ${displayProvider} failed: ${lastError.message}`));
1183
+ if (attempt < maxInterpretationRetries - 1) {
1184
+ await new Promise((resolve2) => setTimeout(resolve2, 1e3 * (attempt + 1)));
1185
+ }
1186
+ }
1187
+ }
1188
+ throw new Error(`${displayProvider} failed after ${maxInterpretationRetries} attempts: ${lastError?.message}`);
1189
+ }
1190
+ /**
1191
+ * Process all commands
1192
+ */
1193
+ async processCommands(commands) {
1194
+ console.log(chalk2.bold("\n\u{1F4DD} Processing commands...\n"));
1195
+ const interpretations = [];
1196
+ for (let i = 0; i < commands.length; i++) {
1197
+ const command = commands[i];
1198
+ console.log(chalk2.cyan(`[${i + 1}/${commands.length}] ${command.actionType}: ${command.description}`));
1199
+ try {
1200
+ const interpretation = await this.processCommand(command);
1201
+ interpretations.push(interpretation);
1202
+ console.log(chalk2.green(` \u2713 ${interpretation.actions.length} action(s) generated
1203
+ `));
1204
+ } catch (error) {
1205
+ console.error(chalk2.red(` \u2717 Failed: ${error.message}
1206
+ `));
1207
+ throw error;
1208
+ }
1209
+ }
1210
+ this.interpretations = interpretations;
1211
+ return interpretations;
1212
+ }
1213
+ /**
1214
+ * Execute tests
1215
+ */
1216
+ async execute(commands) {
1217
+ const startTime = Date.now();
1218
+ let actionsExecuted = 0;
1219
+ let browserInstalled = false;
1220
+ try {
1221
+ console.log(chalk2.bold("\u{1F680} Launching browser...\n"));
1222
+ this.executor = new PlaywrightExecutor(this.config);
1223
+ try {
1224
+ await this.executor.initialize();
1225
+ } catch (error) {
1226
+ const installed = await handleBrowserInstallation(
1227
+ error,
1228
+ this.config.browser || "chromium"
1229
+ );
1230
+ if (installed) {
1231
+ browserInstalled = true;
1232
+ console.log(chalk2.blue("Retrying browser launch...\n"));
1233
+ await this.executor.initialize();
1234
+ } else {
1235
+ throw error;
1236
+ }
1237
+ }
1238
+ console.log(chalk2.bold("\u25B6\uFE0F Executing tests...\n"));
1239
+ for (let i = 0; i < commands.length; i++) {
1240
+ const command = commands[i];
1241
+ console.log(chalk2.cyan(`
1242
+ [${i + 1}/${commands.length}] ${command.actionType}: ${command.description}`));
1243
+ let success = false;
1244
+ let lastError = null;
1245
+ const maxRetries = 5;
1246
+ for (let attempt = 0; attempt < maxRetries && !success; attempt++) {
1247
+ try {
1248
+ const pageContext = await this.executor.getPageContext();
1249
+ if (attempt === 0) {
1250
+ console.log(chalk2.blue(` \u2192 Interpreting with context...`));
1251
+ console.log(chalk2.gray(` Page: ${pageContext.url}`));
1252
+ console.log(chalk2.gray(` Available elements: ${pageContext.interactiveElements.length}`));
1253
+ } else {
1254
+ console.log(chalk2.yellow(` \u27F3 Retry ${attempt}/${maxRetries - 1} - Re-analyzing page...`));
1255
+ console.log(chalk2.gray(` Page: ${pageContext.url}`));
1256
+ console.log(chalk2.gray(` Available elements: ${pageContext.interactiveElements.length}`));
1257
+ }
1258
+ let actions;
1259
+ if (attempt === 0 && this.config.cacheEnabled) {
1260
+ const cached = this.cache.get(command);
1261
+ if (cached) {
1262
+ console.log(chalk2.gray(` \u2713 Using cached interpretation`));
1263
+ actions = cached;
1264
+ } else {
1265
+ actions = await this.interpretWithRetry(command, pageContext);
1266
+ this.cache.set(command, actions);
1267
+ }
1268
+ } else {
1269
+ actions = await this.interpretWithRetry(command, pageContext);
1270
+ if (this.config.cacheEnabled) {
1271
+ this.cache.set(command, actions);
1272
+ }
1273
+ }
1274
+ console.log(chalk2.gray(` \u2713 Generated ${actions.length} action(s)`));
1275
+ for (const action of actions) {
1276
+ await this.executor.executeAction(action);
1277
+ actionsExecuted++;
1278
+ console.log(chalk2.gray(` \u2713 ${action.action}${action.selector ? ` (${action.selector})` : ""}`));
1279
+ }
1280
+ const interpretation = createInterpretationResult(command, actions, false);
1281
+ this.interpretations.push(interpretation);
1282
+ success = true;
1283
+ console.log(chalk2.green(` \u2713 Success
1284
+ `));
1285
+ } catch (error) {
1286
+ lastError = error;
1287
+ if (attempt < maxRetries - 1) {
1288
+ console.log(chalk2.yellow(` \u26A0 Failed: ${lastError.message}`));
1289
+ await new Promise((resolve2) => setTimeout(resolve2, 1e3 * (attempt + 1)));
1290
+ } else {
1291
+ throw new Error(`Failed after ${maxRetries} attempts: ${lastError.message}`);
1292
+ }
1293
+ }
1294
+ }
1295
+ }
1296
+ console.log(chalk2.bold("\n\u{1F4C4} Generating Playwright code...\n"));
1297
+ const generatedFile = await this.generator.saveTestFile(this.interpretations);
1298
+ console.log(chalk2.green(`\u2713 Test file saved: ${generatedFile}`));
1299
+ const duration = Date.now() - startTime;
1300
+ return {
1301
+ success: true,
1302
+ duration,
1303
+ actionsExecuted,
1304
+ generatedFile
1305
+ };
1306
+ } catch (error) {
1307
+ const duration = Date.now() - startTime;
1308
+ return {
1309
+ success: false,
1310
+ error: error.message,
1311
+ duration,
1312
+ actionsExecuted
1313
+ };
1314
+ } finally {
1315
+ if (this.executor) {
1316
+ await this.executor.close();
1317
+ }
1318
+ }
1319
+ }
1320
+ /**
1321
+ * Generate code only (no execution)
1322
+ */
1323
+ async generateOnly(commands) {
1324
+ console.log(chalk2.bold("\u{1F4DD} Processing commands for code generation...\n"));
1325
+ const interpretations = await this.processCommands(commands);
1326
+ console.log(chalk2.bold("\n\u{1F4C4} Generating Playwright code...\n"));
1327
+ const generatedFile = await this.generator.saveTestFile(interpretations);
1328
+ console.log(chalk2.green(`\u2713 Test file saved: ${generatedFile}`));
1329
+ return generatedFile;
1330
+ }
1331
+ /**
1332
+ * Get interpretations
1333
+ */
1334
+ getInterpretations() {
1335
+ return this.interpretations;
1336
+ }
1337
+ };
1338
+
1339
+ // src/cli.ts
1340
+ function showHelp() {
1341
+ console.log(chalk3.bold("\n\u26A1 Atezca - AI-Powered E2E Testing\n"));
1342
+ console.log("Usage:");
1343
+ console.log(" az run <file> Execute test file");
1344
+ console.log(" az generate <file> Generate Playwright code without execution");
1345
+ console.log(" az init Create .atezcarc config file");
1346
+ console.log(" az cache clear Clear interpretation cache");
1347
+ console.log(" az cache stats Show cache statistics");
1348
+ console.log(" az help Show this help message");
1349
+ console.log("");
1350
+ console.log("Examples:");
1351
+ console.log(" az run test.spec.js");
1352
+ console.log(" az generate test.spec.js");
1353
+ console.log(" az init");
1354
+ console.log("");
1355
+ }
1356
+ async function runTest(filepath) {
1357
+ try {
1358
+ console.log(chalk3.blue(`Loading test file: ${filepath}
1359
+ `));
1360
+ if (!fs4.existsSync(filepath)) {
1361
+ console.error(chalk3.red(`Error: File not found: ${filepath}`));
1362
+ process.exit(1);
1363
+ }
1364
+ const absPath = path3.resolve(filepath);
1365
+ await import(`file://${absPath}`);
1366
+ let state2 = getState();
1367
+ if (!state2.config || state2.commands.length === 0) {
1368
+ try {
1369
+ const currentDir = path3.dirname(new URL(import.meta.url).pathname);
1370
+ const indexModule = await import(path3.join(currentDir, "index.js"));
1371
+ const altState = indexModule.getState();
1372
+ if (altState.config) {
1373
+ state2 = altState;
1374
+ }
1375
+ } catch (e) {
1376
+ }
1377
+ }
1378
+ if (!state2.config) {
1379
+ console.error(chalk3.red("Error: az.setup() was not called in the test file"));
1380
+ process.exit(1);
1381
+ }
1382
+ if (state2.commands.length === 0) {
1383
+ console.error(chalk3.yellow("Warning: No test commands found"));
1384
+ process.exit(0);
1385
+ }
1386
+ console.log(chalk3.bold(`Found ${state2.commands.length} test command(s)
1387
+ `));
1388
+ const runner = new TestRunner(state2.config);
1389
+ const result = await runner.execute(state2.commands);
1390
+ console.log(chalk3.bold("\n" + "=".repeat(50)));
1391
+ if (result.success) {
1392
+ console.log(chalk3.green.bold("\n\u2713 Test passed!\n"));
1393
+ console.log(chalk3.gray(`Duration: ${(result.duration / 1e3).toFixed(2)}s`));
1394
+ console.log(chalk3.gray(`Actions executed: ${result.actionsExecuted}`));
1395
+ if (result.generatedFile) {
1396
+ console.log(chalk3.gray(`Generated file: ${result.generatedFile}`));
1397
+ }
1398
+ process.exit(0);
1399
+ } else {
1400
+ console.log(chalk3.red.bold("\n\u2717 Test failed!\n"));
1401
+ console.log(chalk3.red(`Error: ${result.error}`));
1402
+ console.log(chalk3.gray(`Duration: ${(result.duration / 1e3).toFixed(2)}s`));
1403
+ console.log(chalk3.gray(`Actions executed: ${result.actionsExecuted}`));
1404
+ process.exit(1);
1405
+ }
1406
+ } catch (error) {
1407
+ console.error(chalk3.red("\n\u2717 Error:"), error.message);
1408
+ process.exit(1);
1409
+ }
1410
+ }
1411
+ async function generateTest(filepath) {
1412
+ try {
1413
+ console.log(chalk3.blue(`Loading test file: ${filepath}
1414
+ `));
1415
+ if (!fs4.existsSync(filepath)) {
1416
+ console.error(chalk3.red(`Error: File not found: ${filepath}`));
1417
+ process.exit(1);
1418
+ }
1419
+ const absPath = path3.resolve(filepath);
1420
+ await import(`file://${absPath}`);
1421
+ let state2 = getState();
1422
+ if (!state2.config || state2.commands.length === 0) {
1423
+ try {
1424
+ const currentDir = path3.dirname(new URL(import.meta.url).pathname);
1425
+ const indexModule = await import(path3.join(currentDir, "index.js"));
1426
+ const altState = indexModule.getState();
1427
+ if (altState.config) {
1428
+ state2 = altState;
1429
+ }
1430
+ } catch (e) {
1431
+ }
1432
+ }
1433
+ if (!state2.config) {
1434
+ console.error(chalk3.red("Error: az.setup() was not called in the test file"));
1435
+ process.exit(1);
1436
+ }
1437
+ if (state2.commands.length === 0) {
1438
+ console.error(chalk3.yellow("Warning: No test commands found"));
1439
+ process.exit(0);
1440
+ }
1441
+ console.log(chalk3.bold(`Found ${state2.commands.length} test command(s)
1442
+ `));
1443
+ const runner = new TestRunner(state2.config);
1444
+ const generatedFile = await runner.generateOnly(state2.commands);
1445
+ console.log(chalk3.green.bold("\n\u2713 Code generation complete!\n"));
1446
+ console.log(chalk3.gray(`Generated file: ${generatedFile}`));
1447
+ console.log(chalk3.gray("\nRun with: npx playwright test"));
1448
+ process.exit(0);
1449
+ } catch (error) {
1450
+ console.error(chalk3.red("\n\u2717 Error:"), error.message);
1451
+ process.exit(1);
1452
+ }
1453
+ }
1454
+ function initConfig() {
1455
+ const filepath = ".atezcarc";
1456
+ if (fs4.existsSync(filepath)) {
1457
+ console.log(chalk3.yellow(`Warning: ${filepath} already exists`));
1458
+ process.exit(0);
1459
+ }
1460
+ ConfigLoader.createDefaultConfig(filepath);
1461
+ console.log(chalk3.green(`\u2713 Created ${filepath}`));
1462
+ console.log(chalk3.gray("\nDon't forget to set your ANTHROPIC_API_KEY in .env or .atezcarc"));
1463
+ process.exit(0);
1464
+ }
1465
+ function clearCache() {
1466
+ const cache = new CacheManager();
1467
+ cache.clear();
1468
+ console.log(chalk3.green("\u2713 Cache cleared"));
1469
+ process.exit(0);
1470
+ }
1471
+ function showCacheStats() {
1472
+ const cache = new CacheManager();
1473
+ const stats = cache.getStats();
1474
+ console.log(chalk3.bold("\nCache Statistics:\n"));
1475
+ console.log(chalk3.gray(`Entries: ${stats.size}`));
1476
+ if (stats.oldestEntry) {
1477
+ const oldest = new Date(stats.oldestEntry);
1478
+ console.log(chalk3.gray(`Oldest entry: ${oldest.toLocaleString()}`));
1479
+ }
1480
+ if (stats.newestEntry) {
1481
+ const newest = new Date(stats.newestEntry);
1482
+ console.log(chalk3.gray(`Newest entry: ${newest.toLocaleString()}`));
1483
+ }
1484
+ if (stats.size === 0) {
1485
+ console.log(chalk3.yellow("\nCache is empty"));
1486
+ }
1487
+ console.log("");
1488
+ process.exit(0);
1489
+ }
1490
+ async function main() {
1491
+ const args = process.argv.slice(2);
1492
+ if (args.length === 0 || args[0] === "help" || args[0] === "--help" || args[0] === "-h") {
1493
+ showHelp();
1494
+ process.exit(0);
1495
+ }
1496
+ const command = args[0];
1497
+ const subcommand = args[1];
1498
+ switch (command) {
1499
+ case "run":
1500
+ if (!subcommand) {
1501
+ console.error(chalk3.red("Error: Please specify a test file"));
1502
+ console.log("Usage: az run <file>");
1503
+ process.exit(1);
1504
+ }
1505
+ await runTest(subcommand);
1506
+ break;
1507
+ case "generate":
1508
+ if (!subcommand) {
1509
+ console.error(chalk3.red("Error: Please specify a test file"));
1510
+ console.log("Usage: az generate <file>");
1511
+ process.exit(1);
1512
+ }
1513
+ await generateTest(subcommand);
1514
+ break;
1515
+ case "init":
1516
+ initConfig();
1517
+ break;
1518
+ case "cache":
1519
+ if (subcommand === "clear") {
1520
+ clearCache();
1521
+ } else if (subcommand === "stats") {
1522
+ showCacheStats();
1523
+ } else {
1524
+ console.error(chalk3.red("Error: Unknown cache command"));
1525
+ console.log("Usage: az cache <clear|stats>");
1526
+ process.exit(1);
1527
+ }
1528
+ break;
1529
+ default:
1530
+ console.error(chalk3.red(`Error: Unknown command: ${command}`));
1531
+ showHelp();
1532
+ process.exit(1);
1533
+ }
1534
+ }
1535
+ main().catch((error) => {
1536
+ console.error(chalk3.red("Fatal error:"), error);
1537
+ process.exit(1);
1538
+ });
1539
+ //# sourceMappingURL=cli.mjs.map