@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/scripts/init.mjs CHANGED
@@ -7,18 +7,17 @@
7
7
  // 4. Create project via POST /api/v1/projects
8
8
  // 5. Write arcality.config
9
9
 
10
- import 'dotenv/config';
11
- import fs from 'node:fs';
12
- import path from 'node:path';
13
- import { fileURLToPath } from 'url';
14
- import { intro, outro, text, password as passwordPrompt, spinner, note, isCancel, cancel, confirm } from '@clack/prompts';
15
- import chalk from 'chalk';
16
- import figlet from 'figlet';
17
- import {
18
- configExists,
19
- loadProjectConfig,
20
- saveProjectConfig,
21
- createConfig,
10
+ import 'dotenv/config';
11
+ import fs from 'node:fs';
12
+ import path from 'node:path';
13
+ import { fileURLToPath } from 'url';
14
+ import { intro, outro, text, password as passwordPrompt, spinner, note, isCancel, cancel, confirm } from '@clack/prompts';
15
+ import chalk from 'chalk';
16
+ import {
17
+ configExists,
18
+ loadProjectConfig,
19
+ saveProjectConfig,
20
+ createConfig,
22
21
  } from '../src/configManager.mjs';
23
22
  import { getApiUrl } from '../src/configLoader.mjs';
24
23
 
@@ -26,14 +25,14 @@ const __filename = fileURLToPath(import.meta.url);
26
25
  const __dirname = path.dirname(__filename);
27
26
  const PACKAGE_ROOT = process.env.ARCALITY_ROOT || path.resolve(__dirname, '..');
28
27
 
29
- // ── Helpers ──
28
+ // ── Helpers ──
30
29
 
31
- function getVersion() {
32
- try {
33
- const pkg = JSON.parse(fs.readFileSync(path.join(PACKAGE_ROOT, 'package.json'), 'utf8'));
34
- return pkg.version || 'unknown';
35
- } catch {
36
- return 'unknown';
30
+ function getVersion() {
31
+ try {
32
+ const pkg = JSON.parse(fs.readFileSync(path.join(PACKAGE_ROOT, 'package.json'), 'utf8'));
33
+ return pkg.version || 'unknown';
34
+ } catch {
35
+ return 'unknown';
37
36
  }
38
37
  }
39
38
 
@@ -56,75 +55,93 @@ function detectFramework(projectRoot) {
56
55
  }
57
56
  }
58
57
 
59
- function isValidUrl(url) {
60
- try {
61
- const u = new URL(url);
62
- return u.protocol === 'http:' || u.protocol === 'https:';
63
- } catch {
64
- return false;
65
- }
66
- }
58
+ function isValidUrl(url) {
59
+ try {
60
+ const u = new URL(url);
61
+ return u.protocol === 'http:' || u.protocol === 'https:';
62
+ } catch {
63
+ return false;
64
+ }
65
+ }
66
+
67
+ function showInitBanner(version, projectRoot) {
68
+ const sentinelAscii = `
69
+ █████ ██████ ██████ █████ ██ ██ ████████ ██ ██
70
+ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
71
+ ███████ ██████ ██ ███████ ██ ██ ██ ████
72
+ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
73
+ ██ ██ ██ ██ ██████ ██ ██ ███████ ██ ██ ██`;
74
+
75
+ intro(chalk.gray(sentinelAscii));
76
+
77
+ note(
78
+ chalk.gray('◉ Modo de Configuración: ') + chalk.bold.white('ACTIVO') + '\n' +
79
+ chalk.gray('◉ Enlace con Backend: ') + chalk.bold.white('PREPARANDO') + '\n' +
80
+ chalk.gray('◉ Generador de Contexto: ') + chalk.bold.white('DISPONIBLE') + '\n' +
81
+ chalk.gray('─'.repeat(50)) + '\n' +
82
+ chalk.gray('Versión del Sistema: ') + chalk.white(`v${version}`),
83
+ 'Estado Central de Arcality'
84
+ );
85
+
86
+ note(
87
+ chalk.gray('Nodo Objetivo: ') + chalk.white(process.env.NODE_ENV ?? 'development') + '\n' +
88
+ chalk.gray('Proyecto Local: ') + chalk.white(projectRoot) + '\n' +
89
+ chalk.gray('Comando: ') + chalk.white('arcality init'),
90
+ 'Inicialización del Proyecto'
91
+ );
92
+ }
67
93
 
68
94
  // ── Main ──
69
95
 
70
- async function main() {
71
- const version = getVersion();
72
- const projectRoot = process.cwd();
73
-
74
- console.clear();
75
- const logo = figlet.textSync('Arcality', { font: 'Standard' });
76
- intro(chalk.cyanBright(logo));
77
-
78
- note(
79
- chalk.magentaBright('Project Initialization') + '\n' +
80
- chalk.gray('─'.repeat(50)) + '\n' +
81
- chalk.bold.white('📦 Version: ') + chalk.yellow(version) + '\n' +
82
- chalk.bold.white('📁 Project: ') + chalk.yellow(projectRoot),
83
- 'arcality init'
84
- );
96
+ async function main() {
97
+ const version = getVersion();
98
+ const projectRoot = process.cwd();
99
+
100
+ console.clear();
101
+ showInitBanner(version, projectRoot);
85
102
 
86
103
  // ── Check for existing config ──
87
- if (configExists(projectRoot)) {
88
- const existing = loadProjectConfig(projectRoot);
89
- if (existing) {
90
- note(
91
- chalk.yellow('⚠️ An arcality.config already exists in this project.') + '\n\n' +
92
- chalk.white(' Project: ') + chalk.cyan(existing.project?.name || 'unknown') + '\n' +
93
- chalk.white(' Base URL: ') + chalk.cyan(existing.project?.baseUrl || 'unknown') + '\n' +
94
- chalk.white(' Project ID: ') + chalk.gray(existing.projectId || 'unknown'),
95
- 'Existing Configuration'
96
- );
97
-
98
- const overwrite = await confirm({
99
- message: 'Do you want to reconfigure? This will create a NEW project in backend.',
100
- });
101
-
102
- if (isCancel(overwrite) || !overwrite) {
103
- outro(chalk.cyan('Configuration unchanged. Use `arcality run` to start.'));
104
- return;
105
- }
106
- }
107
- }
104
+ if (configExists(projectRoot)) {
105
+ const existing = loadProjectConfig(projectRoot);
106
+ if (existing) {
107
+ note(
108
+ chalk.yellow('⚠️ Ya existe un arcality.config en este proyecto.') + '\n\n' +
109
+ chalk.white(' Proyecto: ') + chalk.cyan(existing.project?.name || 'unknown') + '\n' +
110
+ chalk.white(' Base URL: ') + chalk.cyan(existing.project?.baseUrl || 'unknown') + '\n' +
111
+ chalk.white(' ID del Proyecto: ') + chalk.gray(existing.projectId || 'desconocido'),
112
+ 'Configuración Existente'
113
+ );
114
+
115
+ const overwrite = await confirm({
116
+ message: '¿Deseas reconfigurar? Esto creará un NUEVO proyecto en backend.',
117
+ });
118
+
119
+ if (isCancel(overwrite) || !overwrite) {
120
+ outro(chalk.cyan('Configuración sin cambios. Usa `arcality run` para iniciar.'));
121
+ return;
122
+ }
123
+ }
124
+ }
108
125
 
109
126
  // ── Step 1: API Key ──
110
127
  const apiKey = await text({
111
- message: chalk.cyan('🔑 Enter your Arcality API Key:'),
128
+ message: chalk.cyan('🔑 Ingresa tu API Key de Arcality:'),
112
129
  placeholder: 'arc_live_xxxxx',
113
130
  validate: (v) => {
114
- if (!v || !v.trim()) return 'API Key is required.';
115
- if (!v.startsWith('arc_')) return 'API Key must start with "arc_"';
131
+ if (!v || !v.trim()) return 'La API Key es obligatoria.';
132
+ if (!v.startsWith('arc_')) return 'La API Key debe iniciar con "arc_"';
116
133
  },
117
134
  });
118
135
 
119
136
  if (isCancel(apiKey)) {
120
- cancel('Setup cancelled.');
137
+ cancel('Configuración cancelada.');
121
138
  process.exit(0);
122
139
  }
123
140
 
124
141
  // ── Step 2: Validate API Key (uses internal API URL) ──
125
142
  const apiUrl = getApiUrl();
126
143
  const s = spinner();
127
- s.start('Validating API Key...');
144
+ s.start('Validando API Key...');
128
145
 
129
146
  let organizationId = null;
130
147
 
@@ -140,12 +157,12 @@ async function main() {
140
157
  if (!res.ok) {
141
158
  const errText = await res.text().catch(() => '');
142
159
  const errorMap = {
143
- 401: '❌ Invalid API Key. Please check your key and try again.',
144
- 403: `❌ Authentication Failed (HTTP 403). Server says: ${errText.slice(0, 150)}`,
145
- };
146
- const msg = errorMap[res.status] || `❌ Server error (HTTP ${res.status}): ${errText.slice(0, 150)}`;
147
- s.stop(chalk.red(msg));
148
- console.log(chalk.yellow('\n Press Enter to return to menu...'));
160
+ 401: '❌ API Key inválida. Revísala e inténtalo de nuevo.',
161
+ 403: `❌ Autenticación fallida (HTTP 403). El servidor respondió: ${errText.slice(0, 150)}`,
162
+ };
163
+ const msg = errorMap[res.status] || `❌ Error del servidor (HTTP ${res.status}): ${errText.slice(0, 150)}`;
164
+ s.stop(chalk.red(msg));
165
+ console.log(chalk.yellow('\n Presiona Enter para volver al menú...'));
149
166
  // Salida limpia (código 0) para que el menú padre no interprete esto como crash
150
167
  await new Promise(r => setTimeout(r, 3000));
151
168
  process.exit(0);
@@ -154,77 +171,77 @@ async function main() {
154
171
  const data = await res.json();
155
172
  organizationId = data.organizationId || data.organization_id || null;
156
173
 
157
- s.stop(chalk.green('✅ API Key validated successfully'));
174
+ s.stop(chalk.green('✅ API Key validada correctamente'));
158
175
 
159
176
  if (data.plan) {
160
177
  note(
161
- chalk.bold.white('📋 Plan: ') + chalk.cyan(data.plan) + '\n' +
162
- chalk.bold.white('🏢 Org: ') + chalk.cyan(organizationId || 'N/A'),
163
- 'Account Info'
178
+ chalk.bold.white('📋 Plan: ') + chalk.cyan(data.plan) + '\n' +
179
+ chalk.bold.white('🏢 Organización: ') + chalk.cyan(organizationId || 'N/A'),
180
+ 'Información de la Cuenta'
164
181
  );
165
182
  }
166
183
  } catch (err) {
167
- s.stop(chalk.red(`❌ Could not connect to Arcality backend: ${err.message}`));
168
- console.log(chalk.yellow(' Make sure the backend is running and accessible.'));
184
+ s.stop(chalk.red(`❌ No se pudo conectar al backend de Arcality: ${err.message}`));
185
+ console.log(chalk.yellow(' Asegúrate de que el backend esté activo y accesible.'));
169
186
  await new Promise(r => setTimeout(r, 3000));
170
187
  process.exit(0);
171
188
  }
172
189
 
173
190
  // ── Step 3: Project details ──
174
191
  const projectName = await text({
175
- message: chalk.cyan('📋 Project name:'),
176
- placeholder: 'My Portal QA',
177
- validate: (v) => {
178
- if (!v || v.trim().length < 2) return 'Project name is required (min 2 chars).';
179
- },
180
- });
181
- if (isCancel(projectName)) { cancel('Setup cancelled.'); process.exit(0); }
182
-
183
- const baseUrl = await text({
184
- message: chalk.cyan('🌐 Base URL of the application to test:'),
185
- placeholder: 'https://staging.example.com',
186
- validate: (v) => {
187
- if (!v || !v.trim()) return 'Base URL is required.';
188
- if (!isValidUrl(v)) return 'Invalid URL. Use http:// or https://';
189
- },
190
- });
191
- if (isCancel(baseUrl)) { cancel('Setup cancelled.'); process.exit(0); }
192
-
193
- const hasLogin = await confirm({
194
- message: chalk.cyan('🔐 Does your application require a Login/Authentication?'),
195
- });
196
- if (isCancel(hasLogin)) { cancel('Setup cancelled.'); process.exit(0); }
192
+ message: chalk.cyan('📋 Nombre del proyecto:'),
193
+ placeholder: 'Mi Portal QA',
194
+ validate: (v) => {
195
+ if (!v || v.trim().length < 2) return 'El nombre del proyecto es obligatorio (mínimo 2 caracteres).';
196
+ },
197
+ });
198
+ if (isCancel(projectName)) { cancel('Configuración cancelada.'); process.exit(0); }
199
+
200
+ const baseUrl = await text({
201
+ message: chalk.cyan('🌐 URL base de la aplicación a probar:'),
202
+ placeholder: 'https://staging.example.com',
203
+ validate: (v) => {
204
+ if (!v || !v.trim()) return 'La URL base es obligatoria.';
205
+ if (!isValidUrl(v)) return 'URL inválida. Usa http:// o https://';
206
+ },
207
+ });
208
+ if (isCancel(baseUrl)) { cancel('Configuración cancelada.'); process.exit(0); }
209
+
210
+ const hasLogin = await confirm({
211
+ message: chalk.cyan('🔐 ¿Tu aplicación requiere inicio de sesión/autenticación?'),
212
+ });
213
+ if (isCancel(hasLogin)) { cancel('Configuración cancelada.'); process.exit(0); }
197
214
 
198
215
  let username = '';
199
216
  let password = '';
200
217
 
201
218
  if (hasLogin) {
202
- username = await text({
203
- message: chalk.cyan('👤 Login username / email:'),
204
- validate: (v) => {
205
- if (!v || !v.trim()) return 'Username is required.';
206
- },
207
- });
208
- if (isCancel(username)) { cancel('Setup cancelled.'); process.exit(0); }
209
-
210
- password = await passwordPrompt({
211
- message: chalk.cyan('🔒 Login password:'),
212
- validate: (v) => {
213
- if (!v || !v.length) return 'Password is required.';
214
- },
215
- });
216
- if (isCancel(password)) { cancel('Setup cancelled.'); process.exit(0); }
219
+ username = await text({
220
+ message: chalk.cyan('👤 Usuario o correo de acceso:'),
221
+ validate: (v) => {
222
+ if (!v || !v.trim()) return 'El usuario es obligatorio.';
223
+ },
224
+ });
225
+ if (isCancel(username)) { cancel('Configuración cancelada.'); process.exit(0); }
226
+
227
+ password = await passwordPrompt({
228
+ message: chalk.cyan('🔒 Contraseña de acceso:'),
229
+ validate: (v) => {
230
+ if (!v || !v.length) return 'La contraseña es obligatoria.';
231
+ },
232
+ });
233
+ if (isCancel(password)) { cancel('Configuración cancelada.'); process.exit(0); }
217
234
  }
218
235
 
219
236
  // ── Step 4: Detect framework ──
220
237
  const frameworkDetected = detectFramework(projectRoot);
221
238
  if (frameworkDetected) {
222
- console.log(chalk.gray(` 🛠️ Framework detected: ${chalk.magenta(frameworkDetected)}`));
239
+ console.log(chalk.gray(` 🛠️ Framework detectado: ${chalk.magenta(frameworkDetected)}`));
223
240
  }
224
241
 
225
242
  // ── Step 5: Create project in backend ──
226
243
  const sCreate = spinner();
227
- sCreate.start('Creating project in Arcality backend...');
244
+ sCreate.start('Creando proyecto en el backend de Arcality...');
228
245
 
229
246
  let projectId = null;
230
247
  try {
@@ -254,8 +271,8 @@ async function main() {
254
271
  // If response is HTML (e.g. Cloudflare 521), show a clean message
255
272
  const isHtml = errText.trim().startsWith('<');
256
273
  const displayError = isHtml
257
- ? `The Arcality backend server is temporarily unavailable (HTTP ${res.status}). The project will be saved locally only.`
258
- : `HTTP ${res.status}: ${errText.slice(0, 200)}`;
274
+ ? `El servidor backend de Arcality no está disponible temporalmente (HTTP ${res.status}). El proyecto se guardará solo de forma local.`
275
+ : `HTTP ${res.status}: ${errText.slice(0, 200)}`;
259
276
 
260
277
  sCreate.stop(chalk.yellow(`⚠️ ${displayError}`));
261
278
  // Don't exit — write the config with a local-only project ID so user can still run
@@ -264,12 +281,12 @@ async function main() {
264
281
  const created = await res.json();
265
282
  projectId = created.id || created.Id;
266
283
  organizationId = organizationId || created.organizationId || created.organization_id || null;
267
- sCreate.stop(chalk.green(`✅ Project created: "${projectName.trim()}" (${projectId})`));
268
- }
269
- } catch (err) {
270
- sCreate.stop(chalk.yellow(`⚠️ Could not reach Arcality server: ${err.message}. Continuing in local mode.`));
271
- projectId = projectId || `local_${Date.now()}`;
272
- }
284
+ sCreate.stop(chalk.green(`✅ Proyecto creado: "${projectName.trim()}" (${projectId})`));
285
+ }
286
+ } catch (err) {
287
+ sCreate.stop(chalk.yellow(`⚠️ No se pudo conectar con el servidor de Arcality: ${err.message}. Se continuará en modo local.`));
288
+ projectId = projectId || `local_${Date.now()}`;
289
+ }
273
290
 
274
291
  // ── Step 6: Write arcality.config ──
275
292
  const config = createConfig({
@@ -286,60 +303,86 @@ async function main() {
286
303
 
287
304
  saveProjectConfig(config, projectRoot);
288
305
 
289
- // ── Step 7: Custom QA Context ──
290
- const wantsContext = await confirm({
291
- message: chalk.cyan('📄 Do you want to generate a local QA Context file (.arcality/qa-context.md) to teach the AI specific business rules?'),
292
- });
293
-
294
- if (!isCancel(wantsContext) && wantsContext) {
295
- const arcalityDir = path.join(projectRoot, '.arcality');
296
- if (!fs.existsSync(arcalityDir)) {
306
+ // ── Step 7: Custom QA Context ──
307
+ note(
308
+ chalk.white('El archivo ') + chalk.cyan('.arcality/qa-context.md') + chalk.white(' se inyecta al prompt del agente antes de cada misión.') + '\n' +
309
+ chalk.white('Úsalo para reglas del negocio, navegación especial y anti-patrones que Arcality deba respetar siempre.'),
310
+ 'QA Context Local'
311
+ );
312
+
313
+ const wantsContext = await confirm({
314
+ message: chalk.cyan('📄 ¿Deseas generar un archivo local QA Context (.arcality/qa-context.md) para enseñarle al agente reglas del negocio?'),
315
+ });
316
+
317
+ if (!isCancel(wantsContext) && wantsContext) {
318
+ const arcalityDir = path.join(projectRoot, '.arcality');
319
+ if (!fs.existsSync(arcalityDir)) {
297
320
  fs.mkdirSync(arcalityDir, { recursive: true });
298
321
  }
299
322
 
300
- const qaContextPath = path.join(arcalityDir, 'qa-context.md');
301
- const qaTemplate = `# 🧠 Arcality QA Context
302
- Este archivo es leído automáticamente por tu Agente Arcality antes de cada misión.
303
- Siéntete libre de borrar, modificar o agregar información para enseñarle a la IA las reglas exclusivas de tu sitio web.
304
-
305
- ## 💼 Reglas Generales del Dominio
306
- > (Ejemplo: Formatos de documentos, restricciones, etc.)
307
- -
308
-
309
- ## 🧭 Navegación y UI Especial
310
- > (Ejemplo: Tiempos de espera, clics dobles necesarios, etc.)
311
- -
312
-
313
- ## 🛑 Casos Prohibidos y Anti-patrones
314
- > (Cosas que el agente NUNCA debe intentar)
315
- -
316
- `;
317
- if (!fs.existsSync(qaContextPath)) {
318
- fs.writeFileSync(qaContextPath, qaTemplate);
319
- }
320
- }
323
+ const qaContextPath = path.join(arcalityDir, 'qa-context.md');
324
+ const qaTemplate = `# Arcality QA Context
325
+ <!--
326
+ Este archivo se inyecta antes de cada mision.
327
+ Escribe reglas cortas, especificas y accionables.
328
+ Usa un bullet por regla real.
329
+ Los bullets que empiecen con "Ejemplo:" se ignoran hasta que los reemplaces.
330
+ -->
331
+
332
+ ## Reglas del Dominio
333
+ <!-- Restricciones de negocio, validaciones duras, formatos, limites -->
334
+ - Ejemplo: Un usuario no puede registrar mas de 24 horas en un mismo dia.
335
+ - Ejemplo: El campo "Numero de empleado" debe conservar ceros a la izquierda.
336
+
337
+ ## Navegacion y UI Especial
338
+ <!-- Pasos raros de la interfaz, cargas lentas, modales, menus no obvios -->
339
+ - Ejemplo: Despues de elegir un proyecto, espera a que cargue la tabla antes de registrar horas.
340
+ - Ejemplo: El menu de acciones siempre se abre desde el icono ":" de la primera fila.
341
+
342
+ ## Casos Prohibidos y Anti-patrones
343
+ <!-- Cosas que Arcality nunca debe intentar -->
344
+ - Ejemplo: No uses "Continuar con Microsoft".
345
+ - Ejemplo: No cierres el modal con Escape; usa el boton "Cancelar".
346
+
347
+ ## Datos y Credenciales de Prueba
348
+ <!-- Usuarios de prueba, ambientes, datos obligatorios o datos que deben evitarse -->
349
+ - Ejemplo: Usa el usuario QA_TIMESHEET_01 para pruebas de captura diaria.
350
+ - Ejemplo: No reutilices folios ya aprobados.
351
+
352
+ ## Resultado Esperado y Validaciones Clave
353
+ <!-- Como saber que la mision realmente termino bien -->
354
+ - Ejemplo: La prueba termina cuando el registro aparece en la tabla "Timesheet - Hoy" de /welcome.
355
+ - Ejemplo: Si aparece la leyenda "Expirado" en el proyecto, la prueba sigue siendo valida.
356
+ `;
357
+ if (!fs.existsSync(qaContextPath)) {
358
+ fs.writeFileSync(qaContextPath, qaTemplate);
359
+ note(chalk.green(`✅ QA Context creado en: ${qaContextPath}`), 'Contexto Guardado');
360
+ } else {
361
+ note(chalk.cyan(`ℹ️ QA Context ya existente: ${qaContextPath}`), 'Contexto Detectado');
362
+ }
363
+ }
321
364
 
322
365
  // ── Summary ──
323
366
  note(
324
- chalk.green('✅ arcality.config created successfully!') + '\n\n' +
325
- chalk.bold.white(' Project: ') + chalk.cyan(config.project.name) + '\n' +
326
- chalk.bold.white(' Base URL: ') + chalk.cyan(config.project.baseUrl) + '\n' +
327
- chalk.bold.white(' Framework: ') + chalk.magenta(config.project.frameworkDetected || 'N/A') + '\n' +
328
- chalk.bold.white(' Project ID: ') + chalk.gray(config.projectId) + '\n' +
329
- chalk.bold.white(' YAML Dir: ') + chalk.yellow(config.runtime.yamlOutputDir),
330
- 'Configuration Saved'
331
- );
332
-
333
- outro(
334
- chalk.cyanBright('🚀 Ready! Run ') +
335
- chalk.bold.white('arcality run') +
336
- chalk.cyanBright(' or ') +
337
- chalk.bold.white('arcality') +
338
- chalk.cyanBright(' to start testing.')
339
- );
340
- }
341
-
342
- main().catch(err => {
343
- console.error(chalk.red(' ❌ Error during init:'), err.message);
344
- process.exit(1);
345
- });
367
+ chalk.green('✅ arcality.config creado correctamente') + '\n\n' +
368
+ chalk.bold.white(' Proyecto: ') + chalk.cyan(config.project.name) + '\n' +
369
+ chalk.bold.white(' URL Base: ') + chalk.cyan(config.project.baseUrl) + '\n' +
370
+ chalk.bold.white(' Framework: ') + chalk.magenta(config.project.frameworkDetected || 'N/A') + '\n' +
371
+ chalk.bold.white(' ID del Proyecto: ') + chalk.gray(config.projectId) + '\n' +
372
+ chalk.bold.white(' Carpeta de Misiones: ') + chalk.yellow(config.runtime.yamlOutputDir),
373
+ 'Configuración Guardada'
374
+ );
375
+
376
+ outro(
377
+ chalk.cyanBright('🚀 Listo. Ejecuta ') +
378
+ chalk.bold.white('arcality run') +
379
+ chalk.cyanBright(' o ') +
380
+ chalk.bold.white('arcality') +
381
+ chalk.cyanBright(' para comenzar a probar.')
382
+ );
383
+ }
384
+
385
+ main().catch(err => {
386
+ console.error(chalk.red(' ❌ Error durante init:'), err.message);
387
+ process.exit(1);
388
+ });
@@ -93,6 +93,16 @@ function getEffectiveApiKey() {
93
93
  return key;
94
94
  }
95
95
 
96
+ /**
97
+ * Gets the effective Project ID.
98
+ * Priority: arcality.config > ARCALITY_PROJECT_ID env var
99
+ */
100
+ function loadProjectId() {
101
+ const localConfig = loadArcalityConfig();
102
+ if (localConfig?.projectId) return localConfig.projectId;
103
+ return process.env.ARCALITY_PROJECT_ID || null;
104
+ }
105
+
96
106
  // ═══════════════════════════════════════════════════════
97
107
  // PUBLIC API
98
108
  // ═══════════════════════════════════════════════════════
@@ -244,6 +254,54 @@ export async function startMission(prompt, targetUrl) {
244
254
  };
245
255
  }
246
256
 
257
+ /**
258
+ * Fetches missions from the backend API.
259
+ * @param {string} projectId
260
+ * @param {string} tags - Comma separated tags (optional)
261
+ * @returns {Promise<Object>} Contains success flag and missions array
262
+ */
263
+ export async function fetchMissions(projectId, tags) {
264
+ const key = getEffectiveApiKey();
265
+ if (!key) return { success: false, error: 'no_api_key', missions: [] };
266
+
267
+ const apiBase = getEffectiveApiBase();
268
+ if (!apiBase) {
269
+ // MOCK MODE: Return empty array for now since there's no backend
270
+ return { success: true, mode: 'mock', missions: [] };
271
+ }
272
+
273
+ try {
274
+ const url = new URL(`${apiBase}/api/v1/projects/${projectId}/missions`);
275
+ if (tags) {
276
+ url.searchParams.append('tags', tags);
277
+ }
278
+
279
+ const controller = new AbortController();
280
+ const timeout = setTimeout(() => controller.abort(), 10000);
281
+ const res = await fetch(url.toString(), {
282
+ method: 'GET',
283
+ headers: {
284
+ 'x-api-key': key
285
+ },
286
+ signal: controller.signal
287
+ });
288
+ clearTimeout(timeout);
289
+
290
+ if (!res.ok) {
291
+ const errText = await res.text();
292
+ console.warn(`[DEBUG] fetchMissions failed: ${res.status} - ${errText}`);
293
+ return { success: false, error: 'server_error', missions: [] };
294
+ }
295
+
296
+ const data = await res.json();
297
+ return { success: true, missions: data.missions || [] };
298
+ } catch (e) {
299
+ const reason = e?.name === 'AbortError' ? 'timeout (10s)' : (e?.message || 'unknown');
300
+ console.warn(`⚠️ fetchMissions failed (${apiBase}): ${reason}.`);
301
+ return { success: false, error: reason, missions: [] };
302
+ }
303
+ }
304
+
247
305
  /**
248
306
  * Marks a mission as completed.
249
307
  * @param {string} missionId
@@ -281,6 +339,75 @@ function simpleHash(str) {
281
339
  return 'ph_' + Math.abs(hash).toString(36);
282
340
  }
283
341
 
342
+ /**
343
+ * Crea una Task en Azure DevOps cuando se detecta un bug en el portal.
344
+ * Si la organización no tiene la integración configurada (HTTP 400) se registra
345
+ * sólo en modo DEBUG y NO interrumpe el flujo del test runner.
346
+ *
347
+ * @param {string} title - Título de la tarea (generalmente la descripción corta del bug)
348
+ * @param {string} description - Descripción detallada del bug detectado
349
+ * @returns {Promise<{success: boolean, taskUrl?: string, error?: string}>}
350
+ */
351
+ export async function createAdoTask(title, description) {
352
+ const apiBase = getEffectiveApiBase();
353
+ if (!apiBase) {
354
+ if (process.env.DEBUG) console.log(`[ADO] No hay ARCALITY_API_URL configurado. Omitiendo creación de Task en Azure DevOps.`);
355
+ return { success: false, error: 'no_api_url' };
356
+ }
357
+
358
+ const projectId = loadProjectId();
359
+ if (!projectId) {
360
+ if (process.env.DEBUG) console.log(`[ADO] No hay Project ID configurado. Omitiendo creación de Task en Azure DevOps.`);
361
+ return { success: false, error: 'no_project_id' };
362
+ }
363
+
364
+ const key = getEffectiveApiKey();
365
+ if (!key) {
366
+ if (process.env.DEBUG) console.log(`[ADO] No hay API key configurada. Omitiendo creación de Task en Azure DevOps.`);
367
+ return { success: false, error: 'no_api_key' };
368
+ }
369
+
370
+ try {
371
+ const controller = new AbortController();
372
+ const timeout = setTimeout(() => controller.abort(), 15000);
373
+
374
+ const res = await fetch(`${apiBase}/api/v1/integrations/azuredevops/task`, {
375
+ method: 'POST',
376
+ headers: {
377
+ 'Content-Type': 'application/json',
378
+ 'x-api-key': key
379
+ },
380
+ body: JSON.stringify({ project_id: projectId, title, description }),
381
+ signal: controller.signal
382
+ });
383
+ clearTimeout(timeout);
384
+
385
+ if (res.status === 400) {
386
+ const data = await res.json().catch(() => ({}));
387
+ // La organización no tiene la integración de ADO configurada — no es un error crítico
388
+ if (data?.error === 'create_task_failed') {
389
+ if (process.env.DEBUG) console.log(`[ADO] Integración no configurada para esta organización: ${data.message}`);
390
+ } else {
391
+ console.warn(`⚠️ [ADO] Error al crear Task en Azure DevOps: ${data?.message || 'Bad Request'}`);
392
+ }
393
+ return { success: false, error: data?.message || 'create_task_failed' };
394
+ }
395
+
396
+ if (!res.ok) {
397
+ console.warn(`⚠️ [ADO] Error HTTP ${res.status} al crear Task en Azure DevOps.`);
398
+ return { success: false, error: `http_${res.status}` };
399
+ }
400
+
401
+ const data = await res.json();
402
+ console.log(`✅ [ADO] Task creada en Azure DevOps: ${data.task_url || '(sin URL)'}`);
403
+ return { success: true, taskUrl: data.task_url };
404
+ } catch (e) {
405
+ const reason = e?.name === 'AbortError' ? 'timeout (15s)' : (e?.message || 'unknown');
406
+ console.warn(`⚠️ [ADO] No se pudo crear la Task en Azure DevOps: ${reason}`);
407
+ return { success: false, error: reason };
408
+ }
409
+ }
410
+
284
411
  /**
285
412
  * Obtiene el Token SAS y el path base para subir evidencias.
286
413
  * @param {string} missionId
@@ -309,7 +436,7 @@ export async function getEvidenceSasToken(missionId, projectId) {
309
436
  console.log(`[DEBUG] SAS Token API Failed: ${res.status} - ${txt}`);
310
437
  return null;
311
438
  }
312
-
439
+
313
440
  const data = await res.json();
314
441
  return data;
315
442
  } catch (e) {