@arcadialdev/arcality 4.1.0 → 4.1.2
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 +260 -170
- 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 +1377 -244
- 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:
|
|
@@ -640,6 +657,23 @@ async function waitForEnter(message = 'Presiona Enter para continuar...', option
|
|
|
640
657
|
});
|
|
641
658
|
}
|
|
642
659
|
|
|
660
|
+
function getSingleEmptyQueryParamName(urlString) {
|
|
661
|
+
if (!urlString) return null;
|
|
662
|
+
|
|
663
|
+
try {
|
|
664
|
+
const parsed = new URL(String(urlString));
|
|
665
|
+
const emptyParams = [];
|
|
666
|
+
|
|
667
|
+
for (const [key, value] of parsed.searchParams.entries()) {
|
|
668
|
+
if (!String(value || '').trim()) emptyParams.push(key);
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
return emptyParams.length === 1 ? emptyParams[0] : null;
|
|
672
|
+
} catch {
|
|
673
|
+
return null;
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
|
|
643
677
|
function getMissionStartPath(missionSpec, collectionMeta = null, fallbackPath = '') {
|
|
644
678
|
return missionSpec.startAt || missionSpec.page || missionSpec.pagePath || collectionMeta?.defaultPagePath || fallbackPath;
|
|
645
679
|
}
|
|
@@ -1114,7 +1148,8 @@ async function runCollectionBatch({
|
|
|
1114
1148
|
ARCALITY_RUN_REPORTS_DIR: mission.reportsDir,
|
|
1115
1149
|
ARCALITY_RUN_PLAYWRIGHT_OUTPUT_DIR: mission.playwrightOutputDir,
|
|
1116
1150
|
ARCALITY_COLLECTION_RUN_ID: runId,
|
|
1117
|
-
ARCALITY_COLLECTION_MISSION_SLUG: mission.slug
|
|
1151
|
+
ARCALITY_COLLECTION_MISSION_SLUG: mission.slug,
|
|
1152
|
+
ARCALITY_DB_VALIDATIONS_JSON: mission.missionSpec.dbValidations.length > 0 ? JSON.stringify(mission.missionSpec.dbValidations) : ''
|
|
1118
1153
|
};
|
|
1119
1154
|
|
|
1120
1155
|
if (runnerPrebuilt) {
|
|
@@ -1214,20 +1249,20 @@ function showBanner() {
|
|
|
1214
1249
|
} catch { }
|
|
1215
1250
|
|
|
1216
1251
|
const sentinelAscii = `
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1252
|
+
█████ ██████ ██████ █████ ██ ██ ████████ ██ ██
|
|
1253
|
+
██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
|
|
1254
|
+
███████ ██████ ██ ███████ ██ ██ ██ ████
|
|
1255
|
+
██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
|
|
1256
|
+
██ ██ ██ ██ ██████ ██ ██ ███████ ██ ██ ██ `;
|
|
1222
1257
|
|
|
1223
1258
|
intro(chalk.gray(sentinelAscii));
|
|
1224
1259
|
|
|
1225
1260
|
note(
|
|
1226
|
-
chalk.gray('
|
|
1227
|
-
chalk.gray('
|
|
1228
|
-
chalk.gray('
|
|
1229
|
-
chalk.gray('
|
|
1230
|
-
chalk.gray('
|
|
1261
|
+
chalk.gray('◉ Motor de Observación: ') + chalk.bold.white('EN LÍNEA') + '\n' +
|
|
1262
|
+
chalk.gray('◉ Módulo de Recuperación de UI: ') + chalk.bold.white('ACTIVO') + '\n' +
|
|
1263
|
+
chalk.gray('◉ Lógica Forense: ') + chalk.bold.white('ACTIVA') + '\n' +
|
|
1264
|
+
chalk.gray('─'.repeat(50)) + '\n' +
|
|
1265
|
+
chalk.gray('Versión del Sistema: ') + chalk.white(`v${version}`),
|
|
1231
1266
|
'Estado Central de Arcality'
|
|
1232
1267
|
);
|
|
1233
1268
|
|
|
@@ -1236,8 +1271,8 @@ function showBanner() {
|
|
|
1236
1271
|
// Show arcality.config status instead of multi-config switching
|
|
1237
1272
|
const hasConfig = !!projectConfig;
|
|
1238
1273
|
const configStatus = hasConfig
|
|
1239
|
-
? chalk.green('
|
|
1240
|
-
: chalk.red('
|
|
1274
|
+
? chalk.green('◉ Sincronización Establecida')
|
|
1275
|
+
: chalk.red('⚠ Desconectado — Ejecuta `arcality init`');
|
|
1241
1276
|
|
|
1242
1277
|
const { key: apiKeyVal, source: apiKeySource } = getApiKey();
|
|
1243
1278
|
const apiKeyDisplay = apiKeyVal
|
|
@@ -1246,7 +1281,7 @@ function showBanner() {
|
|
|
1246
1281
|
|
|
1247
1282
|
const infoLines = [
|
|
1248
1283
|
chalk.gray('Nodo Objetivo: ') + chalk.white(process.env.NODE_ENV ?? 'development'),
|
|
1249
|
-
chalk.gray('Token de
|
|
1284
|
+
chalk.gray('Token de Autenticación: ') + apiKeyDisplay,
|
|
1250
1285
|
chalk.gray('Stack: ') + chalk.white(techStack),
|
|
1251
1286
|
];
|
|
1252
1287
|
|
|
@@ -1256,10 +1291,10 @@ function showBanner() {
|
|
|
1256
1291
|
chalk.gray('ID de Identidad: ') + chalk.white(projectConfig.projectId || 'N/A'),
|
|
1257
1292
|
);
|
|
1258
1293
|
} else {
|
|
1259
|
-
infoLines.push(chalk.gray('
|
|
1294
|
+
infoLines.push(chalk.gray('Configuración: ') + configStatus);
|
|
1260
1295
|
}
|
|
1261
1296
|
|
|
1262
|
-
note(infoLines.join('\n'), '
|
|
1297
|
+
note(infoLines.join('\n'), 'Telemetría del Objetivo');
|
|
1263
1298
|
}
|
|
1264
1299
|
|
|
1265
1300
|
const ARCALITY_BRAIN_URL = "https://api.anthropic.com/v1/messages";
|
|
@@ -1389,7 +1424,7 @@ function run(cmd, args, options = {}) {
|
|
|
1389
1424
|
// properly wraps arguments with spaces in quotes natively for CreateProcessW on Windows.
|
|
1390
1425
|
//
|
|
1391
1426
|
// CRITICAL (Windows): stdin MUST be 'ignore' (not 'inherit') to prevent cmd.exe from
|
|
1392
|
-
// showing the "
|
|
1427
|
+
// showing the "¿Desea terminar el trabajo por lotes (S/N)?" prompt on Ctrl+C.
|
|
1393
1428
|
// When stdin is 'inherit', cmd.exe intercepts Ctrl+C at the console level and kills
|
|
1394
1429
|
// the entire process tree before our SIGINT handler can run, bypassing the finally block.
|
|
1395
1430
|
const p = spawn(cmd, args, {
|
|
@@ -1438,9 +1473,9 @@ function run(cmd, args, options = {}) {
|
|
|
1438
1473
|
fs.writeFileSync(lastRunLog, outputBuffer);
|
|
1439
1474
|
} catch (e) { }
|
|
1440
1475
|
|
|
1441
|
-
// code===0
|
|
1442
|
-
// code===null + signal
|
|
1443
|
-
// code===1
|
|
1476
|
+
// code===0 → clean success
|
|
1477
|
+
// code===null + signal → killed by signal (Ctrl+C) — treat as non-fatal so finally runs
|
|
1478
|
+
// code===1 → test failed (Playwright returns 1 on test failure) — also non-fatal
|
|
1444
1479
|
if (code === 0 || signal !== null || code === 1) resolve();
|
|
1445
1480
|
else {
|
|
1446
1481
|
if (isDebug) console.error(chalk.red(`\n[DEBUG] Process exited with code ${code}`));
|
|
@@ -1464,7 +1499,7 @@ function runNpmScript(args, options = {}) {
|
|
|
1464
1499
|
|
|
1465
1500
|
/**
|
|
1466
1501
|
* Ping the project to register activity.
|
|
1467
|
-
* Uses internal API URL
|
|
1502
|
+
* Uses internal API URL — not user-configurable.
|
|
1468
1503
|
*/
|
|
1469
1504
|
async function pingProject(projectId, apiKey) {
|
|
1470
1505
|
if (!projectId || !apiKey) return;
|
|
@@ -1479,7 +1514,7 @@ async function pingProject(projectId, apiKey) {
|
|
|
1479
1514
|
async function main() {
|
|
1480
1515
|
process.chdir(PROJECT_ROOT);
|
|
1481
1516
|
|
|
1482
|
-
//
|
|
1517
|
+
// Read local .env only for user-managed variables that may still exist.
|
|
1483
1518
|
let initialEnv = parseDotEnvFile(path.join(PROJECT_ROOT, '.env'));
|
|
1484
1519
|
const envProjectId = initialEnv.ARCALITY_PROJECT_ID || process.env.ARCALITY_PROJECT_ID;
|
|
1485
1520
|
if (envProjectId && !isRealProjectId(envProjectId)) {
|
|
@@ -1489,16 +1524,6 @@ async function main() {
|
|
|
1489
1524
|
process.exit(1);
|
|
1490
1525
|
}
|
|
1491
1526
|
|
|
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
1527
|
|
|
1503
1528
|
const argv = process.argv.slice(2);
|
|
1504
1529
|
let initialDiscoverPath = null;
|
|
@@ -1570,6 +1595,7 @@ async function main() {
|
|
|
1570
1595
|
let collectionConcurrency = firstRun ? initialCollectionConcurrency : null;
|
|
1571
1596
|
let skipSavePrompt = firstRun ? initialSkipSavePrompt : false;
|
|
1572
1597
|
let batchMode = firstRun ? initialBatchMode : false;
|
|
1598
|
+
let missionDbValidations = [];
|
|
1573
1599
|
|
|
1574
1600
|
let skipMenu = (firstRun && (prompt || agentMode || smartMode || runAllMode || templateMode)) || agentMode || runAllMode || templateMode;
|
|
1575
1601
|
if (agentMode) skipMenu = true; // Hard-lock: No menu in agent mode
|
|
@@ -1579,7 +1605,7 @@ async function main() {
|
|
|
1579
1605
|
if (!skipMenu) {
|
|
1580
1606
|
showBanner();
|
|
1581
1607
|
try {
|
|
1582
|
-
//
|
|
1608
|
+
// ── Build menu options (single config mode) ──
|
|
1583
1609
|
const missionsDir = process.env.MISSIONS_DIR;
|
|
1584
1610
|
const savedMissionCollections = listMissionCollections(missionsDir);
|
|
1585
1611
|
|
|
@@ -1601,38 +1627,40 @@ async function main() {
|
|
|
1601
1627
|
const rootYamlFiles = rootMissionFiles;
|
|
1602
1628
|
|
|
1603
1629
|
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: '
|
|
1630
|
+
{ label: 'Ejecutar mision', value: 'agent' },
|
|
1631
|
+
...(savedMissionCollections.length ? [{ label: 'Ejecutar coleccion de misiones', value: 'run_collection' }] : []),
|
|
1632
|
+
...(savedMissionCollections.length ? [{ label: 'Ejecutar YAML guardado (.yaml)', value: 'launch_saved' }] : []),
|
|
1633
|
+
...(yamlOutputCollections.length ? [{ label: 'Reusar YAML generado', value: 'reuse_yaml' }] : []),
|
|
1634
|
+
...(rootYamlFiles.length ? [{ label: 'Abrir YAML desde la raiz', value: 'yaml_list' }] : []),
|
|
1635
|
+
{ label: 'Descubrir rutas y generar misiones', value: 'generate_missions' },
|
|
1636
|
+
{ label: 'Crear plantillas YAML base', value: 'generate_templates' },
|
|
1637
|
+
{ label: 'Editar configuracion actual', value: 'reconfigure' },
|
|
1638
|
+
{ label: 'Configurar o reconectar proyecto', value: 'full_reconfigure' },
|
|
1639
|
+
{ label: 'Salir de Arcality', value: 'exit' }
|
|
1612
1640
|
];
|
|
1613
1641
|
|
|
1614
1642
|
const menuOptions = options
|
|
1615
1643
|
.filter(option => !['launch_saved', 'reuse_yaml', 'yaml_list'].includes(option.value))
|
|
1616
1644
|
.map(option => option.value === 'generate_templates'
|
|
1617
|
-
? { ...option, label: '
|
|
1645
|
+
? { ...option, label: 'Crear plantillas YAML base' }
|
|
1618
1646
|
: option
|
|
1619
1647
|
);
|
|
1620
1648
|
|
|
1621
1649
|
if (hasSavedMissionSources) {
|
|
1622
1650
|
const insertIndex = Math.min(menuOptions.length, 2);
|
|
1623
1651
|
menuOptions.splice(insertIndex, 0, {
|
|
1624
|
-
label: '
|
|
1652
|
+
label: 'Ejecutar mision guardada',
|
|
1625
1653
|
value: 'open_saved_mission'
|
|
1626
1654
|
});
|
|
1627
1655
|
}
|
|
1628
1656
|
|
|
1629
1657
|
const action = await select({
|
|
1630
|
-
message: '
|
|
1658
|
+
message: '¿Qué te gustaría hacer?',
|
|
1631
1659
|
options: menuOptions
|
|
1632
1660
|
});
|
|
1633
1661
|
|
|
1634
1662
|
if (isCancel(action) || action === 'exit') {
|
|
1635
|
-
outro(chalk.cyan('
|
|
1663
|
+
outro(chalk.cyan('¡Hasta luego!'));
|
|
1636
1664
|
break;
|
|
1637
1665
|
}
|
|
1638
1666
|
|
|
@@ -1652,11 +1680,43 @@ async function main() {
|
|
|
1652
1680
|
const yamlContent = fs.readFileSync(selectedMission.fullPath, 'utf8');
|
|
1653
1681
|
const missionSpec = normalizeMissionSpec(loadYaml(yamlContent), selectedMission.fullPath);
|
|
1654
1682
|
prompt = missionSpec.prompt || yamlContent;
|
|
1683
|
+
missionDbValidations = missionSpec.dbValidations;
|
|
1655
1684
|
discoverPath = getMissionStartPath(
|
|
1656
1685
|
missionSpec,
|
|
1657
1686
|
readCollectionMeta(path.dirname(selectedMission.fullPath), selectedMission.collection)
|
|
1658
1687
|
);
|
|
1659
1688
|
agentMode = true;
|
|
1689
|
+
} else if (action === 'generate_missions') {
|
|
1690
|
+
const generateScript = path.join(PROJECT_ROOT, 'scripts', 'generate.mjs');
|
|
1691
|
+
const generationMode = await select({
|
|
1692
|
+
message: 'Como quieres usar la autogeneracion?',
|
|
1693
|
+
options: [
|
|
1694
|
+
{ label: 'Solo vista previa (sin archivos)', value: 'dry_run' },
|
|
1695
|
+
{ label: 'Generar archivos YAML ahora', value: 'write_yaml' },
|
|
1696
|
+
{ label: 'Cancelar', value: 'cancel' }
|
|
1697
|
+
]
|
|
1698
|
+
});
|
|
1699
|
+
if (isCancel(generationMode) || generationMode === 'cancel') continue;
|
|
1700
|
+
|
|
1701
|
+
try {
|
|
1702
|
+
await new Promise((resolve, reject) => {
|
|
1703
|
+
const childArgs = generationMode === 'dry_run'
|
|
1704
|
+
? [generateScript, '--dry-run']
|
|
1705
|
+
: [generateScript];
|
|
1706
|
+
const child = spawn('node', childArgs, {
|
|
1707
|
+
stdio: 'inherit',
|
|
1708
|
+
cwd: ORIGINAL_CWD,
|
|
1709
|
+
env: { ...process.env, ARCALITY_ROOT: PROJECT_ROOT }
|
|
1710
|
+
});
|
|
1711
|
+
child.on('exit', code => code === 0 ? resolve() : reject(new Error(`Generate exited with ${code}`)));
|
|
1712
|
+
child.on('error', reject);
|
|
1713
|
+
});
|
|
1714
|
+
} catch (e) {
|
|
1715
|
+
console.log(chalk.red(`\nAutogeneracion fallida: ${e.message}`));
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
await waitForEnter('Presiona Enter para volver al menu...');
|
|
1719
|
+
continue;
|
|
1660
1720
|
} else if (action === 'generate_templates') {
|
|
1661
1721
|
const createdMain = ensureMissionTemplates(missionsDir);
|
|
1662
1722
|
const yamlDir = projectConfig ? getYamlOutputDir(projectConfig, ORIGINAL_CWD) : null;
|
|
@@ -1671,7 +1731,30 @@ async function main() {
|
|
|
1671
1731
|
await waitForEnter('Presiona Enter para volver al menu...');
|
|
1672
1732
|
continue;
|
|
1673
1733
|
} else if (action === 'reconfigure') {
|
|
1674
|
-
|
|
1734
|
+
const editorScript = path.join(PROJECT_ROOT, 'scripts', 'edit-config.mjs');
|
|
1735
|
+
try {
|
|
1736
|
+
await new Promise((resolve, reject) => {
|
|
1737
|
+
const child = spawn('node', [editorScript], {
|
|
1738
|
+
stdio: 'inherit',
|
|
1739
|
+
cwd: ORIGINAL_CWD,
|
|
1740
|
+
env: { ...process.env, ARCALITY_ROOT: PROJECT_ROOT }
|
|
1741
|
+
});
|
|
1742
|
+
child.on('exit', code => code === 0 ? resolve() : reject(new Error(`Config editor exited with ${code}`)));
|
|
1743
|
+
child.on('error', reject);
|
|
1744
|
+
});
|
|
1745
|
+
|
|
1746
|
+
projectConfig = loadProjectConfig(ORIGINAL_CWD) || loadProjectConfig(PROJECT_ROOT);
|
|
1747
|
+
if (projectConfig) {
|
|
1748
|
+
injectConfigToEnv(projectConfig);
|
|
1749
|
+
}
|
|
1750
|
+
setupEnvironment();
|
|
1751
|
+
|
|
1752
|
+
} catch (e) {
|
|
1753
|
+
console.log(chalk.red(`\nConfig update failed: ${e.message}`));
|
|
1754
|
+
await new Promise(r => setTimeout(r, 3000));
|
|
1755
|
+
}
|
|
1756
|
+
continue;
|
|
1757
|
+
} else if (action === 'full_reconfigure') {
|
|
1675
1758
|
const initScript = path.join(PROJECT_ROOT, 'scripts', 'init.mjs');
|
|
1676
1759
|
try {
|
|
1677
1760
|
await new Promise((resolve, reject) => {
|
|
@@ -1683,18 +1766,15 @@ async function main() {
|
|
|
1683
1766
|
child.on('exit', code => code === 0 ? resolve() : reject(new Error(`Init exited with ${code}`)));
|
|
1684
1767
|
child.on('error', reject);
|
|
1685
1768
|
});
|
|
1686
|
-
|
|
1687
|
-
// ── REFRESH CONFIG ──
|
|
1688
|
-
// Recargar el archivo que init.mjs acaba de escribir
|
|
1769
|
+
|
|
1689
1770
|
projectConfig = loadProjectConfig(ORIGINAL_CWD) || loadProjectConfig(PROJECT_ROOT);
|
|
1690
1771
|
if (projectConfig) {
|
|
1691
1772
|
injectConfigToEnv(projectConfig);
|
|
1692
1773
|
}
|
|
1693
1774
|
setupEnvironment();
|
|
1694
|
-
|
|
1775
|
+
|
|
1695
1776
|
} catch (e) {
|
|
1696
|
-
|
|
1697
|
-
console.log(chalk.red(`\n⌠Reconfigure failed: ${e.message}`));
|
|
1777
|
+
console.log(chalk.red(`\nReconnect failed: ${e.message}`));
|
|
1698
1778
|
await new Promise(r => setTimeout(r, 3000));
|
|
1699
1779
|
}
|
|
1700
1780
|
continue;
|
|
@@ -1709,6 +1789,7 @@ async function main() {
|
|
|
1709
1789
|
const yamlContent = fs.readFileSync(selectedMission.fullPath, 'utf8');
|
|
1710
1790
|
const missionSpec = normalizeMissionSpec(loadYaml(yamlContent), selectedMission.fullPath);
|
|
1711
1791
|
prompt = missionSpec.prompt || yamlContent;
|
|
1792
|
+
missionDbValidations = missionSpec.dbValidations;
|
|
1712
1793
|
discoverPath = getMissionStartPath(missionSpec, readCollectionMeta(path.dirname(selectedMission.fullPath), selectedMission.collection));
|
|
1713
1794
|
agentMode = true;
|
|
1714
1795
|
} else if (action === 'reuse_yaml') {
|
|
@@ -1723,6 +1804,7 @@ async function main() {
|
|
|
1723
1804
|
const yamlContent = fs.readFileSync(selectedMission.fullPath, 'utf8');
|
|
1724
1805
|
const missionSpec = normalizeMissionSpec(loadYaml(yamlContent), selectedMission.fullPath);
|
|
1725
1806
|
prompt = missionSpec.prompt || yamlContent;
|
|
1807
|
+
missionDbValidations = missionSpec.dbValidations;
|
|
1726
1808
|
discoverPath = getMissionStartPath(missionSpec, readCollectionMeta(path.dirname(selectedMission.fullPath), selectedMission.collection));
|
|
1727
1809
|
agentMode = true;
|
|
1728
1810
|
} else if (action === 'yaml_list') {
|
|
@@ -1735,6 +1817,7 @@ async function main() {
|
|
|
1735
1817
|
const yamlContent = fs.readFileSync(path.join(PROJECT_ROOT, sel), 'utf8');
|
|
1736
1818
|
const missionSpec = normalizeMissionSpec(loadYaml(yamlContent), path.join(PROJECT_ROOT, sel));
|
|
1737
1819
|
prompt = missionSpec.prompt || yamlContent;
|
|
1820
|
+
missionDbValidations = missionSpec.dbValidations;
|
|
1738
1821
|
discoverPath = getMissionStartPath(missionSpec);
|
|
1739
1822
|
agentMode = true;
|
|
1740
1823
|
}
|
|
@@ -1765,7 +1848,7 @@ async function main() {
|
|
|
1765
1848
|
if (!batchCollection && process.stdin.isTTY) {
|
|
1766
1849
|
const availableCollections = listMissionCollections(process.env.MISSIONS_DIR);
|
|
1767
1850
|
const selectedBatchCollection = await select({
|
|
1768
|
-
message: 'Elige la
|
|
1851
|
+
message: 'Elige la colección a ejecutar:',
|
|
1769
1852
|
options: [
|
|
1770
1853
|
{ label: `Todas las colecciones (${availableCollections.length})`, value: '__ALL__' },
|
|
1771
1854
|
...availableCollections.map(collection => ({
|
|
@@ -1823,16 +1906,16 @@ async function main() {
|
|
|
1823
1906
|
if (agentMode) {
|
|
1824
1907
|
if (!prompt || prompt.trim().length < 4) {
|
|
1825
1908
|
const p = await text({
|
|
1826
|
-
message: chalk.cyan('
|
|
1909
|
+
message: chalk.cyan('🤖 Misión para Arcality (Requerida):'),
|
|
1827
1910
|
placeholder: 'Ej., Llena el formulario de registro con datos aleatorios',
|
|
1828
1911
|
validate: v => {
|
|
1829
|
-
if (!v || !v.trim()) return 'La
|
|
1830
|
-
if (v.trim().length < 4) return 'La
|
|
1912
|
+
if (!v || !v.trim()) return 'La misión es requerida para iniciar.';
|
|
1913
|
+
if (v.trim().length < 4) return 'La misión debe ser más descriptiva (mínimo 4 caracteres).';
|
|
1831
1914
|
}
|
|
1832
1915
|
});
|
|
1833
1916
|
|
|
1834
1917
|
if (isCancel(p)) {
|
|
1835
|
-
cancel('
|
|
1918
|
+
cancel('Misión cancelada. Regresando al menú principal...');
|
|
1836
1919
|
agentMode = false;
|
|
1837
1920
|
prompt = "";
|
|
1838
1921
|
skipMenu = false;
|
|
@@ -1843,9 +1926,10 @@ async function main() {
|
|
|
1843
1926
|
|
|
1844
1927
|
if (!discoverPath) {
|
|
1845
1928
|
const knownBaseUrl = projectConfig?.project?.baseUrl || process.env.BASE_URL;
|
|
1929
|
+
const emptyQueryParamName = getSingleEmptyQueryParamName(knownBaseUrl);
|
|
1846
1930
|
const promptMsg = knownBaseUrl
|
|
1847
|
-
?
|
|
1848
|
-
: '
|
|
1931
|
+
? `🌐 URL Completa de Navegación (relative to ${knownBaseUrl}). Deja vacío para usarla tal cual${emptyQueryParamName ? ` o escribe solo el valor de "${emptyQueryParamName}"` : ''}:`
|
|
1932
|
+
: '🌐 URL Completa de Navegación (ej., http://localhost:3000/):';
|
|
1849
1933
|
|
|
1850
1934
|
const d = await text({
|
|
1851
1935
|
message: chalk.cyan(promptMsg),
|
|
@@ -1857,13 +1941,13 @@ async function main() {
|
|
|
1857
1941
|
});
|
|
1858
1942
|
|
|
1859
1943
|
if (isCancel(d)) {
|
|
1860
|
-
cancel('
|
|
1944
|
+
cancel('Misión abortada. Regresando al menú...');
|
|
1861
1945
|
agentMode = false;
|
|
1862
1946
|
prompt = "";
|
|
1863
1947
|
skipMenu = false;
|
|
1864
1948
|
continue;
|
|
1865
1949
|
}
|
|
1866
|
-
discoverPath = d;
|
|
1950
|
+
discoverPath = typeof d === 'string' ? d.trim() : d;
|
|
1867
1951
|
}
|
|
1868
1952
|
}
|
|
1869
1953
|
|
|
@@ -1873,7 +1957,7 @@ async function main() {
|
|
|
1873
1957
|
const libDotEnv = parseDotEnvFile(path.join(PROJECT_ROOT, '.env'));
|
|
1874
1958
|
const dotEnv = { ...userDotEnv, ...libDotEnv };
|
|
1875
1959
|
|
|
1876
|
-
//
|
|
1960
|
+
// ── API Key and Quota Validation ──
|
|
1877
1961
|
const { validateApiKey, startMission: requestMission } = await import('../src/arcalityClient.mjs');
|
|
1878
1962
|
|
|
1879
1963
|
const validation = await validateApiKey();
|
|
@@ -1887,13 +1971,13 @@ async function main() {
|
|
|
1887
1971
|
}
|
|
1888
1972
|
if (!validation.valid) {
|
|
1889
1973
|
const errorMessages = {
|
|
1890
|
-
'no_api_key': '
|
|
1891
|
-
'invalid_format': '
|
|
1892
|
-
'invalid_api_key': '
|
|
1893
|
-
'plan_expired': '
|
|
1974
|
+
'no_api_key': '🔑 API Key no encontrada.\n Configúrala vía `arcality init` o en .env (ARCALITY_API_KEY)',
|
|
1975
|
+
'invalid_format': '🔑 API Key inválida. Debe comenzar con "arc_"',
|
|
1976
|
+
'invalid_api_key': '🔑 API Key no reconocida por el servidor',
|
|
1977
|
+
'plan_expired': '💳 Tu plan ha expirado',
|
|
1894
1978
|
'backend_unavailable': 'No se pudo conectar con Arcality. La herramienta requiere conexion al backend para validar la cuenta.'
|
|
1895
1979
|
};
|
|
1896
|
-
note(chalk.red(errorMessages[validation.error] || `Error: ${validation.error}`), '
|
|
1980
|
+
note(chalk.red(errorMessages[validation.error] || `Error: ${validation.error}`), '❌ Autenticación');
|
|
1897
1981
|
agentMode = false;
|
|
1898
1982
|
prompt = "";
|
|
1899
1983
|
skipMenu = false;
|
|
@@ -1902,11 +1986,11 @@ async function main() {
|
|
|
1902
1986
|
|
|
1903
1987
|
// Show plan info
|
|
1904
1988
|
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 ? '
|
|
1989
|
+
const dailyLimitDisplay = validation.daily_limit === -1 ? '∞' : (validation.daily_limit || 35);
|
|
1990
|
+
const remainingDisplay = validation.remaining >= 999999 ? '∞' : validation.remaining;
|
|
1907
1991
|
note(
|
|
1908
|
-
chalk.gray('
|
|
1909
|
-
chalk.gray('
|
|
1992
|
+
chalk.gray('Conexión: ') + chalk.cyanBright('SECURE ') + modeLabel + '\n' +
|
|
1993
|
+
chalk.gray('Autorización: ') + chalk.white(validation.plan || 'internal') + '\n' +
|
|
1910
1994
|
chalk.gray('Misiones: ') + chalk.white(`${validation.daily_used || 0}/${dailyLimitDisplay}`) + '\n' +
|
|
1911
1995
|
chalk.gray('Reservas: ') + chalk.white(remainingDisplay),
|
|
1912
1996
|
'Enlace Arcality'
|
|
@@ -1944,15 +2028,15 @@ async function main() {
|
|
|
1944
2028
|
const projects = data.projects || [];
|
|
1945
2029
|
|
|
1946
2030
|
const projOptions = projects.map(p => ({ label: p.name || p.Name, value: p.id || p.Id }));
|
|
1947
|
-
projOptions.push({ label: '
|
|
2031
|
+
projOptions.push({ label: '➕ Crear nuevo proyecto', value: 'new' });
|
|
1948
2032
|
|
|
1949
2033
|
const projSelection = await select({
|
|
1950
|
-
message: chalk.cyan('Elige un proyecto para esta
|
|
2034
|
+
message: chalk.cyan('Elige un proyecto para esta misión:'),
|
|
1951
2035
|
options: projOptions
|
|
1952
2036
|
});
|
|
1953
2037
|
|
|
1954
2038
|
if (isCancel(projSelection)) {
|
|
1955
|
-
cancel('
|
|
2039
|
+
cancel('Misión abortada.');
|
|
1956
2040
|
agentMode = false; prompt = ""; skipMenu = false; continue;
|
|
1957
2041
|
}
|
|
1958
2042
|
|
|
@@ -1978,9 +2062,9 @@ async function main() {
|
|
|
1978
2062
|
note(chalk.red('El backend no devolvio un Project ID valido.'), 'Proyecto no conectado');
|
|
1979
2063
|
selectedProjectId = null;
|
|
1980
2064
|
}
|
|
1981
|
-
if (selectedProjectId) note(chalk.green(
|
|
2065
|
+
if (selectedProjectId) note(chalk.green(`✅ Proyecto creado: ${newName}`));
|
|
1982
2066
|
} else {
|
|
1983
|
-
note(chalk.red(
|
|
2067
|
+
note(chalk.red(`❌ Falló al crear proyecto.`));
|
|
1984
2068
|
}
|
|
1985
2069
|
} else {
|
|
1986
2070
|
selectedProjectId = projSelection;
|
|
@@ -2013,8 +2097,12 @@ async function main() {
|
|
|
2013
2097
|
continue;
|
|
2014
2098
|
}
|
|
2015
2099
|
|
|
2016
|
-
//
|
|
2017
|
-
const
|
|
2100
|
+
// ── Start Mission ──
|
|
2101
|
+
const effectiveBaseUrl = projectConfig?.project?.baseUrl || dotEnv.BASE_URL || process.env.BASE_URL || '';
|
|
2102
|
+
const missionTargetUrl = discoverPath === '' && effectiveBaseUrl
|
|
2103
|
+
? effectiveBaseUrl
|
|
2104
|
+
: (discoverPath || '/');
|
|
2105
|
+
const mission = await requestMission(prompt, missionTargetUrl);
|
|
2018
2106
|
if (!mission.allowed) {
|
|
2019
2107
|
if (mission.error === 'mission_limit_exceeded') {
|
|
2020
2108
|
note(
|
|
@@ -2044,19 +2132,19 @@ async function main() {
|
|
|
2044
2132
|
|
|
2045
2133
|
const s = spinner();
|
|
2046
2134
|
if (!process.argv.includes('--debug')) {
|
|
2047
|
-
s.start(
|
|
2135
|
+
s.start(`◉ [ARCALITY INICIADO] Ejecutando directiva: "${prompt}" ${chalk.gray(`(${mission.daily_used}/${mission.daily_limit})`)}`);
|
|
2048
2136
|
} else {
|
|
2049
|
-
console.log(chalk.cyan(`\n
|
|
2137
|
+
console.log(chalk.cyan(`\n◉ [ARCALITY INICIADO] Ejecutando directiva: "${prompt}" ${chalk.gray(`(${mission.daily_used}/${mission.daily_limit})`)}`));
|
|
2050
2138
|
}
|
|
2051
2139
|
|
|
2052
2140
|
const finalProjectId = process.env.ARCALITY_PROJECT_ID || selectedProjectId;
|
|
2053
2141
|
|
|
2054
|
-
//
|
|
2142
|
+
// ── Cancelación Manual (Ctrl+C) — Estrategia bulletproof ──
|
|
2055
2143
|
// El problema: @clack/prompts registra su propio SIGINT handler cada tick del spinner,
|
|
2056
2144
|
// que llama a process.exit(0) directamente. removeAllListeners() no es suficiente porque
|
|
2057
|
-
// Clack lo re-registra en cada frame de
|
|
2145
|
+
// Clack lo re-registra en cada frame de animación.
|
|
2058
2146
|
//
|
|
2059
|
-
// La
|
|
2147
|
+
// La solución: interceptar process.exit() antes de que Clack lo llame,
|
|
2060
2148
|
// y usar prependListener para que NUESTRO handler siempre se ejecute primero.
|
|
2061
2149
|
|
|
2062
2150
|
let activeChildProcess = null;
|
|
@@ -2068,18 +2156,18 @@ async function main() {
|
|
|
2068
2156
|
const _origProcessExit = process.exit.bind(process);
|
|
2069
2157
|
process.exit = (code) => {
|
|
2070
2158
|
if (_missionActive) {
|
|
2071
|
-
// Clack (u otra lib)
|
|
2072
|
-
// Nuestro cancelHandler ya
|
|
2073
|
-
// El proceso se
|
|
2159
|
+
// Clack (u otra lib) está intentando matar el proceso — lo bloqueamos.
|
|
2160
|
+
// Nuestro cancelHandler ya habrá sido invocado vía prependListener.
|
|
2161
|
+
// El proceso se cerrará naturalmente cuando el child process termine.
|
|
2074
2162
|
return;
|
|
2075
2163
|
}
|
|
2076
2164
|
_origProcessExit(code);
|
|
2077
2165
|
};
|
|
2078
2166
|
|
|
2079
|
-
//
|
|
2080
|
-
// @clack/prompts pausea stdin y desactiva raw mode al terminar un men
|
|
2081
|
-
// Debemos reactivarlo
|
|
2082
|
-
// siga funcionando durante toda la
|
|
2167
|
+
// ── RESTORE RAW MODE FOR WINDOWS FIX ──
|
|
2168
|
+
// @clack/prompts pausea stdin y desactiva raw mode al terminar un menú.
|
|
2169
|
+
// Debemos reactivarlo aquí para que nuestro interceptor global de Ctrl+C
|
|
2170
|
+
// siga funcionando durante toda la misión.
|
|
2083
2171
|
if (process.stdin.isTTY && process.stdin.setRawMode) {
|
|
2084
2172
|
try {
|
|
2085
2173
|
process.stdin.setRawMode(true);
|
|
@@ -2090,21 +2178,21 @@ async function main() {
|
|
|
2090
2178
|
const cancelHandler = () => {
|
|
2091
2179
|
sigintCount++;
|
|
2092
2180
|
if (sigintCount > 3) {
|
|
2093
|
-
console.log(chalk.red('\n
|
|
2181
|
+
console.log(chalk.red('\n🛑 Forzando salida inmediata...'));
|
|
2094
2182
|
_missionActive = false;
|
|
2095
2183
|
_origProcessExit(1);
|
|
2096
2184
|
return;
|
|
2097
2185
|
}
|
|
2098
2186
|
|
|
2099
2187
|
if (userCanceledMission) {
|
|
2100
|
-
console.log(chalk.yellow('\n
|
|
2188
|
+
console.log(chalk.yellow('\n⏳ Cancelación en progreso, esperando reporte... (Ctrl+C x4 para forzar cierre)'));
|
|
2101
2189
|
return;
|
|
2102
2190
|
}
|
|
2103
2191
|
userCanceledMission = true;
|
|
2104
2192
|
|
|
2105
|
-
console.log(chalk.yellow('\n\
|
|
2193
|
+
console.log(chalk.yellow('\n\n⚠️ [ARCALITY] Directiva abortada por el operador.'));
|
|
2106
2194
|
if (!process.argv.includes('--debug')) {
|
|
2107
|
-
try { s.stop(chalk.yellow('
|
|
2195
|
+
try { s.stop(chalk.yellow('⚠️ Directiva abortada.')); } catch { /* ignore */ }
|
|
2108
2196
|
}
|
|
2109
2197
|
|
|
2110
2198
|
// Terminar el proceso hijo de Playwright directamente (Windows-safe)
|
|
@@ -2136,8 +2224,8 @@ async function main() {
|
|
|
2136
2224
|
} catch { /* silencioso */ }
|
|
2137
2225
|
|
|
2138
2226
|
console.log(chalk.gray(' >> Esperando que Playwright cierre y genere el reporte...'));
|
|
2139
|
-
// Playwright
|
|
2140
|
-
// y
|
|
2227
|
+
// Playwright también recibe el SIGINT nativamente desde la terminal Windows
|
|
2228
|
+
// y cerrará su proceso limpiamente, lo que resolverá la Promise del run() → finally
|
|
2141
2229
|
};
|
|
2142
2230
|
|
|
2143
2231
|
// prependListener asegura que nuestro handler se ejecuta ANTES que el de Clack
|
|
@@ -2145,19 +2233,20 @@ async function main() {
|
|
|
2145
2233
|
process.prependListener('SIGTERM', cancelHandler);
|
|
2146
2234
|
|
|
2147
2235
|
|
|
2148
|
-
// Build env for the runner
|
|
2149
|
-
// ARCALITY_API_URL must be explicit
|
|
2236
|
+
// Build env for the runner — merge arcality.config values.
|
|
2237
|
+
// ARCALITY_API_URL must be explicit — it comes from the library's internal config,
|
|
2150
2238
|
// not from the user's project .env, so we inject it directly via getApiUrl().
|
|
2151
2239
|
const fallbackBaseUrl = (!projectConfig && discoverPath?.startsWith('http')) ? discoverPath : undefined;
|
|
2152
2240
|
|
|
2153
2241
|
const mergedEnv = {
|
|
2154
2242
|
...dotEnv,
|
|
2155
2243
|
...process.env,
|
|
2156
|
-
|
|
2244
|
+
ARCALITY_ROOT: PROJECT_ROOT,
|
|
2245
|
+
ARCALITY_API_URL: getApiUrl(), // Internal URL — always injected explicitly
|
|
2157
2246
|
ARCALITY_PROJECT_ID: finalProjectId,
|
|
2158
2247
|
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 || '', //
|
|
2248
|
+
ARCALITY_MISSION_ID: mission.mission_id || mission.missionId || mission.MissionId || '', // ← Propagate mission_id to every proxy call
|
|
2249
|
+
ARCALITY_ORG_ID: validation.organization_id || validation.organizationId || validation.OrganizationId || process.env.ARCALITY_ORG_ID || '', // ← Propagate org_id for pattern persistence
|
|
2161
2250
|
BASE_URL: projectConfig?.project?.baseUrl || dotEnv.BASE_URL || process.env.BASE_URL || fallbackBaseUrl || 'http://localhost:3000',
|
|
2162
2251
|
LOGIN_USER: projectConfig?.auth?.username || dotEnv.LOGIN_USER || process.env.LOGIN_USER,
|
|
2163
2252
|
LOGIN_PASSWORD: projectConfig?.auth?.password || dotEnv.LOGIN_PASSWORD || process.env.LOGIN_PASSWORD,
|
|
@@ -2168,7 +2257,8 @@ async function main() {
|
|
|
2168
2257
|
CONTEXT_DIR: process.env.CONTEXT_DIR,
|
|
2169
2258
|
REPORTS_DIR: process.env.REPORTS_DIR,
|
|
2170
2259
|
SMART_PROMPT: prompt,
|
|
2171
|
-
TARGET_PATH: discoverPath
|
|
2260
|
+
TARGET_PATH: discoverPath ?? '/',
|
|
2261
|
+
ARCALITY_DB_VALIDATIONS_JSON: missionDbValidations.length > 0 ? JSON.stringify(missionDbValidations) : (process.env.ARCALITY_DB_VALIDATIONS_JSON || process.env.ARCALITY_DB_VALIDATIONS || '')
|
|
2172
2262
|
};
|
|
2173
2263
|
|
|
2174
2264
|
if (finalProjectId) {
|
|
@@ -2176,17 +2266,17 @@ async function main() {
|
|
|
2176
2266
|
console.log(chalk.gray(`>> ARCALITY_ORG_ID: ${mergedEnv.ARCALITY_ORG_ID || 'NONE'}`));
|
|
2177
2267
|
}
|
|
2178
2268
|
|
|
2179
|
-
//
|
|
2269
|
+
// ── Resolve execution paths ──
|
|
2180
2270
|
// tsx and playwright are HOISTED to ORIGINAL_CWD/node_modules when installed via npm.
|
|
2181
2271
|
// We run from ORIGINAL_CWD so relative paths for playwright work.
|
|
2182
2272
|
// For the test file and config (inside the arcality package), we use absolute paths.
|
|
2183
2273
|
//
|
|
2184
|
-
// KEY: tsx loader uses file:// URL (pathToFileURL) so spaces encode as %20
|
|
2274
|
+
// KEY: tsx loader uses file:// URL (pathToFileURL) so spaces encode as %20 — no quoting needed.
|
|
2185
2275
|
|
|
2186
2276
|
const testFile = path.join(PROJECT_ROOT, 'tests', '_helpers', 'agentic-runner.spec.ts');
|
|
2187
2277
|
const configFile = path.join(PROJECT_ROOT, 'playwright.config.js');
|
|
2188
2278
|
|
|
2189
|
-
// Resolve playwright CLI
|
|
2279
|
+
// Resolve playwright CLI — CRITICAL singleton alignment:
|
|
2190
2280
|
//
|
|
2191
2281
|
// @playwright/test uses `playwright` internally. When npm detects a version mismatch
|
|
2192
2282
|
// between the top-level `playwright` and what `@playwright/test` needs, it installs
|
|
@@ -2194,15 +2284,15 @@ async function main() {
|
|
|
2194
2284
|
//
|
|
2195
2285
|
// Playwright's test runner tracks the active suite via a module-level singleton
|
|
2196
2286
|
// (currentSuite). If the CLI uses a DIFFERENT playwright instance than the one
|
|
2197
|
-
// @playwright/test loaded, their singletons are out of sync
|
|
2287
|
+
// @playwright/test loaded, their singletons are out of sync → "did not expect test()
|
|
2198
2288
|
// to be called here".
|
|
2199
2289
|
//
|
|
2200
2290
|
// Fix: ALWAYS prefer the playwright nested INSIDE @playwright/test, because that is
|
|
2201
2291
|
// guaranteed to be the same instance that @playwright/test uses for its internals.
|
|
2202
2292
|
//
|
|
2203
2293
|
// Resolution priority:
|
|
2204
|
-
// 1. <project>/@playwright/test/node_modules/playwright/cli.js
|
|
2205
|
-
// 2. <project>/playwright/cli.js
|
|
2294
|
+
// 1. <project>/@playwright/test/node_modules/playwright/cli.js ← nested (safe)
|
|
2295
|
+
// 2. <project>/playwright/cli.js ← sibling top-level
|
|
2206
2296
|
// (repeated for ORIGINAL_CWD and PROJECT_ROOT)
|
|
2207
2297
|
let playwrightCli;
|
|
2208
2298
|
try {
|
|
@@ -2221,9 +2311,9 @@ async function main() {
|
|
|
2221
2311
|
|
|
2222
2312
|
if (!testRunnerPath) throw resolveError;
|
|
2223
2313
|
|
|
2224
|
-
const testRunnerDir = path.dirname(testRunnerPath); //
|
|
2314
|
+
const testRunnerDir = path.dirname(testRunnerPath); // → .../node_modules/@playwright/test/
|
|
2225
2315
|
|
|
2226
|
-
// Priority 1: playwright nested INSIDE @playwright/test
|
|
2316
|
+
// Priority 1: playwright nested INSIDE @playwright/test — same singleton as the package uses
|
|
2227
2317
|
const nestedCli = path.join(testRunnerDir, 'node_modules', 'playwright', 'cli.js');
|
|
2228
2318
|
// Priority 2: playwright as a top-level sibling (clean installs, versions aligned)
|
|
2229
2319
|
const siblingCli = path.join(testRunnerDir, '..', '..', 'playwright', 'cli.js');
|
|
@@ -2242,7 +2332,7 @@ async function main() {
|
|
|
2242
2332
|
console.log(chalk.gray(`[DEBUG] Using playwright CLI: ${playwrightCli}`));
|
|
2243
2333
|
}
|
|
2244
2334
|
} catch (e) {
|
|
2245
|
-
console.error(chalk.red('\n[FATAL]
|
|
2335
|
+
console.error(chalk.red('\n[FATAL] Núcleo de Playwright no encontrado. Asegúrate de que @playwright/test esté instalado.'));
|
|
2246
2336
|
console.error(chalk.red(e.message));
|
|
2247
2337
|
console.error(chalk.yellow('\nConsejo: Ejecuta `npm install @playwright/test` en tu directorio de proyecto.'));
|
|
2248
2338
|
_missionActive = false;
|
|
@@ -2254,9 +2344,9 @@ async function main() {
|
|
|
2254
2344
|
const postProcessSigint = () => {
|
|
2255
2345
|
_postProcessSigintCount++;
|
|
2256
2346
|
if (_postProcessSigintCount === 1) {
|
|
2257
|
-
console.log(chalk.yellow('\n
|
|
2347
|
+
console.log(chalk.yellow('\n⏳ Finalizando reporte de telemetría, por favor espera... (Ctrl+C de nuevo para salir ya)'));
|
|
2258
2348
|
} else {
|
|
2259
|
-
console.log(chalk.red('\n
|
|
2349
|
+
console.log(chalk.red('\n🛑 Saliendo forzado por el usuario.'));
|
|
2260
2350
|
process.removeAllListeners('SIGINT');
|
|
2261
2351
|
process.removeAllListeners('SIGTERM');
|
|
2262
2352
|
process.exit(0);
|
|
@@ -2274,20 +2364,20 @@ async function main() {
|
|
|
2274
2364
|
}
|
|
2275
2365
|
} else
|
|
2276
2366
|
|
|
2277
|
-
//
|
|
2367
|
+
// ── Auto-compilar el runner antes de ejecutar Playwright para asegurar que los cambios de rama/merge estén aplicados ──
|
|
2278
2368
|
try {
|
|
2279
2369
|
if (!process.argv.includes('--debug')) {
|
|
2280
|
-
s.message('
|
|
2370
|
+
s.message('⚙️ Compilando agente de pruebas (Arcality Runner)...');
|
|
2281
2371
|
} else {
|
|
2282
|
-
console.log(chalk.cyan('
|
|
2372
|
+
console.log(chalk.cyan('⚙️ Compilando agente de pruebas (Arcality Runner)...'));
|
|
2283
2373
|
}
|
|
2284
2374
|
await ensureRunnerBundle({ debug: process.argv.includes('--debug') });
|
|
2285
2375
|
} catch (err) {
|
|
2286
|
-
console.warn(chalk.yellow(`\
|
|
2376
|
+
console.warn(chalk.yellow(`\n⚠️ Advertencia: No se pudo auto-compilar el runner: ${err.message}`));
|
|
2287
2377
|
}
|
|
2288
2378
|
|
|
2289
2379
|
try {
|
|
2290
|
-
// No test file path is passed
|
|
2380
|
+
// No test file path is passed — playwright.config.js uses testMatch:'agentic-runner.spec.ts'
|
|
2291
2381
|
// This eliminates the Windows regex path issue permanently on any user's machine.
|
|
2292
2382
|
await run('node', [
|
|
2293
2383
|
'--no-warnings',
|
|
@@ -2299,7 +2389,7 @@ async function main() {
|
|
|
2299
2389
|
cwd: PROJECT_ROOT,
|
|
2300
2390
|
env: mergedEnv,
|
|
2301
2391
|
onStatus: (msg) => { if (!process.argv.includes('--debug')) s.message(msg); else console.log(chalk.gray(`>> Status: ${msg}`)); },
|
|
2302
|
-
onSpawn: (child) => { activeChildProcess = child; } //
|
|
2392
|
+
onSpawn: (child) => { activeChildProcess = child; } // ← Capturar referencia para cancelHandler
|
|
2303
2393
|
});
|
|
2304
2394
|
|
|
2305
2395
|
process.removeAllListeners('SIGINT');
|
|
@@ -2310,11 +2400,11 @@ async function main() {
|
|
|
2310
2400
|
if (userCanceledMission) {
|
|
2311
2401
|
missionResult = 'canceled';
|
|
2312
2402
|
finalFailReason = 'user_canceled';
|
|
2313
|
-
// Detener el spinner
|
|
2403
|
+
// Detener el spinner explícitamente antes de salir
|
|
2314
2404
|
if (!process.argv.includes('--debug')) {
|
|
2315
|
-
try { s.stop(chalk.yellow('
|
|
2405
|
+
try { s.stop(chalk.yellow('⚠️ Misión cancelada por el usuario.')); } catch { }
|
|
2316
2406
|
}
|
|
2317
|
-
return; // El handler ya
|
|
2407
|
+
return; // El handler ya tomó el control, finally se ejecutará
|
|
2318
2408
|
}
|
|
2319
2409
|
|
|
2320
2410
|
const persistedMissionResult = readMissionResult(
|
|
@@ -2346,24 +2436,24 @@ async function main() {
|
|
|
2346
2436
|
if (!process.argv.includes('--debug')) s.stop(chalk.green('\u2705 Mision completada exitosamente.'));
|
|
2347
2437
|
else console.log(chalk.green('\u2705 Mision completada exitosamente.'));
|
|
2348
2438
|
|
|
2349
|
-
//
|
|
2439
|
+
// ── Ping Project ──
|
|
2350
2440
|
await pingProject(finalProjectId, process.env.ARCALITY_API_KEY);
|
|
2351
2441
|
|
|
2352
|
-
//
|
|
2442
|
+
// ── Save Mission YAML ──
|
|
2353
2443
|
let saveDecision = 'no';
|
|
2354
2444
|
if (!skipSavePrompt && !batchMode) {
|
|
2355
2445
|
saveDecision = await select({
|
|
2356
|
-
message: '
|
|
2446
|
+
message: '¿Quieres guardar esta misión para ejecutarla de nuevo más tarde?',
|
|
2357
2447
|
options: [
|
|
2358
|
-
{ label: '
|
|
2359
|
-
{ label: '
|
|
2448
|
+
{ label: '✅ Sí, guardar como YAML', value: 'yes' },
|
|
2449
|
+
{ label: '❌ No, gracias', value: 'no' }
|
|
2360
2450
|
]
|
|
2361
2451
|
});
|
|
2362
2452
|
}
|
|
2363
2453
|
|
|
2364
2454
|
if (saveDecision === 'yes') {
|
|
2365
2455
|
const name = await text({
|
|
2366
|
-
message: 'Nombre de la
|
|
2456
|
+
message: 'Nombre de la misión (ej., login_valido):',
|
|
2367
2457
|
placeholder: 'my_mission',
|
|
2368
2458
|
validate: v => v.length < 3 ? 'Nombre muy corto' : undefined
|
|
2369
2459
|
});
|
|
@@ -2420,9 +2510,9 @@ async function main() {
|
|
|
2420
2510
|
fs.mkdirSync(yamlOutputTargetDir, { recursive: true });
|
|
2421
2511
|
const yamlPathOutput = path.join(yamlOutputTargetDir, `${safeName}.yaml`);
|
|
2422
2512
|
fs.writeFileSync(yamlPathOutput, yamlData);
|
|
2423
|
-
note(chalk.green(
|
|
2513
|
+
note(chalk.green(`✅ Misión guardada en: ${yamlPathOutput}`), 'Persistencia Arcality');
|
|
2424
2514
|
} else {
|
|
2425
|
-
note(chalk.green(
|
|
2515
|
+
note(chalk.green(`✅ Misión guardada en: ${yamlPathMissions}`), 'Persistencia Arcality');
|
|
2426
2516
|
}
|
|
2427
2517
|
}
|
|
2428
2518
|
}
|
|
@@ -2454,7 +2544,7 @@ async function main() {
|
|
|
2454
2544
|
if (userCanceledMission) {
|
|
2455
2545
|
missionResult = 'canceled';
|
|
2456
2546
|
finalFailReason = 'user_canceled';
|
|
2457
|
-
finalFailReasoning = 'La
|
|
2547
|
+
finalFailReasoning = 'La misión fue detenida manualmente por el operador (Ctrl+C). Esto no es una falla técnica del sistema.';
|
|
2458
2548
|
await new Promise(r => setTimeout(r, 800));
|
|
2459
2549
|
}
|
|
2460
2550
|
|
|
@@ -2463,9 +2553,9 @@ async function main() {
|
|
|
2463
2553
|
|
|
2464
2554
|
const sRep = spinner();
|
|
2465
2555
|
if (!process.argv.includes('--debug')) {
|
|
2466
|
-
sRep.start('
|
|
2556
|
+
sRep.start('📊 Generando reporte...');
|
|
2467
2557
|
} else {
|
|
2468
|
-
console.log('
|
|
2558
|
+
console.log('📊 Generando reporte...');
|
|
2469
2559
|
}
|
|
2470
2560
|
|
|
2471
2561
|
await run("node", [path.join(PROJECT_ROOT, "scripts/rebrand-report.mjs")]).catch(() => { });
|
|
@@ -2496,7 +2586,7 @@ async function main() {
|
|
|
2496
2586
|
}
|
|
2497
2587
|
} catch(e) {}
|
|
2498
2588
|
|
|
2499
|
-
//
|
|
2589
|
+
// ── Upload Evidence (solo si NO fue cancelado por usuario) ──
|
|
2500
2590
|
if (missionResult !== 'canceled') {
|
|
2501
2591
|
try {
|
|
2502
2592
|
const { getEvidenceSasToken } = await import('../src/arcalityClient.mjs');
|
|
@@ -2510,7 +2600,7 @@ async function main() {
|
|
|
2510
2600
|
|
|
2511
2601
|
if (sasData && actualSasUrl && actualBasePath) {
|
|
2512
2602
|
if (!process.argv.includes('--debug')) {
|
|
2513
|
-
sRep.start('
|
|
2603
|
+
sRep.start('☁️ Subiendo evidencia a Azure...');
|
|
2514
2604
|
}
|
|
2515
2605
|
process.removeAllListeners('SIGINT');
|
|
2516
2606
|
process.removeAllListeners('SIGTERM');
|
|
@@ -2524,26 +2614,26 @@ async function main() {
|
|
|
2524
2614
|
finalReportUrl = `${parsedSas.origin}${parsedSas.pathname}/${actualBasePath}/index.html`;
|
|
2525
2615
|
} catch(e) {}
|
|
2526
2616
|
|
|
2527
|
-
if (!process.argv.includes('--debug')) sRep.stop(chalk.green('
|
|
2617
|
+
if (!process.argv.includes('--debug')) sRep.stop(chalk.green('✅ Evidencia subida exitosamente.'));
|
|
2528
2618
|
}
|
|
2529
2619
|
} catch (e) {
|
|
2530
|
-
if (process.argv.includes('--debug')) console.log(chalk.yellow(
|
|
2531
|
-
if (!process.argv.includes('--debug')) sRep.stop(chalk.yellow('
|
|
2620
|
+
if (process.argv.includes('--debug')) console.log(chalk.yellow(`⚠️ Could not upload evidence: ${e.message}`));
|
|
2621
|
+
if (!process.argv.includes('--debug')) sRep.stop(chalk.yellow('⚠️ Fallo al subir evidencia.'));
|
|
2532
2622
|
}
|
|
2533
2623
|
} else {
|
|
2534
|
-
console.log(chalk.gray(' >> Saltando subida de evidencia (la
|
|
2624
|
+
console.log(chalk.gray(' >> Saltando subida de evidencia (la misión fue cancelada).'));
|
|
2535
2625
|
}
|
|
2536
2626
|
|
|
2537
|
-
//
|
|
2627
|
+
// ── End Mission — SIEMPRE se llama, independientemente de si la evidencia subió ──
|
|
2538
2628
|
try {
|
|
2539
2629
|
const { endMission } = await import('../src/arcalityClient.mjs');
|
|
2540
2630
|
await Promise.race([
|
|
2541
2631
|
endMission(mission.mission_id, missionResult || 'failed', finalUsageData, finalFailReason, finalReportUrl, finalFailReasoning),
|
|
2542
2632
|
new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), 8000))
|
|
2543
2633
|
]);
|
|
2544
|
-
if (process.argv.includes('--debug')) console.log(chalk.gray(`[DEBUG] endMission called
|
|
2634
|
+
if (process.argv.includes('--debug')) console.log(chalk.gray(`[DEBUG] endMission called → result: ${missionResult}, failReason: ${finalFailReason}`));
|
|
2545
2635
|
} catch (e) {
|
|
2546
|
-
if (process.argv.includes('--debug')) console.log(chalk.yellow(
|
|
2636
|
+
if (process.argv.includes('--debug')) console.log(chalk.yellow(`⚠️ Could not end mission in backend: ${e.message}`));
|
|
2547
2637
|
}
|
|
2548
2638
|
|
|
2549
2639
|
if (globalReportProcess) {
|
|
@@ -2558,10 +2648,10 @@ async function main() {
|
|
|
2558
2648
|
}
|
|
2559
2649
|
|
|
2560
2650
|
if (!batchMode && fs.existsSync(arcalityPath)) {
|
|
2561
|
-
console.log(chalk.blue(
|
|
2651
|
+
console.log(chalk.blue(`🌐 Abriendo Reporte de Arcality: ${arcalityPath}`));
|
|
2562
2652
|
openLocalFile(arcalityPath);
|
|
2563
2653
|
} else if (!fs.existsSync(arcalityPath)) {
|
|
2564
|
-
console.log(chalk.yellow(
|
|
2654
|
+
console.log(chalk.yellow(`⚠️ Reporte no generado (la prueba pudo haber fallado antes de completarse).`));
|
|
2565
2655
|
}
|
|
2566
2656
|
|
|
2567
2657
|
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
@@ -2572,7 +2662,7 @@ async function main() {
|
|
|
2572
2662
|
new Promise(resolve => setTimeout(resolve, 5000))
|
|
2573
2663
|
]);
|
|
2574
2664
|
|
|
2575
|
-
// Limpiar handlers del post-proceso ANTES de esperar Enter, ya que toda la
|
|
2665
|
+
// Limpiar handlers del post-proceso ANTES de esperar Enter, ya que toda la telemetría y reportes ya finalizaron
|
|
2576
2666
|
process.removeListener('SIGINT', postProcessSigint);
|
|
2577
2667
|
process.removeListener('SIGTERM', postProcessSigint);
|
|
2578
2668
|
|
|
@@ -2584,10 +2674,10 @@ async function main() {
|
|
|
2584
2674
|
}
|
|
2585
2675
|
|
|
2586
2676
|
if (false && !batchMode) {
|
|
2587
|
-
// Esperar Enter para regresar al
|
|
2677
|
+
// Esperar Enter para regresar al menú
|
|
2588
2678
|
// 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
|
|
2679
|
+
// no procesa Enter correctamente cuando setRawMode(true) está activo.
|
|
2680
|
+
console.log(chalk.gray('\n Presiona Enter para regresar al menú...'));
|
|
2591
2681
|
await new Promise(resolve => {
|
|
2592
2682
|
const onKey = (key) => {
|
|
2593
2683
|
const keyStr = String(key);
|
|
@@ -2599,7 +2689,7 @@ async function main() {
|
|
|
2599
2689
|
process.stdin.setRawMode(false);
|
|
2600
2690
|
}
|
|
2601
2691
|
// FIX: pausar stdin si vamos a salir para que el event loop no se quede colgado.
|
|
2602
|
-
// Si volvemos al men
|
|
2692
|
+
// Si volvemos al menú, no lo pausamos (para evitar el bug de exit en clack).
|
|
2603
2693
|
if (skipMenu) {
|
|
2604
2694
|
process.stdin.pause();
|
|
2605
2695
|
}
|
|
@@ -2612,22 +2702,22 @@ async function main() {
|
|
|
2612
2702
|
}
|
|
2613
2703
|
};
|
|
2614
2704
|
|
|
2615
|
-
//
|
|
2705
|
+
// ── BUGFIX: @clack/prompts llama explícitamente a process.stdin.pause()
|
|
2616
2706
|
// al terminar cada select()/text(). En Node.js, agregar un listener 'data'
|
|
2617
|
-
// a un stream
|
|
2618
|
-
// Resultado: el event loop queda
|
|
2707
|
+
// a un stream EXPLÍCITAMENTE pausado NO lo reactiva automáticamente.
|
|
2708
|
+
// Resultado: el event loop queda vacío y el proceso termina antes de que
|
|
2619
2709
|
// el usuario pueda presionar Enter.
|
|
2620
|
-
//
|
|
2710
|
+
// Solución: llamar resume() explícitamente antes de registrar el listener.
|
|
2621
2711
|
if (process.stdin.isTTY && process.stdin.setRawMode) {
|
|
2622
2712
|
process.stdin.setRawMode(true); // raw mode para capturar Enter inmediatamente
|
|
2623
2713
|
}
|
|
2624
|
-
process.stdin.resume(); //
|
|
2714
|
+
process.stdin.resume(); // ← CRÍTICO: reactiva stdin para mantener el event loop vivo
|
|
2625
2715
|
process.stdin.on('data', onKey);
|
|
2626
2716
|
});
|
|
2627
2717
|
}
|
|
2628
2718
|
|
|
2629
2719
|
if (skipMenu) {
|
|
2630
|
-
outro(chalk.cyan('
|
|
2720
|
+
outro(chalk.cyan('¡Misión terminada! Hasta luego.'));
|
|
2631
2721
|
return;
|
|
2632
2722
|
}
|
|
2633
2723
|
|