@toolstackhq/create-qa-patterns 1.0.1 → 1.0.3

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 (3) hide show
  1. package/README.md +21 -0
  2. package/index.js +376 -24
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -29,3 +29,24 @@ create-qa-patterns playwright-template my-project
29
29
  ## Supported templates
30
30
 
31
31
  - `playwright-template`
32
+
33
+ ## Interactive flow
34
+
35
+ When run in a terminal, the CLI shows:
36
+
37
+ - a template picker with keyboard selection
38
+ - short template descriptions
39
+ - scaffold progress while files are generated
40
+ - optional post-generate actions for:
41
+ - `npm install`
42
+ - `npx playwright install`
43
+ - `npm test`
44
+
45
+ ## Prerequisite checks
46
+
47
+ The CLI checks:
48
+
49
+ - required Node.js version
50
+ - `npm` availability for install and test actions
51
+ - `npx` availability for Playwright browser installation
52
+ - `docker` availability and warns if it is missing
package/index.js CHANGED
@@ -2,8 +2,15 @@
2
2
 
3
3
  const fs = require("node:fs");
4
4
  const path = require("node:path");
5
+ const readline = require("node:readline");
6
+ const { spawn, spawnSync } = require("node:child_process");
5
7
 
6
8
  const DEFAULT_TEMPLATE = "playwright-template";
9
+ const MIN_NODE_VERSION = {
10
+ major: 18,
11
+ minor: 18,
12
+ patch: 0
13
+ };
7
14
  const DEFAULT_GITIGNORE = `node_modules/
8
15
 
9
16
  .env
@@ -16,11 +23,22 @@ allure-report/
16
23
  test-results/
17
24
  playwright-report/
18
25
  `;
19
- const TEMPLATE_ALIASES = new Map([
20
- ["playwright", DEFAULT_TEMPLATE],
21
- ["pw", DEFAULT_TEMPLATE],
22
- [DEFAULT_TEMPLATE, DEFAULT_TEMPLATE]
23
- ]);
26
+
27
+ const TEMPLATES = [
28
+ {
29
+ id: DEFAULT_TEMPLATE,
30
+ aliases: ["playwright", "pw"],
31
+ label: "Playwright Template",
32
+ description: "TypeScript starter with page objects, fixtures, multi-environment config, reporting, linting, CI and Docker."
33
+ }
34
+ ];
35
+
36
+ const TEMPLATE_ALIASES = new Map(
37
+ TEMPLATES.flatMap((template) => [
38
+ [template.id, template.id],
39
+ ...template.aliases.map((alias) => [alias, template.id])
40
+ ])
41
+ );
24
42
 
25
43
  function printHelp() {
26
44
  process.stdout.write(`create-qa-patterns
@@ -30,6 +48,9 @@ Usage:
30
48
  create-qa-patterns <target-directory>
31
49
  create-qa-patterns <template> [target-directory]
32
50
 
51
+ Interactive mode:
52
+ When run without an explicit template, the CLI shows an interactive template picker.
53
+
33
54
  Supported templates:
34
55
  playwright-template
35
56
  playwright
@@ -37,11 +58,209 @@ Supported templates:
37
58
  `);
38
59
  }
39
60
 
61
+ function parseNodeVersion(version) {
62
+ const normalized = version.replace(/^v/, "");
63
+ const [major = "0", minor = "0", patch = "0"] = normalized.split(".");
64
+
65
+ return {
66
+ major: Number.parseInt(major, 10),
67
+ minor: Number.parseInt(minor, 10),
68
+ patch: Number.parseInt(patch, 10)
69
+ };
70
+ }
71
+
72
+ function isNodeVersionSupported(version) {
73
+ if (version.major !== MIN_NODE_VERSION.major) {
74
+ return version.major > MIN_NODE_VERSION.major;
75
+ }
76
+
77
+ if (version.minor !== MIN_NODE_VERSION.minor) {
78
+ return version.minor > MIN_NODE_VERSION.minor;
79
+ }
80
+
81
+ return version.patch >= MIN_NODE_VERSION.patch;
82
+ }
83
+
84
+ function assertSupportedNodeVersion() {
85
+ const currentVersion = parseNodeVersion(process.version);
86
+
87
+ if (!isNodeVersionSupported(currentVersion)) {
88
+ throw new Error(
89
+ `Node ${MIN_NODE_VERSION.major}.${MIN_NODE_VERSION.minor}.${MIN_NODE_VERSION.patch}+ is required. Current version: ${process.version}`
90
+ );
91
+ }
92
+ }
93
+
40
94
  function resolveTemplate(value) {
41
95
  return TEMPLATE_ALIASES.get(value);
42
96
  }
43
97
 
44
- function resolveScaffoldArgs(args) {
98
+ function getTemplate(templateId) {
99
+ return TEMPLATES.find((template) => template.id === templateId);
100
+ }
101
+
102
+ function sleep(ms) {
103
+ return new Promise((resolve) => setTimeout(resolve, ms));
104
+ }
105
+
106
+ function commandExists(command) {
107
+ const result = spawnSync(getCommandName(command), ["--version"], {
108
+ stdio: "ignore"
109
+ });
110
+
111
+ return !result.error && result.status === 0;
112
+ }
113
+
114
+ function collectPrerequisites() {
115
+ return {
116
+ npm: commandExists("npm"),
117
+ npx: commandExists("npx"),
118
+ docker: commandExists("docker")
119
+ };
120
+ }
121
+
122
+ function printPrerequisiteWarnings(prerequisites) {
123
+ if (!prerequisites.npm) {
124
+ process.stdout.write("Warning: npm was not found. Automated install and test steps will be unavailable.\n");
125
+ }
126
+
127
+ if (!prerequisites.npx) {
128
+ process.stdout.write("Warning: npx was not found. Playwright browser installation will be unavailable.\n");
129
+ }
130
+
131
+ if (!prerequisites.docker) {
132
+ process.stdout.write("Warning: docker was not found. Docker-based template flows will not run until Docker is installed.\n");
133
+ }
134
+
135
+ if (!prerequisites.npm || !prerequisites.npx || !prerequisites.docker) {
136
+ process.stdout.write("\n");
137
+ }
138
+ }
139
+
140
+ function createLineInterface() {
141
+ return readline.createInterface({
142
+ input: process.stdin,
143
+ output: process.stdout
144
+ });
145
+ }
146
+
147
+ function askQuestion(prompt) {
148
+ const lineInterface = createLineInterface();
149
+
150
+ return new Promise((resolve) => {
151
+ lineInterface.question(prompt, (answer) => {
152
+ lineInterface.close();
153
+ resolve(answer.trim());
154
+ });
155
+ });
156
+ }
157
+
158
+ async function askYesNo(prompt, defaultValue = true) {
159
+ const suffix = defaultValue ? " [Y/n] " : " [y/N] ";
160
+
161
+ while (true) {
162
+ const answer = (await askQuestion(`${prompt}${suffix}`)).toLowerCase();
163
+
164
+ if (!answer) {
165
+ return defaultValue;
166
+ }
167
+
168
+ if (["y", "yes"].includes(answer)) {
169
+ return true;
170
+ }
171
+
172
+ if (["n", "no"].includes(answer)) {
173
+ return false;
174
+ }
175
+
176
+ process.stdout.write("Please answer yes or no.\n");
177
+ }
178
+ }
179
+
180
+ async function selectTemplateInteractively() {
181
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
182
+ return DEFAULT_TEMPLATE;
183
+ }
184
+
185
+ readline.emitKeypressEvents(process.stdin);
186
+
187
+ if (typeof process.stdin.setRawMode === "function") {
188
+ process.stdin.setRawMode(true);
189
+ }
190
+
191
+ let selectedIndex = 0;
192
+ let renderedLines = 0;
193
+
194
+ const render = () => {
195
+ if (renderedLines > 0) {
196
+ readline.moveCursor(process.stdout, 0, -renderedLines);
197
+ readline.clearScreenDown(process.stdout);
198
+ }
199
+
200
+ const lines = [
201
+ "Select a template",
202
+ "Use ↑/↓ to choose and press Enter to continue.",
203
+ ""
204
+ ];
205
+
206
+ for (let index = 0; index < TEMPLATES.length; index += 1) {
207
+ const template = TEMPLATES[index];
208
+ const marker = index === selectedIndex ? ">" : " ";
209
+ lines.push(`${marker} ${template.label}`);
210
+ lines.push(` ${template.description}`);
211
+ lines.push("");
212
+ }
213
+
214
+ renderedLines = lines.length;
215
+ process.stdout.write(`${lines.join("\n")}\n`);
216
+ };
217
+
218
+ render();
219
+
220
+ return new Promise((resolve) => {
221
+ const handleKeypress = (_, key) => {
222
+ if (!key) {
223
+ return;
224
+ }
225
+
226
+ if (key.name === "up") {
227
+ selectedIndex = (selectedIndex - 1 + TEMPLATES.length) % TEMPLATES.length;
228
+ render();
229
+ return;
230
+ }
231
+
232
+ if (key.name === "down") {
233
+ selectedIndex = (selectedIndex + 1) % TEMPLATES.length;
234
+ render();
235
+ return;
236
+ }
237
+
238
+ if (key.name === "return") {
239
+ process.stdin.off("keypress", handleKeypress);
240
+ if (typeof process.stdin.setRawMode === "function") {
241
+ process.stdin.setRawMode(false);
242
+ }
243
+ readline.clearScreenDown(process.stdout);
244
+ process.stdout.write(`Selected: ${TEMPLATES[selectedIndex].label}\n\n`);
245
+ resolve(TEMPLATES[selectedIndex].id);
246
+ return;
247
+ }
248
+
249
+ if (key.ctrl && key.name === "c") {
250
+ process.stdin.off("keypress", handleKeypress);
251
+ if (typeof process.stdin.setRawMode === "function") {
252
+ process.stdin.setRawMode(false);
253
+ }
254
+ process.stdout.write("\n");
255
+ process.exit(1);
256
+ }
257
+ };
258
+
259
+ process.stdin.on("keypress", handleKeypress);
260
+ });
261
+ }
262
+
263
+ function resolveNonInteractiveArgs(args) {
45
264
  if (args.length === 0) {
46
265
  return {
47
266
  templateName: DEFAULT_TEMPLATE,
@@ -52,6 +271,7 @@ function resolveScaffoldArgs(args) {
52
271
 
53
272
  if (args.length === 1) {
54
273
  const templateName = resolveTemplate(args[0]);
274
+
55
275
  if (templateName) {
56
276
  return {
57
277
  templateName,
@@ -69,6 +289,7 @@ function resolveScaffoldArgs(args) {
69
289
 
70
290
  if (args.length === 2) {
71
291
  const templateName = resolveTemplate(args[0]);
292
+
72
293
  if (!templateName) {
73
294
  throw new Error(`Unsupported template "${args[0]}". Use "playwright-template".`);
74
295
  }
@@ -83,6 +304,30 @@ function resolveScaffoldArgs(args) {
83
304
  throw new Error("Too many arguments. Run `create-qa-patterns --help` for usage.");
84
305
  }
85
306
 
307
+ async function resolveScaffoldArgs(args) {
308
+ const explicitTemplate = args[0] && resolveTemplate(args[0]);
309
+
310
+ if (explicitTemplate) {
311
+ return resolveNonInteractiveArgs(args);
312
+ }
313
+
314
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
315
+ return resolveNonInteractiveArgs(args);
316
+ }
317
+
318
+ const templateName = await selectTemplateInteractively();
319
+ const defaultTarget = args[0] ? args[0] : ".";
320
+ const targetAnswer = await askQuestion(`Target directory (${defaultTarget}): `);
321
+ const targetValue = targetAnswer || defaultTarget;
322
+ const targetDirectory = path.resolve(process.cwd(), targetValue);
323
+
324
+ return {
325
+ templateName,
326
+ targetDirectory,
327
+ generatedInCurrentDirectory: targetDirectory === process.cwd()
328
+ };
329
+ }
330
+
86
331
  function ensureScaffoldTarget(targetDirectory) {
87
332
  if (!fs.existsSync(targetDirectory)) {
88
333
  fs.mkdirSync(targetDirectory, { recursive: true });
@@ -148,52 +393,159 @@ function customizeProject(targetDirectory) {
148
393
  }
149
394
  }
150
395
 
151
- function scaffoldProject(templateName, targetDirectory) {
396
+ function renderProgress(completed, total, label) {
397
+ const width = 24;
398
+ const filled = Math.round((completed / total) * width);
399
+ const empty = width - filled;
400
+ const bar = `${"=".repeat(filled)}${" ".repeat(empty)}`;
401
+ const percentage = `${Math.round((completed / total) * 100)}`.padStart(3, " ");
402
+ process.stdout.write(`\r[${bar}] ${percentage}% ${label}`);
403
+ }
404
+
405
+ async function scaffoldProject(templateName, targetDirectory) {
152
406
  const templateDirectory = path.resolve(__dirname, "templates", templateName);
153
407
 
154
408
  if (!fs.existsSync(templateDirectory)) {
155
409
  throw new Error(`Template files are missing for "${templateName}".`);
156
410
  }
157
411
 
412
+ const steps = [
413
+ "Validating target directory",
414
+ "Copying template files",
415
+ "Customizing project files",
416
+ "Finalizing scaffold"
417
+ ];
418
+
419
+ renderProgress(0, steps.length, "Preparing scaffold");
158
420
  ensureScaffoldTarget(targetDirectory);
421
+ await sleep(60);
422
+
423
+ renderProgress(1, steps.length, steps[0]);
424
+ await sleep(80);
425
+
159
426
  fs.cpSync(templateDirectory, targetDirectory, { recursive: true });
427
+ renderProgress(2, steps.length, steps[1]);
428
+ await sleep(80);
429
+
160
430
  customizeProject(targetDirectory);
431
+ renderProgress(3, steps.length, steps[2]);
432
+ await sleep(80);
433
+
434
+ renderProgress(4, steps.length, steps[3]);
435
+ await sleep(60);
436
+ process.stdout.write("\n");
161
437
  }
162
438
 
163
- function printNextSteps(targetDirectory, generatedInCurrentDirectory) {
164
- process.stdout.write(`Generated ${DEFAULT_TEMPLATE} in ${targetDirectory}
439
+ function getCommandName(base) {
440
+ if (process.platform === "win32") {
441
+ return `${base}.cmd`;
442
+ }
165
443
 
166
- Next steps:
167
- `);
444
+ return base;
445
+ }
446
+
447
+ function runCommand(command, args, cwd) {
448
+ return new Promise((resolve, reject) => {
449
+ const child = spawn(getCommandName(command), args, {
450
+ cwd,
451
+ stdio: "inherit"
452
+ });
453
+
454
+ child.on("close", (code) => {
455
+ if (code === 0) {
456
+ resolve();
457
+ return;
458
+ }
459
+
460
+ reject(new Error(`${command} ${args.join(" ")} exited with code ${code}`));
461
+ });
462
+
463
+ child.on("error", reject);
464
+ });
465
+ }
466
+
467
+ function printSuccess(templateName, targetDirectory, generatedInCurrentDirectory) {
468
+ const template = getTemplate(templateName);
469
+
470
+ process.stdout.write(`\nSuccess
471
+ Generated ${template ? template.label : templateName} in ${targetDirectory}
472
+ \n`);
168
473
 
169
474
  if (!generatedInCurrentDirectory) {
170
- process.stdout.write(` cd ${path.relative(process.cwd(), targetDirectory) || "."}
171
- `);
475
+ process.stdout.write(`Change directory first:\n cd ${path.relative(process.cwd(), targetDirectory) || "."}\n\n`);
172
476
  }
477
+ }
173
478
 
174
- process.stdout.write(` npm install
175
- npx playwright install
176
- npm test
177
- `);
479
+ function printNextSteps(targetDirectory, generatedInCurrentDirectory) {
480
+ process.stdout.write("Next steps:\n");
481
+
482
+ if (!generatedInCurrentDirectory) {
483
+ process.stdout.write(` cd ${path.relative(process.cwd(), targetDirectory) || "."}\n`);
484
+ }
485
+
486
+ process.stdout.write(" npm install\n");
487
+ process.stdout.write(" npx playwright install\n");
488
+ process.stdout.write(" npm test\n");
178
489
  }
179
490
 
180
- function main() {
491
+ async function runPostGenerateActions(targetDirectory) {
492
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
493
+ return;
494
+ }
495
+
496
+ const prerequisites = collectPrerequisites();
497
+
498
+ if (prerequisites.npm) {
499
+ const shouldInstallDependencies = await askYesNo("Run npm install now?", true);
500
+
501
+ if (shouldInstallDependencies) {
502
+ await runCommand("npm", ["install"], targetDirectory);
503
+ }
504
+ } else {
505
+ process.stdout.write("Skipping npm install prompt because npm is not available.\n");
506
+ }
507
+
508
+ if (prerequisites.npx) {
509
+ const shouldInstallPlaywright = await askYesNo("Run npx playwright install now?", true);
510
+
511
+ if (shouldInstallPlaywright) {
512
+ await runCommand("npx", ["playwright", "install"], targetDirectory);
513
+ }
514
+ } else {
515
+ process.stdout.write("Skipping Playwright browser install prompt because npx is not available.\n");
516
+ }
517
+
518
+ if (prerequisites.npm) {
519
+ const shouldRunTests = await askYesNo("Run npm test now?", false);
520
+
521
+ if (shouldRunTests) {
522
+ await runCommand("npm", ["test"], targetDirectory);
523
+ }
524
+ } else {
525
+ process.stdout.write("Skipping npm test prompt because npm is not available.\n");
526
+ }
527
+ }
528
+
529
+ async function main() {
181
530
  const args = process.argv.slice(2);
182
531
 
532
+ assertSupportedNodeVersion();
533
+
183
534
  if (args.includes("--help") || args.includes("-h")) {
184
535
  printHelp();
185
536
  return;
186
537
  }
187
538
 
188
- const { templateName, targetDirectory, generatedInCurrentDirectory } = resolveScaffoldArgs(args);
189
- scaffoldProject(templateName, targetDirectory);
539
+ const { templateName, targetDirectory, generatedInCurrentDirectory } = await resolveScaffoldArgs(args);
540
+ printPrerequisiteWarnings(collectPrerequisites());
541
+ await scaffoldProject(templateName, targetDirectory);
542
+ printSuccess(templateName, targetDirectory, generatedInCurrentDirectory);
543
+ await runPostGenerateActions(targetDirectory);
190
544
  printNextSteps(targetDirectory, generatedInCurrentDirectory);
191
545
  }
192
546
 
193
- try {
194
- main();
195
- } catch (error) {
547
+ main().catch((error) => {
196
548
  const message = error instanceof Error ? error.message : String(error);
197
549
  process.stderr.write(`${message}\n`);
198
550
  process.exit(1);
199
- }
551
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@toolstackhq/create-qa-patterns",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "CLI for generating QA framework templates.",
5
5
  "license": "MIT",
6
6
  "repository": {