@arcadialdev/arcality 2.4.24 → 2.4.25
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 -28
- package/.agents/skills/security-qa/SKILL.md +254 -0
- package/README.md +95 -95
- package/bin/arcality.mjs +55 -55
- package/package.json +68 -68
- package/playwright.config.js +33 -33
- package/scripts/gen-and-run.mjs +4 -1
- package/scripts/init.mjs +297 -297
- package/scripts/postinstall.mjs +77 -77
- package/scripts/setup.mjs +166 -166
- package/src/configLoader.mjs +179 -179
- package/src/configManager.mjs +172 -172
- package/src/envSetup.ts +205 -205
- package/src/index.ts +25 -25
- package/src/services/collectiveMemoryService.ts +1 -1
- package/src/services/securityScanner.ts +128 -0
- package/tests/_helpers/ArcalityReporter.js +632 -229
- package/tests/_helpers/ArcalityReporter.ts +262 -27
- package/tests/_helpers/agentic-runner.bundle.spec.js +357 -5
- package/tests/_helpers/agentic-runner.spec.ts +37 -1
- package/tests/_helpers/ai-agent-helper.ts +128 -6
- package/tests/_helpers/qa-security-tools.ts +265 -0
package/scripts/init.mjs
CHANGED
|
@@ -1,297 +1,297 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// scripts/init.mjs — Command: arcality init
|
|
3
|
-
// Single-configuration setup flow:
|
|
4
|
-
// 1. Prompt API key → validate with backend
|
|
5
|
-
// 2. Prompt project name, baseUrl, username, password
|
|
6
|
-
// 3. Detect framework (package.json only, NO .git)
|
|
7
|
-
// 4. Create project via POST /api/v1/projects
|
|
8
|
-
// 5. Write arcality.config
|
|
9
|
-
|
|
10
|
-
import 'dotenv/config';
|
|
11
|
-
import fs from 'node:fs';
|
|
12
|
-
import path from 'node:path';
|
|
13
|
-
import { fileURLToPath } from 'url';
|
|
14
|
-
import { intro, outro, text, password as passwordPrompt, spinner, note, isCancel, cancel, confirm } from '@clack/prompts';
|
|
15
|
-
import chalk from 'chalk';
|
|
16
|
-
import figlet from 'figlet';
|
|
17
|
-
import {
|
|
18
|
-
configExists,
|
|
19
|
-
loadProjectConfig,
|
|
20
|
-
saveProjectConfig,
|
|
21
|
-
createConfig,
|
|
22
|
-
} from '../src/configManager.mjs';
|
|
23
|
-
import { getApiUrl } from '../src/configLoader.mjs';
|
|
24
|
-
|
|
25
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
26
|
-
const __dirname = path.dirname(__filename);
|
|
27
|
-
const PACKAGE_ROOT = process.env.ARCALITY_ROOT || path.resolve(__dirname, '..');
|
|
28
|
-
|
|
29
|
-
// ── Helpers ──
|
|
30
|
-
|
|
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';
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function detectFramework(projectRoot) {
|
|
41
|
-
try {
|
|
42
|
-
const pkgPath = path.join(projectRoot, 'package.json');
|
|
43
|
-
if (!fs.existsSync(pkgPath)) return null;
|
|
44
|
-
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
45
|
-
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
46
|
-
if (deps['next']) return 'Next.js';
|
|
47
|
-
if (deps['nuxt'] || deps['nuxt3']) return 'Nuxt';
|
|
48
|
-
if (deps['vite']) return 'Vite';
|
|
49
|
-
if (deps['react-scripts']) return 'Create React App';
|
|
50
|
-
if (deps['@angular/core']) return 'Angular';
|
|
51
|
-
if (deps['vue']) return 'Vue.js';
|
|
52
|
-
if (deps['svelte'] || deps['@sveltejs/kit']) return 'Svelte';
|
|
53
|
-
return null;
|
|
54
|
-
} catch {
|
|
55
|
-
return null;
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
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
|
-
}
|
|
67
|
-
|
|
68
|
-
// ── Main ──
|
|
69
|
-
|
|
70
|
-
async function main() {
|
|
71
|
-
const version = getVersion();
|
|
72
|
-
const projectRoot = process.cwd();
|
|
73
|
-
|
|
74
|
-
console.clear();
|
|
75
|
-
const logo = figlet.textSync('Arcality', { font: 'Standard' });
|
|
76
|
-
intro(chalk.cyanBright(logo));
|
|
77
|
-
|
|
78
|
-
note(
|
|
79
|
-
chalk.magentaBright('Project Initialization') + '\n' +
|
|
80
|
-
chalk.gray('─'.repeat(50)) + '\n' +
|
|
81
|
-
chalk.bold.white('📦 Version: ') + chalk.yellow(version) + '\n' +
|
|
82
|
-
chalk.bold.white('📁 Project: ') + chalk.yellow(projectRoot),
|
|
83
|
-
'arcality init'
|
|
84
|
-
);
|
|
85
|
-
|
|
86
|
-
// ── Check for existing config ──
|
|
87
|
-
if (configExists(projectRoot)) {
|
|
88
|
-
const existing = loadProjectConfig(projectRoot);
|
|
89
|
-
if (existing) {
|
|
90
|
-
note(
|
|
91
|
-
chalk.yellow('⚠️ An arcality.config already exists in this project.') + '\n\n' +
|
|
92
|
-
chalk.white(' Project: ') + chalk.cyan(existing.project?.name || 'unknown') + '\n' +
|
|
93
|
-
chalk.white(' Base URL: ') + chalk.cyan(existing.project?.baseUrl || 'unknown') + '\n' +
|
|
94
|
-
chalk.white(' Project ID: ') + chalk.gray(existing.projectId || 'unknown'),
|
|
95
|
-
'Existing Configuration'
|
|
96
|
-
);
|
|
97
|
-
|
|
98
|
-
const overwrite = await confirm({
|
|
99
|
-
message: 'Do you want to reconfigure? This will create a NEW project in backend.',
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
if (isCancel(overwrite) || !overwrite) {
|
|
103
|
-
outro(chalk.cyan('Configuration unchanged. Use `arcality run` to start.'));
|
|
104
|
-
return;
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// ── Step 1: API Key ──
|
|
110
|
-
const apiKey = await text({
|
|
111
|
-
message: chalk.cyan('🔑 Enter your Arcality API Key:'),
|
|
112
|
-
placeholder: 'arc_k_live_xxxxx',
|
|
113
|
-
validate: (v) => {
|
|
114
|
-
if (!v || !v.trim()) return 'API Key is required.';
|
|
115
|
-
if (!v.startsWith('arc_k_')) return 'API Key must start with "arc_k_"';
|
|
116
|
-
},
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
if (isCancel(apiKey)) {
|
|
120
|
-
cancel('Setup cancelled.');
|
|
121
|
-
process.exit(0);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// ── Step 2: Validate API Key (uses internal API URL) ──
|
|
125
|
-
const apiUrl = getApiUrl();
|
|
126
|
-
const s = spinner();
|
|
127
|
-
s.start('Validating API Key...');
|
|
128
|
-
|
|
129
|
-
let organizationId = null;
|
|
130
|
-
|
|
131
|
-
try {
|
|
132
|
-
const res = await fetch(`${apiUrl}/api/v1/auth/validate`, {
|
|
133
|
-
method: 'POST',
|
|
134
|
-
headers: {
|
|
135
|
-
'Content-Type': 'application/json',
|
|
136
|
-
'x-api-key': apiKey.trim(),
|
|
137
|
-
},
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
if (!res.ok) {
|
|
141
|
-
const errText = await res.text().catch(() => '');
|
|
142
|
-
const errorMap = {
|
|
143
|
-
401: '❌ Invalid API Key. Please check your key and try again.',
|
|
144
|
-
403: `❌ Authentication Failed (HTTP 403). Server says: ${errText.slice(0, 150)}`,
|
|
145
|
-
};
|
|
146
|
-
s.stop(chalk.red(errorMap[res.status] || `❌ Server error (HTTP ${res.status}): ${errText.slice(0, 150)}`));
|
|
147
|
-
process.exit(1);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
const data = await res.json();
|
|
151
|
-
organizationId = data.organizationId || data.organization_id || null;
|
|
152
|
-
|
|
153
|
-
s.stop(chalk.green('✅ API Key validated successfully'));
|
|
154
|
-
|
|
155
|
-
if (data.plan) {
|
|
156
|
-
note(
|
|
157
|
-
chalk.bold.white('📋 Plan: ') + chalk.cyan(data.plan) + '\n' +
|
|
158
|
-
chalk.bold.white('🏢 Org: ') + chalk.cyan(organizationId || 'N/A'),
|
|
159
|
-
'Account Info'
|
|
160
|
-
);
|
|
161
|
-
}
|
|
162
|
-
} catch (err) {
|
|
163
|
-
s.stop(chalk.red(`❌ Could not connect to Arcality backend: ${err.message}`));
|
|
164
|
-
console.log(chalk.yellow(' Make sure the backend is running and accessible.'));
|
|
165
|
-
process.exit(1);
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// ── Step 3: Project details ──
|
|
169
|
-
const projectName = await text({
|
|
170
|
-
message: chalk.cyan('📋 Project name:'),
|
|
171
|
-
placeholder: 'My Portal QA',
|
|
172
|
-
validate: (v) => {
|
|
173
|
-
if (!v || v.trim().length < 2) return 'Project name is required (min 2 chars).';
|
|
174
|
-
},
|
|
175
|
-
});
|
|
176
|
-
if (isCancel(projectName)) { cancel('Setup cancelled.'); process.exit(0); }
|
|
177
|
-
|
|
178
|
-
const baseUrl = await text({
|
|
179
|
-
message: chalk.cyan('🌐 Base URL of the application to test:'),
|
|
180
|
-
placeholder: 'https://staging.example.com',
|
|
181
|
-
validate: (v) => {
|
|
182
|
-
if (!v || !v.trim()) return 'Base URL is required.';
|
|
183
|
-
if (!isValidUrl(v)) return 'Invalid URL. Use http:// or https://';
|
|
184
|
-
},
|
|
185
|
-
});
|
|
186
|
-
if (isCancel(baseUrl)) { cancel('Setup cancelled.'); process.exit(0); }
|
|
187
|
-
|
|
188
|
-
const username = await text({
|
|
189
|
-
message: chalk.cyan('👤 Login username / email:'),
|
|
190
|
-
validate: (v) => {
|
|
191
|
-
if (!v || !v.trim()) return 'Username is required.';
|
|
192
|
-
},
|
|
193
|
-
});
|
|
194
|
-
if (isCancel(username)) { cancel('Setup cancelled.'); process.exit(0); }
|
|
195
|
-
|
|
196
|
-
const password = await passwordPrompt({
|
|
197
|
-
message: chalk.cyan('🔒 Login password:'),
|
|
198
|
-
validate: (v) => {
|
|
199
|
-
if (!v || !v.length) return 'Password is required.';
|
|
200
|
-
},
|
|
201
|
-
});
|
|
202
|
-
if (isCancel(password)) { cancel('Setup cancelled.'); process.exit(0); }
|
|
203
|
-
|
|
204
|
-
// ── Step 4: Detect framework ──
|
|
205
|
-
const frameworkDetected = detectFramework(projectRoot);
|
|
206
|
-
if (frameworkDetected) {
|
|
207
|
-
console.log(chalk.gray(` 🛠️ Framework detected: ${chalk.magenta(frameworkDetected)}`));
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
// ── Step 5: Create project in backend ──
|
|
211
|
-
const sCreate = spinner();
|
|
212
|
-
sCreate.start('Creating project in Arcality backend...');
|
|
213
|
-
|
|
214
|
-
let projectId = null;
|
|
215
|
-
try {
|
|
216
|
-
const payload = {
|
|
217
|
-
name: projectName.trim(),
|
|
218
|
-
base_url: baseUrl.trim(),
|
|
219
|
-
framework_detected: frameworkDetected || undefined,
|
|
220
|
-
arcality_version: version,
|
|
221
|
-
config: {
|
|
222
|
-
source: 'npm-cli',
|
|
223
|
-
single_configuration_mode: true,
|
|
224
|
-
yaml_reuse_enabled: true,
|
|
225
|
-
},
|
|
226
|
-
};
|
|
227
|
-
|
|
228
|
-
const res = await fetch(`${apiUrl}/api/v1/projects`, {
|
|
229
|
-
method: 'POST',
|
|
230
|
-
headers: {
|
|
231
|
-
'Content-Type': 'application/json',
|
|
232
|
-
'x-api-key': apiKey.trim(),
|
|
233
|
-
},
|
|
234
|
-
body: JSON.stringify(payload),
|
|
235
|
-
});
|
|
236
|
-
|
|
237
|
-
if (!res.ok) {
|
|
238
|
-
const errText = await res.text().catch(() => '');
|
|
239
|
-
// If response is HTML (e.g. Cloudflare 521), show a clean message
|
|
240
|
-
const isHtml = errText.trim().startsWith('<');
|
|
241
|
-
const displayError = isHtml
|
|
242
|
-
? `The Arcality backend server is temporarily unavailable (HTTP ${res.status}). The project will be saved locally only.`
|
|
243
|
-
: `HTTP ${res.status}: ${errText.slice(0, 200)}`;
|
|
244
|
-
|
|
245
|
-
sCreate.stop(chalk.yellow(`⚠️ ${displayError}`));
|
|
246
|
-
// Don't exit — write the config with a local-only project ID so user can still run
|
|
247
|
-
projectId = projectId || `local_${Date.now()}`;
|
|
248
|
-
} else {
|
|
249
|
-
const created = await res.json();
|
|
250
|
-
projectId = created.id || created.Id;
|
|
251
|
-
organizationId = organizationId || created.organizationId || created.organization_id || null;
|
|
252
|
-
sCreate.stop(chalk.green(`✅ Project created: "${projectName.trim()}" (${projectId})`));
|
|
253
|
-
}
|
|
254
|
-
} catch (err) {
|
|
255
|
-
sCreate.stop(chalk.yellow(`⚠️ Could not reach Arcality server: ${err.message}. Continuing in local mode.`));
|
|
256
|
-
projectId = projectId || `local_${Date.now()}`;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
// ── Step 6: Write arcality.config ──
|
|
260
|
-
const config = createConfig({
|
|
261
|
-
apiKey: apiKey.trim(),
|
|
262
|
-
organizationId,
|
|
263
|
-
projectId,
|
|
264
|
-
projectName: projectName.trim(),
|
|
265
|
-
baseUrl: baseUrl.trim(),
|
|
266
|
-
frameworkDetected,
|
|
267
|
-
username: username.trim(),
|
|
268
|
-
password,
|
|
269
|
-
arcalityVersion: version,
|
|
270
|
-
});
|
|
271
|
-
|
|
272
|
-
saveProjectConfig(config, projectRoot);
|
|
273
|
-
|
|
274
|
-
// ── Summary ──
|
|
275
|
-
note(
|
|
276
|
-
chalk.green('✅ arcality.config created successfully!') + '\n\n' +
|
|
277
|
-
chalk.bold.white(' Project: ') + chalk.cyan(config.project.name) + '\n' +
|
|
278
|
-
chalk.bold.white(' Base URL: ') + chalk.cyan(config.project.baseUrl) + '\n' +
|
|
279
|
-
chalk.bold.white(' Framework: ') + chalk.magenta(config.project.frameworkDetected || 'N/A') + '\n' +
|
|
280
|
-
chalk.bold.white(' Project ID: ') + chalk.gray(config.projectId) + '\n' +
|
|
281
|
-
chalk.bold.white(' YAML Dir: ') + chalk.yellow(config.runtime.yamlOutputDir),
|
|
282
|
-
'Configuration Saved'
|
|
283
|
-
);
|
|
284
|
-
|
|
285
|
-
outro(
|
|
286
|
-
chalk.cyanBright('🚀 Ready! Run ') +
|
|
287
|
-
chalk.bold.white('arcality run') +
|
|
288
|
-
chalk.cyanBright(' or ') +
|
|
289
|
-
chalk.bold.white('arcality') +
|
|
290
|
-
chalk.cyanBright(' to start testing.')
|
|
291
|
-
);
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
main().catch(err => {
|
|
295
|
-
console.error(chalk.red(' ❌ Error during init:'), err.message);
|
|
296
|
-
process.exit(1);
|
|
297
|
-
});
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// scripts/init.mjs — Command: arcality init
|
|
3
|
+
// Single-configuration setup flow:
|
|
4
|
+
// 1. Prompt API key → validate with backend
|
|
5
|
+
// 2. Prompt project name, baseUrl, username, password
|
|
6
|
+
// 3. Detect framework (package.json only, NO .git)
|
|
7
|
+
// 4. Create project via POST /api/v1/projects
|
|
8
|
+
// 5. Write arcality.config
|
|
9
|
+
|
|
10
|
+
import 'dotenv/config';
|
|
11
|
+
import fs from 'node:fs';
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
import { fileURLToPath } from 'url';
|
|
14
|
+
import { intro, outro, text, password as passwordPrompt, spinner, note, isCancel, cancel, confirm } from '@clack/prompts';
|
|
15
|
+
import chalk from 'chalk';
|
|
16
|
+
import figlet from 'figlet';
|
|
17
|
+
import {
|
|
18
|
+
configExists,
|
|
19
|
+
loadProjectConfig,
|
|
20
|
+
saveProjectConfig,
|
|
21
|
+
createConfig,
|
|
22
|
+
} from '../src/configManager.mjs';
|
|
23
|
+
import { getApiUrl } from '../src/configLoader.mjs';
|
|
24
|
+
|
|
25
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
26
|
+
const __dirname = path.dirname(__filename);
|
|
27
|
+
const PACKAGE_ROOT = process.env.ARCALITY_ROOT || path.resolve(__dirname, '..');
|
|
28
|
+
|
|
29
|
+
// ── Helpers ──
|
|
30
|
+
|
|
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';
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function detectFramework(projectRoot) {
|
|
41
|
+
try {
|
|
42
|
+
const pkgPath = path.join(projectRoot, 'package.json');
|
|
43
|
+
if (!fs.existsSync(pkgPath)) return null;
|
|
44
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
45
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
46
|
+
if (deps['next']) return 'Next.js';
|
|
47
|
+
if (deps['nuxt'] || deps['nuxt3']) return 'Nuxt';
|
|
48
|
+
if (deps['vite']) return 'Vite';
|
|
49
|
+
if (deps['react-scripts']) return 'Create React App';
|
|
50
|
+
if (deps['@angular/core']) return 'Angular';
|
|
51
|
+
if (deps['vue']) return 'Vue.js';
|
|
52
|
+
if (deps['svelte'] || deps['@sveltejs/kit']) return 'Svelte';
|
|
53
|
+
return null;
|
|
54
|
+
} catch {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
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
|
+
}
|
|
67
|
+
|
|
68
|
+
// ── Main ──
|
|
69
|
+
|
|
70
|
+
async function main() {
|
|
71
|
+
const version = getVersion();
|
|
72
|
+
const projectRoot = process.cwd();
|
|
73
|
+
|
|
74
|
+
console.clear();
|
|
75
|
+
const logo = figlet.textSync('Arcality', { font: 'Standard' });
|
|
76
|
+
intro(chalk.cyanBright(logo));
|
|
77
|
+
|
|
78
|
+
note(
|
|
79
|
+
chalk.magentaBright('Project Initialization') + '\n' +
|
|
80
|
+
chalk.gray('─'.repeat(50)) + '\n' +
|
|
81
|
+
chalk.bold.white('📦 Version: ') + chalk.yellow(version) + '\n' +
|
|
82
|
+
chalk.bold.white('📁 Project: ') + chalk.yellow(projectRoot),
|
|
83
|
+
'arcality init'
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
// ── Check for existing config ──
|
|
87
|
+
if (configExists(projectRoot)) {
|
|
88
|
+
const existing = loadProjectConfig(projectRoot);
|
|
89
|
+
if (existing) {
|
|
90
|
+
note(
|
|
91
|
+
chalk.yellow('⚠️ An arcality.config already exists in this project.') + '\n\n' +
|
|
92
|
+
chalk.white(' Project: ') + chalk.cyan(existing.project?.name || 'unknown') + '\n' +
|
|
93
|
+
chalk.white(' Base URL: ') + chalk.cyan(existing.project?.baseUrl || 'unknown') + '\n' +
|
|
94
|
+
chalk.white(' Project ID: ') + chalk.gray(existing.projectId || 'unknown'),
|
|
95
|
+
'Existing Configuration'
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
const overwrite = await confirm({
|
|
99
|
+
message: 'Do you want to reconfigure? This will create a NEW project in backend.',
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
if (isCancel(overwrite) || !overwrite) {
|
|
103
|
+
outro(chalk.cyan('Configuration unchanged. Use `arcality run` to start.'));
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ── Step 1: API Key ──
|
|
110
|
+
const apiKey = await text({
|
|
111
|
+
message: chalk.cyan('🔑 Enter your Arcality API Key:'),
|
|
112
|
+
placeholder: 'arc_k_live_xxxxx',
|
|
113
|
+
validate: (v) => {
|
|
114
|
+
if (!v || !v.trim()) return 'API Key is required.';
|
|
115
|
+
if (!v.startsWith('arc_k_')) return 'API Key must start with "arc_k_"';
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
if (isCancel(apiKey)) {
|
|
120
|
+
cancel('Setup cancelled.');
|
|
121
|
+
process.exit(0);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ── Step 2: Validate API Key (uses internal API URL) ──
|
|
125
|
+
const apiUrl = getApiUrl();
|
|
126
|
+
const s = spinner();
|
|
127
|
+
s.start('Validating API Key...');
|
|
128
|
+
|
|
129
|
+
let organizationId = null;
|
|
130
|
+
|
|
131
|
+
try {
|
|
132
|
+
const res = await fetch(`${apiUrl}/api/v1/auth/validate`, {
|
|
133
|
+
method: 'POST',
|
|
134
|
+
headers: {
|
|
135
|
+
'Content-Type': 'application/json',
|
|
136
|
+
'x-api-key': apiKey.trim(),
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
if (!res.ok) {
|
|
141
|
+
const errText = await res.text().catch(() => '');
|
|
142
|
+
const errorMap = {
|
|
143
|
+
401: '❌ Invalid API Key. Please check your key and try again.',
|
|
144
|
+
403: `❌ Authentication Failed (HTTP 403). Server says: ${errText.slice(0, 150)}`,
|
|
145
|
+
};
|
|
146
|
+
s.stop(chalk.red(errorMap[res.status] || `❌ Server error (HTTP ${res.status}): ${errText.slice(0, 150)}`));
|
|
147
|
+
process.exit(1);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const data = await res.json();
|
|
151
|
+
organizationId = data.organizationId || data.organization_id || null;
|
|
152
|
+
|
|
153
|
+
s.stop(chalk.green('✅ API Key validated successfully'));
|
|
154
|
+
|
|
155
|
+
if (data.plan) {
|
|
156
|
+
note(
|
|
157
|
+
chalk.bold.white('📋 Plan: ') + chalk.cyan(data.plan) + '\n' +
|
|
158
|
+
chalk.bold.white('🏢 Org: ') + chalk.cyan(organizationId || 'N/A'),
|
|
159
|
+
'Account Info'
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
} catch (err) {
|
|
163
|
+
s.stop(chalk.red(`❌ Could not connect to Arcality backend: ${err.message}`));
|
|
164
|
+
console.log(chalk.yellow(' Make sure the backend is running and accessible.'));
|
|
165
|
+
process.exit(1);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ── Step 3: Project details ──
|
|
169
|
+
const projectName = await text({
|
|
170
|
+
message: chalk.cyan('📋 Project name:'),
|
|
171
|
+
placeholder: 'My Portal QA',
|
|
172
|
+
validate: (v) => {
|
|
173
|
+
if (!v || v.trim().length < 2) return 'Project name is required (min 2 chars).';
|
|
174
|
+
},
|
|
175
|
+
});
|
|
176
|
+
if (isCancel(projectName)) { cancel('Setup cancelled.'); process.exit(0); }
|
|
177
|
+
|
|
178
|
+
const baseUrl = await text({
|
|
179
|
+
message: chalk.cyan('🌐 Base URL of the application to test:'),
|
|
180
|
+
placeholder: 'https://staging.example.com',
|
|
181
|
+
validate: (v) => {
|
|
182
|
+
if (!v || !v.trim()) return 'Base URL is required.';
|
|
183
|
+
if (!isValidUrl(v)) return 'Invalid URL. Use http:// or https://';
|
|
184
|
+
},
|
|
185
|
+
});
|
|
186
|
+
if (isCancel(baseUrl)) { cancel('Setup cancelled.'); process.exit(0); }
|
|
187
|
+
|
|
188
|
+
const username = await text({
|
|
189
|
+
message: chalk.cyan('👤 Login username / email:'),
|
|
190
|
+
validate: (v) => {
|
|
191
|
+
if (!v || !v.trim()) return 'Username is required.';
|
|
192
|
+
},
|
|
193
|
+
});
|
|
194
|
+
if (isCancel(username)) { cancel('Setup cancelled.'); process.exit(0); }
|
|
195
|
+
|
|
196
|
+
const password = await passwordPrompt({
|
|
197
|
+
message: chalk.cyan('🔒 Login password:'),
|
|
198
|
+
validate: (v) => {
|
|
199
|
+
if (!v || !v.length) return 'Password is required.';
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
if (isCancel(password)) { cancel('Setup cancelled.'); process.exit(0); }
|
|
203
|
+
|
|
204
|
+
// ── Step 4: Detect framework ──
|
|
205
|
+
const frameworkDetected = detectFramework(projectRoot);
|
|
206
|
+
if (frameworkDetected) {
|
|
207
|
+
console.log(chalk.gray(` 🛠️ Framework detected: ${chalk.magenta(frameworkDetected)}`));
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// ── Step 5: Create project in backend ──
|
|
211
|
+
const sCreate = spinner();
|
|
212
|
+
sCreate.start('Creating project in Arcality backend...');
|
|
213
|
+
|
|
214
|
+
let projectId = null;
|
|
215
|
+
try {
|
|
216
|
+
const payload = {
|
|
217
|
+
name: projectName.trim(),
|
|
218
|
+
base_url: baseUrl.trim(),
|
|
219
|
+
framework_detected: frameworkDetected || undefined,
|
|
220
|
+
arcality_version: version,
|
|
221
|
+
config: {
|
|
222
|
+
source: 'npm-cli',
|
|
223
|
+
single_configuration_mode: true,
|
|
224
|
+
yaml_reuse_enabled: true,
|
|
225
|
+
},
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
const res = await fetch(`${apiUrl}/api/v1/projects`, {
|
|
229
|
+
method: 'POST',
|
|
230
|
+
headers: {
|
|
231
|
+
'Content-Type': 'application/json',
|
|
232
|
+
'x-api-key': apiKey.trim(),
|
|
233
|
+
},
|
|
234
|
+
body: JSON.stringify(payload),
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
if (!res.ok) {
|
|
238
|
+
const errText = await res.text().catch(() => '');
|
|
239
|
+
// If response is HTML (e.g. Cloudflare 521), show a clean message
|
|
240
|
+
const isHtml = errText.trim().startsWith('<');
|
|
241
|
+
const displayError = isHtml
|
|
242
|
+
? `The Arcality backend server is temporarily unavailable (HTTP ${res.status}). The project will be saved locally only.`
|
|
243
|
+
: `HTTP ${res.status}: ${errText.slice(0, 200)}`;
|
|
244
|
+
|
|
245
|
+
sCreate.stop(chalk.yellow(`⚠️ ${displayError}`));
|
|
246
|
+
// Don't exit — write the config with a local-only project ID so user can still run
|
|
247
|
+
projectId = projectId || `local_${Date.now()}`;
|
|
248
|
+
} else {
|
|
249
|
+
const created = await res.json();
|
|
250
|
+
projectId = created.id || created.Id;
|
|
251
|
+
organizationId = organizationId || created.organizationId || created.organization_id || null;
|
|
252
|
+
sCreate.stop(chalk.green(`✅ Project created: "${projectName.trim()}" (${projectId})`));
|
|
253
|
+
}
|
|
254
|
+
} catch (err) {
|
|
255
|
+
sCreate.stop(chalk.yellow(`⚠️ Could not reach Arcality server: ${err.message}. Continuing in local mode.`));
|
|
256
|
+
projectId = projectId || `local_${Date.now()}`;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// ── Step 6: Write arcality.config ──
|
|
260
|
+
const config = createConfig({
|
|
261
|
+
apiKey: apiKey.trim(),
|
|
262
|
+
organizationId,
|
|
263
|
+
projectId,
|
|
264
|
+
projectName: projectName.trim(),
|
|
265
|
+
baseUrl: baseUrl.trim(),
|
|
266
|
+
frameworkDetected,
|
|
267
|
+
username: username.trim(),
|
|
268
|
+
password,
|
|
269
|
+
arcalityVersion: version,
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
saveProjectConfig(config, projectRoot);
|
|
273
|
+
|
|
274
|
+
// ── Summary ──
|
|
275
|
+
note(
|
|
276
|
+
chalk.green('✅ arcality.config created successfully!') + '\n\n' +
|
|
277
|
+
chalk.bold.white(' Project: ') + chalk.cyan(config.project.name) + '\n' +
|
|
278
|
+
chalk.bold.white(' Base URL: ') + chalk.cyan(config.project.baseUrl) + '\n' +
|
|
279
|
+
chalk.bold.white(' Framework: ') + chalk.magenta(config.project.frameworkDetected || 'N/A') + '\n' +
|
|
280
|
+
chalk.bold.white(' Project ID: ') + chalk.gray(config.projectId) + '\n' +
|
|
281
|
+
chalk.bold.white(' YAML Dir: ') + chalk.yellow(config.runtime.yamlOutputDir),
|
|
282
|
+
'Configuration Saved'
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
outro(
|
|
286
|
+
chalk.cyanBright('🚀 Ready! Run ') +
|
|
287
|
+
chalk.bold.white('arcality run') +
|
|
288
|
+
chalk.cyanBright(' or ') +
|
|
289
|
+
chalk.bold.white('arcality') +
|
|
290
|
+
chalk.cyanBright(' to start testing.')
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
main().catch(err => {
|
|
295
|
+
console.error(chalk.red(' ❌ Error during init:'), err.message);
|
|
296
|
+
process.exit(1);
|
|
297
|
+
});
|