@toolstackhq/create-qa-patterns 1.0.1 → 1.0.2

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 +12 -0
  2. package/index.js +287 -24
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -29,3 +29,15 @@ 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`
package/index.js CHANGED
@@ -2,6 +2,8 @@
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 } = require("node:child_process");
5
7
 
6
8
  const DEFAULT_TEMPLATE = "playwright-template";
7
9
  const DEFAULT_GITIGNORE = `node_modules/
@@ -16,11 +18,22 @@ allure-report/
16
18
  test-results/
17
19
  playwright-report/
18
20
  `;
19
- const TEMPLATE_ALIASES = new Map([
20
- ["playwright", DEFAULT_TEMPLATE],
21
- ["pw", DEFAULT_TEMPLATE],
22
- [DEFAULT_TEMPLATE, DEFAULT_TEMPLATE]
23
- ]);
21
+
22
+ const TEMPLATES = [
23
+ {
24
+ id: DEFAULT_TEMPLATE,
25
+ aliases: ["playwright", "pw"],
26
+ label: "Playwright Template",
27
+ description: "TypeScript starter with page objects, fixtures, multi-environment config, reporting, linting, CI and Docker."
28
+ }
29
+ ];
30
+
31
+ const TEMPLATE_ALIASES = new Map(
32
+ TEMPLATES.flatMap((template) => [
33
+ [template.id, template.id],
34
+ ...template.aliases.map((alias) => [alias, template.id])
35
+ ])
36
+ );
24
37
 
25
38
  function printHelp() {
26
39
  process.stdout.write(`create-qa-patterns
@@ -30,6 +43,9 @@ Usage:
30
43
  create-qa-patterns <target-directory>
31
44
  create-qa-patterns <template> [target-directory]
32
45
 
46
+ Interactive mode:
47
+ When run without an explicit template, the CLI shows an interactive template picker.
48
+
33
49
  Supported templates:
34
50
  playwright-template
35
51
  playwright
@@ -41,7 +57,138 @@ function resolveTemplate(value) {
41
57
  return TEMPLATE_ALIASES.get(value);
42
58
  }
43
59
 
44
- function resolveScaffoldArgs(args) {
60
+ function getTemplate(templateId) {
61
+ return TEMPLATES.find((template) => template.id === templateId);
62
+ }
63
+
64
+ function sleep(ms) {
65
+ return new Promise((resolve) => setTimeout(resolve, ms));
66
+ }
67
+
68
+ function createLineInterface() {
69
+ return readline.createInterface({
70
+ input: process.stdin,
71
+ output: process.stdout
72
+ });
73
+ }
74
+
75
+ function askQuestion(prompt) {
76
+ const lineInterface = createLineInterface();
77
+
78
+ return new Promise((resolve) => {
79
+ lineInterface.question(prompt, (answer) => {
80
+ lineInterface.close();
81
+ resolve(answer.trim());
82
+ });
83
+ });
84
+ }
85
+
86
+ async function askYesNo(prompt, defaultValue = true) {
87
+ const suffix = defaultValue ? " [Y/n] " : " [y/N] ";
88
+
89
+ while (true) {
90
+ const answer = (await askQuestion(`${prompt}${suffix}`)).toLowerCase();
91
+
92
+ if (!answer) {
93
+ return defaultValue;
94
+ }
95
+
96
+ if (["y", "yes"].includes(answer)) {
97
+ return true;
98
+ }
99
+
100
+ if (["n", "no"].includes(answer)) {
101
+ return false;
102
+ }
103
+
104
+ process.stdout.write("Please answer yes or no.\n");
105
+ }
106
+ }
107
+
108
+ async function selectTemplateInteractively() {
109
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
110
+ return DEFAULT_TEMPLATE;
111
+ }
112
+
113
+ readline.emitKeypressEvents(process.stdin);
114
+
115
+ if (typeof process.stdin.setRawMode === "function") {
116
+ process.stdin.setRawMode(true);
117
+ }
118
+
119
+ let selectedIndex = 0;
120
+ let renderedLines = 0;
121
+
122
+ const render = () => {
123
+ if (renderedLines > 0) {
124
+ readline.moveCursor(process.stdout, 0, -renderedLines);
125
+ readline.clearScreenDown(process.stdout);
126
+ }
127
+
128
+ const lines = [
129
+ "Select a template",
130
+ "Use ↑/↓ to choose and press Enter to continue.",
131
+ ""
132
+ ];
133
+
134
+ for (let index = 0; index < TEMPLATES.length; index += 1) {
135
+ const template = TEMPLATES[index];
136
+ const marker = index === selectedIndex ? ">" : " ";
137
+ lines.push(`${marker} ${template.label}`);
138
+ lines.push(` ${template.description}`);
139
+ lines.push("");
140
+ }
141
+
142
+ renderedLines = lines.length;
143
+ process.stdout.write(`${lines.join("\n")}\n`);
144
+ };
145
+
146
+ render();
147
+
148
+ return new Promise((resolve) => {
149
+ const handleKeypress = (_, key) => {
150
+ if (!key) {
151
+ return;
152
+ }
153
+
154
+ if (key.name === "up") {
155
+ selectedIndex = (selectedIndex - 1 + TEMPLATES.length) % TEMPLATES.length;
156
+ render();
157
+ return;
158
+ }
159
+
160
+ if (key.name === "down") {
161
+ selectedIndex = (selectedIndex + 1) % TEMPLATES.length;
162
+ render();
163
+ return;
164
+ }
165
+
166
+ if (key.name === "return") {
167
+ process.stdin.off("keypress", handleKeypress);
168
+ if (typeof process.stdin.setRawMode === "function") {
169
+ process.stdin.setRawMode(false);
170
+ }
171
+ readline.clearScreenDown(process.stdout);
172
+ process.stdout.write(`Selected: ${TEMPLATES[selectedIndex].label}\n\n`);
173
+ resolve(TEMPLATES[selectedIndex].id);
174
+ return;
175
+ }
176
+
177
+ if (key.ctrl && key.name === "c") {
178
+ process.stdin.off("keypress", handleKeypress);
179
+ if (typeof process.stdin.setRawMode === "function") {
180
+ process.stdin.setRawMode(false);
181
+ }
182
+ process.stdout.write("\n");
183
+ process.exit(1);
184
+ }
185
+ };
186
+
187
+ process.stdin.on("keypress", handleKeypress);
188
+ });
189
+ }
190
+
191
+ function resolveNonInteractiveArgs(args) {
45
192
  if (args.length === 0) {
46
193
  return {
47
194
  templateName: DEFAULT_TEMPLATE,
@@ -52,6 +199,7 @@ function resolveScaffoldArgs(args) {
52
199
 
53
200
  if (args.length === 1) {
54
201
  const templateName = resolveTemplate(args[0]);
202
+
55
203
  if (templateName) {
56
204
  return {
57
205
  templateName,
@@ -69,6 +217,7 @@ function resolveScaffoldArgs(args) {
69
217
 
70
218
  if (args.length === 2) {
71
219
  const templateName = resolveTemplate(args[0]);
220
+
72
221
  if (!templateName) {
73
222
  throw new Error(`Unsupported template "${args[0]}". Use "playwright-template".`);
74
223
  }
@@ -83,6 +232,30 @@ function resolveScaffoldArgs(args) {
83
232
  throw new Error("Too many arguments. Run `create-qa-patterns --help` for usage.");
84
233
  }
85
234
 
235
+ async function resolveScaffoldArgs(args) {
236
+ const explicitTemplate = args[0] && resolveTemplate(args[0]);
237
+
238
+ if (explicitTemplate) {
239
+ return resolveNonInteractiveArgs(args);
240
+ }
241
+
242
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
243
+ return resolveNonInteractiveArgs(args);
244
+ }
245
+
246
+ const templateName = await selectTemplateInteractively();
247
+ const defaultTarget = args[0] ? args[0] : ".";
248
+ const targetAnswer = await askQuestion(`Target directory (${defaultTarget}): `);
249
+ const targetValue = targetAnswer || defaultTarget;
250
+ const targetDirectory = path.resolve(process.cwd(), targetValue);
251
+
252
+ return {
253
+ templateName,
254
+ targetDirectory,
255
+ generatedInCurrentDirectory: targetDirectory === process.cwd()
256
+ };
257
+ }
258
+
86
259
  function ensureScaffoldTarget(targetDirectory) {
87
260
  if (!fs.existsSync(targetDirectory)) {
88
261
  fs.mkdirSync(targetDirectory, { recursive: true });
@@ -148,36 +321,126 @@ function customizeProject(targetDirectory) {
148
321
  }
149
322
  }
150
323
 
151
- function scaffoldProject(templateName, targetDirectory) {
324
+ function renderProgress(completed, total, label) {
325
+ const width = 24;
326
+ const filled = Math.round((completed / total) * width);
327
+ const empty = width - filled;
328
+ const bar = `${"=".repeat(filled)}${" ".repeat(empty)}`;
329
+ const percentage = `${Math.round((completed / total) * 100)}`.padStart(3, " ");
330
+ process.stdout.write(`\r[${bar}] ${percentage}% ${label}`);
331
+ }
332
+
333
+ async function scaffoldProject(templateName, targetDirectory) {
152
334
  const templateDirectory = path.resolve(__dirname, "templates", templateName);
153
335
 
154
336
  if (!fs.existsSync(templateDirectory)) {
155
337
  throw new Error(`Template files are missing for "${templateName}".`);
156
338
  }
157
339
 
340
+ const steps = [
341
+ "Validating target directory",
342
+ "Copying template files",
343
+ "Customizing project files",
344
+ "Finalizing scaffold"
345
+ ];
346
+
347
+ renderProgress(0, steps.length, "Preparing scaffold");
158
348
  ensureScaffoldTarget(targetDirectory);
349
+ await sleep(60);
350
+
351
+ renderProgress(1, steps.length, steps[0]);
352
+ await sleep(80);
353
+
159
354
  fs.cpSync(templateDirectory, targetDirectory, { recursive: true });
355
+ renderProgress(2, steps.length, steps[1]);
356
+ await sleep(80);
357
+
160
358
  customizeProject(targetDirectory);
359
+ renderProgress(3, steps.length, steps[2]);
360
+ await sleep(80);
361
+
362
+ renderProgress(4, steps.length, steps[3]);
363
+ await sleep(60);
364
+ process.stdout.write("\n");
161
365
  }
162
366
 
163
- function printNextSteps(targetDirectory, generatedInCurrentDirectory) {
164
- process.stdout.write(`Generated ${DEFAULT_TEMPLATE} in ${targetDirectory}
367
+ function getCommandName(base) {
368
+ if (process.platform === "win32") {
369
+ return `${base}.cmd`;
370
+ }
165
371
 
166
- Next steps:
167
- `);
372
+ return base;
373
+ }
374
+
375
+ function runCommand(command, args, cwd) {
376
+ return new Promise((resolve, reject) => {
377
+ const child = spawn(getCommandName(command), args, {
378
+ cwd,
379
+ stdio: "inherit"
380
+ });
381
+
382
+ child.on("close", (code) => {
383
+ if (code === 0) {
384
+ resolve();
385
+ return;
386
+ }
387
+
388
+ reject(new Error(`${command} ${args.join(" ")} exited with code ${code}`));
389
+ });
390
+
391
+ child.on("error", reject);
392
+ });
393
+ }
394
+
395
+ function printSuccess(templateName, targetDirectory, generatedInCurrentDirectory) {
396
+ const template = getTemplate(templateName);
397
+
398
+ process.stdout.write(`\nSuccess
399
+ Generated ${template ? template.label : templateName} in ${targetDirectory}
400
+ \n`);
168
401
 
169
402
  if (!generatedInCurrentDirectory) {
170
- process.stdout.write(` cd ${path.relative(process.cwd(), targetDirectory) || "."}
171
- `);
403
+ process.stdout.write(`Change directory first:\n cd ${path.relative(process.cwd(), targetDirectory) || "."}\n\n`);
172
404
  }
405
+ }
173
406
 
174
- process.stdout.write(` npm install
175
- npx playwright install
176
- npm test
177
- `);
407
+ function printNextSteps(targetDirectory, generatedInCurrentDirectory) {
408
+ process.stdout.write("Next steps:\n");
409
+
410
+ if (!generatedInCurrentDirectory) {
411
+ process.stdout.write(` cd ${path.relative(process.cwd(), targetDirectory) || "."}\n`);
412
+ }
413
+
414
+ process.stdout.write(" npm install\n");
415
+ process.stdout.write(" npx playwright install\n");
416
+ process.stdout.write(" npm test\n");
178
417
  }
179
418
 
180
- function main() {
419
+ async function runPostGenerateActions(targetDirectory) {
420
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
421
+ return;
422
+ }
423
+
424
+ const shouldInstallDependencies = await askYesNo("Run npm install now?", true);
425
+
426
+ if (shouldInstallDependencies) {
427
+ await runCommand("npm", ["install"], targetDirectory);
428
+ }
429
+
430
+ const shouldInstallPlaywright = await askYesNo("Run npx playwright install now?", true);
431
+
432
+ if (shouldInstallPlaywright) {
433
+ await runCommand("npx", ["playwright", "install"], targetDirectory);
434
+ }
435
+
436
+ const shouldRunTests = await askYesNo("Run npm test now?", false);
437
+
438
+ if (shouldRunTests) {
439
+ await runCommand("npm", ["test"], targetDirectory);
440
+ }
441
+ }
442
+
443
+ async function main() {
181
444
  const args = process.argv.slice(2);
182
445
 
183
446
  if (args.includes("--help") || args.includes("-h")) {
@@ -185,15 +448,15 @@ function main() {
185
448
  return;
186
449
  }
187
450
 
188
- const { templateName, targetDirectory, generatedInCurrentDirectory } = resolveScaffoldArgs(args);
189
- scaffoldProject(templateName, targetDirectory);
451
+ const { templateName, targetDirectory, generatedInCurrentDirectory } = await resolveScaffoldArgs(args);
452
+ await scaffoldProject(templateName, targetDirectory);
453
+ printSuccess(templateName, targetDirectory, generatedInCurrentDirectory);
454
+ await runPostGenerateActions(targetDirectory);
190
455
  printNextSteps(targetDirectory, generatedInCurrentDirectory);
191
456
  }
192
457
 
193
- try {
194
- main();
195
- } catch (error) {
458
+ main().catch((error) => {
196
459
  const message = error instanceof Error ? error.message : String(error);
197
460
  process.stderr.write(`${message}\n`);
198
461
  process.exit(1);
199
- }
462
+ });
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.2",
4
4
  "description": "CLI for generating QA framework templates.",
5
5
  "license": "MIT",
6
6
  "repository": {