@arcadialdev/arcality 3.0.0 → 3.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -1
- package/public/ArcalityImagen.png +0 -0
- package/public/logo-arcadial-blanco.png +0 -0
- package/public/logo.png +0 -0
- package/scripts/gen-and-run.mjs +950 -176
- package/scripts/init.mjs +221 -178
- package/scripts/rebrand-report.mjs +16 -2
- package/tests/_helpers/ArcalityReporter.js +1237 -712
- package/tests/_helpers/agentic-runner.bundle.spec.js +285 -52
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,10 +159,613 @@ 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
770
|
|
|
166
771
|
let version = 'unknown';
|
|
@@ -440,24 +1045,47 @@ async function main() {
|
|
|
440
1045
|
initialEnv = parseDotEnvFile(path.join(PROJECT_ROOT, '.env'));
|
|
441
1046
|
}
|
|
442
1047
|
|
|
443
|
-
const argv = process.argv.slice(2);
|
|
444
|
-
let initialDiscoverPath = null;
|
|
445
|
-
let initialSmartMode = false;
|
|
446
|
-
let initialAgentMode = false;
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
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
|
|
461
1089
|
|
|
462
1090
|
const promptArg = argv.join(" ").trim();
|
|
463
1091
|
let firstRun = true;
|
|
@@ -466,59 +1094,116 @@ async function main() {
|
|
|
466
1094
|
setupEnvironment();
|
|
467
1095
|
|
|
468
1096
|
while (true) {
|
|
469
|
-
let prompt = firstRun ? promptArg : "";
|
|
470
|
-
let agentMode = firstRun ? initialAgentMode : false;
|
|
471
|
-
let smartMode = firstRun ? initialSmartMode : false;
|
|
472
|
-
let discoverPath = firstRun ? initialDiscoverPath : null;
|
|
473
|
-
|
|
474
|
-
let
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
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 {
|
|
482
1115
|
// ── Build menu options (single config mode) ──
|
|
483
1116
|
const missionsDir = process.env.MISSIONS_DIR;
|
|
484
|
-
const
|
|
485
|
-
? fs.readdirSync(missionsDir).filter(f => f.endsWith('.yaml') || f.endsWith('.yml'))
|
|
486
|
-
: [];
|
|
1117
|
+
const savedMissionCollections = listMissionCollections(missionsDir);
|
|
487
1118
|
|
|
488
1119
|
// Check for YAML files in the yaml output dir from arcality.config
|
|
489
|
-
let
|
|
1120
|
+
let projectMissionCollections = [];
|
|
490
1121
|
if (projectConfig) {
|
|
491
1122
|
const yamlDir = getYamlOutputDir(projectConfig, ORIGINAL_CWD);
|
|
492
1123
|
if (fs.existsSync(yamlDir)) {
|
|
493
|
-
|
|
1124
|
+
projectMissionCollections = listMissionCollections(yamlDir);
|
|
494
1125
|
}
|
|
495
1126
|
}
|
|
496
1127
|
|
|
497
1128
|
// Root-level YAML templates
|
|
498
|
-
const
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
{ label: '
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
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({
|
|
510
1163
|
message: '¿Qué te gustaría hacer?',
|
|
511
|
-
options
|
|
512
|
-
});
|
|
1164
|
+
options: menuOptions
|
|
1165
|
+
});
|
|
513
1166
|
|
|
514
1167
|
if (isCancel(action) || action === 'exit') {
|
|
515
1168
|
outro(chalk.cyan('¡Hasta luego!'));
|
|
516
1169
|
break;
|
|
517
1170
|
}
|
|
518
1171
|
|
|
519
|
-
if (action === 'agent') {
|
|
520
|
-
agentMode = true;
|
|
521
|
-
} 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') {
|
|
522
1207
|
// Launch arcality init in the user's project directory
|
|
523
1208
|
const initScript = path.join(PROJECT_ROOT, 'scripts', 'init.mjs');
|
|
524
1209
|
try {
|
|
@@ -546,49 +1231,101 @@ async function main() {
|
|
|
546
1231
|
await new Promise(r => setTimeout(r, 3000));
|
|
547
1232
|
}
|
|
548
1233
|
continue;
|
|
549
|
-
} else if (action === 'launch_saved') {
|
|
550
|
-
const
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
const
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
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 }))
|
|
565
1265
|
});
|
|
566
1266
|
if (isCancel(sel)) continue;
|
|
567
|
-
|
|
568
|
-
const yamlContent = fs.readFileSync(path.join(
|
|
569
|
-
const
|
|
570
|
-
prompt =
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
message: 'Elige el archivo YAML:',
|
|
575
|
-
options: rootYamlFiles.map(f => ({ label: f, value: f }))
|
|
576
|
-
});
|
|
577
|
-
if (isCancel(sel)) continue;
|
|
578
|
-
|
|
579
|
-
const yamlContent = fs.readFileSync(path.join(PROJECT_ROOT, sel), 'utf8');
|
|
580
|
-
const data = loadYaml(yamlContent);
|
|
581
|
-
prompt = data.mision || data.prompt || yamlContent;
|
|
582
|
-
agentMode = true;
|
|
583
|
-
}
|
|
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
|
+
}
|
|
584
1274
|
} catch (e) {
|
|
585
1275
|
console.error("Menu error:", e);
|
|
586
1276
|
break;
|
|
587
|
-
}
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
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) {
|
|
592
1329
|
if (!prompt || prompt.trim().length < 4) {
|
|
593
1330
|
const p = await text({
|
|
594
1331
|
message: chalk.cyan('🤖 Misión para Arcality (Requerida):'),
|
|
@@ -1026,36 +1763,66 @@ async function main() {
|
|
|
1026
1763
|
// ── Ping Project ──
|
|
1027
1764
|
await pingProject(finalProjectId, process.env.ARCALITY_API_KEY);
|
|
1028
1765
|
|
|
1029
|
-
// ── Save Mission YAML ──
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
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') {
|
|
1039
1779
|
const name = await text({
|
|
1040
1780
|
message: 'Nombre de la misión (ej., login_valido):',
|
|
1041
1781
|
placeholder: 'my_mission',
|
|
1042
1782
|
validate: v => v.length < 3 ? 'Nombre muy corto' : undefined
|
|
1043
1783
|
});
|
|
1044
1784
|
|
|
1045
|
-
if (!isCancel(name)) {
|
|
1046
|
-
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();
|
|
1047
1787
|
|
|
1048
1788
|
// Save to missions dir (existing flow)
|
|
1049
1789
|
const missionsDir = process.env.MISSIONS_DIR;
|
|
1050
|
-
const
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
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';
|
|
1054
1810
|
|
|
1055
1811
|
const smartYamlPath = path.join(process.env.CONTEXT_DIR, 'last-mission-smart.yaml');
|
|
1056
|
-
if (fs.existsSync(smartYamlPath)) {
|
|
1057
|
-
|
|
1058
|
-
|
|
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
|
+
}
|
|
1059
1826
|
|
|
1060
1827
|
// Internal fallback backup for the agent
|
|
1061
1828
|
fs.writeFileSync(yamlPathMissions, yamlData);
|
|
@@ -1063,7 +1830,9 @@ async function main() {
|
|
|
1063
1830
|
// Primary save to user's specified directory from arcality.config
|
|
1064
1831
|
if (projectConfig) {
|
|
1065
1832
|
const yamlOutputDir = ensureYamlOutputDir(projectConfig, ORIGINAL_CWD);
|
|
1066
|
-
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`);
|
|
1067
1836
|
fs.writeFileSync(yamlPathOutput, yamlData);
|
|
1068
1837
|
note(chalk.green(`✅ Misión guardada en: ${yamlPathOutput}`), 'Persistencia Arcality');
|
|
1069
1838
|
} else {
|
|
@@ -1121,8 +1890,11 @@ async function main() {
|
|
|
1121
1890
|
try {
|
|
1122
1891
|
const ctxDir = process.env.CONTEXT_DIR;
|
|
1123
1892
|
if (fs.existsSync(ctxDir)) {
|
|
1124
|
-
const files = fs.readdirSync(ctxDir);
|
|
1125
|
-
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();
|
|
1126
1898
|
if (logFiles.length > 0) {
|
|
1127
1899
|
const lastLog = path.join(ctxDir, logFiles[logFiles.length - 1]);
|
|
1128
1900
|
const logData = JSON.parse(fs.readFileSync(lastLog, 'utf8'));
|
|
@@ -1193,16 +1965,16 @@ async function main() {
|
|
|
1193
1965
|
globalReportProcess = null;
|
|
1194
1966
|
}
|
|
1195
1967
|
|
|
1196
|
-
if (fs.existsSync(arcalityPath)) {
|
|
1197
|
-
console.log(chalk.blue(`🌐 Abriendo Reporte de Arcality: ${arcalityPath}`));
|
|
1198
|
-
if (process.platform === "win32") {
|
|
1199
|
-
exec(`start "" "${arcalityPath}"`);
|
|
1200
|
-
} else {
|
|
1201
|
-
exec(`open "${arcalityPath}"`);
|
|
1202
|
-
}
|
|
1203
|
-
} else {
|
|
1204
|
-
console.log(chalk.yellow(`⚠️ Reporte no generado (la prueba pudo haber fallado antes de completarse).`));
|
|
1205
|
-
}
|
|
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
|
+
}
|
|
1206
1978
|
|
|
1207
1979
|
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
1208
1980
|
|
|
@@ -1216,46 +1988,48 @@ async function main() {
|
|
|
1216
1988
|
process.removeListener('SIGINT', postProcessSigint);
|
|
1217
1989
|
process.removeListener('SIGTERM', postProcessSigint);
|
|
1218
1990
|
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
const
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
process.stdin.setRawMode
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
process.stdin.setRawMode
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
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
|
+
}
|
|
1259
2033
|
|
|
1260
2034
|
if (skipMenu) {
|
|
1261
2035
|
outro(chalk.cyan('¡Misión terminada! Hasta luego.'));
|