@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/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
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
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('⚠️
|
|
92
|
-
chalk.white('
|
|
93
|
-
chalk.white(' Base URL: ') + chalk.cyan(existing.project?.baseUrl || 'unknown') + '\n' +
|
|
94
|
-
chalk.white('
|
|
95
|
-
'
|
|
96
|
-
);
|
|
97
|
-
|
|
98
|
-
const overwrite = await confirm({
|
|
99
|
-
message: '
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
if (isCancel(overwrite) || !overwrite) {
|
|
103
|
-
outro(chalk.cyan('
|
|
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('🔑
|
|
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
|
|
115
|
-
if (!v.startsWith('arc_')) return 'API Key
|
|
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('
|
|
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('
|
|
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: '❌
|
|
144
|
-
403: `❌
|
|
145
|
-
};
|
|
146
|
-
const msg = errorMap[res.status] || `❌
|
|
147
|
-
s.stop(chalk.red(msg));
|
|
148
|
-
console.log(chalk.yellow('\n
|
|
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
|
|
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('🏢
|
|
163
|
-
'
|
|
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(`❌
|
|
168
|
-
console.log(chalk.yellow('
|
|
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('📋
|
|
176
|
-
placeholder: '
|
|
177
|
-
validate: (v) => {
|
|
178
|
-
if (!v || v.trim().length < 2) return '
|
|
179
|
-
},
|
|
180
|
-
});
|
|
181
|
-
if (isCancel(projectName)) { cancel('
|
|
182
|
-
|
|
183
|
-
const baseUrl = await text({
|
|
184
|
-
message: chalk.cyan('🌐
|
|
185
|
-
placeholder: 'https://staging.example.com',
|
|
186
|
-
validate: (v) => {
|
|
187
|
-
if (!v || !v.trim()) return '
|
|
188
|
-
if (!isValidUrl(v)) return '
|
|
189
|
-
},
|
|
190
|
-
});
|
|
191
|
-
if (isCancel(baseUrl)) { cancel('
|
|
192
|
-
|
|
193
|
-
const hasLogin = await confirm({
|
|
194
|
-
message: chalk.cyan('🔐
|
|
195
|
-
});
|
|
196
|
-
if (isCancel(hasLogin)) { cancel('
|
|
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('👤
|
|
204
|
-
validate: (v) => {
|
|
205
|
-
if (!v || !v.trim()) return '
|
|
206
|
-
},
|
|
207
|
-
});
|
|
208
|
-
if (isCancel(username)) { cancel('
|
|
209
|
-
|
|
210
|
-
password = await passwordPrompt({
|
|
211
|
-
message: chalk.cyan('🔒
|
|
212
|
-
validate: (v) => {
|
|
213
|
-
if (!v || !v.length) return '
|
|
214
|
-
},
|
|
215
|
-
});
|
|
216
|
-
if (isCancel(password)) { cancel('
|
|
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
|
|
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('
|
|
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
|
-
? `
|
|
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(`✅
|
|
268
|
-
}
|
|
269
|
-
} catch (err) {
|
|
270
|
-
sCreate.stop(chalk.yellow(`⚠️
|
|
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
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
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 = `#
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
##
|
|
310
|
-
|
|
311
|
-
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
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
|
|
325
|
-
chalk.bold.white('
|
|
326
|
-
chalk.bold.white(' Base
|
|
327
|
-
chalk.bold.white(' Framework: ') + chalk.magenta(config.project.frameworkDetected || 'N/A') + '\n' +
|
|
328
|
-
chalk.bold.white('
|
|
329
|
-
chalk.bold.white('
|
|
330
|
-
'
|
|
331
|
-
);
|
|
332
|
-
|
|
333
|
-
outro(
|
|
334
|
-
chalk.cyanBright('🚀
|
|
335
|
-
chalk.bold.white('arcality run') +
|
|
336
|
-
chalk.cyanBright('
|
|
337
|
-
chalk.bold.white('arcality') +
|
|
338
|
-
chalk.cyanBright('
|
|
339
|
-
);
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
main().catch(err => {
|
|
343
|
-
console.error(chalk.red(' ❌ Error
|
|
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
|
+
});
|
package/src/arcalityClient.mjs
CHANGED
|
@@ -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) {
|