@arcadialdev/arcality 2.6.7 → 3.0.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.
@@ -119,7 +119,7 @@ function updateDotEnvFile(updates) {
119
119
  keys.forEach(k => { process.env[k] = updates[k]; });
120
120
  }
121
121
 
122
- function setupEnvironment() {
122
+ function setupEnvironment() {
123
123
 
124
124
  // Use config name from arcality.config or fallback
125
125
  const configName = projectConfig?.project?.name || 'Default';
@@ -133,14 +133,16 @@ function setupEnvironment() {
133
133
  process.env.CONTEXT_DIR = path.join(configDir, 'context');
134
134
  process.env.REPORTS_DIR = path.join(configDir, 'reports');
135
135
 
136
- [process.env.MISSIONS_DIR, process.env.CONTEXT_DIR, process.env.REPORTS_DIR].forEach(dir => {
137
- if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
138
- });
139
-
140
- // Also ensure YAML output dir from arcality.config
141
- if (projectConfig) {
142
- ensureYamlOutputDir(projectConfig, ORIGINAL_CWD);
143
- }
136
+ [process.env.MISSIONS_DIR, process.env.CONTEXT_DIR, process.env.REPORTS_DIR].forEach(dir => {
137
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
138
+ });
139
+ ensureMissionTemplates(process.env.MISSIONS_DIR);
140
+
141
+ // Also ensure YAML output dir from arcality.config
142
+ if (projectConfig) {
143
+ const yamlOutputDir = ensureYamlOutputDir(projectConfig, ORIGINAL_CWD);
144
+ ensureMissionTemplates(yamlOutputDir);
145
+ }
144
146
 
145
147
  // Framework detection from package.json (NO .git)
146
148
  let techStack = 'Not detected';
@@ -157,12 +159,614 @@ function setupEnvironment() {
157
159
  techStack = projectConfig.project.frameworkDetected;
158
160
  }
159
161
 
160
- return { configName, baseUrl, techStack, configDir };
161
- }
162
-
163
- function showBanner() {
162
+ return { configName, baseUrl, techStack, configDir };
163
+ }
164
+
165
+ function isYamlFile(fileName) {
166
+ return fileName.endsWith('.yaml') || fileName.endsWith('.yml');
167
+ }
168
+
169
+ function isCollectionMetaFile(fileName) {
170
+ const lower = String(fileName || '').toLowerCase();
171
+ return lower === '_collection.yaml' || lower === '_collection.yml';
172
+ }
173
+
174
+ function isSystemCollectionDir(dirName) {
175
+ return String(dirName || '').toLowerCase() === '_templates';
176
+ }
177
+
178
+ function toPosixPath(value) {
179
+ return value.split(path.sep).join('/');
180
+ }
181
+
182
+ function sanitizePathSegment(segment) {
183
+ return String(segment || '')
184
+ .trim()
185
+ .replace(/[^a-zA-Z0-9_-]/g, '_')
186
+ .replace(/_+/g, '_')
187
+ .replace(/^_+|_+$/g, '')
188
+ .toLowerCase();
189
+ }
190
+
191
+ function sanitizeCollectionPath(input) {
192
+ const segments = String(input || '')
193
+ .split(/[\\/]+/)
194
+ .map(sanitizePathSegment)
195
+ .filter(Boolean);
196
+
197
+ return segments.join(path.sep);
198
+ }
199
+
200
+ function listYamlFilesInDir(dir) {
201
+ if (!dir || !fs.existsSync(dir)) return [];
202
+ return fs.readdirSync(dir, { withFileTypes: true })
203
+ .filter(entry => entry.isFile() && isYamlFile(entry.name) && !isCollectionMetaFile(entry.name))
204
+ .map(entry => entry.name)
205
+ .sort((a, b) => a.localeCompare(b));
206
+ }
207
+
208
+ function readYamlFile(filePath) {
209
+ try {
210
+ return loadYaml(fs.readFileSync(filePath, 'utf8'));
211
+ } catch {
212
+ return null;
213
+ }
214
+ }
215
+
216
+ function normalizeStringList(value) {
217
+ if (Array.isArray(value)) {
218
+ return value.map(v => String(v).trim()).filter(Boolean);
219
+ }
220
+ if (typeof value === 'string') {
221
+ return value.split(',').map(v => v.trim()).filter(Boolean);
222
+ }
223
+ return [];
224
+ }
225
+
226
+ function readCollectionMeta(dir, rel = '') {
227
+ const candidates = ['_collection.yaml', '_collection.yml']
228
+ .map(name => path.join(dir, name))
229
+ .filter(fs.existsSync);
230
+
231
+ if (candidates.length === 0) {
232
+ return {
233
+ label: rel ? toPosixPath(rel) : 'General (sin carpeta)',
234
+ description: '',
235
+ tags: []
236
+ };
237
+ }
238
+
239
+ const raw = readYamlFile(candidates[0]);
240
+ const meta = raw && typeof raw === 'object' ? raw : {};
241
+ return {
242
+ label: String(meta.name || meta.title || (rel ? toPosixPath(rel) : 'General (sin carpeta)')).trim(),
243
+ description: String(meta.description || '').trim(),
244
+ tags: normalizeStringList(meta.tags),
245
+ defaultPagePath: String(meta.default_page_path || meta.defaultPagePath || meta.start_at || meta.StartAt || '').trim()
246
+ };
247
+ }
248
+
249
+ function normalizeMissionSpec(rawMission, filePath = '') {
250
+ const mission = rawMission && typeof rawMission === 'object' ? rawMission : {};
251
+ const fallbackName = filePath ? path.basename(filePath, path.extname(filePath)) : 'mission';
252
+ const prompt = String(
253
+ mission.prompt ||
254
+ mission.mision ||
255
+ mission.task ||
256
+ ''
257
+ ).trim();
258
+
259
+ return {
260
+ raw: mission,
261
+ name: String(mission.name || fallbackName).trim(),
262
+ prompt,
263
+ page: String(mission.page || '').trim(),
264
+ pagePath: String(mission.page_path || mission.pagePath || '').trim(),
265
+ startAt: String(mission.start_at || mission.startAt || mission.StartAt || '').trim(),
266
+ expectedResult: String(mission.expected_result || mission.expectedResult || '').trim(),
267
+ collection: String(mission.collection || '').trim(),
268
+ suite: String(mission.suite || '').trim(),
269
+ priority: String(mission.priority || '').trim(),
270
+ tags: normalizeStringList(mission.tags),
271
+ timeout: Number.isFinite(Number(mission.timeout)) ? Number(mission.timeout) : null,
272
+ credentialId: String(mission.credential_id || mission.credentialId || '').trim()
273
+ };
274
+ }
275
+
276
+ function listMissionCollections(rootDir) {
277
+ if (!rootDir || !fs.existsSync(rootDir)) return [];
278
+
279
+ const collections = [];
280
+ const visit = (dir, rel = '') => {
281
+ const yamlFiles = listYamlFilesInDir(dir);
282
+ const meta = readCollectionMeta(dir, rel);
283
+ if (yamlFiles.length > 0) {
284
+ collections.push({
285
+ label: meta.label,
286
+ description: meta.description,
287
+ tags: meta.tags,
288
+ value: rel,
289
+ dir,
290
+ count: yamlFiles.length
291
+ });
292
+ }
293
+
294
+ const subdirs = fs.readdirSync(dir, { withFileTypes: true })
295
+ .filter(entry => entry.isDirectory() && !isSystemCollectionDir(entry.name))
296
+ .map(entry => entry.name)
297
+ .sort((a, b) => a.localeCompare(b));
298
+
299
+ for (const subdir of subdirs) {
300
+ visit(path.join(dir, subdir), rel ? path.join(rel, subdir) : subdir);
301
+ }
302
+ };
303
+
304
+ visit(rootDir);
305
+ return collections;
306
+ }
307
+
308
+ function listMissionDirectories(rootDir) {
309
+ if (!rootDir || !fs.existsSync(rootDir)) return [];
310
+
311
+ const dirs = [];
312
+ const visit = (dir, rel = '') => {
313
+ const meta = readCollectionMeta(dir, rel);
314
+ dirs.push({
315
+ label: meta.label,
316
+ description: meta.description,
317
+ tags: meta.tags,
318
+ value: rel,
319
+ dir
320
+ });
321
+
322
+ const subdirs = fs.readdirSync(dir, { withFileTypes: true })
323
+ .filter(entry => entry.isDirectory() && !isSystemCollectionDir(entry.name))
324
+ .map(entry => entry.name)
325
+ .sort((a, b) => a.localeCompare(b));
326
+
327
+ for (const subdir of subdirs) {
328
+ visit(path.join(dir, subdir), rel ? path.join(rel, subdir) : subdir);
329
+ }
330
+ };
331
+
332
+ visit(rootDir);
333
+ return dirs;
334
+ }
335
+
336
+ async function selectMissionYaml(rootDir, collectionMessage = 'Elige la carpeta de misiones:', missionMessage = 'Elige la mision guardada:') {
337
+ const collections = listMissionCollections(rootDir);
338
+ if (collections.length === 0) return null;
339
+
340
+ let collection = collections[0];
341
+ if (collections.length > 1) {
342
+ const selectedCollection = await select({
343
+ message: collectionMessage,
344
+ options: collections.map(c => ({
345
+ label: `${c.label} (${c.count})${c.description ? ` - ${c.description}` : ''}`,
346
+ value: c.value
347
+ }))
348
+ });
349
+ if (isCancel(selectedCollection)) return null;
350
+ collection = collections.find(c => c.value === selectedCollection) || collection;
351
+ }
352
+
353
+ const files = listYamlFilesInDir(collection.dir);
354
+ const selectedFile = await select({
355
+ message: missionMessage,
356
+ options: files.map(file => {
357
+ const missionSpec = normalizeMissionSpec(readYamlFile(path.join(collection.dir, file)), file);
358
+ const labelParts = [missionSpec.name || file];
359
+ if (missionSpec.priority) labelParts.push(`[${missionSpec.priority}]`);
360
+ if (missionSpec.tags.length > 0) labelParts.push(`{${missionSpec.tags.join(', ')}}`);
361
+ return { label: labelParts.join(' '), value: file };
362
+ })
363
+ });
364
+ if (isCancel(selectedFile)) return null;
365
+
366
+ return {
367
+ collection: collection.value,
368
+ file: selectedFile,
369
+ fullPath: path.join(collection.dir, selectedFile),
370
+ collectionLabel: collection.label
371
+ };
372
+ }
373
+
374
+ async function selectSavedMissionSource({ libraryDir, projectDir, rootDir }) {
375
+ const sources = [];
376
+
377
+ const libraryCollections = listMissionCollections(libraryDir);
378
+ if (libraryCollections.length > 0) {
379
+ sources.push({
380
+ key: 'library',
381
+ label: 'Biblioteca Arcality',
382
+ description: 'Misiones archivadas por Arcality',
383
+ dir: libraryDir
384
+ });
385
+ }
386
+
387
+ const projectCollections = projectDir ? listMissionCollections(projectDir) : [];
388
+ if (projectCollections.length > 0) {
389
+ sources.push({
390
+ key: 'project',
391
+ label: 'Carpeta del proyecto',
392
+ description: 'Misiones guardadas junto al proyecto',
393
+ dir: projectDir
394
+ });
395
+ }
396
+
397
+ const rootMissionFiles = rootDir && fs.existsSync(rootDir)
398
+ ? fs.readdirSync(rootDir).filter(file => isYamlFile(file))
399
+ : [];
400
+ if (rootMissionFiles.length > 0) {
401
+ sources.push({
402
+ key: 'root',
403
+ label: 'Raiz del proyecto',
404
+ description: 'Archivos de mision ubicados en la raiz',
405
+ dir: rootDir,
406
+ files: rootMissionFiles
407
+ });
408
+ }
409
+
410
+ if (sources.length === 0) return null;
411
+
412
+ let source = sources[0];
413
+ if (sources.length > 1) {
414
+ const selectedSource = await select({
415
+ message: '¿De dónde quieres abrir la misión?',
416
+ options: sources.map(item => ({
417
+ label: `${item.label}${item.description ? ` - ${item.description}` : ''}`,
418
+ value: item.key
419
+ }))
420
+ });
421
+ if (isCancel(selectedSource)) return null;
422
+ source = sources.find(item => item.key === selectedSource) || source;
423
+ }
424
+
425
+ if (source.key === 'root') {
426
+ const selectedFile = await select({
427
+ message: 'Elige la misión guardada:',
428
+ options: source.files.map(file => ({ label: file, value: file }))
429
+ });
430
+ if (isCancel(selectedFile)) return null;
431
+
432
+ return {
433
+ source: source.label,
434
+ collection: '',
435
+ collectionLabel: source.label,
436
+ file: selectedFile,
437
+ fullPath: path.join(source.dir, selectedFile)
438
+ };
439
+ }
440
+
441
+ const selectedMission = await selectMissionYaml(
442
+ source.dir,
443
+ `Elige la carpeta de ${source.label.toLowerCase()}:`,
444
+ 'Elige la misión guardada:'
445
+ );
446
+
447
+ if (!selectedMission) return null;
448
+
449
+ return {
450
+ ...selectedMission,
451
+ source: source.label
452
+ };
453
+ }
454
+
455
+ async function selectMissionSaveDir(rootDir) {
456
+ if (!fs.existsSync(rootDir)) fs.mkdirSync(rootDir, { recursive: true });
457
+
458
+ const directories = listMissionDirectories(rootDir);
459
+ const createNewValue = '__CREATE_NEW_COLLECTION__';
460
+ const selectedDir = await select({
461
+ message: 'Carpeta para guardar la mision:',
462
+ options: [
463
+ ...directories.map(d => ({
464
+ label: `${d.label}${d.description ? ` - ${d.description}` : ''}`,
465
+ value: d.value
466
+ })),
467
+ { label: '+ Crear nueva carpeta', value: createNewValue }
468
+ ]
469
+ });
470
+
471
+ if (isCancel(selectedDir)) return null;
472
+
473
+ let relDir = selectedDir;
474
+ if (selectedDir === createNewValue) {
475
+ const newDir = await text({
476
+ message: 'Nombre de la nueva carpeta (ej., smoke/login):',
477
+ placeholder: 'regression/login',
478
+ validate: v => {
479
+ if (!v || !v.trim()) return 'El nombre de la carpeta es requerido.';
480
+ if (!sanitizeCollectionPath(v)) return 'Usa letras, numeros, guiones o diagonales.';
481
+ }
482
+ });
483
+ if (isCancel(newDir)) return null;
484
+ relDir = sanitizeCollectionPath(newDir);
485
+ }
486
+
487
+ const targetDir = relDir ? path.join(rootDir, relDir) : rootDir;
488
+ fs.mkdirSync(targetDir, { recursive: true });
489
+ return { relDir, targetDir };
490
+ }
491
+
492
+ function ensureMissionTemplates(rootDir) {
493
+ if (!rootDir) return [];
494
+ const templatesDir = path.join(rootDir, '_templates');
495
+ if (!fs.existsSync(templatesDir)) fs.mkdirSync(templatesDir, { recursive: true });
496
+
497
+ const missionTemplatePath = path.join(templatesDir, 'mission.template.yaml');
498
+ const collectionTemplatePath = path.join(templatesDir, 'collection.template.yaml');
499
+ const created = [];
500
+
501
+ const missionTemplate = `# Arcality mission template
502
+ # Use this file as a base when you want to create your own tests manually.
503
+ #
504
+ # name:
505
+ # Friendly mission name shown in future menus and reports.
506
+ name: "TS_registrar_timesheet_hoy"
507
+ #
508
+ # prompt:
509
+ # Main instruction Arcality will execute. Be explicit about the expected flow.
510
+ prompt: "Inicia sesion, abre el primer pendiente y registra un timesheet valido."
511
+ #
512
+ # page_path:
513
+ # Relative route where the mission should begin. It is resolved against BASE_URL.
514
+ page_path: "/welcome"
515
+ #
516
+ # project:
517
+ # Optional project label for traceability.
518
+ project: "MiProyectoQA"
519
+ #
520
+ # collection:
521
+ # Logical folder/category for the mission library.
522
+ collection: "timesheets/hoy"
523
+ #
524
+ # suite:
525
+ # High-level group used for future batch execution and reporting.
526
+ suite: "timesheets"
527
+ #
528
+ # tags:
529
+ # Free-form labels for filtering and organization.
530
+ tags:
531
+ - "smoke"
532
+ - "timesheets"
533
+ #
534
+ # priority:
535
+ # Suggested values: low, normal, high, critical
536
+ priority: "normal"
537
+ #
538
+ # expected_result:
539
+ # Short statement describing what proves success.
540
+ expected_result: "El registro nuevo aparece en la tabla Timesheet - Hoy."
541
+ #
542
+ # credential_id:
543
+ # Optional placeholder for future centralized credentials.
544
+ credential_id: ""
545
+ #
546
+ # hooks:
547
+ # Reserved for future before/after automation hooks.
548
+ # hooks:
549
+ # before: []
550
+ # after: []
551
+ created_at: "2026-01-01T00:00:00.000Z"
552
+ `;
553
+
554
+ const collectionTemplate = `# Arcality collection template
555
+ # Save this as _collection.yaml inside a mission folder to customize how the folder is shown.
556
+ #
557
+ # name:
558
+ # Friendly collection name displayed in menus.
559
+ name: "Timesheets diarios"
560
+ #
561
+ # description:
562
+ # One-line explanation for users choosing the collection.
563
+ description: "Misiones para registrar y validar timesheets del dia."
564
+ #
565
+ # tags:
566
+ # Optional labels inherited conceptually by the collection.
567
+ tags:
568
+ - "timesheets"
569
+ - "daily"
570
+ #
571
+ # default_page_path:
572
+ # Optional fallback route used when a mission in this folder does not define page_path.
573
+ default_page_path: "/welcome"
574
+ `;
575
+
576
+ if (!fs.existsSync(missionTemplatePath)) {
577
+ fs.writeFileSync(missionTemplatePath, missionTemplate, 'utf8');
578
+ created.push(missionTemplatePath);
579
+ }
580
+ if (!fs.existsSync(collectionTemplatePath)) {
581
+ fs.writeFileSync(collectionTemplatePath, collectionTemplate, 'utf8');
582
+ created.push(collectionTemplatePath);
583
+ }
584
+
585
+ return created;
586
+ }
587
+
588
+ async function waitForEnter(message = 'Presiona Enter para continuar...') {
589
+ if (!process.stdin.isTTY) return;
590
+
591
+ console.log(chalk.gray(`\n ${message}`));
592
+ await new Promise(resolve => {
593
+ const onKey = (key) => {
594
+ const keyStr = String(key);
595
+ if (keyStr.includes('\r') || keyStr.includes('\n') || keyStr === '\u0003') {
596
+ process.stdin.off('data', onKey);
597
+
598
+ if (process.stdin.isTTY && process.stdin.setRawMode) {
599
+ process.stdin.setRawMode(false);
600
+ }
601
+
602
+ if (keyStr === '\u0003') {
603
+ process.exit(0);
604
+ } else {
605
+ resolve();
606
+ }
607
+ }
608
+ };
609
+
610
+ if (process.stdin.isTTY && process.stdin.setRawMode) {
611
+ process.stdin.setRawMode(true);
612
+ }
613
+ process.stdin.resume();
614
+ process.stdin.on('data', onKey);
615
+ });
616
+ }
617
+
618
+ function getMissionStartPath(missionSpec, collectionMeta = null, fallbackPath = '') {
619
+ return missionSpec.pagePath || missionSpec.startAt || missionSpec.page || collectionMeta?.defaultPagePath || fallbackPath;
620
+ }
621
+
622
+ function listMissionLogFiles(contextDir) {
623
+ if (!contextDir || !fs.existsSync(contextDir)) return [];
624
+ return fs.readdirSync(contextDir)
625
+ .filter(file => (
626
+ (file.startsWith('agent-log-') || file.startsWith('arcality-log-')) &&
627
+ file.endsWith('.json')
628
+ ))
629
+ .sort();
630
+ }
631
+
632
+ function readLatestMissionLog(contextDir, knownFiles = new Set()) {
633
+ const files = listMissionLogFiles(contextDir);
634
+ const newFiles = files.filter(file => !knownFiles.has(file));
635
+ const targetFile = (newFiles.length > 0 ? newFiles[newFiles.length - 1] : files[files.length - 1]);
636
+ if (!targetFile) return null;
637
+
638
+ try {
639
+ const fullPath = path.join(contextDir, targetFile);
640
+ return {
641
+ file: fullPath,
642
+ data: JSON.parse(fs.readFileSync(fullPath, 'utf8'))
643
+ };
644
+ } catch {
645
+ return null;
646
+ }
647
+ }
648
+
649
+ async function runCollectionBatch({
650
+ missionsRootDir,
651
+ selectedCollection = null,
652
+ debug = false
653
+ }) {
654
+ const collections = listMissionCollections(missionsRootDir);
655
+ if (collections.length === 0) {
656
+ note(chalk.yellow('No hay colecciones con misiones guardadas para ejecutar.'), 'Run All');
657
+ return;
658
+ }
659
+
660
+ let targetCollections = collections;
661
+ if (selectedCollection && selectedCollection !== '__ALL__') {
662
+ targetCollections = collections.filter(c => c.value === selectedCollection);
663
+ }
664
+
665
+ if (targetCollections.length === 0) {
666
+ note(chalk.yellow('La coleccion solicitada no existe o no contiene misiones.'), 'Run All');
667
+ return;
668
+ }
669
+
670
+ const missionsToRun = [];
671
+ for (const collection of targetCollections) {
672
+ const files = listYamlFilesInDir(collection.dir);
673
+ for (const file of files) {
674
+ missionsToRun.push({
675
+ collection,
676
+ file,
677
+ fullPath: path.join(collection.dir, file)
678
+ });
679
+ }
680
+ }
681
+
682
+ if (missionsToRun.length === 0) {
683
+ note(chalk.yellow('No se encontraron misiones ejecutables en la seleccion actual.'), 'Run All');
684
+ return;
685
+ }
686
+
687
+ const sBatch = spinner();
688
+ if (!debug) {
689
+ sBatch.start(`Ejecutando ${missionsToRun.length} misiones de ${targetCollections.length} coleccion(es)...`);
690
+ }
691
+
692
+ const summary = [];
693
+ for (let index = 0; index < missionsToRun.length; index++) {
694
+ const missionEntry = missionsToRun[index];
695
+ const rawMission = readYamlFile(missionEntry.fullPath);
696
+ const missionSpec = normalizeMissionSpec(rawMission, missionEntry.fullPath);
697
+ const collectionMeta = readCollectionMeta(missionEntry.collection.dir, missionEntry.collection.value);
698
+ const discoverPath = getMissionStartPath(missionSpec, collectionMeta, '/');
699
+ const contextDir = process.env.CONTEXT_DIR;
700
+ const knownLogFiles = new Set(listMissionLogFiles(contextDir));
701
+
702
+ if (!missionSpec.prompt) {
703
+ summary.push({
704
+ name: missionSpec.name || missionEntry.file,
705
+ collection: missionEntry.collection.label,
706
+ success: false,
707
+ reason: 'missing_prompt'
708
+ });
709
+ continue;
710
+ }
711
+
712
+ if (!debug) {
713
+ sBatch.message(`[${index + 1}/${missionsToRun.length}] ${missionSpec.name} (${missionEntry.collection.label})`);
714
+ } else {
715
+ console.log(chalk.cyan(`[RUN-ALL] ${index + 1}/${missionsToRun.length} -> ${missionSpec.name} [${missionEntry.collection.label}]`));
716
+ }
717
+
718
+ try {
719
+ await run(process.execPath, [
720
+ path.join(PROJECT_ROOT, 'scripts', 'gen-and-run.mjs'),
721
+ '--agent',
722
+ '--batch',
723
+ '--skip-save-prompt',
724
+ `--discover=${discoverPath}`,
725
+ missionSpec.prompt
726
+ ], {
727
+ cwd: ORIGINAL_CWD,
728
+ env: {
729
+ ...process.env,
730
+ ARCALITY_ROOT: PROJECT_ROOT,
731
+ ARCALITY_BATCH_MODE: 'true'
732
+ }
733
+ });
734
+
735
+ const latestLog = readLatestMissionLog(contextDir, knownLogFiles);
736
+ const success = latestLog?.data?.success === true;
737
+ summary.push({
738
+ name: missionSpec.name || missionEntry.file,
739
+ collection: missionEntry.collection.label,
740
+ success,
741
+ reason: latestLog?.data?.fail_reason || null
742
+ });
743
+ } catch (error) {
744
+ summary.push({
745
+ name: missionSpec.name || missionEntry.file,
746
+ collection: missionEntry.collection.label,
747
+ success: false,
748
+ reason: error instanceof Error ? error.message : 'runner_error'
749
+ });
750
+ }
751
+ }
752
+
753
+ const passed = summary.filter(item => item.success).length;
754
+ const failed = summary.length - passed;
755
+
756
+ if (!debug) {
757
+ sBatch.stop(chalk.green(`Coleccion ejecutada. Exitosas: ${passed} | Fallidas: ${failed}`));
758
+ }
759
+
760
+ const lines = summary.map(item => {
761
+ const status = item.success ? 'PASS' : 'FAIL';
762
+ const reason = item.reason ? ` (${item.reason})` : '';
763
+ return `- [${status}] ${item.collection} -> ${item.name}${reason}`;
764
+ });
765
+ note(lines.join('\n'), 'Resumen Run All');
766
+ }
767
+
768
+ function showBanner() {
164
769
  console.clear();
165
- const projectName = 'Arcality';
166
770
 
167
771
  let version = 'unknown';
168
772
  try {
@@ -170,19 +774,22 @@ function showBanner() {
170
774
  version = pkg.version || 'unknown';
171
775
  } catch { }
172
776
 
173
- const logo = figlet.textSync(projectName, {
174
- font: 'Standard',
175
- horizontalLayout: 'default',
176
- verticalLayout: 'default',
177
- });
777
+ const sentinelAscii = `
778
+ █████ ██████ ██████ █████ ██ ██ ████████ ██ ██
779
+ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
780
+ ███████ ██████ ██ ███████ ██ ██ ██ ████
781
+ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
782
+ ██ ██ ██ ██ ██████ ██ ██ ███████ ██ ██ ██ `;
178
783
 
179
- intro(chalk.cyanBright(logo));
784
+ intro(chalk.gray(sentinelAscii));
180
785
 
181
786
  note(
182
- chalk.magentaBright('Powered by Arcadial') + '\n' +
787
+ chalk.gray(' Motor de Observación: ') + chalk.bold.white('EN LÍNEA') + '\n' +
788
+ chalk.gray('◉ Módulo de Recuperación de UI: ') + chalk.bold.white('ACTIVO') + '\n' +
789
+ chalk.gray('◉ Lógica Forense: ') + chalk.bold.white('ACTIVA') + '\n' +
183
790
  chalk.gray('─'.repeat(50)) + '\n' +
184
- chalk.bold.white('📦 Version: ') + chalk.yellow(version),
185
- 'System Information'
791
+ chalk.gray('Versión del Sistema: ') + chalk.white(`v${version}`),
792
+ 'Estado Central de Arcality'
186
793
  );
187
794
 
188
795
  const { configName, baseUrl, techStack, configDir } = setupEnvironment();
@@ -190,30 +797,30 @@ function showBanner() {
190
797
  // Show arcality.config status instead of multi-config switching
191
798
  const hasConfig = !!projectConfig;
192
799
  const configStatus = hasConfig
193
- ? chalk.green(' Configured')
194
- : chalk.red(' Not configured run `arcality init`');
800
+ ? chalk.green(' Sincronización Establecida')
801
+ : chalk.red(' DesconectadoEjecuta `arcality init`');
195
802
 
196
803
  const { key: apiKeyVal, source: apiKeySource } = getApiKey();
197
804
  const apiKeyDisplay = apiKeyVal
198
- ? chalk.green(maskApiKey(apiKeyVal)) + chalk.gray(` (${apiKeySource})`)
199
- : chalk.red('Not configured');
805
+ ? chalk.green(maskApiKey(apiKeyVal)) + chalk.gray(` [${apiKeySource}]`)
806
+ : chalk.red('NO VERIFICADO');
200
807
 
201
808
  const infoLines = [
202
- chalk.bold.white('⚙️ Environment: ') + chalk.yellow(process.env.NODE_ENV ?? 'development'),
203
- chalk.bold.white('🔑 API Key: ') + apiKeyDisplay,
204
- chalk.bold.white('🛠️ Stack: ') + chalk.magenta(techStack),
809
+ chalk.gray('Nodo Objetivo: ') + chalk.white(process.env.NODE_ENV ?? 'development'),
810
+ chalk.gray('Token de Autenticación: ') + apiKeyDisplay,
811
+ chalk.gray('Stack: ') + chalk.white(techStack),
205
812
  ];
206
813
 
207
814
  if (hasConfig) {
208
815
  infoLines.splice(1, 0,
209
- chalk.bold.white('📋 Project: ') + chalk.cyan(configName) + chalk.gray(` (${baseUrl})`),
210
- chalk.bold.white('🆔 Project ID: ') + chalk.gray(projectConfig.projectId || 'N/A'),
816
+ chalk.gray('Proyecto: ') + chalk.white(configName) + chalk.gray(` [${baseUrl}]`),
817
+ chalk.gray('ID de Identidad: ') + chalk.white(projectConfig.projectId || 'N/A'),
211
818
  );
212
819
  } else {
213
- infoLines.push(chalk.bold.white('📋 Config: ') + configStatus);
820
+ infoLines.push(chalk.gray('Configuración: ') + configStatus);
214
821
  }
215
822
 
216
- note(infoLines.join('\n'), 'Active Configuration');
823
+ note(infoLines.join('\n'), 'Telemetría del Objetivo');
217
824
  }
218
825
 
219
826
  const ARCALITY_BRAIN_URL = "https://api.anthropic.com/v1/messages";
@@ -438,24 +1045,47 @@ async function main() {
438
1045
  initialEnv = parseDotEnvFile(path.join(PROJECT_ROOT, '.env'));
439
1046
  }
440
1047
 
441
- const argv = process.argv.slice(2);
442
- let initialDiscoverPath = null;
443
- let initialSmartMode = false;
444
- let initialAgentMode = false;
445
-
446
- const dIndex = argv.findIndex(a => a === '--discover' || a.startsWith('--discover='));
447
- if (dIndex !== -1) {
448
- const token = argv[dIndex];
449
- if (token.includes('=')) initialDiscoverPath = token.split('=')[1];
450
- else if (argv[dIndex + 1]) { initialDiscoverPath = argv[dIndex + 1]; argv.splice(dIndex + 1, 1); }
451
- argv.splice(dIndex, 1);
452
- }
453
- const sIndex = argv.findIndex(a => a === '--smart');
454
- if (sIndex !== -1) { initialSmartMode = true; argv.splice(sIndex, 1); }
455
- const aIndex = argv.findIndex(a => a === '--agent');
456
- if (aIndex !== -1) { initialAgentMode = true; argv.splice(aIndex, 1); }
457
- const debugIndex = argv.findIndex(a => a === '--debug');
458
- if (debugIndex !== -1) { argv.splice(debugIndex, 1); } // Handled by run() naturally now
1048
+ const argv = process.argv.slice(2);
1049
+ let initialDiscoverPath = null;
1050
+ let initialSmartMode = false;
1051
+ let initialAgentMode = false;
1052
+ let initialRunAllMode = false;
1053
+ let initialTemplateMode = false;
1054
+ let initialSkipSavePrompt = process.env.ARCALITY_SKIP_SAVE_PROMPT === 'true';
1055
+ let initialBatchMode = process.env.ARCALITY_BATCH_MODE === 'true';
1056
+ let initialCollectionPath = null;
1057
+
1058
+ const dIndex = argv.findIndex(a => a === '--discover' || a.startsWith('--discover='));
1059
+ if (dIndex !== -1) {
1060
+ const token = argv[dIndex];
1061
+ if (token.includes('=')) initialDiscoverPath = token.split('=')[1];
1062
+ else if (argv[dIndex + 1]) { initialDiscoverPath = argv[dIndex + 1]; argv.splice(dIndex + 1, 1); }
1063
+ argv.splice(dIndex, 1);
1064
+ }
1065
+ const sIndex = argv.findIndex(a => a === '--smart');
1066
+ if (sIndex !== -1) { initialSmartMode = true; argv.splice(sIndex, 1); }
1067
+ const aIndex = argv.findIndex(a => a === '--agent');
1068
+ if (aIndex !== -1) { initialAgentMode = true; argv.splice(aIndex, 1); }
1069
+ const runAllIndex = argv.findIndex(a => a === '--run-all');
1070
+ if (runAllIndex !== -1) { initialRunAllMode = true; argv.splice(runAllIndex, 1); }
1071
+ const templateIndex = argv.findIndex(a => a === 'template' || a === '--template');
1072
+ if (templateIndex !== -1) { initialTemplateMode = true; argv.splice(templateIndex, 1); }
1073
+ const skipSaveIndex = argv.findIndex(a => a === '--skip-save-prompt');
1074
+ if (skipSaveIndex !== -1) { initialSkipSavePrompt = true; argv.splice(skipSaveIndex, 1); }
1075
+ const batchIndex = argv.findIndex(a => a === '--batch');
1076
+ if (batchIndex !== -1) { initialBatchMode = true; argv.splice(batchIndex, 1); }
1077
+ const collectionIndex = argv.findIndex(a => a === '--collection' || a.startsWith('--collection='));
1078
+ if (collectionIndex !== -1) {
1079
+ const token = argv[collectionIndex];
1080
+ if (token.includes('=')) initialCollectionPath = sanitizeCollectionPath(token.split('=')[1]);
1081
+ else if (argv[collectionIndex + 1]) {
1082
+ initialCollectionPath = sanitizeCollectionPath(argv[collectionIndex + 1]);
1083
+ argv.splice(collectionIndex + 1, 1);
1084
+ }
1085
+ argv.splice(collectionIndex, 1);
1086
+ }
1087
+ const debugIndex = argv.findIndex(a => a === '--debug');
1088
+ if (debugIndex !== -1) { argv.splice(debugIndex, 1); } // Handled by run() naturally now
459
1089
 
460
1090
  const promptArg = argv.join(" ").trim();
461
1091
  let firstRun = true;
@@ -464,59 +1094,116 @@ async function main() {
464
1094
  setupEnvironment();
465
1095
 
466
1096
  while (true) {
467
- let prompt = firstRun ? promptArg : "";
468
- let agentMode = firstRun ? initialAgentMode : false;
469
- let smartMode = firstRun ? initialSmartMode : false;
470
- let discoverPath = firstRun ? initialDiscoverPath : null;
471
-
472
- let skipMenu = (firstRun && (prompt || agentMode || smartMode)) || agentMode;
473
- if (agentMode) skipMenu = true; // Hard-lock: No menu in agent mode
474
-
475
- firstRun = false;
476
-
477
- if (!skipMenu) {
478
- showBanner();
479
- try {
1097
+ let prompt = firstRun ? promptArg : "";
1098
+ let agentMode = firstRun ? initialAgentMode : false;
1099
+ let smartMode = firstRun ? initialSmartMode : false;
1100
+ let discoverPath = firstRun ? initialDiscoverPath : null;
1101
+ let runAllMode = firstRun ? initialRunAllMode : false;
1102
+ let templateMode = firstRun ? initialTemplateMode : false;
1103
+ let collectionPath = firstRun ? initialCollectionPath : null;
1104
+ let skipSavePrompt = firstRun ? initialSkipSavePrompt : false;
1105
+ let batchMode = firstRun ? initialBatchMode : false;
1106
+
1107
+ let skipMenu = (firstRun && (prompt || agentMode || smartMode || runAllMode || templateMode)) || agentMode || runAllMode || templateMode;
1108
+ if (agentMode) skipMenu = true; // Hard-lock: No menu in agent mode
1109
+
1110
+ firstRun = false;
1111
+
1112
+ if (!skipMenu) {
1113
+ showBanner();
1114
+ try {
480
1115
  // ── Build menu options (single config mode) ──
481
1116
  const missionsDir = process.env.MISSIONS_DIR;
482
- const savedMissions = fs.existsSync(missionsDir)
483
- ? fs.readdirSync(missionsDir).filter(f => f.endsWith('.yaml') || f.endsWith('.yml'))
484
- : [];
1117
+ const savedMissionCollections = listMissionCollections(missionsDir);
485
1118
 
486
1119
  // Check for YAML files in the yaml output dir from arcality.config
487
- let yamlOutputFiles = [];
1120
+ let projectMissionCollections = [];
488
1121
  if (projectConfig) {
489
1122
  const yamlDir = getYamlOutputDir(projectConfig, ORIGINAL_CWD);
490
1123
  if (fs.existsSync(yamlDir)) {
491
- yamlOutputFiles = fs.readdirSync(yamlDir).filter(f => f.endsWith('.yaml') || f.endsWith('.yml'));
1124
+ projectMissionCollections = listMissionCollections(yamlDir);
492
1125
  }
493
1126
  }
494
1127
 
495
1128
  // Root-level YAML templates
496
- const rootYamlFiles = fs.readdirSync(PROJECT_ROOT).filter(f => f.endsWith('.yaml') || f.endsWith('.yml'));
497
-
498
- const options = [
499
- { label: '🤖 Run Autonomous Agent (Prompt)', value: 'agent' },
500
- ...(savedMissions.length ? [{ label: '🚀 Launch Saved Mission (.yaml)', value: 'launch_saved' }] : []),
501
- ...(yamlOutputFiles.length ? [{ label: '♻️ Reuse YAML + Auto-Guide (if prev. run exists)', value: 'reuse_yaml' }] : []),
502
- ...(rootYamlFiles.length ? [{ label: '📂 Choose Root YAML (Templates)', value: 'yaml_list' }] : []),
503
- { label: '🔄 Reconfigure (arcality init)', value: 'reconfigure' },
504
- { label: ' Exit', value: 'exit' }
505
- ];
506
-
507
- const action = await select({
508
- message: 'What would you like to do?',
509
- options
510
- });
1129
+ const rootMissionFiles = fs.existsSync(ORIGINAL_CWD)
1130
+ ? fs.readdirSync(ORIGINAL_CWD).filter(f => isYamlFile(f))
1131
+ : [];
1132
+ const hasSavedMissionSources = savedMissionCollections.length || projectMissionCollections.length || rootMissionFiles.length;
1133
+ const yamlOutputCollections = projectMissionCollections;
1134
+ const rootYamlFiles = rootMissionFiles;
1135
+
1136
+ const options = [
1137
+ { label: '◉ Desplegar Arcality (Prompt)', value: 'agent' },
1138
+ ...(savedMissionCollections.length ? [{ label: '▶ Ejecutar colección completa', value: 'run_collection' }] : []),
1139
+ ...(savedMissionCollections.length ? [{ label: '⟳ Lanzar Misión Archivada (.yaml)', value: 'launch_saved' }] : []),
1140
+ ...(yamlOutputCollections.length ? [{ label: '⟶ Activar Modo GUÍA (Reusar YAML)', value: 'reuse_yaml' }] : []),
1141
+ ...(rootYamlFiles.length ? [{ label: '📂 Cargar Matriz de Patrones (YAML Raíz)', value: 'yaml_list' }] : []),
1142
+ { label: '📄 Generar templates YAML', value: 'generate_templates' },
1143
+ { label: '◎ Recalibrar Sistema (init)', value: 'reconfigure' },
1144
+ { label: '✕ Terminar Proceso', value: 'exit' }
1145
+ ];
1146
+
1147
+ const menuOptions = options
1148
+ .filter(option => !['launch_saved', 'reuse_yaml', 'yaml_list'].includes(option.value))
1149
+ .map(option => option.value === 'generate_templates'
1150
+ ? { ...option, label: '◫ Generar plantillas de misión' }
1151
+ : option
1152
+ );
1153
+
1154
+ if (hasSavedMissionSources) {
1155
+ const insertIndex = Math.min(menuOptions.length, 2);
1156
+ menuOptions.splice(insertIndex, 0, {
1157
+ label: '◌ Abrir misión guardada',
1158
+ value: 'open_saved_mission'
1159
+ });
1160
+ }
1161
+
1162
+ const action = await select({
1163
+ message: '¿Qué te gustaría hacer?',
1164
+ options: menuOptions
1165
+ });
511
1166
 
512
1167
  if (isCancel(action) || action === 'exit') {
513
- outro(chalk.cyan('See you later!'));
1168
+ outro(chalk.cyan('¡Hasta luego!'));
514
1169
  break;
515
1170
  }
516
1171
 
517
- if (action === 'agent') {
518
- agentMode = true;
519
- } else if (action === 'reconfigure') {
1172
+ if (action === 'agent') {
1173
+ agentMode = true;
1174
+ } else if (action === 'run_collection') {
1175
+ runAllMode = true;
1176
+ } else if (action === 'open_saved_mission') {
1177
+ const projectDir = projectConfig ? getYamlOutputDir(projectConfig, ORIGINAL_CWD) : null;
1178
+ const selectedMission = await selectSavedMissionSource({
1179
+ libraryDir: missionsDir,
1180
+ projectDir,
1181
+ rootDir: ORIGINAL_CWD
1182
+ });
1183
+ if (!selectedMission) continue;
1184
+
1185
+ const yamlContent = fs.readFileSync(selectedMission.fullPath, 'utf8');
1186
+ const missionSpec = normalizeMissionSpec(loadYaml(yamlContent), selectedMission.fullPath);
1187
+ prompt = missionSpec.prompt || yamlContent;
1188
+ discoverPath = getMissionStartPath(
1189
+ missionSpec,
1190
+ readCollectionMeta(path.dirname(selectedMission.fullPath), selectedMission.collection)
1191
+ );
1192
+ agentMode = true;
1193
+ } else if (action === 'generate_templates') {
1194
+ const createdMain = ensureMissionTemplates(missionsDir);
1195
+ const yamlDir = projectConfig ? getYamlOutputDir(projectConfig, ORIGINAL_CWD) : null;
1196
+ const createdSecondary = yamlDir ? ensureMissionTemplates(yamlDir) : [];
1197
+ const createdFiles = [...createdMain, ...createdSecondary];
1198
+ note(
1199
+ createdFiles.length > 0
1200
+ ? chalk.green(`Templates creados:\n${createdFiles.join('\n')}`)
1201
+ : chalk.cyan(`Los templates ya existen en ${path.join(missionsDir, '_templates')}`),
1202
+ 'Templates Arcality'
1203
+ );
1204
+ await waitForEnter('Presiona Enter para volver al menu...');
1205
+ continue;
1206
+ } else if (action === 'reconfigure') {
520
1207
  // Launch arcality init in the user's project directory
521
1208
  const initScript = path.join(PROJECT_ROOT, 'scripts', 'init.mjs');
522
1209
  try {
@@ -544,61 +1231,113 @@ async function main() {
544
1231
  await new Promise(r => setTimeout(r, 3000));
545
1232
  }
546
1233
  continue;
547
- } else if (action === 'launch_saved') {
548
- const sel = await select({
549
- message: 'Choose saved mission:',
550
- options: savedMissions.map(f => ({ label: f, value: f }))
551
- });
552
- if (isCancel(sel)) continue;
553
-
554
- const yamlContent = fs.readFileSync(path.join(missionsDir, sel), 'utf8');
555
- const data = loadYaml(yamlContent);
556
- prompt = data.prompt || data.mision || yamlContent;
557
- agentMode = true;
558
- } else if (action === 'reuse_yaml') {
559
- const yamlDir = getYamlOutputDir(projectConfig, ORIGINAL_CWD);
560
- const sel = await select({
561
- message: 'Choose YAML to reuse:',
562
- options: yamlOutputFiles.map(f => ({ label: f, value: f }))
563
- });
564
- if (isCancel(sel)) continue;
565
-
566
- const yamlContent = fs.readFileSync(path.join(yamlDir, sel), 'utf8');
567
- const data = loadYaml(yamlContent);
568
- prompt = data.prompt || data.mision || yamlContent;
569
- agentMode = true;
570
- } else if (action === 'yaml_list') {
571
- const sel = await select({
572
- message: 'Choose YAML file:',
573
- options: rootYamlFiles.map(f => ({ label: f, value: f }))
1234
+ } else if (action === 'launch_saved') {
1235
+ const selectedMission = await selectMissionYaml(
1236
+ missionsDir,
1237
+ 'Elige la carpeta de misiones:',
1238
+ 'Elige la mision guardada:'
1239
+ );
1240
+ if (!selectedMission) continue;
1241
+
1242
+ const yamlContent = fs.readFileSync(selectedMission.fullPath, 'utf8');
1243
+ const missionSpec = normalizeMissionSpec(loadYaml(yamlContent), selectedMission.fullPath);
1244
+ prompt = missionSpec.prompt || yamlContent;
1245
+ discoverPath = getMissionStartPath(missionSpec, readCollectionMeta(path.dirname(selectedMission.fullPath), selectedMission.collection));
1246
+ agentMode = true;
1247
+ } else if (action === 'reuse_yaml') {
1248
+ const yamlDir = getYamlOutputDir(projectConfig, ORIGINAL_CWD);
1249
+ const selectedMission = await selectMissionYaml(
1250
+ yamlDir,
1251
+ 'Elige la carpeta de YAML:',
1252
+ 'Elige el YAML a reusar:'
1253
+ );
1254
+ if (!selectedMission) continue;
1255
+
1256
+ const yamlContent = fs.readFileSync(selectedMission.fullPath, 'utf8');
1257
+ const missionSpec = normalizeMissionSpec(loadYaml(yamlContent), selectedMission.fullPath);
1258
+ prompt = missionSpec.prompt || yamlContent;
1259
+ discoverPath = getMissionStartPath(missionSpec, readCollectionMeta(path.dirname(selectedMission.fullPath), selectedMission.collection));
1260
+ agentMode = true;
1261
+ } else if (action === 'yaml_list') {
1262
+ const sel = await select({
1263
+ message: 'Elige el archivo YAML:',
1264
+ options: rootYamlFiles.map(f => ({ label: f, value: f }))
574
1265
  });
575
1266
  if (isCancel(sel)) continue;
576
-
577
- const yamlContent = fs.readFileSync(path.join(PROJECT_ROOT, sel), 'utf8');
578
- const data = loadYaml(yamlContent);
579
- prompt = data.mision || data.prompt || yamlContent;
580
- agentMode = true;
581
- }
1267
+
1268
+ const yamlContent = fs.readFileSync(path.join(PROJECT_ROOT, sel), 'utf8');
1269
+ const missionSpec = normalizeMissionSpec(loadYaml(yamlContent), path.join(PROJECT_ROOT, sel));
1270
+ prompt = missionSpec.prompt || yamlContent;
1271
+ discoverPath = getMissionStartPath(missionSpec);
1272
+ agentMode = true;
1273
+ }
582
1274
  } catch (e) {
583
1275
  console.error("Menu error:", e);
584
1276
  break;
585
- }
586
- }
587
-
588
- // --- VALIDATION AND CAPTURE OF PARAMETERS FOR THE AGENT ---
589
- if (agentMode) {
1277
+ }
1278
+ }
1279
+
1280
+ if (templateMode) {
1281
+ const createdMain = ensureMissionTemplates(process.env.MISSIONS_DIR);
1282
+ const yamlDir = projectConfig ? getYamlOutputDir(projectConfig, ORIGINAL_CWD) : null;
1283
+ const createdSecondary = yamlDir ? ensureMissionTemplates(yamlDir) : [];
1284
+ const createdFiles = [...createdMain, ...createdSecondary];
1285
+ note(
1286
+ createdFiles.length > 0
1287
+ ? chalk.green(`Templates creados:\n${createdFiles.join('\n')}`)
1288
+ : chalk.cyan(`Los templates ya existen en ${path.join(process.env.MISSIONS_DIR, '_templates')}`),
1289
+ 'Templates Arcality'
1290
+ );
1291
+
1292
+ if (skipMenu) return;
1293
+ continue;
1294
+ }
1295
+
1296
+ if (runAllMode) {
1297
+ let batchCollection = collectionPath;
1298
+ if (!batchCollection && process.stdin.isTTY) {
1299
+ const availableCollections = listMissionCollections(process.env.MISSIONS_DIR);
1300
+ const selectedBatchCollection = await select({
1301
+ message: 'Elige la colección a ejecutar:',
1302
+ options: [
1303
+ { label: `Todas las colecciones (${availableCollections.length})`, value: '__ALL__' },
1304
+ ...availableCollections.map(collection => ({
1305
+ label: `${collection.label} (${collection.count})${collection.description ? ` - ${collection.description}` : ''}`,
1306
+ value: collection.value
1307
+ }))
1308
+ ]
1309
+ });
1310
+ if (isCancel(selectedBatchCollection)) {
1311
+ if (skipMenu) return;
1312
+ continue;
1313
+ }
1314
+ batchCollection = selectedBatchCollection;
1315
+ }
1316
+
1317
+ await runCollectionBatch({
1318
+ missionsRootDir: process.env.MISSIONS_DIR,
1319
+ selectedCollection: batchCollection || '__ALL__',
1320
+ debug: process.argv.includes('--debug')
1321
+ });
1322
+
1323
+ if (skipMenu) return;
1324
+ continue;
1325
+ }
1326
+
1327
+ // --- VALIDATION AND CAPTURE OF PARAMETERS FOR THE AGENT ---
1328
+ if (agentMode) {
590
1329
  if (!prompt || prompt.trim().length < 4) {
591
1330
  const p = await text({
592
- message: chalk.cyan('🤖 Mission for the Agent (Required):'),
593
- placeholder: 'E.g., Fill the signup form with random data',
1331
+ message: chalk.cyan('🤖 Misión para Arcality (Requerida):'),
1332
+ placeholder: 'Ej., Llena el formulario de registro con datos aleatorios',
594
1333
  validate: v => {
595
- if (!v || !v.trim()) return 'The mission is required to start.';
596
- if (v.trim().length < 4) return 'The mission must be more descriptive (minimum 4 characters).';
1334
+ if (!v || !v.trim()) return 'La misión es requerida para iniciar.';
1335
+ if (v.trim().length < 4) return 'La misión debe ser más descriptiva (mínimo 4 caracteres).';
597
1336
  }
598
1337
  });
599
1338
 
600
1339
  if (isCancel(p)) {
601
- cancel('Mission cancelled. Returning to main menu...');
1340
+ cancel('Misión cancelada. Regresando al menú principal...');
602
1341
  agentMode = false;
603
1342
  prompt = "";
604
1343
  skipMenu = false;
@@ -611,19 +1350,19 @@ async function main() {
611
1350
  const knownBaseUrl = projectConfig?.project?.baseUrl || process.env.BASE_URL;
612
1351
  const promptMsg = knownBaseUrl
613
1352
  ? `🌐 Initial navigation path (relative to ${knownBaseUrl}):`
614
- : '🌐 Full Navigation URL (e.g., http://localhost:3000/):';
1353
+ : '🌐 URL Completa de Navegación (ej., http://localhost:3000/):';
615
1354
 
616
1355
  const d = await text({
617
1356
  message: chalk.cyan(promptMsg),
618
1357
  placeholder: knownBaseUrl ? '/' : 'http://localhost:3000/',
619
1358
  validate: v => {
620
- if (!knownBaseUrl && !v.startsWith('http')) return 'Please provide a full URL with http:// or https://';
1359
+ if (!knownBaseUrl && !v.startsWith('http')) return 'Por favor provee una URL completa con http:// o https://';
621
1360
  return undefined;
622
1361
  }
623
1362
  });
624
1363
 
625
1364
  if (isCancel(d)) {
626
- cancel('Mission aborted. Returning to menu...');
1365
+ cancel('Misión abortada. Regresando al menú...');
627
1366
  agentMode = false;
628
1367
  prompt = "";
629
1368
  skipMenu = false;
@@ -653,12 +1392,12 @@ async function main() {
653
1392
  }
654
1393
  if (!validation.valid) {
655
1394
  const errorMessages = {
656
- 'no_api_key': '🔑 API Key not found.\n Configure it via `arcality init` or in .env (ARCALITY_API_KEY)',
657
- 'invalid_format': '🔑 Invalid API Key. It must start with "arc_"',
658
- 'invalid_api_key': '🔑 API Key not recognized by the server',
659
- 'plan_expired': '💳 Your plan has expired'
1395
+ 'no_api_key': '🔑 API Key no encontrada.\n Configúrala vía `arcality init` o en .env (ARCALITY_API_KEY)',
1396
+ 'invalid_format': '🔑 API Key inválida. Debe comenzar con "arc_"',
1397
+ 'invalid_api_key': '🔑 API Key no reconocida por el servidor',
1398
+ 'plan_expired': '💳 Tu plan ha expirado'
660
1399
  };
661
- note(chalk.red(errorMessages[validation.error] || `Error: ${validation.error}`), '❌ Authentication');
1400
+ note(chalk.red(errorMessages[validation.error] || `Error: ${validation.error}`), '❌ Autenticación');
662
1401
  agentMode = false;
663
1402
  prompt = "";
664
1403
  skipMenu = false;
@@ -666,15 +1405,15 @@ async function main() {
666
1405
  }
667
1406
 
668
1407
  // Show plan info
669
- const modeLabel = validation.mode === 'mock' ? chalk.gray('(local)') : chalk.green('(live)');
1408
+ const modeLabel = validation.mode === 'mock' ? chalk.gray('[sandbox]') : chalk.cyanBright('[live-net]');
670
1409
  const dailyLimitDisplay = validation.daily_limit === -1 ? '∞' : (validation.daily_limit || 35);
671
1410
  const remainingDisplay = validation.remaining >= 999999 ? '∞' : validation.remaining;
672
1411
  note(
673
- chalk.bold.white(' Valid API Key ') + modeLabel + '\n' +
674
- chalk.bold.white('📋 Plan: ') + chalk.cyan(validation.plan || 'internal') + '\n' +
675
- chalk.bold.white('📊 Missions today: ') + chalk.yellow(`${validation.daily_used || 0}/${dailyLimitDisplay}`) + '\n' +
676
- chalk.bold.white('🎯 Remaining: ') + chalk.green(remainingDisplay),
677
- 'Arcality Account'
1412
+ chalk.gray('Conexión: ') + chalk.cyanBright('SECURE ') + modeLabel + '\n' +
1413
+ chalk.gray('Autorización: ') + chalk.white(validation.plan || 'internal') + '\n' +
1414
+ chalk.gray('Misiones: ') + chalk.white(`${validation.daily_used || 0}/${dailyLimitDisplay}`) + '\n' +
1415
+ chalk.gray('Reservas: ') + chalk.white(remainingDisplay),
1416
+ 'Enlace Arcality'
678
1417
  );
679
1418
 
680
1419
  // --- RESOLVE PROJECT ID ---
@@ -697,21 +1436,21 @@ async function main() {
697
1436
  const projects = data.projects || [];
698
1437
 
699
1438
  const projOptions = projects.map(p => ({ label: p.name || p.Name, value: p.id || p.Id }));
700
- projOptions.push({ label: '➕ Create new project', value: 'new' });
1439
+ projOptions.push({ label: '➕ Crear nuevo proyecto', value: 'new' });
701
1440
 
702
1441
  const projSelection = await select({
703
- message: chalk.cyan('Choose a project for this mission:'),
1442
+ message: chalk.cyan('Elige un proyecto para esta misión:'),
704
1443
  options: projOptions
705
1444
  });
706
1445
 
707
1446
  if (isCancel(projSelection)) {
708
- cancel('Mission aborted.');
1447
+ cancel('Misión abortada.');
709
1448
  agentMode = false; prompt = ""; skipMenu = false; continue;
710
1449
  }
711
1450
 
712
1451
  if (projSelection === 'new') {
713
- const newName = await text({ message: 'Project name:', validate: v => !v ? 'Required' : undefined });
714
- if (isCancel(newName)) { cancel('Aborted'); agentMode = false; prompt = ""; skipMenu = false; continue; }
1452
+ const newName = await text({ message: 'Nombre del proyecto:', validate: v => !v ? 'Requerido' : undefined });
1453
+ if (isCancel(newName)) { cancel('Abortado'); agentMode = false; prompt = ""; skipMenu = false; continue; }
715
1454
 
716
1455
  const createRes = await fetch(`${apiBase}/api/v1/projects`, {
717
1456
  method: 'POST',
@@ -727,9 +1466,9 @@ async function main() {
727
1466
  if (createRes.ok) {
728
1467
  const created = await createRes.json();
729
1468
  selectedProjectId = created.id || created.Id;
730
- note(chalk.green(`✅ Project created: ${newName}`));
1469
+ note(chalk.green(`✅ Proyecto creado: ${newName}`));
731
1470
  } else {
732
- note(chalk.red(`❌ Failed to create project.`));
1471
+ note(chalk.red(`❌ Falló al crear proyecto.`));
733
1472
  }
734
1473
  } else {
735
1474
  selectedProjectId = projSelection;
@@ -750,10 +1489,10 @@ async function main() {
750
1489
  const mission = await requestMission(prompt, discoverPath || '/');
751
1490
  if (!mission.allowed) {
752
1491
  note(
753
- chalk.red(`❌ ${mission.error === 'mission_limit_exceeded' ? 'Daily mission limit reached' : mission.error}`) + '\n' +
1492
+ chalk.red(`❌ ${mission.error === 'mission_limit_exceeded' ? 'Límite diario de misiones alcanzado' : mission.error}`) + '\n' +
754
1493
  chalk.yellow(`📊 Used: ${mission.daily_used}/${mission.daily_limit}`) + '\n' +
755
1494
  chalk.gray(`🔄 Resets: ${mission.resets_at ? new Date(mission.resets_at).toLocaleString() : 'tomorrow'}`),
756
- '⚠️ Limit Reached'
1495
+ '⚠️ Límite Alcanzado'
757
1496
  );
758
1497
  agentMode = false;
759
1498
  prompt = "";
@@ -763,9 +1502,9 @@ async function main() {
763
1502
 
764
1503
  const s = spinner();
765
1504
  if (!process.argv.includes('--debug')) {
766
- s.start(`🤖 [AGENT MODE] Starting mission: "${prompt}" ${chalk.gray(`(${mission.daily_used}/${mission.daily_limit} today)`)}`);
1505
+ s.start(`◉ [ARCALITY INICIADO] Ejecutando directiva: "${prompt}" ${chalk.gray(`(${mission.daily_used}/${mission.daily_limit})`)}`);
767
1506
  } else {
768
- console.log(chalk.cyan(`\n🤖 [AGENT MODE] Starting mission: "${prompt}" ${chalk.gray(`(${mission.daily_used}/${mission.daily_limit} today)`)}`));
1507
+ console.log(chalk.cyan(`\n [ARCALITY INICIADO] Ejecutando directiva: "${prompt}" ${chalk.gray(`(${mission.daily_used}/${mission.daily_limit})`)}`));
769
1508
  }
770
1509
 
771
1510
  const finalProjectId = process.env.ARCALITY_PROJECT_ID || selectedProjectId;
@@ -821,9 +1560,9 @@ async function main() {
821
1560
  }
822
1561
  userCanceledMission = true;
823
1562
 
824
- console.log(chalk.yellow('\n\n⚠️ [ARCALITY] Misión cancelada por el usuario.'));
1563
+ console.log(chalk.yellow('\n\n⚠️ [ARCALITY] Directiva abortada por el operador.'));
825
1564
  if (!process.argv.includes('--debug')) {
826
- try { s.stop(chalk.yellow('⚠️ Misión cancelada.')); } catch { /* ignore */ }
1565
+ try { s.stop(chalk.yellow('⚠️ Directiva abortada.')); } catch { /* ignore */ }
827
1566
  }
828
1567
 
829
1568
  // Terminar el proceso hijo de Playwright directamente (Windows-safe)
@@ -842,7 +1581,7 @@ async function main() {
842
1581
  if (ctxDir && fs.existsSync(ctxDir)) {
843
1582
  const cancelLog = {
844
1583
  prompt,
845
- history: ['[CANCELADO POR USUARIO] La prueba fue detenida manualmente antes de completarse.'],
1584
+ history: ['[ABORTO DEL OPERADOR] La directiva fue detenida manualmente antes de completarse.'],
846
1585
  steps: 0,
847
1586
  success: false,
848
1587
  error: true,
@@ -850,7 +1589,7 @@ async function main() {
850
1589
  canceled_by_user: true,
851
1590
  timestamp: new Date().toISOString()
852
1591
  };
853
- fs.writeFileSync(path.join(ctxDir, `agent-log-${Date.now()}.json`), JSON.stringify(cancelLog, null, 2));
1592
+ fs.writeFileSync(path.join(ctxDir, `arcality-log-${Date.now()}.json`), JSON.stringify(cancelLog, null, 2));
854
1593
  }
855
1594
  } catch { /* silencioso */ }
856
1595
 
@@ -958,21 +1697,34 @@ async function main() {
958
1697
  console.log(chalk.gray(`[DEBUG] Using playwright CLI: ${playwrightCli}`));
959
1698
  }
960
1699
  } catch (e) {
961
- console.error(chalk.red('\n[FATAL] Playwright core not found. Make sure @playwright/test is installed.'));
1700
+ console.error(chalk.red('\n[FATAL] Núcleo de Playwright no encontrado. Asegúrate de que @playwright/test esté instalado.'));
962
1701
  console.error(chalk.red(e.message));
963
- console.error(chalk.yellow('\nTip: Run `npm install @playwright/test` in your project directory.'));
1702
+ console.error(chalk.yellow('\nConsejo: Ejecuta `npm install @playwright/test` en tu directorio de proyecto.'));
964
1703
  _missionActive = false;
965
1704
  process.exit = _origProcessExit;
966
1705
  _origProcessExit(1);
967
1706
  }
968
1707
 
969
- const postProcessSigint = () => console.log(chalk.yellow('\n⏳ Finalizando tareas de reporte, por favor espera...'));
1708
+ const postProcessSigint = () => console.log(chalk.yellow('\n⏳ Finalizando reporte de telemetría, por favor espera...'));
970
1709
  let missionResult = null;
971
1710
  let finalFailReason = null;
972
1711
  let finalUsageData = undefined;
973
1712
  let finalReportUrl = null;
974
1713
  let finalFailReasoning = null;
975
1714
 
1715
+ // ── Auto-compilar el runner antes de ejecutar Playwright para asegurar que los cambios de rama/merge estén aplicados ──
1716
+ try {
1717
+ if (!process.argv.includes('--debug')) {
1718
+ s.message('⚙️ Compilando agente de pruebas (Arcality Runner)...');
1719
+ } else {
1720
+ console.log(chalk.cyan('⚙️ Compilando agente de pruebas (Arcality Runner)...'));
1721
+ }
1722
+ const npmCmd = process.platform === 'win32' ? 'npm.cmd' : 'npm';
1723
+ await run(npmCmd, ['run', 'build:runner'], { cwd: PROJECT_ROOT });
1724
+ } catch (err) {
1725
+ console.warn(chalk.yellow(`\n⚠️ Advertencia: No se pudo auto-compilar el runner: ${err.message}`));
1726
+ }
1727
+
976
1728
  try {
977
1729
  // No test file path is passed — playwright.config.js uses testMatch:'agentic-runner.spec.ts'
978
1730
  // This eliminates the Windows regex path issue permanently on any user's machine.
@@ -1005,42 +1757,72 @@ async function main() {
1005
1757
  }
1006
1758
 
1007
1759
  missionResult = 'success';
1008
- if (!process.argv.includes('--debug')) s.stop(chalk.green('✅ Mission completed successfully.'));
1009
- else console.log(chalk.green('✅ Mission completed successfully.'));
1760
+ if (!process.argv.includes('--debug')) s.stop(chalk.green('✅ Misión completada exitosamente.'));
1761
+ else console.log(chalk.green('✅ Misión completada exitosamente.'));
1010
1762
 
1011
1763
  // ── Ping Project ──
1012
1764
  await pingProject(finalProjectId, process.env.ARCALITY_API_KEY);
1013
1765
 
1014
- // ── Save Mission YAML ──
1015
- const saveDecision = await select({
1016
- message: 'Do you want to save this mission to run it again later?',
1017
- options: [
1018
- { label: ' Yes, save as YAML', value: 'yes' },
1019
- { label: '❌ No, thanks', value: 'no' }
1020
- ]
1021
- });
1022
-
1023
- if (saveDecision === 'yes') {
1766
+ // ── Save Mission YAML ──
1767
+ let saveDecision = 'no';
1768
+ if (!skipSavePrompt && !batchMode) {
1769
+ saveDecision = await select({
1770
+ message: '¿Quieres guardar esta misión para ejecutarla de nuevo más tarde?',
1771
+ options: [
1772
+ { label: '✅ Sí, guardar como YAML', value: 'yes' },
1773
+ { label: '❌ No, gracias', value: 'no' }
1774
+ ]
1775
+ });
1776
+ }
1777
+
1778
+ if (saveDecision === 'yes') {
1024
1779
  const name = await text({
1025
- message: 'Mission name (e.g., valid_login):',
1780
+ message: 'Nombre de la misión (ej., login_valido):',
1026
1781
  placeholder: 'my_mission',
1027
- validate: v => v.length < 3 ? 'Name too short' : undefined
1782
+ validate: v => v.length < 3 ? 'Nombre muy corto' : undefined
1028
1783
  });
1029
1784
 
1030
- if (!isCancel(name)) {
1031
- const safeName = name.trim().replace(/[^a-z0-9]/gi, '_').toLowerCase();
1785
+ if (!isCancel(name)) {
1786
+ const safeName = name.trim().replace(/[^a-z0-9]/gi, '_').toLowerCase();
1032
1787
 
1033
1788
  // Save to missions dir (existing flow)
1034
1789
  const missionsDir = process.env.MISSIONS_DIR;
1035
- const yamlPathMissions = path.join(missionsDir, `${safeName}.yaml`);
1036
-
1037
- const safeProjectName = projectConfig?.project?.name || 'Default';
1038
- let yamlData = `name: ${JSON.stringify(name)}\nprompt: ${JSON.stringify(prompt)}\nproject: ${JSON.stringify(safeProjectName)}\ncreated_at: ${JSON.stringify(new Date().toISOString())}\n`;
1790
+ const missionSaveDir = await selectMissionSaveDir(missionsDir);
1791
+ if (!missionSaveDir) {
1792
+ note(chalk.yellow('Guardado de mision cancelado.'), 'Persistencia Arcality');
1793
+ continue;
1794
+ }
1795
+ const yamlPathMissions = path.join(missionSaveDir.targetDir, `${safeName}.yaml`);
1796
+
1797
+ const safeProjectName = projectConfig?.project?.name || 'Default';
1798
+ const collectionValue = missionSaveDir.relDir ? toPosixPath(missionSaveDir.relDir) : '';
1799
+ const suiteValue = missionSaveDir.relDir ? toPosixPath(missionSaveDir.relDir).split('/')[0] : '';
1800
+ let yamlData = [
1801
+ `name: ${JSON.stringify(name)}`,
1802
+ `prompt: ${JSON.stringify(prompt)}`,
1803
+ `project: ${JSON.stringify(safeProjectName)}`,
1804
+ ...(collectionValue ? [`collection: ${JSON.stringify(collectionValue)}`] : []),
1805
+ ...(suiteValue ? [`suite: ${JSON.stringify(suiteValue)}`] : []),
1806
+ 'tags: []',
1807
+ `priority: ${JSON.stringify('normal')}`,
1808
+ `created_at: ${JSON.stringify(new Date().toISOString())}`
1809
+ ].join('\n') + '\n';
1039
1810
 
1040
1811
  const smartYamlPath = path.join(process.env.CONTEXT_DIR, 'last-mission-smart.yaml');
1041
- if (fs.existsSync(smartYamlPath)) {
1042
- yamlData = fs.readFileSync(smartYamlPath, 'utf8');
1043
- }
1812
+ if (fs.existsSync(smartYamlPath)) {
1813
+ const smartYamlContent = fs.readFileSync(smartYamlPath, 'utf8').trim();
1814
+ yamlData = [
1815
+ `name: ${JSON.stringify(name)}`,
1816
+ `prompt: ${JSON.stringify(prompt)}`,
1817
+ `project: ${JSON.stringify(safeProjectName)}`,
1818
+ ...(collectionValue ? [`collection: ${JSON.stringify(collectionValue)}`] : []),
1819
+ ...(suiteValue ? [`suite: ${JSON.stringify(suiteValue)}`] : []),
1820
+ 'tags: []',
1821
+ `priority: ${JSON.stringify('normal')}`,
1822
+ `created_at: ${JSON.stringify(new Date().toISOString())}`,
1823
+ smartYamlContent
1824
+ ].join('\n') + '\n';
1825
+ }
1044
1826
 
1045
1827
  // Internal fallback backup for the agent
1046
1828
  fs.writeFileSync(yamlPathMissions, yamlData);
@@ -1048,11 +1830,13 @@ async function main() {
1048
1830
  // Primary save to user's specified directory from arcality.config
1049
1831
  if (projectConfig) {
1050
1832
  const yamlOutputDir = ensureYamlOutputDir(projectConfig, ORIGINAL_CWD);
1051
- const yamlPathOutput = path.join(yamlOutputDir, `${safeName}.yaml`);
1833
+ const yamlOutputTargetDir = missionSaveDir.relDir ? path.join(yamlOutputDir, missionSaveDir.relDir) : yamlOutputDir;
1834
+ fs.mkdirSync(yamlOutputTargetDir, { recursive: true });
1835
+ const yamlPathOutput = path.join(yamlOutputTargetDir, `${safeName}.yaml`);
1052
1836
  fs.writeFileSync(yamlPathOutput, yamlData);
1053
- note(chalk.green(`✅ Mission saved at: ${yamlPathOutput}`), 'Arcality Persistence');
1837
+ note(chalk.green(`✅ Misión guardada en: ${yamlPathOutput}`), 'Persistencia Arcality');
1054
1838
  } else {
1055
- note(chalk.green(`✅ Mission saved at: ${yamlPathMissions}`), 'Arcality Persistence');
1839
+ note(chalk.green(`✅ Misión guardada en: ${yamlPathMissions}`), 'Persistencia Arcality');
1056
1840
  }
1057
1841
  }
1058
1842
  }
@@ -1061,12 +1845,12 @@ async function main() {
1061
1845
  if (userCanceledMission) {
1062
1846
  missionResult = 'canceled';
1063
1847
  finalFailReason = 'user_canceled';
1064
- console.log(chalk.yellow('\n⚠️ Misión cancelada — generando reporte de evidencia...'));
1848
+ console.log(chalk.yellow('\n⚠️ Directiva abortada — generando reporte de evidencia...'));
1065
1849
  } else {
1066
1850
  missionResult = 'failed';
1067
1851
  finalFailReason = 'timeout_or_crash';
1068
- s.stop(chalk.red(`❌ Mission could not be completed.`));
1069
- console.error(chalk.red(`\nError Details: ${e.message}`));
1852
+ s.stop(chalk.red(`❌ Arcality no pudo completar la directiva.`));
1853
+ console.error(chalk.red(`\nDetalles del Error: ${e.message}`));
1070
1854
  if (e.stack && process.argv.includes('--debug')) {
1071
1855
  console.error(chalk.gray(e.stack));
1072
1856
  }
@@ -1081,7 +1865,7 @@ async function main() {
1081
1865
  if (userCanceledMission) {
1082
1866
  missionResult = 'canceled';
1083
1867
  finalFailReason = 'user_canceled';
1084
- finalFailReasoning = 'La misión fue detenida manualmente por el usuario (Ctrl+C) antes de completarse. No se trata de un fallo técnico del sistema ni del portal.';
1868
+ finalFailReasoning = 'La misión fue detenida manualmente por el operador (Ctrl+C). Esto no es una falla técnica del sistema.';
1085
1869
  await new Promise(r => setTimeout(r, 800));
1086
1870
  }
1087
1871
 
@@ -1090,9 +1874,9 @@ async function main() {
1090
1874
 
1091
1875
  const sRep = spinner();
1092
1876
  if (!process.argv.includes('--debug')) {
1093
- sRep.start('📊 Generating report...');
1877
+ sRep.start('📊 Generando reporte...');
1094
1878
  } else {
1095
- console.log('📊 Generating report...');
1879
+ console.log('📊 Generando reporte...');
1096
1880
  }
1097
1881
 
1098
1882
  await run("node", [path.join(PROJECT_ROOT, "scripts/rebrand-report.mjs")]).catch(() => { });
@@ -1106,8 +1890,11 @@ async function main() {
1106
1890
  try {
1107
1891
  const ctxDir = process.env.CONTEXT_DIR;
1108
1892
  if (fs.existsSync(ctxDir)) {
1109
- const files = fs.readdirSync(ctxDir);
1110
- const logFiles = files.filter(f => f.startsWith('agent-log-') && f.endsWith('.json')).sort();
1893
+ const files = fs.readdirSync(ctxDir);
1894
+ const logFiles = files.filter(f =>
1895
+ (f.startsWith('arcality-log-') || f.startsWith('agent-log-')) &&
1896
+ f.endsWith('.json')
1897
+ ).sort();
1111
1898
  if (logFiles.length > 0) {
1112
1899
  const lastLog = path.join(ctxDir, logFiles[logFiles.length - 1]);
1113
1900
  const logData = JSON.parse(fs.readFileSync(lastLog, 'utf8'));
@@ -1134,7 +1921,7 @@ async function main() {
1134
1921
 
1135
1922
  if (sasData && actualSasUrl && actualBasePath) {
1136
1923
  if (!process.argv.includes('--debug')) {
1137
- sRep.start('☁️ Uploading evidence to Azure...');
1924
+ sRep.start('☁️ Subiendo evidencia a Azure...');
1138
1925
  }
1139
1926
  process.removeAllListeners('SIGINT');
1140
1927
  process.removeAllListeners('SIGTERM');
@@ -1148,14 +1935,14 @@ async function main() {
1148
1935
  finalReportUrl = `${parsedSas.origin}${parsedSas.pathname}/${actualBasePath}/index.html`;
1149
1936
  } catch(e) {}
1150
1937
 
1151
- if (!process.argv.includes('--debug')) sRep.stop(chalk.green('✅ Evidence uploaded successfully.'));
1938
+ if (!process.argv.includes('--debug')) sRep.stop(chalk.green('✅ Evidencia subida exitosamente.'));
1152
1939
  }
1153
1940
  } catch (e) {
1154
1941
  if (process.argv.includes('--debug')) console.log(chalk.yellow(`⚠️ Could not upload evidence: ${e.message}`));
1155
- if (!process.argv.includes('--debug')) sRep.stop(chalk.yellow('⚠️ Evidence upload failed.'));
1942
+ if (!process.argv.includes('--debug')) sRep.stop(chalk.yellow('⚠️ Fallo al subir evidencia.'));
1156
1943
  }
1157
1944
  } else {
1158
- console.log(chalk.gray(' >> Skipping evidence upload (mission was canceled).'));
1945
+ console.log(chalk.gray(' >> Saltando subida de evidencia (la misión fue cancelada).'));
1159
1946
  }
1160
1947
 
1161
1948
  // ── End Mission — SIEMPRE se llama, independientemente de si la evidencia subió ──
@@ -1178,16 +1965,16 @@ async function main() {
1178
1965
  globalReportProcess = null;
1179
1966
  }
1180
1967
 
1181
- if (fs.existsSync(arcalityPath)) {
1182
- console.log(chalk.blue(`🌐 Opening Arcality Report: ${arcalityPath}`));
1183
- if (process.platform === "win32") {
1184
- exec(`start "" "${arcalityPath}"`);
1185
- } else {
1186
- exec(`open "${arcalityPath}"`);
1187
- }
1188
- } else {
1189
- console.log(chalk.yellow(`⚠️ Report not generated (test may have crashed before completing).`));
1190
- }
1968
+ if (!batchMode && fs.existsSync(arcalityPath)) {
1969
+ console.log(chalk.blue(`🌐 Abriendo Reporte de Arcality: ${arcalityPath}`));
1970
+ if (process.platform === "win32") {
1971
+ exec(`start "" "${arcalityPath}"`);
1972
+ } else {
1973
+ exec(`open "${arcalityPath}"`);
1974
+ }
1975
+ } else if (!fs.existsSync(arcalityPath)) {
1976
+ console.log(chalk.yellow(`⚠️ Reporte no generado (la prueba pudo haber fallado antes de completarse).`));
1977
+ }
1191
1978
 
1192
1979
  await new Promise(resolve => setTimeout(resolve, 2000));
1193
1980
 
@@ -1197,52 +1984,61 @@ async function main() {
1197
1984
  process.env.ARCALITY_API_KEY
1198
1985
  );
1199
1986
 
1200
- // Esperar Enter para regresar al menú
1201
- // Usamos escucha directa de stdin (raw mode) porque @clack/text
1202
- // no procesa Enter correctamente cuando setRawMode(true) está activo.
1203
- console.log(chalk.gray('\n Press Enter to return to menu...'));
1204
- await new Promise(resolve => {
1205
- const onKey = (key) => {
1206
- const keyStr = String(key);
1207
- if (keyStr.includes('\r') || keyStr.includes('\n') || keyStr === '\u0003') {
1208
- process.stdin.off('data', onKey);
1209
-
1210
- // Restaurar stdin en modo normal para @clack/prompts
1211
- if (process.stdin.isTTY && process.stdin.setRawMode) {
1212
- process.stdin.setRawMode(false);
1213
- }
1214
- process.stdin.pause();
1987
+ // Limpiar handlers del post-proceso ANTES de esperar Enter, ya que toda la telemetría y reportes ya finalizaron
1988
+ process.removeListener('SIGINT', postProcessSigint);
1989
+ process.removeListener('SIGTERM', postProcessSigint);
1215
1990
 
1216
- if (keyStr === '\u0003') process.emit('SIGINT'); // Ctrl+C = salir
1217
- else resolve();
1218
- }
1219
- };
1220
-
1221
- // ── BUGFIX: @clack/prompts llama explícitamente a process.stdin.pause()
1222
- // al terminar cada select()/text(). En Node.js, agregar un listener 'data'
1223
- // a un stream EXPLÍCITAMENTE pausado NO lo reactiva automáticamente.
1224
- // Resultado: el event loop queda vacío y el proceso termina antes de que
1225
- // el usuario pueda presionar Enter.
1226
- // Solución: llamar resume() explícitamente antes de registrar el listener.
1227
- if (process.stdin.isTTY && process.stdin.setRawMode) {
1228
- process.stdin.setRawMode(true); // raw mode para capturar Enter inmediatamente
1229
- }
1230
- process.stdin.resume(); // ← CRÍTICO: reactiva stdin para mantener el event loop vivo
1231
- process.stdin.on('data', onKey);
1232
- });
1991
+ if (!batchMode) {
1992
+ // Esperar Enter para regresar al menú
1993
+ // Usamos escucha directa de stdin (raw mode) porque @clack/text
1994
+ // no procesa Enter correctamente cuando setRawMode(true) está activo.
1995
+ console.log(chalk.gray('\n Presiona Enter para regresar al menú...'));
1996
+ await new Promise(resolve => {
1997
+ const onKey = (key) => {
1998
+ const keyStr = String(key);
1999
+ if (keyStr.includes('\r') || keyStr.includes('\n') || keyStr === '\u0003') {
2000
+ process.stdin.off('data', onKey);
2001
+
2002
+ // Restaurar stdin en modo normal para @clack/prompts
2003
+ if (process.stdin.isTTY && process.stdin.setRawMode) {
2004
+ process.stdin.setRawMode(false);
2005
+ }
2006
+ // FIX: pausar stdin si vamos a salir para que el event loop no se quede colgado.
2007
+ // Si volvemos al menú, no lo pausamos (para evitar el bug de exit en clack).
2008
+ if (skipMenu) {
2009
+ process.stdin.pause();
2010
+ }
2011
+
2012
+ if (keyStr === '\u0003') {
2013
+ process.exit(0); // Ctrl+C = salir inmediatamente
2014
+ } else {
2015
+ resolve();
2016
+ }
2017
+ }
2018
+ };
2019
+
2020
+ // ── BUGFIX: @clack/prompts llama explícitamente a process.stdin.pause()
2021
+ // al terminar cada select()/text(). En Node.js, agregar un listener 'data'
2022
+ // a un stream EXPLÍCITAMENTE pausado NO lo reactiva automáticamente.
2023
+ // Resultado: el event loop queda vacío y el proceso termina antes de que
2024
+ // el usuario pueda presionar Enter.
2025
+ // Solución: llamar resume() explícitamente antes de registrar el listener.
2026
+ if (process.stdin.isTTY && process.stdin.setRawMode) {
2027
+ process.stdin.setRawMode(true); // raw mode para capturar Enter inmediatamente
2028
+ }
2029
+ process.stdin.resume(); // ← CRÍTICO: reactiva stdin para mantener el event loop vivo
2030
+ process.stdin.on('data', onKey);
2031
+ });
2032
+ }
1233
2033
 
1234
2034
  if (skipMenu) {
1235
- outro(chalk.cyan('Mission finished! See you later.'));
2035
+ outro(chalk.cyan('¡Misión terminada! Hasta luego.'));
1236
2036
  return;
1237
2037
  }
1238
2038
 
1239
2039
  agentMode = false;
1240
2040
  prompt = "";
1241
2041
  firstRun = false;
1242
-
1243
- // Limpiar handlers del post-proceso
1244
- process.removeListener('SIGINT', postProcessSigint);
1245
- process.removeListener('SIGTERM', postProcessSigint);
1246
2042
  }
1247
2043
  }
1248
2044
  firstRun = false;