@arcadialdev/arcality 4.1.0 → 4.1.1

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 (36) hide show
  1. package/.agents/skills/db-validation-evidence/SKILL.md +43 -0
  2. package/.agents/skills/db-validation-evidence/agents/openai.yaml +4 -0
  3. package/.agents/skills/db-validation-evidence/references/mission-patterns.md +130 -0
  4. package/.agents/skills/evidence-synthesis-reporting/SKILL.md +31 -0
  5. package/.agents/skills/form-expert/SKILL.md +98 -0
  6. package/.agents/skills/investigation-protocol/SKILL.md +56 -0
  7. package/.agents/skills/mission-generation-reviewer/SKILL.md +25 -0
  8. package/.agents/skills/modal-master/SKILL.md +46 -0
  9. package/.agents/skills/native-control-expert/SKILL.md +74 -0
  10. package/.agents/skills/qa-context-governance/SKILL.md +23 -0
  11. package/.agents/skills/security-evidence-triage/SKILL.md +23 -0
  12. package/.agents/skills/test-data-lifecycle/SKILL.md +24 -0
  13. package/README.md +103 -163
  14. package/bin/arcality.mjs +25 -25
  15. package/package.json +75 -75
  16. package/scripts/edit-config.mjs +843 -0
  17. package/scripts/gen-and-run.mjs +237 -169
  18. package/scripts/generate.mjs +236 -236
  19. package/scripts/init.mjs +47 -47
  20. package/src/configManager.mjs +13 -13
  21. package/src/envSetup.ts +229 -205
  22. package/src/services/codebaseAnalyzer.mjs +59 -59
  23. package/src/services/databaseValidationService.mjs +598 -0
  24. package/src/services/executionEvidenceService.mjs +124 -0
  25. package/src/services/generatedMissionSchema.mjs +76 -76
  26. package/src/services/generatedMissionStore.mjs +117 -117
  27. package/src/services/generationContext.mjs +242 -242
  28. package/src/services/mcpStdioClient.mjs +204 -0
  29. package/src/services/missionGenerator.mjs +329 -329
  30. package/src/services/routeDiscovery.mjs +762 -762
  31. package/tests/_helpers/ArcalityReporter.js +1342 -1255
  32. package/tests/_helpers/agentic-runner.bundle.spec.js +1354 -243
  33. package/.agent/skills/form-expert.md +0 -102
  34. package/.agent/skills/investigation-protocol.md +0 -61
  35. package/.agent/skills/modal-master.md +0 -41
  36. package/.agent/skills/native-control-expert.md +0 -82
package/src/envSetup.ts CHANGED
@@ -1,205 +1,229 @@
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
- }
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
+ function updateEnvFilePreservingEntries(updates: Record<string, string>): void {
82
+ const p = envPath();
83
+ const raw = fs.existsSync(p) ? fs.readFileSync(p, 'utf8') : '';
84
+ const lines = raw ? raw.split(/\r?\n/) : ['# Local user-managed environment', ''];
85
+ const keyToLine = new Map<string, number>();
86
+
87
+ lines.forEach((line, index) => {
88
+ const idx = line.indexOf('=');
89
+ if (idx === -1) return;
90
+ const key = line.slice(0, idx).trim();
91
+ if (key && !key.startsWith('#')) keyToLine.set(key, index);
92
+ });
93
+
94
+ for (const [key, value] of Object.entries(updates)) {
95
+ const rendered = `${key}=${escapeEnvValue(value)}`;
96
+ if (keyToLine.has(key)) {
97
+ lines[keyToLine.get(key)!] = rendered;
98
+ } else {
99
+ if (lines.length > 0 && lines[lines.length - 1] !== '') lines.push('');
100
+ lines.push(rendered);
101
+ keyToLine.set(key, lines.length - 1);
102
+ }
103
+ }
104
+
105
+ if (lines[lines.length - 1] !== '') lines.push('');
106
+ fs.writeFileSync(p, lines.join('\n'), 'utf8');
107
+ }
108
+ /**
109
+ * Loads arcality.config if it exists.
110
+ */
111
+ function loadArcalityConfig(): ArcalityConfigFile | null {
112
+ const p = arcalityConfigPath();
113
+ try {
114
+ if (!fs.existsSync(p)) return null;
115
+ let raw = fs.readFileSync(p, 'utf8');
116
+ if (raw.charCodeAt(0) === 0xFEFF) raw = raw.slice(1);
117
+ return JSON.parse(raw);
118
+ } catch {
119
+ return null;
120
+ }
121
+ }
122
+
123
+ function writeEnv(values: EnvValues): void {
124
+ const existing = readExistingEnv();
125
+ const merged = { ...existing, ...values };
126
+ const updates: Record<string, string> = {
127
+ BASE_URL: merged.BASE_URL ?? '',
128
+ LOGIN_USER: merged.LOGIN_USER ?? '',
129
+ LOGIN_PASSWORD: merged.LOGIN_PASSWORD ?? ''
130
+ };
131
+
132
+ if (merged.ARCALITY_PROJECT_ID) {
133
+ updates.ARCALITY_PROJECT_ID = merged.ARCALITY_PROJECT_ID;
134
+ }
135
+
136
+ updateEnvFilePreservingEntries(updates);
137
+ }
138
+
139
+ function isValidBaseUrl(url: string): boolean {
140
+ try {
141
+ const u = new URL(url);
142
+ return u.protocol === 'http:' || u.protocol === 'https:';
143
+ } catch {
144
+ return false;
145
+ }
146
+ }
147
+
148
+ /**
149
+ * Ensures environment is configured for test execution.
150
+ * In single-config mode, reads from arcality.config first.
151
+ * Falls back to .env prompts for backward compatibility.
152
+ */
153
+ export async function ensureEnvInteractive(): Promise<void> {
154
+ // Try to load arcality.config first
155
+ const arcalityConfig = loadArcalityConfig();
156
+
157
+ if (arcalityConfig) {
158
+ // Inject config values into process.env for the current session
159
+ if (arcalityConfig.project?.baseUrl) {
160
+ process.env.BASE_URL = arcalityConfig.project.baseUrl;
161
+ }
162
+ if (arcalityConfig.auth?.username) {
163
+ process.env.LOGIN_USER = arcalityConfig.auth.username;
164
+ }
165
+ if (arcalityConfig.auth?.password) {
166
+ process.env.LOGIN_PASSWORD = arcalityConfig.auth.password;
167
+ }
168
+ if (arcalityConfig.projectId) {
169
+ process.env.ARCALITY_PROJECT_ID = arcalityConfig.projectId;
170
+ }
171
+ if (arcalityConfig.apiKey) {
172
+ process.env.ARCALITY_API_KEY = arcalityConfig.apiKey;
173
+ }
174
+
175
+ // All good - no prompts needed
176
+ return;
177
+ }
178
+
179
+ // Fallback: prompt for missing .env values (legacy mode)
180
+ const existing = readExistingEnv();
181
+ const questions: prompts.PromptObject[] = [];
182
+
183
+ if (!existing.BASE_URL) {
184
+ questions.push({
185
+ type: 'text',
186
+ name: 'BASE_URL',
187
+ message: 'Base URL (ej: http://localhost:3000):',
188
+ initial: 'http://localhost:3000',
189
+ validate: (v: string) =>
190
+ isValidBaseUrl(v) ? true : 'URL inválida (usa http/https)',
191
+ });
192
+ }
193
+
194
+ if (!existing.LOGIN_USER) {
195
+ questions.push({
196
+ type: 'text',
197
+ name: 'LOGIN_USER',
198
+ message: 'Usuario:',
199
+ validate: (v: string) =>
200
+ v?.trim().length ? true : 'Usuario requerido',
201
+ });
202
+ }
203
+
204
+ if (!existing.LOGIN_PASSWORD) {
205
+ questions.push({
206
+ type: 'password',
207
+ name: 'LOGIN_PASSWORD',
208
+ message: 'Contraseña:',
209
+ validate: (v: string) =>
210
+ v?.length ? true : 'Contraseña requerida',
211
+ });
212
+ }
213
+
214
+ if (questions.length > 0) {
215
+ const res = await prompts(questions, {
216
+ onCancel: () => {
217
+ throw new UserCancelledError();
218
+ },
219
+ });
220
+
221
+ const merged: EnvValues = {
222
+ BASE_URL: res.BASE_URL ?? existing.BASE_URL,
223
+ LOGIN_USER: res.LOGIN_USER ?? existing.LOGIN_USER,
224
+ LOGIN_PASSWORD: res.LOGIN_PASSWORD ?? existing.LOGIN_PASSWORD,
225
+ };
226
+ writeEnv(merged);
227
+ console.log('Archivo .env actualizado.');
228
+ }
229
+ }
@@ -1,59 +1,59 @@
1
- import fs from 'node:fs';
2
- import path from 'node:path';
3
- import { discoverProjectRoutes } from './routeDiscovery.mjs';
4
-
5
- function readJson(filePath) {
6
- const raw = fs.readFileSync(filePath, 'utf8');
7
- return JSON.parse(raw);
8
- }
9
-
10
- function detectFramework(pkg) {
11
- const deps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
12
- if (deps.next) return 'next';
13
- if (deps.vite) return 'vite';
14
- if (deps['react-scripts']) return 'cra';
15
- return 'unknown';
16
- }
17
-
18
- export function analyzeCodebase(projectRoot = process.cwd(), options = {}) {
19
- const pkgPath = path.join(projectRoot, 'package.json');
20
- const result = {
21
- projectRoot,
22
- packageJsonPath: pkgPath,
23
- projectName: path.basename(projectRoot),
24
- framework: 'unknown',
25
- scripts: [],
26
- dependencies: [],
27
- routeCount: 0,
28
- routes: [],
29
- warnings: []
30
- };
31
-
32
- if (!fs.existsSync(pkgPath)) {
33
- result.warnings.push('No se encontro package.json en el proyecto destino.');
34
- return result;
35
- }
36
-
37
- try {
38
- const pkg = readJson(pkgPath);
39
- result.projectName = pkg.name || result.projectName;
40
- result.framework = detectFramework(pkg);
41
- result.scripts = Object.keys(pkg.scripts || {});
42
- result.dependencies = Object.keys({ ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) });
43
- result.routes = discoverProjectRoutes(projectRoot, result.framework, {
44
- baseUrl: options.baseUrl || ''
45
- });
46
- result.routeCount = result.routes.length;
47
-
48
- if (result.routeCount === 0) {
49
- result.warnings.push(`No se detectaron rutas automaticamente para el framework "${result.framework}".`);
50
- }
51
- if (result.framework === 'unknown') {
52
- result.warnings.push('Framework no reconocido automaticamente. Se usaron heuristicas de fallback.');
53
- }
54
- } catch (error) {
55
- result.warnings.push(`No se pudo analizar el proyecto: ${error.message}`);
56
- }
57
-
58
- return result;
59
- }
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { discoverProjectRoutes } from './routeDiscovery.mjs';
4
+
5
+ function readJson(filePath) {
6
+ const raw = fs.readFileSync(filePath, 'utf8');
7
+ return JSON.parse(raw);
8
+ }
9
+
10
+ function detectFramework(pkg) {
11
+ const deps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
12
+ if (deps.next) return 'next';
13
+ if (deps.vite) return 'vite';
14
+ if (deps['react-scripts']) return 'cra';
15
+ return 'unknown';
16
+ }
17
+
18
+ export function analyzeCodebase(projectRoot = process.cwd(), options = {}) {
19
+ const pkgPath = path.join(projectRoot, 'package.json');
20
+ const result = {
21
+ projectRoot,
22
+ packageJsonPath: pkgPath,
23
+ projectName: path.basename(projectRoot),
24
+ framework: 'unknown',
25
+ scripts: [],
26
+ dependencies: [],
27
+ routeCount: 0,
28
+ routes: [],
29
+ warnings: []
30
+ };
31
+
32
+ if (!fs.existsSync(pkgPath)) {
33
+ result.warnings.push('No se encontro package.json en el proyecto destino.');
34
+ return result;
35
+ }
36
+
37
+ try {
38
+ const pkg = readJson(pkgPath);
39
+ result.projectName = pkg.name || result.projectName;
40
+ result.framework = detectFramework(pkg);
41
+ result.scripts = Object.keys(pkg.scripts || {});
42
+ result.dependencies = Object.keys({ ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) });
43
+ result.routes = discoverProjectRoutes(projectRoot, result.framework, {
44
+ baseUrl: options.baseUrl || ''
45
+ });
46
+ result.routeCount = result.routes.length;
47
+
48
+ if (result.routeCount === 0) {
49
+ result.warnings.push(`No se detectaron rutas automaticamente para el framework "${result.framework}".`);
50
+ }
51
+ if (result.framework === 'unknown') {
52
+ result.warnings.push('Framework no reconocido automaticamente. Se usaron heuristicas de fallback.');
53
+ }
54
+ } catch (error) {
55
+ result.warnings.push(`No se pudo analizar el proyecto: ${error.message}`);
56
+ }
57
+
58
+ return result;
59
+ }