qa360 2.0.11 → 2.0.13

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 (42) hide show
  1. package/dist/commands/ai.js +26 -14
  2. package/dist/commands/ask.d.ts +75 -23
  3. package/dist/commands/ask.js +413 -265
  4. package/dist/commands/crawl.d.ts +24 -0
  5. package/dist/commands/crawl.js +121 -0
  6. package/dist/commands/history.js +38 -3
  7. package/dist/commands/init.d.ts +89 -95
  8. package/dist/commands/init.js +282 -200
  9. package/dist/commands/run.d.ts +1 -0
  10. package/dist/core/adapters/playwright-ui.d.ts +45 -7
  11. package/dist/core/adapters/playwright-ui.js +365 -59
  12. package/dist/core/assertions/engine.d.ts +51 -0
  13. package/dist/core/assertions/engine.js +530 -0
  14. package/dist/core/assertions/index.d.ts +11 -0
  15. package/dist/core/assertions/index.js +11 -0
  16. package/dist/core/assertions/types.d.ts +121 -0
  17. package/dist/core/assertions/types.js +37 -0
  18. package/dist/core/crawler/index.d.ts +57 -0
  19. package/dist/core/crawler/index.js +281 -0
  20. package/dist/core/crawler/journey-generator.d.ts +49 -0
  21. package/dist/core/crawler/journey-generator.js +412 -0
  22. package/dist/core/crawler/page-analyzer.d.ts +88 -0
  23. package/dist/core/crawler/page-analyzer.js +709 -0
  24. package/dist/core/crawler/selector-generator.d.ts +34 -0
  25. package/dist/core/crawler/selector-generator.js +240 -0
  26. package/dist/core/crawler/types.d.ts +353 -0
  27. package/dist/core/crawler/types.js +6 -0
  28. package/dist/core/generation/crawler-pack-generator.d.ts +44 -0
  29. package/dist/core/generation/crawler-pack-generator.js +231 -0
  30. package/dist/core/generation/index.d.ts +2 -0
  31. package/dist/core/generation/index.js +2 -0
  32. package/dist/core/index.d.ts +3 -0
  33. package/dist/core/index.js +4 -0
  34. package/dist/core/types/pack-v1.d.ts +90 -0
  35. package/dist/index.js +6 -2
  36. package/examples/accessibility.yml +39 -16
  37. package/examples/api-basic.yml +19 -14
  38. package/examples/complete.yml +134 -42
  39. package/examples/fullstack.yml +66 -31
  40. package/examples/security.yml +47 -15
  41. package/examples/ui-basic.yml +16 -12
  42. package/package.json +3 -2
@@ -1,5 +1,5 @@
1
1
  /**
2
- * QA360 Init Command - Interactive pack generator
2
+ * QA360 Init Command - Interactive pack generator (v2)
3
3
  *
4
4
  * Usage:
5
5
  * qa360 init
@@ -15,97 +15,295 @@ import { dump } from 'js-yaml';
15
15
  // Use createRequire for CommonJS module inquirer
16
16
  const require = createRequire(import.meta.url);
17
17
  const inquirer = require('inquirer');
18
- /**
19
- * Available templates
20
- */
18
+ // ============================================================
19
+ // Available templates (v2)
20
+ // ============================================================
21
21
  export const TEMPLATES = {
22
22
  'api-basic': {
23
23
  name: 'API Basic',
24
24
  description: 'Simple API smoke tests (REST/GraphQL)',
25
- gates: ['api_smoke'],
25
+ gates: ['api-smoke'],
26
26
  icon: 'šŸ”Œ',
27
27
  },
28
28
  'ui-basic': {
29
29
  name: 'UI Basic',
30
30
  description: 'Basic UI/E2E browser tests',
31
- gates: ['ui'],
31
+ gates: ['ui-smoke', 'a11y'],
32
32
  icon: '🌐',
33
33
  },
34
34
  'fullstack': {
35
35
  name: 'Full Stack',
36
36
  description: 'API + UI + Performance tests',
37
- gates: ['api_smoke', 'ui', 'perf'],
37
+ gates: ['api-smoke', 'api-crud', 'ui-smoke', 'perf'],
38
38
  icon: 'šŸŽÆ',
39
39
  },
40
40
  'security': {
41
41
  name: 'Security',
42
- description: 'SAST, DAST, secrets scanning, dependency checks',
43
- gates: ['sast', 'dast', 'secrets', 'deps'],
42
+ description: 'SAST, DAST, secrets scanning',
43
+ gates: ['api-smoke', 'sast', 'dast', 'secrets'],
44
44
  icon: 'šŸ›”ļø',
45
45
  },
46
46
  'complete': {
47
47
  name: 'Complete',
48
48
  description: 'All quality gates (API, UI, Performance, Security, Accessibility)',
49
- gates: ['api_smoke', 'ui', 'a11y', 'perf', 'sast', 'dast', 'secrets', 'deps'],
49
+ gates: ['api-smoke', 'api-crud', 'ui-smoke', 'a11y', 'perf', 'sast', 'dast', 'secrets'],
50
50
  icon: '✨',
51
51
  },
52
52
  };
53
- /**
54
- * Gate descriptions with beginner-friendly explanations
55
- */
53
+ // ============================================================
54
+ // Gate descriptions with beginner-friendly explanations
55
+ // ============================================================
56
56
  export const GATE_DESCRIPTIONS = {
57
- api_smoke: {
57
+ 'api-smoke': {
58
58
  short: 'API smoke tests (REST/GraphQL health checks)',
59
59
  beginner: 'Tests if your API is alive and responds correctly. Like checking if the server is up.',
60
60
  example: 'Checks if GET /api/health returns 200 OK',
61
61
  icon: 'šŸ”Œ',
62
+ adapter: 'playwright-api',
63
+ },
64
+ 'api-crud': {
65
+ short: 'API CRUD tests (Create, Read, Update, Delete)',
66
+ beginner: 'Tests the full lifecycle of your data operations.',
67
+ example: 'Tests POST /users, GET /users, PUT /users/1, DELETE /users/1',
68
+ icon: 'šŸ”§',
69
+ adapter: 'playwright-api',
62
70
  },
63
- ui: {
71
+ 'ui-smoke': {
64
72
  short: 'UI/E2E tests (browser automation)',
65
73
  beginner: 'Tests your website in a real browser. Checks if pages load and buttons work.',
66
74
  example: 'Opens your site and verifies the login form appears',
67
75
  icon: '🌐',
76
+ adapter: 'playwright-ui',
68
77
  },
69
- a11y: {
78
+ 'a11y': {
70
79
  short: 'Accessibility tests (WCAG compliance)',
71
80
  beginner: 'Tests if your site is usable by people with disabilities (screen readers, color blindness).',
72
81
  example: 'Checks if images have alt text for blind users',
73
82
  icon: '♿',
83
+ adapter: 'playwright-ui',
74
84
  },
75
- perf: {
85
+ 'perf': {
76
86
  short: 'Performance tests (load/stress testing)',
77
87
  beginner: 'Tests if your site stays fast when many people visit at once.',
78
88
  example: 'Simulates 100 users visiting simultaneously',
79
89
  icon: '⚔',
90
+ adapter: 'k6-perf',
80
91
  },
81
- sast: {
92
+ 'sast': {
82
93
  short: 'Static security analysis (code scanning)',
83
94
  beginner: 'Analyzes your source code for security vulnerabilities without running it.',
84
95
  example: 'Finds hardcoded passwords or unsafe functions',
85
96
  icon: 'šŸ”',
97
+ adapter: 'semgrep-sast',
86
98
  },
87
- dast: {
99
+ 'dast': {
88
100
  short: 'Dynamic security testing (runtime scanning)',
89
101
  beginner: 'Attacks your running application to find security weaknesses.',
90
102
  example: 'Tries common attacks like SQL injection',
91
103
  icon: 'āš”ļø',
104
+ adapter: 'zap-dast',
92
105
  },
93
- secrets: {
106
+ 'secrets': {
94
107
  short: 'Secrets detection (credential scanning)',
95
108
  beginner: 'Scans for accidentally committed passwords, API keys, or tokens.',
96
109
  example: 'Finds AWS keys in your code',
97
110
  icon: 'šŸ”‘',
98
- },
99
- deps: {
100
- short: 'Dependency vulnerability checks',
101
- beginner: 'Checks if your libraries have known security issues.',
102
- example: 'Checks if you use an old version of a package with bugs',
103
- icon: 'šŸ“¦',
111
+ adapter: 'gitleaks-secrets',
104
112
  },
105
113
  };
106
- /**
107
- * Beginner mode - Step by step guided experience
108
- */
114
+ // ============================================================
115
+ // Helper: Create gate config
116
+ // ============================================================
117
+ function createGate(adapter, baseUrl, extraConfig = {}) {
118
+ return {
119
+ adapter,
120
+ enabled: true,
121
+ config: {
122
+ baseUrl,
123
+ ...extraConfig,
124
+ },
125
+ options: {
126
+ timeout: 30000,
127
+ retries: 2,
128
+ },
129
+ };
130
+ }
131
+ // ============================================================
132
+ // Helper: Generate v2 pack from selected gates
133
+ // ============================================================
134
+ function generateV2Pack(name, gates, apiTarget, webTarget) {
135
+ const pack = {
136
+ version: 2,
137
+ name,
138
+ description: `QA360 test pack for ${name}`,
139
+ gates: {},
140
+ execution: {
141
+ default_timeout: 30000,
142
+ default_retries: 2,
143
+ on_failure: 'continue',
144
+ parallel: true,
145
+ },
146
+ };
147
+ const apiBaseUrl = apiTarget ? `\${BASE_URL:-${apiTarget}}` : '${BASE_URL:-https://api.example.com}';
148
+ const webBaseUrl = webTarget ? `\${BASE_URL:-${webTarget}}` : '${BASE_URL:-https://example.com}';
149
+ // Build gates
150
+ for (const gate of gates) {
151
+ const gateInfo = GATE_DESCRIPTIONS[gate];
152
+ if (!gateInfo)
153
+ continue;
154
+ switch (gate) {
155
+ case 'api-smoke':
156
+ pack.gates['api-health'] = createGate('playwright-api', apiBaseUrl, {
157
+ smoke: [
158
+ 'GET /health -> 200',
159
+ 'GET /status -> 200',
160
+ ],
161
+ });
162
+ break;
163
+ case 'api-crud':
164
+ pack.gates['api-crud'] = createGate('playwright-api', apiBaseUrl, {
165
+ smoke: [
166
+ 'GET /api/v1/users -> 200',
167
+ 'POST /api/v1/users -> 201',
168
+ 'PUT /api/v1/users/1 -> 200',
169
+ 'DELETE /api/v1/users/1 -> 204',
170
+ ],
171
+ });
172
+ pack.gates['api-crud'].budgets = {
173
+ p95_ms: 1000,
174
+ error_rate: 0.01,
175
+ };
176
+ break;
177
+ case 'ui-smoke':
178
+ pack.gates['ui-smoke'] = createGate('playwright-ui', webBaseUrl, {
179
+ pages: [
180
+ {
181
+ url: '/',
182
+ expectedElements: ['body', 'main'],
183
+ },
184
+ ],
185
+ });
186
+ break;
187
+ case 'a11y':
188
+ pack.gates['a11y'] = {
189
+ adapter: 'playwright-ui',
190
+ enabled: true,
191
+ config: {
192
+ baseUrl: webBaseUrl,
193
+ pages: [
194
+ {
195
+ url: '/',
196
+ a11yRules: ['wcag2a', 'wcag2aa'],
197
+ },
198
+ ],
199
+ },
200
+ budgets: {
201
+ violations: 10,
202
+ a11y_score: 90,
203
+ },
204
+ };
205
+ break;
206
+ case 'perf':
207
+ pack.gates['perf'] = {
208
+ adapter: 'k6-perf',
209
+ enabled: true,
210
+ config: {
211
+ script: `import http from 'k6/http';
212
+ import { check } from 'k6';
213
+
214
+ export const options = {
215
+ stages: [
216
+ { duration: '10s', target: 10 },
217
+ { duration: '20s', target: 50 },
218
+ { duration: '10s', target: 0 },
219
+ ],
220
+ };
221
+
222
+ export default function () {
223
+ const res = http.get('${apiBaseUrl}/health');
224
+ check(res, {
225
+ 'status is 200': (r) => r.status === 200,
226
+ 'response time < 500ms': (r) => r.timings.duration < 500,
227
+ });
228
+ }`,
229
+ },
230
+ budgets: {
231
+ p95_ms: 2000,
232
+ p99_ms: 5000,
233
+ error_rate: 0.05,
234
+ },
235
+ options: {
236
+ timeout: 60000,
237
+ retries: 1,
238
+ },
239
+ };
240
+ break;
241
+ case 'sast':
242
+ pack.gates['sast'] = {
243
+ adapter: 'semgrep-sast',
244
+ enabled: true,
245
+ config: {
246
+ rules: ['security', 'owasp-top-10'],
247
+ paths: ['src/', 'lib/'],
248
+ },
249
+ budgets: {
250
+ high_findings: 0,
251
+ medium_findings: 10,
252
+ },
253
+ };
254
+ break;
255
+ case 'dast':
256
+ pack.gates['dast'] = {
257
+ adapter: 'zap-dast',
258
+ enabled: true,
259
+ config: {
260
+ target: 'api',
261
+ profile: 'baseline',
262
+ url: apiBaseUrl,
263
+ scanType: 'baseline',
264
+ },
265
+ budgets: {
266
+ high_findings: 5,
267
+ medium_findings: 20,
268
+ },
269
+ options: {
270
+ timeout: 120000,
271
+ },
272
+ };
273
+ break;
274
+ case 'secrets':
275
+ pack.gates['secrets'] = {
276
+ adapter: 'gitleaks-secrets',
277
+ enabled: true,
278
+ config: {
279
+ paths: ['.', 'src/', 'config/'],
280
+ },
281
+ budgets: {
282
+ critical_findings: 0,
283
+ },
284
+ };
285
+ break;
286
+ }
287
+ }
288
+ // Add hooks if needed
289
+ if (gates.includes('perf') || gates.includes('dast')) {
290
+ pack.hooks = {
291
+ beforeAll: [
292
+ {
293
+ type: 'wait_on',
294
+ wait_for: {
295
+ resource: `${apiBaseUrl}/health`,
296
+ timeout: 30000,
297
+ },
298
+ },
299
+ ],
300
+ };
301
+ }
302
+ return pack;
303
+ }
304
+ // ============================================================
305
+ // Beginner mode - Step by step guided experience
306
+ // ============================================================
109
307
  async function beginnerMode() {
110
308
  console.log(chalk.bold.cyan('\n╔════════════════════════════════════════════════════════════╗'));
111
309
  console.log(chalk.bold.cyan('ā•‘ šŸ‘‹ Welcome to QA360 - Your Quality Assistant! ā•‘'));
@@ -136,25 +334,27 @@ async function beginnerMode() {
136
334
  name: 'appType',
137
335
  message: 'What type of application are you testing?',
138
336
  choices: [
139
- { name: 'šŸ”Œ API / Backend - REST or GraphQL API', value: 'api' },
140
- { name: '🌐 Website / Frontend - Web application', value: 'web' },
337
+ { name: 'šŸ”Œ API / Backend - REST or GraphQL API', value: 'api-basic' },
338
+ { name: '🌐 Website / Frontend - Web application', value: 'ui-basic' },
141
339
  { name: 'šŸŽÆ Full Stack - Both API and Website', value: 'fullstack' },
142
340
  { name: 'šŸ›”ļø Security Scan - Check for vulnerabilities', value: 'security' },
143
341
  { name: '✨ Everything - All tests enabled', value: 'complete' },
144
342
  ],
145
343
  },
146
344
  ]);
147
- console.log(chalk.green(` āœ“ You chose: ${appType}\n`));
345
+ console.log(chalk.green(` āœ“ You chose: ${TEMPLATES[appType].name}\n`));
148
346
  // Step 3: Gates explanation and selection
149
- const suggestedGates = TEMPLATES[appType === 'complete' ? 'complete' : appType === 'fullstack' ? 'fullstack' : appType === 'api' ? 'api-basic' : appType === 'web' ? 'ui-basic' : 'security']?.gates || [];
347
+ const suggestedGates = TEMPLATES[appType].gates;
150
348
  console.log(chalk.bold('Step 3ļøāƒ£ - Quality Gates'));
151
349
  console.log(chalk.gray(' Gates are different types of tests. We\'ll suggest some based on your choice.\n'));
152
350
  console.log(chalk.cyan(' Suggested gates for you:\n'));
153
351
  for (const gate of suggestedGates) {
154
352
  const info = GATE_DESCRIPTIONS[gate];
155
- console.log(chalk.cyan(` ${info?.icon} ${gate}`));
156
- console.log(chalk.gray(` ${info?.beginner}`));
157
- console.log(chalk.gray(` Example: ${info?.example}\n`));
353
+ if (info) {
354
+ console.log(chalk.cyan(` ${info.icon} ${gate}`));
355
+ console.log(chalk.gray(` ${info.beginner}`));
356
+ console.log(chalk.gray(` Example: ${info.example}\n`));
357
+ }
158
358
  }
159
359
  const { useSuggested } = await inquirer.prompt([
160
360
  {
@@ -165,6 +365,8 @@ async function beginnerMode() {
165
365
  },
166
366
  ]);
167
367
  let gates = suggestedGates;
368
+ let apiTarget;
369
+ let webTarget;
168
370
  if (!useSuggested) {
169
371
  const { customGates } = await inquirer.prompt([
170
372
  {
@@ -172,7 +374,7 @@ async function beginnerMode() {
172
374
  name: 'customGates',
173
375
  message: 'Select the gates you want:',
174
376
  choices: Object.entries(GATE_DESCRIPTIONS).map(([key, info]) => ({
175
- name: `${info?.icon} ${key} - ${info?.short}`,
377
+ name: `${info.icon} ${key} - ${info.short}`,
176
378
  value: key,
177
379
  })),
178
380
  validate: (input) => input.length > 0 || 'Select at least one gate',
@@ -184,8 +386,9 @@ async function beginnerMode() {
184
386
  // Step 4: URLs
185
387
  console.log(chalk.bold('Step 4ļøāƒ£ - Application URLs'));
186
388
  console.log(chalk.gray(' Where is your application running?\n'));
187
- const targets = {};
188
- if (gates.includes('api_smoke')) {
389
+ const hasApiGates = gates.some((g) => ['api-smoke', 'api-crud', 'perf', 'dast'].includes(g));
390
+ const hasUiGates = gates.some((g) => ['ui-smoke', 'a11y'].includes(g));
391
+ if (hasApiGates) {
189
392
  console.log(chalk.cyan(' šŸ“” API Configuration'));
190
393
  const apiAnswers = await inquirer.prompt([
191
394
  {
@@ -195,20 +398,11 @@ async function beginnerMode() {
195
398
  default: 'https://api.example.com',
196
399
  validate: (input) => input.startsWith('http') || 'Must start with http:// or https://',
197
400
  },
198
- {
199
- type: 'input',
200
- name: 'smokeTests',
201
- message: 'Which endpoints should we test? (format: "METHOD /path -> status")',
202
- default: 'GET /health -> 200',
203
- },
204
401
  ]);
205
- targets.api = {
206
- baseUrl: apiAnswers.apiUrl,
207
- smoke: [apiAnswers.smokeTests],
208
- };
209
- console.log(chalk.green(` āœ“ API: ${apiAnswers.apiUrl}\n`));
402
+ apiTarget = apiAnswers.apiUrl;
403
+ console.log(chalk.green(` āœ“ API: ${apiTarget}\n`));
210
404
  }
211
- if (gates.includes('ui') || gates.includes('a11y')) {
405
+ if (hasUiGates) {
212
406
  console.log(chalk.cyan(' 🌐 Website Configuration'));
213
407
  const uiAnswers = await inquirer.prompt([
214
408
  {
@@ -219,50 +413,22 @@ async function beginnerMode() {
219
413
  validate: (input) => input.startsWith('http') || 'Must start with http:// or https://',
220
414
  },
221
415
  ]);
222
- targets.web = {
223
- baseUrl: uiAnswers.webUrl,
224
- pages: [uiAnswers.webUrl],
225
- };
226
- console.log(chalk.green(` āœ“ Website: ${uiAnswers.webUrl}\n`));
416
+ webTarget = uiAnswers.webUrl;
417
+ console.log(chalk.green(` āœ“ Website: ${webTarget}\n`));
227
418
  }
228
419
  // Build pack
229
- const pack = {
230
- version: 1,
231
- name,
232
- gates: gates,
233
- targets,
234
- };
235
- // Add budgets
236
- if (gates.includes('perf') || gates.includes('a11y')) {
237
- pack.budgets = {};
238
- if (gates.includes('perf')) {
239
- pack.budgets.perf_p95_ms = 2000;
240
- }
241
- if (gates.includes('a11y')) {
242
- pack.budgets.a11y_min = 90;
243
- }
244
- }
245
- // Add security config
246
- if (gates.some((g) => ['sast', 'dast', 'secrets', 'deps'].includes(g))) {
247
- pack.security = {};
248
- if (gates.includes('sast')) {
249
- pack.security.sast = { max_critical: 0, max_high: 3 };
250
- }
251
- if (gates.includes('dast')) {
252
- pack.security.dast = { max_high: 5 };
253
- }
254
- }
420
+ const pack = generateV2Pack(name, gates, apiTarget, webTarget);
255
421
  // Final summary
256
422
  console.log(chalk.bold.cyan('\n✨ Almost done! Here\'s your test pack summary:\n'));
257
423
  console.log(chalk.white(` Name: ${chalk.bold(name)}`));
258
424
  console.log(chalk.white(` Gates: ${gates.map(g => `${GATE_DESCRIPTIONS[g]?.icon} ${g}`).join(', ')}`));
259
- console.log(chalk.white(` API: ${targets.api?.baseUrl || 'N/A'}`));
260
- console.log(chalk.white(` Website: ${targets.web?.baseUrl || 'N/A'}`));
425
+ console.log(chalk.white(` API: ${apiTarget || 'N/A'}`));
426
+ console.log(chalk.white(` Website: ${webTarget || 'N/A'}`));
261
427
  return pack;
262
428
  }
263
- /**
264
- * Interactive prompts
265
- */
429
+ // ============================================================
430
+ // Interactive prompts
431
+ // ============================================================
266
432
  async function promptForConfig() {
267
433
  const answers = await inquirer.prompt([
268
434
  {
@@ -277,7 +443,7 @@ async function promptForConfig() {
277
443
  name: 'template',
278
444
  message: 'Choose a template:',
279
445
  choices: Object.entries(TEMPLATES).map(([key, value]) => ({
280
- name: `${value.name} - ${value.description}`,
446
+ name: `${value.icon} ${value.name} - ${value.description}`,
281
447
  value: key,
282
448
  })),
283
449
  },
@@ -296,7 +462,7 @@ async function promptForConfig() {
296
462
  name: 'gates',
297
463
  message: 'Select quality gates to enable:',
298
464
  choices: Object.entries(GATE_DESCRIPTIONS).map(([key, desc]) => ({
299
- name: `${key} - ${desc.short}`,
465
+ name: `${desc.icon} ${key} - ${desc.short}`,
300
466
  value: key,
301
467
  checked: gates.includes(key),
302
468
  })),
@@ -307,7 +473,9 @@ async function promptForConfig() {
307
473
  }
308
474
  // Prompt for target URLs based on selected gates
309
475
  const targets = {};
310
- if (gates.includes('api_smoke')) {
476
+ const hasApiGates = gates.some((g) => ['api-smoke', 'api-crud', 'perf', 'dast'].includes(g));
477
+ const hasUiGates = gates.some((g) => ['ui-smoke', 'a11y'].includes(g));
478
+ if (hasApiGates) {
311
479
  const apiAnswers = await inquirer.prompt([
312
480
  {
313
481
  type: 'input',
@@ -316,19 +484,10 @@ async function promptForConfig() {
316
484
  default: 'https://api.example.com',
317
485
  validate: (input) => input.startsWith('http') || 'Must be a valid URL',
318
486
  },
319
- {
320
- type: 'input',
321
- name: 'smokeTests',
322
- message: 'Smoke test endpoints (comma-separated):',
323
- default: 'GET /health -> 200, GET /status -> 200',
324
- },
325
487
  ]);
326
- targets.api = {
327
- baseUrl: apiAnswers.apiUrl,
328
- smoke: apiAnswers.smokeTests.split(',').map((s) => s.trim()),
329
- };
488
+ targets.api = apiAnswers.apiUrl;
330
489
  }
331
- if (gates.includes('ui') || gates.includes('a11y')) {
490
+ if (hasUiGates) {
332
491
  const uiAnswers = await inquirer.prompt([
333
492
  {
334
493
  type: 'input',
@@ -337,117 +496,38 @@ async function promptForConfig() {
337
496
  default: 'https://example.com',
338
497
  validate: (input) => input.startsWith('http') || 'Must be a valid URL',
339
498
  },
340
- {
341
- type: 'input',
342
- name: 'pages',
343
- message: 'Pages to test (comma-separated):',
344
- default: 'https://example.com, https://example.com/about',
345
- },
346
499
  ]);
347
- targets.web = {
348
- baseUrl: uiAnswers.webUrl,
349
- pages: uiAnswers.pages.split(',').map((s) => s.trim()),
350
- };
351
- }
352
- // Build pack config
353
- const pack = {
354
- version: 1,
355
- name: answers.name,
356
- gates: gates,
357
- targets,
358
- };
359
- // Add budgets if perf/a11y gates enabled
360
- if (gates.includes('perf') || gates.includes('a11y')) {
361
- pack.budgets = {};
362
- if (gates.includes('perf')) {
363
- pack.budgets.perf_p95_ms = 2000;
364
- }
365
- if (gates.includes('a11y')) {
366
- pack.budgets.a11y_min = 90;
367
- }
500
+ targets.web = uiAnswers.webUrl;
368
501
  }
369
- // Add security config if security gates enabled
370
- if (gates.some((g) => ['sast', 'dast', 'secrets', 'deps'].includes(g))) {
371
- pack.security = {};
372
- if (gates.includes('sast')) {
373
- pack.security.sast = {
374
- max_critical: 0,
375
- max_high: 3,
376
- };
377
- }
378
- if (gates.includes('dast')) {
379
- pack.security.dast = {
380
- max_high: 5,
381
- };
382
- }
383
- }
384
- return pack;
502
+ return generateV2Pack(answers.name, gates, targets.api, targets.web);
385
503
  }
386
- /**
387
- * Generate pack from template
388
- */
504
+ // ============================================================
505
+ // Generate pack from template
506
+ // ============================================================
389
507
  function generateFromTemplate(templateKey, name) {
390
508
  const template = TEMPLATES[templateKey];
391
509
  if (!template) {
392
510
  throw new Error(`Unknown template: ${templateKey}`);
393
511
  }
394
- const pack = {
395
- version: 1,
396
- name: name || `${templateKey}-tests`,
397
- gates: template.gates,
398
- targets: {},
399
- };
400
- // Add default targets based on gates
401
- if (template.gates.includes('api_smoke')) {
402
- pack.targets.api = {
403
- baseUrl: 'https://api.example.com',
404
- smoke: [
405
- 'GET /health -> 200',
406
- 'GET /status -> 200',
407
- ],
408
- };
512
+ // Determine targets based on template
513
+ let apiTarget;
514
+ let webTarget;
515
+ const hasApiGates = template.gates.some((g) => ['api-smoke', 'api-crud', 'perf', 'dast'].includes(g));
516
+ const hasUiGates = template.gates.some((g) => ['ui-smoke', 'a11y'].includes(g));
517
+ if (hasApiGates) {
518
+ apiTarget = 'https://api.example.com';
409
519
  }
410
- if (template.gates.includes('ui') || template.gates.includes('a11y')) {
411
- pack.targets.web = {
412
- baseUrl: 'https://example.com',
413
- pages: [
414
- 'https://example.com',
415
- ],
416
- };
520
+ if (hasUiGates) {
521
+ webTarget = 'https://example.com';
417
522
  }
418
- // Add budgets
419
- if (template.gates.includes('perf') || template.gates.includes('a11y')) {
420
- pack.budgets = {};
421
- if (template.gates.includes('perf')) {
422
- pack.budgets.perf_p95_ms = 2000;
423
- }
424
- if (template.gates.includes('a11y')) {
425
- pack.budgets.a11y_min = 90;
426
- }
427
- }
428
- // Add security config
429
- if (template.gates.some((g) => ['sast', 'dast', 'secrets', 'deps'].includes(g))) {
430
- pack.security = {};
431
- if (template.gates.includes('sast')) {
432
- pack.security.sast = {
433
- max_critical: 0,
434
- max_high: 3,
435
- };
436
- }
437
- if (template.gates.includes('dast')) {
438
- pack.security.dast = {
439
- max_high: 5,
440
- };
441
- }
442
- }
443
- return pack;
523
+ return generateV2Pack(name || `${templateKey}-tests`, template.gates, apiTarget, webTarget);
444
524
  }
445
- /**
446
- * Main init command
447
- */
525
+ // ============================================================
526
+ // Main init command
527
+ // ============================================================
448
528
  export async function initCommand(options = {}) {
449
529
  try {
450
- console.log(chalk.bold.cyan('\nšŸš€ QA360 Pack Generator\n'));
530
+ console.log(chalk.bold.cyan('\nšŸš€ QA360 Pack Generator (v2)\n'));
451
531
  // Determine output file
452
532
  const outputFile = options.output
453
533
  ? resolve(options.output)
@@ -492,6 +572,8 @@ export async function initCommand(options = {}) {
492
572
  indent: 2,
493
573
  lineWidth: 120,
494
574
  noRefs: true,
575
+ quotingType: '"',
576
+ forceQuotes: false,
495
577
  });
496
578
  // Ensure directory exists
497
579
  const dir = resolve(outputFile, '..');
@@ -502,7 +584,7 @@ export async function initCommand(options = {}) {
502
584
  writeFileSync(outputFile, yaml, 'utf-8');
503
585
  console.log(chalk.green.bold('\nāœ… Pack created successfully!'));
504
586
  console.log(chalk.gray(`\nšŸ“„ File: ${outputFile}`));
505
- console.log(chalk.gray(`šŸ“‹ Gates: ${pack.gates?.join(', ')}`));
587
+ console.log(chalk.gray(`šŸ“‹ Gates: ${Object.keys(pack.gates).join(', ')}`));
506
588
  console.log(chalk.cyan('\nšŸ“š Next steps:'));
507
589
  console.log(chalk.gray(' 1. Edit the pack file to customize your tests'));
508
590
  console.log(chalk.gray(' 2. Run: qa360 run'));