@arcadialdev/arcality 2.4.48 → 2.4.50

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arcadialdev/arcality",
3
- "version": "2.4.48",
3
+ "version": "2.4.50",
4
4
  "description": "El primer ingeniero QA Autónomo integrado al CI/CD. Creado por Arcadial.",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -51,7 +51,8 @@
51
51
  "@types/figlet": "^1.7.0",
52
52
  "@types/node": "^25.0.9",
53
53
  "@types/prompts": "^2.4.9",
54
- "esbuild": "^0.20.2"
54
+ "esbuild": "^0.20.2",
55
+ "typescript": "^6.0.3"
55
56
  },
56
57
  "dependencies": {
57
58
  "@azure/storage-blob": "^12.31.0",
@@ -69,4 +70,4 @@
69
70
  "terminal-image": "^1.1.0",
70
71
  "tsx": "^4.21.0"
71
72
  }
72
- }
73
+ }
@@ -34,7 +34,7 @@ const PROJECT_ROOT = process.env.ARCALITY_ROOT || path.join(__dirname, "..");
34
34
  const ORIGINAL_CWD = process.cwd();
35
35
 
36
36
  // ── Load arcality.config if present ──
37
- const projectConfig = loadProjectConfig(ORIGINAL_CWD) || loadProjectConfig(PROJECT_ROOT);
37
+ let projectConfig = loadProjectConfig(ORIGINAL_CWD) || loadProjectConfig(PROJECT_ROOT);
38
38
  if (projectConfig) {
39
39
  injectConfigToEnv(projectConfig);
40
40
  }
@@ -324,6 +324,9 @@ function run(cmd, args, options = {}) {
324
324
  env,
325
325
  });
326
326
 
327
+ // Exponer el proceso hijo para que el cancelHandler pueda matarlo
328
+ if (options.onSpawn) options.onSpawn(p);
329
+
327
330
  let outputBuffer = '';
328
331
  const handleData = (data) => {
329
332
  const str = data.toString();
@@ -467,7 +470,6 @@ async function main() {
467
470
  ...(savedMissions.length ? [{ label: '🚀 Launch Saved Mission (.yaml)', value: 'launch_saved' }] : []),
468
471
  ...(yamlOutputFiles.length ? [{ label: '♻️ Reuse YAML + Auto-Guide (if prev. run exists)', value: 'reuse_yaml' }] : []),
469
472
  ...(rootYamlFiles.length ? [{ label: '📂 Choose Root YAML (Templates)', value: 'yaml_list' }] : []),
470
- { label: '📋 View Current Configuration', value: 'view_config' },
471
473
  { label: '🔄 Reconfigure (arcality init)', value: 'reconfigure' },
472
474
  { label: '❌ Exit', value: 'exit' }
473
475
  ];
@@ -484,28 +486,6 @@ async function main() {
484
486
 
485
487
  if (action === 'agent') {
486
488
  agentMode = true;
487
- } else if (action === 'view_config') {
488
- if (projectConfig) {
489
- note(
490
- chalk.bold.white('📋 Project: ') + chalk.cyan(projectConfig.project?.name) + '\n' +
491
- chalk.bold.white('🌐 Base URL: ') + chalk.cyan(projectConfig.project?.baseUrl) + '\n' +
492
- chalk.bold.white('🛠️ Framework: ') + chalk.magenta(projectConfig.project?.frameworkDetected || 'N/A') + '\n' +
493
- chalk.bold.white('🆔 Project ID: ') + chalk.gray(projectConfig.projectId) + '\n' +
494
- chalk.bold.white('🏢 Org ID: ') + chalk.gray(projectConfig.organizationId || 'N/A') + '\n' +
495
- chalk.bold.white('👤 User: ') + chalk.cyan(projectConfig.auth?.username) + '\n' +
496
- chalk.bold.white('📂 YAML Dir: ') + chalk.yellow(projectConfig.runtime?.yamlOutputDir) + '\n' +
497
- chalk.bold.white('♻️ Reuse YAMLs: ') + chalk.cyan(projectConfig.runtime?.reuseSuccessfulYamls ? '✅' : '❌') + '\n' +
498
- chalk.bold.white('📦 Version: ') + chalk.yellow(projectConfig.meta?.arcalityVersion || 'N/A'),
499
- 'arcality.config'
500
- );
501
- } else {
502
- note(
503
- chalk.yellow('No arcality.config found.') + '\n' +
504
- chalk.gray('Run `arcality init` to configure this project.'),
505
- 'No Configuration'
506
- );
507
- }
508
- continue;
509
489
  } else if (action === 'reconfigure') {
510
490
  // Launch arcality init in the user's project directory
511
491
  const initScript = path.join(PROJECT_ROOT, 'scripts', 'init.mjs');
@@ -519,7 +499,20 @@ async function main() {
519
499
  child.on('exit', code => code === 0 ? resolve() : reject(new Error(`Init exited with ${code}`)));
520
500
  child.on('error', reject);
521
501
  });
522
- } catch { /* user cancelled or error */ }
502
+
503
+ // ── REFRESH CONFIG ──
504
+ // Recargar el archivo que init.mjs acaba de escribir
505
+ projectConfig = loadProjectConfig(ORIGINAL_CWD) || loadProjectConfig(PROJECT_ROOT);
506
+ if (projectConfig) {
507
+ injectConfigToEnv(projectConfig);
508
+ }
509
+ setupEnvironment();
510
+
511
+ } catch (e) {
512
+ // Mostrar el error claramente antes de que console.clear() lo borre
513
+ console.log(chalk.red(`\n❌ Reconfigure failed: ${e.message}`));
514
+ await new Promise(r => setTimeout(r, 3000));
515
+ }
523
516
  continue;
524
517
  } else if (action === 'launch_saved') {
525
518
  const sel = await select({
@@ -747,6 +740,63 @@ async function main() {
747
740
 
748
741
  const finalProjectId = process.env.ARCALITY_PROJECT_ID || selectedProjectId;
749
742
 
743
+ // ── SIGINT Handler: Cancelación manual (Ctrl+C / ESC) ──
744
+ // Se registra AQUÍ, justo antes del spawn, para que solo aplique durante la ejecución.
745
+ // Al cancelar: mata el proceso hijo, guarda estado "canceled", llama endMission y genera reporte.
746
+ let activeChildProcess = null; // Referencia al proceso hijo de Playwright
747
+ let userCanceledMission = false;
748
+ const cancelHandler = async () => {
749
+ if (userCanceledMission) return; // Evitar doble disparo
750
+ userCanceledMission = true;
751
+
752
+ console.log(chalk.yellow('\n\n⚠️ [ARCALITY] Misión cancelada por el usuario.'));
753
+ if (!process.argv.includes('--debug')) s.stop(chalk.yellow('⚠️ Misión cancelada por el usuario.'));
754
+
755
+ // 1. Escribir un agent-log de cancelación para que el reporte lo refleje
756
+ try {
757
+ const ctxDir = process.env.CONTEXT_DIR;
758
+ if (ctxDir && fs.existsSync(ctxDir)) {
759
+ const cancelLog = {
760
+ prompt,
761
+ history: ['[CANCELADO POR USUARIO] La prueba fue detenida manualmente antes de completarse.'],
762
+ steps: 0,
763
+ success: false,
764
+ error: true,
765
+ fail_reason: 'user_canceled',
766
+ canceled_by_user: true,
767
+ timestamp: new Date().toISOString()
768
+ };
769
+ fs.writeFileSync(path.join(ctxDir, `agent-log-${Date.now()}.json`), JSON.stringify(cancelLog, null, 2));
770
+ }
771
+ } catch { /* silencioso */ }
772
+
773
+ // 2. Llamar endMission con estado 'canceled'
774
+ try {
775
+ const { endMission: endM } = await import('../src/arcalityClient.mjs');
776
+ await endM(mission.mission_id, 'canceled', null, 'user_canceled');
777
+ console.log(chalk.gray(' >> Misión registrada como "cancelada" en el servidor.'));
778
+ } catch { /* silencioso si el backend no está disponible */ }
779
+
780
+ // 3. Matar el proceso hijo de Playwright — hacerlo AL FINAL para dar tiempo
781
+ // a que el agente-log y endMission terminen antes del kill.
782
+ // NO llamar process.exit() aquí: el finally del try/catch principal
783
+ // se encarga de generar el reporte visual y abrirlo en el browser.
784
+ if (activeChildProcess) {
785
+ try {
786
+ if (process.platform === 'win32') {
787
+ spawn('taskkill', ['/pid', activeChildProcess.pid, '/f', '/t'], { stdio: 'ignore', windowsHide: true });
788
+ } else {
789
+ activeChildProcess.kill('SIGTERM');
790
+ }
791
+ } catch { /* silencioso */ }
792
+ }
793
+ // El bloque finally del try/catch de run() tomará el control
794
+ // a partir de aquí (el proceso hijo ya terminó → run() rechaza → catch → finally).
795
+ };
796
+
797
+ process.once('SIGINT', cancelHandler);
798
+ process.once('SIGTERM', cancelHandler);
799
+
750
800
  // Build env for the runner — merge arcality.config values.
751
801
  // ARCALITY_API_URL must be explicit — it comes from the library's internal config,
752
802
  // not from the user's project .env, so we inject it directly via getApiUrl().
@@ -812,10 +862,17 @@ async function main() {
812
862
  '--headed',
813
863
  '--config', 'playwright.config.js'
814
864
  ], {
815
- cwd: PROJECT_ROOT, // Run from the arcality package root
865
+ cwd: PROJECT_ROOT,
816
866
  env: mergedEnv,
817
- onStatus: (msg) => { if (!process.argv.includes('--debug')) s.message(msg); else console.log(chalk.gray(`>> Status: ${msg}`)); }
867
+ onStatus: (msg) => { if (!process.argv.includes('--debug')) s.message(msg); else console.log(chalk.gray(`>> Status: ${msg}`)); },
868
+ onSpawn: (child) => { activeChildProcess = child; } // ← Capturar referencia para cancelHandler
818
869
  });
870
+
871
+ // Eliminar handlers SIGINT ahora que el proceso terminó normalmente
872
+ process.off('SIGINT', cancelHandler);
873
+ process.off('SIGTERM', cancelHandler);
874
+
875
+ if (userCanceledMission) return; // El handler ya tomó el control
819
876
  if (!process.argv.includes('--debug')) s.stop(chalk.green('✅ Mission completed successfully.'));
820
877
  else console.log(chalk.green('✅ Mission completed successfully.'));
821
878
 
@@ -905,50 +962,66 @@ async function main() {
905
962
  }
906
963
  }
907
964
  } catch (e) {
908
- s.stop(chalk.red(`❌ Mission could not be completed.`));
909
- console.error(chalk.red(`\nError Details: ${e.message}`));
910
- if (e.stack && process.argv.includes('--debug')) {
911
- console.error(chalk.gray(e.stack));
912
- }
965
+ // Eliminar handlers SIGINT en el path de error también
966
+ process.off('SIGINT', cancelHandler);
967
+ process.off('SIGTERM', cancelHandler);
968
+
969
+ if (userCanceledMission) {
970
+ // El cancelHandler ya registró la cancelación en el servidor.
971
+ // Solo mostramos un mensaje y dejamos que el finally genere el reporte.
972
+ console.log(chalk.yellow('\n⚠️ Misión cancelada — generando reporte de evidencia...'));
973
+ } else {
974
+ s.stop(chalk.red(`❌ Mission could not be completed.`));
975
+ console.error(chalk.red(`\nError Details: ${e.message}`));
976
+ if (e.stack && process.argv.includes('--debug')) {
977
+ console.error(chalk.gray(e.stack));
978
+ }
913
979
 
914
- // End mission with failure
915
- try {
916
- let finalUsage = undefined;
917
- let failReason = "timeout_or_crash";
980
+ // Si NO fue cancelación de usuario, cerrar la misión como fallida en el servidor
918
981
  try {
919
- const ctxDir = process.env.CONTEXT_DIR;
920
- if (fs.existsSync(ctxDir)) {
921
- const files = fs.readdirSync(ctxDir);
922
- const logFiles = files.filter(f => f.startsWith('agent-log-') && f.endsWith('.json')).sort();
923
- if (logFiles.length > 0) {
924
- const lastLog = path.join(ctxDir, logFiles[logFiles.length - 1]);
925
- const logData = JSON.parse(fs.readFileSync(lastLog, 'utf8'));
926
- finalUsage = logData.usage;
927
- failReason = logData.fail_reason || failReason;
982
+ let finalUsage = undefined;
983
+ let failReason = "timeout_or_crash";
984
+ try {
985
+ const ctxDir = process.env.CONTEXT_DIR;
986
+ if (fs.existsSync(ctxDir)) {
987
+ const files = fs.readdirSync(ctxDir);
988
+ const logFiles = files.filter(f => f.startsWith('agent-log-') && f.endsWith('.json')).sort();
989
+ if (logFiles.length > 0) {
990
+ const lastLog = path.join(ctxDir, logFiles[logFiles.length - 1]);
991
+ const logData = JSON.parse(fs.readFileSync(lastLog, 'utf8'));
992
+ finalUsage = logData.usage;
993
+ failReason = logData.fail_reason || failReason;
994
+ }
928
995
  }
929
- }
930
- } catch(e) {}
996
+ } catch(e) {}
931
997
 
932
- // ── Upload Evidence ──
933
- const { endMission, getEvidenceSasToken } = await import('../src/arcalityClient.mjs');
934
- const { uploadEvidence } = await import('../src/EvidenceUploader.mjs');
998
+ // ── Upload Evidence ──
999
+ const { endMission, getEvidenceSasToken } = await import('../src/arcalityClient.mjs');
1000
+ const { uploadEvidence } = await import('../src/EvidenceUploader.mjs');
935
1001
 
936
- try {
937
- const sasData = await getEvidenceSasToken(mission.mission_id, finalProjectId);
938
- if (sasData && sasData.sasUrl) {
939
- if (!process.argv.includes('--debug')) s.start('☁️ Uploading evidence to Azure...');
940
- const reportsDir = process.env.REPORTS_DIR || path.join(ORIGINAL_CWD, 'tests-report');
941
-
942
- await run("node", [path.join(PROJECT_ROOT, "scripts/rebrand-report.mjs")]).catch(() => { });
943
-
944
- await uploadEvidence(reportsDir, sasData.sasUrl, sasData.basePath);
945
- if (!process.argv.includes('--debug')) s.stop(chalk.green('✅ Evidence uploaded successfully.'));
946
- }
947
- } catch (e) {}
1002
+ try {
1003
+ const sasData = await getEvidenceSasToken(mission.mission_id, finalProjectId);
1004
+ if (sasData && sasData.sasUrl) {
1005
+ if (!process.argv.includes('--debug')) s.start('☁️ Uploading evidence to Azure...');
1006
+ const reportsDir = process.env.REPORTS_DIR || path.join(ORIGINAL_CWD, 'tests-report');
948
1007
 
949
- await endMission(mission.mission_id, 'failed', finalUsage, failReason);
950
- } catch { }
1008
+ await run("node", [path.join(PROJECT_ROOT, "scripts/rebrand-report.mjs")]).catch(() => { });
1009
+
1010
+ await uploadEvidence(reportsDir, sasData.sasUrl, sasData.basePath);
1011
+ if (!process.argv.includes('--debug')) s.stop(chalk.green('✅ Evidence uploaded successfully.'));
1012
+ }
1013
+ } catch (e) {}
1014
+
1015
+ await endMission(mission.mission_id, 'failed', finalUsage, failReason);
1016
+ } catch { }
1017
+ }
951
1018
  } finally {
1019
+ // Si fue cancelación de usuario, dar un breve margen para que el cancelHandler
1020
+ // async (endMission, escritura de log) termine antes de generar el reporte.
1021
+ if (userCanceledMission) {
1022
+ await new Promise(r => setTimeout(r, 800));
1023
+ }
1024
+
952
1025
  const sRep = spinner();
953
1026
  sRep.start('📊 Generating report...');
954
1027
  await run("node", [path.join(PROJECT_ROOT, "scripts/rebrand-report.mjs")]).catch(() => { });
package/scripts/init.mjs CHANGED
@@ -143,8 +143,12 @@ async function main() {
143
143
  401: '❌ Invalid API Key. Please check your key and try again.',
144
144
  403: `❌ Authentication Failed (HTTP 403). Server says: ${errText.slice(0, 150)}`,
145
145
  };
146
- s.stop(chalk.red(errorMap[res.status] || `❌ Server error (HTTP ${res.status}): ${errText.slice(0, 150)}`));
147
- process.exit(1);
146
+ const msg = errorMap[res.status] || `❌ Server error (HTTP ${res.status}): ${errText.slice(0, 150)}`;
147
+ s.stop(chalk.red(msg));
148
+ console.log(chalk.yellow('\n Press Enter to return to menu...'));
149
+ // Salida limpia (código 0) para que el menú padre no interprete esto como crash
150
+ await new Promise(r => setTimeout(r, 3000));
151
+ process.exit(0);
148
152
  }
149
153
 
150
154
  const data = await res.json();
@@ -162,7 +166,8 @@ async function main() {
162
166
  } catch (err) {
163
167
  s.stop(chalk.red(`❌ Could not connect to Arcality backend: ${err.message}`));
164
168
  console.log(chalk.yellow(' Make sure the backend is running and accessible.'));
165
- process.exit(1);
169
+ await new Promise(r => setTimeout(r, 3000));
170
+ process.exit(0);
166
171
  }
167
172
 
168
173
  // ── Step 3: Project details ──