@arcadialdev/arcality 3.0.1 → 3.0.3
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 +12 -6
- package/bin/arcality.mjs +9 -8
- package/package.json +2 -1
- package/playwright.config.js +18 -16
- package/public/ArcalityImagen.png +0 -0
- package/public/logo-arcadial-blanco.png +0 -0
- package/public/logo.png +0 -0
- package/scripts/arcality-logs.mjs +167 -93
- package/scripts/gen-and-run.mjs +1706 -1158
- package/scripts/init.mjs +55 -19
- package/scripts/rebrand-report.mjs +16 -2
- package/scripts/setup.mjs +120 -166
- package/src/KnowledgeService.ts +29 -20
- package/src/arcalityClient.mjs +33 -63
- package/src/configLoader.mjs +35 -39
- package/src/configManager.mjs +48 -39
- package/src/playwrightAssets.mjs +27 -0
- package/src/services/collectiveMemoryService.ts +37 -26
- package/tests/_helpers/ArcalityReporter.js +1240 -712
- package/tests/_helpers/agentic-runner.bundle.spec.js +213 -140
package/scripts/gen-and-run.mjs
CHANGED
|
@@ -1,139 +1,153 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// scripts/gen-and-run.mjs
|
|
3
|
-
// Main Arcality CLI loop — v3 Single Configuration Mode
|
|
4
|
-
// Reads from arcality.config instead of multi-config .env approach.
|
|
5
|
-
|
|
6
|
-
import 'dotenv/config';
|
|
7
|
-
import fs from "node:fs";
|
|
8
|
-
import path from "node:path";
|
|
9
|
-
import { createRequire } from 'node:module';
|
|
10
|
-
import { spawn
|
|
11
|
-
import
|
|
12
|
-
import
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
import
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
process.stdin.
|
|
43
|
-
process.stdin.
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
if (!
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
}
|
|
121
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// scripts/gen-and-run.mjs
|
|
3
|
+
// Main Arcality CLI loop — v3 Single Configuration Mode
|
|
4
|
+
// Reads from arcality.config instead of multi-config .env approach.
|
|
5
|
+
|
|
6
|
+
import 'dotenv/config';
|
|
7
|
+
import fs from "node:fs";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import { createRequire } from 'node:module';
|
|
10
|
+
import { spawn } from "node:child_process";
|
|
11
|
+
import { createInterface } from "node:readline";
|
|
12
|
+
import chalk from "chalk";
|
|
13
|
+
import { fileURLToPath, pathToFileURL } from 'url';
|
|
14
|
+
const require = createRequire(import.meta.url);
|
|
15
|
+
import figlet from "figlet";
|
|
16
|
+
import { intro, outro, select, text, spinner, note, isCancel, cancel } from '@clack/prompts';
|
|
17
|
+
import { load as loadYaml } from 'js-yaml';
|
|
18
|
+
import { loadGlobalConfig, getApiKey, getApiUrl, maskApiKey, setupProcessEnv } from '../src/configLoader.mjs';
|
|
19
|
+
import {
|
|
20
|
+
configExists,
|
|
21
|
+
loadProjectConfig,
|
|
22
|
+
validateConfig,
|
|
23
|
+
injectConfigToEnv,
|
|
24
|
+
getYamlOutputDir,
|
|
25
|
+
ensureYamlOutputDir,
|
|
26
|
+
isRealProjectId,
|
|
27
|
+
} from '../src/configManager.mjs';
|
|
28
|
+
|
|
29
|
+
// Load global config at startup (injects keys into process.env)
|
|
30
|
+
setupProcessEnv();
|
|
31
|
+
|
|
32
|
+
// ── WINDOWS FIX: Interceptar Ctrl+C antes que cmd.exe ──
|
|
33
|
+
// En Windows, `arcality` se ejecuta vía arcality.cmd (creado por npm para el campo bin).
|
|
34
|
+
// cmd.exe intercepta Ctrl+C y muestra "¿Desea terminar el trabajo por lotes (S/N)?".
|
|
35
|
+
// Al presionar S, mata el árbol de procesos antes de que nuestro finally pueda correr.
|
|
36
|
+
//
|
|
37
|
+
// La solución: setRawMode(true) hace que Node capture los bytes del teclado directamente,
|
|
38
|
+
// ANTES de que cmd.exe los procese. Al detectar Ctrl+C (byte 0x03) lo convertimos en
|
|
39
|
+
// un SIGINT de Node — cmd.exe nunca lo ve y nunca muestra el prompt.
|
|
40
|
+
if (process.stdin.isTTY && process.stdin.setRawMode) {
|
|
41
|
+
try {
|
|
42
|
+
process.stdin.setRawMode(true);
|
|
43
|
+
process.stdin.resume();
|
|
44
|
+
process.stdin.setEncoding('utf8');
|
|
45
|
+
process.stdin.on('data', (key) => {
|
|
46
|
+
if (key === '\u0003') { // Ctrl+C
|
|
47
|
+
process.emit('SIGINT');
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
} catch (e) {
|
|
51
|
+
// Si falla (ej. en CI sin TTY), continuar sin raw mode
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
56
|
+
const __dirname = path.dirname(__filename);
|
|
57
|
+
const PROJECT_ROOT = process.env.ARCALITY_ROOT || path.join(__dirname, "..");
|
|
58
|
+
const RUNNER_SOURCE_FILE = path.join(PROJECT_ROOT, 'tests', '_helpers', 'agentic-runner.spec.ts');
|
|
59
|
+
const RUNNER_BUNDLE_FILE = path.join(PROJECT_ROOT, 'tests', '_helpers', 'agentic-runner.bundle.spec.js');
|
|
60
|
+
|
|
61
|
+
const ORIGINAL_CWD = process.cwd();
|
|
62
|
+
const COLLECTION_DEFAULT_CONCURRENCY = 3;
|
|
63
|
+
const COLLECTION_MAX_CONCURRENCY = 5;
|
|
64
|
+
|
|
65
|
+
// ── Load arcality.config if present ──
|
|
66
|
+
let projectConfig = loadProjectConfig(ORIGINAL_CWD) || loadProjectConfig(PROJECT_ROOT);
|
|
67
|
+
if (projectConfig) {
|
|
68
|
+
if (!isRealProjectId(projectConfig.projectId)) {
|
|
69
|
+
console.error(chalk.red('\n[FATAL] Proyecto Arcality no conectado.'));
|
|
70
|
+
console.error(chalk.yellow('El arcality.config actual no tiene un Project ID real del backend.'));
|
|
71
|
+
console.error(chalk.gray('Ejecuta `arcality init` para registrar el proyecto en Arcality antes de correr misiones.\n'));
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
injectConfigToEnv(projectConfig);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function parseDotEnvFile(p) {
|
|
78
|
+
try {
|
|
79
|
+
const raw = fs.readFileSync(p, 'utf8');
|
|
80
|
+
const lines = raw.split(/\r?\n/);
|
|
81
|
+
const out = {};
|
|
82
|
+
for (const line of lines) {
|
|
83
|
+
const trimmed = line.trim();
|
|
84
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
85
|
+
const idx = trimmed.indexOf('=');
|
|
86
|
+
if (idx === -1) continue;
|
|
87
|
+
let k = trimmed.slice(0, idx).trim();
|
|
88
|
+
let v = trimmed.slice(idx + 1).trim();
|
|
89
|
+
if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) v = v.slice(1, -1);
|
|
90
|
+
out[k] = v;
|
|
91
|
+
}
|
|
92
|
+
return out;
|
|
93
|
+
} catch {
|
|
94
|
+
return {};
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function updateDotEnvFile(updates) {
|
|
99
|
+
const p = path.join(PROJECT_ROOT, '.env');
|
|
100
|
+
let content = '';
|
|
101
|
+
try {
|
|
102
|
+
content = fs.readFileSync(p, 'utf8');
|
|
103
|
+
} catch {
|
|
104
|
+
content = '';
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
let lines = content.split(/\r?\n/);
|
|
108
|
+
const keys = Object.keys(updates);
|
|
109
|
+
const updatedKeys = new Set();
|
|
110
|
+
|
|
111
|
+
lines = lines.map(line => {
|
|
112
|
+
const trimmed = line.trim();
|
|
113
|
+
if (!trimmed || trimmed.startsWith('#')) return line;
|
|
114
|
+
const idx = trimmed.indexOf('=');
|
|
115
|
+
if (idx === -1) return line;
|
|
116
|
+
const k = trimmed.slice(0, idx).trim();
|
|
117
|
+
if (keys.includes(k)) {
|
|
118
|
+
updatedKeys.add(k);
|
|
119
|
+
return `${k}=${updates[k]}`;
|
|
120
|
+
}
|
|
121
|
+
return line;
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
keys.forEach(k => {
|
|
125
|
+
if (!updatedKeys.has(k)) {
|
|
126
|
+
lines.push(`${k}=${updates[k]}`);
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
fs.writeFileSync(p, lines.join('\n'), 'utf8');
|
|
131
|
+
keys.forEach(k => { process.env[k] = updates[k]; });
|
|
132
|
+
}
|
|
133
|
+
|
|
122
134
|
function setupEnvironment() {
|
|
123
|
-
|
|
124
|
-
// Use config name from arcality.config or fallback
|
|
125
|
-
const configName = projectConfig?.project?.name || 'Default';
|
|
126
|
-
const baseUrl = projectConfig?.project?.baseUrl || process.env.BASE_URL || 'http://localhost';
|
|
127
|
-
|
|
128
|
-
const rootOutDir = path.join(ORIGINAL_CWD, '.arcality', 'out');
|
|
129
|
-
const configDir = path.join(rootOutDir, configName.replace(/[^a-zA-Z0-9_-]/g, '_'));
|
|
130
|
-
|
|
131
|
-
process.env.
|
|
132
|
-
process.env.
|
|
133
|
-
process.env.
|
|
134
|
-
process.env.
|
|
135
|
-
|
|
136
|
-
|
|
135
|
+
|
|
136
|
+
// Use config name from arcality.config or fallback
|
|
137
|
+
const configName = projectConfig?.project?.name || 'Default';
|
|
138
|
+
const baseUrl = projectConfig?.project?.baseUrl || process.env.BASE_URL || 'http://localhost';
|
|
139
|
+
|
|
140
|
+
const rootOutDir = path.join(ORIGINAL_CWD, '.arcality', 'out');
|
|
141
|
+
const configDir = path.join(rootOutDir, configName.replace(/[^a-zA-Z0-9_-]/g, '_'));
|
|
142
|
+
|
|
143
|
+
const runDomainDir = process.env.ARCALITY_RUN_DOMAIN_DIR || configDir;
|
|
144
|
+
process.env.DOMAIN_DIR = runDomainDir;
|
|
145
|
+
process.env.MISSIONS_DIR = process.env.ARCALITY_RUN_MISSIONS_DIR || path.join(configDir, 'missions');
|
|
146
|
+
process.env.CONTEXT_DIR = process.env.ARCALITY_RUN_CONTEXT_DIR || path.join(runDomainDir, 'context');
|
|
147
|
+
process.env.REPORTS_DIR = process.env.ARCALITY_RUN_REPORTS_DIR || path.join(runDomainDir, 'reports');
|
|
148
|
+
process.env.PLAYWRIGHT_OUTPUT_DIR = process.env.ARCALITY_RUN_PLAYWRIGHT_OUTPUT_DIR || path.join(runDomainDir, 'playwright-output');
|
|
149
|
+
|
|
150
|
+
[process.env.MISSIONS_DIR, process.env.CONTEXT_DIR, process.env.REPORTS_DIR, process.env.PLAYWRIGHT_OUTPUT_DIR].forEach(dir => {
|
|
137
151
|
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
138
152
|
});
|
|
139
153
|
ensureMissionTemplates(process.env.MISSIONS_DIR);
|
|
@@ -143,22 +157,22 @@ function setupEnvironment() {
|
|
|
143
157
|
const yamlOutputDir = ensureYamlOutputDir(projectConfig, ORIGINAL_CWD);
|
|
144
158
|
ensureMissionTemplates(yamlOutputDir);
|
|
145
159
|
}
|
|
146
|
-
|
|
147
|
-
// Framework detection from package.json (NO .git)
|
|
148
|
-
let techStack = 'Not detected';
|
|
149
|
-
try {
|
|
150
|
-
const pkg = JSON.parse(fs.readFileSync(path.join(PROJECT_ROOT, 'package.json'), 'utf8'));
|
|
151
|
-
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
152
|
-
if (deps['next']) techStack = 'Next.js 🚀';
|
|
153
|
-
else if (deps['vite']) techStack = 'Vite ⚡';
|
|
154
|
-
else if (deps['react-scripts']) techStack = 'Create React App ⚛️';
|
|
155
|
-
} catch { }
|
|
156
|
-
|
|
157
|
-
// Override with arcality.config detection if available
|
|
158
|
-
if (projectConfig?.project?.frameworkDetected) {
|
|
159
|
-
techStack = projectConfig.project.frameworkDetected;
|
|
160
|
-
}
|
|
161
|
-
|
|
160
|
+
|
|
161
|
+
// Framework detection from package.json (NO .git)
|
|
162
|
+
let techStack = 'Not detected';
|
|
163
|
+
try {
|
|
164
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(PROJECT_ROOT, 'package.json'), 'utf8'));
|
|
165
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
166
|
+
if (deps['next']) techStack = 'Next.js 🚀';
|
|
167
|
+
else if (deps['vite']) techStack = 'Vite ⚡';
|
|
168
|
+
else if (deps['react-scripts']) techStack = 'Create React App ⚛️';
|
|
169
|
+
} catch { }
|
|
170
|
+
|
|
171
|
+
// Override with arcality.config detection if available
|
|
172
|
+
if (projectConfig?.project?.frameworkDetected) {
|
|
173
|
+
techStack = projectConfig.project.frameworkDetected;
|
|
174
|
+
}
|
|
175
|
+
|
|
162
176
|
return { configName, baseUrl, techStack, configDir };
|
|
163
177
|
}
|
|
164
178
|
|
|
@@ -585,33 +599,42 @@ default_page_path: "/welcome"
|
|
|
585
599
|
return created;
|
|
586
600
|
}
|
|
587
601
|
|
|
588
|
-
async function waitForEnter(message = 'Presiona Enter para continuar...') {
|
|
602
|
+
async function waitForEnter(message = 'Presiona Enter para continuar...', options = {}) {
|
|
589
603
|
if (!process.stdin.isTTY) return;
|
|
590
604
|
|
|
591
605
|
console.log(chalk.gray(`\n ${message}`));
|
|
592
606
|
await new Promise(resolve => {
|
|
593
|
-
const
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
607
|
+
const restoreRawMode = options.restoreRawMode !== false;
|
|
608
|
+
const pauseOnResolve = options.pauseOnResolve === true;
|
|
609
|
+
if (process.stdin.isTTY && process.stdin.setRawMode) {
|
|
610
|
+
process.stdin.setRawMode(false);
|
|
611
|
+
}
|
|
597
612
|
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
613
|
+
const rl = createInterface({
|
|
614
|
+
input: process.stdin,
|
|
615
|
+
output: process.stdout
|
|
616
|
+
});
|
|
601
617
|
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
618
|
+
const cleanup = () => {
|
|
619
|
+
rl.close();
|
|
620
|
+
if (process.stdin.isTTY && process.stdin.setRawMode && restoreRawMode) {
|
|
621
|
+
process.stdin.setRawMode(true);
|
|
622
|
+
process.stdin.resume();
|
|
623
|
+
}
|
|
624
|
+
if (pauseOnResolve) {
|
|
625
|
+
process.stdin.pause();
|
|
607
626
|
}
|
|
608
627
|
};
|
|
609
628
|
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
629
|
+
rl.on('SIGINT', () => {
|
|
630
|
+
cleanup();
|
|
631
|
+
process.exit(0);
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
rl.question('', () => {
|
|
635
|
+
cleanup();
|
|
636
|
+
resolve();
|
|
637
|
+
});
|
|
615
638
|
});
|
|
616
639
|
}
|
|
617
640
|
|
|
@@ -646,15 +669,320 @@ function readLatestMissionLog(contextDir, knownFiles = new Set()) {
|
|
|
646
669
|
}
|
|
647
670
|
}
|
|
648
671
|
|
|
672
|
+
function readMissionResult(runDomainDir, contextDir, knownFiles = new Set()) {
|
|
673
|
+
const persistedResultPath = runDomainDir
|
|
674
|
+
? path.join(runDomainDir, 'mission-result.json')
|
|
675
|
+
: null;
|
|
676
|
+
|
|
677
|
+
if (persistedResultPath && fs.existsSync(persistedResultPath)) {
|
|
678
|
+
try {
|
|
679
|
+
return {
|
|
680
|
+
file: persistedResultPath,
|
|
681
|
+
data: JSON.parse(fs.readFileSync(persistedResultPath, 'utf8'))
|
|
682
|
+
};
|
|
683
|
+
} catch {
|
|
684
|
+
// Fallback below.
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
return readLatestMissionLog(contextDir, knownFiles);
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
function normalizeCollectionConcurrency(value, fallback = COLLECTION_DEFAULT_CONCURRENCY) {
|
|
692
|
+
const n = Number(value);
|
|
693
|
+
if (!Number.isFinite(n) || n < 1) return fallback;
|
|
694
|
+
return Math.min(COLLECTION_MAX_CONCURRENCY, Math.max(1, Math.floor(n)));
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
function createCollectionRunId() {
|
|
698
|
+
return new Date().toISOString()
|
|
699
|
+
.replace(/\.\d{3}Z$/, 'Z')
|
|
700
|
+
.replace(/[^0-9TZ]/g, '')
|
|
701
|
+
.replace('T', '-')
|
|
702
|
+
.replace('Z', '');
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
function formatPathForHtml(value) {
|
|
706
|
+
return String(value || '').split(path.sep).join('/');
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
function escapeHtml(value) {
|
|
710
|
+
return String(value ?? '')
|
|
711
|
+
.replace(/&/g, '&')
|
|
712
|
+
.replace(/</g, '<')
|
|
713
|
+
.replace(/>/g, '>')
|
|
714
|
+
.replace(/"/g, '"')
|
|
715
|
+
.replace(/'/g, ''');
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
function createUniqueMissionSlug(missionSpec, fileName, index, usedSlugs) {
|
|
719
|
+
const baseName = missionSpec.name || fileName || `mission-${index + 1}`;
|
|
720
|
+
const base = sanitizePathSegment(baseName) || `mission_${index + 1}`;
|
|
721
|
+
const prefix = String(index + 1).padStart(2, '0');
|
|
722
|
+
let candidate = `${prefix}-${base}`;
|
|
723
|
+
let suffix = 2;
|
|
724
|
+
|
|
725
|
+
while (usedSlugs.has(candidate)) {
|
|
726
|
+
candidate = `${prefix}-${base}-${suffix}`;
|
|
727
|
+
suffix += 1;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
usedSlugs.add(candidate);
|
|
731
|
+
return candidate;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
async function ensureRunnerBundle({ debug = false, spinnerRef = null, statusMessage = 'Compilando Arcality Runner...' } = {}) {
|
|
735
|
+
const sourceExists = fs.existsSync(RUNNER_SOURCE_FILE);
|
|
736
|
+
const bundleExists = fs.existsSync(RUNNER_BUNDLE_FILE);
|
|
737
|
+
|
|
738
|
+
if (!sourceExists) {
|
|
739
|
+
if (bundleExists) {
|
|
740
|
+
if (debug) {
|
|
741
|
+
console.log(chalk.gray('[DEBUG] Runner fuente no incluido en el paquete; usando bundle precompilado.'));
|
|
742
|
+
}
|
|
743
|
+
return true;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
console.warn(chalk.yellow('\nAdvertencia: No se encontro el runner fuente ni el bundle precompilado.'));
|
|
747
|
+
return false;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
try {
|
|
751
|
+
if (spinnerRef && !debug) {
|
|
752
|
+
spinnerRef.message(statusMessage);
|
|
753
|
+
} else if (debug) {
|
|
754
|
+
console.log(chalk.cyan(statusMessage));
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
await runNpmScript(['run', 'build:runner'], {
|
|
758
|
+
cwd: PROJECT_ROOT,
|
|
759
|
+
streamOutput: debug
|
|
760
|
+
});
|
|
761
|
+
return true;
|
|
762
|
+
} catch (err) {
|
|
763
|
+
console.warn(chalk.yellow(`\nAdvertencia: No se pudo compilar el runner: ${err.message}`));
|
|
764
|
+
return false;
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
async function buildRunnerForCollection(debug = false, spinnerRef = null) {
|
|
769
|
+
return ensureRunnerBundle({
|
|
770
|
+
debug,
|
|
771
|
+
spinnerRef,
|
|
772
|
+
statusMessage: debug
|
|
773
|
+
? '[RUN-ALL] Compilando Arcality Runner una sola vez para la coleccion...'
|
|
774
|
+
: 'Compilando Arcality Runner una sola vez para la coleccion...'
|
|
775
|
+
});
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
async function runWithConcurrency(items, concurrency, worker) {
|
|
779
|
+
const results = new Array(items.length);
|
|
780
|
+
let nextIndex = 0;
|
|
781
|
+
const workerCount = Math.min(Math.max(1, concurrency), items.length);
|
|
782
|
+
|
|
783
|
+
await Promise.all(Array.from({ length: workerCount }, async () => {
|
|
784
|
+
while (nextIndex < items.length) {
|
|
785
|
+
const currentIndex = nextIndex;
|
|
786
|
+
nextIndex += 1;
|
|
787
|
+
results[currentIndex] = await worker(items[currentIndex], currentIndex);
|
|
788
|
+
}
|
|
789
|
+
}));
|
|
790
|
+
|
|
791
|
+
return results;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
function writeCollectionArtifacts(runDir, summary, metadata) {
|
|
795
|
+
const summaryPath = path.join(runDir, 'summary.json');
|
|
796
|
+
const reportDir = path.join(runDir, 'collection-report');
|
|
797
|
+
const reportPath = path.join(reportDir, 'index.html');
|
|
798
|
+
|
|
799
|
+
if (!fs.existsSync(reportDir)) fs.mkdirSync(reportDir, { recursive: true });
|
|
800
|
+
|
|
801
|
+
const passed = summary.filter(item => item.success).length;
|
|
802
|
+
const failed = summary.length - passed;
|
|
803
|
+
const startedAt = metadata.startedAt || new Date().toISOString();
|
|
804
|
+
const finishedAt = new Date().toISOString();
|
|
805
|
+
const durationMs = summary.reduce((total, item) => total + (item.durationMs || 0), 0);
|
|
806
|
+
|
|
807
|
+
fs.writeFileSync(summaryPath, JSON.stringify({
|
|
808
|
+
runId: metadata.runId,
|
|
809
|
+
collection: metadata.collectionLabel,
|
|
810
|
+
concurrency: metadata.concurrency,
|
|
811
|
+
startedAt,
|
|
812
|
+
finishedAt,
|
|
813
|
+
total: summary.length,
|
|
814
|
+
passed,
|
|
815
|
+
failed,
|
|
816
|
+
missions: summary
|
|
817
|
+
}, null, 2));
|
|
818
|
+
|
|
819
|
+
const rows = summary.map(item => {
|
|
820
|
+
const reportExists = item.reportPath && fs.existsSync(item.reportPath);
|
|
821
|
+
const relativeReport = reportExists
|
|
822
|
+
? formatPathForHtml(path.relative(reportDir, item.reportPath))
|
|
823
|
+
: '';
|
|
824
|
+
const status = item.success ? 'Exitosa' : 'Error';
|
|
825
|
+
const tone = item.success ? 'passed' : 'failed';
|
|
826
|
+
const reportLink = reportExists
|
|
827
|
+
? `<a href="${escapeHtml(relativeReport)}">Abrir reporte</a>`
|
|
828
|
+
: '<span class="muted">Sin reporte</span>';
|
|
829
|
+
const reason = item.reason ? escapeHtml(item.reason) : '<span class="muted">-</span>';
|
|
830
|
+
|
|
831
|
+
return `
|
|
832
|
+
<tr>
|
|
833
|
+
<td><span class="status ${tone}">${status}</span></td>
|
|
834
|
+
<td>
|
|
835
|
+
<strong>${escapeHtml(item.name)}</strong>
|
|
836
|
+
<small>${escapeHtml(item.collection || 'General')}</small>
|
|
837
|
+
</td>
|
|
838
|
+
<td>${reason}</td>
|
|
839
|
+
<td>${item.durationMs ? Math.round(item.durationMs / 1000) + 's' : '-'}</td>
|
|
840
|
+
<td>${reportLink}</td>
|
|
841
|
+
</tr>
|
|
842
|
+
`;
|
|
843
|
+
}).join('');
|
|
844
|
+
|
|
845
|
+
const html = `<!DOCTYPE html>
|
|
846
|
+
<html lang="es">
|
|
847
|
+
<head>
|
|
848
|
+
<meta charset="UTF-8">
|
|
849
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
850
|
+
<title>Arcality Collection Report</title>
|
|
851
|
+
<style>
|
|
852
|
+
:root {
|
|
853
|
+
--bg: #07080d;
|
|
854
|
+
--surface: #10131b;
|
|
855
|
+
--line: #293042;
|
|
856
|
+
--text: #f7f4ff;
|
|
857
|
+
--muted: #a9afc0;
|
|
858
|
+
--ok: #36d399;
|
|
859
|
+
--bad: #ff5d7a;
|
|
860
|
+
--accent: #75c7ff;
|
|
861
|
+
}
|
|
862
|
+
* { box-sizing: border-box; }
|
|
863
|
+
body {
|
|
864
|
+
margin: 0;
|
|
865
|
+
min-height: 100vh;
|
|
866
|
+
background: var(--bg);
|
|
867
|
+
color: var(--text);
|
|
868
|
+
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
869
|
+
}
|
|
870
|
+
main { width: min(1180px, calc(100% - 48px)); margin: 0 auto; padding: 44px 0 64px; }
|
|
871
|
+
header { display: flex; justify-content: space-between; gap: 24px; align-items: flex-start; margin-bottom: 34px; }
|
|
872
|
+
h1 { margin: 0 0 10px; font-size: clamp(38px, 6vw, 72px); line-height: .95; letter-spacing: 0; }
|
|
873
|
+
p { margin: 0; color: var(--muted); font-size: 16px; }
|
|
874
|
+
.badge { border: 1px solid var(--line); border-radius: 8px; padding: 16px 18px; min-width: 250px; background: rgba(255,255,255,.02); }
|
|
875
|
+
.badge small { color: var(--muted); display: block; text-transform: uppercase; font-size: 11px; letter-spacing: .14em; margin-bottom: 8px; }
|
|
876
|
+
.badge strong { display: block; font-size: 22px; }
|
|
877
|
+
.grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); border: 1px solid var(--line); border-radius: 8px; overflow: hidden; margin-bottom: 28px; }
|
|
878
|
+
.metric { padding: 20px; background: var(--surface); border-right: 1px solid var(--line); }
|
|
879
|
+
.metric:last-child { border-right: 0; }
|
|
880
|
+
.metric b { display: block; font-size: 30px; margin-bottom: 8px; }
|
|
881
|
+
.metric span { color: var(--muted); text-transform: uppercase; font-size: 11px; letter-spacing: .14em; font-weight: 700; }
|
|
882
|
+
table { width: 100%; border-collapse: collapse; background: var(--surface); border: 1px solid var(--line); border-radius: 8px; overflow: hidden; }
|
|
883
|
+
th, td { text-align: left; padding: 16px; border-bottom: 1px solid var(--line); vertical-align: top; }
|
|
884
|
+
th { color: var(--muted); text-transform: uppercase; font-size: 11px; letter-spacing: .14em; }
|
|
885
|
+
tr:last-child td { border-bottom: 0; }
|
|
886
|
+
td small { display: block; color: var(--muted); margin-top: 4px; }
|
|
887
|
+
a { color: var(--accent); text-decoration: none; font-weight: 700; }
|
|
888
|
+
.status { display: inline-flex; border-radius: 999px; padding: 5px 9px; font-size: 11px; font-weight: 800; text-transform: uppercase; letter-spacing: .08em; }
|
|
889
|
+
.status.passed { color: var(--ok); background: rgba(54, 211, 153, .12); }
|
|
890
|
+
.status.failed { color: var(--bad); background: rgba(255, 93, 122, .12); }
|
|
891
|
+
.muted { color: var(--muted); }
|
|
892
|
+
@media (max-width: 760px) {
|
|
893
|
+
main { width: min(100% - 28px, 1180px); padding-top: 28px; }
|
|
894
|
+
header { display: block; }
|
|
895
|
+
.badge { margin-top: 22px; }
|
|
896
|
+
.grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
897
|
+
.metric:nth-child(2) { border-right: 0; }
|
|
898
|
+
table, thead, tbody, tr, th, td { display: block; }
|
|
899
|
+
thead { display: none; }
|
|
900
|
+
tr { border-bottom: 1px solid var(--line); }
|
|
901
|
+
td { border-bottom: 0; padding: 12px 14px; }
|
|
902
|
+
}
|
|
903
|
+
</style>
|
|
904
|
+
</head>
|
|
905
|
+
<body>
|
|
906
|
+
<main>
|
|
907
|
+
<header>
|
|
908
|
+
<div>
|
|
909
|
+
<h1>Reporte de coleccion</h1>
|
|
910
|
+
<p>${escapeHtml(metadata.collectionLabel)} ejecutada con hasta ${metadata.concurrency} misiones simultaneas.</p>
|
|
911
|
+
</div>
|
|
912
|
+
<div class="badge">
|
|
913
|
+
<small>Estado general</small>
|
|
914
|
+
<strong>${failed > 0 ? 'Ejecucion con errores' : 'Ejecucion exitosa'}</strong>
|
|
915
|
+
<p>${escapeHtml(startedAt)} - ${escapeHtml(finishedAt)}</p>
|
|
916
|
+
</div>
|
|
917
|
+
</header>
|
|
918
|
+
<section class="grid">
|
|
919
|
+
<div class="metric"><b>${summary.length}</b><span>Misiones</span></div>
|
|
920
|
+
<div class="metric"><b>${passed}</b><span>Exitosas</span></div>
|
|
921
|
+
<div class="metric"><b>${failed}</b><span>Errores</span></div>
|
|
922
|
+
<div class="metric"><b>${Math.round(durationMs / 1000)}s</b><span>Tiempo acumulado</span></div>
|
|
923
|
+
</section>
|
|
924
|
+
<table>
|
|
925
|
+
<thead>
|
|
926
|
+
<tr>
|
|
927
|
+
<th>Estado</th>
|
|
928
|
+
<th>Mision</th>
|
|
929
|
+
<th>Razon</th>
|
|
930
|
+
<th>Duracion</th>
|
|
931
|
+
<th>Evidencia</th>
|
|
932
|
+
</tr>
|
|
933
|
+
</thead>
|
|
934
|
+
<tbody>${rows}</tbody>
|
|
935
|
+
</table>
|
|
936
|
+
</main>
|
|
937
|
+
</body>
|
|
938
|
+
</html>`;
|
|
939
|
+
|
|
940
|
+
fs.writeFileSync(reportPath, html);
|
|
941
|
+
return { summaryPath, reportPath };
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
function openLocalFile(filePath) {
|
|
945
|
+
if (!filePath || !fs.existsSync(filePath)) return false;
|
|
946
|
+
|
|
947
|
+
try {
|
|
948
|
+
if (process.platform === 'win32') {
|
|
949
|
+
const child = spawn('cmd.exe', ['/d', '/s', '/c', 'start', '', filePath], {
|
|
950
|
+
detached: true,
|
|
951
|
+
stdio: 'ignore',
|
|
952
|
+
windowsHide: true
|
|
953
|
+
});
|
|
954
|
+
child.unref();
|
|
955
|
+
} else if (process.platform === 'darwin') {
|
|
956
|
+
const child = spawn('open', [filePath], { detached: true, stdio: 'ignore' });
|
|
957
|
+
child.unref();
|
|
958
|
+
} else {
|
|
959
|
+
const child = spawn('xdg-open', [filePath], { detached: true, stdio: 'ignore' });
|
|
960
|
+
child.unref();
|
|
961
|
+
}
|
|
962
|
+
return true;
|
|
963
|
+
} catch {
|
|
964
|
+
return false;
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
function openCollectionReport(reportPath) {
|
|
969
|
+
if (!reportPath || !fs.existsSync(reportPath) || process.env.CI === 'true') return;
|
|
970
|
+
if (!process.stdin.isTTY) return;
|
|
971
|
+
|
|
972
|
+
openLocalFile(reportPath);
|
|
973
|
+
}
|
|
974
|
+
|
|
649
975
|
async function runCollectionBatch({
|
|
650
976
|
missionsRootDir,
|
|
651
977
|
selectedCollection = null,
|
|
652
|
-
debug = false
|
|
978
|
+
debug = false,
|
|
979
|
+
concurrency = 1,
|
|
980
|
+
interactive = false
|
|
653
981
|
}) {
|
|
654
982
|
const collections = listMissionCollections(missionsRootDir);
|
|
655
983
|
if (collections.length === 0) {
|
|
656
984
|
note(chalk.yellow('No hay colecciones con misiones guardadas para ejecutar.'), 'Run All');
|
|
657
|
-
return;
|
|
985
|
+
return { summary: [], failed: 0 };
|
|
658
986
|
}
|
|
659
987
|
|
|
660
988
|
let targetCollections = collections;
|
|
@@ -664,7 +992,7 @@ async function runCollectionBatch({
|
|
|
664
992
|
|
|
665
993
|
if (targetCollections.length === 0) {
|
|
666
994
|
note(chalk.yellow('La coleccion solicitada no existe o no contiene misiones.'), 'Run All');
|
|
667
|
-
return;
|
|
995
|
+
return { summary: [], failed: 0 };
|
|
668
996
|
}
|
|
669
997
|
|
|
670
998
|
const missionsToRun = [];
|
|
@@ -681,38 +1009,114 @@ async function runCollectionBatch({
|
|
|
681
1009
|
|
|
682
1010
|
if (missionsToRun.length === 0) {
|
|
683
1011
|
note(chalk.yellow('No se encontraron misiones ejecutables en la seleccion actual.'), 'Run All');
|
|
684
|
-
return;
|
|
1012
|
+
return { summary: [], failed: 0 };
|
|
685
1013
|
}
|
|
686
1014
|
|
|
1015
|
+
const requestedConcurrency = normalizeCollectionConcurrency(
|
|
1016
|
+
concurrency,
|
|
1017
|
+
interactive ? COLLECTION_DEFAULT_CONCURRENCY : 1
|
|
1018
|
+
);
|
|
1019
|
+
const startedAt = new Date().toISOString();
|
|
1020
|
+
const runId = createCollectionRunId();
|
|
1021
|
+
const envInfo = setupEnvironment();
|
|
1022
|
+
const runDir = path.join(envInfo.configDir, 'runs', runId);
|
|
1023
|
+
const missionsRunDir = path.join(runDir, 'missions');
|
|
1024
|
+
const collectionLabel = selectedCollection && selectedCollection !== '__ALL__'
|
|
1025
|
+
? (targetCollections[0]?.label || selectedCollection)
|
|
1026
|
+
: `Todas las colecciones (${targetCollections.length})`;
|
|
1027
|
+
|
|
1028
|
+
if (!fs.existsSync(missionsRunDir)) fs.mkdirSync(missionsRunDir, { recursive: true });
|
|
1029
|
+
|
|
687
1030
|
const sBatch = spinner();
|
|
688
1031
|
if (!debug) {
|
|
689
|
-
sBatch.start(`
|
|
1032
|
+
sBatch.start(`Preparando ${missionsToRun.length} misiones de ${targetCollections.length} coleccion(es)...`);
|
|
690
1033
|
}
|
|
691
1034
|
|
|
692
1035
|
const summary = [];
|
|
1036
|
+
const runnableMissions = [];
|
|
1037
|
+
const usedSlugs = new Set();
|
|
1038
|
+
|
|
693
1039
|
for (let index = 0; index < missionsToRun.length; index++) {
|
|
694
1040
|
const missionEntry = missionsToRun[index];
|
|
695
1041
|
const rawMission = readYamlFile(missionEntry.fullPath);
|
|
696
1042
|
const missionSpec = normalizeMissionSpec(rawMission, missionEntry.fullPath);
|
|
697
1043
|
const collectionMeta = readCollectionMeta(missionEntry.collection.dir, missionEntry.collection.value);
|
|
698
1044
|
const discoverPath = getMissionStartPath(missionSpec, collectionMeta, '/');
|
|
699
|
-
const
|
|
700
|
-
const
|
|
1045
|
+
const slug = createUniqueMissionSlug(missionSpec, missionEntry.file, index, usedSlugs);
|
|
1046
|
+
const missionDir = path.join(missionsRunDir, slug);
|
|
1047
|
+
const contextDir = path.join(missionDir, 'context');
|
|
1048
|
+
const reportsDir = path.join(missionDir, 'reports');
|
|
1049
|
+
const playwrightOutputDir = path.join(missionDir, 'playwright-output');
|
|
701
1050
|
|
|
702
1051
|
if (!missionSpec.prompt) {
|
|
703
1052
|
summary.push({
|
|
1053
|
+
index,
|
|
704
1054
|
name: missionSpec.name || missionEntry.file,
|
|
705
1055
|
collection: missionEntry.collection.label,
|
|
706
1056
|
success: false,
|
|
707
|
-
reason: 'missing_prompt'
|
|
1057
|
+
reason: 'missing_prompt',
|
|
1058
|
+
durationMs: 0,
|
|
1059
|
+
reportPath: null,
|
|
1060
|
+
contextDir,
|
|
1061
|
+
reportsDir,
|
|
1062
|
+
playwrightOutputDir
|
|
708
1063
|
});
|
|
709
1064
|
continue;
|
|
710
1065
|
}
|
|
711
1066
|
|
|
1067
|
+
runnableMissions.push({
|
|
1068
|
+
index,
|
|
1069
|
+
missionEntry,
|
|
1070
|
+
missionSpec,
|
|
1071
|
+
collectionMeta,
|
|
1072
|
+
discoverPath,
|
|
1073
|
+
slug,
|
|
1074
|
+
missionDir,
|
|
1075
|
+
contextDir,
|
|
1076
|
+
reportsDir,
|
|
1077
|
+
playwrightOutputDir,
|
|
1078
|
+
name: missionSpec.name || missionEntry.file,
|
|
1079
|
+
collectionLabel: missionEntry.collection.label
|
|
1080
|
+
});
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
const runnerPrebuilt = runnableMissions.length > 0
|
|
1084
|
+
? await buildRunnerForCollection(debug, sBatch)
|
|
1085
|
+
: false;
|
|
1086
|
+
let effectiveConcurrency = Math.min(requestedConcurrency, Math.max(1, runnableMissions.length));
|
|
1087
|
+
|
|
1088
|
+
if (!runnerPrebuilt && effectiveConcurrency > 1) {
|
|
1089
|
+
effectiveConcurrency = 1;
|
|
1090
|
+
note(
|
|
1091
|
+
'No se pudo precompilar el runner antes del lote. Para evitar compilaciones simultaneas, esta coleccion se ejecutara de forma secuencial.',
|
|
1092
|
+
'Concurrencia ajustada'
|
|
1093
|
+
);
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
let completed = 0;
|
|
1097
|
+
const runnableResults = await runWithConcurrency(runnableMissions, effectiveConcurrency, async (mission) => {
|
|
1098
|
+
const startedMissionAt = Date.now();
|
|
1099
|
+
|
|
712
1100
|
if (!debug) {
|
|
713
|
-
sBatch.message(`[${
|
|
1101
|
+
sBatch.message(`[${completed}/${runnableMissions.length}] En curso hasta ${effectiveConcurrency}. Iniciando ${mission.name}`);
|
|
714
1102
|
} else {
|
|
715
|
-
console.log(chalk.cyan(`[RUN-ALL] ${index + 1}/${missionsToRun.length} -> ${
|
|
1103
|
+
console.log(chalk.cyan(`[RUN-ALL] ${mission.index + 1}/${missionsToRun.length} -> ${mission.name} [${mission.collectionLabel}]`));
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
const childEnv = {
|
|
1107
|
+
...process.env,
|
|
1108
|
+
ARCALITY_ROOT: PROJECT_ROOT,
|
|
1109
|
+
ARCALITY_BATCH_MODE: 'true',
|
|
1110
|
+
ARCALITY_RUN_DOMAIN_DIR: mission.missionDir,
|
|
1111
|
+
ARCALITY_RUN_CONTEXT_DIR: mission.contextDir,
|
|
1112
|
+
ARCALITY_RUN_REPORTS_DIR: mission.reportsDir,
|
|
1113
|
+
ARCALITY_RUN_PLAYWRIGHT_OUTPUT_DIR: mission.playwrightOutputDir,
|
|
1114
|
+
ARCALITY_COLLECTION_RUN_ID: runId,
|
|
1115
|
+
ARCALITY_COLLECTION_MISSION_SLUG: mission.slug
|
|
1116
|
+
};
|
|
1117
|
+
|
|
1118
|
+
if (runnerPrebuilt) {
|
|
1119
|
+
childEnv.ARCALITY_RUNNER_PREBUILT = 'true';
|
|
716
1120
|
}
|
|
717
1121
|
|
|
718
1122
|
try {
|
|
@@ -721,40 +1125,66 @@ async function runCollectionBatch({
|
|
|
721
1125
|
'--agent',
|
|
722
1126
|
'--batch',
|
|
723
1127
|
'--skip-save-prompt',
|
|
724
|
-
`--discover=${discoverPath}`,
|
|
725
|
-
missionSpec.prompt
|
|
1128
|
+
`--discover=${mission.discoverPath}`,
|
|
1129
|
+
mission.missionSpec.prompt
|
|
726
1130
|
], {
|
|
727
1131
|
cwd: ORIGINAL_CWD,
|
|
728
|
-
env:
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
ARCALITY_BATCH_MODE: 'true'
|
|
732
|
-
}
|
|
1132
|
+
env: childEnv,
|
|
1133
|
+
streamOutput: debug,
|
|
1134
|
+
lastRunLog: path.join(mission.missionDir, 'last-run.log')
|
|
733
1135
|
});
|
|
734
1136
|
|
|
735
|
-
const latestLog =
|
|
1137
|
+
const latestLog = readMissionResult(mission.missionDir, mission.contextDir);
|
|
736
1138
|
const success = latestLog?.data?.success === true;
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
1139
|
+
return {
|
|
1140
|
+
index: mission.index,
|
|
1141
|
+
name: mission.name,
|
|
1142
|
+
collection: mission.collectionLabel,
|
|
740
1143
|
success,
|
|
741
1144
|
reason: latestLog?.data?.fail_reason || null
|
|
742
|
-
|
|
1145
|
+
|| (!latestLog ? 'missing_mission_log' : null),
|
|
1146
|
+
durationMs: Date.now() - startedMissionAt,
|
|
1147
|
+
reportPath: path.join(mission.reportsDir, 'index.html'),
|
|
1148
|
+
contextDir: mission.contextDir,
|
|
1149
|
+
reportsDir: mission.reportsDir
|
|
1150
|
+
};
|
|
743
1151
|
} catch (error) {
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
1152
|
+
return {
|
|
1153
|
+
index: mission.index,
|
|
1154
|
+
name: mission.name,
|
|
1155
|
+
collection: mission.collectionLabel,
|
|
747
1156
|
success: false,
|
|
748
|
-
reason: error instanceof Error ? error.message : 'runner_error'
|
|
749
|
-
|
|
1157
|
+
reason: error instanceof Error ? error.message : 'runner_error',
|
|
1158
|
+
durationMs: Date.now() - startedMissionAt,
|
|
1159
|
+
reportPath: path.join(mission.reportsDir, 'index.html'),
|
|
1160
|
+
contextDir: mission.contextDir,
|
|
1161
|
+
reportsDir: mission.reportsDir
|
|
1162
|
+
};
|
|
1163
|
+
} finally {
|
|
1164
|
+
completed += 1;
|
|
1165
|
+
if (!debug) {
|
|
1166
|
+
sBatch.message(`[${completed}/${runnableMissions.length}] Misiones terminadas. Concurrencia ${effectiveConcurrency}.`);
|
|
1167
|
+
}
|
|
750
1168
|
}
|
|
751
|
-
}
|
|
1169
|
+
});
|
|
1170
|
+
|
|
1171
|
+
summary.push(...runnableResults.filter(Boolean));
|
|
1172
|
+
summary.sort((a, b) => a.index - b.index);
|
|
752
1173
|
|
|
753
1174
|
const passed = summary.filter(item => item.success).length;
|
|
754
1175
|
const failed = summary.length - passed;
|
|
1176
|
+
const artifacts = writeCollectionArtifacts(runDir, summary, {
|
|
1177
|
+
runId,
|
|
1178
|
+
collectionLabel,
|
|
1179
|
+
concurrency: effectiveConcurrency,
|
|
1180
|
+
startedAt
|
|
1181
|
+
});
|
|
755
1182
|
|
|
756
1183
|
if (!debug) {
|
|
757
|
-
|
|
1184
|
+
const finalMessage = failed > 0
|
|
1185
|
+
? chalk.yellow(`Coleccion ejecutada. Exitosas: ${passed} | Fallidas: ${failed}`)
|
|
1186
|
+
: chalk.green(`Coleccion ejecutada. Exitosas: ${passed} | Fallidas: ${failed}`);
|
|
1187
|
+
sBatch.stop(finalMessage);
|
|
758
1188
|
}
|
|
759
1189
|
|
|
760
1190
|
const lines = summary.map(item => {
|
|
@@ -763,288 +1193,311 @@ async function runCollectionBatch({
|
|
|
763
1193
|
return `- [${status}] ${item.collection} -> ${item.name}${reason}`;
|
|
764
1194
|
});
|
|
765
1195
|
note(lines.join('\n'), 'Resumen Run All');
|
|
1196
|
+
note(
|
|
1197
|
+
`Resumen JSON: ${artifacts.summaryPath}\nReporte HTML: ${artifacts.reportPath}\nNota: la ejecucion paralela abre varias sesiones del portal. Arcality no puede validar reglas internas de sesion, bloqueo de usuario o datos compartidos del sistema probado.`,
|
|
1198
|
+
'Evidencia de coleccion'
|
|
1199
|
+
);
|
|
1200
|
+
|
|
1201
|
+
openCollectionReport(artifacts.reportPath);
|
|
1202
|
+
return { summary, failed, runDir, reportPath: artifacts.reportPath };
|
|
766
1203
|
}
|
|
767
1204
|
|
|
768
1205
|
function showBanner() {
|
|
769
|
-
console.clear();
|
|
770
|
-
|
|
771
|
-
let version = 'unknown';
|
|
772
|
-
try {
|
|
773
|
-
const pkg = JSON.parse(fs.readFileSync(path.join(PROJECT_ROOT, 'package.json'), 'utf8'));
|
|
774
|
-
version = pkg.version || 'unknown';
|
|
775
|
-
} catch { }
|
|
776
|
-
|
|
777
|
-
const sentinelAscii = `
|
|
778
|
-
█████ ██████ ██████ █████ ██ ██ ████████ ██ ██
|
|
779
|
-
██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
|
|
780
|
-
███████ ██████ ██ ███████ ██ ██ ██ ████
|
|
781
|
-
██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
|
|
782
|
-
██ ██ ██ ██ ██████ ██ ██ ███████ ██ ██ ██ `;
|
|
783
|
-
|
|
784
|
-
intro(chalk.gray(sentinelAscii));
|
|
785
|
-
|
|
786
|
-
note(
|
|
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' +
|
|
790
|
-
chalk.gray('─'.repeat(50)) + '\n' +
|
|
791
|
-
chalk.gray('Versión del Sistema: ') + chalk.white(`v${version}`),
|
|
792
|
-
'Estado Central de Arcality'
|
|
793
|
-
);
|
|
794
|
-
|
|
795
|
-
const { configName, baseUrl, techStack, configDir } = setupEnvironment();
|
|
796
|
-
|
|
797
|
-
// Show arcality.config status instead of multi-config switching
|
|
798
|
-
const hasConfig = !!projectConfig;
|
|
799
|
-
const configStatus = hasConfig
|
|
800
|
-
? chalk.green('◉ Sincronización Establecida')
|
|
801
|
-
: chalk.red('⚠ Desconectado — Ejecuta `arcality init`');
|
|
802
|
-
|
|
803
|
-
const { key: apiKeyVal, source: apiKeySource } = getApiKey();
|
|
804
|
-
const apiKeyDisplay = apiKeyVal
|
|
805
|
-
? chalk.green(maskApiKey(apiKeyVal)) + chalk.gray(` [${apiKeySource}]`)
|
|
806
|
-
: chalk.red('NO VERIFICADO');
|
|
807
|
-
|
|
808
|
-
const infoLines = [
|
|
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),
|
|
812
|
-
];
|
|
813
|
-
|
|
814
|
-
if (hasConfig) {
|
|
815
|
-
infoLines.splice(1, 0,
|
|
816
|
-
chalk.gray('Proyecto: ') + chalk.white(configName) + chalk.gray(` [${baseUrl}]`),
|
|
817
|
-
chalk.gray('ID de Identidad: ') + chalk.white(projectConfig.projectId || 'N/A'),
|
|
818
|
-
);
|
|
819
|
-
} else {
|
|
820
|
-
infoLines.push(chalk.gray('Configuración: ') + configStatus);
|
|
821
|
-
}
|
|
822
|
-
|
|
823
|
-
note(infoLines.join('\n'), 'Telemetría del Objetivo');
|
|
824
|
-
}
|
|
825
|
-
|
|
826
|
-
const ARCALITY_BRAIN_URL = "https://api.anthropic.com/v1/messages";
|
|
827
|
-
const ARCALITY_MODEL = process.env.CLAUDE_MODEL || "claude-4-5-sonnet-latest";
|
|
828
|
-
const OUT_DIR = path.join(PROJECT_ROOT, "tests");
|
|
829
|
-
const LOGS_DIR = path.join(PROJECT_ROOT, "logs");
|
|
830
|
-
|
|
831
|
-
async function arcalityChat(messages) {
|
|
832
|
-
const internalApiUrl = getApiUrl();
|
|
833
|
-
const isProxyMode = !!internalApiUrl;
|
|
834
|
-
|
|
835
|
-
if (!process.env.ANTHROPIC_API_KEY && !isProxyMode) {
|
|
836
|
-
throw new Error("Missing ANTHROPIC_API_KEY in .env");
|
|
837
|
-
}
|
|
838
|
-
|
|
839
|
-
const systemMessage = messages.find(m => m.role === 'system');
|
|
840
|
-
const userMessages = messages.filter(m => m.role !== 'system');
|
|
841
|
-
|
|
842
|
-
try {
|
|
843
|
-
const endpointUrl = isProxyMode
|
|
844
|
-
? `${internalApiUrl}/api/v1/ai/proxy`
|
|
845
|
-
: ARCALITY_BRAIN_URL;
|
|
846
|
-
|
|
847
|
-
const headers = {
|
|
848
|
-
"Content-Type": "application/json"
|
|
849
|
-
};
|
|
850
|
-
|
|
851
|
-
if (isProxyMode) {
|
|
852
|
-
headers["x-api-key"] = process.env.ARCALITY_API_KEY || "";
|
|
853
|
-
const missionId = process.env.ARCALITY_MISSION_ID || '';
|
|
854
|
-
if (missionId) headers["x-mission-id"] = missionId;
|
|
855
|
-
} else {
|
|
856
|
-
headers["x-api-key"] = process.env.ANTHROPIC_API_KEY;
|
|
857
|
-
headers["anthropic-version"] = "2023-06-01";
|
|
858
|
-
}
|
|
859
|
-
|
|
860
|
-
const res = await fetch(endpointUrl, {
|
|
861
|
-
method: "POST",
|
|
862
|
-
headers,
|
|
863
|
-
body: JSON.stringify({
|
|
864
|
-
model: ARCALITY_MODEL,
|
|
865
|
-
system: systemMessage ? systemMessage.content : undefined,
|
|
866
|
-
messages: userMessages,
|
|
867
|
-
max_tokens: 4000,
|
|
868
|
-
temperature: 0.3
|
|
869
|
-
})
|
|
870
|
-
});
|
|
871
|
-
|
|
872
|
-
if (!res.ok) {
|
|
873
|
-
const txt = await res.text();
|
|
874
|
-
throw new Error(`Arcality Brain Error ${res.status}: ${txt}`);
|
|
875
|
-
}
|
|
876
|
-
|
|
877
|
-
const data = await res.json();
|
|
878
|
-
const content = data.content?.[0]?.text || "";
|
|
879
|
-
|
|
880
|
-
try {
|
|
881
|
-
if (!fs.existsSync(LOGS_DIR)) fs.mkdirSync(LOGS_DIR, { recursive: true });
|
|
882
|
-
fs.writeFileSync(path.join(LOGS_DIR, 'last-ai-response.txt'), content);
|
|
883
|
-
} catch (e) { /* ignore */ }
|
|
884
|
-
|
|
885
|
-
return content;
|
|
886
|
-
} catch (err) {
|
|
887
|
-
throw new Error(`Error communicating with Arcality brain: ${err.message}`);
|
|
888
|
-
}
|
|
889
|
-
}
|
|
890
|
-
|
|
891
|
-
function extractBlocks(text) {
|
|
892
|
-
const cleaned = text
|
|
893
|
-
.replace(/```[a-zA-Z]*\n?/g, "")
|
|
894
|
-
.replace(/```/g, "")
|
|
895
|
-
.trim();
|
|
896
|
-
|
|
897
|
-
const fnMatch = cleaned.match(/^FILENAME:\s*(.+)$/m);
|
|
898
|
-
const contentIdx = cleaned.indexOf("CONTENT:");
|
|
899
|
-
|
|
900
|
-
if (!fnMatch || contentIdx === -1) {
|
|
901
|
-
throw new Error(
|
|
902
|
-
`Invalid format. Expected:\nFILENAME: <file.spec.ts>\nCONTENT:\n<code>\n\nResponse:\n${cleaned}`
|
|
903
|
-
);
|
|
904
|
-
}
|
|
905
|
-
|
|
906
|
-
const filename = fnMatch[1].trim();
|
|
907
|
-
const content = cleaned.slice(contentIdx + "CONTENT:".length).trim();
|
|
908
|
-
|
|
909
|
-
return { filename, content, cleaned };
|
|
910
|
-
}
|
|
911
|
-
|
|
912
|
-
function validateSpec(ts) {
|
|
913
|
-
const errors = [];
|
|
914
|
-
if (!/test\s*\(.*async\s*\(\s*\{\s*page\s*\}\s*\)\s*=>/s.test(ts)) {
|
|
915
|
-
errors.push('Missing fixture: test("...", async ({ page }) => { ... })');
|
|
916
|
-
}
|
|
917
|
-
if (/from\s+['"]playwright['"]/.test(ts)) {
|
|
918
|
-
errors.push("Forbidden to import from core. Use only the Arcality SDK.");
|
|
919
|
-
}
|
|
920
|
-
if (!ts.includes("import { test, expect } from '@playwright/test'")) {
|
|
921
|
-
errors.push("Must import exactly the Arcality engine.");
|
|
922
|
-
}
|
|
923
|
-
return errors;
|
|
924
|
-
}
|
|
925
|
-
|
|
926
|
-
function sanitizeFilename(name, fallback) {
|
|
927
|
-
if (typeof name !== "string") return fallback;
|
|
928
|
-
const safe = name.trim().replace(/[<>:"/\\|?*\u0000-\u001F]/g, "_");
|
|
929
|
-
if (!safe.endsWith(".spec.ts")) return fallback;
|
|
930
|
-
return safe;
|
|
931
|
-
}
|
|
932
|
-
|
|
933
|
-
function run(cmd, args, options = {}) {
|
|
934
|
-
const isDebug = process.argv.includes('--debug');
|
|
935
|
-
const cwd = options.cwd || process.cwd();
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
const
|
|
941
|
-
const
|
|
942
|
-
const
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
if (isDebug) console.log(chalk.gray(
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
//
|
|
950
|
-
//
|
|
951
|
-
//
|
|
952
|
-
//
|
|
953
|
-
//
|
|
954
|
-
//
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
if (p.
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
outputBuffer += data.toString();
|
|
991
|
-
});
|
|
992
|
-
}
|
|
993
|
-
|
|
994
|
-
p.on('exit', (code, signal) => {
|
|
995
|
-
const lastRunLog = path.join(LOGS_DIR, 'last-run.log');
|
|
996
|
-
try {
|
|
997
|
-
|
|
998
|
-
fs.
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
// code===
|
|
1003
|
-
// code===
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
}
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1206
|
+
console.clear();
|
|
1207
|
+
|
|
1208
|
+
let version = 'unknown';
|
|
1209
|
+
try {
|
|
1210
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(PROJECT_ROOT, 'package.json'), 'utf8'));
|
|
1211
|
+
version = pkg.version || 'unknown';
|
|
1212
|
+
} catch { }
|
|
1213
|
+
|
|
1214
|
+
const sentinelAscii = `
|
|
1215
|
+
█████ ██████ ██████ █████ ██ ██ ████████ ██ ██
|
|
1216
|
+
██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
|
|
1217
|
+
███████ ██████ ██ ███████ ██ ██ ██ ████
|
|
1218
|
+
██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
|
|
1219
|
+
██ ██ ██ ██ ██████ ██ ██ ███████ ██ ██ ██ `;
|
|
1220
|
+
|
|
1221
|
+
intro(chalk.gray(sentinelAscii));
|
|
1222
|
+
|
|
1223
|
+
note(
|
|
1224
|
+
chalk.gray('◉ Motor de Observación: ') + chalk.bold.white('EN LÍNEA') + '\n' +
|
|
1225
|
+
chalk.gray('◉ Módulo de Recuperación de UI: ') + chalk.bold.white('ACTIVO') + '\n' +
|
|
1226
|
+
chalk.gray('◉ Lógica Forense: ') + chalk.bold.white('ACTIVA') + '\n' +
|
|
1227
|
+
chalk.gray('─'.repeat(50)) + '\n' +
|
|
1228
|
+
chalk.gray('Versión del Sistema: ') + chalk.white(`v${version}`),
|
|
1229
|
+
'Estado Central de Arcality'
|
|
1230
|
+
);
|
|
1231
|
+
|
|
1232
|
+
const { configName, baseUrl, techStack, configDir } = setupEnvironment();
|
|
1233
|
+
|
|
1234
|
+
// Show arcality.config status instead of multi-config switching
|
|
1235
|
+
const hasConfig = !!projectConfig;
|
|
1236
|
+
const configStatus = hasConfig
|
|
1237
|
+
? chalk.green('◉ Sincronización Establecida')
|
|
1238
|
+
: chalk.red('⚠ Desconectado — Ejecuta `arcality init`');
|
|
1239
|
+
|
|
1240
|
+
const { key: apiKeyVal, source: apiKeySource } = getApiKey();
|
|
1241
|
+
const apiKeyDisplay = apiKeyVal
|
|
1242
|
+
? chalk.green(maskApiKey(apiKeyVal)) + chalk.gray(` [${apiKeySource}]`)
|
|
1243
|
+
: chalk.red('NO VERIFICADO');
|
|
1244
|
+
|
|
1245
|
+
const infoLines = [
|
|
1246
|
+
chalk.gray('Nodo Objetivo: ') + chalk.white(process.env.NODE_ENV ?? 'development'),
|
|
1247
|
+
chalk.gray('Token de Autenticación: ') + apiKeyDisplay,
|
|
1248
|
+
chalk.gray('Stack: ') + chalk.white(techStack),
|
|
1249
|
+
];
|
|
1250
|
+
|
|
1251
|
+
if (hasConfig) {
|
|
1252
|
+
infoLines.splice(1, 0,
|
|
1253
|
+
chalk.gray('Proyecto: ') + chalk.white(configName) + chalk.gray(` [${baseUrl}]`),
|
|
1254
|
+
chalk.gray('ID de Identidad: ') + chalk.white(projectConfig.projectId || 'N/A'),
|
|
1255
|
+
);
|
|
1256
|
+
} else {
|
|
1257
|
+
infoLines.push(chalk.gray('Configuración: ') + configStatus);
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
note(infoLines.join('\n'), 'Telemetría del Objetivo');
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
const ARCALITY_BRAIN_URL = "https://api.anthropic.com/v1/messages";
|
|
1264
|
+
const ARCALITY_MODEL = process.env.CLAUDE_MODEL || "claude-4-5-sonnet-latest";
|
|
1265
|
+
const OUT_DIR = path.join(PROJECT_ROOT, "tests");
|
|
1266
|
+
const LOGS_DIR = path.join(PROJECT_ROOT, "logs");
|
|
1267
|
+
|
|
1268
|
+
async function arcalityChat(messages) {
|
|
1269
|
+
const internalApiUrl = getApiUrl();
|
|
1270
|
+
const isProxyMode = !!internalApiUrl;
|
|
1271
|
+
|
|
1272
|
+
if (!process.env.ANTHROPIC_API_KEY && !isProxyMode) {
|
|
1273
|
+
throw new Error("Missing ANTHROPIC_API_KEY in .env");
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
const systemMessage = messages.find(m => m.role === 'system');
|
|
1277
|
+
const userMessages = messages.filter(m => m.role !== 'system');
|
|
1278
|
+
|
|
1279
|
+
try {
|
|
1280
|
+
const endpointUrl = isProxyMode
|
|
1281
|
+
? `${internalApiUrl}/api/v1/ai/proxy`
|
|
1282
|
+
: ARCALITY_BRAIN_URL;
|
|
1283
|
+
|
|
1284
|
+
const headers = {
|
|
1285
|
+
"Content-Type": "application/json"
|
|
1286
|
+
};
|
|
1287
|
+
|
|
1288
|
+
if (isProxyMode) {
|
|
1289
|
+
headers["x-api-key"] = process.env.ARCALITY_API_KEY || "";
|
|
1290
|
+
const missionId = process.env.ARCALITY_MISSION_ID || '';
|
|
1291
|
+
if (missionId) headers["x-mission-id"] = missionId;
|
|
1292
|
+
} else {
|
|
1293
|
+
headers["x-api-key"] = process.env.ANTHROPIC_API_KEY;
|
|
1294
|
+
headers["anthropic-version"] = "2023-06-01";
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
const res = await fetch(endpointUrl, {
|
|
1298
|
+
method: "POST",
|
|
1299
|
+
headers,
|
|
1300
|
+
body: JSON.stringify({
|
|
1301
|
+
model: ARCALITY_MODEL,
|
|
1302
|
+
system: systemMessage ? systemMessage.content : undefined,
|
|
1303
|
+
messages: userMessages,
|
|
1304
|
+
max_tokens: 4000,
|
|
1305
|
+
temperature: 0.3
|
|
1306
|
+
})
|
|
1307
|
+
});
|
|
1308
|
+
|
|
1309
|
+
if (!res.ok) {
|
|
1310
|
+
const txt = await res.text();
|
|
1311
|
+
throw new Error(`Arcality Brain Error ${res.status}: ${txt}`);
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
const data = await res.json();
|
|
1315
|
+
const content = data.content?.[0]?.text || "";
|
|
1316
|
+
|
|
1317
|
+
try {
|
|
1318
|
+
if (!fs.existsSync(LOGS_DIR)) fs.mkdirSync(LOGS_DIR, { recursive: true });
|
|
1319
|
+
fs.writeFileSync(path.join(LOGS_DIR, 'last-ai-response.txt'), content);
|
|
1320
|
+
} catch (e) { /* ignore */ }
|
|
1321
|
+
|
|
1322
|
+
return content;
|
|
1323
|
+
} catch (err) {
|
|
1324
|
+
throw new Error(`Error communicating with Arcality brain: ${err.message}`);
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
function extractBlocks(text) {
|
|
1329
|
+
const cleaned = text
|
|
1330
|
+
.replace(/```[a-zA-Z]*\n?/g, "")
|
|
1331
|
+
.replace(/```/g, "")
|
|
1332
|
+
.trim();
|
|
1333
|
+
|
|
1334
|
+
const fnMatch = cleaned.match(/^FILENAME:\s*(.+)$/m);
|
|
1335
|
+
const contentIdx = cleaned.indexOf("CONTENT:");
|
|
1336
|
+
|
|
1337
|
+
if (!fnMatch || contentIdx === -1) {
|
|
1338
|
+
throw new Error(
|
|
1339
|
+
`Invalid format. Expected:\nFILENAME: <file.spec.ts>\nCONTENT:\n<code>\n\nResponse:\n${cleaned}`
|
|
1340
|
+
);
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
const filename = fnMatch[1].trim();
|
|
1344
|
+
const content = cleaned.slice(contentIdx + "CONTENT:".length).trim();
|
|
1345
|
+
|
|
1346
|
+
return { filename, content, cleaned };
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
function validateSpec(ts) {
|
|
1350
|
+
const errors = [];
|
|
1351
|
+
if (!/test\s*\(.*async\s*\(\s*\{\s*page\s*\}\s*\)\s*=>/s.test(ts)) {
|
|
1352
|
+
errors.push('Missing fixture: test("...", async ({ page }) => { ... })');
|
|
1353
|
+
}
|
|
1354
|
+
if (/from\s+['"]playwright['"]/.test(ts)) {
|
|
1355
|
+
errors.push("Forbidden to import from core. Use only the Arcality SDK.");
|
|
1356
|
+
}
|
|
1357
|
+
if (!ts.includes("import { test, expect } from '@playwright/test'")) {
|
|
1358
|
+
errors.push("Must import exactly the Arcality engine.");
|
|
1359
|
+
}
|
|
1360
|
+
return errors;
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
function sanitizeFilename(name, fallback) {
|
|
1364
|
+
if (typeof name !== "string") return fallback;
|
|
1365
|
+
const safe = name.trim().replace(/[<>:"/\\|?*\u0000-\u001F]/g, "_");
|
|
1366
|
+
if (!safe.endsWith(".spec.ts")) return fallback;
|
|
1367
|
+
return safe;
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
function run(cmd, args, options = {}) {
|
|
1371
|
+
const isDebug = process.argv.includes('--debug');
|
|
1372
|
+
const cwd = options.cwd || process.cwd();
|
|
1373
|
+
const streamOutput = options.streamOutput !== false || isDebug;
|
|
1374
|
+
|
|
1375
|
+
return new Promise((resolve, reject) => {
|
|
1376
|
+
// NODE_PATH: let Node resolve modules from both the arcality package and user's project
|
|
1377
|
+
const appNodeModules = path.join(PROJECT_ROOT, 'node_modules');
|
|
1378
|
+
const userNodeModules = path.join(ORIGINAL_CWD, 'node_modules');
|
|
1379
|
+
const existingNodePath = process.env.NODE_PATH || '';
|
|
1380
|
+
const newNodePath = [appNodeModules, userNodeModules, existingNodePath].filter(Boolean).join(path.delimiter);
|
|
1381
|
+
|
|
1382
|
+
const env = { ...options.env || process.env, NODE_PATH: newNodePath };
|
|
1383
|
+
if (isDebug) console.log(chalk.gray(`\n[DEBUG] CWD: ${cwd}`));
|
|
1384
|
+
if (isDebug) console.log(chalk.gray(`[DEBUG] Running: ${cmd} ${args.join(' ')}`));
|
|
1385
|
+
|
|
1386
|
+
// Use standard cross-platform spawn. When shell is false, Node's child_process
|
|
1387
|
+
// properly wraps arguments with spaces in quotes natively for CreateProcessW on Windows.
|
|
1388
|
+
//
|
|
1389
|
+
// CRITICAL (Windows): stdin MUST be 'ignore' (not 'inherit') to prevent cmd.exe from
|
|
1390
|
+
// showing the "¿Desea terminar el trabajo por lotes (S/N)?" prompt on Ctrl+C.
|
|
1391
|
+
// When stdin is 'inherit', cmd.exe intercepts Ctrl+C at the console level and kills
|
|
1392
|
+
// the entire process tree before our SIGINT handler can run, bypassing the finally block.
|
|
1393
|
+
const p = spawn(cmd, args, {
|
|
1394
|
+
stdio: isDebug ? ['ignore', 'inherit', 'inherit'] : ['ignore', 'pipe', 'pipe'],
|
|
1395
|
+
shell: false,
|
|
1396
|
+
cwd,
|
|
1397
|
+
env,
|
|
1398
|
+
});
|
|
1399
|
+
|
|
1400
|
+
// Exponer el proceso hijo para que el cancelHandler pueda matarlo
|
|
1401
|
+
if (options.onSpawn) options.onSpawn(p);
|
|
1402
|
+
|
|
1403
|
+
let outputBuffer = '';
|
|
1404
|
+
const handleData = (data) => {
|
|
1405
|
+
const str = data.toString();
|
|
1406
|
+
outputBuffer += str;
|
|
1407
|
+
|
|
1408
|
+
if (str.includes('>>ARCALITY_STATUS>>')) {
|
|
1409
|
+
const lines = str.split('\n');
|
|
1410
|
+
for (const line of lines) {
|
|
1411
|
+
if (line.includes('>>ARCALITY_STATUS>>')) {
|
|
1412
|
+
const status = line.split('>>ARCALITY_STATUS>>')[1].trim();
|
|
1413
|
+
if (options.onStatus) options.onStatus(status);
|
|
1414
|
+
} else if (line.trim() && streamOutput) {
|
|
1415
|
+
process.stdout.write(line + '\n');
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
} else if (streamOutput) {
|
|
1419
|
+
process.stdout.write(data);
|
|
1420
|
+
}
|
|
1421
|
+
};
|
|
1422
|
+
|
|
1423
|
+
if (p.stdout) p.stdout.on('data', handleData);
|
|
1424
|
+
if (p.stderr) {
|
|
1425
|
+
p.stderr.on('data', (data) => {
|
|
1426
|
+
if (streamOutput) process.stderr.write(data);
|
|
1427
|
+
outputBuffer += data.toString();
|
|
1428
|
+
});
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
p.on('exit', (code, signal) => {
|
|
1432
|
+
const lastRunLog = options.lastRunLog || path.join(LOGS_DIR, 'last-run.log');
|
|
1433
|
+
try {
|
|
1434
|
+
const lastRunLogDir = path.dirname(lastRunLog);
|
|
1435
|
+
if (!fs.existsSync(lastRunLogDir)) fs.mkdirSync(lastRunLogDir, { recursive: true });
|
|
1436
|
+
fs.writeFileSync(lastRunLog, outputBuffer);
|
|
1437
|
+
} catch (e) { }
|
|
1438
|
+
|
|
1439
|
+
// code===0 → clean success
|
|
1440
|
+
// code===null + signal → killed by signal (Ctrl+C) — treat as non-fatal so finally runs
|
|
1441
|
+
// code===1 → test failed (Playwright returns 1 on test failure) — also non-fatal
|
|
1442
|
+
if (code === 0 || signal !== null || code === 1) resolve();
|
|
1443
|
+
else {
|
|
1444
|
+
if (isDebug) console.error(chalk.red(`\n[DEBUG] Process exited with code ${code}`));
|
|
1445
|
+
reject(new Error(`Process exited with code ${code}`));
|
|
1446
|
+
}
|
|
1447
|
+
});
|
|
1448
|
+
|
|
1449
|
+
p.on('error', (err) => {
|
|
1450
|
+
if (isDebug) console.error(chalk.red(`\n[DEBUG] Process failed to spawn: ${err.message}`));
|
|
1451
|
+
reject(err);
|
|
1452
|
+
});
|
|
1453
|
+
});
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
function runNpmScript(args, options = {}) {
|
|
1457
|
+
if (process.platform === 'win32') {
|
|
1458
|
+
return run('cmd.exe', ['/d', '/s', '/c', 'npm.cmd', ...args], options);
|
|
1459
|
+
}
|
|
1460
|
+
return run('npm', args, options);
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
/**
|
|
1464
|
+
* Ping the project to register activity.
|
|
1465
|
+
* Uses internal API URL — not user-configurable.
|
|
1466
|
+
*/
|
|
1467
|
+
async function pingProject(projectId, apiKey) {
|
|
1468
|
+
if (!projectId || !apiKey) return;
|
|
1469
|
+
try {
|
|
1470
|
+
await fetch(`${getApiUrl()}/api/v1/projects/${projectId}/ping`, {
|
|
1471
|
+
method: 'PATCH',
|
|
1472
|
+
headers: { 'x-api-key': apiKey },
|
|
1473
|
+
});
|
|
1474
|
+
} catch { /* silent */ }
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
async function main() {
|
|
1478
|
+
process.chdir(PROJECT_ROOT);
|
|
1479
|
+
|
|
1480
|
+
// Backward compat: initialize .env if ACTIVE_CONFIG missing (legacy)
|
|
1481
|
+
let initialEnv = parseDotEnvFile(path.join(PROJECT_ROOT, '.env'));
|
|
1482
|
+
const envProjectId = initialEnv.ARCALITY_PROJECT_ID || process.env.ARCALITY_PROJECT_ID;
|
|
1483
|
+
if (envProjectId && !isRealProjectId(envProjectId)) {
|
|
1484
|
+
console.error(chalk.red('\n[FATAL] ARCALITY_PROJECT_ID invalido.'));
|
|
1485
|
+
console.error(chalk.yellow('La herramienta no acepta Project IDs locales o mock.'));
|
|
1486
|
+
console.error(chalk.gray('Ejecuta `arcality init` para registrar el proyecto en Arcality.\n'));
|
|
1487
|
+
process.exit(1);
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
if (!projectConfig && !initialEnv.ACTIVE_CONFIG) {
|
|
1491
|
+
updateDotEnvFile({
|
|
1492
|
+
ACTIVE_CONFIG: 'Default',
|
|
1493
|
+
SAVED_CONFIGS: 'Default',
|
|
1494
|
+
Default_URL: initialEnv.BASE_URL || '',
|
|
1495
|
+
Default_USER: initialEnv.LOGIN_USER || '',
|
|
1496
|
+
Default_PASS: initialEnv.LOGIN_PASSWORD || ''
|
|
1497
|
+
});
|
|
1498
|
+
initialEnv = parseDotEnvFile(path.join(PROJECT_ROOT, '.env'));
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1048
1501
|
const argv = process.argv.slice(2);
|
|
1049
1502
|
let initialDiscoverPath = null;
|
|
1050
1503
|
let initialSmartMode = false;
|
|
@@ -1054,6 +1507,7 @@ async function main() {
|
|
|
1054
1507
|
let initialSkipSavePrompt = process.env.ARCALITY_SKIP_SAVE_PROMPT === 'true';
|
|
1055
1508
|
let initialBatchMode = process.env.ARCALITY_BATCH_MODE === 'true';
|
|
1056
1509
|
let initialCollectionPath = null;
|
|
1510
|
+
let initialCollectionConcurrency = null;
|
|
1057
1511
|
|
|
1058
1512
|
const dIndex = argv.findIndex(a => a === '--discover' || a.startsWith('--discover='));
|
|
1059
1513
|
if (dIndex !== -1) {
|
|
@@ -1084,16 +1538,26 @@ async function main() {
|
|
|
1084
1538
|
}
|
|
1085
1539
|
argv.splice(collectionIndex, 1);
|
|
1086
1540
|
}
|
|
1541
|
+
const concurrencyIndex = argv.findIndex(a => a === '--concurrency' || a.startsWith('--concurrency=') || a === '--parallel' || a.startsWith('--parallel='));
|
|
1542
|
+
if (concurrencyIndex !== -1) {
|
|
1543
|
+
const token = argv[concurrencyIndex];
|
|
1544
|
+
if (token.includes('=')) initialCollectionConcurrency = normalizeCollectionConcurrency(token.split('=')[1], 1);
|
|
1545
|
+
else if (argv[concurrencyIndex + 1]) {
|
|
1546
|
+
initialCollectionConcurrency = normalizeCollectionConcurrency(argv[concurrencyIndex + 1], 1);
|
|
1547
|
+
argv.splice(concurrencyIndex + 1, 1);
|
|
1548
|
+
}
|
|
1549
|
+
argv.splice(concurrencyIndex, 1);
|
|
1550
|
+
}
|
|
1087
1551
|
const debugIndex = argv.findIndex(a => a === '--debug');
|
|
1088
1552
|
if (debugIndex !== -1) { argv.splice(debugIndex, 1); } // Handled by run() naturally now
|
|
1089
|
-
|
|
1090
|
-
const promptArg = argv.join(" ").trim();
|
|
1091
|
-
let firstRun = true;
|
|
1092
|
-
let globalReportProcess = null;
|
|
1093
|
-
|
|
1094
|
-
setupEnvironment();
|
|
1095
|
-
|
|
1096
|
-
while (true) {
|
|
1553
|
+
|
|
1554
|
+
const promptArg = argv.join(" ").trim();
|
|
1555
|
+
let firstRun = true;
|
|
1556
|
+
let globalReportProcess = null;
|
|
1557
|
+
|
|
1558
|
+
setupEnvironment();
|
|
1559
|
+
|
|
1560
|
+
while (true) {
|
|
1097
1561
|
let prompt = firstRun ? promptArg : "";
|
|
1098
1562
|
let agentMode = firstRun ? initialAgentMode : false;
|
|
1099
1563
|
let smartMode = firstRun ? initialSmartMode : false;
|
|
@@ -1101,6 +1565,7 @@ async function main() {
|
|
|
1101
1565
|
let runAllMode = firstRun ? initialRunAllMode : false;
|
|
1102
1566
|
let templateMode = firstRun ? initialTemplateMode : false;
|
|
1103
1567
|
let collectionPath = firstRun ? initialCollectionPath : null;
|
|
1568
|
+
let collectionConcurrency = firstRun ? initialCollectionConcurrency : null;
|
|
1104
1569
|
let skipSavePrompt = firstRun ? initialSkipSavePrompt : false;
|
|
1105
1570
|
let batchMode = firstRun ? initialBatchMode : false;
|
|
1106
1571
|
|
|
@@ -1108,31 +1573,31 @@ async function main() {
|
|
|
1108
1573
|
if (agentMode) skipMenu = true; // Hard-lock: No menu in agent mode
|
|
1109
1574
|
|
|
1110
1575
|
firstRun = false;
|
|
1111
|
-
|
|
1576
|
+
|
|
1112
1577
|
if (!skipMenu) {
|
|
1113
1578
|
showBanner();
|
|
1114
1579
|
try {
|
|
1115
|
-
// ── Build menu options (single config mode) ──
|
|
1116
|
-
const missionsDir = process.env.MISSIONS_DIR;
|
|
1580
|
+
// ── Build menu options (single config mode) ──
|
|
1581
|
+
const missionsDir = process.env.MISSIONS_DIR;
|
|
1117
1582
|
const savedMissionCollections = listMissionCollections(missionsDir);
|
|
1118
|
-
|
|
1119
|
-
// Check for YAML files in the yaml output dir from arcality.config
|
|
1583
|
+
|
|
1584
|
+
// Check for YAML files in the yaml output dir from arcality.config
|
|
1120
1585
|
let projectMissionCollections = [];
|
|
1121
|
-
if (projectConfig) {
|
|
1122
|
-
const yamlDir = getYamlOutputDir(projectConfig, ORIGINAL_CWD);
|
|
1123
|
-
if (fs.existsSync(yamlDir)) {
|
|
1586
|
+
if (projectConfig) {
|
|
1587
|
+
const yamlDir = getYamlOutputDir(projectConfig, ORIGINAL_CWD);
|
|
1588
|
+
if (fs.existsSync(yamlDir)) {
|
|
1124
1589
|
projectMissionCollections = listMissionCollections(yamlDir);
|
|
1125
|
-
}
|
|
1126
|
-
}
|
|
1127
|
-
|
|
1128
|
-
// Root-level YAML templates
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
// Root-level YAML templates
|
|
1129
1594
|
const rootMissionFiles = fs.existsSync(ORIGINAL_CWD)
|
|
1130
1595
|
? fs.readdirSync(ORIGINAL_CWD).filter(f => isYamlFile(f))
|
|
1131
1596
|
: [];
|
|
1132
1597
|
const hasSavedMissionSources = savedMissionCollections.length || projectMissionCollections.length || rootMissionFiles.length;
|
|
1133
1598
|
const yamlOutputCollections = projectMissionCollections;
|
|
1134
1599
|
const rootYamlFiles = rootMissionFiles;
|
|
1135
|
-
|
|
1600
|
+
|
|
1136
1601
|
const options = [
|
|
1137
1602
|
{ label: '◉ Desplegar Arcality (Prompt)', value: 'agent' },
|
|
1138
1603
|
...(savedMissionCollections.length ? [{ label: '▶ Ejecutar colección completa', value: 'run_collection' }] : []),
|
|
@@ -1143,7 +1608,7 @@ async function main() {
|
|
|
1143
1608
|
{ label: '◎ Recalibrar Sistema (init)', value: 'reconfigure' },
|
|
1144
1609
|
{ label: '✕ Terminar Proceso', value: 'exit' }
|
|
1145
1610
|
];
|
|
1146
|
-
|
|
1611
|
+
|
|
1147
1612
|
const menuOptions = options
|
|
1148
1613
|
.filter(option => !['launch_saved', 'reuse_yaml', 'yaml_list'].includes(option.value))
|
|
1149
1614
|
.map(option => option.value === 'generate_templates'
|
|
@@ -1160,15 +1625,15 @@ async function main() {
|
|
|
1160
1625
|
}
|
|
1161
1626
|
|
|
1162
1627
|
const action = await select({
|
|
1163
|
-
message: '¿Qué te gustaría hacer?',
|
|
1628
|
+
message: '¿Qué te gustaría hacer?',
|
|
1164
1629
|
options: menuOptions
|
|
1165
1630
|
});
|
|
1166
|
-
|
|
1167
|
-
if (isCancel(action) || action === 'exit') {
|
|
1168
|
-
outro(chalk.cyan('¡Hasta luego!'));
|
|
1169
|
-
break;
|
|
1170
|
-
}
|
|
1171
|
-
|
|
1631
|
+
|
|
1632
|
+
if (isCancel(action) || action === 'exit') {
|
|
1633
|
+
outro(chalk.cyan('¡Hasta luego!'));
|
|
1634
|
+
break;
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1172
1637
|
if (action === 'agent') {
|
|
1173
1638
|
agentMode = true;
|
|
1174
1639
|
} else if (action === 'run_collection') {
|
|
@@ -1204,33 +1669,33 @@ async function main() {
|
|
|
1204
1669
|
await waitForEnter('Presiona Enter para volver al menu...');
|
|
1205
1670
|
continue;
|
|
1206
1671
|
} else if (action === 'reconfigure') {
|
|
1207
|
-
// Launch arcality init in the user's project directory
|
|
1208
|
-
const initScript = path.join(PROJECT_ROOT, 'scripts', 'init.mjs');
|
|
1209
|
-
try {
|
|
1210
|
-
await new Promise((resolve, reject) => {
|
|
1211
|
-
const child = spawn('node', [initScript], {
|
|
1212
|
-
stdio: 'inherit',
|
|
1213
|
-
cwd: ORIGINAL_CWD,
|
|
1214
|
-
env: { ...process.env, ARCALITY_ROOT: PROJECT_ROOT }
|
|
1215
|
-
});
|
|
1216
|
-
child.on('exit', code => code === 0 ? resolve() : reject(new Error(`Init exited with ${code}`)));
|
|
1217
|
-
child.on('error', reject);
|
|
1218
|
-
});
|
|
1219
|
-
|
|
1220
|
-
// ── REFRESH CONFIG ──
|
|
1221
|
-
// Recargar el archivo que init.mjs acaba de escribir
|
|
1222
|
-
projectConfig = loadProjectConfig(ORIGINAL_CWD) || loadProjectConfig(PROJECT_ROOT);
|
|
1223
|
-
if (projectConfig) {
|
|
1224
|
-
injectConfigToEnv(projectConfig);
|
|
1225
|
-
}
|
|
1226
|
-
setupEnvironment();
|
|
1227
|
-
|
|
1228
|
-
} catch (e) {
|
|
1229
|
-
// Mostrar el error claramente antes de que console.clear() lo borre
|
|
1230
|
-
console.log(chalk.red(`\n❌ Reconfigure failed: ${e.message}`));
|
|
1231
|
-
await new Promise(r => setTimeout(r, 3000));
|
|
1232
|
-
}
|
|
1233
|
-
continue;
|
|
1672
|
+
// Launch arcality init in the user's project directory
|
|
1673
|
+
const initScript = path.join(PROJECT_ROOT, 'scripts', 'init.mjs');
|
|
1674
|
+
try {
|
|
1675
|
+
await new Promise((resolve, reject) => {
|
|
1676
|
+
const child = spawn('node', [initScript], {
|
|
1677
|
+
stdio: 'inherit',
|
|
1678
|
+
cwd: ORIGINAL_CWD,
|
|
1679
|
+
env: { ...process.env, ARCALITY_ROOT: PROJECT_ROOT }
|
|
1680
|
+
});
|
|
1681
|
+
child.on('exit', code => code === 0 ? resolve() : reject(new Error(`Init exited with ${code}`)));
|
|
1682
|
+
child.on('error', reject);
|
|
1683
|
+
});
|
|
1684
|
+
|
|
1685
|
+
// ── REFRESH CONFIG ──
|
|
1686
|
+
// Recargar el archivo que init.mjs acaba de escribir
|
|
1687
|
+
projectConfig = loadProjectConfig(ORIGINAL_CWD) || loadProjectConfig(PROJECT_ROOT);
|
|
1688
|
+
if (projectConfig) {
|
|
1689
|
+
injectConfigToEnv(projectConfig);
|
|
1690
|
+
}
|
|
1691
|
+
setupEnvironment();
|
|
1692
|
+
|
|
1693
|
+
} catch (e) {
|
|
1694
|
+
// Mostrar el error claramente antes de que console.clear() lo borre
|
|
1695
|
+
console.log(chalk.red(`\n❌ Reconfigure failed: ${e.message}`));
|
|
1696
|
+
await new Promise(r => setTimeout(r, 3000));
|
|
1697
|
+
}
|
|
1698
|
+
continue;
|
|
1234
1699
|
} else if (action === 'launch_saved') {
|
|
1235
1700
|
const selectedMission = await selectMissionYaml(
|
|
1236
1701
|
missionsDir,
|
|
@@ -1262,8 +1727,8 @@ async function main() {
|
|
|
1262
1727
|
const sel = await select({
|
|
1263
1728
|
message: 'Elige el archivo YAML:',
|
|
1264
1729
|
options: rootYamlFiles.map(f => ({ label: f, value: f }))
|
|
1265
|
-
});
|
|
1266
|
-
if (isCancel(sel)) continue;
|
|
1730
|
+
});
|
|
1731
|
+
if (isCancel(sel)) continue;
|
|
1267
1732
|
|
|
1268
1733
|
const yamlContent = fs.readFileSync(path.join(PROJECT_ROOT, sel), 'utf8');
|
|
1269
1734
|
const missionSpec = normalizeMissionSpec(loadYaml(yamlContent), path.join(PROJECT_ROOT, sel));
|
|
@@ -1271,9 +1736,9 @@ async function main() {
|
|
|
1271
1736
|
discoverPath = getMissionStartPath(missionSpec);
|
|
1272
1737
|
agentMode = true;
|
|
1273
1738
|
}
|
|
1274
|
-
} catch (e) {
|
|
1275
|
-
console.error("Menu error:", e);
|
|
1276
|
-
break;
|
|
1739
|
+
} catch (e) {
|
|
1740
|
+
console.error("Menu error:", e);
|
|
1741
|
+
break;
|
|
1277
1742
|
}
|
|
1278
1743
|
}
|
|
1279
1744
|
|
|
@@ -1314,10 +1779,38 @@ async function main() {
|
|
|
1314
1779
|
batchCollection = selectedBatchCollection;
|
|
1315
1780
|
}
|
|
1316
1781
|
|
|
1782
|
+
let batchConcurrency = normalizeCollectionConcurrency(
|
|
1783
|
+
collectionConcurrency,
|
|
1784
|
+
skipMenu ? 1 : COLLECTION_DEFAULT_CONCURRENCY
|
|
1785
|
+
);
|
|
1786
|
+
|
|
1787
|
+
if (!skipMenu && process.stdin.isTTY) {
|
|
1788
|
+
note(
|
|
1789
|
+
'La ejecucion paralela abre varias sesiones del portal al mismo tiempo. Arcality no puede conocer ni validar reglas internas de sesion, bloqueo por usuario, datos compartidos o limites del sistema probado; si tu portal no permite sesiones paralelas con el mismo usuario, usa 1.',
|
|
1790
|
+
'Sesiones del portal'
|
|
1791
|
+
);
|
|
1792
|
+
|
|
1793
|
+
const selectedConcurrency = await select({
|
|
1794
|
+
message: 'Cuantas misiones quieres ejecutar al mismo tiempo?',
|
|
1795
|
+
options: [
|
|
1796
|
+
{ label: `3 simultaneas (recomendado)`, value: 3 },
|
|
1797
|
+
{ label: '1 secuencial', value: 1 },
|
|
1798
|
+
{ label: '5 simultaneas (maximo)', value: 5 }
|
|
1799
|
+
]
|
|
1800
|
+
});
|
|
1801
|
+
if (isCancel(selectedConcurrency)) {
|
|
1802
|
+
if (skipMenu) return;
|
|
1803
|
+
continue;
|
|
1804
|
+
}
|
|
1805
|
+
batchConcurrency = normalizeCollectionConcurrency(selectedConcurrency, COLLECTION_DEFAULT_CONCURRENCY);
|
|
1806
|
+
}
|
|
1807
|
+
|
|
1317
1808
|
await runCollectionBatch({
|
|
1318
1809
|
missionsRootDir: process.env.MISSIONS_DIR,
|
|
1319
1810
|
selectedCollection: batchCollection || '__ALL__',
|
|
1320
|
-
debug: process.argv.includes('--debug')
|
|
1811
|
+
debug: process.argv.includes('--debug'),
|
|
1812
|
+
concurrency: batchConcurrency,
|
|
1813
|
+
interactive: !skipMenu
|
|
1321
1814
|
});
|
|
1322
1815
|
|
|
1323
1816
|
if (skipMenu) return;
|
|
@@ -1326,443 +1819,495 @@ async function main() {
|
|
|
1326
1819
|
|
|
1327
1820
|
// --- VALIDATION AND CAPTURE OF PARAMETERS FOR THE AGENT ---
|
|
1328
1821
|
if (agentMode) {
|
|
1329
|
-
if (!prompt || prompt.trim().length < 4) {
|
|
1330
|
-
const p = await text({
|
|
1331
|
-
message: chalk.cyan('🤖 Misión para Arcality (Requerida):'),
|
|
1332
|
-
placeholder: 'Ej., Llena el formulario de registro con datos aleatorios',
|
|
1333
|
-
validate: v => {
|
|
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).';
|
|
1336
|
-
}
|
|
1337
|
-
});
|
|
1338
|
-
|
|
1339
|
-
if (isCancel(p)) {
|
|
1340
|
-
cancel('Misión cancelada. Regresando al menú principal...');
|
|
1341
|
-
agentMode = false;
|
|
1342
|
-
prompt = "";
|
|
1343
|
-
skipMenu = false;
|
|
1344
|
-
continue;
|
|
1345
|
-
}
|
|
1346
|
-
prompt = p;
|
|
1347
|
-
}
|
|
1348
|
-
|
|
1349
|
-
if (!discoverPath) {
|
|
1350
|
-
const knownBaseUrl = projectConfig?.project?.baseUrl || process.env.BASE_URL;
|
|
1351
|
-
const promptMsg = knownBaseUrl
|
|
1352
|
-
? `🌐
|
|
1353
|
-
: '🌐 URL Completa de Navegación (ej., http://localhost:3000/):';
|
|
1354
|
-
|
|
1355
|
-
const d = await text({
|
|
1356
|
-
message: chalk.cyan(promptMsg),
|
|
1357
|
-
placeholder: knownBaseUrl ? '/' : 'http://localhost:3000/',
|
|
1358
|
-
validate: v => {
|
|
1359
|
-
if (!knownBaseUrl && !v.startsWith('http')) return 'Por favor provee una URL completa con http:// o https://';
|
|
1360
|
-
return undefined;
|
|
1361
|
-
}
|
|
1362
|
-
});
|
|
1363
|
-
|
|
1364
|
-
if (isCancel(d)) {
|
|
1365
|
-
cancel('Misión abortada. Regresando al menú...');
|
|
1366
|
-
agentMode = false;
|
|
1367
|
-
prompt = "";
|
|
1368
|
-
skipMenu = false;
|
|
1369
|
-
continue;
|
|
1370
|
-
}
|
|
1371
|
-
discoverPath = d;
|
|
1372
|
-
}
|
|
1373
|
-
}
|
|
1374
|
-
|
|
1375
|
-
if (agentMode) {
|
|
1376
|
-
// Load environment from BOTH the user's project and the library's root
|
|
1377
|
-
const userDotEnv = parseDotEnvFile(path.join(ORIGINAL_CWD, '.env'));
|
|
1378
|
-
const libDotEnv = parseDotEnvFile(path.join(PROJECT_ROOT, '.env'));
|
|
1379
|
-
const dotEnv = { ...userDotEnv, ...libDotEnv };
|
|
1380
|
-
|
|
1381
|
-
// ── API Key and Quota Validation ──
|
|
1382
|
-
const { validateApiKey, startMission: requestMission } = await import('../src/arcalityClient.mjs');
|
|
1383
|
-
|
|
1384
|
-
const validation = await validateApiKey();
|
|
1385
|
-
if (process.env.DEBUG || process.argv.includes('--debug')) {
|
|
1386
|
-
console.log(chalk.gray(`[DEBUG] Validation result: ${JSON.stringify(validation, null, 2)}`));
|
|
1387
|
-
}
|
|
1388
|
-
|
|
1389
|
-
const detectedOrgId = validation.organization_id || validation.organizationId || validation.OrganizationId;
|
|
1390
|
-
if (detectedOrgId) {
|
|
1391
|
-
process.env.ARCALITY_ORG_ID = detectedOrgId;
|
|
1392
|
-
}
|
|
1393
|
-
if (!validation.valid) {
|
|
1394
|
-
const errorMessages = {
|
|
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'
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
const
|
|
1410
|
-
const
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
chalk.gray('
|
|
1414
|
-
chalk.gray('
|
|
1415
|
-
chalk.gray('
|
|
1416
|
-
'
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
if (
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
//
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
//
|
|
1654
|
-
//
|
|
1655
|
-
//
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
let
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
//
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1822
|
+
if (!prompt || prompt.trim().length < 4) {
|
|
1823
|
+
const p = await text({
|
|
1824
|
+
message: chalk.cyan('🤖 Misión para Arcality (Requerida):'),
|
|
1825
|
+
placeholder: 'Ej., Llena el formulario de registro con datos aleatorios',
|
|
1826
|
+
validate: v => {
|
|
1827
|
+
if (!v || !v.trim()) return 'La misión es requerida para iniciar.';
|
|
1828
|
+
if (v.trim().length < 4) return 'La misión debe ser más descriptiva (mínimo 4 caracteres).';
|
|
1829
|
+
}
|
|
1830
|
+
});
|
|
1831
|
+
|
|
1832
|
+
if (isCancel(p)) {
|
|
1833
|
+
cancel('Misión cancelada. Regresando al menú principal...');
|
|
1834
|
+
agentMode = false;
|
|
1835
|
+
prompt = "";
|
|
1836
|
+
skipMenu = false;
|
|
1837
|
+
continue;
|
|
1838
|
+
}
|
|
1839
|
+
prompt = p;
|
|
1840
|
+
}
|
|
1841
|
+
|
|
1842
|
+
if (!discoverPath) {
|
|
1843
|
+
const knownBaseUrl = projectConfig?.project?.baseUrl || process.env.BASE_URL;
|
|
1844
|
+
const promptMsg = knownBaseUrl
|
|
1845
|
+
? `🌐 URL Completa de Navegación (relative to ${knownBaseUrl}):`
|
|
1846
|
+
: '🌐 URL Completa de Navegación (ej., http://localhost:3000/):';
|
|
1847
|
+
|
|
1848
|
+
const d = await text({
|
|
1849
|
+
message: chalk.cyan(promptMsg),
|
|
1850
|
+
placeholder: knownBaseUrl ? '/' : 'http://localhost:3000/',
|
|
1851
|
+
validate: v => {
|
|
1852
|
+
if (!knownBaseUrl && !v.startsWith('http')) return 'Por favor provee una URL completa con http:// o https://';
|
|
1853
|
+
return undefined;
|
|
1854
|
+
}
|
|
1855
|
+
});
|
|
1856
|
+
|
|
1857
|
+
if (isCancel(d)) {
|
|
1858
|
+
cancel('Misión abortada. Regresando al menú...');
|
|
1859
|
+
agentMode = false;
|
|
1860
|
+
prompt = "";
|
|
1861
|
+
skipMenu = false;
|
|
1862
|
+
continue;
|
|
1863
|
+
}
|
|
1864
|
+
discoverPath = d;
|
|
1865
|
+
}
|
|
1866
|
+
}
|
|
1867
|
+
|
|
1868
|
+
if (agentMode) {
|
|
1869
|
+
// Load environment from BOTH the user's project and the library's root
|
|
1870
|
+
const userDotEnv = parseDotEnvFile(path.join(ORIGINAL_CWD, '.env'));
|
|
1871
|
+
const libDotEnv = parseDotEnvFile(path.join(PROJECT_ROOT, '.env'));
|
|
1872
|
+
const dotEnv = { ...userDotEnv, ...libDotEnv };
|
|
1873
|
+
|
|
1874
|
+
// ── API Key and Quota Validation ──
|
|
1875
|
+
const { validateApiKey, startMission: requestMission } = await import('../src/arcalityClient.mjs');
|
|
1876
|
+
|
|
1877
|
+
const validation = await validateApiKey();
|
|
1878
|
+
if (process.env.DEBUG || process.argv.includes('--debug')) {
|
|
1879
|
+
console.log(chalk.gray(`[DEBUG] Validation result: ${JSON.stringify(validation, null, 2)}`));
|
|
1880
|
+
}
|
|
1881
|
+
|
|
1882
|
+
const detectedOrgId = validation.organization_id || validation.organizationId || validation.OrganizationId;
|
|
1883
|
+
if (detectedOrgId) {
|
|
1884
|
+
process.env.ARCALITY_ORG_ID = detectedOrgId;
|
|
1885
|
+
}
|
|
1886
|
+
if (!validation.valid) {
|
|
1887
|
+
const errorMessages = {
|
|
1888
|
+
'no_api_key': '🔑 API Key no encontrada.\n Configúrala vía `arcality init` o en .env (ARCALITY_API_KEY)',
|
|
1889
|
+
'invalid_format': '🔑 API Key inválida. Debe comenzar con "arc_"',
|
|
1890
|
+
'invalid_api_key': '🔑 API Key no reconocida por el servidor',
|
|
1891
|
+
'plan_expired': '💳 Tu plan ha expirado',
|
|
1892
|
+
'backend_unavailable': 'No se pudo conectar con Arcality. La herramienta requiere conexion al backend para validar la cuenta.'
|
|
1893
|
+
};
|
|
1894
|
+
note(chalk.red(errorMessages[validation.error] || `Error: ${validation.error}`), '❌ Autenticación');
|
|
1895
|
+
agentMode = false;
|
|
1896
|
+
prompt = "";
|
|
1897
|
+
skipMenu = false;
|
|
1898
|
+
continue;
|
|
1899
|
+
}
|
|
1900
|
+
|
|
1901
|
+
// Show plan info
|
|
1902
|
+
const modeLabel = validation.mode === 'mock' ? chalk.gray('[sandbox]') : chalk.cyanBright('[live-net]');
|
|
1903
|
+
const dailyLimitDisplay = validation.daily_limit === -1 ? '∞' : (validation.daily_limit || 35);
|
|
1904
|
+
const remainingDisplay = validation.remaining >= 999999 ? '∞' : validation.remaining;
|
|
1905
|
+
note(
|
|
1906
|
+
chalk.gray('Conexión: ') + chalk.cyanBright('SECURE ') + modeLabel + '\n' +
|
|
1907
|
+
chalk.gray('Autorización: ') + chalk.white(validation.plan || 'internal') + '\n' +
|
|
1908
|
+
chalk.gray('Misiones: ') + chalk.white(`${validation.daily_used || 0}/${dailyLimitDisplay}`) + '\n' +
|
|
1909
|
+
chalk.gray('Reservas: ') + chalk.white(remainingDisplay),
|
|
1910
|
+
'Enlace Arcality'
|
|
1911
|
+
);
|
|
1912
|
+
|
|
1913
|
+
// --- RESOLVE PROJECT ID ---
|
|
1914
|
+
let selectedProjectId = projectConfig?.projectId || dotEnv.ARCALITY_PROJECT_ID || process.env.ARCALITY_PROJECT_ID;
|
|
1915
|
+
const apiBase = getApiUrl();
|
|
1916
|
+
|
|
1917
|
+
if (selectedProjectId && !isRealProjectId(selectedProjectId)) {
|
|
1918
|
+
note(
|
|
1919
|
+
chalk.red('Project ID invalido: la herramienta no puede ejecutarse con IDs locales o mock.') + '\n' +
|
|
1920
|
+
chalk.gray('Ejecuta `arcality init` para registrar este proyecto en Arcality.'),
|
|
1921
|
+
'Proyecto no conectado'
|
|
1922
|
+
);
|
|
1923
|
+
agentMode = false;
|
|
1924
|
+
prompt = "";
|
|
1925
|
+
skipMenu = false;
|
|
1926
|
+
continue;
|
|
1927
|
+
}
|
|
1928
|
+
|
|
1929
|
+
if (!selectedProjectId) {
|
|
1930
|
+
// If no project ID from config, try to create/select from backend
|
|
1931
|
+
try {
|
|
1932
|
+
const projController = new AbortController();
|
|
1933
|
+
const projTimeout = setTimeout(() => projController.abort(), 5000);
|
|
1934
|
+
const projRes = await fetch(`${apiBase}/api/v1/projects`, {
|
|
1935
|
+
headers: { 'x-api-key': process.env.ARCALITY_API_KEY || '' },
|
|
1936
|
+
signal: projController.signal
|
|
1937
|
+
});
|
|
1938
|
+
clearTimeout(projTimeout);
|
|
1939
|
+
|
|
1940
|
+
if (projRes.ok) {
|
|
1941
|
+
const data = await projRes.json();
|
|
1942
|
+
const projects = data.projects || [];
|
|
1943
|
+
|
|
1944
|
+
const projOptions = projects.map(p => ({ label: p.name || p.Name, value: p.id || p.Id }));
|
|
1945
|
+
projOptions.push({ label: '➕ Crear nuevo proyecto', value: 'new' });
|
|
1946
|
+
|
|
1947
|
+
const projSelection = await select({
|
|
1948
|
+
message: chalk.cyan('Elige un proyecto para esta misión:'),
|
|
1949
|
+
options: projOptions
|
|
1950
|
+
});
|
|
1951
|
+
|
|
1952
|
+
if (isCancel(projSelection)) {
|
|
1953
|
+
cancel('Misión abortada.');
|
|
1954
|
+
agentMode = false; prompt = ""; skipMenu = false; continue;
|
|
1955
|
+
}
|
|
1956
|
+
|
|
1957
|
+
if (projSelection === 'new') {
|
|
1958
|
+
const newName = await text({ message: 'Nombre del proyecto:', validate: v => !v ? 'Requerido' : undefined });
|
|
1959
|
+
if (isCancel(newName)) { cancel('Abortado'); agentMode = false; prompt = ""; skipMenu = false; continue; }
|
|
1960
|
+
|
|
1961
|
+
const createRes = await fetch(`${apiBase}/api/v1/projects`, {
|
|
1962
|
+
method: 'POST',
|
|
1963
|
+
headers: { 'Content-Type': 'application/json', 'x-api-key': process.env.ARCALITY_API_KEY || '' },
|
|
1964
|
+
body: JSON.stringify({
|
|
1965
|
+
name: newName,
|
|
1966
|
+
base_url: projectConfig?.project?.baseUrl || discoverPath || '/',
|
|
1967
|
+
arcality_version: projectConfig?.meta?.arcalityVersion || 'unknown',
|
|
1968
|
+
config: { source: 'npm-cli', single_configuration_mode: true, yaml_reuse_enabled: true }
|
|
1969
|
+
})
|
|
1970
|
+
});
|
|
1971
|
+
|
|
1972
|
+
if (createRes.ok) {
|
|
1973
|
+
const created = await createRes.json();
|
|
1974
|
+
selectedProjectId = created.id || created.Id;
|
|
1975
|
+
if (!isRealProjectId(selectedProjectId)) {
|
|
1976
|
+
note(chalk.red('El backend no devolvio un Project ID valido.'), 'Proyecto no conectado');
|
|
1977
|
+
selectedProjectId = null;
|
|
1978
|
+
}
|
|
1979
|
+
if (selectedProjectId) note(chalk.green(`✅ Proyecto creado: ${newName}`));
|
|
1980
|
+
} else {
|
|
1981
|
+
note(chalk.red(`❌ Falló al crear proyecto.`));
|
|
1982
|
+
}
|
|
1983
|
+
} else {
|
|
1984
|
+
selectedProjectId = projSelection;
|
|
1985
|
+
}
|
|
1986
|
+
|
|
1987
|
+
if (selectedProjectId) {
|
|
1988
|
+
process.env.ARCALITY_PROJECT_ID = selectedProjectId;
|
|
1989
|
+
}
|
|
1990
|
+
}
|
|
1991
|
+
} catch (e) {
|
|
1992
|
+
note(
|
|
1993
|
+
chalk.red(`No se pudo conectar con el servidor de proyectos: ${e.message}`) + '\n' +
|
|
1994
|
+
chalk.gray('Arcality requiere un proyecto real conectado antes de ejecutar misiones.'),
|
|
1995
|
+
'Proyecto no conectado'
|
|
1996
|
+
);
|
|
1997
|
+
}
|
|
1998
|
+
} else {
|
|
1999
|
+
process.env.ARCALITY_PROJECT_ID = selectedProjectId;
|
|
2000
|
+
}
|
|
2001
|
+
|
|
2002
|
+
if (!isRealProjectId(selectedProjectId)) {
|
|
2003
|
+
note(
|
|
2004
|
+
chalk.red('No hay un Project ID real asignado para esta ejecucion.') + '\n' +
|
|
2005
|
+
chalk.gray('Ejecuta `arcality init` y vuelve a intentar.'),
|
|
2006
|
+
'Proyecto no conectado'
|
|
2007
|
+
);
|
|
2008
|
+
agentMode = false;
|
|
2009
|
+
prompt = "";
|
|
2010
|
+
skipMenu = false;
|
|
2011
|
+
continue;
|
|
2012
|
+
}
|
|
2013
|
+
|
|
2014
|
+
// ── Start Mission ──
|
|
2015
|
+
const mission = await requestMission(prompt, discoverPath || '/');
|
|
2016
|
+
if (!mission.allowed) {
|
|
2017
|
+
if (mission.error === 'mission_limit_exceeded') {
|
|
2018
|
+
note(
|
|
2019
|
+
chalk.red('Limite diario de misiones alcanzado') + '\n' +
|
|
2020
|
+
chalk.yellow(`Misiones usadas: ${mission.daily_used}/${mission.daily_limit}`) + '\n' +
|
|
2021
|
+
chalk.gray(`Reinicio: ${mission.resets_at ? new Date(mission.resets_at).toLocaleString() : 'manana'}`),
|
|
2022
|
+
'Limite alcanzado'
|
|
2023
|
+
);
|
|
2024
|
+
} else {
|
|
2025
|
+
const missionErrors = {
|
|
2026
|
+
no_project_id: 'No hay Project ID real conectado para esta ejecucion.',
|
|
2027
|
+
backend_unavailable: 'No se pudo iniciar la mision porque Arcality no respondio.',
|
|
2028
|
+
server_error: 'Arcality no pudo registrar el inicio de la mision.',
|
|
2029
|
+
no_api_key: 'API Key no encontrada.'
|
|
2030
|
+
};
|
|
2031
|
+
note(
|
|
2032
|
+
chalk.red(missionErrors[mission.error] || `No se pudo iniciar la mision: ${mission.error}`) + '\n' +
|
|
2033
|
+
chalk.gray('La prueba no se ejecutara en modo local. Revisa `arcality init` y la conexion al backend.'),
|
|
2034
|
+
'Mision detenida'
|
|
2035
|
+
);
|
|
2036
|
+
}
|
|
2037
|
+
agentMode = false;
|
|
2038
|
+
prompt = "";
|
|
2039
|
+
skipMenu = false;
|
|
2040
|
+
continue;
|
|
2041
|
+
}
|
|
2042
|
+
|
|
2043
|
+
const s = spinner();
|
|
2044
|
+
if (!process.argv.includes('--debug')) {
|
|
2045
|
+
s.start(`◉ [ARCALITY INICIADO] Ejecutando directiva: "${prompt}" ${chalk.gray(`(${mission.daily_used}/${mission.daily_limit})`)}`);
|
|
2046
|
+
} else {
|
|
2047
|
+
console.log(chalk.cyan(`\n◉ [ARCALITY INICIADO] Ejecutando directiva: "${prompt}" ${chalk.gray(`(${mission.daily_used}/${mission.daily_limit})`)}`));
|
|
2048
|
+
}
|
|
2049
|
+
|
|
2050
|
+
const finalProjectId = process.env.ARCALITY_PROJECT_ID || selectedProjectId;
|
|
2051
|
+
|
|
2052
|
+
// ── Cancelación Manual (Ctrl+C) — Estrategia bulletproof ──
|
|
2053
|
+
// El problema: @clack/prompts registra su propio SIGINT handler cada tick del spinner,
|
|
2054
|
+
// que llama a process.exit(0) directamente. removeAllListeners() no es suficiente porque
|
|
2055
|
+
// Clack lo re-registra en cada frame de animación.
|
|
2056
|
+
//
|
|
2057
|
+
// La solución: interceptar process.exit() antes de que Clack lo llame,
|
|
2058
|
+
// y usar prependListener para que NUESTRO handler siempre se ejecute primero.
|
|
2059
|
+
|
|
2060
|
+
let activeChildProcess = null;
|
|
2061
|
+
let userCanceledMission = false;
|
|
2062
|
+
let sigintCount = 0;
|
|
2063
|
+
let _missionActive = true; // Mientras true, bloqueamos el process.exit de Clack
|
|
2064
|
+
|
|
2065
|
+
// Guardar el process.exit original y reemplazarlo temporalmente
|
|
2066
|
+
const _origProcessExit = process.exit.bind(process);
|
|
2067
|
+
process.exit = (code) => {
|
|
2068
|
+
if (_missionActive) {
|
|
2069
|
+
// Clack (u otra lib) está intentando matar el proceso — lo bloqueamos.
|
|
2070
|
+
// Nuestro cancelHandler ya habrá sido invocado vía prependListener.
|
|
2071
|
+
// El proceso se cerrará naturalmente cuando el child process termine.
|
|
2072
|
+
return;
|
|
2073
|
+
}
|
|
2074
|
+
_origProcessExit(code);
|
|
2075
|
+
};
|
|
2076
|
+
|
|
2077
|
+
// ── RESTORE RAW MODE FOR WINDOWS FIX ──
|
|
2078
|
+
// @clack/prompts pausea stdin y desactiva raw mode al terminar un menú.
|
|
2079
|
+
// Debemos reactivarlo aquí para que nuestro interceptor global de Ctrl+C
|
|
2080
|
+
// siga funcionando durante toda la misión.
|
|
2081
|
+
if (process.stdin.isTTY && process.stdin.setRawMode) {
|
|
2082
|
+
try {
|
|
2083
|
+
process.stdin.setRawMode(true);
|
|
2084
|
+
process.stdin.resume();
|
|
2085
|
+
} catch(e) {}
|
|
2086
|
+
}
|
|
2087
|
+
|
|
2088
|
+
const cancelHandler = () => {
|
|
2089
|
+
sigintCount++;
|
|
2090
|
+
if (sigintCount > 3) {
|
|
2091
|
+
console.log(chalk.red('\n🛑 Forzando salida inmediata...'));
|
|
2092
|
+
_missionActive = false;
|
|
2093
|
+
_origProcessExit(1);
|
|
2094
|
+
return;
|
|
2095
|
+
}
|
|
2096
|
+
|
|
2097
|
+
if (userCanceledMission) {
|
|
2098
|
+
console.log(chalk.yellow('\n⏳ Cancelación en progreso, esperando reporte... (Ctrl+C x4 para forzar cierre)'));
|
|
2099
|
+
return;
|
|
2100
|
+
}
|
|
2101
|
+
userCanceledMission = true;
|
|
2102
|
+
|
|
2103
|
+
console.log(chalk.yellow('\n\n⚠️ [ARCALITY] Directiva abortada por el operador.'));
|
|
2104
|
+
if (!process.argv.includes('--debug')) {
|
|
2105
|
+
try { s.stop(chalk.yellow('⚠️ Directiva abortada.')); } catch { /* ignore */ }
|
|
2106
|
+
}
|
|
2107
|
+
|
|
2108
|
+
// Terminar el proceso hijo de Playwright directamente (Windows-safe)
|
|
2109
|
+
if (activeChildProcess && !activeChildProcess.killed) {
|
|
2110
|
+
try {
|
|
2111
|
+
if (process.platform === 'win32') {
|
|
2112
|
+
spawn('taskkill', ['/pid', String(activeChildProcess.pid), '/f', '/t'], { stdio: 'ignore', windowsHide: true });
|
|
2113
|
+
} else {
|
|
2114
|
+
activeChildProcess.kill('SIGTERM');
|
|
2115
|
+
}
|
|
2116
|
+
} catch { /* silencioso */ }
|
|
2117
|
+
}
|
|
2118
|
+
|
|
2119
|
+
try {
|
|
2120
|
+
const ctxDir = process.env.CONTEXT_DIR;
|
|
2121
|
+
if (ctxDir && fs.existsSync(ctxDir)) {
|
|
2122
|
+
const cancelLog = {
|
|
2123
|
+
prompt,
|
|
2124
|
+
history: ['[ABORTO DEL OPERADOR] La directiva fue detenida manualmente antes de completarse.'],
|
|
2125
|
+
steps: 0,
|
|
2126
|
+
success: false,
|
|
2127
|
+
error: true,
|
|
2128
|
+
fail_reason: 'user_canceled',
|
|
2129
|
+
canceled_by_user: true,
|
|
2130
|
+
timestamp: new Date().toISOString()
|
|
2131
|
+
};
|
|
2132
|
+
fs.writeFileSync(path.join(ctxDir, `arcality-log-${Date.now()}.json`), JSON.stringify(cancelLog, null, 2));
|
|
2133
|
+
}
|
|
2134
|
+
} catch { /* silencioso */ }
|
|
2135
|
+
|
|
2136
|
+
console.log(chalk.gray(' >> Esperando que Playwright cierre y genere el reporte...'));
|
|
2137
|
+
// Playwright también recibe el SIGINT nativamente desde la terminal Windows
|
|
2138
|
+
// y cerrará su proceso limpiamente, lo que resolverá la Promise del run() → finally
|
|
2139
|
+
};
|
|
2140
|
+
|
|
2141
|
+
// prependListener asegura que nuestro handler se ejecuta ANTES que el de Clack
|
|
2142
|
+
process.prependListener('SIGINT', cancelHandler);
|
|
2143
|
+
process.prependListener('SIGTERM', cancelHandler);
|
|
2144
|
+
|
|
2145
|
+
|
|
2146
|
+
// Build env for the runner — merge arcality.config values.
|
|
2147
|
+
// ARCALITY_API_URL must be explicit — it comes from the library's internal config,
|
|
2148
|
+
// not from the user's project .env, so we inject it directly via getApiUrl().
|
|
2149
|
+
const fallbackBaseUrl = (!projectConfig && discoverPath?.startsWith('http')) ? discoverPath : undefined;
|
|
2150
|
+
|
|
2151
|
+
const mergedEnv = {
|
|
2152
|
+
...dotEnv,
|
|
2153
|
+
...process.env,
|
|
2154
|
+
ARCALITY_API_URL: getApiUrl(), // Internal URL — always injected explicitly
|
|
2155
|
+
ARCALITY_PROJECT_ID: finalProjectId,
|
|
2156
|
+
ARCALITY_MISSION_ID: mission.mission_id || mission.missionId || mission.MissionId || '', // ← Propagate mission_id to every proxy call
|
|
2157
|
+
ARCALITY_ORG_ID: validation.organization_id || validation.organizationId || validation.OrganizationId || process.env.ARCALITY_ORG_ID || '', // ← Propagate org_id for pattern persistence
|
|
2158
|
+
BASE_URL: projectConfig?.project?.baseUrl || dotEnv.BASE_URL || process.env.BASE_URL || fallbackBaseUrl || 'http://localhost:3000',
|
|
2159
|
+
LOGIN_USER: projectConfig?.auth?.username || dotEnv.LOGIN_USER || process.env.LOGIN_USER,
|
|
2160
|
+
LOGIN_PASSWORD: projectConfig?.auth?.password || dotEnv.LOGIN_PASSWORD || process.env.LOGIN_PASSWORD,
|
|
2161
|
+
DOMAIN_DIR: process.env.DOMAIN_DIR,
|
|
2162
|
+
MISSIONS_DIR: process.env.MISSIONS_DIR,
|
|
2163
|
+
CONTEXT_DIR: process.env.CONTEXT_DIR,
|
|
2164
|
+
REPORTS_DIR: process.env.REPORTS_DIR,
|
|
2165
|
+
SMART_PROMPT: prompt,
|
|
2166
|
+
TARGET_PATH: discoverPath || '/'
|
|
2167
|
+
};
|
|
2168
|
+
|
|
2169
|
+
if (finalProjectId) {
|
|
2170
|
+
console.log(chalk.gray(`>> ARCALITY_PROJECT_ID: ${finalProjectId}`));
|
|
2171
|
+
console.log(chalk.gray(`>> ARCALITY_ORG_ID: ${mergedEnv.ARCALITY_ORG_ID || 'NONE'}`));
|
|
2172
|
+
}
|
|
2173
|
+
|
|
2174
|
+
// ── Resolve execution paths ──
|
|
2175
|
+
// tsx and playwright are HOISTED to ORIGINAL_CWD/node_modules when installed via npm.
|
|
2176
|
+
// We run from ORIGINAL_CWD so relative paths for playwright work.
|
|
2177
|
+
// For the test file and config (inside the arcality package), we use absolute paths.
|
|
2178
|
+
//
|
|
2179
|
+
// KEY: tsx loader uses file:// URL (pathToFileURL) so spaces encode as %20 — no quoting needed.
|
|
2180
|
+
|
|
2181
|
+
const testFile = path.join(PROJECT_ROOT, 'tests', '_helpers', 'agentic-runner.spec.ts');
|
|
2182
|
+
const configFile = path.join(PROJECT_ROOT, 'playwright.config.js');
|
|
2183
|
+
|
|
2184
|
+
// Resolve playwright CLI — CRITICAL singleton alignment:
|
|
2185
|
+
//
|
|
2186
|
+
// @playwright/test uses `playwright` internally. When npm detects a version mismatch
|
|
2187
|
+
// between the top-level `playwright` and what `@playwright/test` needs, it installs
|
|
2188
|
+
// a NESTED copy at: @playwright/test/node_modules/playwright/
|
|
2189
|
+
//
|
|
2190
|
+
// Playwright's test runner tracks the active suite via a module-level singleton
|
|
2191
|
+
// (currentSuite). If the CLI uses a DIFFERENT playwright instance than the one
|
|
2192
|
+
// @playwright/test loaded, their singletons are out of sync → "did not expect test()
|
|
2193
|
+
// to be called here".
|
|
2194
|
+
//
|
|
2195
|
+
// Fix: ALWAYS prefer the playwright nested INSIDE @playwright/test, because that is
|
|
2196
|
+
// guaranteed to be the same instance that @playwright/test uses for its internals.
|
|
2197
|
+
//
|
|
2198
|
+
// Resolution priority:
|
|
2199
|
+
// 1. <project>/@playwright/test/node_modules/playwright/cli.js ← nested (safe)
|
|
2200
|
+
// 2. <project>/playwright/cli.js ← sibling top-level
|
|
2201
|
+
// (repeated for ORIGINAL_CWD and PROJECT_ROOT)
|
|
2202
|
+
let playwrightCli;
|
|
2203
|
+
try {
|
|
2204
|
+
const searchPaths = [ORIGINAL_CWD, PROJECT_ROOT];
|
|
2205
|
+
let testRunnerPath = null;
|
|
2206
|
+
let resolveError = null;
|
|
2207
|
+
|
|
2208
|
+
for (const searchPath of searchPaths) {
|
|
2209
|
+
try {
|
|
2210
|
+
testRunnerPath = require.resolve('@playwright/test', { paths: [searchPath] });
|
|
2211
|
+
break;
|
|
2212
|
+
} catch (e) {
|
|
2213
|
+
resolveError = e;
|
|
2214
|
+
}
|
|
2215
|
+
}
|
|
2216
|
+
|
|
2217
|
+
if (!testRunnerPath) throw resolveError;
|
|
2218
|
+
|
|
2219
|
+
const testRunnerDir = path.dirname(testRunnerPath); // → .../node_modules/@playwright/test/
|
|
2220
|
+
|
|
2221
|
+
// Priority 1: playwright nested INSIDE @playwright/test — same singleton as the package uses
|
|
2222
|
+
const nestedCli = path.join(testRunnerDir, 'node_modules', 'playwright', 'cli.js');
|
|
2223
|
+
// Priority 2: playwright as a top-level sibling (clean installs, versions aligned)
|
|
2224
|
+
const siblingCli = path.join(testRunnerDir, '..', '..', 'playwright', 'cli.js');
|
|
2225
|
+
|
|
2226
|
+
if (fs.existsSync(nestedCli)) {
|
|
2227
|
+
playwrightCli = nestedCli;
|
|
2228
|
+
} else if (fs.existsSync(siblingCli)) {
|
|
2229
|
+
playwrightCli = siblingCli;
|
|
2230
|
+
} else {
|
|
2231
|
+
throw new Error(
|
|
2232
|
+
`Playwright CLI not found. Tried:\n (nested) ${nestedCli}\n (sibling) ${siblingCli}`
|
|
2233
|
+
);
|
|
2234
|
+
}
|
|
2235
|
+
|
|
2236
|
+
if (process.argv.includes('--debug')) {
|
|
2237
|
+
console.log(chalk.gray(`[DEBUG] Using playwright CLI: ${playwrightCli}`));
|
|
2238
|
+
}
|
|
2239
|
+
} catch (e) {
|
|
2240
|
+
console.error(chalk.red('\n[FATAL] Núcleo de Playwright no encontrado. Asegúrate de que @playwright/test esté instalado.'));
|
|
2241
|
+
console.error(chalk.red(e.message));
|
|
2242
|
+
console.error(chalk.yellow('\nConsejo: Ejecuta `npm install @playwright/test` en tu directorio de proyecto.'));
|
|
2243
|
+
_missionActive = false;
|
|
2244
|
+
process.exit = _origProcessExit;
|
|
2245
|
+
_origProcessExit(1);
|
|
2246
|
+
}
|
|
2247
|
+
|
|
2248
|
+
const postProcessSigint = () => console.log(chalk.yellow('\n⏳ Finalizando reporte de telemetría, por favor espera...'));
|
|
2249
|
+
let missionResult = null;
|
|
2250
|
+
let finalFailReason = null;
|
|
2251
|
+
let finalUsageData = undefined;
|
|
2252
|
+
let finalReportUrl = null;
|
|
2253
|
+
let finalFailReasoning = null;
|
|
2254
|
+
|
|
2255
|
+
if (process.env.ARCALITY_RUNNER_PREBUILT === 'true') {
|
|
2256
|
+
if (process.argv.includes('--debug')) {
|
|
2257
|
+
console.log(chalk.gray('[DEBUG] Runner precompilado por la coleccion. Omitiendo build local.'));
|
|
2258
|
+
}
|
|
2259
|
+
} else
|
|
2260
|
+
|
|
2261
|
+
// ── Auto-compilar el runner antes de ejecutar Playwright para asegurar que los cambios de rama/merge estén aplicados ──
|
|
2262
|
+
try {
|
|
2263
|
+
if (!process.argv.includes('--debug')) {
|
|
2264
|
+
s.message('⚙️ Compilando agente de pruebas (Arcality Runner)...');
|
|
2265
|
+
} else {
|
|
2266
|
+
console.log(chalk.cyan('⚙️ Compilando agente de pruebas (Arcality Runner)...'));
|
|
2267
|
+
}
|
|
2268
|
+
await ensureRunnerBundle({ debug: process.argv.includes('--debug') });
|
|
2269
|
+
} catch (err) {
|
|
2270
|
+
console.warn(chalk.yellow(`\n⚠️ Advertencia: No se pudo auto-compilar el runner: ${err.message}`));
|
|
2271
|
+
}
|
|
2272
|
+
|
|
2273
|
+
try {
|
|
2274
|
+
// No test file path is passed — playwright.config.js uses testMatch:'agentic-runner.spec.ts'
|
|
2275
|
+
// This eliminates the Windows regex path issue permanently on any user's machine.
|
|
2276
|
+
await run('node', [
|
|
2277
|
+
'--no-warnings',
|
|
2278
|
+
playwrightCli,
|
|
2279
|
+
'test',
|
|
2280
|
+
'--headed',
|
|
2281
|
+
'--config', 'playwright.config.js'
|
|
2282
|
+
], {
|
|
2283
|
+
cwd: PROJECT_ROOT,
|
|
2284
|
+
env: mergedEnv,
|
|
2285
|
+
onStatus: (msg) => { if (!process.argv.includes('--debug')) s.message(msg); else console.log(chalk.gray(`>> Status: ${msg}`)); },
|
|
2286
|
+
onSpawn: (child) => { activeChildProcess = child; } // ← Capturar referencia para cancelHandler
|
|
2287
|
+
});
|
|
2288
|
+
|
|
2289
|
+
process.removeAllListeners('SIGINT');
|
|
2290
|
+
process.removeAllListeners('SIGTERM');
|
|
2291
|
+
process.on('SIGINT', postProcessSigint);
|
|
2292
|
+
process.on('SIGTERM', postProcessSigint);
|
|
2293
|
+
|
|
2294
|
+
if (userCanceledMission) {
|
|
2295
|
+
missionResult = 'canceled';
|
|
2296
|
+
finalFailReason = 'user_canceled';
|
|
2297
|
+
// Detener el spinner explícitamente antes de salir
|
|
2298
|
+
if (!process.argv.includes('--debug')) {
|
|
2299
|
+
try { s.stop(chalk.yellow('⚠️ Misión cancelada por el usuario.')); } catch { }
|
|
2300
|
+
}
|
|
2301
|
+
return; // El handler ya tomó el control, finally se ejecutará
|
|
2302
|
+
}
|
|
2303
|
+
|
|
2304
|
+
missionResult = 'success';
|
|
2305
|
+
if (!process.argv.includes('--debug')) s.stop(chalk.green('✅ Misión completada exitosamente.'));
|
|
2306
|
+
else console.log(chalk.green('✅ Misión completada exitosamente.'));
|
|
2307
|
+
|
|
2308
|
+
// ── Ping Project ──
|
|
2309
|
+
await pingProject(finalProjectId, process.env.ARCALITY_API_KEY);
|
|
2310
|
+
|
|
1766
2311
|
// ── Save Mission YAML ──
|
|
1767
2312
|
let saveDecision = 'no';
|
|
1768
2313
|
if (!skipSavePrompt && !batchMode) {
|
|
@@ -1776,17 +2321,17 @@ async function main() {
|
|
|
1776
2321
|
}
|
|
1777
2322
|
|
|
1778
2323
|
if (saveDecision === 'yes') {
|
|
1779
|
-
const name = await text({
|
|
1780
|
-
message: 'Nombre de la misión (ej., login_valido):',
|
|
1781
|
-
placeholder: 'my_mission',
|
|
1782
|
-
validate: v => v.length < 3 ? 'Nombre muy corto' : undefined
|
|
1783
|
-
});
|
|
1784
|
-
|
|
2324
|
+
const name = await text({
|
|
2325
|
+
message: 'Nombre de la misión (ej., login_valido):',
|
|
2326
|
+
placeholder: 'my_mission',
|
|
2327
|
+
validate: v => v.length < 3 ? 'Nombre muy corto' : undefined
|
|
2328
|
+
});
|
|
2329
|
+
|
|
1785
2330
|
if (!isCancel(name)) {
|
|
1786
2331
|
const safeName = name.trim().replace(/[^a-z0-9]/gi, '_').toLowerCase();
|
|
1787
|
-
|
|
1788
|
-
// Save to missions dir (existing flow)
|
|
1789
|
-
const missionsDir = process.env.MISSIONS_DIR;
|
|
2332
|
+
|
|
2333
|
+
// Save to missions dir (existing flow)
|
|
2334
|
+
const missionsDir = process.env.MISSIONS_DIR;
|
|
1790
2335
|
const missionSaveDir = await selectMissionSaveDir(missionsDir);
|
|
1791
2336
|
if (!missionSaveDir) {
|
|
1792
2337
|
note(chalk.yellow('Guardado de mision cancelado.'), 'Persistencia Arcality');
|
|
@@ -1807,8 +2352,8 @@ async function main() {
|
|
|
1807
2352
|
`priority: ${JSON.stringify('normal')}`,
|
|
1808
2353
|
`created_at: ${JSON.stringify(new Date().toISOString())}`
|
|
1809
2354
|
].join('\n') + '\n';
|
|
1810
|
-
|
|
1811
|
-
const smartYamlPath = path.join(process.env.CONTEXT_DIR, 'last-mission-smart.yaml');
|
|
2355
|
+
|
|
2356
|
+
const smartYamlPath = path.join(process.env.CONTEXT_DIR, 'last-mission-smart.yaml');
|
|
1812
2357
|
if (fs.existsSync(smartYamlPath)) {
|
|
1813
2358
|
const smartYamlContent = fs.readFileSync(smartYamlPath, 'utf8').trim();
|
|
1814
2359
|
yamlData = [
|
|
@@ -1823,172 +2368,175 @@ async function main() {
|
|
|
1823
2368
|
smartYamlContent
|
|
1824
2369
|
].join('\n') + '\n';
|
|
1825
2370
|
}
|
|
1826
|
-
|
|
1827
|
-
// Internal fallback backup for the agent
|
|
1828
|
-
fs.writeFileSync(yamlPathMissions, yamlData);
|
|
1829
|
-
|
|
1830
|
-
// Primary save to user's specified directory from arcality.config
|
|
1831
|
-
if (projectConfig) {
|
|
1832
|
-
const yamlOutputDir = ensureYamlOutputDir(projectConfig, ORIGINAL_CWD);
|
|
2371
|
+
|
|
2372
|
+
// Internal fallback backup for the agent
|
|
2373
|
+
fs.writeFileSync(yamlPathMissions, yamlData);
|
|
2374
|
+
|
|
2375
|
+
// Primary save to user's specified directory from arcality.config
|
|
2376
|
+
if (projectConfig) {
|
|
2377
|
+
const yamlOutputDir = ensureYamlOutputDir(projectConfig, ORIGINAL_CWD);
|
|
1833
2378
|
const yamlOutputTargetDir = missionSaveDir.relDir ? path.join(yamlOutputDir, missionSaveDir.relDir) : yamlOutputDir;
|
|
1834
2379
|
fs.mkdirSync(yamlOutputTargetDir, { recursive: true });
|
|
1835
2380
|
const yamlPathOutput = path.join(yamlOutputTargetDir, `${safeName}.yaml`);
|
|
1836
|
-
fs.writeFileSync(yamlPathOutput, yamlData);
|
|
1837
|
-
note(chalk.green(`✅ Misión guardada en: ${yamlPathOutput}`), 'Persistencia Arcality');
|
|
1838
|
-
} else {
|
|
1839
|
-
note(chalk.green(`✅ Misión guardada en: ${yamlPathMissions}`), 'Persistencia Arcality');
|
|
1840
|
-
}
|
|
1841
|
-
}
|
|
1842
|
-
}
|
|
1843
|
-
} catch (e) {
|
|
1844
|
-
// SIGINT ya fue manejado por cancelHandler — solo clasificamos el resultado
|
|
1845
|
-
if (userCanceledMission) {
|
|
1846
|
-
missionResult = 'canceled';
|
|
1847
|
-
finalFailReason = 'user_canceled';
|
|
1848
|
-
console.log(chalk.yellow('\n⚠️ Directiva abortada — generando reporte de evidencia...'));
|
|
1849
|
-
} else {
|
|
1850
|
-
missionResult = 'failed';
|
|
1851
|
-
finalFailReason = 'timeout_or_crash';
|
|
1852
|
-
s.stop(chalk.red(`❌ Arcality no pudo completar la directiva.`));
|
|
1853
|
-
console.error(chalk.red(`\nDetalles del Error: ${e.message}`));
|
|
1854
|
-
if (e.stack && process.argv.includes('--debug')) {
|
|
1855
|
-
console.error(chalk.gray(e.stack));
|
|
1856
|
-
}
|
|
1857
|
-
}
|
|
1858
|
-
} finally {
|
|
1859
|
-
// Desactivar el bloqueo de process.exit y restaurar el original
|
|
1860
|
-
_missionActive = false;
|
|
1861
|
-
process.exit = _origProcessExit;
|
|
1862
|
-
process.removeListener('SIGINT', cancelHandler);
|
|
1863
|
-
process.removeListener('SIGTERM', cancelHandler);
|
|
1864
|
-
|
|
1865
|
-
if (userCanceledMission) {
|
|
1866
|
-
missionResult = 'canceled';
|
|
1867
|
-
finalFailReason = 'user_canceled';
|
|
1868
|
-
finalFailReasoning = 'La misión fue detenida manualmente por el operador (Ctrl+C). Esto no es una falla técnica del sistema.';
|
|
1869
|
-
await new Promise(r => setTimeout(r, 800));
|
|
1870
|
-
}
|
|
1871
|
-
|
|
1872
|
-
process.on('SIGINT', postProcessSigint);
|
|
1873
|
-
process.on('SIGTERM', postProcessSigint);
|
|
1874
|
-
|
|
1875
|
-
const sRep = spinner();
|
|
1876
|
-
if (!process.argv.includes('--debug')) {
|
|
1877
|
-
sRep.start('📊 Generando reporte...');
|
|
1878
|
-
} else {
|
|
1879
|
-
console.log('📊 Generando reporte...');
|
|
1880
|
-
}
|
|
1881
|
-
|
|
1882
|
-
await run("node", [path.join(PROJECT_ROOT, "scripts/rebrand-report.mjs")]).catch(() => { });
|
|
1883
|
-
|
|
1884
|
-
const reportDir = process.env.REPORTS_DIR || 'tests-report';
|
|
1885
|
-
const arcalityPath = path.resolve(reportDir, 'index.html');
|
|
1886
|
-
|
|
1887
|
-
sRep.stop();
|
|
1888
|
-
|
|
1889
|
-
// Read final usage and failReason from agent-log
|
|
1890
|
-
try {
|
|
1891
|
-
const ctxDir = process.env.CONTEXT_DIR;
|
|
1892
|
-
if (fs.existsSync(ctxDir)) {
|
|
2381
|
+
fs.writeFileSync(yamlPathOutput, yamlData);
|
|
2382
|
+
note(chalk.green(`✅ Misión guardada en: ${yamlPathOutput}`), 'Persistencia Arcality');
|
|
2383
|
+
} else {
|
|
2384
|
+
note(chalk.green(`✅ Misión guardada en: ${yamlPathMissions}`), 'Persistencia Arcality');
|
|
2385
|
+
}
|
|
2386
|
+
}
|
|
2387
|
+
}
|
|
2388
|
+
} catch (e) {
|
|
2389
|
+
// SIGINT ya fue manejado por cancelHandler — solo clasificamos el resultado
|
|
2390
|
+
if (userCanceledMission) {
|
|
2391
|
+
missionResult = 'canceled';
|
|
2392
|
+
finalFailReason = 'user_canceled';
|
|
2393
|
+
console.log(chalk.yellow('\n⚠️ Directiva abortada — generando reporte de evidencia...'));
|
|
2394
|
+
} else {
|
|
2395
|
+
missionResult = 'failed';
|
|
2396
|
+
finalFailReason = 'timeout_or_crash';
|
|
2397
|
+
s.stop(chalk.red(`❌ Arcality no pudo completar la directiva.`));
|
|
2398
|
+
console.error(chalk.red(`\nDetalles del Error: ${e.message}`));
|
|
2399
|
+
if (e.stack && process.argv.includes('--debug')) {
|
|
2400
|
+
console.error(chalk.gray(e.stack));
|
|
2401
|
+
}
|
|
2402
|
+
}
|
|
2403
|
+
} finally {
|
|
2404
|
+
// Desactivar el bloqueo de process.exit y restaurar el original
|
|
2405
|
+
_missionActive = false;
|
|
2406
|
+
process.exit = _origProcessExit;
|
|
2407
|
+
process.removeListener('SIGINT', cancelHandler);
|
|
2408
|
+
process.removeListener('SIGTERM', cancelHandler);
|
|
2409
|
+
|
|
2410
|
+
if (userCanceledMission) {
|
|
2411
|
+
missionResult = 'canceled';
|
|
2412
|
+
finalFailReason = 'user_canceled';
|
|
2413
|
+
finalFailReasoning = 'La misión fue detenida manualmente por el operador (Ctrl+C). Esto no es una falla técnica del sistema.';
|
|
2414
|
+
await new Promise(r => setTimeout(r, 800));
|
|
2415
|
+
}
|
|
2416
|
+
|
|
2417
|
+
process.on('SIGINT', postProcessSigint);
|
|
2418
|
+
process.on('SIGTERM', postProcessSigint);
|
|
2419
|
+
|
|
2420
|
+
const sRep = spinner();
|
|
2421
|
+
if (!process.argv.includes('--debug')) {
|
|
2422
|
+
sRep.start('📊 Generando reporte...');
|
|
2423
|
+
} else {
|
|
2424
|
+
console.log('📊 Generando reporte...');
|
|
2425
|
+
}
|
|
2426
|
+
|
|
2427
|
+
await run("node", [path.join(PROJECT_ROOT, "scripts/rebrand-report.mjs")]).catch(() => { });
|
|
2428
|
+
|
|
2429
|
+
const reportDir = process.env.REPORTS_DIR || 'tests-report';
|
|
2430
|
+
const arcalityPath = path.resolve(reportDir, 'index.html');
|
|
2431
|
+
|
|
2432
|
+
sRep.stop();
|
|
2433
|
+
|
|
2434
|
+
// Read final usage and failReason from agent-log
|
|
2435
|
+
try {
|
|
2436
|
+
const ctxDir = process.env.CONTEXT_DIR;
|
|
2437
|
+
if (fs.existsSync(ctxDir)) {
|
|
1893
2438
|
const files = fs.readdirSync(ctxDir);
|
|
1894
2439
|
const logFiles = files.filter(f =>
|
|
1895
2440
|
(f.startsWith('arcality-log-') || f.startsWith('agent-log-')) &&
|
|
1896
2441
|
f.endsWith('.json')
|
|
1897
2442
|
).sort();
|
|
1898
|
-
if (logFiles.length > 0) {
|
|
1899
|
-
const lastLog = path.join(ctxDir, logFiles[logFiles.length - 1]);
|
|
1900
|
-
const logData = JSON.parse(fs.readFileSync(lastLog, 'utf8'));
|
|
1901
|
-
finalUsageData = logData.usage || finalUsageData;
|
|
1902
|
-
finalFailReasoning = logData.fail_reasoning || null;
|
|
1903
|
-
if (missionResult === 'failed' && !finalFailReason) {
|
|
1904
|
-
finalFailReason = logData.fail_reason || "timeout_or_crash";
|
|
1905
|
-
}
|
|
1906
|
-
}
|
|
1907
|
-
}
|
|
1908
|
-
} catch(e) {}
|
|
1909
|
-
|
|
1910
|
-
// ── Upload Evidence (solo si NO fue cancelado por usuario) ──
|
|
1911
|
-
if (missionResult !== 'canceled') {
|
|
1912
|
-
try {
|
|
1913
|
-
const { getEvidenceSasToken } = await import('../src/arcalityClient.mjs');
|
|
1914
|
-
const { uploadEvidence } = await import('../src/EvidenceUploader.mjs');
|
|
1915
|
-
|
|
1916
|
-
const sasData = await getEvidenceSasToken(mission.mission_id, finalProjectId);
|
|
1917
|
-
if (process.argv.includes('--debug')) console.log(`[DEBUG] sasData received:`, sasData);
|
|
1918
|
-
|
|
1919
|
-
const actualSasUrl = sasData?.sasTokenUrl || sasData?.sasUrl || sasData?.sas_url;
|
|
1920
|
-
const actualBasePath = sasData?.basePath || sasData?.base_path;
|
|
1921
|
-
|
|
1922
|
-
if (sasData && actualSasUrl && actualBasePath) {
|
|
1923
|
-
if (!process.argv.includes('--debug')) {
|
|
1924
|
-
sRep.start('☁️ Subiendo evidencia a Azure...');
|
|
1925
|
-
}
|
|
1926
|
-
process.removeAllListeners('SIGINT');
|
|
1927
|
-
process.removeAllListeners('SIGTERM');
|
|
1928
|
-
process.on('SIGINT', postProcessSigint);
|
|
1929
|
-
process.on('SIGTERM', postProcessSigint);
|
|
1930
|
-
|
|
1931
|
-
await uploadEvidence(reportDir, actualSasUrl, actualBasePath);
|
|
1932
|
-
|
|
1933
|
-
try {
|
|
1934
|
-
const parsedSas = new URL(actualSasUrl);
|
|
1935
|
-
finalReportUrl = `${parsedSas.origin}${parsedSas.pathname}/${actualBasePath}/index.html`;
|
|
1936
|
-
} catch(e) {}
|
|
1937
|
-
|
|
1938
|
-
if (!process.argv.includes('--debug')) sRep.stop(chalk.green('✅ Evidencia subida exitosamente.'));
|
|
1939
|
-
}
|
|
1940
|
-
} catch (e) {
|
|
1941
|
-
if (process.argv.includes('--debug')) console.log(chalk.yellow(`⚠️ Could not upload evidence: ${e.message}`));
|
|
1942
|
-
if (!process.argv.includes('--debug')) sRep.stop(chalk.yellow('⚠️ Fallo al subir evidencia.'));
|
|
1943
|
-
}
|
|
1944
|
-
} else {
|
|
1945
|
-
console.log(chalk.gray(' >> Saltando subida de evidencia (la misión fue cancelada).'));
|
|
1946
|
-
}
|
|
1947
|
-
|
|
1948
|
-
// ── End Mission — SIEMPRE se llama, independientemente de si la evidencia subió ──
|
|
1949
|
-
try {
|
|
1950
|
-
const { endMission } = await import('../src/arcalityClient.mjs');
|
|
1951
|
-
await endMission(mission.mission_id, missionResult || 'failed', finalUsageData, finalFailReason, finalReportUrl, finalFailReasoning);
|
|
1952
|
-
if (process.argv.includes('--debug')) console.log(chalk.gray(`[DEBUG] endMission called → result: ${missionResult}, failReason: ${finalFailReason}`));
|
|
1953
|
-
} catch (e) {
|
|
1954
|
-
if (process.argv.includes('--debug')) console.log(chalk.yellow(`⚠️ Could not end mission in backend: ${e.message}`));
|
|
1955
|
-
}
|
|
1956
|
-
|
|
1957
|
-
if (globalReportProcess) {
|
|
1958
|
-
try {
|
|
1959
|
-
if (process.platform === "win32") {
|
|
1960
|
-
spawn("taskkill", ["/pid", globalReportProcess.pid, "/f", "/t"], { stdio: 'ignore', windowsHide: true });
|
|
1961
|
-
} else {
|
|
1962
|
-
globalReportProcess.kill();
|
|
1963
|
-
}
|
|
1964
|
-
} catch (e) { /* ignore */ }
|
|
1965
|
-
globalReportProcess = null;
|
|
1966
|
-
}
|
|
1967
|
-
|
|
2443
|
+
if (logFiles.length > 0) {
|
|
2444
|
+
const lastLog = path.join(ctxDir, logFiles[logFiles.length - 1]);
|
|
2445
|
+
const logData = JSON.parse(fs.readFileSync(lastLog, 'utf8'));
|
|
2446
|
+
finalUsageData = logData.usage || finalUsageData;
|
|
2447
|
+
finalFailReasoning = logData.fail_reasoning || null;
|
|
2448
|
+
if (missionResult === 'failed' && !finalFailReason) {
|
|
2449
|
+
finalFailReason = logData.fail_reason || "timeout_or_crash";
|
|
2450
|
+
}
|
|
2451
|
+
}
|
|
2452
|
+
}
|
|
2453
|
+
} catch(e) {}
|
|
2454
|
+
|
|
2455
|
+
// ── Upload Evidence (solo si NO fue cancelado por usuario) ──
|
|
2456
|
+
if (missionResult !== 'canceled') {
|
|
2457
|
+
try {
|
|
2458
|
+
const { getEvidenceSasToken } = await import('../src/arcalityClient.mjs');
|
|
2459
|
+
const { uploadEvidence } = await import('../src/EvidenceUploader.mjs');
|
|
2460
|
+
|
|
2461
|
+
const sasData = await getEvidenceSasToken(mission.mission_id, finalProjectId);
|
|
2462
|
+
if (process.argv.includes('--debug')) console.log(`[DEBUG] sasData received:`, sasData);
|
|
2463
|
+
|
|
2464
|
+
const actualSasUrl = sasData?.sasTokenUrl || sasData?.sasUrl || sasData?.sas_url;
|
|
2465
|
+
const actualBasePath = sasData?.basePath || sasData?.base_path;
|
|
2466
|
+
|
|
2467
|
+
if (sasData && actualSasUrl && actualBasePath) {
|
|
2468
|
+
if (!process.argv.includes('--debug')) {
|
|
2469
|
+
sRep.start('☁️ Subiendo evidencia a Azure...');
|
|
2470
|
+
}
|
|
2471
|
+
process.removeAllListeners('SIGINT');
|
|
2472
|
+
process.removeAllListeners('SIGTERM');
|
|
2473
|
+
process.on('SIGINT', postProcessSigint);
|
|
2474
|
+
process.on('SIGTERM', postProcessSigint);
|
|
2475
|
+
|
|
2476
|
+
await uploadEvidence(reportDir, actualSasUrl, actualBasePath);
|
|
2477
|
+
|
|
2478
|
+
try {
|
|
2479
|
+
const parsedSas = new URL(actualSasUrl);
|
|
2480
|
+
finalReportUrl = `${parsedSas.origin}${parsedSas.pathname}/${actualBasePath}/index.html`;
|
|
2481
|
+
} catch(e) {}
|
|
2482
|
+
|
|
2483
|
+
if (!process.argv.includes('--debug')) sRep.stop(chalk.green('✅ Evidencia subida exitosamente.'));
|
|
2484
|
+
}
|
|
2485
|
+
} catch (e) {
|
|
2486
|
+
if (process.argv.includes('--debug')) console.log(chalk.yellow(`⚠️ Could not upload evidence: ${e.message}`));
|
|
2487
|
+
if (!process.argv.includes('--debug')) sRep.stop(chalk.yellow('⚠️ Fallo al subir evidencia.'));
|
|
2488
|
+
}
|
|
2489
|
+
} else {
|
|
2490
|
+
console.log(chalk.gray(' >> Saltando subida de evidencia (la misión fue cancelada).'));
|
|
2491
|
+
}
|
|
2492
|
+
|
|
2493
|
+
// ── End Mission — SIEMPRE se llama, independientemente de si la evidencia subió ──
|
|
2494
|
+
try {
|
|
2495
|
+
const { endMission } = await import('../src/arcalityClient.mjs');
|
|
2496
|
+
await endMission(mission.mission_id, missionResult || 'failed', finalUsageData, finalFailReason, finalReportUrl, finalFailReasoning);
|
|
2497
|
+
if (process.argv.includes('--debug')) console.log(chalk.gray(`[DEBUG] endMission called → result: ${missionResult}, failReason: ${finalFailReason}`));
|
|
2498
|
+
} catch (e) {
|
|
2499
|
+
if (process.argv.includes('--debug')) console.log(chalk.yellow(`⚠️ Could not end mission in backend: ${e.message}`));
|
|
2500
|
+
}
|
|
2501
|
+
|
|
2502
|
+
if (globalReportProcess) {
|
|
2503
|
+
try {
|
|
2504
|
+
if (process.platform === "win32") {
|
|
2505
|
+
spawn("taskkill", ["/pid", globalReportProcess.pid, "/f", "/t"], { stdio: 'ignore', windowsHide: true });
|
|
2506
|
+
} else {
|
|
2507
|
+
globalReportProcess.kill();
|
|
2508
|
+
}
|
|
2509
|
+
} catch (e) { /* ignore */ }
|
|
2510
|
+
globalReportProcess = null;
|
|
2511
|
+
}
|
|
2512
|
+
|
|
1968
2513
|
if (!batchMode && fs.existsSync(arcalityPath)) {
|
|
1969
2514
|
console.log(chalk.blue(`🌐 Abriendo Reporte de Arcality: ${arcalityPath}`));
|
|
1970
|
-
|
|
1971
|
-
exec(`start "" "${arcalityPath}"`);
|
|
1972
|
-
} else {
|
|
1973
|
-
exec(`open "${arcalityPath}"`);
|
|
1974
|
-
}
|
|
2515
|
+
openLocalFile(arcalityPath);
|
|
1975
2516
|
} else if (!fs.existsSync(arcalityPath)) {
|
|
1976
2517
|
console.log(chalk.yellow(`⚠️ Reporte no generado (la prueba pudo haber fallado antes de completarse).`));
|
|
1977
2518
|
}
|
|
1978
|
-
|
|
1979
|
-
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
1980
|
-
|
|
1981
|
-
// Ping project after report
|
|
1982
|
-
await pingProject(
|
|
1983
|
-
process.env.ARCALITY_PROJECT_ID,
|
|
1984
|
-
process.env.ARCALITY_API_KEY
|
|
1985
|
-
);
|
|
1986
|
-
|
|
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);
|
|
1990
|
-
|
|
2519
|
+
|
|
2520
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
2521
|
+
|
|
2522
|
+
// Ping project after report
|
|
2523
|
+
await pingProject(
|
|
2524
|
+
process.env.ARCALITY_PROJECT_ID,
|
|
2525
|
+
process.env.ARCALITY_API_KEY
|
|
2526
|
+
);
|
|
2527
|
+
|
|
2528
|
+
// Limpiar handlers del post-proceso ANTES de esperar Enter, ya que toda la telemetría y reportes ya finalizaron
|
|
2529
|
+
process.removeListener('SIGINT', postProcessSigint);
|
|
2530
|
+
process.removeListener('SIGTERM', postProcessSigint);
|
|
2531
|
+
|
|
1991
2532
|
if (!batchMode) {
|
|
2533
|
+
await waitForEnter('Presiona Enter para regresar al menu...', {
|
|
2534
|
+
pauseOnResolve: skipMenu,
|
|
2535
|
+
restoreRawMode: !skipMenu
|
|
2536
|
+
});
|
|
2537
|
+
}
|
|
2538
|
+
|
|
2539
|
+
if (false && !batchMode) {
|
|
1992
2540
|
// Esperar Enter para regresar al menú
|
|
1993
2541
|
// Usamos escucha directa de stdin (raw mode) porque @clack/text
|
|
1994
2542
|
// no procesa Enter correctamente cuando setRawMode(true) está activo.
|
|
@@ -2030,22 +2578,22 @@ async function main() {
|
|
|
2030
2578
|
process.stdin.on('data', onKey);
|
|
2031
2579
|
});
|
|
2032
2580
|
}
|
|
2033
|
-
|
|
2034
|
-
if (skipMenu) {
|
|
2035
|
-
outro(chalk.cyan('¡Misión terminada! Hasta luego.'));
|
|
2036
|
-
return;
|
|
2037
|
-
}
|
|
2038
|
-
|
|
2039
|
-
agentMode = false;
|
|
2040
|
-
prompt = "";
|
|
2041
|
-
firstRun = false;
|
|
2042
|
-
}
|
|
2043
|
-
}
|
|
2044
|
-
firstRun = false;
|
|
2045
|
-
}
|
|
2046
|
-
}
|
|
2047
|
-
|
|
2048
|
-
main().catch(err => {
|
|
2049
|
-
console.error(err);
|
|
2050
|
-
process.exit(1);
|
|
2051
|
-
});
|
|
2581
|
+
|
|
2582
|
+
if (skipMenu) {
|
|
2583
|
+
outro(chalk.cyan('¡Misión terminada! Hasta luego.'));
|
|
2584
|
+
return;
|
|
2585
|
+
}
|
|
2586
|
+
|
|
2587
|
+
agentMode = false;
|
|
2588
|
+
prompt = "";
|
|
2589
|
+
firstRun = false;
|
|
2590
|
+
}
|
|
2591
|
+
}
|
|
2592
|
+
firstRun = false;
|
|
2593
|
+
}
|
|
2594
|
+
}
|
|
2595
|
+
|
|
2596
|
+
main().catch(err => {
|
|
2597
|
+
console.error(err);
|
|
2598
|
+
process.exit(1);
|
|
2599
|
+
});
|