@arcadialdev/arcality 2.2.0
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/.agents/skills/e2e-testing-expert/SKILL.md +28 -0
- package/.agents/skills/frontend-design/LICENSE.txt +177 -0
- package/.agents/skills/frontend-design/SKILL.md +42 -0
- package/.agents/skills/nodejs-backend-patterns/SKILL.md +639 -0
- package/.agents/skills/nodejs-backend-patterns/references/advanced-patterns.md +430 -0
- package/.agents/skills/playwright-best-practices/LICENSE.md +7 -0
- package/.agents/skills/playwright-best-practices/README.md +147 -0
- package/.agents/skills/playwright-best-practices/SKILL.md +303 -0
- package/.agents/skills/playwright-best-practices/advanced/authentication-flows.md +360 -0
- package/.agents/skills/playwright-best-practices/advanced/authentication.md +871 -0
- package/.agents/skills/playwright-best-practices/advanced/clock-mocking.md +364 -0
- package/.agents/skills/playwright-best-practices/advanced/mobile-testing.md +409 -0
- package/.agents/skills/playwright-best-practices/advanced/multi-context.md +288 -0
- package/.agents/skills/playwright-best-practices/advanced/multi-user.md +393 -0
- package/.agents/skills/playwright-best-practices/advanced/network-advanced.md +452 -0
- package/.agents/skills/playwright-best-practices/advanced/third-party.md +464 -0
- package/.agents/skills/playwright-best-practices/architecture/pom-vs-fixtures.md +363 -0
- package/.agents/skills/playwright-best-practices/architecture/test-architecture.md +369 -0
- package/.agents/skills/playwright-best-practices/architecture/when-to-mock.md +383 -0
- package/.agents/skills/playwright-best-practices/browser-apis/browser-apis.md +391 -0
- package/.agents/skills/playwright-best-practices/browser-apis/iframes.md +403 -0
- package/.agents/skills/playwright-best-practices/browser-apis/service-workers.md +504 -0
- package/.agents/skills/playwright-best-practices/browser-apis/websockets.md +403 -0
- package/.agents/skills/playwright-best-practices/core/annotations.md +424 -0
- package/.agents/skills/playwright-best-practices/core/assertions-waiting.md +361 -0
- package/.agents/skills/playwright-best-practices/core/configuration.md +452 -0
- package/.agents/skills/playwright-best-practices/core/fixtures-hooks.md +417 -0
- package/.agents/skills/playwright-best-practices/core/global-setup.md +434 -0
- package/.agents/skills/playwright-best-practices/core/locators.md +242 -0
- package/.agents/skills/playwright-best-practices/core/page-object-model.md +315 -0
- package/.agents/skills/playwright-best-practices/core/projects-dependencies.md +453 -0
- package/.agents/skills/playwright-best-practices/core/test-data.md +492 -0
- package/.agents/skills/playwright-best-practices/core/test-suite-structure.md +361 -0
- package/.agents/skills/playwright-best-practices/core/test-tags.md +298 -0
- package/.agents/skills/playwright-best-practices/debugging/console-errors.md +420 -0
- package/.agents/skills/playwright-best-practices/debugging/debugging.md +504 -0
- package/.agents/skills/playwright-best-practices/debugging/error-testing.md +360 -0
- package/.agents/skills/playwright-best-practices/debugging/flaky-tests.md +496 -0
- package/.agents/skills/playwright-best-practices/frameworks/angular.md +530 -0
- package/.agents/skills/playwright-best-practices/frameworks/nextjs.md +469 -0
- package/.agents/skills/playwright-best-practices/frameworks/react.md +531 -0
- package/.agents/skills/playwright-best-practices/frameworks/vue.md +574 -0
- package/.agents/skills/playwright-best-practices/infrastructure-ci-cd/ci-cd.md +468 -0
- package/.agents/skills/playwright-best-practices/infrastructure-ci-cd/docker.md +283 -0
- package/.agents/skills/playwright-best-practices/infrastructure-ci-cd/github-actions.md +546 -0
- package/.agents/skills/playwright-best-practices/infrastructure-ci-cd/gitlab.md +397 -0
- package/.agents/skills/playwright-best-practices/infrastructure-ci-cd/other-providers.md +521 -0
- package/.agents/skills/playwright-best-practices/infrastructure-ci-cd/parallel-sharding.md +371 -0
- package/.agents/skills/playwright-best-practices/infrastructure-ci-cd/performance.md +453 -0
- package/.agents/skills/playwright-best-practices/infrastructure-ci-cd/reporting.md +424 -0
- package/.agents/skills/playwright-best-practices/infrastructure-ci-cd/test-coverage.md +497 -0
- package/.agents/skills/playwright-best-practices/testing-patterns/accessibility.md +359 -0
- package/.agents/skills/playwright-best-practices/testing-patterns/api-testing.md +719 -0
- package/.agents/skills/playwright-best-practices/testing-patterns/browser-extensions.md +506 -0
- package/.agents/skills/playwright-best-practices/testing-patterns/canvas-webgl.md +493 -0
- package/.agents/skills/playwright-best-practices/testing-patterns/component-testing.md +500 -0
- package/.agents/skills/playwright-best-practices/testing-patterns/drag-drop.md +576 -0
- package/.agents/skills/playwright-best-practices/testing-patterns/electron.md +509 -0
- package/.agents/skills/playwright-best-practices/testing-patterns/file-operations.md +377 -0
- package/.agents/skills/playwright-best-practices/testing-patterns/file-upload-download.md +562 -0
- package/.agents/skills/playwright-best-practices/testing-patterns/forms-validation.md +561 -0
- package/.agents/skills/playwright-best-practices/testing-patterns/graphql-testing.md +331 -0
- package/.agents/skills/playwright-best-practices/testing-patterns/i18n.md +508 -0
- package/.agents/skills/playwright-best-practices/testing-patterns/performance-testing.md +476 -0
- package/.agents/skills/playwright-best-practices/testing-patterns/security-testing.md +430 -0
- package/.agents/skills/playwright-best-practices/testing-patterns/visual-regression.md +634 -0
- package/.env.example +21 -0
- package/README.md +30 -0
- package/bin/arcality.mjs +86 -0
- package/package.json +66 -0
- package/playwright.config.ts +12 -0
- package/scripts/cleanup-qmsdev.mjs +63 -0
- package/scripts/discover-view.mjs +52 -0
- package/scripts/extract-view.mjs +64 -0
- package/scripts/gen-and-run.mjs +838 -0
- package/scripts/init.mjs +290 -0
- package/scripts/migrate-to-central-out.mjs +157 -0
- package/scripts/postinstall.mjs +63 -0
- package/scripts/rebrand-report.mjs +241 -0
- package/scripts/setup.mjs +166 -0
- package/src/KnowledgeService.ts +239 -0
- package/src/arcalityClient.mjs +266 -0
- package/src/configLoader.mjs +179 -0
- package/src/configManager.mjs +172 -0
- package/src/consoleBanner.ts +32 -0
- package/src/envSetup.ts +205 -0
- package/src/index.ts +25 -0
- package/src/projectInspector.ts +42 -0
- package/src/services/collectiveMemoryService.ts +178 -0
- package/src/testRunner.ts +201 -0
- package/tests/_helpers/ArcalityReporter.ts +490 -0
- package/tests/_helpers/agentic-runner.spec.ts +741 -0
- package/tests/_helpers/ai-agent-helper.ts +1573 -0
- package/tests/_helpers/discover-view.spec.ts +238 -0
- package/tests/_helpers/extract-view.spec.ts +118 -0
- package/tests/_helpers/qa-tools.ts +333 -0
- package/tests/_helpers/smart-action.spec.ts +1458 -0
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
// src/arcalityClient.mjs
|
|
2
|
+
// Client for communicating with Arcality Backend
|
|
3
|
+
// Supports arcality.config (primary) and .env (fallback)
|
|
4
|
+
// Includes MOCK MODE for development without backend
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { getApiKey, getApiUrl, CONFIG_DIR } from './configLoader.mjs';
|
|
8
|
+
|
|
9
|
+
// ── Backend config (internal, not user-configurable) ──
|
|
10
|
+
const DAILY_MISSION_LIMIT = 50;
|
|
11
|
+
const USAGE_FILE = path.join(CONFIG_DIR, 'usage.json');
|
|
12
|
+
|
|
13
|
+
// ═══════════════════════════════════════════════════════
|
|
14
|
+
// MOCK MODE: Local simulation (no backend)
|
|
15
|
+
// Tracks daily usage in ~/.arcality/usage.json
|
|
16
|
+
// ═══════════════════════════════════════════════════════
|
|
17
|
+
|
|
18
|
+
function getTodayKey() {
|
|
19
|
+
return new Date().toISOString().slice(0, 10);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function loadLocalUsage() {
|
|
23
|
+
try {
|
|
24
|
+
if (fs.existsSync(USAGE_FILE)) {
|
|
25
|
+
return JSON.parse(fs.readFileSync(USAGE_FILE, 'utf8'));
|
|
26
|
+
}
|
|
27
|
+
} catch { }
|
|
28
|
+
return {};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function saveLocalUsage(usage) {
|
|
32
|
+
try {
|
|
33
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
34
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
35
|
+
}
|
|
36
|
+
fs.writeFileSync(USAGE_FILE, JSON.stringify(usage, null, 2), 'utf8');
|
|
37
|
+
} catch { }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function getLocalDailyUsage() {
|
|
41
|
+
const usage = loadLocalUsage();
|
|
42
|
+
const today = getTodayKey();
|
|
43
|
+
return usage[today] || 0;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function incrementLocalUsage() {
|
|
47
|
+
const usage = loadLocalUsage();
|
|
48
|
+
const today = getTodayKey();
|
|
49
|
+
usage[today] = (usage[today] || 0) + 1;
|
|
50
|
+
|
|
51
|
+
const keys = Object.keys(usage).sort().reverse();
|
|
52
|
+
const cleaned = {};
|
|
53
|
+
for (const k of keys.slice(0, 7)) {
|
|
54
|
+
cleaned[k] = usage[k];
|
|
55
|
+
}
|
|
56
|
+
saveLocalUsage(cleaned);
|
|
57
|
+
return cleaned[today];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ═══════════════════════════════════════════════════════
|
|
61
|
+
// ARCALITY CONFIG LOADER (for reading arcality.config directly)
|
|
62
|
+
// ═══════════════════════════════════════════════════════
|
|
63
|
+
|
|
64
|
+
function loadArcalityConfig() {
|
|
65
|
+
try {
|
|
66
|
+
const configPath = path.join(process.cwd(), 'arcality.config');
|
|
67
|
+
if (fs.existsSync(configPath)) {
|
|
68
|
+
let raw = fs.readFileSync(configPath, 'utf8');
|
|
69
|
+
if (raw.charCodeAt(0) === 0xFEFF) raw = raw.slice(1);
|
|
70
|
+
return JSON.parse(raw);
|
|
71
|
+
}
|
|
72
|
+
} catch { }
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Gets the effective API base URL.
|
|
78
|
+
* Uses the internal getApiUrl() — NOT configurable by user.
|
|
79
|
+
*/
|
|
80
|
+
function getEffectiveApiBase() {
|
|
81
|
+
return getApiUrl();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Gets the effective API Key.
|
|
86
|
+
* Priority: arcality.config > env > global config
|
|
87
|
+
*/
|
|
88
|
+
function getEffectiveApiKey() {
|
|
89
|
+
const localConfig = loadArcalityConfig();
|
|
90
|
+
if (localConfig?.apiKey) return localConfig.apiKey;
|
|
91
|
+
|
|
92
|
+
const { key } = getApiKey();
|
|
93
|
+
return key;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ═══════════════════════════════════════════════════════
|
|
97
|
+
// PUBLIC API
|
|
98
|
+
// ═══════════════════════════════════════════════════════
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* ArcalityClient class — used by KnowledgeService and other modules.
|
|
102
|
+
*/
|
|
103
|
+
export class ArcalityClient {
|
|
104
|
+
constructor(apiKey) {
|
|
105
|
+
this.apiUrl = getApiUrl();
|
|
106
|
+
this.apiKey = apiKey;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Validates the API Key.
|
|
112
|
+
* - If backend is available (ARCALITY_API_URL) → validates remotely
|
|
113
|
+
* - If no backend → mock mode with local validation
|
|
114
|
+
*/
|
|
115
|
+
export async function validateApiKey() {
|
|
116
|
+
const key = getEffectiveApiKey();
|
|
117
|
+
|
|
118
|
+
if (!key) {
|
|
119
|
+
return { valid: false, error: 'no_api_key', mode: 'mock' };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (!key.startsWith('arc_k_')) {
|
|
123
|
+
return { valid: false, error: 'invalid_format', mode: 'mock' };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const apiBase = getEffectiveApiBase();
|
|
127
|
+
|
|
128
|
+
// ── LIVE mode: Backend available ──
|
|
129
|
+
if (apiBase) {
|
|
130
|
+
try {
|
|
131
|
+
const res = await fetch(`${apiBase}/api/v1/auth/validate`, {
|
|
132
|
+
method: 'POST',
|
|
133
|
+
headers: {
|
|
134
|
+
'Content-Type': 'application/json',
|
|
135
|
+
'x-api-key': key
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
if (res.status === 401) return { valid: false, error: 'invalid_api_key', mode: 'live' };
|
|
140
|
+
if (res.status === 403) return { valid: false, error: 'plan_expired', mode: 'live' };
|
|
141
|
+
if (!res.ok) return { valid: false, error: 'server_error', mode: 'live' };
|
|
142
|
+
|
|
143
|
+
const data = await res.json();
|
|
144
|
+
return { ...data, valid: true, mode: 'live' };
|
|
145
|
+
} catch {
|
|
146
|
+
console.warn('⚠️ Backend not available, using local mode');
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ── MOCK mode: No backend ──
|
|
151
|
+
const dailyUsed = getLocalDailyUsage();
|
|
152
|
+
return {
|
|
153
|
+
valid: true,
|
|
154
|
+
mode: 'mock',
|
|
155
|
+
plan: 'internal',
|
|
156
|
+
daily_used: dailyUsed,
|
|
157
|
+
daily_limit: DAILY_MISSION_LIMIT,
|
|
158
|
+
remaining: Math.max(0, DAILY_MISSION_LIMIT - dailyUsed)
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Requests to start a mission.
|
|
164
|
+
* @param {string} prompt - The mission prompt (only a hash is sent, never the raw text)
|
|
165
|
+
* @param {string} targetUrl - Target portal URL
|
|
166
|
+
*/
|
|
167
|
+
export async function startMission(prompt, targetUrl) {
|
|
168
|
+
const key = getEffectiveApiKey();
|
|
169
|
+
if (!key) return { allowed: false, error: 'no_api_key' };
|
|
170
|
+
|
|
171
|
+
const apiBase = getEffectiveApiBase();
|
|
172
|
+
|
|
173
|
+
// ── LIVE mode ──
|
|
174
|
+
if (apiBase) {
|
|
175
|
+
try {
|
|
176
|
+
const projectId = process.env.ARCALITY_PROJECT_ID || loadArcalityConfig()?.projectId;
|
|
177
|
+
|
|
178
|
+
const res = await fetch(`${apiBase}/api/v1/missions/start`, {
|
|
179
|
+
method: 'POST',
|
|
180
|
+
headers: {
|
|
181
|
+
'Content-Type': 'application/json',
|
|
182
|
+
'x-api-key': key
|
|
183
|
+
},
|
|
184
|
+
body: JSON.stringify({
|
|
185
|
+
prompt_hash: simpleHash(prompt),
|
|
186
|
+
target_url: targetUrl,
|
|
187
|
+
project_id: projectId || undefined,
|
|
188
|
+
})
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
if (res.status === 429) {
|
|
192
|
+
const data = await res.json();
|
|
193
|
+
return { allowed: false, error: 'mission_limit_exceeded', ...data };
|
|
194
|
+
}
|
|
195
|
+
if (!res.ok) return { allowed: false, error: 'server_error' };
|
|
196
|
+
|
|
197
|
+
return { allowed: true, ...(await res.json()) };
|
|
198
|
+
} catch {
|
|
199
|
+
// Fallback to mock if backend is down
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ── MOCK mode ──
|
|
204
|
+
const dailyUsed = getLocalDailyUsage();
|
|
205
|
+
|
|
206
|
+
if (dailyUsed >= DAILY_MISSION_LIMIT) {
|
|
207
|
+
const tomorrow = new Date();
|
|
208
|
+
tomorrow.setDate(tomorrow.getDate() + 1);
|
|
209
|
+
tomorrow.setHours(0, 0, 0, 0);
|
|
210
|
+
|
|
211
|
+
return {
|
|
212
|
+
allowed: false,
|
|
213
|
+
error: 'mission_limit_exceeded',
|
|
214
|
+
daily_used: dailyUsed,
|
|
215
|
+
daily_limit: DAILY_MISSION_LIMIT,
|
|
216
|
+
remaining: 0,
|
|
217
|
+
resets_at: tomorrow.toISOString()
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const newCount = incrementLocalUsage();
|
|
222
|
+
return {
|
|
223
|
+
allowed: true,
|
|
224
|
+
mode: 'mock',
|
|
225
|
+
mission_id: `mock_${Date.now().toString(36)}`,
|
|
226
|
+
daily_used: newCount,
|
|
227
|
+
daily_limit: DAILY_MISSION_LIMIT,
|
|
228
|
+
remaining: DAILY_MISSION_LIMIT - newCount
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Marks a mission as completed.
|
|
234
|
+
* @param {string} missionId
|
|
235
|
+
* @param {'success'|'failed'|'cancelled'} result
|
|
236
|
+
*/
|
|
237
|
+
export async function endMission(missionId, result) {
|
|
238
|
+
const apiBase = getEffectiveApiBase();
|
|
239
|
+
if (!apiBase || !missionId) return { ok: true };
|
|
240
|
+
|
|
241
|
+
const key = getEffectiveApiKey();
|
|
242
|
+
try {
|
|
243
|
+
await fetch(`${apiBase}/api/v1/missions/${missionId}/end`, {
|
|
244
|
+
method: 'POST',
|
|
245
|
+
headers: {
|
|
246
|
+
'Content-Type': 'application/json',
|
|
247
|
+
'x-api-key': key
|
|
248
|
+
},
|
|
249
|
+
body: JSON.stringify({ result })
|
|
250
|
+
});
|
|
251
|
+
} catch { }
|
|
252
|
+
return { ok: true };
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Simple hash of the prompt — we never send the raw text to backend
|
|
257
|
+
*/
|
|
258
|
+
function simpleHash(str) {
|
|
259
|
+
let hash = 0;
|
|
260
|
+
for (let i = 0; i < str.length; i++) {
|
|
261
|
+
const char = str.charCodeAt(i);
|
|
262
|
+
hash = ((hash << 5) - hash) + char;
|
|
263
|
+
hash |= 0;
|
|
264
|
+
}
|
|
265
|
+
return 'ph_' + Math.abs(hash).toString(36);
|
|
266
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
// src/configLoader.mjs
|
|
2
|
+
// Configuration loader — supports arcality.config (primary) and .env / global config (fallback)
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import os from 'node:os';
|
|
6
|
+
import dotenv from 'dotenv';
|
|
7
|
+
|
|
8
|
+
dotenv.config();
|
|
9
|
+
|
|
10
|
+
export const CONFIG_DIR = path.join(os.homedir(), '.arcality');
|
|
11
|
+
export const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Returns the internal API URL for the Arcality backend.
|
|
15
|
+
* This is loaded exclusively from the tool's .env file (via dotenv).
|
|
16
|
+
* It is NOT configurable by the user, CLI flags, or arcality.config.
|
|
17
|
+
* To change it, modify the ARCALITY_API_URL variable in the tool's .env.
|
|
18
|
+
* @returns {string}
|
|
19
|
+
*/
|
|
20
|
+
export function getApiUrl() {
|
|
21
|
+
return process.env.ARCALITY_API_URL || 'http://localhost:5164';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Loads the local arcality.config file from cwd (if it exists).
|
|
26
|
+
* @returns {object|null}
|
|
27
|
+
*/
|
|
28
|
+
function loadLocalArcalityConfig() {
|
|
29
|
+
try {
|
|
30
|
+
const configPath = path.join(process.cwd(), 'arcality.config');
|
|
31
|
+
if (fs.existsSync(configPath)) {
|
|
32
|
+
let raw = fs.readFileSync(configPath, 'utf8');
|
|
33
|
+
if (raw.charCodeAt(0) === 0xFEFF) raw = raw.slice(1);
|
|
34
|
+
return JSON.parse(raw);
|
|
35
|
+
}
|
|
36
|
+
} catch { }
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Loads configuration from multiple sources:
|
|
42
|
+
* 1. arcality.config (local project config — highest priority)
|
|
43
|
+
* 2. .env (environment variables)
|
|
44
|
+
* 3. ~/.arcality/config.json (global config)
|
|
45
|
+
*
|
|
46
|
+
* NOTE: ARCALITY_API_URL is NOT included here — use getApiUrl() instead.
|
|
47
|
+
*
|
|
48
|
+
* @returns {{
|
|
49
|
+
* ARCALITY_API_KEY: string,
|
|
50
|
+
* ARCALITY_PROJECT_ID?: string
|
|
51
|
+
* }}
|
|
52
|
+
*/
|
|
53
|
+
export function loadConfig() {
|
|
54
|
+
const localConfig = loadLocalArcalityConfig();
|
|
55
|
+
const globalConfig = loadGlobalConfig();
|
|
56
|
+
|
|
57
|
+
const config = {
|
|
58
|
+
ARCALITY_API_KEY: localConfig?.apiKey || process.env.ARCALITY_API_KEY || globalConfig?.api_key,
|
|
59
|
+
ARCALITY_PROJECT_ID: localConfig?.projectId || process.env.ARCALITY_PROJECT_ID,
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
if (!config.ARCALITY_API_KEY) {
|
|
63
|
+
throw new Error('ARCALITY_API_KEY is not defined. Run `arcality init` or set it in your .env file.');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return config;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Reads the user's global config (~/.arcality/config.json)
|
|
72
|
+
* @returns {{ api_key?: string, install_dir?: string, version?: string } | null}
|
|
73
|
+
*/
|
|
74
|
+
export function loadGlobalConfig() {
|
|
75
|
+
try {
|
|
76
|
+
if (fs.existsSync(CONFIG_FILE)) {
|
|
77
|
+
let raw = fs.readFileSync(CONFIG_FILE, 'utf8');
|
|
78
|
+
// Strip BOM that PowerShell adds with -Encoding UTF8
|
|
79
|
+
if (raw.charCodeAt(0) === 0xFEFF) raw = raw.slice(1);
|
|
80
|
+
return JSON.parse(raw);
|
|
81
|
+
}
|
|
82
|
+
} catch { }
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Saves/updates the user's global config
|
|
88
|
+
* @param {object} updates - Fields to update
|
|
89
|
+
*/
|
|
90
|
+
export function saveGlobalConfig(updates) {
|
|
91
|
+
try {
|
|
92
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
93
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
94
|
+
}
|
|
95
|
+
const existing = loadGlobalConfig() || {};
|
|
96
|
+
const merged = { ...existing, ...updates };
|
|
97
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(merged, null, 2), 'utf8');
|
|
98
|
+
return true;
|
|
99
|
+
} catch {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Gets the API Key (priority: arcality.config > env var > global config)
|
|
106
|
+
* @returns {{ key: string | null, source: 'config' | 'env' | 'global' | 'none' }}
|
|
107
|
+
*/
|
|
108
|
+
export function getApiKey() {
|
|
109
|
+
// 1. Local arcality.config
|
|
110
|
+
const localConfig = loadLocalArcalityConfig();
|
|
111
|
+
if (localConfig?.apiKey) {
|
|
112
|
+
process.env.ARCALITY_API_KEY = localConfig.apiKey;
|
|
113
|
+
return { key: localConfig.apiKey, source: 'config' };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// 2. Environment variable (.env or shell)
|
|
117
|
+
if (process.env.ARCALITY_API_KEY) {
|
|
118
|
+
return { key: process.env.ARCALITY_API_KEY, source: 'env' };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// 3. Global config ~/.arcality/config.json
|
|
122
|
+
const globalCfg = loadGlobalConfig();
|
|
123
|
+
if (globalCfg?.api_key) {
|
|
124
|
+
process.env.ARCALITY_API_KEY = globalCfg.api_key;
|
|
125
|
+
return { key: globalCfg.api_key, source: 'global' };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return { key: null, source: 'none' };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Loads additional secrets (like Anthropic) from global config
|
|
133
|
+
*/
|
|
134
|
+
export function setupProcessEnv() {
|
|
135
|
+
const config = loadGlobalConfig();
|
|
136
|
+
if (!config) return;
|
|
137
|
+
|
|
138
|
+
// Load Arcality API Key
|
|
139
|
+
getApiKey();
|
|
140
|
+
|
|
141
|
+
// Load Anthropic API Key if not already in env
|
|
142
|
+
if (config.anthropic_api_key && !process.env.ANTHROPIC_API_KEY) {
|
|
143
|
+
process.env.ANTHROPIC_API_KEY = config.anthropic_api_key;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Load default model if exists
|
|
147
|
+
if (config.model && !process.env.CLAUDE_MODEL) {
|
|
148
|
+
process.env.CLAUDE_MODEL = config.model;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Also inject arcality.config values into process.env for backward compat
|
|
152
|
+
const localConfig = loadLocalArcalityConfig();
|
|
153
|
+
if (localConfig) {
|
|
154
|
+
if (localConfig.apiKey && !process.env.ARCALITY_API_KEY) {
|
|
155
|
+
process.env.ARCALITY_API_KEY = localConfig.apiKey;
|
|
156
|
+
}
|
|
157
|
+
if (localConfig.projectId && !process.env.ARCALITY_PROJECT_ID) {
|
|
158
|
+
process.env.ARCALITY_PROJECT_ID = localConfig.projectId;
|
|
159
|
+
}
|
|
160
|
+
if (localConfig.project?.baseUrl && !process.env.BASE_URL) {
|
|
161
|
+
process.env.BASE_URL = localConfig.project.baseUrl;
|
|
162
|
+
}
|
|
163
|
+
if (localConfig.auth?.username && !process.env.LOGIN_USER) {
|
|
164
|
+
process.env.LOGIN_USER = localConfig.auth.username;
|
|
165
|
+
}
|
|
166
|
+
if (localConfig.auth?.password && !process.env.LOGIN_PASSWORD) {
|
|
167
|
+
process.env.LOGIN_PASSWORD = localConfig.auth.password;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Visual mask of API Key for banner display
|
|
174
|
+
* "arc_k_live_abc123xyz" → "arc_k_****xyz"
|
|
175
|
+
*/
|
|
176
|
+
export function maskApiKey(key) {
|
|
177
|
+
if (!key || key.length < 8) return '***';
|
|
178
|
+
return key.slice(0, 6) + '****' + key.slice(-4);
|
|
179
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
// src/configManager.mjs
|
|
2
|
+
// Manages the local arcality.config file — single configuration per project.
|
|
3
|
+
// Replaces multi-config .env approach.
|
|
4
|
+
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
|
|
8
|
+
const CONFIG_FILENAME = 'arcality.config';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Resolves the path to arcality.config in the project root.
|
|
12
|
+
* @param {string} [projectRoot] - Override project root. Defaults to cwd.
|
|
13
|
+
* @returns {string}
|
|
14
|
+
*/
|
|
15
|
+
export function getConfigPath(projectRoot) {
|
|
16
|
+
return path.join(projectRoot || process.cwd(), CONFIG_FILENAME);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Checks if arcality.config exists in the project.
|
|
21
|
+
* @param {string} [projectRoot]
|
|
22
|
+
* @returns {boolean}
|
|
23
|
+
*/
|
|
24
|
+
export function configExists(projectRoot) {
|
|
25
|
+
return fs.existsSync(getConfigPath(projectRoot));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Reads and parses the arcality.config file.
|
|
30
|
+
* @param {string} [projectRoot]
|
|
31
|
+
* @returns {ArcalityConfig | null}
|
|
32
|
+
*/
|
|
33
|
+
export function loadProjectConfig(projectRoot) {
|
|
34
|
+
const configPath = getConfigPath(projectRoot);
|
|
35
|
+
try {
|
|
36
|
+
if (!fs.existsSync(configPath)) return null;
|
|
37
|
+
let raw = fs.readFileSync(configPath, 'utf8');
|
|
38
|
+
// Strip BOM if present
|
|
39
|
+
if (raw.charCodeAt(0) === 0xFEFF) raw = raw.slice(1);
|
|
40
|
+
return JSON.parse(raw);
|
|
41
|
+
} catch (err) {
|
|
42
|
+
console.error(`⚠️ Error reading ${CONFIG_FILENAME}: ${err.message}`);
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Writes the arcality.config file.
|
|
49
|
+
* @param {ArcalityConfig} config
|
|
50
|
+
* @param {string} [projectRoot]
|
|
51
|
+
*/
|
|
52
|
+
export function saveProjectConfig(config, projectRoot) {
|
|
53
|
+
const configPath = getConfigPath(projectRoot);
|
|
54
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf8');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Validates that the config has all required fields for running.
|
|
59
|
+
* @param {ArcalityConfig} config
|
|
60
|
+
* @returns {{ valid: boolean, missing: string[] }}
|
|
61
|
+
*/
|
|
62
|
+
export function validateConfig(config) {
|
|
63
|
+
const missing = [];
|
|
64
|
+
|
|
65
|
+
if (!config) {
|
|
66
|
+
return { valid: false, missing: ['arcality.config file'] };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (!config.apiKey) missing.push('apiKey');
|
|
70
|
+
if (!config.projectId) missing.push('projectId');
|
|
71
|
+
if (!config.project?.baseUrl) missing.push('project.baseUrl');
|
|
72
|
+
if (!config.auth?.username) missing.push('auth.username');
|
|
73
|
+
if (!config.auth?.password) missing.push('auth.password');
|
|
74
|
+
|
|
75
|
+
return { valid: missing.length === 0, missing };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Creates a fresh arcality.config structure.
|
|
80
|
+
* @param {object} params
|
|
81
|
+
* @returns {ArcalityConfig}
|
|
82
|
+
*/
|
|
83
|
+
export function createConfig({
|
|
84
|
+
apiKey,
|
|
85
|
+
organizationId,
|
|
86
|
+
projectId,
|
|
87
|
+
projectName,
|
|
88
|
+
baseUrl,
|
|
89
|
+
frameworkDetected,
|
|
90
|
+
username,
|
|
91
|
+
password,
|
|
92
|
+
arcalityVersion,
|
|
93
|
+
yamlOutputDir = './arcality',
|
|
94
|
+
}) {
|
|
95
|
+
return {
|
|
96
|
+
version: '1',
|
|
97
|
+
apiKey,
|
|
98
|
+
organizationId: organizationId || null,
|
|
99
|
+
projectId,
|
|
100
|
+
project: {
|
|
101
|
+
name: projectName,
|
|
102
|
+
baseUrl,
|
|
103
|
+
frameworkDetected: frameworkDetected || null,
|
|
104
|
+
},
|
|
105
|
+
auth: {
|
|
106
|
+
username,
|
|
107
|
+
password,
|
|
108
|
+
},
|
|
109
|
+
runtime: {
|
|
110
|
+
yamlOutputDir,
|
|
111
|
+
reuseSuccessfulYamls: true,
|
|
112
|
+
singleConfigurationMode: true,
|
|
113
|
+
},
|
|
114
|
+
meta: {
|
|
115
|
+
arcalityVersion: arcalityVersion || 'unknown',
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Injects arcality.config values into process.env for backward compat
|
|
122
|
+
* with existing modules that read from process.env.
|
|
123
|
+
* @param {ArcalityConfig} config
|
|
124
|
+
*/
|
|
125
|
+
export function injectConfigToEnv(config) {
|
|
126
|
+
if (!config) return;
|
|
127
|
+
|
|
128
|
+
if (config.apiKey) process.env.ARCALITY_API_KEY = config.apiKey;
|
|
129
|
+
if (config.projectId) process.env.ARCALITY_PROJECT_ID = config.projectId;
|
|
130
|
+
if (config.project?.baseUrl) {
|
|
131
|
+
process.env.BASE_URL = config.project.baseUrl;
|
|
132
|
+
}
|
|
133
|
+
if (config.auth?.username) process.env.LOGIN_USER = config.auth.username;
|
|
134
|
+
if (config.auth?.password) process.env.LOGIN_PASSWORD = config.auth.password;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Gets the YAML output directory from config, falling back to default.
|
|
139
|
+
* @param {ArcalityConfig} config
|
|
140
|
+
* @param {string} [projectRoot]
|
|
141
|
+
* @returns {string} absolute path
|
|
142
|
+
*/
|
|
143
|
+
export function getYamlOutputDir(config, projectRoot) {
|
|
144
|
+
const dir = config?.runtime?.yamlOutputDir || './arcality';
|
|
145
|
+
return path.resolve(projectRoot || process.cwd(), dir);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Ensures the YAML output directory exists.
|
|
150
|
+
* @param {ArcalityConfig} config
|
|
151
|
+
* @param {string} [projectRoot]
|
|
152
|
+
* @returns {string} absolute path to the directory
|
|
153
|
+
*/
|
|
154
|
+
export function ensureYamlOutputDir(config, projectRoot) {
|
|
155
|
+
const dir = getYamlOutputDir(config, projectRoot);
|
|
156
|
+
if (!fs.existsSync(dir)) {
|
|
157
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
158
|
+
}
|
|
159
|
+
return dir;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* @typedef {object} ArcalityConfig
|
|
164
|
+
* @property {string} version
|
|
165
|
+
* @property {string} apiKey
|
|
166
|
+
* @property {string|null} organizationId
|
|
167
|
+
* @property {string} projectId
|
|
168
|
+
* @property {{ name: string, baseUrl: string, frameworkDetected: string|null }} project
|
|
169
|
+
* @property {{ username: string, password: string }} auth
|
|
170
|
+
* @property {{ yamlOutputDir: string, reuseSuccessfulYamls: boolean, singleConfigurationMode: boolean }} runtime
|
|
171
|
+
* @property {{ arcalityVersion: string }} meta
|
|
172
|
+
*/
|
|
@@ -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
|
+
}
|