@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.
Files changed (97) hide show
  1. package/.agents/skills/e2e-testing-expert/SKILL.md +28 -0
  2. package/.agents/skills/frontend-design/LICENSE.txt +177 -0
  3. package/.agents/skills/frontend-design/SKILL.md +42 -0
  4. package/.agents/skills/nodejs-backend-patterns/SKILL.md +639 -0
  5. package/.agents/skills/nodejs-backend-patterns/references/advanced-patterns.md +430 -0
  6. package/.agents/skills/playwright-best-practices/LICENSE.md +7 -0
  7. package/.agents/skills/playwright-best-practices/README.md +147 -0
  8. package/.agents/skills/playwright-best-practices/SKILL.md +303 -0
  9. package/.agents/skills/playwright-best-practices/advanced/authentication-flows.md +360 -0
  10. package/.agents/skills/playwright-best-practices/advanced/authentication.md +871 -0
  11. package/.agents/skills/playwright-best-practices/advanced/clock-mocking.md +364 -0
  12. package/.agents/skills/playwright-best-practices/advanced/mobile-testing.md +409 -0
  13. package/.agents/skills/playwright-best-practices/advanced/multi-context.md +288 -0
  14. package/.agents/skills/playwright-best-practices/advanced/multi-user.md +393 -0
  15. package/.agents/skills/playwright-best-practices/advanced/network-advanced.md +452 -0
  16. package/.agents/skills/playwright-best-practices/advanced/third-party.md +464 -0
  17. package/.agents/skills/playwright-best-practices/architecture/pom-vs-fixtures.md +363 -0
  18. package/.agents/skills/playwright-best-practices/architecture/test-architecture.md +369 -0
  19. package/.agents/skills/playwright-best-practices/architecture/when-to-mock.md +383 -0
  20. package/.agents/skills/playwright-best-practices/browser-apis/browser-apis.md +391 -0
  21. package/.agents/skills/playwright-best-practices/browser-apis/iframes.md +403 -0
  22. package/.agents/skills/playwright-best-practices/browser-apis/service-workers.md +504 -0
  23. package/.agents/skills/playwright-best-practices/browser-apis/websockets.md +403 -0
  24. package/.agents/skills/playwright-best-practices/core/annotations.md +424 -0
  25. package/.agents/skills/playwright-best-practices/core/assertions-waiting.md +361 -0
  26. package/.agents/skills/playwright-best-practices/core/configuration.md +452 -0
  27. package/.agents/skills/playwright-best-practices/core/fixtures-hooks.md +417 -0
  28. package/.agents/skills/playwright-best-practices/core/global-setup.md +434 -0
  29. package/.agents/skills/playwright-best-practices/core/locators.md +242 -0
  30. package/.agents/skills/playwright-best-practices/core/page-object-model.md +315 -0
  31. package/.agents/skills/playwright-best-practices/core/projects-dependencies.md +453 -0
  32. package/.agents/skills/playwright-best-practices/core/test-data.md +492 -0
  33. package/.agents/skills/playwright-best-practices/core/test-suite-structure.md +361 -0
  34. package/.agents/skills/playwright-best-practices/core/test-tags.md +298 -0
  35. package/.agents/skills/playwright-best-practices/debugging/console-errors.md +420 -0
  36. package/.agents/skills/playwright-best-practices/debugging/debugging.md +504 -0
  37. package/.agents/skills/playwright-best-practices/debugging/error-testing.md +360 -0
  38. package/.agents/skills/playwright-best-practices/debugging/flaky-tests.md +496 -0
  39. package/.agents/skills/playwright-best-practices/frameworks/angular.md +530 -0
  40. package/.agents/skills/playwright-best-practices/frameworks/nextjs.md +469 -0
  41. package/.agents/skills/playwright-best-practices/frameworks/react.md +531 -0
  42. package/.agents/skills/playwright-best-practices/frameworks/vue.md +574 -0
  43. package/.agents/skills/playwright-best-practices/infrastructure-ci-cd/ci-cd.md +468 -0
  44. package/.agents/skills/playwright-best-practices/infrastructure-ci-cd/docker.md +283 -0
  45. package/.agents/skills/playwright-best-practices/infrastructure-ci-cd/github-actions.md +546 -0
  46. package/.agents/skills/playwright-best-practices/infrastructure-ci-cd/gitlab.md +397 -0
  47. package/.agents/skills/playwright-best-practices/infrastructure-ci-cd/other-providers.md +521 -0
  48. package/.agents/skills/playwright-best-practices/infrastructure-ci-cd/parallel-sharding.md +371 -0
  49. package/.agents/skills/playwright-best-practices/infrastructure-ci-cd/performance.md +453 -0
  50. package/.agents/skills/playwright-best-practices/infrastructure-ci-cd/reporting.md +424 -0
  51. package/.agents/skills/playwright-best-practices/infrastructure-ci-cd/test-coverage.md +497 -0
  52. package/.agents/skills/playwright-best-practices/testing-patterns/accessibility.md +359 -0
  53. package/.agents/skills/playwright-best-practices/testing-patterns/api-testing.md +719 -0
  54. package/.agents/skills/playwright-best-practices/testing-patterns/browser-extensions.md +506 -0
  55. package/.agents/skills/playwright-best-practices/testing-patterns/canvas-webgl.md +493 -0
  56. package/.agents/skills/playwright-best-practices/testing-patterns/component-testing.md +500 -0
  57. package/.agents/skills/playwright-best-practices/testing-patterns/drag-drop.md +576 -0
  58. package/.agents/skills/playwright-best-practices/testing-patterns/electron.md +509 -0
  59. package/.agents/skills/playwright-best-practices/testing-patterns/file-operations.md +377 -0
  60. package/.agents/skills/playwright-best-practices/testing-patterns/file-upload-download.md +562 -0
  61. package/.agents/skills/playwright-best-practices/testing-patterns/forms-validation.md +561 -0
  62. package/.agents/skills/playwright-best-practices/testing-patterns/graphql-testing.md +331 -0
  63. package/.agents/skills/playwright-best-practices/testing-patterns/i18n.md +508 -0
  64. package/.agents/skills/playwright-best-practices/testing-patterns/performance-testing.md +476 -0
  65. package/.agents/skills/playwright-best-practices/testing-patterns/security-testing.md +430 -0
  66. package/.agents/skills/playwright-best-practices/testing-patterns/visual-regression.md +634 -0
  67. package/.env.example +21 -0
  68. package/README.md +30 -0
  69. package/bin/arcality.mjs +86 -0
  70. package/package.json +66 -0
  71. package/playwright.config.ts +12 -0
  72. package/scripts/cleanup-qmsdev.mjs +63 -0
  73. package/scripts/discover-view.mjs +52 -0
  74. package/scripts/extract-view.mjs +64 -0
  75. package/scripts/gen-and-run.mjs +838 -0
  76. package/scripts/init.mjs +290 -0
  77. package/scripts/migrate-to-central-out.mjs +157 -0
  78. package/scripts/postinstall.mjs +63 -0
  79. package/scripts/rebrand-report.mjs +241 -0
  80. package/scripts/setup.mjs +166 -0
  81. package/src/KnowledgeService.ts +239 -0
  82. package/src/arcalityClient.mjs +266 -0
  83. package/src/configLoader.mjs +179 -0
  84. package/src/configManager.mjs +172 -0
  85. package/src/consoleBanner.ts +32 -0
  86. package/src/envSetup.ts +205 -0
  87. package/src/index.ts +25 -0
  88. package/src/projectInspector.ts +42 -0
  89. package/src/services/collectiveMemoryService.ts +178 -0
  90. package/src/testRunner.ts +201 -0
  91. package/tests/_helpers/ArcalityReporter.ts +490 -0
  92. package/tests/_helpers/agentic-runner.spec.ts +741 -0
  93. package/tests/_helpers/ai-agent-helper.ts +1573 -0
  94. package/tests/_helpers/discover-view.spec.ts +238 -0
  95. package/tests/_helpers/extract-view.spec.ts +118 -0
  96. package/tests/_helpers/qa-tools.ts +333 -0
  97. 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
+ }