@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.
- 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 +237 -169
- 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 +1354 -243
- 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/scripts/gen-and-run.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// scripts/gen-and-run.mjs
|
|
3
|
-
// Main Arcality CLI loop
|
|
3
|
+
// Main Arcality CLI loop — v3 Single Configuration Mode
|
|
4
4
|
// Reads from arcality.config instead of multi-config .env approach.
|
|
5
5
|
|
|
6
6
|
import 'dotenv/config';
|
|
@@ -30,14 +30,14 @@ import {
|
|
|
30
30
|
// Load global config at startup (injects keys into process.env)
|
|
31
31
|
setupProcessEnv();
|
|
32
32
|
|
|
33
|
-
//
|
|
34
|
-
// En Windows, `arcality` se ejecuta
|
|
35
|
-
// cmd.exe intercepta Ctrl+C y muestra "
|
|
36
|
-
// Al presionar S, mata el
|
|
33
|
+
// ── WINDOWS FIX: Interceptar Ctrl+C antes que cmd.exe ──
|
|
34
|
+
// En Windows, `arcality` se ejecuta vía arcality.cmd (creado por npm para el campo bin).
|
|
35
|
+
// cmd.exe intercepta Ctrl+C y muestra "¿Desea terminar el trabajo por lotes (S/N)?".
|
|
36
|
+
// Al presionar S, mata el árbol de procesos antes de que nuestro finally pueda correr.
|
|
37
37
|
//
|
|
38
|
-
// La
|
|
38
|
+
// La solución: setRawMode(true) hace que Node capture los bytes del teclado directamente,
|
|
39
39
|
// ANTES de que cmd.exe los procese. Al detectar Ctrl+C (byte 0x03) lo convertimos en
|
|
40
|
-
// un SIGINT de Node
|
|
40
|
+
// un SIGINT de Node — cmd.exe nunca lo ve y nunca muestra el prompt.
|
|
41
41
|
if (process.stdin.isTTY && process.stdin.setRawMode) {
|
|
42
42
|
try {
|
|
43
43
|
process.stdin.setRawMode(true);
|
|
@@ -63,7 +63,7 @@ const ORIGINAL_CWD = process.cwd();
|
|
|
63
63
|
const COLLECTION_DEFAULT_CONCURRENCY = 3;
|
|
64
64
|
const COLLECTION_MAX_CONCURRENCY = 5;
|
|
65
65
|
|
|
66
|
-
//
|
|
66
|
+
// ── Load arcality.config if present ──
|
|
67
67
|
let projectConfig = loadProjectConfig(ORIGINAL_CWD) || loadProjectConfig(PROJECT_ROOT);
|
|
68
68
|
if (projectConfig) {
|
|
69
69
|
if (!isRealProjectId(projectConfig.projectId)) {
|
|
@@ -164,9 +164,9 @@ function setupEnvironment() {
|
|
|
164
164
|
try {
|
|
165
165
|
const pkg = JSON.parse(fs.readFileSync(path.join(PROJECT_ROOT, 'package.json'), 'utf8'));
|
|
166
166
|
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
167
|
-
if (deps['next']) techStack = 'Next.js
|
|
168
|
-
else if (deps['vite']) techStack = 'Vite
|
|
169
|
-
else if (deps['react-scripts']) techStack = 'Create React App
|
|
167
|
+
if (deps['next']) techStack = 'Next.js 🚀';
|
|
168
|
+
else if (deps['vite']) techStack = 'Vite ⚡';
|
|
169
|
+
else if (deps['react-scripts']) techStack = 'Create React App ⚛️';
|
|
170
170
|
} catch { }
|
|
171
171
|
|
|
172
172
|
// Override with arcality.config detection if available
|
|
@@ -284,7 +284,8 @@ function normalizeMissionSpec(rawMission, filePath = '') {
|
|
|
284
284
|
priority: String(mission.priority || '').trim(),
|
|
285
285
|
tags: normalizeStringList(mission.tags),
|
|
286
286
|
timeout: Number.isFinite(Number(mission.timeout)) ? Number(mission.timeout) : null,
|
|
287
|
-
credentialId: String(mission.credential_id || mission.credentialId || '').trim()
|
|
287
|
+
credentialId: String(mission.credential_id || mission.credentialId || '').trim(),
|
|
288
|
+
dbValidations: Array.isArray(mission.db_validations) ? mission.db_validations : (Array.isArray(mission.dbValidations) ? mission.dbValidations : [])
|
|
288
289
|
};
|
|
289
290
|
}
|
|
290
291
|
|
|
@@ -427,7 +428,7 @@ async function selectSavedMissionSource({ libraryDir, projectDir, rootDir }) {
|
|
|
427
428
|
let source = sources[0];
|
|
428
429
|
if (sources.length > 1) {
|
|
429
430
|
const selectedSource = await select({
|
|
430
|
-
message: '
|
|
431
|
+
message: '¿De dónde quieres abrir la misión?',
|
|
431
432
|
options: sources.map(item => ({
|
|
432
433
|
label: `${item.label}${item.description ? ` - ${item.description}` : ''}`,
|
|
433
434
|
value: item.key
|
|
@@ -439,7 +440,7 @@ async function selectSavedMissionSource({ libraryDir, projectDir, rootDir }) {
|
|
|
439
440
|
|
|
440
441
|
if (source.key === 'root') {
|
|
441
442
|
const selectedFile = await select({
|
|
442
|
-
message: 'Elige la
|
|
443
|
+
message: 'Elige la misión guardada:',
|
|
443
444
|
options: source.files.map(file => ({ label: file, value: file }))
|
|
444
445
|
});
|
|
445
446
|
if (isCancel(selectedFile)) return null;
|
|
@@ -456,7 +457,7 @@ async function selectSavedMissionSource({ libraryDir, projectDir, rootDir }) {
|
|
|
456
457
|
const selectedMission = await selectMissionYaml(
|
|
457
458
|
source.dir,
|
|
458
459
|
`Elige la carpeta de ${source.label.toLowerCase()}:`,
|
|
459
|
-
'Elige la
|
|
460
|
+
'Elige la misión guardada:'
|
|
460
461
|
);
|
|
461
462
|
|
|
462
463
|
if (!selectedMission) return null;
|
|
@@ -558,6 +559,22 @@ expected_result: "El registro nuevo aparece en la tabla Timesheet - Hoy."
|
|
|
558
559
|
# Optional placeholder for future centralized credentials.
|
|
559
560
|
credential_id: ""
|
|
560
561
|
#
|
|
562
|
+
# db_validations:
|
|
563
|
+
# Optional post-mission database corroboration. Each query_id must exist in .arcality/db-validations.yaml.
|
|
564
|
+
# Supports exists and exact field checks against the first returned row.
|
|
565
|
+
db_validations: []
|
|
566
|
+
# Example:
|
|
567
|
+
# db_validations:
|
|
568
|
+
# - name: timesheet_creado_en_bd
|
|
569
|
+
# query_id: timesheet_by_description
|
|
570
|
+
# params:
|
|
571
|
+
# description: "Timesheet QA 001"
|
|
572
|
+
# expect:
|
|
573
|
+
# exists: true
|
|
574
|
+
# fields:
|
|
575
|
+
# status: "ACTIVE"
|
|
576
|
+
#
|
|
577
|
+
#
|
|
561
578
|
# hooks:
|
|
562
579
|
# Reserved for future before/after automation hooks.
|
|
563
580
|
# hooks:
|
|
@@ -1114,7 +1131,8 @@ async function runCollectionBatch({
|
|
|
1114
1131
|
ARCALITY_RUN_REPORTS_DIR: mission.reportsDir,
|
|
1115
1132
|
ARCALITY_RUN_PLAYWRIGHT_OUTPUT_DIR: mission.playwrightOutputDir,
|
|
1116
1133
|
ARCALITY_COLLECTION_RUN_ID: runId,
|
|
1117
|
-
ARCALITY_COLLECTION_MISSION_SLUG: mission.slug
|
|
1134
|
+
ARCALITY_COLLECTION_MISSION_SLUG: mission.slug,
|
|
1135
|
+
ARCALITY_DB_VALIDATIONS_JSON: mission.missionSpec.dbValidations.length > 0 ? JSON.stringify(mission.missionSpec.dbValidations) : ''
|
|
1118
1136
|
};
|
|
1119
1137
|
|
|
1120
1138
|
if (runnerPrebuilt) {
|
|
@@ -1214,20 +1232,20 @@ function showBanner() {
|
|
|
1214
1232
|
} catch { }
|
|
1215
1233
|
|
|
1216
1234
|
const sentinelAscii = `
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1235
|
+
█████ ██████ ██████ █████ ██ ██ ████████ ██ ██
|
|
1236
|
+
██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
|
|
1237
|
+
███████ ██████ ██ ███████ ██ ██ ██ ████
|
|
1238
|
+
██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
|
|
1239
|
+
██ ██ ██ ██ ██████ ██ ██ ███████ ██ ██ ██ `;
|
|
1222
1240
|
|
|
1223
1241
|
intro(chalk.gray(sentinelAscii));
|
|
1224
1242
|
|
|
1225
1243
|
note(
|
|
1226
|
-
chalk.gray('
|
|
1227
|
-
chalk.gray('
|
|
1228
|
-
chalk.gray('
|
|
1229
|
-
chalk.gray('
|
|
1230
|
-
chalk.gray('
|
|
1244
|
+
chalk.gray('◉ Motor de Observación: ') + chalk.bold.white('EN LÍNEA') + '\n' +
|
|
1245
|
+
chalk.gray('◉ Módulo de Recuperación de UI: ') + chalk.bold.white('ACTIVO') + '\n' +
|
|
1246
|
+
chalk.gray('◉ Lógica Forense: ') + chalk.bold.white('ACTIVA') + '\n' +
|
|
1247
|
+
chalk.gray('─'.repeat(50)) + '\n' +
|
|
1248
|
+
chalk.gray('Versión del Sistema: ') + chalk.white(`v${version}`),
|
|
1231
1249
|
'Estado Central de Arcality'
|
|
1232
1250
|
);
|
|
1233
1251
|
|
|
@@ -1236,8 +1254,8 @@ function showBanner() {
|
|
|
1236
1254
|
// Show arcality.config status instead of multi-config switching
|
|
1237
1255
|
const hasConfig = !!projectConfig;
|
|
1238
1256
|
const configStatus = hasConfig
|
|
1239
|
-
? chalk.green('
|
|
1240
|
-
: chalk.red('
|
|
1257
|
+
? chalk.green('◉ Sincronización Establecida')
|
|
1258
|
+
: chalk.red('⚠ Desconectado — Ejecuta `arcality init`');
|
|
1241
1259
|
|
|
1242
1260
|
const { key: apiKeyVal, source: apiKeySource } = getApiKey();
|
|
1243
1261
|
const apiKeyDisplay = apiKeyVal
|
|
@@ -1246,7 +1264,7 @@ function showBanner() {
|
|
|
1246
1264
|
|
|
1247
1265
|
const infoLines = [
|
|
1248
1266
|
chalk.gray('Nodo Objetivo: ') + chalk.white(process.env.NODE_ENV ?? 'development'),
|
|
1249
|
-
chalk.gray('Token de
|
|
1267
|
+
chalk.gray('Token de Autenticación: ') + apiKeyDisplay,
|
|
1250
1268
|
chalk.gray('Stack: ') + chalk.white(techStack),
|
|
1251
1269
|
];
|
|
1252
1270
|
|
|
@@ -1256,10 +1274,10 @@ function showBanner() {
|
|
|
1256
1274
|
chalk.gray('ID de Identidad: ') + chalk.white(projectConfig.projectId || 'N/A'),
|
|
1257
1275
|
);
|
|
1258
1276
|
} else {
|
|
1259
|
-
infoLines.push(chalk.gray('
|
|
1277
|
+
infoLines.push(chalk.gray('Configuración: ') + configStatus);
|
|
1260
1278
|
}
|
|
1261
1279
|
|
|
1262
|
-
note(infoLines.join('\n'), '
|
|
1280
|
+
note(infoLines.join('\n'), 'Telemetría del Objetivo');
|
|
1263
1281
|
}
|
|
1264
1282
|
|
|
1265
1283
|
const ARCALITY_BRAIN_URL = "https://api.anthropic.com/v1/messages";
|
|
@@ -1389,7 +1407,7 @@ function run(cmd, args, options = {}) {
|
|
|
1389
1407
|
// properly wraps arguments with spaces in quotes natively for CreateProcessW on Windows.
|
|
1390
1408
|
//
|
|
1391
1409
|
// CRITICAL (Windows): stdin MUST be 'ignore' (not 'inherit') to prevent cmd.exe from
|
|
1392
|
-
// showing the "
|
|
1410
|
+
// showing the "¿Desea terminar el trabajo por lotes (S/N)?" prompt on Ctrl+C.
|
|
1393
1411
|
// When stdin is 'inherit', cmd.exe intercepts Ctrl+C at the console level and kills
|
|
1394
1412
|
// the entire process tree before our SIGINT handler can run, bypassing the finally block.
|
|
1395
1413
|
const p = spawn(cmd, args, {
|
|
@@ -1438,9 +1456,9 @@ function run(cmd, args, options = {}) {
|
|
|
1438
1456
|
fs.writeFileSync(lastRunLog, outputBuffer);
|
|
1439
1457
|
} catch (e) { }
|
|
1440
1458
|
|
|
1441
|
-
// code===0
|
|
1442
|
-
// code===null + signal
|
|
1443
|
-
// code===1
|
|
1459
|
+
// code===0 → clean success
|
|
1460
|
+
// code===null + signal → killed by signal (Ctrl+C) — treat as non-fatal so finally runs
|
|
1461
|
+
// code===1 → test failed (Playwright returns 1 on test failure) — also non-fatal
|
|
1444
1462
|
if (code === 0 || signal !== null || code === 1) resolve();
|
|
1445
1463
|
else {
|
|
1446
1464
|
if (isDebug) console.error(chalk.red(`\n[DEBUG] Process exited with code ${code}`));
|
|
@@ -1464,7 +1482,7 @@ function runNpmScript(args, options = {}) {
|
|
|
1464
1482
|
|
|
1465
1483
|
/**
|
|
1466
1484
|
* Ping the project to register activity.
|
|
1467
|
-
* Uses internal API URL
|
|
1485
|
+
* Uses internal API URL — not user-configurable.
|
|
1468
1486
|
*/
|
|
1469
1487
|
async function pingProject(projectId, apiKey) {
|
|
1470
1488
|
if (!projectId || !apiKey) return;
|
|
@@ -1479,7 +1497,7 @@ async function pingProject(projectId, apiKey) {
|
|
|
1479
1497
|
async function main() {
|
|
1480
1498
|
process.chdir(PROJECT_ROOT);
|
|
1481
1499
|
|
|
1482
|
-
//
|
|
1500
|
+
// Read local .env only for user-managed variables that may still exist.
|
|
1483
1501
|
let initialEnv = parseDotEnvFile(path.join(PROJECT_ROOT, '.env'));
|
|
1484
1502
|
const envProjectId = initialEnv.ARCALITY_PROJECT_ID || process.env.ARCALITY_PROJECT_ID;
|
|
1485
1503
|
if (envProjectId && !isRealProjectId(envProjectId)) {
|
|
@@ -1489,16 +1507,6 @@ async function main() {
|
|
|
1489
1507
|
process.exit(1);
|
|
1490
1508
|
}
|
|
1491
1509
|
|
|
1492
|
-
if (!projectConfig && !initialEnv.ACTIVE_CONFIG) {
|
|
1493
|
-
updateDotEnvFile({
|
|
1494
|
-
ACTIVE_CONFIG: 'Default',
|
|
1495
|
-
SAVED_CONFIGS: 'Default',
|
|
1496
|
-
Default_URL: initialEnv.BASE_URL || '',
|
|
1497
|
-
Default_USER: initialEnv.LOGIN_USER || '',
|
|
1498
|
-
Default_PASS: initialEnv.LOGIN_PASSWORD || ''
|
|
1499
|
-
});
|
|
1500
|
-
initialEnv = parseDotEnvFile(path.join(PROJECT_ROOT, '.env'));
|
|
1501
|
-
}
|
|
1502
1510
|
|
|
1503
1511
|
const argv = process.argv.slice(2);
|
|
1504
1512
|
let initialDiscoverPath = null;
|
|
@@ -1570,6 +1578,7 @@ async function main() {
|
|
|
1570
1578
|
let collectionConcurrency = firstRun ? initialCollectionConcurrency : null;
|
|
1571
1579
|
let skipSavePrompt = firstRun ? initialSkipSavePrompt : false;
|
|
1572
1580
|
let batchMode = firstRun ? initialBatchMode : false;
|
|
1581
|
+
let missionDbValidations = [];
|
|
1573
1582
|
|
|
1574
1583
|
let skipMenu = (firstRun && (prompt || agentMode || smartMode || runAllMode || templateMode)) || agentMode || runAllMode || templateMode;
|
|
1575
1584
|
if (agentMode) skipMenu = true; // Hard-lock: No menu in agent mode
|
|
@@ -1579,7 +1588,7 @@ async function main() {
|
|
|
1579
1588
|
if (!skipMenu) {
|
|
1580
1589
|
showBanner();
|
|
1581
1590
|
try {
|
|
1582
|
-
//
|
|
1591
|
+
// ── Build menu options (single config mode) ──
|
|
1583
1592
|
const missionsDir = process.env.MISSIONS_DIR;
|
|
1584
1593
|
const savedMissionCollections = listMissionCollections(missionsDir);
|
|
1585
1594
|
|
|
@@ -1601,38 +1610,40 @@ async function main() {
|
|
|
1601
1610
|
const rootYamlFiles = rootMissionFiles;
|
|
1602
1611
|
|
|
1603
1612
|
const options = [
|
|
1604
|
-
{ label: '
|
|
1605
|
-
...(savedMissionCollections.length ? [{ label: '
|
|
1606
|
-
...(savedMissionCollections.length ? [{ label: '
|
|
1607
|
-
...(yamlOutputCollections.length ? [{ label: '
|
|
1608
|
-
...(rootYamlFiles.length ? [{ label: '
|
|
1609
|
-
{ label: '
|
|
1610
|
-
{ label: '
|
|
1611
|
-
{ label: '
|
|
1613
|
+
{ label: 'Ejecutar mision', value: 'agent' },
|
|
1614
|
+
...(savedMissionCollections.length ? [{ label: 'Ejecutar coleccion de misiones', value: 'run_collection' }] : []),
|
|
1615
|
+
...(savedMissionCollections.length ? [{ label: 'Ejecutar YAML guardado (.yaml)', value: 'launch_saved' }] : []),
|
|
1616
|
+
...(yamlOutputCollections.length ? [{ label: 'Reusar YAML generado', value: 'reuse_yaml' }] : []),
|
|
1617
|
+
...(rootYamlFiles.length ? [{ label: 'Abrir YAML desde la raiz', value: 'yaml_list' }] : []),
|
|
1618
|
+
{ label: 'Descubrir rutas y generar misiones', value: 'generate_missions' },
|
|
1619
|
+
{ label: 'Crear plantillas YAML base', value: 'generate_templates' },
|
|
1620
|
+
{ label: 'Editar configuracion actual', value: 'reconfigure' },
|
|
1621
|
+
{ label: 'Configurar o reconectar proyecto', value: 'full_reconfigure' },
|
|
1622
|
+
{ label: 'Salir de Arcality', value: 'exit' }
|
|
1612
1623
|
];
|
|
1613
1624
|
|
|
1614
1625
|
const menuOptions = options
|
|
1615
1626
|
.filter(option => !['launch_saved', 'reuse_yaml', 'yaml_list'].includes(option.value))
|
|
1616
1627
|
.map(option => option.value === 'generate_templates'
|
|
1617
|
-
? { ...option, label: '
|
|
1628
|
+
? { ...option, label: 'Crear plantillas YAML base' }
|
|
1618
1629
|
: option
|
|
1619
1630
|
);
|
|
1620
1631
|
|
|
1621
1632
|
if (hasSavedMissionSources) {
|
|
1622
1633
|
const insertIndex = Math.min(menuOptions.length, 2);
|
|
1623
1634
|
menuOptions.splice(insertIndex, 0, {
|
|
1624
|
-
label: '
|
|
1635
|
+
label: 'Ejecutar mision guardada',
|
|
1625
1636
|
value: 'open_saved_mission'
|
|
1626
1637
|
});
|
|
1627
1638
|
}
|
|
1628
1639
|
|
|
1629
1640
|
const action = await select({
|
|
1630
|
-
message: '
|
|
1641
|
+
message: '¿Qué te gustaría hacer?',
|
|
1631
1642
|
options: menuOptions
|
|
1632
1643
|
});
|
|
1633
1644
|
|
|
1634
1645
|
if (isCancel(action) || action === 'exit') {
|
|
1635
|
-
outro(chalk.cyan('
|
|
1646
|
+
outro(chalk.cyan('¡Hasta luego!'));
|
|
1636
1647
|
break;
|
|
1637
1648
|
}
|
|
1638
1649
|
|
|
@@ -1652,11 +1663,43 @@ async function main() {
|
|
|
1652
1663
|
const yamlContent = fs.readFileSync(selectedMission.fullPath, 'utf8');
|
|
1653
1664
|
const missionSpec = normalizeMissionSpec(loadYaml(yamlContent), selectedMission.fullPath);
|
|
1654
1665
|
prompt = missionSpec.prompt || yamlContent;
|
|
1666
|
+
missionDbValidations = missionSpec.dbValidations;
|
|
1655
1667
|
discoverPath = getMissionStartPath(
|
|
1656
1668
|
missionSpec,
|
|
1657
1669
|
readCollectionMeta(path.dirname(selectedMission.fullPath), selectedMission.collection)
|
|
1658
1670
|
);
|
|
1659
1671
|
agentMode = true;
|
|
1672
|
+
} else if (action === 'generate_missions') {
|
|
1673
|
+
const generateScript = path.join(PROJECT_ROOT, 'scripts', 'generate.mjs');
|
|
1674
|
+
const generationMode = await select({
|
|
1675
|
+
message: 'Como quieres usar la autogeneracion?',
|
|
1676
|
+
options: [
|
|
1677
|
+
{ label: 'Solo vista previa (sin archivos)', value: 'dry_run' },
|
|
1678
|
+
{ label: 'Generar archivos YAML ahora', value: 'write_yaml' },
|
|
1679
|
+
{ label: 'Cancelar', value: 'cancel' }
|
|
1680
|
+
]
|
|
1681
|
+
});
|
|
1682
|
+
if (isCancel(generationMode) || generationMode === 'cancel') continue;
|
|
1683
|
+
|
|
1684
|
+
try {
|
|
1685
|
+
await new Promise((resolve, reject) => {
|
|
1686
|
+
const childArgs = generationMode === 'dry_run'
|
|
1687
|
+
? [generateScript, '--dry-run']
|
|
1688
|
+
: [generateScript];
|
|
1689
|
+
const child = spawn('node', childArgs, {
|
|
1690
|
+
stdio: 'inherit',
|
|
1691
|
+
cwd: ORIGINAL_CWD,
|
|
1692
|
+
env: { ...process.env, ARCALITY_ROOT: PROJECT_ROOT }
|
|
1693
|
+
});
|
|
1694
|
+
child.on('exit', code => code === 0 ? resolve() : reject(new Error(`Generate exited with ${code}`)));
|
|
1695
|
+
child.on('error', reject);
|
|
1696
|
+
});
|
|
1697
|
+
} catch (e) {
|
|
1698
|
+
console.log(chalk.red(`\nAutogeneracion fallida: ${e.message}`));
|
|
1699
|
+
}
|
|
1700
|
+
|
|
1701
|
+
await waitForEnter('Presiona Enter para volver al menu...');
|
|
1702
|
+
continue;
|
|
1660
1703
|
} else if (action === 'generate_templates') {
|
|
1661
1704
|
const createdMain = ensureMissionTemplates(missionsDir);
|
|
1662
1705
|
const yamlDir = projectConfig ? getYamlOutputDir(projectConfig, ORIGINAL_CWD) : null;
|
|
@@ -1671,7 +1714,30 @@ async function main() {
|
|
|
1671
1714
|
await waitForEnter('Presiona Enter para volver al menu...');
|
|
1672
1715
|
continue;
|
|
1673
1716
|
} else if (action === 'reconfigure') {
|
|
1674
|
-
|
|
1717
|
+
const editorScript = path.join(PROJECT_ROOT, 'scripts', 'edit-config.mjs');
|
|
1718
|
+
try {
|
|
1719
|
+
await new Promise((resolve, reject) => {
|
|
1720
|
+
const child = spawn('node', [editorScript], {
|
|
1721
|
+
stdio: 'inherit',
|
|
1722
|
+
cwd: ORIGINAL_CWD,
|
|
1723
|
+
env: { ...process.env, ARCALITY_ROOT: PROJECT_ROOT }
|
|
1724
|
+
});
|
|
1725
|
+
child.on('exit', code => code === 0 ? resolve() : reject(new Error(`Config editor exited with ${code}`)));
|
|
1726
|
+
child.on('error', reject);
|
|
1727
|
+
});
|
|
1728
|
+
|
|
1729
|
+
projectConfig = loadProjectConfig(ORIGINAL_CWD) || loadProjectConfig(PROJECT_ROOT);
|
|
1730
|
+
if (projectConfig) {
|
|
1731
|
+
injectConfigToEnv(projectConfig);
|
|
1732
|
+
}
|
|
1733
|
+
setupEnvironment();
|
|
1734
|
+
|
|
1735
|
+
} catch (e) {
|
|
1736
|
+
console.log(chalk.red(`\nConfig update failed: ${e.message}`));
|
|
1737
|
+
await new Promise(r => setTimeout(r, 3000));
|
|
1738
|
+
}
|
|
1739
|
+
continue;
|
|
1740
|
+
} else if (action === 'full_reconfigure') {
|
|
1675
1741
|
const initScript = path.join(PROJECT_ROOT, 'scripts', 'init.mjs');
|
|
1676
1742
|
try {
|
|
1677
1743
|
await new Promise((resolve, reject) => {
|
|
@@ -1683,18 +1749,15 @@ async function main() {
|
|
|
1683
1749
|
child.on('exit', code => code === 0 ? resolve() : reject(new Error(`Init exited with ${code}`)));
|
|
1684
1750
|
child.on('error', reject);
|
|
1685
1751
|
});
|
|
1686
|
-
|
|
1687
|
-
// ── REFRESH CONFIG ──
|
|
1688
|
-
// Recargar el archivo que init.mjs acaba de escribir
|
|
1752
|
+
|
|
1689
1753
|
projectConfig = loadProjectConfig(ORIGINAL_CWD) || loadProjectConfig(PROJECT_ROOT);
|
|
1690
1754
|
if (projectConfig) {
|
|
1691
1755
|
injectConfigToEnv(projectConfig);
|
|
1692
1756
|
}
|
|
1693
1757
|
setupEnvironment();
|
|
1694
|
-
|
|
1758
|
+
|
|
1695
1759
|
} catch (e) {
|
|
1696
|
-
|
|
1697
|
-
console.log(chalk.red(`\n⌠Reconfigure failed: ${e.message}`));
|
|
1760
|
+
console.log(chalk.red(`\nReconnect failed: ${e.message}`));
|
|
1698
1761
|
await new Promise(r => setTimeout(r, 3000));
|
|
1699
1762
|
}
|
|
1700
1763
|
continue;
|
|
@@ -1709,6 +1772,7 @@ async function main() {
|
|
|
1709
1772
|
const yamlContent = fs.readFileSync(selectedMission.fullPath, 'utf8');
|
|
1710
1773
|
const missionSpec = normalizeMissionSpec(loadYaml(yamlContent), selectedMission.fullPath);
|
|
1711
1774
|
prompt = missionSpec.prompt || yamlContent;
|
|
1775
|
+
missionDbValidations = missionSpec.dbValidations;
|
|
1712
1776
|
discoverPath = getMissionStartPath(missionSpec, readCollectionMeta(path.dirname(selectedMission.fullPath), selectedMission.collection));
|
|
1713
1777
|
agentMode = true;
|
|
1714
1778
|
} else if (action === 'reuse_yaml') {
|
|
@@ -1723,6 +1787,7 @@ async function main() {
|
|
|
1723
1787
|
const yamlContent = fs.readFileSync(selectedMission.fullPath, 'utf8');
|
|
1724
1788
|
const missionSpec = normalizeMissionSpec(loadYaml(yamlContent), selectedMission.fullPath);
|
|
1725
1789
|
prompt = missionSpec.prompt || yamlContent;
|
|
1790
|
+
missionDbValidations = missionSpec.dbValidations;
|
|
1726
1791
|
discoverPath = getMissionStartPath(missionSpec, readCollectionMeta(path.dirname(selectedMission.fullPath), selectedMission.collection));
|
|
1727
1792
|
agentMode = true;
|
|
1728
1793
|
} else if (action === 'yaml_list') {
|
|
@@ -1735,6 +1800,7 @@ async function main() {
|
|
|
1735
1800
|
const yamlContent = fs.readFileSync(path.join(PROJECT_ROOT, sel), 'utf8');
|
|
1736
1801
|
const missionSpec = normalizeMissionSpec(loadYaml(yamlContent), path.join(PROJECT_ROOT, sel));
|
|
1737
1802
|
prompt = missionSpec.prompt || yamlContent;
|
|
1803
|
+
missionDbValidations = missionSpec.dbValidations;
|
|
1738
1804
|
discoverPath = getMissionStartPath(missionSpec);
|
|
1739
1805
|
agentMode = true;
|
|
1740
1806
|
}
|
|
@@ -1765,7 +1831,7 @@ async function main() {
|
|
|
1765
1831
|
if (!batchCollection && process.stdin.isTTY) {
|
|
1766
1832
|
const availableCollections = listMissionCollections(process.env.MISSIONS_DIR);
|
|
1767
1833
|
const selectedBatchCollection = await select({
|
|
1768
|
-
message: 'Elige la
|
|
1834
|
+
message: 'Elige la colección a ejecutar:',
|
|
1769
1835
|
options: [
|
|
1770
1836
|
{ label: `Todas las colecciones (${availableCollections.length})`, value: '__ALL__' },
|
|
1771
1837
|
...availableCollections.map(collection => ({
|
|
@@ -1823,16 +1889,16 @@ async function main() {
|
|
|
1823
1889
|
if (agentMode) {
|
|
1824
1890
|
if (!prompt || prompt.trim().length < 4) {
|
|
1825
1891
|
const p = await text({
|
|
1826
|
-
message: chalk.cyan('
|
|
1892
|
+
message: chalk.cyan('🤖 Misión para Arcality (Requerida):'),
|
|
1827
1893
|
placeholder: 'Ej., Llena el formulario de registro con datos aleatorios',
|
|
1828
1894
|
validate: v => {
|
|
1829
|
-
if (!v || !v.trim()) return 'La
|
|
1830
|
-
if (v.trim().length < 4) return 'La
|
|
1895
|
+
if (!v || !v.trim()) return 'La misión es requerida para iniciar.';
|
|
1896
|
+
if (v.trim().length < 4) return 'La misión debe ser más descriptiva (mínimo 4 caracteres).';
|
|
1831
1897
|
}
|
|
1832
1898
|
});
|
|
1833
1899
|
|
|
1834
1900
|
if (isCancel(p)) {
|
|
1835
|
-
cancel('
|
|
1901
|
+
cancel('Misión cancelada. Regresando al menú principal...');
|
|
1836
1902
|
agentMode = false;
|
|
1837
1903
|
prompt = "";
|
|
1838
1904
|
skipMenu = false;
|
|
@@ -1844,8 +1910,8 @@ async function main() {
|
|
|
1844
1910
|
if (!discoverPath) {
|
|
1845
1911
|
const knownBaseUrl = projectConfig?.project?.baseUrl || process.env.BASE_URL;
|
|
1846
1912
|
const promptMsg = knownBaseUrl
|
|
1847
|
-
?
|
|
1848
|
-
: '
|
|
1913
|
+
? `🌐 URL Completa de Navegación (relative to ${knownBaseUrl}):`
|
|
1914
|
+
: '🌐 URL Completa de Navegación (ej., http://localhost:3000/):';
|
|
1849
1915
|
|
|
1850
1916
|
const d = await text({
|
|
1851
1917
|
message: chalk.cyan(promptMsg),
|
|
@@ -1857,7 +1923,7 @@ async function main() {
|
|
|
1857
1923
|
});
|
|
1858
1924
|
|
|
1859
1925
|
if (isCancel(d)) {
|
|
1860
|
-
cancel('
|
|
1926
|
+
cancel('Misión abortada. Regresando al menú...');
|
|
1861
1927
|
agentMode = false;
|
|
1862
1928
|
prompt = "";
|
|
1863
1929
|
skipMenu = false;
|
|
@@ -1873,7 +1939,7 @@ async function main() {
|
|
|
1873
1939
|
const libDotEnv = parseDotEnvFile(path.join(PROJECT_ROOT, '.env'));
|
|
1874
1940
|
const dotEnv = { ...userDotEnv, ...libDotEnv };
|
|
1875
1941
|
|
|
1876
|
-
//
|
|
1942
|
+
// ── API Key and Quota Validation ──
|
|
1877
1943
|
const { validateApiKey, startMission: requestMission } = await import('../src/arcalityClient.mjs');
|
|
1878
1944
|
|
|
1879
1945
|
const validation = await validateApiKey();
|
|
@@ -1887,13 +1953,13 @@ async function main() {
|
|
|
1887
1953
|
}
|
|
1888
1954
|
if (!validation.valid) {
|
|
1889
1955
|
const errorMessages = {
|
|
1890
|
-
'no_api_key': '
|
|
1891
|
-
'invalid_format': '
|
|
1892
|
-
'invalid_api_key': '
|
|
1893
|
-
'plan_expired': '
|
|
1956
|
+
'no_api_key': '🔑 API Key no encontrada.\n Configúrala vía `arcality init` o en .env (ARCALITY_API_KEY)',
|
|
1957
|
+
'invalid_format': '🔑 API Key inválida. Debe comenzar con "arc_"',
|
|
1958
|
+
'invalid_api_key': '🔑 API Key no reconocida por el servidor',
|
|
1959
|
+
'plan_expired': '💳 Tu plan ha expirado',
|
|
1894
1960
|
'backend_unavailable': 'No se pudo conectar con Arcality. La herramienta requiere conexion al backend para validar la cuenta.'
|
|
1895
1961
|
};
|
|
1896
|
-
note(chalk.red(errorMessages[validation.error] || `Error: ${validation.error}`), '
|
|
1962
|
+
note(chalk.red(errorMessages[validation.error] || `Error: ${validation.error}`), '❌ Autenticación');
|
|
1897
1963
|
agentMode = false;
|
|
1898
1964
|
prompt = "";
|
|
1899
1965
|
skipMenu = false;
|
|
@@ -1902,11 +1968,11 @@ async function main() {
|
|
|
1902
1968
|
|
|
1903
1969
|
// Show plan info
|
|
1904
1970
|
const modeLabel = validation.mode === 'mock' ? chalk.gray('[sandbox]') : chalk.cyanBright('[live-net]');
|
|
1905
|
-
const dailyLimitDisplay = validation.daily_limit === -1 ? '
|
|
1906
|
-
const remainingDisplay = validation.remaining >= 999999 ? '
|
|
1971
|
+
const dailyLimitDisplay = validation.daily_limit === -1 ? '∞' : (validation.daily_limit || 35);
|
|
1972
|
+
const remainingDisplay = validation.remaining >= 999999 ? '∞' : validation.remaining;
|
|
1907
1973
|
note(
|
|
1908
|
-
chalk.gray('
|
|
1909
|
-
chalk.gray('
|
|
1974
|
+
chalk.gray('Conexión: ') + chalk.cyanBright('SECURE ') + modeLabel + '\n' +
|
|
1975
|
+
chalk.gray('Autorización: ') + chalk.white(validation.plan || 'internal') + '\n' +
|
|
1910
1976
|
chalk.gray('Misiones: ') + chalk.white(`${validation.daily_used || 0}/${dailyLimitDisplay}`) + '\n' +
|
|
1911
1977
|
chalk.gray('Reservas: ') + chalk.white(remainingDisplay),
|
|
1912
1978
|
'Enlace Arcality'
|
|
@@ -1944,15 +2010,15 @@ async function main() {
|
|
|
1944
2010
|
const projects = data.projects || [];
|
|
1945
2011
|
|
|
1946
2012
|
const projOptions = projects.map(p => ({ label: p.name || p.Name, value: p.id || p.Id }));
|
|
1947
|
-
projOptions.push({ label: '
|
|
2013
|
+
projOptions.push({ label: '➕ Crear nuevo proyecto', value: 'new' });
|
|
1948
2014
|
|
|
1949
2015
|
const projSelection = await select({
|
|
1950
|
-
message: chalk.cyan('Elige un proyecto para esta
|
|
2016
|
+
message: chalk.cyan('Elige un proyecto para esta misión:'),
|
|
1951
2017
|
options: projOptions
|
|
1952
2018
|
});
|
|
1953
2019
|
|
|
1954
2020
|
if (isCancel(projSelection)) {
|
|
1955
|
-
cancel('
|
|
2021
|
+
cancel('Misión abortada.');
|
|
1956
2022
|
agentMode = false; prompt = ""; skipMenu = false; continue;
|
|
1957
2023
|
}
|
|
1958
2024
|
|
|
@@ -1978,9 +2044,9 @@ async function main() {
|
|
|
1978
2044
|
note(chalk.red('El backend no devolvio un Project ID valido.'), 'Proyecto no conectado');
|
|
1979
2045
|
selectedProjectId = null;
|
|
1980
2046
|
}
|
|
1981
|
-
if (selectedProjectId) note(chalk.green(
|
|
2047
|
+
if (selectedProjectId) note(chalk.green(`✅ Proyecto creado: ${newName}`));
|
|
1982
2048
|
} else {
|
|
1983
|
-
note(chalk.red(
|
|
2049
|
+
note(chalk.red(`❌ Falló al crear proyecto.`));
|
|
1984
2050
|
}
|
|
1985
2051
|
} else {
|
|
1986
2052
|
selectedProjectId = projSelection;
|
|
@@ -2013,7 +2079,7 @@ async function main() {
|
|
|
2013
2079
|
continue;
|
|
2014
2080
|
}
|
|
2015
2081
|
|
|
2016
|
-
//
|
|
2082
|
+
// ── Start Mission ──
|
|
2017
2083
|
const mission = await requestMission(prompt, discoverPath || '/');
|
|
2018
2084
|
if (!mission.allowed) {
|
|
2019
2085
|
if (mission.error === 'mission_limit_exceeded') {
|
|
@@ -2044,19 +2110,19 @@ async function main() {
|
|
|
2044
2110
|
|
|
2045
2111
|
const s = spinner();
|
|
2046
2112
|
if (!process.argv.includes('--debug')) {
|
|
2047
|
-
s.start(
|
|
2113
|
+
s.start(`◉ [ARCALITY INICIADO] Ejecutando directiva: "${prompt}" ${chalk.gray(`(${mission.daily_used}/${mission.daily_limit})`)}`);
|
|
2048
2114
|
} else {
|
|
2049
|
-
console.log(chalk.cyan(`\n
|
|
2115
|
+
console.log(chalk.cyan(`\n◉ [ARCALITY INICIADO] Ejecutando directiva: "${prompt}" ${chalk.gray(`(${mission.daily_used}/${mission.daily_limit})`)}`));
|
|
2050
2116
|
}
|
|
2051
2117
|
|
|
2052
2118
|
const finalProjectId = process.env.ARCALITY_PROJECT_ID || selectedProjectId;
|
|
2053
2119
|
|
|
2054
|
-
//
|
|
2120
|
+
// ── Cancelación Manual (Ctrl+C) — Estrategia bulletproof ──
|
|
2055
2121
|
// El problema: @clack/prompts registra su propio SIGINT handler cada tick del spinner,
|
|
2056
2122
|
// que llama a process.exit(0) directamente. removeAllListeners() no es suficiente porque
|
|
2057
|
-
// Clack lo re-registra en cada frame de
|
|
2123
|
+
// Clack lo re-registra en cada frame de animación.
|
|
2058
2124
|
//
|
|
2059
|
-
// La
|
|
2125
|
+
// La solución: interceptar process.exit() antes de que Clack lo llame,
|
|
2060
2126
|
// y usar prependListener para que NUESTRO handler siempre se ejecute primero.
|
|
2061
2127
|
|
|
2062
2128
|
let activeChildProcess = null;
|
|
@@ -2068,18 +2134,18 @@ async function main() {
|
|
|
2068
2134
|
const _origProcessExit = process.exit.bind(process);
|
|
2069
2135
|
process.exit = (code) => {
|
|
2070
2136
|
if (_missionActive) {
|
|
2071
|
-
// Clack (u otra lib)
|
|
2072
|
-
// Nuestro cancelHandler ya
|
|
2073
|
-
// El proceso se
|
|
2137
|
+
// Clack (u otra lib) está intentando matar el proceso — lo bloqueamos.
|
|
2138
|
+
// Nuestro cancelHandler ya habrá sido invocado vía prependListener.
|
|
2139
|
+
// El proceso se cerrará naturalmente cuando el child process termine.
|
|
2074
2140
|
return;
|
|
2075
2141
|
}
|
|
2076
2142
|
_origProcessExit(code);
|
|
2077
2143
|
};
|
|
2078
2144
|
|
|
2079
|
-
//
|
|
2080
|
-
// @clack/prompts pausea stdin y desactiva raw mode al terminar un men
|
|
2081
|
-
// Debemos reactivarlo
|
|
2082
|
-
// siga funcionando durante toda la
|
|
2145
|
+
// ── RESTORE RAW MODE FOR WINDOWS FIX ──
|
|
2146
|
+
// @clack/prompts pausea stdin y desactiva raw mode al terminar un menú.
|
|
2147
|
+
// Debemos reactivarlo aquí para que nuestro interceptor global de Ctrl+C
|
|
2148
|
+
// siga funcionando durante toda la misión.
|
|
2083
2149
|
if (process.stdin.isTTY && process.stdin.setRawMode) {
|
|
2084
2150
|
try {
|
|
2085
2151
|
process.stdin.setRawMode(true);
|
|
@@ -2090,21 +2156,21 @@ async function main() {
|
|
|
2090
2156
|
const cancelHandler = () => {
|
|
2091
2157
|
sigintCount++;
|
|
2092
2158
|
if (sigintCount > 3) {
|
|
2093
|
-
console.log(chalk.red('\n
|
|
2159
|
+
console.log(chalk.red('\n🛑 Forzando salida inmediata...'));
|
|
2094
2160
|
_missionActive = false;
|
|
2095
2161
|
_origProcessExit(1);
|
|
2096
2162
|
return;
|
|
2097
2163
|
}
|
|
2098
2164
|
|
|
2099
2165
|
if (userCanceledMission) {
|
|
2100
|
-
console.log(chalk.yellow('\n
|
|
2166
|
+
console.log(chalk.yellow('\n⏳ Cancelación en progreso, esperando reporte... (Ctrl+C x4 para forzar cierre)'));
|
|
2101
2167
|
return;
|
|
2102
2168
|
}
|
|
2103
2169
|
userCanceledMission = true;
|
|
2104
2170
|
|
|
2105
|
-
console.log(chalk.yellow('\n\
|
|
2171
|
+
console.log(chalk.yellow('\n\n⚠️ [ARCALITY] Directiva abortada por el operador.'));
|
|
2106
2172
|
if (!process.argv.includes('--debug')) {
|
|
2107
|
-
try { s.stop(chalk.yellow('
|
|
2173
|
+
try { s.stop(chalk.yellow('⚠️ Directiva abortada.')); } catch { /* ignore */ }
|
|
2108
2174
|
}
|
|
2109
2175
|
|
|
2110
2176
|
// Terminar el proceso hijo de Playwright directamente (Windows-safe)
|
|
@@ -2136,8 +2202,8 @@ async function main() {
|
|
|
2136
2202
|
} catch { /* silencioso */ }
|
|
2137
2203
|
|
|
2138
2204
|
console.log(chalk.gray(' >> Esperando que Playwright cierre y genere el reporte...'));
|
|
2139
|
-
// Playwright
|
|
2140
|
-
// y
|
|
2205
|
+
// Playwright también recibe el SIGINT nativamente desde la terminal Windows
|
|
2206
|
+
// y cerrará su proceso limpiamente, lo que resolverá la Promise del run() → finally
|
|
2141
2207
|
};
|
|
2142
2208
|
|
|
2143
2209
|
// prependListener asegura que nuestro handler se ejecuta ANTES que el de Clack
|
|
@@ -2145,19 +2211,20 @@ async function main() {
|
|
|
2145
2211
|
process.prependListener('SIGTERM', cancelHandler);
|
|
2146
2212
|
|
|
2147
2213
|
|
|
2148
|
-
// Build env for the runner
|
|
2149
|
-
// ARCALITY_API_URL must be explicit
|
|
2214
|
+
// Build env for the runner — merge arcality.config values.
|
|
2215
|
+
// ARCALITY_API_URL must be explicit — it comes from the library's internal config,
|
|
2150
2216
|
// not from the user's project .env, so we inject it directly via getApiUrl().
|
|
2151
2217
|
const fallbackBaseUrl = (!projectConfig && discoverPath?.startsWith('http')) ? discoverPath : undefined;
|
|
2152
2218
|
|
|
2153
2219
|
const mergedEnv = {
|
|
2154
2220
|
...dotEnv,
|
|
2155
2221
|
...process.env,
|
|
2156
|
-
|
|
2222
|
+
ARCALITY_ROOT: PROJECT_ROOT,
|
|
2223
|
+
ARCALITY_API_URL: getApiUrl(), // Internal URL — always injected explicitly
|
|
2157
2224
|
ARCALITY_PROJECT_ID: finalProjectId,
|
|
2158
2225
|
ARCALITY_PROJECT_ROOT: ORIGINAL_CWD,
|
|
2159
|
-
ARCALITY_MISSION_ID: mission.mission_id || mission.missionId || mission.MissionId || '', //
|
|
2160
|
-
ARCALITY_ORG_ID: validation.organization_id || validation.organizationId || validation.OrganizationId || process.env.ARCALITY_ORG_ID || '', //
|
|
2226
|
+
ARCALITY_MISSION_ID: mission.mission_id || mission.missionId || mission.MissionId || '', // ← Propagate mission_id to every proxy call
|
|
2227
|
+
ARCALITY_ORG_ID: validation.organization_id || validation.organizationId || validation.OrganizationId || process.env.ARCALITY_ORG_ID || '', // ← Propagate org_id for pattern persistence
|
|
2161
2228
|
BASE_URL: projectConfig?.project?.baseUrl || dotEnv.BASE_URL || process.env.BASE_URL || fallbackBaseUrl || 'http://localhost:3000',
|
|
2162
2229
|
LOGIN_USER: projectConfig?.auth?.username || dotEnv.LOGIN_USER || process.env.LOGIN_USER,
|
|
2163
2230
|
LOGIN_PASSWORD: projectConfig?.auth?.password || dotEnv.LOGIN_PASSWORD || process.env.LOGIN_PASSWORD,
|
|
@@ -2168,7 +2235,8 @@ async function main() {
|
|
|
2168
2235
|
CONTEXT_DIR: process.env.CONTEXT_DIR,
|
|
2169
2236
|
REPORTS_DIR: process.env.REPORTS_DIR,
|
|
2170
2237
|
SMART_PROMPT: prompt,
|
|
2171
|
-
TARGET_PATH: discoverPath || '/'
|
|
2238
|
+
TARGET_PATH: discoverPath || '/',
|
|
2239
|
+
ARCALITY_DB_VALIDATIONS_JSON: missionDbValidations.length > 0 ? JSON.stringify(missionDbValidations) : (process.env.ARCALITY_DB_VALIDATIONS_JSON || process.env.ARCALITY_DB_VALIDATIONS || '')
|
|
2172
2240
|
};
|
|
2173
2241
|
|
|
2174
2242
|
if (finalProjectId) {
|
|
@@ -2176,17 +2244,17 @@ async function main() {
|
|
|
2176
2244
|
console.log(chalk.gray(`>> ARCALITY_ORG_ID: ${mergedEnv.ARCALITY_ORG_ID || 'NONE'}`));
|
|
2177
2245
|
}
|
|
2178
2246
|
|
|
2179
|
-
//
|
|
2247
|
+
// ── Resolve execution paths ──
|
|
2180
2248
|
// tsx and playwright are HOISTED to ORIGINAL_CWD/node_modules when installed via npm.
|
|
2181
2249
|
// We run from ORIGINAL_CWD so relative paths for playwright work.
|
|
2182
2250
|
// For the test file and config (inside the arcality package), we use absolute paths.
|
|
2183
2251
|
//
|
|
2184
|
-
// KEY: tsx loader uses file:// URL (pathToFileURL) so spaces encode as %20
|
|
2252
|
+
// KEY: tsx loader uses file:// URL (pathToFileURL) so spaces encode as %20 — no quoting needed.
|
|
2185
2253
|
|
|
2186
2254
|
const testFile = path.join(PROJECT_ROOT, 'tests', '_helpers', 'agentic-runner.spec.ts');
|
|
2187
2255
|
const configFile = path.join(PROJECT_ROOT, 'playwright.config.js');
|
|
2188
2256
|
|
|
2189
|
-
// Resolve playwright CLI
|
|
2257
|
+
// Resolve playwright CLI — CRITICAL singleton alignment:
|
|
2190
2258
|
//
|
|
2191
2259
|
// @playwright/test uses `playwright` internally. When npm detects a version mismatch
|
|
2192
2260
|
// between the top-level `playwright` and what `@playwright/test` needs, it installs
|
|
@@ -2194,15 +2262,15 @@ async function main() {
|
|
|
2194
2262
|
//
|
|
2195
2263
|
// Playwright's test runner tracks the active suite via a module-level singleton
|
|
2196
2264
|
// (currentSuite). If the CLI uses a DIFFERENT playwright instance than the one
|
|
2197
|
-
// @playwright/test loaded, their singletons are out of sync
|
|
2265
|
+
// @playwright/test loaded, their singletons are out of sync → "did not expect test()
|
|
2198
2266
|
// to be called here".
|
|
2199
2267
|
//
|
|
2200
2268
|
// Fix: ALWAYS prefer the playwright nested INSIDE @playwright/test, because that is
|
|
2201
2269
|
// guaranteed to be the same instance that @playwright/test uses for its internals.
|
|
2202
2270
|
//
|
|
2203
2271
|
// Resolution priority:
|
|
2204
|
-
// 1. <project>/@playwright/test/node_modules/playwright/cli.js
|
|
2205
|
-
// 2. <project>/playwright/cli.js
|
|
2272
|
+
// 1. <project>/@playwright/test/node_modules/playwright/cli.js ← nested (safe)
|
|
2273
|
+
// 2. <project>/playwright/cli.js ← sibling top-level
|
|
2206
2274
|
// (repeated for ORIGINAL_CWD and PROJECT_ROOT)
|
|
2207
2275
|
let playwrightCli;
|
|
2208
2276
|
try {
|
|
@@ -2221,9 +2289,9 @@ async function main() {
|
|
|
2221
2289
|
|
|
2222
2290
|
if (!testRunnerPath) throw resolveError;
|
|
2223
2291
|
|
|
2224
|
-
const testRunnerDir = path.dirname(testRunnerPath); //
|
|
2292
|
+
const testRunnerDir = path.dirname(testRunnerPath); // → .../node_modules/@playwright/test/
|
|
2225
2293
|
|
|
2226
|
-
// Priority 1: playwright nested INSIDE @playwright/test
|
|
2294
|
+
// Priority 1: playwright nested INSIDE @playwright/test — same singleton as the package uses
|
|
2227
2295
|
const nestedCli = path.join(testRunnerDir, 'node_modules', 'playwright', 'cli.js');
|
|
2228
2296
|
// Priority 2: playwright as a top-level sibling (clean installs, versions aligned)
|
|
2229
2297
|
const siblingCli = path.join(testRunnerDir, '..', '..', 'playwright', 'cli.js');
|
|
@@ -2242,7 +2310,7 @@ async function main() {
|
|
|
2242
2310
|
console.log(chalk.gray(`[DEBUG] Using playwright CLI: ${playwrightCli}`));
|
|
2243
2311
|
}
|
|
2244
2312
|
} catch (e) {
|
|
2245
|
-
console.error(chalk.red('\n[FATAL]
|
|
2313
|
+
console.error(chalk.red('\n[FATAL] Núcleo de Playwright no encontrado. Asegúrate de que @playwright/test esté instalado.'));
|
|
2246
2314
|
console.error(chalk.red(e.message));
|
|
2247
2315
|
console.error(chalk.yellow('\nConsejo: Ejecuta `npm install @playwright/test` en tu directorio de proyecto.'));
|
|
2248
2316
|
_missionActive = false;
|
|
@@ -2254,9 +2322,9 @@ async function main() {
|
|
|
2254
2322
|
const postProcessSigint = () => {
|
|
2255
2323
|
_postProcessSigintCount++;
|
|
2256
2324
|
if (_postProcessSigintCount === 1) {
|
|
2257
|
-
console.log(chalk.yellow('\n
|
|
2325
|
+
console.log(chalk.yellow('\n⏳ Finalizando reporte de telemetría, por favor espera... (Ctrl+C de nuevo para salir ya)'));
|
|
2258
2326
|
} else {
|
|
2259
|
-
console.log(chalk.red('\n
|
|
2327
|
+
console.log(chalk.red('\n🛑 Saliendo forzado por el usuario.'));
|
|
2260
2328
|
process.removeAllListeners('SIGINT');
|
|
2261
2329
|
process.removeAllListeners('SIGTERM');
|
|
2262
2330
|
process.exit(0);
|
|
@@ -2274,20 +2342,20 @@ async function main() {
|
|
|
2274
2342
|
}
|
|
2275
2343
|
} else
|
|
2276
2344
|
|
|
2277
|
-
//
|
|
2345
|
+
// ── Auto-compilar el runner antes de ejecutar Playwright para asegurar que los cambios de rama/merge estén aplicados ──
|
|
2278
2346
|
try {
|
|
2279
2347
|
if (!process.argv.includes('--debug')) {
|
|
2280
|
-
s.message('
|
|
2348
|
+
s.message('⚙️ Compilando agente de pruebas (Arcality Runner)...');
|
|
2281
2349
|
} else {
|
|
2282
|
-
console.log(chalk.cyan('
|
|
2350
|
+
console.log(chalk.cyan('⚙️ Compilando agente de pruebas (Arcality Runner)...'));
|
|
2283
2351
|
}
|
|
2284
2352
|
await ensureRunnerBundle({ debug: process.argv.includes('--debug') });
|
|
2285
2353
|
} catch (err) {
|
|
2286
|
-
console.warn(chalk.yellow(`\
|
|
2354
|
+
console.warn(chalk.yellow(`\n⚠️ Advertencia: No se pudo auto-compilar el runner: ${err.message}`));
|
|
2287
2355
|
}
|
|
2288
2356
|
|
|
2289
2357
|
try {
|
|
2290
|
-
// No test file path is passed
|
|
2358
|
+
// No test file path is passed — playwright.config.js uses testMatch:'agentic-runner.spec.ts'
|
|
2291
2359
|
// This eliminates the Windows regex path issue permanently on any user's machine.
|
|
2292
2360
|
await run('node', [
|
|
2293
2361
|
'--no-warnings',
|
|
@@ -2299,7 +2367,7 @@ async function main() {
|
|
|
2299
2367
|
cwd: PROJECT_ROOT,
|
|
2300
2368
|
env: mergedEnv,
|
|
2301
2369
|
onStatus: (msg) => { if (!process.argv.includes('--debug')) s.message(msg); else console.log(chalk.gray(`>> Status: ${msg}`)); },
|
|
2302
|
-
onSpawn: (child) => { activeChildProcess = child; } //
|
|
2370
|
+
onSpawn: (child) => { activeChildProcess = child; } // ← Capturar referencia para cancelHandler
|
|
2303
2371
|
});
|
|
2304
2372
|
|
|
2305
2373
|
process.removeAllListeners('SIGINT');
|
|
@@ -2310,11 +2378,11 @@ async function main() {
|
|
|
2310
2378
|
if (userCanceledMission) {
|
|
2311
2379
|
missionResult = 'canceled';
|
|
2312
2380
|
finalFailReason = 'user_canceled';
|
|
2313
|
-
// Detener el spinner
|
|
2381
|
+
// Detener el spinner explícitamente antes de salir
|
|
2314
2382
|
if (!process.argv.includes('--debug')) {
|
|
2315
|
-
try { s.stop(chalk.yellow('
|
|
2383
|
+
try { s.stop(chalk.yellow('⚠️ Misión cancelada por el usuario.')); } catch { }
|
|
2316
2384
|
}
|
|
2317
|
-
return; // El handler ya
|
|
2385
|
+
return; // El handler ya tomó el control, finally se ejecutará
|
|
2318
2386
|
}
|
|
2319
2387
|
|
|
2320
2388
|
const persistedMissionResult = readMissionResult(
|
|
@@ -2346,24 +2414,24 @@ async function main() {
|
|
|
2346
2414
|
if (!process.argv.includes('--debug')) s.stop(chalk.green('\u2705 Mision completada exitosamente.'));
|
|
2347
2415
|
else console.log(chalk.green('\u2705 Mision completada exitosamente.'));
|
|
2348
2416
|
|
|
2349
|
-
//
|
|
2417
|
+
// ── Ping Project ──
|
|
2350
2418
|
await pingProject(finalProjectId, process.env.ARCALITY_API_KEY);
|
|
2351
2419
|
|
|
2352
|
-
//
|
|
2420
|
+
// ── Save Mission YAML ──
|
|
2353
2421
|
let saveDecision = 'no';
|
|
2354
2422
|
if (!skipSavePrompt && !batchMode) {
|
|
2355
2423
|
saveDecision = await select({
|
|
2356
|
-
message: '
|
|
2424
|
+
message: '¿Quieres guardar esta misión para ejecutarla de nuevo más tarde?',
|
|
2357
2425
|
options: [
|
|
2358
|
-
{ label: '
|
|
2359
|
-
{ label: '
|
|
2426
|
+
{ label: '✅ Sí, guardar como YAML', value: 'yes' },
|
|
2427
|
+
{ label: '❌ No, gracias', value: 'no' }
|
|
2360
2428
|
]
|
|
2361
2429
|
});
|
|
2362
2430
|
}
|
|
2363
2431
|
|
|
2364
2432
|
if (saveDecision === 'yes') {
|
|
2365
2433
|
const name = await text({
|
|
2366
|
-
message: 'Nombre de la
|
|
2434
|
+
message: 'Nombre de la misión (ej., login_valido):',
|
|
2367
2435
|
placeholder: 'my_mission',
|
|
2368
2436
|
validate: v => v.length < 3 ? 'Nombre muy corto' : undefined
|
|
2369
2437
|
});
|
|
@@ -2420,9 +2488,9 @@ async function main() {
|
|
|
2420
2488
|
fs.mkdirSync(yamlOutputTargetDir, { recursive: true });
|
|
2421
2489
|
const yamlPathOutput = path.join(yamlOutputTargetDir, `${safeName}.yaml`);
|
|
2422
2490
|
fs.writeFileSync(yamlPathOutput, yamlData);
|
|
2423
|
-
note(chalk.green(
|
|
2491
|
+
note(chalk.green(`✅ Misión guardada en: ${yamlPathOutput}`), 'Persistencia Arcality');
|
|
2424
2492
|
} else {
|
|
2425
|
-
note(chalk.green(
|
|
2493
|
+
note(chalk.green(`✅ Misión guardada en: ${yamlPathMissions}`), 'Persistencia Arcality');
|
|
2426
2494
|
}
|
|
2427
2495
|
}
|
|
2428
2496
|
}
|
|
@@ -2454,7 +2522,7 @@ async function main() {
|
|
|
2454
2522
|
if (userCanceledMission) {
|
|
2455
2523
|
missionResult = 'canceled';
|
|
2456
2524
|
finalFailReason = 'user_canceled';
|
|
2457
|
-
finalFailReasoning = 'La
|
|
2525
|
+
finalFailReasoning = 'La misión fue detenida manualmente por el operador (Ctrl+C). Esto no es una falla técnica del sistema.';
|
|
2458
2526
|
await new Promise(r => setTimeout(r, 800));
|
|
2459
2527
|
}
|
|
2460
2528
|
|
|
@@ -2463,9 +2531,9 @@ async function main() {
|
|
|
2463
2531
|
|
|
2464
2532
|
const sRep = spinner();
|
|
2465
2533
|
if (!process.argv.includes('--debug')) {
|
|
2466
|
-
sRep.start('
|
|
2534
|
+
sRep.start('📊 Generando reporte...');
|
|
2467
2535
|
} else {
|
|
2468
|
-
console.log('
|
|
2536
|
+
console.log('📊 Generando reporte...');
|
|
2469
2537
|
}
|
|
2470
2538
|
|
|
2471
2539
|
await run("node", [path.join(PROJECT_ROOT, "scripts/rebrand-report.mjs")]).catch(() => { });
|
|
@@ -2496,7 +2564,7 @@ async function main() {
|
|
|
2496
2564
|
}
|
|
2497
2565
|
} catch(e) {}
|
|
2498
2566
|
|
|
2499
|
-
//
|
|
2567
|
+
// ── Upload Evidence (solo si NO fue cancelado por usuario) ──
|
|
2500
2568
|
if (missionResult !== 'canceled') {
|
|
2501
2569
|
try {
|
|
2502
2570
|
const { getEvidenceSasToken } = await import('../src/arcalityClient.mjs');
|
|
@@ -2510,7 +2578,7 @@ async function main() {
|
|
|
2510
2578
|
|
|
2511
2579
|
if (sasData && actualSasUrl && actualBasePath) {
|
|
2512
2580
|
if (!process.argv.includes('--debug')) {
|
|
2513
|
-
sRep.start('
|
|
2581
|
+
sRep.start('☁️ Subiendo evidencia a Azure...');
|
|
2514
2582
|
}
|
|
2515
2583
|
process.removeAllListeners('SIGINT');
|
|
2516
2584
|
process.removeAllListeners('SIGTERM');
|
|
@@ -2524,26 +2592,26 @@ async function main() {
|
|
|
2524
2592
|
finalReportUrl = `${parsedSas.origin}${parsedSas.pathname}/${actualBasePath}/index.html`;
|
|
2525
2593
|
} catch(e) {}
|
|
2526
2594
|
|
|
2527
|
-
if (!process.argv.includes('--debug')) sRep.stop(chalk.green('
|
|
2595
|
+
if (!process.argv.includes('--debug')) sRep.stop(chalk.green('✅ Evidencia subida exitosamente.'));
|
|
2528
2596
|
}
|
|
2529
2597
|
} catch (e) {
|
|
2530
|
-
if (process.argv.includes('--debug')) console.log(chalk.yellow(
|
|
2531
|
-
if (!process.argv.includes('--debug')) sRep.stop(chalk.yellow('
|
|
2598
|
+
if (process.argv.includes('--debug')) console.log(chalk.yellow(`⚠️ Could not upload evidence: ${e.message}`));
|
|
2599
|
+
if (!process.argv.includes('--debug')) sRep.stop(chalk.yellow('⚠️ Fallo al subir evidencia.'));
|
|
2532
2600
|
}
|
|
2533
2601
|
} else {
|
|
2534
|
-
console.log(chalk.gray(' >> Saltando subida de evidencia (la
|
|
2602
|
+
console.log(chalk.gray(' >> Saltando subida de evidencia (la misión fue cancelada).'));
|
|
2535
2603
|
}
|
|
2536
2604
|
|
|
2537
|
-
//
|
|
2605
|
+
// ── End Mission — SIEMPRE se llama, independientemente de si la evidencia subió ──
|
|
2538
2606
|
try {
|
|
2539
2607
|
const { endMission } = await import('../src/arcalityClient.mjs');
|
|
2540
2608
|
await Promise.race([
|
|
2541
2609
|
endMission(mission.mission_id, missionResult || 'failed', finalUsageData, finalFailReason, finalReportUrl, finalFailReasoning),
|
|
2542
2610
|
new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), 8000))
|
|
2543
2611
|
]);
|
|
2544
|
-
if (process.argv.includes('--debug')) console.log(chalk.gray(`[DEBUG] endMission called
|
|
2612
|
+
if (process.argv.includes('--debug')) console.log(chalk.gray(`[DEBUG] endMission called → result: ${missionResult}, failReason: ${finalFailReason}`));
|
|
2545
2613
|
} catch (e) {
|
|
2546
|
-
if (process.argv.includes('--debug')) console.log(chalk.yellow(
|
|
2614
|
+
if (process.argv.includes('--debug')) console.log(chalk.yellow(`⚠️ Could not end mission in backend: ${e.message}`));
|
|
2547
2615
|
}
|
|
2548
2616
|
|
|
2549
2617
|
if (globalReportProcess) {
|
|
@@ -2558,10 +2626,10 @@ async function main() {
|
|
|
2558
2626
|
}
|
|
2559
2627
|
|
|
2560
2628
|
if (!batchMode && fs.existsSync(arcalityPath)) {
|
|
2561
|
-
console.log(chalk.blue(
|
|
2629
|
+
console.log(chalk.blue(`🌐 Abriendo Reporte de Arcality: ${arcalityPath}`));
|
|
2562
2630
|
openLocalFile(arcalityPath);
|
|
2563
2631
|
} else if (!fs.existsSync(arcalityPath)) {
|
|
2564
|
-
console.log(chalk.yellow(
|
|
2632
|
+
console.log(chalk.yellow(`⚠️ Reporte no generado (la prueba pudo haber fallado antes de completarse).`));
|
|
2565
2633
|
}
|
|
2566
2634
|
|
|
2567
2635
|
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
@@ -2572,7 +2640,7 @@ async function main() {
|
|
|
2572
2640
|
new Promise(resolve => setTimeout(resolve, 5000))
|
|
2573
2641
|
]);
|
|
2574
2642
|
|
|
2575
|
-
// Limpiar handlers del post-proceso ANTES de esperar Enter, ya que toda la
|
|
2643
|
+
// Limpiar handlers del post-proceso ANTES de esperar Enter, ya que toda la telemetría y reportes ya finalizaron
|
|
2576
2644
|
process.removeListener('SIGINT', postProcessSigint);
|
|
2577
2645
|
process.removeListener('SIGTERM', postProcessSigint);
|
|
2578
2646
|
|
|
@@ -2584,10 +2652,10 @@ async function main() {
|
|
|
2584
2652
|
}
|
|
2585
2653
|
|
|
2586
2654
|
if (false && !batchMode) {
|
|
2587
|
-
// Esperar Enter para regresar al
|
|
2655
|
+
// Esperar Enter para regresar al menú
|
|
2588
2656
|
// Usamos escucha directa de stdin (raw mode) porque @clack/text
|
|
2589
|
-
// no procesa Enter correctamente cuando setRawMode(true)
|
|
2590
|
-
console.log(chalk.gray('\n Presiona Enter para regresar al men
|
|
2657
|
+
// no procesa Enter correctamente cuando setRawMode(true) está activo.
|
|
2658
|
+
console.log(chalk.gray('\n Presiona Enter para regresar al menú...'));
|
|
2591
2659
|
await new Promise(resolve => {
|
|
2592
2660
|
const onKey = (key) => {
|
|
2593
2661
|
const keyStr = String(key);
|
|
@@ -2599,7 +2667,7 @@ async function main() {
|
|
|
2599
2667
|
process.stdin.setRawMode(false);
|
|
2600
2668
|
}
|
|
2601
2669
|
// FIX: pausar stdin si vamos a salir para que el event loop no se quede colgado.
|
|
2602
|
-
// Si volvemos al men
|
|
2670
|
+
// Si volvemos al menú, no lo pausamos (para evitar el bug de exit en clack).
|
|
2603
2671
|
if (skipMenu) {
|
|
2604
2672
|
process.stdin.pause();
|
|
2605
2673
|
}
|
|
@@ -2612,22 +2680,22 @@ async function main() {
|
|
|
2612
2680
|
}
|
|
2613
2681
|
};
|
|
2614
2682
|
|
|
2615
|
-
//
|
|
2683
|
+
// ── BUGFIX: @clack/prompts llama explícitamente a process.stdin.pause()
|
|
2616
2684
|
// al terminar cada select()/text(). En Node.js, agregar un listener 'data'
|
|
2617
|
-
// a un stream
|
|
2618
|
-
// Resultado: el event loop queda
|
|
2685
|
+
// a un stream EXPLÍCITAMENTE pausado NO lo reactiva automáticamente.
|
|
2686
|
+
// Resultado: el event loop queda vacío y el proceso termina antes de que
|
|
2619
2687
|
// el usuario pueda presionar Enter.
|
|
2620
|
-
//
|
|
2688
|
+
// Solución: llamar resume() explícitamente antes de registrar el listener.
|
|
2621
2689
|
if (process.stdin.isTTY && process.stdin.setRawMode) {
|
|
2622
2690
|
process.stdin.setRawMode(true); // raw mode para capturar Enter inmediatamente
|
|
2623
2691
|
}
|
|
2624
|
-
process.stdin.resume(); //
|
|
2692
|
+
process.stdin.resume(); // ← CRÍTICO: reactiva stdin para mantener el event loop vivo
|
|
2625
2693
|
process.stdin.on('data', onKey);
|
|
2626
2694
|
});
|
|
2627
2695
|
}
|
|
2628
2696
|
|
|
2629
2697
|
if (skipMenu) {
|
|
2630
|
-
outro(chalk.cyan('
|
|
2698
|
+
outro(chalk.cyan('¡Misión terminada! Hasta luego.'));
|
|
2631
2699
|
return;
|
|
2632
2700
|
}
|
|
2633
2701
|
|
|
@@ -2643,4 +2711,4 @@ async function main() {
|
|
|
2643
2711
|
main().catch(err => {
|
|
2644
2712
|
console.error(err);
|
|
2645
2713
|
process.exit(1);
|
|
2646
|
-
});
|
|
2714
|
+
});
|