@arcadialdev/arcality 2.4.50 → 2.6.0

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/README.md CHANGED
@@ -29,43 +29,47 @@
29
29
  npm install @arcadialdev/arcality
30
30
  ```
31
31
 
32
- > After install, Arcality **automatically adds the following scripts** to your `package.json`:
33
- > ```json
34
- > "arcality": "arcality run",
35
- > "arcality:init": "arcality init",
36
- > "arcality:run": "arcality run"
37
- > ```
38
32
 
39
33
  ### 2. Initialize your project
40
- Run the setup wizard at the root of your project. Arcality will auto-detect your framework and scaffold the `arcality.config.json`.
34
+ Run the setup wizard to connect your project and generate the configuration.
41
35
 
42
36
  ```bash
43
- npm run arcality:init
37
+ npx arcality init
44
38
  ```
45
39
 
46
- ### 3. Run an autonomous mission
47
-
40
+ ### 3. Run your first mission
48
41
  ```bash
49
- # Interactive menu
50
- npm run arcality
42
+ # Open interactive menu
43
+ npx arcality
44
+
45
+ # Or run directly (Ghost Mode)
46
+ npx arcality run
47
+ ```
48
+
51
49
 
52
- # Direct mission
53
- npm run arcality:run
50
+ ### 📦 Updating Arcality
51
+ Run the following to get the latest version:
52
+ ```bash
53
+ npm update @arcadialdev/arcality
54
54
  ```
55
55
 
56
+ ### ⚙️ Configuration (`arcality.config.json`)
57
+ After initializing with `npx arcality init`, a config file is created in the project root. Edit it to set your project name, base URL, and authentication details. The CLI reads this file on every run, ensuring consistent environment variables.
58
+
59
+
56
60
  ---
57
61
 
58
62
  ## 💻 CLI Commands Cheatsheet
59
63
 
60
- The Arcality flow is designed to be used globally or project-wide via \`npx\`. Below are all currently supported commands:
64
+ The Arcality flow is designed to be used globally or project-wide via `npx`. Below are all currently supported commands:
61
65
 
62
66
  | Command | Description | Internal Behavior |
63
67
  | :--- | :--- | :--- |
64
- | \`npx arcality\` | **Interactive Menu**<br/>Launches a visual menu in the terminal to guide the user. | Prompts for the Mission to execute. Ideal for new users who want a guided experience. |
65
- | \`npx arcality init\` | **Initial Configuration**<br/>Connects the current project to Arcality Cloud. | Prompts for your API Key and Project ID, generates \`arcality.config.json\`, and validates the ADO / Portal connection. |
66
- | \`npx arcality setup\` | **Dependency Setup**<br/>Downloads browser binaries. | Silently executes the installation of internal Playwright browsers (Chromium) ensuring the local machine can run tests. |
67
- | \`npx arcality run\` | **Mission Runner (Agent Mode)**<br/>Launches the QA Agent directly. | Skips the interactive menu, scans the _package.json_ ("Ghost Mode"), injects ephemeral _autoskills_, and unleashes the AI agent. Ideal for CI/CD. |
68
- | \`npx arcality --logs\` | **History Viewer**<br/>Displays test performance and tokens spent. | Reads the local \`arcality-history.log\` and renders a colorful table listing each mission, token consumption, and success status. |
68
+ | `npx arcality` | **Interactive Menu**<br/>Launches a visual menu in the terminal to guide the user. | Prompts for the Mission to execute. Ideal for new users who want a guided experience. |
69
+ | `npx arcality init` | **Initial Configuration**<br/>Connects the current project to Arcadial. | Prompts for your API Key and Project ID, generates `arcality.config.json`, and validates the portal connection. |
70
+ | `npx arcality run` | **Mission Runner (Agent Mode)**<br/>Launches the QA Agent directly. | Skips the interactive menu, scans the _package.json_ ("Ghost Mode"), and unleashes the AI agent. Ideal for CI/CD. |
71
+ | `npx arcality --logs` | **History Viewer**<br/>Displays test performance and tokens spent. | Reads the local `arcality-history.log` and renders a colorful table listing each mission and success status. |
72
+
69
73
 
70
74
  ---
71
75
 
@@ -83,7 +87,7 @@ Arcality operates on a highly decoupled modular system designed for maximum resi
83
87
 
84
88
  ## 📁 Configuration (`arcality.config.json`)
85
89
 
86
- When you run `arcality init`, a configuration is generated in your project root.
90
+ When you run `npx arcality init`, a configuration is generated in your project root.
87
91
  ```json
88
92
  {
89
93
  "project": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arcadialdev/arcality",
3
- "version": "2.4.50",
3
+ "version": "2.6.0",
4
4
  "description": "El primer ingeniero QA Autónomo integrado al CI/CD. Creado por Arcadial.",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -770,12 +770,8 @@ async function main() {
770
770
  }
771
771
  } catch { /* silencioso */ }
772
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 */ }
773
+ // 2. Registraremos la misión como 'canceled' en el bloque finally
774
+ console.log(chalk.gray(' >> Registrando misión como "cancelada"...'));
779
775
 
780
776
  // 3. Matar el proceso hijo de Playwright — hacerlo AL FINAL para dar tiempo
781
777
  // a que el agente-log y endMission terminen antes del kill.
@@ -852,6 +848,11 @@ async function main() {
852
848
  process.exit(1);
853
849
  }
854
850
 
851
+ let missionResult = null;
852
+ let finalFailReason = null;
853
+ let finalUsageData = undefined;
854
+ let finalReportUrl = null;
855
+
855
856
  try {
856
857
  // No test file path is passed — playwright.config.js uses testMatch:'agentic-runner.spec.ts'
857
858
  // This eliminates the Windows regex path issue permanently on any user's machine.
@@ -872,46 +873,15 @@ async function main() {
872
873
  process.off('SIGINT', cancelHandler);
873
874
  process.off('SIGTERM', cancelHandler);
874
875
 
875
- if (userCanceledMission) return; // El handler ya tomó el control
876
- if (!process.argv.includes('--debug')) s.stop(chalk.green('✅ Mission completed successfully.'));
877
- else console.log(chalk.green('✅ Mission completed successfully.'));
878
-
879
- let finalUsage = undefined;
880
- try {
881
- const ctxDir = process.env.CONTEXT_DIR;
882
- if (fs.existsSync(ctxDir)) {
883
- const files = fs.readdirSync(ctxDir);
884
- const logFiles = files.filter(f => f.startsWith('agent-log-') && f.endsWith('.json')).sort();
885
- if (logFiles.length > 0) {
886
- const lastLog = path.join(ctxDir, logFiles[logFiles.length - 1]);
887
- const logData = JSON.parse(fs.readFileSync(lastLog, 'utf8'));
888
- finalUsage = logData.usage;
889
- }
890
- }
891
- } catch(e) {}
892
-
893
- // ── Upload Evidence ──
894
- const { endMission, getEvidenceSasToken } = await import('../src/arcalityClient.mjs');
895
- const { uploadEvidence } = await import('../src/EvidenceUploader.mjs');
896
-
897
- try {
898
- const sasData = await getEvidenceSasToken(mission.mission_id, finalProjectId);
899
- if (sasData && sasData.sasUrl) {
900
- if (!process.argv.includes('--debug')) s.start('☁️ Uploading evidence to Azure...');
901
- const reportsDir = process.env.REPORTS_DIR || path.join(ORIGINAL_CWD, 'tests-report');
902
-
903
- await run("node", [path.join(PROJECT_ROOT, "scripts/rebrand-report.mjs")]).catch(() => { });
904
-
905
- await uploadEvidence(reportsDir, sasData.sasUrl, sasData.basePath);
906
- if (!process.argv.includes('--debug')) s.stop(chalk.green('✅ Evidence uploaded successfully.'));
907
- }
908
- } catch (e) {
909
- if (process.argv.includes('--debug')) console.log(chalk.yellow(`⚠️ Could not upload evidence: ${e.message}`));
910
- if (!process.argv.includes('--debug')) s.stop(chalk.yellow('⚠️ Evidence upload failed.'));
876
+ if (userCanceledMission) {
877
+ missionResult = 'canceled';
878
+ finalFailReason = 'user_canceled';
879
+ return; // El handler ya tomó el control, finally se ejecutará y luego saldrá del CLI
911
880
  }
912
881
 
913
- // ── End Mission ──
914
- await endMission(mission.mission_id, 'success', finalUsage);
882
+ missionResult = 'success';
883
+ if (!process.argv.includes('--debug')) s.stop(chalk.green('✅ Mission completed successfully.'));
884
+ else console.log(chalk.green('✅ Mission completed successfully.'));
915
885
 
916
886
  // ── Ping Project ──
917
887
  await pingProject(finalProjectId, process.env.ARCALITY_API_KEY);
@@ -967,58 +937,24 @@ async function main() {
967
937
  process.off('SIGTERM', cancelHandler);
968
938
 
969
939
  if (userCanceledMission) {
940
+ missionResult = 'canceled';
941
+ finalFailReason = 'user_canceled';
970
942
  // El cancelHandler ya registró la cancelación en el servidor.
971
943
  // Solo mostramos un mensaje y dejamos que el finally genere el reporte.
972
944
  console.log(chalk.yellow('\n⚠️ Misión cancelada — generando reporte de evidencia...'));
973
945
  } else {
946
+ missionResult = 'failed';
947
+ finalFailReason = 'timeout_or_crash';
974
948
  s.stop(chalk.red(`❌ Mission could not be completed.`));
975
949
  console.error(chalk.red(`\nError Details: ${e.message}`));
976
950
  if (e.stack && process.argv.includes('--debug')) {
977
951
  console.error(chalk.gray(e.stack));
978
952
  }
979
-
980
- // Si NO fue cancelación de usuario, cerrar la misión como fallida en el servidor
981
- try {
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
- }
995
- }
996
- } catch(e) {}
997
-
998
- // ── Upload Evidence ──
999
- const { endMission, getEvidenceSasToken } = await import('../src/arcalityClient.mjs');
1000
- const { uploadEvidence } = await import('../src/EvidenceUploader.mjs');
1001
-
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');
1007
-
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
953
  }
1018
954
  } 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
955
  if (userCanceledMission) {
956
+ missionResult = 'canceled';
957
+ finalFailReason = 'user_canceled';
1022
958
  await new Promise(r => setTimeout(r, 800));
1023
959
  }
1024
960
 
@@ -1029,6 +965,58 @@ async function main() {
1029
965
  const reportDir = process.env.REPORTS_DIR || 'tests-report';
1030
966
  const arcalityPath = path.resolve(reportDir, 'index.html');
1031
967
 
968
+ sRep.stop(chalk.cyan('Report process initialized.'));
969
+
970
+ // Read final usage and failReason from agent-log
971
+ try {
972
+ const ctxDir = process.env.CONTEXT_DIR;
973
+ if (fs.existsSync(ctxDir)) {
974
+ const files = fs.readdirSync(ctxDir);
975
+ const logFiles = files.filter(f => f.startsWith('agent-log-') && f.endsWith('.json')).sort();
976
+ if (logFiles.length > 0) {
977
+ const lastLog = path.join(ctxDir, logFiles[logFiles.length - 1]);
978
+ const logData = JSON.parse(fs.readFileSync(lastLog, 'utf8'));
979
+ finalUsageData = logData.usage || finalUsageData;
980
+ if (missionResult === 'failed' && !finalFailReason) {
981
+ finalFailReason = logData.fail_reason || "timeout_or_crash";
982
+ }
983
+ }
984
+ }
985
+ } catch(e) {}
986
+
987
+ // ── Upload Evidence ──
988
+ try {
989
+ const { endMission, getEvidenceSasToken } = await import('../src/arcalityClient.mjs');
990
+ const { uploadEvidence } = await import('../src/EvidenceUploader.mjs');
991
+
992
+ const sasData = await getEvidenceSasToken(mission.mission_id, finalProjectId);
993
+ console.log(`[DEBUG] sasData received:`, sasData);
994
+
995
+ // Soporte para camelCase y snake_case debido al serializador JSON del backend
996
+ const actualSasUrl = sasData?.sasTokenUrl || sasData?.sasUrl || sasData?.sas_url;
997
+ const actualBasePath = sasData?.basePath || sasData?.base_path;
998
+
999
+ if (sasData && actualSasUrl && actualBasePath) {
1000
+ if (!process.argv.includes('--debug')) sRep.start('☁️ Uploading evidence to Azure...');
1001
+
1002
+ await uploadEvidence(reportDir, actualSasUrl, actualBasePath);
1003
+
1004
+ // Construct the public URL
1005
+ try {
1006
+ const parsedSas = new URL(actualSasUrl);
1007
+ finalReportUrl = `${parsedSas.origin}${parsedSas.pathname}/${actualBasePath}/index.html`;
1008
+ } catch(e) {}
1009
+
1010
+ if (!process.argv.includes('--debug')) sRep.stop(chalk.green('✅ Evidence uploaded successfully.'));
1011
+ }
1012
+
1013
+ // ── End Mission ──
1014
+ await endMission(mission.mission_id, missionResult || 'failed', finalUsageData, finalFailReason, finalReportUrl);
1015
+ } catch (e) {
1016
+ if (process.argv.includes('--debug')) console.log(chalk.yellow(`⚠️ Could not upload evidence or end mission: ${e.message}`));
1017
+ if (!process.argv.includes('--debug')) sRep.stop(chalk.yellow('⚠️ Evidence upload or end mission failed.'));
1018
+ }
1019
+
1032
1020
  if (globalReportProcess) {
1033
1021
  try {
1034
1022
  if (process.platform === "win32") {
@@ -1,45 +1,45 @@
1
- import fs from 'node:fs';
2
- import path from 'node:path';
3
- import { ContainerClient } from '@azure/storage-blob';
4
- import mime from 'mime-types';
5
-
6
- /**
7
- * Subes la evidencia de prueba iterativamente a Azure Blob Storage usando una URL de SAS.
8
- * @param {string} reportsDir Directorio local de reportes.
9
- * @param {string} sasUrl URL de SAS para el ContainerClient.
10
- * @param {string} basePath Path base dentro del contenedor de Azure.
11
- */
12
- export async function uploadEvidence(reportsDir, sasUrl, basePath) {
13
- if (!fs.existsSync(reportsDir)) return;
14
-
15
- const containerClient = new ContainerClient(sasUrl);
16
-
17
- async function walkAndUpload(currentDir, relativePath = '') {
18
- const entries = fs.readdirSync(currentDir, { withFileTypes: true });
19
- const uploadPromises = [];
20
-
21
- for (const entry of entries) {
22
- const fullPath = path.join(currentDir, entry.name);
23
- const entryRelativePath = path.posix.join(relativePath, entry.name);
24
-
25
- if (entry.isDirectory()) {
26
- uploadPromises.push(walkAndUpload(fullPath, entryRelativePath));
27
- } else {
28
- // Blob path en Azure: basePath/ruta_relativa_del_archivo
29
- const blobPath = `${basePath}/${entryRelativePath}`;
30
- const blockBlobClient = containerClient.getBlockBlobClient(blobPath);
31
-
32
- const contentType = mime.lookup(entry.name) || 'application/octet-stream';
33
-
34
- uploadPromises.push(
35
- blockBlobClient.uploadFile(fullPath, {
36
- blobHTTPHeaders: { blobContentType: contentType }
37
- })
38
- );
39
- }
40
- }
41
- await Promise.all(uploadPromises);
42
- }
43
-
44
- await walkAndUpload(reportsDir);
45
- }
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { ContainerClient } from '@azure/storage-blob';
4
+ import mime from 'mime-types';
5
+
6
+ /**
7
+ * Subes la evidencia de prueba iterativamente a Azure Blob Storage usando una URL de SAS.
8
+ * @param {string} reportsDir Directorio local de reportes.
9
+ * @param {string} sasUrl URL de SAS para el ContainerClient.
10
+ * @param {string} basePath Path base dentro del contenedor de Azure.
11
+ */
12
+ export async function uploadEvidence(reportsDir, sasUrl, basePath) {
13
+ if (!fs.existsSync(reportsDir)) return;
14
+
15
+ const containerClient = new ContainerClient(sasUrl);
16
+
17
+ async function walkAndUpload(currentDir, relativePath = '') {
18
+ const entries = fs.readdirSync(currentDir, { withFileTypes: true });
19
+ const uploadPromises = [];
20
+
21
+ for (const entry of entries) {
22
+ const fullPath = path.join(currentDir, entry.name);
23
+ const entryRelativePath = path.posix.join(relativePath, entry.name);
24
+
25
+ if (entry.isDirectory()) {
26
+ uploadPromises.push(walkAndUpload(fullPath, entryRelativePath));
27
+ } else {
28
+ // Blob path en Azure: basePath/ruta_relativa_del_archivo
29
+ const blobPath = `${basePath}/${entryRelativePath}`;
30
+ const blockBlobClient = containerClient.getBlockBlobClient(blobPath);
31
+
32
+ const contentType = mime.lookup(entry.name) || 'application/octet-stream';
33
+
34
+ uploadPromises.push(
35
+ blockBlobClient.uploadFile(fullPath, {
36
+ blobHTTPHeaders: { blobContentType: contentType }
37
+ })
38
+ );
39
+ }
40
+ }
41
+ await Promise.all(uploadPromises);
42
+ }
43
+
44
+ await walkAndUpload(reportsDir);
45
+ }
@@ -250,7 +250,7 @@ export async function startMission(prompt, targetUrl) {
250
250
  * @param {'success'|'failed'|'cancelled'} result
251
251
  * @param {object} usage - Optional usage statistics directly from the AI
252
252
  */
253
- export async function endMission(missionId, result, usage = null, failReason = null) {
253
+ export async function endMission(missionId, result, usage = null, failReason = null, reportUrl = null) {
254
254
  const apiBase = getEffectiveApiBase();
255
255
  if (!apiBase || !missionId) return { ok: true };
256
256
 
@@ -262,7 +262,7 @@ export async function endMission(missionId, result, usage = null, failReason = n
262
262
  'Content-Type': 'application/json',
263
263
  'x-api-key': key
264
264
  },
265
- body: JSON.stringify({ result, usage, failReason })
265
+ body: JSON.stringify({ result, usage, failReason, reportUrl })
266
266
  });
267
267
  } catch { }
268
268
  return { ok: true };
@@ -288,25 +288,32 @@ function simpleHash(str) {
288
288
  */
289
289
  export async function getEvidenceSasToken(missionId, projectId) {
290
290
  const apiBase = getEffectiveApiBase();
291
- if (!apiBase || !missionId || !projectId) return null;
291
+ if (!apiBase || !missionId) return null;
292
292
 
293
293
  const key = getEffectiveApiKey();
294
294
  if (!key) return null;
295
295
 
296
296
  try {
297
+ const organization_id = process.env.ARCALITY_ORG_ID || '';
297
298
  const res = await fetch(`${apiBase}/api/v1/missions/${missionId}/evidence/sas`, {
298
299
  method: 'POST',
299
300
  headers: {
300
301
  'Content-Type': 'application/json',
301
- 'Authorization': `Bearer ${key}`
302
+ 'x-api-key': key
302
303
  },
303
- body: JSON.stringify({ projectId })
304
+ body: JSON.stringify({ organization_id, project_id: projectId })
304
305
  });
305
306
 
306
- if (!res.ok) return null;
307
+ if (!res.ok) {
308
+ const txt = await res.text();
309
+ console.log(`[DEBUG] SAS Token API Failed: ${res.status} - ${txt}`);
310
+ return null;
311
+ }
307
312
 
308
- return await res.json();
309
- } catch {
313
+ const data = await res.json();
314
+ return data;
315
+ } catch (e) {
316
+ console.log(`[DEBUG] SAS Token API Error: ${e.message}`);
310
317
  return null;
311
318
  }
312
319
  }