@arcadialdev/arcality 4.0.2 → 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.
- package/.agents/skills/db-validation-evidence/SKILL.md +43 -0
- package/.agents/skills/db-validation-evidence/agents/openai.yaml +4 -0
- package/.agents/skills/db-validation-evidence/references/mission-patterns.md +130 -0
- package/.agents/skills/evidence-synthesis-reporting/SKILL.md +31 -0
- package/.agents/skills/form-expert/SKILL.md +98 -0
- package/.agents/skills/investigation-protocol/SKILL.md +56 -0
- package/.agents/skills/mission-generation-reviewer/SKILL.md +25 -0
- package/.agents/skills/modal-master/SKILL.md +46 -0
- package/.agents/skills/native-control-expert/SKILL.md +74 -0
- package/.agents/skills/qa-context-governance/SKILL.md +23 -0
- package/.agents/skills/security-evidence-triage/SKILL.md +23 -0
- package/.agents/skills/test-data-lifecycle/SKILL.md +24 -0
- package/README.md +103 -163
- package/bin/arcality.mjs +25 -25
- package/package.json +75 -75
- package/scripts/edit-config.mjs +843 -0
- package/scripts/gen-and-run.mjs +2705 -2609
- package/scripts/generate.mjs +236 -236
- package/scripts/init.mjs +47 -47
- package/src/configManager.mjs +13 -13
- package/src/envSetup.ts +229 -205
- package/src/services/codebaseAnalyzer.mjs +59 -59
- package/src/services/databaseValidationService.mjs +598 -0
- package/src/services/executionEvidenceService.mjs +124 -0
- package/src/services/generatedMissionSchema.mjs +76 -76
- package/src/services/generatedMissionStore.mjs +117 -117
- package/src/services/generationContext.mjs +242 -242
- package/src/services/mcpStdioClient.mjs +204 -0
- package/src/services/missionGenerator.mjs +329 -329
- package/src/services/routeDiscovery.mjs +762 -762
- package/tests/_helpers/ArcalityReporter.js +1342 -1255
- package/tests/_helpers/agentic-runner.bundle.spec.js +1363 -245
- package/.agent/skills/form-expert.md +0 -102
- package/.agent/skills/investigation-protocol.md +0 -61
- package/.agent/skills/modal-master.md +0 -41
- 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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
if (
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
];
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
};
|
|
202
|
-
|
|
203
|
-
|
|
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
|
+
}
|