@arcadialdev/arcality 2.4.28 → 2.4.30

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 (83) hide show
  1. package/README.md +2 -2
  2. package/package.json +3 -7
  3. package/scripts/gen-and-run.mjs +5 -18
  4. package/src/KnowledgeService.ts +256 -0
  5. package/src/arcalityClient.mjs +2 -1
  6. package/src/consoleBanner.ts +32 -0
  7. package/src/envSetup.ts +205 -0
  8. package/src/index.ts +25 -0
  9. package/src/projectInspector.ts +42 -0
  10. package/src/services/collectiveMemoryService.ts +178 -0
  11. package/src/services/securityScanner.ts +128 -0
  12. package/src/testRunner.ts +201 -0
  13. package/tests/_helpers/agentic-runner.bundle.spec.js +2912 -83
  14. package/.agents/skills/e2e-testing-expert/SKILL.md +0 -28
  15. package/.agents/skills/frontend-design/LICENSE.txt +0 -177
  16. package/.agents/skills/frontend-design/SKILL.md +0 -42
  17. package/.agents/skills/nodejs-backend-patterns/SKILL.md +0 -639
  18. package/.agents/skills/nodejs-backend-patterns/references/advanced-patterns.md +0 -430
  19. package/.agents/skills/playwright-best-practices/LICENSE.md +0 -7
  20. package/.agents/skills/playwright-best-practices/README.md +0 -147
  21. package/.agents/skills/playwright-best-practices/SKILL.md +0 -303
  22. package/.agents/skills/playwright-best-practices/advanced/authentication-flows.md +0 -360
  23. package/.agents/skills/playwright-best-practices/advanced/authentication.md +0 -871
  24. package/.agents/skills/playwright-best-practices/advanced/clock-mocking.md +0 -364
  25. package/.agents/skills/playwright-best-practices/advanced/mobile-testing.md +0 -409
  26. package/.agents/skills/playwright-best-practices/advanced/multi-context.md +0 -288
  27. package/.agents/skills/playwright-best-practices/advanced/multi-user.md +0 -393
  28. package/.agents/skills/playwright-best-practices/advanced/network-advanced.md +0 -452
  29. package/.agents/skills/playwright-best-practices/advanced/third-party.md +0 -464
  30. package/.agents/skills/playwright-best-practices/architecture/pom-vs-fixtures.md +0 -363
  31. package/.agents/skills/playwright-best-practices/architecture/test-architecture.md +0 -369
  32. package/.agents/skills/playwright-best-practices/architecture/when-to-mock.md +0 -383
  33. package/.agents/skills/playwright-best-practices/browser-apis/browser-apis.md +0 -391
  34. package/.agents/skills/playwright-best-practices/browser-apis/iframes.md +0 -403
  35. package/.agents/skills/playwright-best-practices/browser-apis/service-workers.md +0 -504
  36. package/.agents/skills/playwright-best-practices/browser-apis/websockets.md +0 -403
  37. package/.agents/skills/playwright-best-practices/core/annotations.md +0 -424
  38. package/.agents/skills/playwright-best-practices/core/assertions-waiting.md +0 -361
  39. package/.agents/skills/playwright-best-practices/core/configuration.md +0 -452
  40. package/.agents/skills/playwright-best-practices/core/fixtures-hooks.md +0 -417
  41. package/.agents/skills/playwright-best-practices/core/global-setup.md +0 -434
  42. package/.agents/skills/playwright-best-practices/core/locators.md +0 -242
  43. package/.agents/skills/playwright-best-practices/core/page-object-model.md +0 -315
  44. package/.agents/skills/playwright-best-practices/core/projects-dependencies.md +0 -453
  45. package/.agents/skills/playwright-best-practices/core/test-data.md +0 -492
  46. package/.agents/skills/playwright-best-practices/core/test-suite-structure.md +0 -361
  47. package/.agents/skills/playwright-best-practices/core/test-tags.md +0 -298
  48. package/.agents/skills/playwright-best-practices/debugging/console-errors.md +0 -420
  49. package/.agents/skills/playwright-best-practices/debugging/debugging.md +0 -504
  50. package/.agents/skills/playwright-best-practices/debugging/error-testing.md +0 -360
  51. package/.agents/skills/playwright-best-practices/debugging/flaky-tests.md +0 -496
  52. package/.agents/skills/playwright-best-practices/frameworks/angular.md +0 -530
  53. package/.agents/skills/playwright-best-practices/frameworks/nextjs.md +0 -469
  54. package/.agents/skills/playwright-best-practices/frameworks/react.md +0 -531
  55. package/.agents/skills/playwright-best-practices/frameworks/vue.md +0 -574
  56. package/.agents/skills/playwright-best-practices/infrastructure-ci-cd/ci-cd.md +0 -468
  57. package/.agents/skills/playwright-best-practices/infrastructure-ci-cd/docker.md +0 -283
  58. package/.agents/skills/playwright-best-practices/infrastructure-ci-cd/github-actions.md +0 -546
  59. package/.agents/skills/playwright-best-practices/infrastructure-ci-cd/gitlab.md +0 -397
  60. package/.agents/skills/playwright-best-practices/infrastructure-ci-cd/other-providers.md +0 -521
  61. package/.agents/skills/playwright-best-practices/infrastructure-ci-cd/parallel-sharding.md +0 -371
  62. package/.agents/skills/playwright-best-practices/infrastructure-ci-cd/performance.md +0 -453
  63. package/.agents/skills/playwright-best-practices/infrastructure-ci-cd/reporting.md +0 -424
  64. package/.agents/skills/playwright-best-practices/infrastructure-ci-cd/test-coverage.md +0 -497
  65. package/.agents/skills/playwright-best-practices/testing-patterns/accessibility.md +0 -359
  66. package/.agents/skills/playwright-best-practices/testing-patterns/api-testing.md +0 -719
  67. package/.agents/skills/playwright-best-practices/testing-patterns/browser-extensions.md +0 -506
  68. package/.agents/skills/playwright-best-practices/testing-patterns/canvas-webgl.md +0 -493
  69. package/.agents/skills/playwright-best-practices/testing-patterns/component-testing.md +0 -500
  70. package/.agents/skills/playwright-best-practices/testing-patterns/drag-drop.md +0 -576
  71. package/.agents/skills/playwright-best-practices/testing-patterns/electron.md +0 -509
  72. package/.agents/skills/playwright-best-practices/testing-patterns/file-operations.md +0 -377
  73. package/.agents/skills/playwright-best-practices/testing-patterns/file-upload-download.md +0 -562
  74. package/.agents/skills/playwright-best-practices/testing-patterns/forms-validation.md +0 -561
  75. package/.agents/skills/playwright-best-practices/testing-patterns/graphql-testing.md +0 -331
  76. package/.agents/skills/playwright-best-practices/testing-patterns/i18n.md +0 -508
  77. package/.agents/skills/playwright-best-practices/testing-patterns/performance-testing.md +0 -476
  78. package/.agents/skills/playwright-best-practices/testing-patterns/security-testing.md +0 -430
  79. package/.agents/skills/playwright-best-practices/testing-patterns/visual-regression.md +0 -634
  80. package/.agents/skills/security-qa/SKILL.md +0 -254
  81. package/public/ArcalityImagen.png +0 -0
  82. package/public/logo-arcadial-blanco.png +0 -0
  83. package/public/logo.png +0 -0
package/README.md CHANGED
@@ -1,9 +1,9 @@
1
1
  <div align="center">
2
- <img src="https://unpkg.com/@arcadialdev/arcality/public/logo-arcadial-blanco.png" width="120" alt="Arcality Logo" />
2
+ <img src="./public/logo-arcadial-blanco.png" width="120" alt="Arcality Logo" />
3
3
  <br>
4
4
  <h1>Arcality Engine</h1>
5
5
  <p><b>Autonomous AI-Powered E2E Web Testing Agent</b></p>
6
-
6
+
7
7
  [![NPM Version](https://img.shields.io/npm/v/@arcadialdev/arcality?color=c084fc&label=NPM&style=for-the-badge)](https://www.npmjs.com/package/@arcadialdev/arcality)
8
8
  </div>
9
9
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arcadialdev/arcality",
3
- "version": "2.4.28",
3
+ "version": "2.4.30",
4
4
  "description": "AI-powered QA testing tool — Autonomous web testing agent by Arcadial",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -8,7 +8,7 @@
8
8
  "arcality": "node scripts/gen-and-run.mjs",
9
9
  "postinstall": "node scripts/postinstall.mjs",
10
10
  "setup": "node scripts/setup.mjs",
11
- "build:runner": "npx esbuild tests/_helpers/agentic-runner.spec.ts --bundle --minify --platform=node --target=node18 --format=cjs --external:@playwright/test --external:chalk --external:dotenv --external:node-fetch --external:js-yaml --external:axios --external:crypto --external:fs --external:path --outfile=tests/_helpers/agentic-runner.bundle.spec.js",
11
+ "build:runner": "npx esbuild tests/_helpers/agentic-runner.spec.ts --bundle --platform=node --target=node18 --format=cjs --external:@playwright/test --external:chalk --external:dotenv --external:node-fetch --external:js-yaml --external:axios --external:crypto --external:fs --external:path --outfile=tests/_helpers/agentic-runner.bundle.spec.js",
12
12
  "prepublishOnly": "npm run build:runner"
13
13
  },
14
14
  "bin": {
@@ -17,13 +17,9 @@
17
17
  "files": [
18
18
  "bin/",
19
19
  "scripts/",
20
- "src/configLoader.mjs",
21
- "src/configManager.mjs",
22
- "src/arcalityClient.mjs",
20
+ "src/",
23
21
  "tests/_helpers/agentic-runner.bundle.spec.js",
24
22
  "tests/_helpers/ArcalityReporter.js",
25
- ".agents/",
26
- "public/",
27
23
  "playwright.config.js",
28
24
  "README.md"
29
25
  ],
@@ -428,10 +428,6 @@ async function main() {
428
428
  let firstRun = true;
429
429
  let globalReportProcess = null;
430
430
 
431
- // Immutable flag: true when started from `arcality run` (--agent flag on CLI).
432
- // Used to exit cleanly instead of showing the interactive menu on completion or errors.
433
- const CLI_AGENT_MODE = initialAgentMode;
434
-
435
431
  setupEnvironment();
436
432
 
437
433
  while (true) {
@@ -632,10 +628,6 @@ async function main() {
632
628
  'plan_expired': '💳 Your plan has expired'
633
629
  };
634
630
  note(chalk.red(errorMessages[validation.error] || `Error: ${validation.error}`), '❌ Authentication');
635
- if (CLI_AGENT_MODE) {
636
- outro(chalk.red('❌ Authentication failed. Exiting.'));
637
- process.exit(1);
638
- }
639
631
  agentMode = false;
640
632
  prompt = "";
641
633
  skipMenu = false;
@@ -728,10 +720,6 @@ async function main() {
728
720
  chalk.gray(`🔄 Resets: ${mission.resets_at ? new Date(mission.resets_at).toLocaleString() : 'tomorrow'}`),
729
721
  '⚠️ Limit Reached'
730
722
  );
731
- if (CLI_AGENT_MODE) {
732
- outro(chalk.red('❌ Mission limit reached. Exiting.'));
733
- process.exit(1);
734
- }
735
723
  agentMode = false;
736
724
  prompt = "";
737
725
  skipMenu = false;
@@ -919,17 +907,16 @@ async function main() {
919
907
  process.env.ARCALITY_API_KEY
920
908
  );
921
909
 
922
- // In CLI agent mode (arcality run), exit immediately — no interactive menu.
923
- if (CLI_AGENT_MODE) {
924
- outro(chalk.cyan('✅ Mission finished! See you later.'));
925
- return;
926
- }
927
-
928
910
  await text({
929
911
  message: 'Press Enter to return to menu...',
930
912
  placeholder: 'Enter'
931
913
  });
932
914
 
915
+ if (skipMenu) {
916
+ outro(chalk.cyan('Mission finished! See you later.'));
917
+ return;
918
+ }
919
+
933
920
  agentMode = false;
934
921
  prompt = "";
935
922
  firstRun = false;
@@ -0,0 +1,256 @@
1
+
2
+ import * as crypto from 'crypto';
3
+ import chalk from 'chalk';
4
+
5
+ /**
6
+ * KnowledgeService (SaaS v2)
7
+ * Servicio centralizado para alimentar y consumir la base de conocimiento persistente de Arcality.
8
+ * Configurado para usar SNAKE_CASE según la especificación del servidor.
9
+ */
10
+
11
+ export interface PortalIngestRequest {
12
+ project_id: string;
13
+ target_url: string;
14
+ page_title?: string;
15
+ dom_hash: string;
16
+ viewport?: { width: number; height: number };
17
+ components: Array<{
18
+ tag: string;
19
+ component_type: "ACTION" | "FIELD" | "INFO" | "NAVIGATION" | "DATA";
20
+ semantic_label: string;
21
+ text_content?: string;
22
+ is_interactive: boolean;
23
+ attributes?: any;
24
+ bounding_rect?: { x: number; y: number; width: number; height: number };
25
+ }>;
26
+ }
27
+
28
+ export interface PortalContextResponse {
29
+ rules: Array<{ title: string; description: string; severity: string }>;
30
+ fields: Array<{ field_identifier: string; field_type: string; is_required: boolean }>;
31
+ }
32
+
33
+ export interface ProjectDto {
34
+ id: string;
35
+ name: string;
36
+ base_url?: string;
37
+ }
38
+
39
+ export class KnowledgeService {
40
+ private static instance: KnowledgeService;
41
+ private apiBase: string;
42
+ private apiKey: string;
43
+ private projectId: string | null = null;
44
+
45
+ private constructor() {
46
+ // API URL is internal-only, loaded from the tool's .env via dotenv
47
+ this.apiBase = process.env.ARCALITY_API_URL || 'https://arcalityqadev.arcadial.lat';
48
+ this.apiKey = process.env.ARCALITY_API_KEY || '';
49
+ this.projectId = process.env.ARCALITY_PROJECT_ID || null;
50
+ }
51
+
52
+ /**
53
+ * Fetch wrapper with a 10-second timeout to prevent hanging.
54
+ * All KnowledgeService calls are telemetry/secondary — they must NEVER block the agent.
55
+ */
56
+ private async fetchWithTimeout(url: string, options: RequestInit = {}, timeoutMs = 10000): Promise<Response> {
57
+ const controller = new AbortController();
58
+ const id = setTimeout(() => controller.abort(), timeoutMs);
59
+ try {
60
+ const res = await fetch(url, { ...options, signal: controller.signal });
61
+ clearTimeout(id);
62
+ return res;
63
+ } catch (err: any) {
64
+ clearTimeout(id);
65
+ throw err;
66
+ }
67
+ }
68
+
69
+ public static getInstance(): KnowledgeService {
70
+ if (!KnowledgeService.instance) {
71
+ KnowledgeService.instance = new KnowledgeService();
72
+ }
73
+ return KnowledgeService.instance;
74
+ }
75
+
76
+ public setProjectId(id: string) {
77
+ this.projectId = id;
78
+ }
79
+
80
+ public getProjectId(): string | null {
81
+ if (!this.projectId || this.projectId === 'undefined' || this.projectId === 'null') {
82
+ this.projectId = process.env.ARCALITY_PROJECT_ID || null;
83
+ }
84
+ return this.projectId;
85
+ }
86
+
87
+ /**
88
+ * 1. Gestión de Proyectos
89
+ */
90
+ public async getProjects(): Promise<ProjectDto[]> {
91
+ try {
92
+ const res = await this.fetchWithTimeout(`${this.apiBase}/api/v1/projects`, {
93
+ headers: { 'x-api-key': this.apiKey }
94
+ });
95
+ if (!res.ok) return [];
96
+ const data = await res.json();
97
+ return data.projects || [];
98
+ } catch (e: any) {
99
+ console.warn(chalk.yellow(`[KnowledgeService] Error al obtener proyectos: ${e.message}`));
100
+ return [];
101
+ }
102
+ }
103
+
104
+ public async createProject(name: string, baseUrl: string): Promise<ProjectDto | null> {
105
+ try {
106
+ const res = await this.fetchWithTimeout(`${this.apiBase}/api/v1/projects`, {
107
+ method: 'POST',
108
+ headers: {
109
+ 'Content-Type': 'application/json',
110
+ 'x-api-key': this.apiKey
111
+ },
112
+ body: JSON.stringify({ name, base_url: baseUrl })
113
+ });
114
+ if (!res.ok) return null;
115
+ return await res.json();
116
+ } catch (e: any) {
117
+ console.error(chalk.red(`[KnowledgeService] Falló creación de proyecto: ${e.message}`));
118
+ return null;
119
+ }
120
+ }
121
+
122
+ /**
123
+ * 2. Ingesta (Post-Percept)
124
+ */
125
+ public async ingest(payload: PortalIngestRequest): Promise<void> {
126
+ if (!payload.project_id || payload.project_id === 'undefined') {
127
+ console.warn(chalk.yellow(`[Knowledge] Ingesta cancelada: project_id es inválido.`));
128
+ return;
129
+ }
130
+
131
+ try {
132
+ const res = await this.fetchWithTimeout(`${this.apiBase}/api/v1/portal/ingest`, {
133
+ method: 'POST',
134
+ headers: {
135
+ 'Content-Type': 'application/json',
136
+ 'x-api-key': this.apiKey
137
+ },
138
+ body: JSON.stringify(payload)
139
+ });
140
+ if (res.status === 200) {
141
+ const data = await res.json();
142
+ if (data.status === 'skipped_duplicate_dom') {
143
+ // console.log(chalk.gray(`[Knowledge] Ingesta omitida (Duplicado).`));
144
+ } else {
145
+ console.log(chalk.cyan(`[Knowledge] Ingesta exitosa (Project: ${payload.project_id})`));
146
+ }
147
+ }
148
+ } catch (e: any) {
149
+ console.warn(chalk.yellow(`[Knowledge] Error en ingesta: ${e.message}`));
150
+ }
151
+ }
152
+
153
+ /**
154
+ * 3. Contexto (Pre-Reasoning)
155
+ */
156
+ public async getContext(url: string): Promise<PortalContextResponse | null> {
157
+ const pid = this.getProjectId();
158
+ if (!pid || pid === 'undefined') return null;
159
+ try {
160
+ const pathUrl = new URL(url).pathname;
161
+ const res = await this.fetchWithTimeout(
162
+ `${this.apiBase}/api/v1/portal/context?project_id=${pid}&path=${encodeURIComponent(pathUrl)}`,
163
+ { headers: { 'x-api-key': this.apiKey } }
164
+ );
165
+ if (!res.ok) return null;
166
+ return await res.json();
167
+ } catch (e: any) {
168
+ console.warn(chalk.yellow(`[Knowledge] Error al obtener contexto: ${e.message}`));
169
+ return null;
170
+ }
171
+ }
172
+
173
+ /**
174
+ * 4. Aprendizaje (portal_domain_rules)
175
+ */
176
+ public async saveRule(title: string, description: string, severity: string = 'important', pageUrl?: string): Promise<void> {
177
+ const pid = this.getProjectId();
178
+ if (!pid || pid === 'undefined') return;
179
+ try {
180
+ const pathUrl = pageUrl ? new URL(pageUrl).pathname : null;
181
+ await fetch(`${this.apiBase}/api/v1/portal/rules`, {
182
+ method: 'POST',
183
+ headers: {
184
+ 'Content-Type': 'application/json',
185
+ 'x-api-key': this.apiKey
186
+ },
187
+ body: JSON.stringify({
188
+ project_id: pid,
189
+ rule_type: "UI_UX", // Default según especificación
190
+ title,
191
+ description,
192
+ severity
193
+ })
194
+ });
195
+ console.log(chalk.green(`[Knowledge] Nueva regla registrada: ${title}`));
196
+ } catch (e) { }
197
+ }
198
+
199
+ /**
200
+ * 5. Catálogo de Campos (portal_field_catalog)
201
+ */
202
+ public async saveField(identifier: string, type: string, isRequired: boolean): Promise<void> {
203
+ const pid = this.getProjectId();
204
+ if (!pid || pid === 'undefined') return;
205
+ try {
206
+ await fetch(`${this.apiBase}/api/v1/portal/fields`, {
207
+ method: 'POST',
208
+ headers: {
209
+ 'Content-Type': 'application/json',
210
+ 'x-api-key': this.apiKey
211
+ },
212
+ body: JSON.stringify({
213
+ project_id: pid,
214
+ field_identifier: identifier,
215
+ field_type: type,
216
+ is_required: isRequired,
217
+ source: "agent_auto"
218
+ })
219
+ });
220
+ console.log(chalk.green(`[Knowledge] Nuevo campo catalogado: ${identifier}`));
221
+ } catch (e) { }
222
+ }
223
+
224
+ /**
225
+ * 6. Conocimiento General (Documentación)
226
+ */
227
+ public async saveKnowledge(title: string, content: string, docType: string = 'PROCESS'): Promise<void> {
228
+ const pid = this.getProjectId();
229
+ if (!pid || pid === 'undefined') return;
230
+ try {
231
+ await fetch(`${this.apiBase}/api/v1/portal/knowledge`, {
232
+ method: 'POST',
233
+ headers: {
234
+ 'Content-Type': 'application/json',
235
+ 'x-api-key': this.apiKey
236
+ },
237
+ body: JSON.stringify({
238
+ project_id: pid,
239
+ doc_type: docType,
240
+ title,
241
+ content,
242
+ source: "agent_auto"
243
+ })
244
+ });
245
+ console.log(chalk.green(`[Knowledge] Nuevo conocimiento guardado: ${title}`));
246
+ } catch (e) { }
247
+ }
248
+
249
+ /**
250
+ * Utilidad: Calcular Hash para Ingesta
251
+ */
252
+ public calculateDomHash(components: any[]): string {
253
+ const fingerprint = components.map(c => `${c.tag}-${c.semanticLabel}`).join('|');
254
+ return crypto.createHash('sha256').update(fingerprint).digest('hex');
255
+ }
256
+ }
@@ -161,7 +161,7 @@ export async function validateApiKey() {
161
161
 
162
162
  /**
163
163
  * Requests to start a mission.
164
- * @param {string} prompt - The mission prompt (only a hash is sent, never the raw text)
164
+ * @param {string} prompt - The mission prompt
165
165
  * @param {string} targetUrl - Target portal URL
166
166
  */
167
167
  export async function startMission(prompt, targetUrl) {
@@ -182,6 +182,7 @@ export async function startMission(prompt, targetUrl) {
182
182
  'x-api-key': key
183
183
  },
184
184
  body: JSON.stringify({
185
+ prompt: prompt,
185
186
  prompt_hash: simpleHash(prompt),
186
187
  target_url: targetUrl,
187
188
  project_id: projectId || undefined,
@@ -0,0 +1,32 @@
1
+ import figlet from 'figlet';
2
+ import chalk from 'chalk';
3
+ import fs from 'fs';
4
+ import path from 'path';
5
+ import { intro, note } from '@clack/prompts';
6
+
7
+ export function showBanner(): void {
8
+ const projectName = 'Arcality';
9
+ const subtitle = 'Powered by Arcadial';
10
+
11
+ let version = 'unknown';
12
+ try {
13
+ const pkg = JSON.parse(fs.readFileSync(path.join(process.cwd(), 'package.json'), 'utf8'));
14
+ version = pkg.version || 'unknown';
15
+ } catch { }
16
+
17
+ const logo = figlet.textSync(projectName, {
18
+ font: 'Standard',
19
+ horizontalLayout: 'default',
20
+ verticalLayout: 'default',
21
+ });
22
+
23
+ intro(chalk.cyanBright(logo));
24
+
25
+ note(
26
+ chalk.magentaBright(subtitle) + '\n' +
27
+ chalk.gray('─'.repeat(50)) + '\n' +
28
+ chalk.bold.white('📦 Version: ') + chalk.yellow(version) + '\n' +
29
+ chalk.bold.white('⚙️ Environment: ') + chalk.yellow(process.env.NODE_ENV ?? 'development'),
30
+ 'System Information'
31
+ );
32
+ }
@@ -0,0 +1,205 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import prompts from 'prompts';
4
+
5
+ export class UserCancelledError extends Error {
6
+ constructor(message = 'Usuario canceló la operación') {
7
+ super(message);
8
+ this.name = 'UserCancelledError';
9
+ }
10
+ }
11
+
12
+ type EnvValues = {
13
+ BASE_URL?: string;
14
+ LOGIN_USER?: string;
15
+ LOGIN_PASSWORD?: string;
16
+ ARCALITY_PROJECT_ID?: string;
17
+ };
18
+
19
+ interface ArcalityConfigFile {
20
+ apiKey?: string;
21
+ projectId?: string;
22
+ project?: {
23
+ name?: string;
24
+ baseUrl?: string;
25
+ frameworkDetected?: string;
26
+ };
27
+ auth?: {
28
+ username?: string;
29
+ password?: string;
30
+ };
31
+ runtime?: {
32
+ yamlOutputDir?: string;
33
+ reuseSuccessfulYamls?: boolean;
34
+ singleConfigurationMode?: boolean;
35
+ };
36
+ }
37
+
38
+ function envPath(): string {
39
+ return path.join(process.cwd(), '.env');
40
+ }
41
+
42
+ function arcalityConfigPath(): string {
43
+ return path.join(process.cwd(), 'arcality.config');
44
+ }
45
+
46
+ function escapeEnvValue(v: string): string {
47
+ const needsQuotes = /[\s"'`\\]/.test(v);
48
+ const cleaned = v.replace(/"/g, '\\"');
49
+ return needsQuotes ? `"${cleaned}"` : cleaned;
50
+ }
51
+
52
+ function parseDotEnv(content: string): Record<string, string> {
53
+ const out: Record<string, string> = {};
54
+ for (const line of content.split(/\r?\n/)) {
55
+ const trimmed = line.trim();
56
+ if (!trimmed || trimmed.startsWith('#')) continue;
57
+
58
+ const idx = trimmed.indexOf('=');
59
+ if (idx === -1) continue;
60
+
61
+ const k = trimmed.slice(0, idx).trim();
62
+ let v = trimmed.slice(idx + 1).trim();
63
+
64
+ if (
65
+ (v.startsWith('"') && v.endsWith('"')) ||
66
+ (v.startsWith("'") && v.endsWith("'"))
67
+ ) {
68
+ v = v.slice(1, -1);
69
+ }
70
+ out[k] = v;
71
+ }
72
+ return out;
73
+ }
74
+
75
+ export function readExistingEnv(): Record<string, string> {
76
+ const p = envPath();
77
+ if (!fs.existsSync(p)) return {};
78
+ return parseDotEnv(fs.readFileSync(p, 'utf8'));
79
+ }
80
+
81
+ /**
82
+ * Loads arcality.config if it exists.
83
+ */
84
+ function loadArcalityConfig(): ArcalityConfigFile | null {
85
+ const p = arcalityConfigPath();
86
+ try {
87
+ if (!fs.existsSync(p)) return null;
88
+ let raw = fs.readFileSync(p, 'utf8');
89
+ if (raw.charCodeAt(0) === 0xFEFF) raw = raw.slice(1);
90
+ return JSON.parse(raw);
91
+ } catch {
92
+ return null;
93
+ }
94
+ }
95
+
96
+ function writeEnv(values: EnvValues): void {
97
+ const existing = readExistingEnv();
98
+ const merged = { ...existing, ...values };
99
+
100
+ const lines = [
101
+ '# Auto-generado por Arcality testRunner',
102
+ `BASE_URL=${escapeEnvValue(merged.BASE_URL ?? '')}`,
103
+ `LOGIN_USER=${escapeEnvValue(merged.LOGIN_USER ?? '')}`,
104
+ `LOGIN_PASSWORD=${escapeEnvValue(merged.LOGIN_PASSWORD ?? '')}`,
105
+ ];
106
+
107
+ if (merged.ARCALITY_PROJECT_ID) {
108
+ lines.push(`ARCALITY_PROJECT_ID=${escapeEnvValue(merged.ARCALITY_PROJECT_ID)}`);
109
+ }
110
+ lines.push('');
111
+
112
+ fs.writeFileSync(envPath(), lines.join('\n'), 'utf8');
113
+ }
114
+
115
+ function isValidBaseUrl(url: string): boolean {
116
+ try {
117
+ const u = new URL(url);
118
+ return u.protocol === 'http:' || u.protocol === 'https:';
119
+ } catch {
120
+ return false;
121
+ }
122
+ }
123
+
124
+ /**
125
+ * Ensures environment is configured for test execution.
126
+ * In single-config mode, reads from arcality.config first.
127
+ * Falls back to .env prompts for backward compatibility.
128
+ */
129
+ export async function ensureEnvInteractive(): Promise<void> {
130
+ // Try to load arcality.config first
131
+ const arcalityConfig = loadArcalityConfig();
132
+
133
+ if (arcalityConfig) {
134
+ // Inject config values into process.env for the current session
135
+ if (arcalityConfig.project?.baseUrl) {
136
+ process.env.BASE_URL = arcalityConfig.project.baseUrl;
137
+ }
138
+ if (arcalityConfig.auth?.username) {
139
+ process.env.LOGIN_USER = arcalityConfig.auth.username;
140
+ }
141
+ if (arcalityConfig.auth?.password) {
142
+ process.env.LOGIN_PASSWORD = arcalityConfig.auth.password;
143
+ }
144
+ if (arcalityConfig.projectId) {
145
+ process.env.ARCALITY_PROJECT_ID = arcalityConfig.projectId;
146
+ }
147
+ if (arcalityConfig.apiKey) {
148
+ process.env.ARCALITY_API_KEY = arcalityConfig.apiKey;
149
+ }
150
+
151
+ // All good — no prompts needed
152
+ return;
153
+ }
154
+
155
+ // Fallback: prompt for missing .env values (legacy mode)
156
+ const existing = readExistingEnv();
157
+ const questions: prompts.PromptObject[] = [];
158
+
159
+ if (!existing.BASE_URL) {
160
+ questions.push({
161
+ type: 'text',
162
+ name: 'BASE_URL',
163
+ message: 'Base URL (ej: http://localhost:3000):',
164
+ initial: 'http://localhost:3000',
165
+ validate: (v: string) =>
166
+ isValidBaseUrl(v) ? true : 'URL inválida (usa http/https)',
167
+ });
168
+ }
169
+
170
+ if (!existing.LOGIN_USER) {
171
+ questions.push({
172
+ type: 'text',
173
+ name: 'LOGIN_USER',
174
+ message: 'Usuario:',
175
+ validate: (v: string) =>
176
+ v?.trim().length ? true : 'Usuario requerido',
177
+ });
178
+ }
179
+
180
+ if (!existing.LOGIN_PASSWORD) {
181
+ questions.push({
182
+ type: 'password',
183
+ name: 'LOGIN_PASSWORD',
184
+ message: 'Contraseña:',
185
+ validate: (v: string) =>
186
+ v?.length ? true : 'Contraseña requerida',
187
+ });
188
+ }
189
+
190
+ if (questions.length > 0) {
191
+ const res = await prompts(questions, {
192
+ onCancel: () => {
193
+ throw new UserCancelledError();
194
+ },
195
+ });
196
+
197
+ const merged: EnvValues = {
198
+ BASE_URL: res.BASE_URL ?? existing.BASE_URL,
199
+ LOGIN_USER: res.LOGIN_USER ?? existing.LOGIN_USER,
200
+ LOGIN_PASSWORD: res.LOGIN_PASSWORD ?? existing.LOGIN_PASSWORD,
201
+ };
202
+ writeEnv(merged);
203
+ console.log('Archivo .env actualizado.');
204
+ }
205
+ }
package/src/index.ts ADDED
@@ -0,0 +1,25 @@
1
+ import 'dotenv/config';
2
+ import { showBanner } from './consoleBanner';
3
+ import { promptAndRunPlaywrightTests } from './testRunner';
4
+ import { KnowledgeService } from './KnowledgeService';
5
+ import { ArcalityClient } from './arcalityClient';
6
+ import { loadConfig } from './configLoader';
7
+
8
+ async function main() {
9
+ showBanner();
10
+
11
+ const config = loadConfig();
12
+ const arcalityClient = new ArcalityClient(config.ARCALITY_API_KEY);
13
+ const knowledgeService = KnowledgeService.getInstance();
14
+
15
+ await promptAndRunPlaywrightTests(arcalityClient, knowledgeService);
16
+ }
17
+
18
+ main().catch((err) => {
19
+ // Normal user cancellation is not an error
20
+ if (err instanceof Error && err.name === 'UserCancelledError') {
21
+ process.exit(130);
22
+ }
23
+ console.error(err);
24
+ process.exit(1);
25
+ });
@@ -0,0 +1,42 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ export interface ProjectContext {
5
+ name: string;
6
+ version: string;
7
+ framework: 'next' | 'vite' | 'cra' | 'unknown';
8
+ scripts: string[];
9
+ dependencies: string[];
10
+ }
11
+
12
+ export function getProjectContext(projectRoot: string): ProjectContext {
13
+ const pkgPath = path.join(projectRoot, 'package.json');
14
+ const context: ProjectContext = {
15
+ name: 'Arcality Project',
16
+ version: '1.0.0',
17
+ framework: 'unknown',
18
+ scripts: [],
19
+ dependencies: []
20
+ };
21
+
22
+ if (!fs.existsSync(pkgPath)) return context;
23
+
24
+ try {
25
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
26
+ context.name = pkg.name || context.name;
27
+ context.version = pkg.version || context.version;
28
+
29
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
30
+ context.dependencies = Object.keys(deps);
31
+ context.scripts = pkg.scripts ? Object.keys(pkg.scripts) : [];
32
+
33
+ if (deps['next']) context.framework = 'next';
34
+ else if (deps['vite']) context.framework = 'vite';
35
+ else if (deps['react-scripts']) context.framework = 'cra';
36
+
37
+ } catch (e) {
38
+ // Ignorar errores de lectura
39
+ }
40
+
41
+ return context;
42
+ }