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