@arcadialdev/arcality 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/.agents/skills/e2e-testing-expert/SKILL.md +28 -0
  2. package/.agents/skills/frontend-design/LICENSE.txt +177 -0
  3. package/.agents/skills/frontend-design/SKILL.md +42 -0
  4. package/.agents/skills/nodejs-backend-patterns/SKILL.md +639 -0
  5. package/.agents/skills/nodejs-backend-patterns/references/advanced-patterns.md +430 -0
  6. package/.agents/skills/playwright-best-practices/LICENSE.md +7 -0
  7. package/.agents/skills/playwright-best-practices/README.md +147 -0
  8. package/.agents/skills/playwright-best-practices/SKILL.md +303 -0
  9. package/.agents/skills/playwright-best-practices/advanced/authentication-flows.md +360 -0
  10. package/.agents/skills/playwright-best-practices/advanced/authentication.md +871 -0
  11. package/.agents/skills/playwright-best-practices/advanced/clock-mocking.md +364 -0
  12. package/.agents/skills/playwright-best-practices/advanced/mobile-testing.md +409 -0
  13. package/.agents/skills/playwright-best-practices/advanced/multi-context.md +288 -0
  14. package/.agents/skills/playwright-best-practices/advanced/multi-user.md +393 -0
  15. package/.agents/skills/playwright-best-practices/advanced/network-advanced.md +452 -0
  16. package/.agents/skills/playwright-best-practices/advanced/third-party.md +464 -0
  17. package/.agents/skills/playwright-best-practices/architecture/pom-vs-fixtures.md +363 -0
  18. package/.agents/skills/playwright-best-practices/architecture/test-architecture.md +369 -0
  19. package/.agents/skills/playwright-best-practices/architecture/when-to-mock.md +383 -0
  20. package/.agents/skills/playwright-best-practices/browser-apis/browser-apis.md +391 -0
  21. package/.agents/skills/playwright-best-practices/browser-apis/iframes.md +403 -0
  22. package/.agents/skills/playwright-best-practices/browser-apis/service-workers.md +504 -0
  23. package/.agents/skills/playwright-best-practices/browser-apis/websockets.md +403 -0
  24. package/.agents/skills/playwright-best-practices/core/annotations.md +424 -0
  25. package/.agents/skills/playwright-best-practices/core/assertions-waiting.md +361 -0
  26. package/.agents/skills/playwright-best-practices/core/configuration.md +452 -0
  27. package/.agents/skills/playwright-best-practices/core/fixtures-hooks.md +417 -0
  28. package/.agents/skills/playwright-best-practices/core/global-setup.md +434 -0
  29. package/.agents/skills/playwright-best-practices/core/locators.md +242 -0
  30. package/.agents/skills/playwright-best-practices/core/page-object-model.md +315 -0
  31. package/.agents/skills/playwright-best-practices/core/projects-dependencies.md +453 -0
  32. package/.agents/skills/playwright-best-practices/core/test-data.md +492 -0
  33. package/.agents/skills/playwright-best-practices/core/test-suite-structure.md +361 -0
  34. package/.agents/skills/playwright-best-practices/core/test-tags.md +298 -0
  35. package/.agents/skills/playwright-best-practices/debugging/console-errors.md +420 -0
  36. package/.agents/skills/playwright-best-practices/debugging/debugging.md +504 -0
  37. package/.agents/skills/playwright-best-practices/debugging/error-testing.md +360 -0
  38. package/.agents/skills/playwright-best-practices/debugging/flaky-tests.md +496 -0
  39. package/.agents/skills/playwright-best-practices/frameworks/angular.md +530 -0
  40. package/.agents/skills/playwright-best-practices/frameworks/nextjs.md +469 -0
  41. package/.agents/skills/playwright-best-practices/frameworks/react.md +531 -0
  42. package/.agents/skills/playwright-best-practices/frameworks/vue.md +574 -0
  43. package/.agents/skills/playwright-best-practices/infrastructure-ci-cd/ci-cd.md +468 -0
  44. package/.agents/skills/playwright-best-practices/infrastructure-ci-cd/docker.md +283 -0
  45. package/.agents/skills/playwright-best-practices/infrastructure-ci-cd/github-actions.md +546 -0
  46. package/.agents/skills/playwright-best-practices/infrastructure-ci-cd/gitlab.md +397 -0
  47. package/.agents/skills/playwright-best-practices/infrastructure-ci-cd/other-providers.md +521 -0
  48. package/.agents/skills/playwright-best-practices/infrastructure-ci-cd/parallel-sharding.md +371 -0
  49. package/.agents/skills/playwright-best-practices/infrastructure-ci-cd/performance.md +453 -0
  50. package/.agents/skills/playwright-best-practices/infrastructure-ci-cd/reporting.md +424 -0
  51. package/.agents/skills/playwright-best-practices/infrastructure-ci-cd/test-coverage.md +497 -0
  52. package/.agents/skills/playwright-best-practices/testing-patterns/accessibility.md +359 -0
  53. package/.agents/skills/playwright-best-practices/testing-patterns/api-testing.md +719 -0
  54. package/.agents/skills/playwright-best-practices/testing-patterns/browser-extensions.md +506 -0
  55. package/.agents/skills/playwright-best-practices/testing-patterns/canvas-webgl.md +493 -0
  56. package/.agents/skills/playwright-best-practices/testing-patterns/component-testing.md +500 -0
  57. package/.agents/skills/playwright-best-practices/testing-patterns/drag-drop.md +576 -0
  58. package/.agents/skills/playwright-best-practices/testing-patterns/electron.md +509 -0
  59. package/.agents/skills/playwright-best-practices/testing-patterns/file-operations.md +377 -0
  60. package/.agents/skills/playwright-best-practices/testing-patterns/file-upload-download.md +562 -0
  61. package/.agents/skills/playwright-best-practices/testing-patterns/forms-validation.md +561 -0
  62. package/.agents/skills/playwright-best-practices/testing-patterns/graphql-testing.md +331 -0
  63. package/.agents/skills/playwright-best-practices/testing-patterns/i18n.md +508 -0
  64. package/.agents/skills/playwright-best-practices/testing-patterns/performance-testing.md +476 -0
  65. package/.agents/skills/playwright-best-practices/testing-patterns/security-testing.md +430 -0
  66. package/.agents/skills/playwright-best-practices/testing-patterns/visual-regression.md +634 -0
  67. package/.env.example +21 -0
  68. package/README.md +30 -0
  69. package/bin/arcality.mjs +86 -0
  70. package/package.json +66 -0
  71. package/playwright.config.ts +12 -0
  72. package/scripts/cleanup-qmsdev.mjs +63 -0
  73. package/scripts/discover-view.mjs +52 -0
  74. package/scripts/extract-view.mjs +64 -0
  75. package/scripts/gen-and-run.mjs +838 -0
  76. package/scripts/init.mjs +290 -0
  77. package/scripts/migrate-to-central-out.mjs +157 -0
  78. package/scripts/postinstall.mjs +63 -0
  79. package/scripts/rebrand-report.mjs +241 -0
  80. package/scripts/setup.mjs +166 -0
  81. package/src/KnowledgeService.ts +239 -0
  82. package/src/arcalityClient.mjs +266 -0
  83. package/src/configLoader.mjs +179 -0
  84. package/src/configManager.mjs +172 -0
  85. package/src/consoleBanner.ts +32 -0
  86. package/src/envSetup.ts +205 -0
  87. package/src/index.ts +25 -0
  88. package/src/projectInspector.ts +42 -0
  89. package/src/services/collectiveMemoryService.ts +178 -0
  90. package/src/testRunner.ts +201 -0
  91. package/tests/_helpers/ArcalityReporter.ts +490 -0
  92. package/tests/_helpers/agentic-runner.spec.ts +741 -0
  93. package/tests/_helpers/ai-agent-helper.ts +1573 -0
  94. package/tests/_helpers/discover-view.spec.ts +238 -0
  95. package/tests/_helpers/extract-view.spec.ts +118 -0
  96. package/tests/_helpers/qa-tools.ts +333 -0
  97. package/tests/_helpers/smart-action.spec.ts +1458 -0
@@ -0,0 +1,838 @@
1
+ #!/usr/bin/env node
2
+ // scripts/gen-and-run.mjs
3
+ // Main Arcality CLI loop — v3 Single Configuration Mode
4
+ // Reads from arcality.config instead of multi-config .env approach.
5
+
6
+ import 'dotenv/config';
7
+ import fs from "node:fs";
8
+ import path from "node:path";
9
+ import { spawn, exec } from "node:child_process";
10
+ import chalk from "chalk";
11
+ import { fileURLToPath } from 'url';
12
+ import figlet from "figlet";
13
+ import { intro, outro, select, text, spinner, note, isCancel, cancel } from '@clack/prompts';
14
+ import { load as loadYaml } from 'js-yaml';
15
+ import { loadGlobalConfig, getApiKey, getApiUrl, maskApiKey, setupProcessEnv } from '../src/configLoader.mjs';
16
+ import {
17
+ configExists,
18
+ loadProjectConfig,
19
+ validateConfig,
20
+ injectConfigToEnv,
21
+ getYamlOutputDir,
22
+ ensureYamlOutputDir,
23
+ } from '../src/configManager.mjs';
24
+
25
+ // Load global config at startup (injects keys into process.env)
26
+ setupProcessEnv();
27
+
28
+ const __filename = fileURLToPath(import.meta.url);
29
+ const __dirname = path.dirname(__filename);
30
+ const PROJECT_ROOT = process.env.ARCALITY_ROOT || path.join(__dirname, "..");
31
+
32
+ const ORIGINAL_CWD = process.cwd();
33
+
34
+ // ── Load arcality.config if present ──
35
+ const projectConfig = loadProjectConfig(ORIGINAL_CWD) || loadProjectConfig(PROJECT_ROOT);
36
+ if (projectConfig) {
37
+ injectConfigToEnv(projectConfig);
38
+ }
39
+
40
+ function parseDotEnvFile(p) {
41
+ try {
42
+ const raw = fs.readFileSync(p, 'utf8');
43
+ const lines = raw.split(/\r?\n/);
44
+ const out = {};
45
+ for (const line of lines) {
46
+ const trimmed = line.trim();
47
+ if (!trimmed || trimmed.startsWith('#')) continue;
48
+ const idx = trimmed.indexOf('=');
49
+ if (idx === -1) continue;
50
+ let k = trimmed.slice(0, idx).trim();
51
+ let v = trimmed.slice(idx + 1).trim();
52
+ if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) v = v.slice(1, -1);
53
+ out[k] = v;
54
+ }
55
+ return out;
56
+ } catch {
57
+ return {};
58
+ }
59
+ }
60
+
61
+ function updateDotEnvFile(updates) {
62
+ const p = path.join(PROJECT_ROOT, '.env');
63
+ let content = '';
64
+ try {
65
+ content = fs.readFileSync(p, 'utf8');
66
+ } catch {
67
+ content = '';
68
+ }
69
+
70
+ let lines = content.split(/\r?\n/);
71
+ const keys = Object.keys(updates);
72
+ const updatedKeys = new Set();
73
+
74
+ lines = lines.map(line => {
75
+ const trimmed = line.trim();
76
+ if (!trimmed || trimmed.startsWith('#')) return line;
77
+ const idx = trimmed.indexOf('=');
78
+ if (idx === -1) return line;
79
+ const k = trimmed.slice(0, idx).trim();
80
+ if (keys.includes(k)) {
81
+ updatedKeys.add(k);
82
+ return `${k}=${updates[k]}`;
83
+ }
84
+ return line;
85
+ });
86
+
87
+ keys.forEach(k => {
88
+ if (!updatedKeys.has(k)) {
89
+ lines.push(`${k}=${updates[k]}`);
90
+ }
91
+ });
92
+
93
+ fs.writeFileSync(p, lines.join('\n'), 'utf8');
94
+ keys.forEach(k => { process.env[k] = updates[k]; });
95
+ }
96
+
97
+ function setupEnvironment() {
98
+ // Load API Key from global config
99
+ const globalConfig = loadGlobalConfig();
100
+ if (globalConfig?.api_key && !process.env.ARCALITY_API_KEY) {
101
+ process.env.ARCALITY_API_KEY = globalConfig.api_key;
102
+ }
103
+
104
+ // Use config name from arcality.config or fallback
105
+ const configName = projectConfig?.project?.name || 'Default';
106
+ const baseUrl = projectConfig?.project?.baseUrl || process.env.BASE_URL || 'http://localhost';
107
+
108
+ const rootOutDir = path.join(ORIGINAL_CWD, '.arcality', 'out');
109
+ const configDir = path.join(rootOutDir, configName.replace(/[^a-zA-Z0-9_-]/g, '_'));
110
+
111
+ process.env.DOMAIN_DIR = configDir;
112
+ process.env.MISSIONS_DIR = path.join(configDir, 'missions');
113
+ process.env.CONTEXT_DIR = path.join(configDir, 'context');
114
+ process.env.REPORTS_DIR = path.join(configDir, 'reports');
115
+
116
+ [process.env.MISSIONS_DIR, process.env.CONTEXT_DIR, process.env.REPORTS_DIR].forEach(dir => {
117
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
118
+ });
119
+
120
+ // Also ensure YAML output dir from arcality.config
121
+ if (projectConfig) {
122
+ ensureYamlOutputDir(projectConfig, ORIGINAL_CWD);
123
+ }
124
+
125
+ // Framework detection from package.json (NO .git)
126
+ let techStack = 'Not detected';
127
+ try {
128
+ const pkg = JSON.parse(fs.readFileSync(path.join(PROJECT_ROOT, 'package.json'), 'utf8'));
129
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
130
+ if (deps['next']) techStack = 'Next.js 🚀';
131
+ else if (deps['vite']) techStack = 'Vite ⚡';
132
+ else if (deps['react-scripts']) techStack = 'Create React App ⚛️';
133
+ } catch { }
134
+
135
+ // Override with arcality.config detection if available
136
+ if (projectConfig?.project?.frameworkDetected) {
137
+ techStack = projectConfig.project.frameworkDetected;
138
+ }
139
+
140
+ return { configName, baseUrl, techStack, configDir };
141
+ }
142
+
143
+ function showBanner() {
144
+ console.clear();
145
+ const projectName = 'Arcality';
146
+
147
+ let version = 'unknown';
148
+ try {
149
+ const pkg = JSON.parse(fs.readFileSync(path.join(PROJECT_ROOT, 'package.json'), 'utf8'));
150
+ version = pkg.version || 'unknown';
151
+ } catch { }
152
+
153
+ const logo = figlet.textSync(projectName, {
154
+ font: 'Standard',
155
+ horizontalLayout: 'default',
156
+ verticalLayout: 'default',
157
+ });
158
+
159
+ intro(chalk.cyanBright(logo));
160
+
161
+ note(
162
+ chalk.magentaBright('Powered by Arcadial') + '\n' +
163
+ chalk.gray('─'.repeat(50)) + '\n' +
164
+ chalk.bold.white('📦 Version: ') + chalk.yellow(version),
165
+ 'System Information'
166
+ );
167
+
168
+ const { configName, baseUrl, techStack, configDir } = setupEnvironment();
169
+
170
+ // Show arcality.config status instead of multi-config switching
171
+ const hasConfig = !!projectConfig;
172
+ const configStatus = hasConfig
173
+ ? chalk.green('✅ Configured')
174
+ : chalk.red('❌ Not configured — run `arcality init`');
175
+
176
+ const { key: apiKeyVal, source: apiKeySource } = getApiKey();
177
+ const apiKeyDisplay = apiKeyVal
178
+ ? chalk.green(maskApiKey(apiKeyVal)) + chalk.gray(` (${apiKeySource})`)
179
+ : chalk.red('Not configured');
180
+
181
+ const infoLines = [
182
+ chalk.bold.white('⚙️ Environment: ') + chalk.yellow(process.env.NODE_ENV ?? 'development'),
183
+ chalk.bold.white('🔑 API Key: ') + apiKeyDisplay,
184
+ chalk.bold.white('🛠️ Stack: ') + chalk.magenta(techStack),
185
+ ];
186
+
187
+ if (hasConfig) {
188
+ infoLines.splice(1, 0,
189
+ chalk.bold.white('📋 Project: ') + chalk.cyan(configName) + chalk.gray(` (${baseUrl})`),
190
+ chalk.bold.white('🆔 Project ID: ') + chalk.gray(projectConfig.projectId || 'N/A'),
191
+ );
192
+ } else {
193
+ infoLines.push(chalk.bold.white('📋 Config: ') + configStatus);
194
+ }
195
+
196
+ note(infoLines.join('\n'), 'Active Configuration');
197
+ }
198
+
199
+ const ARCALITY_BRAIN_URL = "https://api.anthropic.com/v1/messages";
200
+ const ARCALITY_MODEL = process.env.CLAUDE_MODEL || "claude-3-5-sonnet-20241022";
201
+ const OUT_DIR = path.join(PROJECT_ROOT, "tests");
202
+ const LOGS_DIR = path.join(PROJECT_ROOT, "logs");
203
+
204
+ async function arcalityChat(messages) {
205
+ const internalApiUrl = getApiUrl();
206
+ const isProxyMode = !!internalApiUrl;
207
+
208
+ if (!process.env.ANTHROPIC_API_KEY && !isProxyMode) {
209
+ throw new Error("Missing ANTHROPIC_API_KEY in .env");
210
+ }
211
+
212
+ const systemMessage = messages.find(m => m.role === 'system');
213
+ const userMessages = messages.filter(m => m.role !== 'system');
214
+
215
+ try {
216
+ const endpointUrl = isProxyMode
217
+ ? `${internalApiUrl}/api/v1/ai/proxy`
218
+ : ARCALITY_BRAIN_URL;
219
+
220
+ const headers = {
221
+ "Content-Type": "application/json"
222
+ };
223
+
224
+ if (isProxyMode) {
225
+ headers["x-api-key"] = process.env.ARCALITY_API_KEY || "";
226
+ } else {
227
+ headers["x-api-key"] = process.env.ANTHROPIC_API_KEY;
228
+ headers["anthropic-version"] = "2023-06-01";
229
+ }
230
+
231
+ const res = await fetch(endpointUrl, {
232
+ method: "POST",
233
+ headers,
234
+ body: JSON.stringify({
235
+ model: ARCALITY_MODEL,
236
+ system: systemMessage ? systemMessage.content : undefined,
237
+ messages: userMessages,
238
+ max_tokens: 4000,
239
+ temperature: 0.3
240
+ })
241
+ });
242
+
243
+ if (!res.ok) {
244
+ const txt = await res.text();
245
+ throw new Error(`Arcality Brain Error ${res.status}: ${txt}`);
246
+ }
247
+
248
+ const data = await res.json();
249
+ const content = data.content?.[0]?.text || "";
250
+
251
+ try {
252
+ if (!fs.existsSync(LOGS_DIR)) fs.mkdirSync(LOGS_DIR, { recursive: true });
253
+ fs.writeFileSync(path.join(LOGS_DIR, 'last-ai-response.txt'), content);
254
+ } catch (e) { /* ignore */ }
255
+
256
+ return content;
257
+ } catch (err) {
258
+ throw new Error(`Error communicating with Arcality brain: ${err.message}`);
259
+ }
260
+ }
261
+
262
+ function extractBlocks(text) {
263
+ const cleaned = text
264
+ .replace(/```[a-zA-Z]*\n?/g, "")
265
+ .replace(/```/g, "")
266
+ .trim();
267
+
268
+ const fnMatch = cleaned.match(/^FILENAME:\s*(.+)$/m);
269
+ const contentIdx = cleaned.indexOf("CONTENT:");
270
+
271
+ if (!fnMatch || contentIdx === -1) {
272
+ throw new Error(
273
+ `Invalid format. Expected:\nFILENAME: <file.spec.ts>\nCONTENT:\n<code>\n\nResponse:\n${cleaned}`
274
+ );
275
+ }
276
+
277
+ const filename = fnMatch[1].trim();
278
+ const content = cleaned.slice(contentIdx + "CONTENT:".length).trim();
279
+
280
+ return { filename, content, cleaned };
281
+ }
282
+
283
+ function validateSpec(ts) {
284
+ const errors = [];
285
+ if (!/test\s*\(.*async\s*\(\s*\{\s*page\s*\}\s*\)\s*=>/s.test(ts)) {
286
+ errors.push('Missing fixture: test("...", async ({ page }) => { ... })');
287
+ }
288
+ if (/from\s+['"]playwright['"]/.test(ts)) {
289
+ errors.push("Forbidden to import from core. Use only the Arcality SDK.");
290
+ }
291
+ if (!ts.includes("import { test, expect } from '@playwright/test'")) {
292
+ errors.push("Must import exactly the Arcality engine.");
293
+ }
294
+ return errors;
295
+ }
296
+
297
+ function sanitizeFilename(name, fallback) {
298
+ if (typeof name !== "string") return fallback;
299
+ const safe = name.trim().replace(/[<>:"/\\|?*\u0000-\u001F]/g, "_");
300
+ if (!safe.endsWith(".spec.ts")) return fallback;
301
+ return safe;
302
+ }
303
+
304
+ function run(cmd, args, options = {}) {
305
+ return new Promise((resolve, reject) => {
306
+ const appNodeModules = path.join(PROJECT_ROOT, 'node_modules');
307
+ const existingNodePath = process.env.NODE_PATH || '';
308
+ const newNodePath = existingNodePath ? `${appNodeModules}${path.delimiter}${existingNodePath}` : appNodeModules;
309
+
310
+ const p = spawn(cmd, args, {
311
+ stdio: ["ignore", "pipe", "pipe"],
312
+ shell: process.platform === "win32",
313
+ windowsHide: true,
314
+ env: { ...options.env || process.env, NODE_PATH: newNodePath },
315
+ });
316
+
317
+ let outputBuffer = '';
318
+ p.stdout.on('data', (data) => {
319
+ const str = data.toString();
320
+ if (str.includes('>>ARCALITY_STATUS>>')) {
321
+ const lines = str.split('\n');
322
+ for (const line of lines) {
323
+ if (line.includes('>>ARCALITY_STATUS>>')) {
324
+ const status = line.split('>>ARCALITY_STATUS>>')[1].trim();
325
+ if (options.onStatus) options.onStatus(status);
326
+ } else if (line.trim()) {
327
+ process.stdout.write(line + '\n');
328
+ }
329
+ }
330
+ } else {
331
+ process.stdout.write(data);
332
+ }
333
+ outputBuffer += str;
334
+ });
335
+
336
+ p.stderr.on('data', (data) => {
337
+ process.stderr.write(data);
338
+ outputBuffer += data.toString();
339
+ });
340
+
341
+ p.on("exit", (code) => {
342
+ const lastRunLog = path.join(LOGS_DIR, 'last-run.log');
343
+ try {
344
+ if (!fs.existsSync(LOGS_DIR)) fs.mkdirSync(LOGS_DIR, { recursive: true });
345
+ fs.writeFileSync(lastRunLog, outputBuffer);
346
+ } catch (e) { }
347
+
348
+ if (code === 0) resolve();
349
+ else reject(new Error(`Command failed (${code}): ${cmd} ${args.join(" ")}`));
350
+ });
351
+ });
352
+ }
353
+
354
+ /**
355
+ * Ping the project to register activity.
356
+ * Uses internal API URL — not user-configurable.
357
+ */
358
+ async function pingProject(projectId, apiKey) {
359
+ if (!projectId || !apiKey) return;
360
+ try {
361
+ await fetch(`${getApiUrl()}/api/v1/projects/${projectId}/ping`, {
362
+ method: 'PATCH',
363
+ headers: { 'x-api-key': apiKey },
364
+ });
365
+ } catch { /* silent */ }
366
+ }
367
+
368
+ async function main() {
369
+ process.chdir(PROJECT_ROOT);
370
+
371
+ // Backward compat: initialize .env if ACTIVE_CONFIG missing (legacy)
372
+ let initialEnv = parseDotEnvFile(path.join(PROJECT_ROOT, '.env'));
373
+ if (!projectConfig && !initialEnv.ACTIVE_CONFIG) {
374
+ updateDotEnvFile({
375
+ ACTIVE_CONFIG: 'Default',
376
+ SAVED_CONFIGS: 'Default',
377
+ Default_URL: initialEnv.BASE_URL || '',
378
+ Default_USER: initialEnv.LOGIN_USER || '',
379
+ Default_PASS: initialEnv.LOGIN_PASSWORD || ''
380
+ });
381
+ initialEnv = parseDotEnvFile(path.join(PROJECT_ROOT, '.env'));
382
+ }
383
+
384
+ const argv = process.argv.slice(2);
385
+ let initialDiscoverPath = null;
386
+ let initialSmartMode = false;
387
+ let initialAgentMode = false;
388
+
389
+ const dIndex = argv.findIndex(a => a === '--discover' || a.startsWith('--discover='));
390
+ if (dIndex !== -1) {
391
+ const token = argv[dIndex];
392
+ if (token.includes('=')) initialDiscoverPath = token.split('=')[1];
393
+ else if (argv[dIndex + 1]) { initialDiscoverPath = argv[dIndex + 1]; argv.splice(dIndex + 1, 1); }
394
+ argv.splice(dIndex, 1);
395
+ }
396
+ const sIndex = argv.findIndex(a => a === '--smart');
397
+ if (sIndex !== -1) { initialSmartMode = true; argv.splice(sIndex, 1); }
398
+ const aIndex = argv.findIndex(a => a === '--agent');
399
+ if (aIndex !== -1) { initialAgentMode = true; argv.splice(aIndex, 1); }
400
+
401
+ const promptArg = argv.join(" ").trim();
402
+ let firstRun = true;
403
+ let globalReportProcess = null;
404
+
405
+ setupEnvironment();
406
+
407
+ while (true) {
408
+ let prompt = firstRun ? promptArg : "";
409
+ let agentMode = firstRun ? initialAgentMode : false;
410
+ let smartMode = firstRun ? initialSmartMode : false;
411
+ let discoverPath = firstRun ? initialDiscoverPath : null;
412
+
413
+ let skipMenu = firstRun && (prompt || agentMode || smartMode);
414
+ firstRun = false;
415
+
416
+ if (!skipMenu) {
417
+ showBanner();
418
+ try {
419
+ // ── Build menu options (single config mode) ──
420
+ const missionsDir = process.env.MISSIONS_DIR;
421
+ const savedMissions = fs.existsSync(missionsDir)
422
+ ? fs.readdirSync(missionsDir).filter(f => f.endsWith('.yaml') || f.endsWith('.yml'))
423
+ : [];
424
+
425
+ // Check for YAML files in the yaml output dir from arcality.config
426
+ let yamlOutputFiles = [];
427
+ if (projectConfig) {
428
+ const yamlDir = getYamlOutputDir(projectConfig, ORIGINAL_CWD);
429
+ if (fs.existsSync(yamlDir)) {
430
+ yamlOutputFiles = fs.readdirSync(yamlDir).filter(f => f.endsWith('.yaml') || f.endsWith('.yml'));
431
+ }
432
+ }
433
+
434
+ // Root-level YAML templates
435
+ const rootYamlFiles = fs.readdirSync(PROJECT_ROOT).filter(f => f.endsWith('.yaml') || f.endsWith('.yml'));
436
+
437
+ const options = [
438
+ { label: '🤖 Run Autonomous Agent (Prompt)', value: 'agent' },
439
+ ...(savedMissions.length ? [{ label: '🚀 Launch Saved Mission (.yaml)', value: 'launch_saved' }] : []),
440
+ ...(yamlOutputFiles.length ? [{ label: '♻️ Reuse Successful YAML', value: 'reuse_yaml' }] : []),
441
+ ...(rootYamlFiles.length ? [{ label: '📂 Choose Root YAML (Templates)', value: 'yaml_list' }] : []),
442
+ { label: '📋 View Current Configuration', value: 'view_config' },
443
+ { label: '🔄 Reconfigure (arcality init)', value: 'reconfigure' },
444
+ { label: '❌ Exit', value: 'exit' }
445
+ ];
446
+
447
+ const action = await select({
448
+ message: 'What would you like to do?',
449
+ options
450
+ });
451
+
452
+ if (isCancel(action) || action === 'exit') {
453
+ outro(chalk.cyan('See you later!'));
454
+ break;
455
+ }
456
+
457
+ if (action === 'agent') {
458
+ agentMode = true;
459
+ } else if (action === 'view_config') {
460
+ if (projectConfig) {
461
+ note(
462
+ chalk.bold.white('📋 Project: ') + chalk.cyan(projectConfig.project?.name) + '\n' +
463
+ chalk.bold.white('🌐 Base URL: ') + chalk.cyan(projectConfig.project?.baseUrl) + '\n' +
464
+ chalk.bold.white('🛠️ Framework: ') + chalk.magenta(projectConfig.project?.frameworkDetected || 'N/A') + '\n' +
465
+ chalk.bold.white('🆔 Project ID: ') + chalk.gray(projectConfig.projectId) + '\n' +
466
+ chalk.bold.white('🏢 Org ID: ') + chalk.gray(projectConfig.organizationId || 'N/A') + '\n' +
467
+ chalk.bold.white('👤 User: ') + chalk.cyan(projectConfig.auth?.username) + '\n' +
468
+ chalk.bold.white('📂 YAML Dir: ') + chalk.yellow(projectConfig.runtime?.yamlOutputDir) + '\n' +
469
+ chalk.bold.white('♻️ Reuse YAMLs: ') + chalk.cyan(projectConfig.runtime?.reuseSuccessfulYamls ? '✅' : '❌') + '\n' +
470
+ chalk.bold.white('📦 Version: ') + chalk.yellow(projectConfig.meta?.arcalityVersion || 'N/A'),
471
+ 'arcality.config'
472
+ );
473
+ } else {
474
+ note(
475
+ chalk.yellow('No arcality.config found.') + '\n' +
476
+ chalk.gray('Run `arcality init` to configure this project.'),
477
+ 'No Configuration'
478
+ );
479
+ }
480
+ continue;
481
+ } else if (action === 'reconfigure') {
482
+ // Launch arcality init in the user's project directory
483
+ const initScript = path.join(PROJECT_ROOT, 'scripts', 'init.mjs');
484
+ try {
485
+ await new Promise((resolve, reject) => {
486
+ const child = spawn('node', [initScript], {
487
+ stdio: 'inherit',
488
+ cwd: ORIGINAL_CWD,
489
+ env: { ...process.env, ARCALITY_ROOT: PROJECT_ROOT }
490
+ });
491
+ child.on('exit', code => code === 0 ? resolve() : reject(new Error(`Init exited with ${code}`)));
492
+ child.on('error', reject);
493
+ });
494
+ } catch { /* user cancelled or error */ }
495
+ continue;
496
+ } else if (action === 'launch_saved') {
497
+ const sel = await select({
498
+ message: 'Choose saved mission:',
499
+ options: savedMissions.map(f => ({ label: f, value: f }))
500
+ });
501
+ if (isCancel(sel)) continue;
502
+
503
+ const yamlContent = fs.readFileSync(path.join(missionsDir, sel), 'utf8');
504
+ const data = loadYaml(yamlContent);
505
+ prompt = data.prompt || data.mision || yamlContent;
506
+ agentMode = true;
507
+ } else if (action === 'reuse_yaml') {
508
+ const yamlDir = getYamlOutputDir(projectConfig, ORIGINAL_CWD);
509
+ const sel = await select({
510
+ message: 'Choose YAML to reuse:',
511
+ options: yamlOutputFiles.map(f => ({ label: f, value: f }))
512
+ });
513
+ if (isCancel(sel)) continue;
514
+
515
+ const yamlContent = fs.readFileSync(path.join(yamlDir, sel), 'utf8');
516
+ const data = loadYaml(yamlContent);
517
+ prompt = data.prompt || data.mision || yamlContent;
518
+ agentMode = true;
519
+ } else if (action === 'yaml_list') {
520
+ const sel = await select({
521
+ message: 'Choose YAML file:',
522
+ options: rootYamlFiles.map(f => ({ label: f, value: f }))
523
+ });
524
+ if (isCancel(sel)) continue;
525
+
526
+ const yamlContent = fs.readFileSync(path.join(PROJECT_ROOT, sel), 'utf8');
527
+ const data = loadYaml(yamlContent);
528
+ prompt = data.mision || data.prompt || yamlContent;
529
+ agentMode = true;
530
+ }
531
+ } catch (e) {
532
+ console.error("Menu error:", e);
533
+ break;
534
+ }
535
+ }
536
+
537
+ // --- VALIDATION AND CAPTURE OF PARAMETERS FOR THE AGENT ---
538
+ if (agentMode) {
539
+ if (!prompt || prompt.trim().length < 4) {
540
+ const p = await text({
541
+ message: chalk.cyan('🤖 Mission for the Agent (Required):'),
542
+ placeholder: 'E.g., Fill the signup form with random data',
543
+ validate: v => {
544
+ if (!v || !v.trim()) return 'The mission is required to start.';
545
+ if (v.trim().length < 4) return 'The mission must be more descriptive (minimum 4 characters).';
546
+ }
547
+ });
548
+
549
+ if (isCancel(p)) {
550
+ cancel('Mission cancelled. Returning to main menu...');
551
+ agentMode = false;
552
+ prompt = "";
553
+ skipMenu = false;
554
+ continue;
555
+ }
556
+ prompt = p;
557
+ }
558
+
559
+ if (!discoverPath) {
560
+ const d = await text({
561
+ message: chalk.cyan('🌐 Initial navigation path:'),
562
+ initialValue: '/',
563
+ placeholder: '/'
564
+ });
565
+ if (isCancel(d)) {
566
+ cancel('Mission aborted. Returning to menu...');
567
+ agentMode = false;
568
+ prompt = "";
569
+ skipMenu = false;
570
+ continue;
571
+ }
572
+ discoverPath = d;
573
+ }
574
+ }
575
+
576
+ if (agentMode) {
577
+ const dotEnv = parseDotEnvFile(path.join(PROJECT_ROOT, '.env'));
578
+
579
+ // ── API Key and Quota Validation ──
580
+ const { validateApiKey, startMission: requestMission } = await import('../src/arcalityClient.mjs');
581
+
582
+ const validation = await validateApiKey();
583
+ if (!validation.valid) {
584
+ const errorMessages = {
585
+ 'no_api_key': '🔑 API Key not found.\n Configure it via `arcality init` or in .env (ARCALITY_API_KEY)',
586
+ 'invalid_format': '🔑 Invalid API Key. It must start with "arc_k_"',
587
+ 'invalid_api_key': '🔑 API Key not recognized by the server',
588
+ 'plan_expired': '💳 Your plan has expired'
589
+ };
590
+ note(chalk.red(errorMessages[validation.error] || `Error: ${validation.error}`), '❌ Authentication');
591
+ agentMode = false;
592
+ prompt = "";
593
+ skipMenu = false;
594
+ continue;
595
+ }
596
+
597
+ // Show plan info
598
+ const modeLabel = validation.mode === 'mock' ? chalk.gray('(local)') : chalk.green('(live)');
599
+ const dailyLimitDisplay = validation.daily_limit === -1 ? '∞' : (validation.daily_limit || 50);
600
+ const remainingDisplay = validation.remaining >= 999999 ? '∞' : validation.remaining;
601
+ note(
602
+ chalk.bold.white('✅ Valid API Key ') + modeLabel + '\n' +
603
+ chalk.bold.white('📋 Plan: ') + chalk.cyan(validation.plan || 'internal') + '\n' +
604
+ chalk.bold.white('📊 Missions today: ') + chalk.yellow(`${validation.daily_used || 0}/${dailyLimitDisplay}`) + '\n' +
605
+ chalk.bold.white('🎯 Remaining: ') + chalk.green(remainingDisplay),
606
+ 'Arcality Account'
607
+ );
608
+
609
+ // --- RESOLVE PROJECT ID ---
610
+ let selectedProjectId = projectConfig?.projectId || dotEnv.ARCALITY_PROJECT_ID || process.env.ARCALITY_PROJECT_ID;
611
+ const apiBase = getApiUrl();
612
+
613
+ if (!selectedProjectId) {
614
+ // If no project ID from config, try to create/select from backend
615
+ try {
616
+ const projRes = await fetch(`${apiBase}/api/v1/projects`, {
617
+ headers: { 'x-api-key': process.env.ARCALITY_API_KEY || '' }
618
+ });
619
+
620
+ if (projRes.ok) {
621
+ const data = await projRes.json();
622
+ const projects = data.projects || [];
623
+
624
+ const projOptions = projects.map(p => ({ label: p.name || p.Name, value: p.id || p.Id }));
625
+ projOptions.push({ label: '➕ Create new project', value: 'new' });
626
+
627
+ const projSelection = await select({
628
+ message: chalk.cyan('Choose a project for this mission:'),
629
+ options: projOptions
630
+ });
631
+
632
+ if (isCancel(projSelection)) {
633
+ cancel('Mission aborted.');
634
+ agentMode = false; prompt = ""; skipMenu = false; continue;
635
+ }
636
+
637
+ if (projSelection === 'new') {
638
+ const newName = await text({ message: 'Project name:', validate: v => !v ? 'Required' : undefined });
639
+ if (isCancel(newName)) { cancel('Aborted'); agentMode = false; prompt = ""; skipMenu = false; continue; }
640
+
641
+ const createRes = await fetch(`${apiBase}/api/v1/projects`, {
642
+ method: 'POST',
643
+ headers: { 'Content-Type': 'application/json', 'x-api-key': process.env.ARCALITY_API_KEY || '' },
644
+ body: JSON.stringify({
645
+ name: newName,
646
+ base_url: projectConfig?.project?.baseUrl || discoverPath || '/',
647
+ arcality_version: projectConfig?.meta?.arcalityVersion || 'unknown',
648
+ config: { source: 'npm-cli', single_configuration_mode: true, yaml_reuse_enabled: true }
649
+ })
650
+ });
651
+
652
+ if (createRes.ok) {
653
+ const created = await createRes.json();
654
+ selectedProjectId = created.id || created.Id;
655
+ note(chalk.green(`✅ Project created: ${newName}`));
656
+ } else {
657
+ note(chalk.red(`❌ Failed to create project.`));
658
+ }
659
+ } else {
660
+ selectedProjectId = projSelection;
661
+ }
662
+
663
+ if (selectedProjectId) {
664
+ process.env.ARCALITY_PROJECT_ID = selectedProjectId;
665
+ }
666
+ }
667
+ } catch (e) {
668
+ console.log(chalk.yellow(`\n⚠️ Error connecting to project server: ${e.message}. Using default.`));
669
+ }
670
+ } else {
671
+ process.env.ARCALITY_PROJECT_ID = selectedProjectId;
672
+ }
673
+
674
+ // ── Start Mission ──
675
+ const mission = await requestMission(prompt, discoverPath || '/');
676
+ if (!mission.allowed) {
677
+ note(
678
+ chalk.red(`❌ ${mission.error === 'mission_limit_exceeded' ? 'Daily mission limit reached' : mission.error}`) + '\n' +
679
+ chalk.yellow(`📊 Used: ${mission.daily_used}/${mission.daily_limit}`) + '\n' +
680
+ chalk.gray(`🔄 Resets: ${mission.resets_at ? new Date(mission.resets_at).toLocaleString() : 'tomorrow'}`),
681
+ '⚠️ Limit Reached'
682
+ );
683
+ agentMode = false;
684
+ prompt = "";
685
+ skipMenu = false;
686
+ continue;
687
+ }
688
+
689
+ const s = spinner();
690
+ s.start(`🤖 [AGENT MODE] Starting mission: "${prompt}" ${chalk.gray(`(${mission.daily_used}/${mission.daily_limit} today)`)}`);
691
+
692
+ const finalProjectId = process.env.ARCALITY_PROJECT_ID || selectedProjectId;
693
+
694
+ // Build env for the runner — merge arcality.config values
695
+ const mergedEnv = {
696
+ ...dotEnv,
697
+ ...process.env,
698
+ ARCALITY_PROJECT_ID: finalProjectId,
699
+ BASE_URL: projectConfig?.project?.baseUrl || dotEnv.BASE_URL || process.env.BASE_URL,
700
+ LOGIN_USER: projectConfig?.auth?.username || dotEnv.LOGIN_USER || process.env.LOGIN_USER,
701
+ LOGIN_PASSWORD: projectConfig?.auth?.password || dotEnv.LOGIN_PASSWORD || process.env.LOGIN_PASSWORD,
702
+ DOMAIN_DIR: process.env.DOMAIN_DIR,
703
+ MISSIONS_DIR: process.env.MISSIONS_DIR,
704
+ CONTEXT_DIR: process.env.CONTEXT_DIR,
705
+ REPORTS_DIR: process.env.REPORTS_DIR,
706
+ SMART_PROMPT: prompt,
707
+ TARGET_PATH: discoverPath || '/'
708
+ };
709
+
710
+ if (finalProjectId) {
711
+ console.log(chalk.gray(`\n>> ARCALITY_PROJECT_ID: ${finalProjectId}`));
712
+ }
713
+
714
+ try {
715
+ await run("npx", ["playwright", "test", "tests/_helpers/agentic-runner.spec.ts", "--headed"], {
716
+ env: mergedEnv,
717
+ onStatus: (msg) => s.message(msg)
718
+ });
719
+ s.stop(chalk.green('✅ Mission completed successfully.'));
720
+
721
+ // ── End Mission ──
722
+ const { endMission } = await import('../src/arcalityClient.mjs');
723
+ await endMission(mission.mission_id, 'success');
724
+
725
+ // ── Ping Project ──
726
+ await pingProject(finalProjectId, process.env.ARCALITY_API_KEY);
727
+
728
+ // ── Save Mission YAML ──
729
+ const saveDecision = await select({
730
+ message: 'Do you want to save this mission to run it again later?',
731
+ options: [
732
+ { label: '✅ Yes, save as YAML', value: 'yes' },
733
+ { label: '❌ No, thanks', value: 'no' }
734
+ ]
735
+ });
736
+
737
+ if (saveDecision === 'yes') {
738
+ const name = await text({
739
+ message: 'Mission name (e.g., valid_login):',
740
+ placeholder: 'my_mission',
741
+ validate: v => v.length < 3 ? 'Name too short' : undefined
742
+ });
743
+
744
+ if (!isCancel(name)) {
745
+ const safeName = name.trim().replace(/[^a-z0-9]/gi, '_').toLowerCase();
746
+
747
+ // Save to missions dir (existing flow)
748
+ const missionsDir = process.env.MISSIONS_DIR;
749
+ const yamlPathMissions = path.join(missionsDir, `${safeName}.yaml`);
750
+
751
+ let yamlData = `name: "${name}"\nprompt: "${prompt}"\nproject: "${projectConfig?.project?.name || 'Default'}"\ncreated_at: "${new Date().toISOString()}"\n`;
752
+
753
+ const smartYamlPath = path.join(process.env.CONTEXT_DIR, 'last-mission-smart.yaml');
754
+ if (fs.existsSync(smartYamlPath)) {
755
+ yamlData = fs.readFileSync(smartYamlPath, 'utf8');
756
+ }
757
+
758
+ // Internal fallback backup for the agent
759
+ fs.writeFileSync(yamlPathMissions, yamlData);
760
+
761
+ // Primary save to user's specified directory from arcality.config
762
+ if (projectConfig) {
763
+ const yamlOutputDir = ensureYamlOutputDir(projectConfig, ORIGINAL_CWD);
764
+ const yamlPathOutput = path.join(yamlOutputDir, `${safeName}.yaml`);
765
+ fs.writeFileSync(yamlPathOutput, yamlData);
766
+ note(chalk.green(`✅ Mission saved at: ${yamlPathOutput}`), 'Arcality Persistence');
767
+ } else {
768
+ note(chalk.green(`✅ Mission saved at: ${yamlPathMissions}`), 'Arcality Persistence');
769
+ }
770
+ }
771
+ }
772
+ } catch (e) {
773
+ s.stop(chalk.red('❌ Mission could not be completed.'));
774
+
775
+ // End mission with failure
776
+ try {
777
+ const { endMission } = await import('../src/arcalityClient.mjs');
778
+ await endMission(mission.mission_id, 'failed');
779
+ } catch { }
780
+ } finally {
781
+ const sRep = spinner();
782
+ sRep.start('📊 Generating report...');
783
+ await run("node", ["scripts/rebrand-report.mjs"]).catch(() => { });
784
+
785
+ const reportDir = process.env.REPORTS_DIR || 'tests-report';
786
+ const arcalityPath = path.resolve(PROJECT_ROOT, reportDir, 'index.html');
787
+
788
+ if (globalReportProcess) {
789
+ try {
790
+ if (process.platform === "win32") {
791
+ spawn("taskkill", ["/pid", globalReportProcess.pid, "/f", "/t"], { stdio: 'ignore', windowsHide: true });
792
+ } else {
793
+ globalReportProcess.kill();
794
+ }
795
+ } catch (e) { /* ignore */ }
796
+ globalReportProcess = null;
797
+ }
798
+
799
+ console.log(chalk.blue(`🌐 Opening Arcality Report: ${arcalityPath}`));
800
+
801
+ if (process.platform === "win32") {
802
+ exec(`start "" "${arcalityPath}"`);
803
+ } else {
804
+ exec(`open "${arcalityPath}"`);
805
+ }
806
+
807
+ await new Promise(resolve => setTimeout(resolve, 2000));
808
+ sRep.stop(chalk.cyan('Report process initialized.'));
809
+
810
+ // Ping project after report
811
+ await pingProject(
812
+ process.env.ARCALITY_PROJECT_ID,
813
+ process.env.ARCALITY_API_KEY
814
+ );
815
+
816
+ await text({
817
+ message: 'Press Enter to return to menu...',
818
+ placeholder: 'Enter'
819
+ });
820
+
821
+ if (skipMenu) {
822
+ outro(chalk.cyan('Mission finished! See you later.'));
823
+ return;
824
+ }
825
+
826
+ agentMode = false;
827
+ prompt = "";
828
+ firstRun = false;
829
+ }
830
+ }
831
+ firstRun = false;
832
+ }
833
+ }
834
+
835
+ main().catch(err => {
836
+ console.error(err);
837
+ process.exit(1);
838
+ });