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