@arcadialdev/arcality 2.4.28 → 2.4.29
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/README.md +2 -2
- package/package.json +4 -7
- package/scripts/gen-and-run.mjs +5 -18
- package/src/KnowledgeService.ts +256 -0
- package/src/consoleBanner.ts +32 -0
- package/src/envSetup.ts +205 -0
- package/src/index.ts +25 -0
- package/src/projectInspector.ts +42 -0
- package/src/services/collectiveMemoryService.ts +178 -0
- package/src/services/securityScanner.ts +128 -0
- package/src/testRunner.ts +201 -0
- package/tests/_helpers/ArcalityReporter.ts +733 -0
- package/tests/_helpers/agentic-runner.bundle.spec.js +2912 -83
- package/tests/_helpers/agentic-runner.spec.ts +783 -0
- package/tests/_helpers/ai-agent-helper.ts +1660 -0
- package/tests/_helpers/discover-view.spec.ts +238 -0
- package/tests/_helpers/extract-view.spec.ts +118 -0
- package/tests/_helpers/qa-security-tools.ts +265 -0
- package/tests/_helpers/qa-tools.ts +333 -0
- package/tests/_helpers/smart-action.spec.ts +1458 -0
package/README.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
<div align="center">
|
|
2
|
-
<img src="
|
|
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
|
[](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.
|
|
3
|
+
"version": "2.4.29",
|
|
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 --
|
|
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,11 +17,8 @@
|
|
|
17
17
|
"files": [
|
|
18
18
|
"bin/",
|
|
19
19
|
"scripts/",
|
|
20
|
-
"src/
|
|
21
|
-
"
|
|
22
|
-
"src/arcalityClient.mjs",
|
|
23
|
-
"tests/_helpers/agentic-runner.bundle.spec.js",
|
|
24
|
-
"tests/_helpers/ArcalityReporter.js",
|
|
20
|
+
"src/",
|
|
21
|
+
"tests/_helpers/",
|
|
25
22
|
".agents/",
|
|
26
23
|
"public/",
|
|
27
24
|
"playwright.config.js",
|
package/scripts/gen-and-run.mjs
CHANGED
|
@@ -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
|
+
}
|
|
@@ -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
|
+
}
|
package/src/envSetup.ts
ADDED
|
@@ -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
|
+
}
|