@arcadialdev/arcality 2.4.28 → 2.4.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,9 +1,9 @@
1
1
  <div align="center">
2
- <img src="https://unpkg.com/@arcadialdev/arcality/public/logo-arcadial-blanco.png" width="120" alt="Arcality Logo" />
2
+ <img src="./public/logo-arcadial-blanco.png" width="120" alt="Arcality Logo" />
3
3
  <br>
4
4
  <h1>Arcality Engine</h1>
5
5
  <p><b>Autonomous AI-Powered E2E Web Testing Agent</b></p>
6
-
6
+
7
7
  [![NPM Version](https://img.shields.io/npm/v/@arcadialdev/arcality?color=c084fc&label=NPM&style=for-the-badge)](https://www.npmjs.com/package/@arcadialdev/arcality)
8
8
  </div>
9
9
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arcadialdev/arcality",
3
- "version": "2.4.28",
3
+ "version": "2.4.29",
4
4
  "description": "AI-powered QA testing tool — Autonomous web testing agent by Arcadial",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -8,7 +8,7 @@
8
8
  "arcality": "node scripts/gen-and-run.mjs",
9
9
  "postinstall": "node scripts/postinstall.mjs",
10
10
  "setup": "node scripts/setup.mjs",
11
- "build:runner": "npx esbuild tests/_helpers/agentic-runner.spec.ts --bundle --minify --platform=node --target=node18 --format=cjs --external:@playwright/test --external:chalk --external:dotenv --external:node-fetch --external:js-yaml --external:axios --external:crypto --external:fs --external:path --outfile=tests/_helpers/agentic-runner.bundle.spec.js",
11
+ "build:runner": "npx esbuild tests/_helpers/agentic-runner.spec.ts --bundle --platform=node --target=node18 --format=cjs --external:@playwright/test --external:chalk --external:dotenv --external:node-fetch --external:js-yaml --external:axios --external:crypto --external:fs --external:path --outfile=tests/_helpers/agentic-runner.bundle.spec.js",
12
12
  "prepublishOnly": "npm run build:runner"
13
13
  },
14
14
  "bin": {
@@ -17,11 +17,8 @@
17
17
  "files": [
18
18
  "bin/",
19
19
  "scripts/",
20
- "src/configLoader.mjs",
21
- "src/configManager.mjs",
22
- "src/arcalityClient.mjs",
23
- "tests/_helpers/agentic-runner.bundle.spec.js",
24
- "tests/_helpers/ArcalityReporter.js",
20
+ "src/",
21
+ "tests/_helpers/",
25
22
  ".agents/",
26
23
  "public/",
27
24
  "playwright.config.js",
@@ -428,10 +428,6 @@ async function main() {
428
428
  let firstRun = true;
429
429
  let globalReportProcess = null;
430
430
 
431
- // Immutable flag: true when started from `arcality run` (--agent flag on CLI).
432
- // Used to exit cleanly instead of showing the interactive menu on completion or errors.
433
- const CLI_AGENT_MODE = initialAgentMode;
434
-
435
431
  setupEnvironment();
436
432
 
437
433
  while (true) {
@@ -632,10 +628,6 @@ async function main() {
632
628
  'plan_expired': '💳 Your plan has expired'
633
629
  };
634
630
  note(chalk.red(errorMessages[validation.error] || `Error: ${validation.error}`), '❌ Authentication');
635
- if (CLI_AGENT_MODE) {
636
- outro(chalk.red('❌ Authentication failed. Exiting.'));
637
- process.exit(1);
638
- }
639
631
  agentMode = false;
640
632
  prompt = "";
641
633
  skipMenu = false;
@@ -728,10 +720,6 @@ async function main() {
728
720
  chalk.gray(`🔄 Resets: ${mission.resets_at ? new Date(mission.resets_at).toLocaleString() : 'tomorrow'}`),
729
721
  '⚠️ Limit Reached'
730
722
  );
731
- if (CLI_AGENT_MODE) {
732
- outro(chalk.red('❌ Mission limit reached. Exiting.'));
733
- process.exit(1);
734
- }
735
723
  agentMode = false;
736
724
  prompt = "";
737
725
  skipMenu = false;
@@ -919,17 +907,16 @@ async function main() {
919
907
  process.env.ARCALITY_API_KEY
920
908
  );
921
909
 
922
- // In CLI agent mode (arcality run), exit immediately — no interactive menu.
923
- if (CLI_AGENT_MODE) {
924
- outro(chalk.cyan('✅ Mission finished! See you later.'));
925
- return;
926
- }
927
-
928
910
  await text({
929
911
  message: 'Press Enter to return to menu...',
930
912
  placeholder: 'Enter'
931
913
  });
932
914
 
915
+ if (skipMenu) {
916
+ outro(chalk.cyan('Mission finished! See you later.'));
917
+ return;
918
+ }
919
+
933
920
  agentMode = false;
934
921
  prompt = "";
935
922
  firstRun = false;
@@ -0,0 +1,256 @@
1
+
2
+ import * as crypto from 'crypto';
3
+ import chalk from 'chalk';
4
+
5
+ /**
6
+ * KnowledgeService (SaaS v2)
7
+ * Servicio centralizado para alimentar y consumir la base de conocimiento persistente de Arcality.
8
+ * Configurado para usar SNAKE_CASE según la especificación del servidor.
9
+ */
10
+
11
+ export interface PortalIngestRequest {
12
+ project_id: string;
13
+ target_url: string;
14
+ page_title?: string;
15
+ dom_hash: string;
16
+ viewport?: { width: number; height: number };
17
+ components: Array<{
18
+ tag: string;
19
+ component_type: "ACTION" | "FIELD" | "INFO" | "NAVIGATION" | "DATA";
20
+ semantic_label: string;
21
+ text_content?: string;
22
+ is_interactive: boolean;
23
+ attributes?: any;
24
+ bounding_rect?: { x: number; y: number; width: number; height: number };
25
+ }>;
26
+ }
27
+
28
+ export interface PortalContextResponse {
29
+ rules: Array<{ title: string; description: string; severity: string }>;
30
+ fields: Array<{ field_identifier: string; field_type: string; is_required: boolean }>;
31
+ }
32
+
33
+ export interface ProjectDto {
34
+ id: string;
35
+ name: string;
36
+ base_url?: string;
37
+ }
38
+
39
+ export class KnowledgeService {
40
+ private static instance: KnowledgeService;
41
+ private apiBase: string;
42
+ private apiKey: string;
43
+ private projectId: string | null = null;
44
+
45
+ private constructor() {
46
+ // API URL is internal-only, loaded from the tool's .env via dotenv
47
+ this.apiBase = process.env.ARCALITY_API_URL || 'https://arcalityqadev.arcadial.lat';
48
+ this.apiKey = process.env.ARCALITY_API_KEY || '';
49
+ this.projectId = process.env.ARCALITY_PROJECT_ID || null;
50
+ }
51
+
52
+ /**
53
+ * Fetch wrapper with a 10-second timeout to prevent hanging.
54
+ * All KnowledgeService calls are telemetry/secondary — they must NEVER block the agent.
55
+ */
56
+ private async fetchWithTimeout(url: string, options: RequestInit = {}, timeoutMs = 10000): Promise<Response> {
57
+ const controller = new AbortController();
58
+ const id = setTimeout(() => controller.abort(), timeoutMs);
59
+ try {
60
+ const res = await fetch(url, { ...options, signal: controller.signal });
61
+ clearTimeout(id);
62
+ return res;
63
+ } catch (err: any) {
64
+ clearTimeout(id);
65
+ throw err;
66
+ }
67
+ }
68
+
69
+ public static getInstance(): KnowledgeService {
70
+ if (!KnowledgeService.instance) {
71
+ KnowledgeService.instance = new KnowledgeService();
72
+ }
73
+ return KnowledgeService.instance;
74
+ }
75
+
76
+ public setProjectId(id: string) {
77
+ this.projectId = id;
78
+ }
79
+
80
+ public getProjectId(): string | null {
81
+ if (!this.projectId || this.projectId === 'undefined' || this.projectId === 'null') {
82
+ this.projectId = process.env.ARCALITY_PROJECT_ID || null;
83
+ }
84
+ return this.projectId;
85
+ }
86
+
87
+ /**
88
+ * 1. Gestión de Proyectos
89
+ */
90
+ public async getProjects(): Promise<ProjectDto[]> {
91
+ try {
92
+ const res = await this.fetchWithTimeout(`${this.apiBase}/api/v1/projects`, {
93
+ headers: { 'x-api-key': this.apiKey }
94
+ });
95
+ if (!res.ok) return [];
96
+ const data = await res.json();
97
+ return data.projects || [];
98
+ } catch (e: any) {
99
+ console.warn(chalk.yellow(`[KnowledgeService] Error al obtener proyectos: ${e.message}`));
100
+ return [];
101
+ }
102
+ }
103
+
104
+ public async createProject(name: string, baseUrl: string): Promise<ProjectDto | null> {
105
+ try {
106
+ const res = await this.fetchWithTimeout(`${this.apiBase}/api/v1/projects`, {
107
+ method: 'POST',
108
+ headers: {
109
+ 'Content-Type': 'application/json',
110
+ 'x-api-key': this.apiKey
111
+ },
112
+ body: JSON.stringify({ name, base_url: baseUrl })
113
+ });
114
+ if (!res.ok) return null;
115
+ return await res.json();
116
+ } catch (e: any) {
117
+ console.error(chalk.red(`[KnowledgeService] Falló creación de proyecto: ${e.message}`));
118
+ return null;
119
+ }
120
+ }
121
+
122
+ /**
123
+ * 2. Ingesta (Post-Percept)
124
+ */
125
+ public async ingest(payload: PortalIngestRequest): Promise<void> {
126
+ if (!payload.project_id || payload.project_id === 'undefined') {
127
+ console.warn(chalk.yellow(`[Knowledge] Ingesta cancelada: project_id es inválido.`));
128
+ return;
129
+ }
130
+
131
+ try {
132
+ const res = await this.fetchWithTimeout(`${this.apiBase}/api/v1/portal/ingest`, {
133
+ method: 'POST',
134
+ headers: {
135
+ 'Content-Type': 'application/json',
136
+ 'x-api-key': this.apiKey
137
+ },
138
+ body: JSON.stringify(payload)
139
+ });
140
+ if (res.status === 200) {
141
+ const data = await res.json();
142
+ if (data.status === 'skipped_duplicate_dom') {
143
+ // console.log(chalk.gray(`[Knowledge] Ingesta omitida (Duplicado).`));
144
+ } else {
145
+ console.log(chalk.cyan(`[Knowledge] Ingesta exitosa (Project: ${payload.project_id})`));
146
+ }
147
+ }
148
+ } catch (e: any) {
149
+ console.warn(chalk.yellow(`[Knowledge] Error en ingesta: ${e.message}`));
150
+ }
151
+ }
152
+
153
+ /**
154
+ * 3. Contexto (Pre-Reasoning)
155
+ */
156
+ public async getContext(url: string): Promise<PortalContextResponse | null> {
157
+ const pid = this.getProjectId();
158
+ if (!pid || pid === 'undefined') return null;
159
+ try {
160
+ const pathUrl = new URL(url).pathname;
161
+ const res = await this.fetchWithTimeout(
162
+ `${this.apiBase}/api/v1/portal/context?project_id=${pid}&path=${encodeURIComponent(pathUrl)}`,
163
+ { headers: { 'x-api-key': this.apiKey } }
164
+ );
165
+ if (!res.ok) return null;
166
+ return await res.json();
167
+ } catch (e: any) {
168
+ console.warn(chalk.yellow(`[Knowledge] Error al obtener contexto: ${e.message}`));
169
+ return null;
170
+ }
171
+ }
172
+
173
+ /**
174
+ * 4. Aprendizaje (portal_domain_rules)
175
+ */
176
+ public async saveRule(title: string, description: string, severity: string = 'important', pageUrl?: string): Promise<void> {
177
+ const pid = this.getProjectId();
178
+ if (!pid || pid === 'undefined') return;
179
+ try {
180
+ const pathUrl = pageUrl ? new URL(pageUrl).pathname : null;
181
+ await fetch(`${this.apiBase}/api/v1/portal/rules`, {
182
+ method: 'POST',
183
+ headers: {
184
+ 'Content-Type': 'application/json',
185
+ 'x-api-key': this.apiKey
186
+ },
187
+ body: JSON.stringify({
188
+ project_id: pid,
189
+ rule_type: "UI_UX", // Default según especificación
190
+ title,
191
+ description,
192
+ severity
193
+ })
194
+ });
195
+ console.log(chalk.green(`[Knowledge] Nueva regla registrada: ${title}`));
196
+ } catch (e) { }
197
+ }
198
+
199
+ /**
200
+ * 5. Catálogo de Campos (portal_field_catalog)
201
+ */
202
+ public async saveField(identifier: string, type: string, isRequired: boolean): Promise<void> {
203
+ const pid = this.getProjectId();
204
+ if (!pid || pid === 'undefined') return;
205
+ try {
206
+ await fetch(`${this.apiBase}/api/v1/portal/fields`, {
207
+ method: 'POST',
208
+ headers: {
209
+ 'Content-Type': 'application/json',
210
+ 'x-api-key': this.apiKey
211
+ },
212
+ body: JSON.stringify({
213
+ project_id: pid,
214
+ field_identifier: identifier,
215
+ field_type: type,
216
+ is_required: isRequired,
217
+ source: "agent_auto"
218
+ })
219
+ });
220
+ console.log(chalk.green(`[Knowledge] Nuevo campo catalogado: ${identifier}`));
221
+ } catch (e) { }
222
+ }
223
+
224
+ /**
225
+ * 6. Conocimiento General (Documentación)
226
+ */
227
+ public async saveKnowledge(title: string, content: string, docType: string = 'PROCESS'): Promise<void> {
228
+ const pid = this.getProjectId();
229
+ if (!pid || pid === 'undefined') return;
230
+ try {
231
+ await fetch(`${this.apiBase}/api/v1/portal/knowledge`, {
232
+ method: 'POST',
233
+ headers: {
234
+ 'Content-Type': 'application/json',
235
+ 'x-api-key': this.apiKey
236
+ },
237
+ body: JSON.stringify({
238
+ project_id: pid,
239
+ doc_type: docType,
240
+ title,
241
+ content,
242
+ source: "agent_auto"
243
+ })
244
+ });
245
+ console.log(chalk.green(`[Knowledge] Nuevo conocimiento guardado: ${title}`));
246
+ } catch (e) { }
247
+ }
248
+
249
+ /**
250
+ * Utilidad: Calcular Hash para Ingesta
251
+ */
252
+ public calculateDomHash(components: any[]): string {
253
+ const fingerprint = components.map(c => `${c.tag}-${c.semanticLabel}`).join('|');
254
+ return crypto.createHash('sha256').update(fingerprint).digest('hex');
255
+ }
256
+ }
@@ -0,0 +1,32 @@
1
+ import figlet from 'figlet';
2
+ import chalk from 'chalk';
3
+ import fs from 'fs';
4
+ import path from 'path';
5
+ import { intro, note } from '@clack/prompts';
6
+
7
+ export function showBanner(): void {
8
+ const projectName = 'Arcality';
9
+ const subtitle = 'Powered by Arcadial';
10
+
11
+ let version = 'unknown';
12
+ try {
13
+ const pkg = JSON.parse(fs.readFileSync(path.join(process.cwd(), 'package.json'), 'utf8'));
14
+ version = pkg.version || 'unknown';
15
+ } catch { }
16
+
17
+ const logo = figlet.textSync(projectName, {
18
+ font: 'Standard',
19
+ horizontalLayout: 'default',
20
+ verticalLayout: 'default',
21
+ });
22
+
23
+ intro(chalk.cyanBright(logo));
24
+
25
+ note(
26
+ chalk.magentaBright(subtitle) + '\n' +
27
+ chalk.gray('─'.repeat(50)) + '\n' +
28
+ chalk.bold.white('📦 Version: ') + chalk.yellow(version) + '\n' +
29
+ chalk.bold.white('⚙️ Environment: ') + chalk.yellow(process.env.NODE_ENV ?? 'development'),
30
+ 'System Information'
31
+ );
32
+ }
@@ -0,0 +1,205 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import prompts from 'prompts';
4
+
5
+ export class UserCancelledError extends Error {
6
+ constructor(message = 'Usuario canceló la operación') {
7
+ super(message);
8
+ this.name = 'UserCancelledError';
9
+ }
10
+ }
11
+
12
+ type EnvValues = {
13
+ BASE_URL?: string;
14
+ LOGIN_USER?: string;
15
+ LOGIN_PASSWORD?: string;
16
+ ARCALITY_PROJECT_ID?: string;
17
+ };
18
+
19
+ interface ArcalityConfigFile {
20
+ apiKey?: string;
21
+ projectId?: string;
22
+ project?: {
23
+ name?: string;
24
+ baseUrl?: string;
25
+ frameworkDetected?: string;
26
+ };
27
+ auth?: {
28
+ username?: string;
29
+ password?: string;
30
+ };
31
+ runtime?: {
32
+ yamlOutputDir?: string;
33
+ reuseSuccessfulYamls?: boolean;
34
+ singleConfigurationMode?: boolean;
35
+ };
36
+ }
37
+
38
+ function envPath(): string {
39
+ return path.join(process.cwd(), '.env');
40
+ }
41
+
42
+ function arcalityConfigPath(): string {
43
+ return path.join(process.cwd(), 'arcality.config');
44
+ }
45
+
46
+ function escapeEnvValue(v: string): string {
47
+ const needsQuotes = /[\s"'`\\]/.test(v);
48
+ const cleaned = v.replace(/"/g, '\\"');
49
+ return needsQuotes ? `"${cleaned}"` : cleaned;
50
+ }
51
+
52
+ function parseDotEnv(content: string): Record<string, string> {
53
+ const out: Record<string, string> = {};
54
+ for (const line of content.split(/\r?\n/)) {
55
+ const trimmed = line.trim();
56
+ if (!trimmed || trimmed.startsWith('#')) continue;
57
+
58
+ const idx = trimmed.indexOf('=');
59
+ if (idx === -1) continue;
60
+
61
+ const k = trimmed.slice(0, idx).trim();
62
+ let v = trimmed.slice(idx + 1).trim();
63
+
64
+ if (
65
+ (v.startsWith('"') && v.endsWith('"')) ||
66
+ (v.startsWith("'") && v.endsWith("'"))
67
+ ) {
68
+ v = v.slice(1, -1);
69
+ }
70
+ out[k] = v;
71
+ }
72
+ return out;
73
+ }
74
+
75
+ export function readExistingEnv(): Record<string, string> {
76
+ const p = envPath();
77
+ if (!fs.existsSync(p)) return {};
78
+ return parseDotEnv(fs.readFileSync(p, 'utf8'));
79
+ }
80
+
81
+ /**
82
+ * Loads arcality.config if it exists.
83
+ */
84
+ function loadArcalityConfig(): ArcalityConfigFile | null {
85
+ const p = arcalityConfigPath();
86
+ try {
87
+ if (!fs.existsSync(p)) return null;
88
+ let raw = fs.readFileSync(p, 'utf8');
89
+ if (raw.charCodeAt(0) === 0xFEFF) raw = raw.slice(1);
90
+ return JSON.parse(raw);
91
+ } catch {
92
+ return null;
93
+ }
94
+ }
95
+
96
+ function writeEnv(values: EnvValues): void {
97
+ const existing = readExistingEnv();
98
+ const merged = { ...existing, ...values };
99
+
100
+ const lines = [
101
+ '# Auto-generado por Arcality testRunner',
102
+ `BASE_URL=${escapeEnvValue(merged.BASE_URL ?? '')}`,
103
+ `LOGIN_USER=${escapeEnvValue(merged.LOGIN_USER ?? '')}`,
104
+ `LOGIN_PASSWORD=${escapeEnvValue(merged.LOGIN_PASSWORD ?? '')}`,
105
+ ];
106
+
107
+ if (merged.ARCALITY_PROJECT_ID) {
108
+ lines.push(`ARCALITY_PROJECT_ID=${escapeEnvValue(merged.ARCALITY_PROJECT_ID)}`);
109
+ }
110
+ lines.push('');
111
+
112
+ fs.writeFileSync(envPath(), lines.join('\n'), 'utf8');
113
+ }
114
+
115
+ function isValidBaseUrl(url: string): boolean {
116
+ try {
117
+ const u = new URL(url);
118
+ return u.protocol === 'http:' || u.protocol === 'https:';
119
+ } catch {
120
+ return false;
121
+ }
122
+ }
123
+
124
+ /**
125
+ * Ensures environment is configured for test execution.
126
+ * In single-config mode, reads from arcality.config first.
127
+ * Falls back to .env prompts for backward compatibility.
128
+ */
129
+ export async function ensureEnvInteractive(): Promise<void> {
130
+ // Try to load arcality.config first
131
+ const arcalityConfig = loadArcalityConfig();
132
+
133
+ if (arcalityConfig) {
134
+ // Inject config values into process.env for the current session
135
+ if (arcalityConfig.project?.baseUrl) {
136
+ process.env.BASE_URL = arcalityConfig.project.baseUrl;
137
+ }
138
+ if (arcalityConfig.auth?.username) {
139
+ process.env.LOGIN_USER = arcalityConfig.auth.username;
140
+ }
141
+ if (arcalityConfig.auth?.password) {
142
+ process.env.LOGIN_PASSWORD = arcalityConfig.auth.password;
143
+ }
144
+ if (arcalityConfig.projectId) {
145
+ process.env.ARCALITY_PROJECT_ID = arcalityConfig.projectId;
146
+ }
147
+ if (arcalityConfig.apiKey) {
148
+ process.env.ARCALITY_API_KEY = arcalityConfig.apiKey;
149
+ }
150
+
151
+ // All good — no prompts needed
152
+ return;
153
+ }
154
+
155
+ // Fallback: prompt for missing .env values (legacy mode)
156
+ const existing = readExistingEnv();
157
+ const questions: prompts.PromptObject[] = [];
158
+
159
+ if (!existing.BASE_URL) {
160
+ questions.push({
161
+ type: 'text',
162
+ name: 'BASE_URL',
163
+ message: 'Base URL (ej: http://localhost:3000):',
164
+ initial: 'http://localhost:3000',
165
+ validate: (v: string) =>
166
+ isValidBaseUrl(v) ? true : 'URL inválida (usa http/https)',
167
+ });
168
+ }
169
+
170
+ if (!existing.LOGIN_USER) {
171
+ questions.push({
172
+ type: 'text',
173
+ name: 'LOGIN_USER',
174
+ message: 'Usuario:',
175
+ validate: (v: string) =>
176
+ v?.trim().length ? true : 'Usuario requerido',
177
+ });
178
+ }
179
+
180
+ if (!existing.LOGIN_PASSWORD) {
181
+ questions.push({
182
+ type: 'password',
183
+ name: 'LOGIN_PASSWORD',
184
+ message: 'Contraseña:',
185
+ validate: (v: string) =>
186
+ v?.length ? true : 'Contraseña requerida',
187
+ });
188
+ }
189
+
190
+ if (questions.length > 0) {
191
+ const res = await prompts(questions, {
192
+ onCancel: () => {
193
+ throw new UserCancelledError();
194
+ },
195
+ });
196
+
197
+ const merged: EnvValues = {
198
+ BASE_URL: res.BASE_URL ?? existing.BASE_URL,
199
+ LOGIN_USER: res.LOGIN_USER ?? existing.LOGIN_USER,
200
+ LOGIN_PASSWORD: res.LOGIN_PASSWORD ?? existing.LOGIN_PASSWORD,
201
+ };
202
+ writeEnv(merged);
203
+ console.log('Archivo .env actualizado.');
204
+ }
205
+ }
package/src/index.ts ADDED
@@ -0,0 +1,25 @@
1
+ import 'dotenv/config';
2
+ import { showBanner } from './consoleBanner';
3
+ import { promptAndRunPlaywrightTests } from './testRunner';
4
+ import { KnowledgeService } from './KnowledgeService';
5
+ import { ArcalityClient } from './arcalityClient';
6
+ import { loadConfig } from './configLoader';
7
+
8
+ async function main() {
9
+ showBanner();
10
+
11
+ const config = loadConfig();
12
+ const arcalityClient = new ArcalityClient(config.ARCALITY_API_KEY);
13
+ const knowledgeService = KnowledgeService.getInstance();
14
+
15
+ await promptAndRunPlaywrightTests(arcalityClient, knowledgeService);
16
+ }
17
+
18
+ main().catch((err) => {
19
+ // Normal user cancellation is not an error
20
+ if (err instanceof Error && err.name === 'UserCancelledError') {
21
+ process.exit(130);
22
+ }
23
+ console.error(err);
24
+ process.exit(1);
25
+ });
@@ -0,0 +1,42 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ export interface ProjectContext {
5
+ name: string;
6
+ version: string;
7
+ framework: 'next' | 'vite' | 'cra' | 'unknown';
8
+ scripts: string[];
9
+ dependencies: string[];
10
+ }
11
+
12
+ export function getProjectContext(projectRoot: string): ProjectContext {
13
+ const pkgPath = path.join(projectRoot, 'package.json');
14
+ const context: ProjectContext = {
15
+ name: 'Arcality Project',
16
+ version: '1.0.0',
17
+ framework: 'unknown',
18
+ scripts: [],
19
+ dependencies: []
20
+ };
21
+
22
+ if (!fs.existsSync(pkgPath)) return context;
23
+
24
+ try {
25
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
26
+ context.name = pkg.name || context.name;
27
+ context.version = pkg.version || context.version;
28
+
29
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
30
+ context.dependencies = Object.keys(deps);
31
+ context.scripts = pkg.scripts ? Object.keys(pkg.scripts) : [];
32
+
33
+ if (deps['next']) context.framework = 'next';
34
+ else if (deps['vite']) context.framework = 'vite';
35
+ else if (deps['react-scripts']) context.framework = 'cra';
36
+
37
+ } catch (e) {
38
+ // Ignorar errores de lectura
39
+ }
40
+
41
+ return context;
42
+ }