@toolstackhq/create-qa-patterns 1.0.12 → 1.0.14

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 (58) hide show
  1. package/README.md +32 -0
  2. package/index.js +502 -41
  3. package/package.json +1 -1
  4. package/templates/cypress-template/README.md +32 -0
  5. package/templates/cypress-template/allurerc.mjs +10 -0
  6. package/templates/cypress-template/config/README.md +5 -0
  7. package/templates/cypress-template/config/environments.ts +1 -0
  8. package/templates/cypress-template/config/runtime-config.ts +1 -0
  9. package/templates/cypress-template/config/secret-manager.ts +1 -0
  10. package/templates/cypress-template/cypress/e2e/README.md +6 -0
  11. package/templates/cypress-template/cypress/e2e/ui-journey.cy.ts +1 -0
  12. package/templates/cypress-template/cypress/support/README.md +5 -0
  13. package/templates/cypress-template/cypress/support/app-config.ts +1 -0
  14. package/templates/cypress-template/cypress/support/commands.ts +1 -0
  15. package/templates/cypress-template/cypress/support/data/README.md +5 -0
  16. package/templates/cypress-template/cypress/support/data/data-factory.ts +1 -0
  17. package/templates/cypress-template/cypress/support/data/id-generator.ts +1 -0
  18. package/templates/cypress-template/cypress/support/data/seeded-faker.ts +1 -0
  19. package/templates/cypress-template/cypress/support/e2e.ts +1 -0
  20. package/templates/cypress-template/cypress/support/pages/README.md +5 -0
  21. package/templates/cypress-template/cypress/support/pages/login-page.ts +1 -0
  22. package/templates/cypress-template/cypress/support/pages/people-page.ts +1 -0
  23. package/templates/cypress-template/cypress.config.ts +17 -1
  24. package/templates/cypress-template/eslint.config.mjs +1 -1
  25. package/templates/cypress-template/package-lock.json +2857 -109
  26. package/templates/cypress-template/package.json +4 -0
  27. package/templates/cypress-template/scripts/README.md +5 -0
  28. package/templates/cypress-template/scripts/generate-allure-report.mjs +66 -0
  29. package/templates/cypress-template/scripts/run-cypress.mjs +1 -0
  30. package/templates/cypress-template/tsconfig.json +1 -1
  31. package/templates/playwright-template/README.md +20 -0
  32. package/templates/playwright-template/components/README.md +5 -0
  33. package/templates/playwright-template/config/README.md +5 -0
  34. package/templates/playwright-template/config/environments.ts +1 -0
  35. package/templates/playwright-template/config/runtime-config.ts +1 -0
  36. package/templates/playwright-template/config/secret-manager.ts +1 -0
  37. package/templates/playwright-template/data/factories/README.md +6 -0
  38. package/templates/playwright-template/data/factories/data-factory.ts +1 -0
  39. package/templates/playwright-template/data/generators/README.md +5 -0
  40. package/templates/playwright-template/data/generators/id-generator.ts +1 -0
  41. package/templates/playwright-template/data/generators/seeded-faker.ts +1 -0
  42. package/templates/playwright-template/fixtures/README.md +5 -0
  43. package/templates/playwright-template/fixtures/test-fixtures.ts +1 -0
  44. package/templates/playwright-template/pages/README.md +6 -0
  45. package/templates/playwright-template/pages/base-page.ts +1 -0
  46. package/templates/playwright-template/pages/login-page.ts +1 -0
  47. package/templates/playwright-template/pages/people-page.ts +1 -0
  48. package/templates/playwright-template/playwright.config.ts +1 -0
  49. package/templates/playwright-template/reporters/README.md +5 -0
  50. package/templates/playwright-template/reporters/structured-reporter.ts +1 -0
  51. package/templates/playwright-template/scripts/README.md +5 -0
  52. package/templates/playwright-template/scripts/generate-allure-report.mjs +1 -0
  53. package/templates/playwright-template/tests/README.md +7 -0
  54. package/templates/playwright-template/tests/api-people.spec.ts +1 -0
  55. package/templates/playwright-template/tests/ui-journey.spec.ts +1 -0
  56. package/templates/playwright-template/utils/README.md +5 -0
  57. package/templates/playwright-template/utils/logger.ts +1 -0
  58. package/templates/playwright-template/utils/test-step.ts +1 -0
package/README.md CHANGED
@@ -34,6 +34,30 @@ Generate the Cypress template explicitly:
34
34
  create-qa-patterns cypress-template my-project
35
35
  ```
36
36
 
37
+ Generate without post-create prompts, which is useful for CI or scripted setup:
38
+
39
+ ```bash
40
+ create-qa-patterns playwright-template my-project --yes --no-install --no-setup --no-test
41
+ ```
42
+
43
+ ## Upgrade a generated project
44
+
45
+ Generated projects now include a `.qa-patterns.json` metadata file. It tracks the last applied managed template baseline so the CLI can update infrastructure files conservatively later.
46
+
47
+ Check for safe updates:
48
+
49
+ ```bash
50
+ create-qa-patterns upgrade check my-project
51
+ ```
52
+
53
+ Apply only safe managed-file updates:
54
+
55
+ ```bash
56
+ create-qa-patterns upgrade apply --safe my-project
57
+ ```
58
+
59
+ The upgrade flow intentionally avoids overwriting user-owned test and page code. It only manages framework infrastructure such as config, scripts, workflows, and package metadata when those files are still unchanged from the generated baseline.
60
+
37
61
  ## Supported templates
38
62
 
39
63
  - `playwright-template`
@@ -54,6 +78,14 @@ For Playwright projects, the interactive flow also offers:
54
78
 
55
79
  - `npx playwright install`
56
80
 
81
+ For non-interactive automation, the CLI also supports:
82
+
83
+ - `--yes`
84
+ - `--no-install`
85
+ - `--no-setup`
86
+ - `--no-test`
87
+ - `--template <template>`
88
+
57
89
  ## Prerequisite checks
58
90
 
59
91
  The CLI checks:
package/index.js CHANGED
@@ -3,9 +3,12 @@
3
3
  const fs = require("node:fs");
4
4
  const path = require("node:path");
5
5
  const readline = require("node:readline");
6
+ const crypto = require("node:crypto");
6
7
  const { spawn, spawnSync } = require("node:child_process");
7
8
 
8
9
  const DEFAULT_TEMPLATE = "playwright-template";
10
+ const CLI_PACKAGE = require("./package.json");
11
+ const METADATA_FILENAME = ".qa-patterns.json";
9
12
  const MIN_NODE_VERSION = {
10
13
  major: 18,
11
14
  minor: 18,
@@ -42,6 +45,30 @@ test-results/
42
45
  playwright-report/
43
46
  `;
44
47
 
48
+ const MANAGED_FILE_PATTERNS = {
49
+ common: [
50
+ ".env.example",
51
+ ".gitignore",
52
+ "package.json",
53
+ "package-lock.json",
54
+ "tsconfig.json",
55
+ "eslint.config.mjs",
56
+ "allurerc.mjs",
57
+ "config/**",
58
+ "scripts/**",
59
+ ".github/**"
60
+ ],
61
+ "playwright-template": [
62
+ "playwright.config.ts",
63
+ "docker/**",
64
+ "lint/**",
65
+ "reporters/**",
66
+ "utils/logger.ts",
67
+ "utils/test-step.ts"
68
+ ],
69
+ "cypress-template": ["cypress.config.ts"]
70
+ };
71
+
45
72
  const TEMPLATES = [
46
73
  {
47
74
  id: DEFAULT_TEMPLATE,
@@ -109,6 +136,222 @@ const colors = {
109
136
  }
110
137
  };
111
138
 
139
+ function sha256(content) {
140
+ return crypto.createHash("sha256").update(content).digest("hex");
141
+ }
142
+
143
+ function normalizePath(value) {
144
+ return value.split(path.sep).join("/");
145
+ }
146
+
147
+ function getTemplateDirectory(templateId) {
148
+ return path.resolve(__dirname, "templates", templateId);
149
+ }
150
+
151
+ function pathMatchesPattern(relativePath, pattern) {
152
+ if (pattern.endsWith("/**")) {
153
+ const prefix = pattern.slice(0, -3);
154
+ return relativePath === prefix || relativePath.startsWith(`${prefix}/`);
155
+ }
156
+
157
+ return relativePath === pattern;
158
+ }
159
+
160
+ function isManagedFile(template, relativePath) {
161
+ const patterns = [...MANAGED_FILE_PATTERNS.common, ...(MANAGED_FILE_PATTERNS[template.id] || [])];
162
+ return patterns.some((pattern) => pathMatchesPattern(relativePath, pattern));
163
+ }
164
+
165
+ function collectRelativeFiles(rootDirectory) {
166
+ const results = [];
167
+
168
+ function visit(currentDirectory) {
169
+ const entries = fs.readdirSync(currentDirectory, { withFileTypes: true });
170
+
171
+ for (const entry of entries) {
172
+ const absolutePath = path.join(currentDirectory, entry.name);
173
+ const relativePath = normalizePath(path.relative(rootDirectory, absolutePath));
174
+
175
+ if (entry.isDirectory()) {
176
+ visit(absolutePath);
177
+ } else {
178
+ results.push(relativePath);
179
+ }
180
+ }
181
+ }
182
+
183
+ visit(rootDirectory);
184
+
185
+ return results.sort();
186
+ }
187
+
188
+ function transformTemplateFile(relativePath, content, targetDirectory, template) {
189
+ const packageName = toPackageName(targetDirectory, template);
190
+
191
+ if (relativePath === "package.json") {
192
+ const pkg = JSON.parse(content);
193
+ return `${JSON.stringify({ ...pkg, name: packageName }, null, 2)}\n`;
194
+ }
195
+
196
+ if (relativePath === "package-lock.json") {
197
+ const lock = JSON.parse(content);
198
+ return `${JSON.stringify(
199
+ {
200
+ ...lock,
201
+ name: packageName,
202
+ packages: lock.packages
203
+ ? {
204
+ ...lock.packages,
205
+ "": {
206
+ ...lock.packages[""],
207
+ name: packageName
208
+ }
209
+ }
210
+ : lock.packages
211
+ },
212
+ null,
213
+ 2
214
+ )}\n`;
215
+ }
216
+
217
+ return content;
218
+ }
219
+
220
+ function renderTemplateFile(template, relativePath, targetDirectory) {
221
+ if (relativePath === ".gitignore") {
222
+ const gitignorePath = path.join(getTemplateDirectory(template.id), ".gitignore");
223
+ const gitignoreContent = fs.existsSync(gitignorePath) ? fs.readFileSync(gitignorePath, "utf8") : DEFAULT_GITIGNORE;
224
+ return transformTemplateFile(relativePath, gitignoreContent, targetDirectory, template);
225
+ }
226
+
227
+ const sourcePath = path.join(getTemplateDirectory(template.id), relativePath);
228
+ const content = fs.readFileSync(sourcePath, "utf8");
229
+ return transformTemplateFile(relativePath, content, targetDirectory, template);
230
+ }
231
+
232
+ function getManagedRelativePaths(template) {
233
+ const templateDirectory = getTemplateDirectory(template.id);
234
+ const templateFiles = collectRelativeFiles(templateDirectory).filter((relativePath) => isManagedFile(template, relativePath));
235
+ const managedFiles = new Set(templateFiles);
236
+ managedFiles.add(".gitignore");
237
+ managedFiles.delete(METADATA_FILENAME);
238
+ return [...managedFiles].sort();
239
+ }
240
+
241
+ function getMetadataPath(targetDirectory) {
242
+ return path.join(targetDirectory, METADATA_FILENAME);
243
+ }
244
+
245
+ function buildProjectMetadata(template, targetDirectory) {
246
+ const managedFiles = {};
247
+
248
+ for (const relativePath of getManagedRelativePaths(template)) {
249
+ const absolutePath = path.join(targetDirectory, relativePath);
250
+ if (!fs.existsSync(absolutePath)) {
251
+ continue;
252
+ }
253
+
254
+ managedFiles[relativePath] = {
255
+ baselineHash: sha256(fs.readFileSync(absolutePath, "utf8"))
256
+ };
257
+ }
258
+
259
+ return {
260
+ schemaVersion: 1,
261
+ template: template.id,
262
+ templateVersion: CLI_PACKAGE.version,
263
+ packageName: toPackageName(targetDirectory, template),
264
+ generatedAt: new Date().toISOString(),
265
+ managedFiles
266
+ };
267
+ }
268
+
269
+ function writeProjectMetadata(template, targetDirectory, existingMetadata) {
270
+ const nextMetadata = buildProjectMetadata(template, targetDirectory);
271
+
272
+ if (existingMetadata) {
273
+ nextMetadata.generatedAt = existingMetadata.generatedAt || nextMetadata.generatedAt;
274
+ nextMetadata.templateVersion = existingMetadata.templateVersion || nextMetadata.templateVersion;
275
+ }
276
+
277
+ fs.writeFileSync(getMetadataPath(targetDirectory), `${JSON.stringify(nextMetadata, null, 2)}\n`, "utf8");
278
+ return nextMetadata;
279
+ }
280
+
281
+ function readProjectMetadata(targetDirectory) {
282
+ const metadataPath = getMetadataPath(targetDirectory);
283
+
284
+ if (!fs.existsSync(metadataPath)) {
285
+ throw new Error(`No ${METADATA_FILENAME} file found in ${targetDirectory}.`);
286
+ }
287
+
288
+ return JSON.parse(fs.readFileSync(metadataPath, "utf8"));
289
+ }
290
+
291
+ function detectTemplateFromProject(targetDirectory) {
292
+ const metadataPath = getMetadataPath(targetDirectory);
293
+ if (fs.existsSync(metadataPath)) {
294
+ const metadata = JSON.parse(fs.readFileSync(metadataPath, "utf8"));
295
+ return metadata.template;
296
+ }
297
+
298
+ if (fs.existsSync(path.join(targetDirectory, "playwright.config.ts"))) {
299
+ return "playwright-template";
300
+ }
301
+
302
+ if (fs.existsSync(path.join(targetDirectory, "cypress.config.ts"))) {
303
+ return "cypress-template";
304
+ }
305
+
306
+ throw new Error(`Could not detect the template used for ${targetDirectory}.`);
307
+ }
308
+
309
+ function analyzeUpgrade(template, targetDirectory, metadata) {
310
+ const managedPaths = getManagedRelativePaths(template);
311
+ const results = [];
312
+
313
+ for (const relativePath of managedPaths) {
314
+ const absolutePath = path.join(targetDirectory, relativePath);
315
+ const latestContent = renderTemplateFile(template, relativePath, targetDirectory);
316
+ const latestHash = sha256(latestContent);
317
+ const baselineHash = metadata.managedFiles?.[relativePath]?.baselineHash || null;
318
+ const currentExists = fs.existsSync(absolutePath);
319
+ const currentHash = currentExists ? sha256(fs.readFileSync(absolutePath, "utf8")) : null;
320
+
321
+ let status = "up-to-date";
322
+
323
+ if (!baselineHash) {
324
+ if (!currentExists) {
325
+ status = "new-file";
326
+ } else if (currentHash === latestHash) {
327
+ status = "new-file";
328
+ } else {
329
+ status = "conflict";
330
+ }
331
+ } else if (!currentExists) {
332
+ status = "conflict";
333
+ } else if (currentHash === latestHash) {
334
+ status = "up-to-date";
335
+ } else if (currentHash === baselineHash) {
336
+ status = "safe-update";
337
+ } else {
338
+ status = "conflict";
339
+ }
340
+
341
+ results.push({
342
+ relativePath,
343
+ status,
344
+ latestContent,
345
+ latestHash,
346
+ currentHash,
347
+ baselineHash,
348
+ currentExists
349
+ });
350
+ }
351
+
352
+ return results;
353
+ }
354
+
112
355
  function printHelp() {
113
356
  const supportedTemplates = TEMPLATES.map((template) => ` ${template.id}${template.aliases.length > 0 ? ` (${template.aliases.join(", ")})` : ""}`).join("\n");
114
357
 
@@ -118,6 +361,17 @@ Usage:
118
361
  create-qa-patterns
119
362
  create-qa-patterns <target-directory>
120
363
  create-qa-patterns <template> [target-directory]
364
+ create-qa-patterns --template <template> [target-directory]
365
+ create-qa-patterns upgrade check [target-directory]
366
+ create-qa-patterns upgrade apply --safe [target-directory]
367
+
368
+ Options:
369
+ --yes Accept all post-generate prompts
370
+ --no-install Skip npm install
371
+ --no-setup Skip template-specific setup such as Playwright browser install
372
+ --no-test Skip npm test
373
+ --template Explicitly choose a template without using positional arguments
374
+ --safe Required with upgrade apply; only updates unchanged managed files
121
375
 
122
376
  Interactive mode:
123
377
  When run without an explicit template, the CLI shows an interactive template picker.
@@ -127,6 +381,62 @@ ${supportedTemplates}
127
381
  `);
128
382
  }
129
383
 
384
+ function parseCliOptions(args) {
385
+ const options = {
386
+ yes: false,
387
+ noInstall: false,
388
+ noSetup: false,
389
+ noTest: false,
390
+ safe: false,
391
+ templateName: null,
392
+ positionalArgs: []
393
+ };
394
+
395
+ for (let index = 0; index < args.length; index += 1) {
396
+ const arg = args[index];
397
+
398
+ switch (arg) {
399
+ case "--yes":
400
+ options.yes = true;
401
+ break;
402
+ case "--no-install":
403
+ options.noInstall = true;
404
+ break;
405
+ case "--no-setup":
406
+ options.noSetup = true;
407
+ break;
408
+ case "--no-test":
409
+ options.noTest = true;
410
+ break;
411
+ case "--safe":
412
+ options.safe = true;
413
+ break;
414
+ case "--template": {
415
+ const templateValue = args[index + 1];
416
+ if (!templateValue) {
417
+ throw new Error("Missing value for --template.");
418
+ }
419
+
420
+ const templateName = resolveTemplate(templateValue);
421
+ if (!templateName) {
422
+ throw new Error(
423
+ `Unsupported template "${templateValue}". Supported templates: ${TEMPLATES.map((template) => template.id).join(", ")}.`
424
+ );
425
+ }
426
+
427
+ options.templateName = templateName;
428
+ index += 1;
429
+ break;
430
+ }
431
+ default:
432
+ options.positionalArgs.push(arg);
433
+ break;
434
+ }
435
+ }
436
+
437
+ return options;
438
+ }
439
+
130
440
  function parseNodeVersion(version) {
131
441
  const normalized = version.replace(/^v/, "");
132
442
  const [major = "0", minor = "0", patch = "0"] = normalized.split(".");
@@ -347,7 +657,27 @@ async function selectTemplateInteractively() {
347
657
  });
348
658
  }
349
659
 
350
- function resolveNonInteractiveArgs(args) {
660
+ function resolveNonInteractiveArgs(args, options = {}) {
661
+ if (options.templateName) {
662
+ if (args.length > 1) {
663
+ throw new Error("Too many arguments. Run `create-qa-patterns --help` for usage.");
664
+ }
665
+
666
+ if (args.length === 0) {
667
+ return {
668
+ templateName: options.templateName,
669
+ targetDirectory: process.cwd(),
670
+ generatedInCurrentDirectory: true
671
+ };
672
+ }
673
+
674
+ return {
675
+ templateName: options.templateName,
676
+ targetDirectory: path.resolve(process.cwd(), args[0]),
677
+ generatedInCurrentDirectory: false
678
+ };
679
+ }
680
+
351
681
  if (args.length === 0) {
352
682
  return {
353
683
  templateName: DEFAULT_TEMPLATE,
@@ -449,32 +779,24 @@ function updateJsonFile(filePath, update) {
449
779
  }
450
780
 
451
781
  function customizeProject(targetDirectory, template) {
452
- const packageName = toPackageName(targetDirectory, template);
453
782
  const packageJsonPath = path.join(targetDirectory, "package.json");
454
783
  const packageLockPath = path.join(targetDirectory, "package-lock.json");
455
784
  const gitignorePath = path.join(targetDirectory, ".gitignore");
456
785
 
457
786
  if (fs.existsSync(packageJsonPath)) {
458
- updateJsonFile(packageJsonPath, (pkg) => ({
459
- ...pkg,
460
- name: packageName
461
- }));
787
+ fs.writeFileSync(
788
+ packageJsonPath,
789
+ transformTemplateFile("package.json", fs.readFileSync(packageJsonPath, "utf8"), targetDirectory, template),
790
+ "utf8"
791
+ );
462
792
  }
463
793
 
464
794
  if (fs.existsSync(packageLockPath)) {
465
- updateJsonFile(packageLockPath, (lock) => ({
466
- ...lock,
467
- name: packageName,
468
- packages: lock.packages
469
- ? {
470
- ...lock.packages,
471
- "": {
472
- ...lock.packages[""],
473
- name: packageName
474
- }
475
- }
476
- : lock.packages
477
- }));
795
+ fs.writeFileSync(
796
+ packageLockPath,
797
+ transformTemplateFile("package-lock.json", fs.readFileSync(packageLockPath, "utf8"), targetDirectory, template),
798
+ "utf8"
799
+ );
478
800
  }
479
801
 
480
802
  if (!fs.existsSync(gitignorePath)) {
@@ -654,6 +976,85 @@ function formatStatus(status) {
654
976
  }
655
977
  }
656
978
 
979
+ function formatUpgradeStatus(status) {
980
+ switch (status) {
981
+ case "safe-update":
982
+ return colors.green("safe update available");
983
+ case "new-file":
984
+ return colors.green("new managed file available");
985
+ case "conflict":
986
+ return colors.yellow("manual review required");
987
+ default:
988
+ return colors.dim("up to date");
989
+ }
990
+ }
991
+
992
+ function printUpgradeReport(targetDirectory, metadata, results) {
993
+ const safeCount = results.filter((entry) => entry.status === "safe-update").length;
994
+ const newCount = results.filter((entry) => entry.status === "new-file").length;
995
+ const conflictCount = results.filter((entry) => entry.status === "conflict").length;
996
+
997
+ process.stdout.write(`\n${colors.bold("Upgrade check")}\n`);
998
+ process.stdout.write(` Target: ${targetDirectory}\n`);
999
+ process.stdout.write(` Template: ${metadata.template}\n`);
1000
+ process.stdout.write(` Current baseline version: ${metadata.templateVersion}\n`);
1001
+ process.stdout.write(` CLI template version: ${CLI_PACKAGE.version}\n`);
1002
+ process.stdout.write(` Safe updates: ${safeCount}\n`);
1003
+ process.stdout.write(` New managed files: ${newCount}\n`);
1004
+ process.stdout.write(` Conflicts: ${conflictCount}\n\n`);
1005
+
1006
+ for (const entry of results) {
1007
+ if (entry.status === "up-to-date") {
1008
+ continue;
1009
+ }
1010
+
1011
+ process.stdout.write(` ${entry.relativePath}: ${formatUpgradeStatus(entry.status)}\n`);
1012
+ }
1013
+
1014
+ if (safeCount === 0 && newCount === 0 && conflictCount === 0) {
1015
+ process.stdout.write(`${colors.green("Everything already matches the current managed template files.")}\n`);
1016
+ }
1017
+
1018
+ process.stdout.write("\n");
1019
+ }
1020
+
1021
+ function applySafeUpdates(targetDirectory, metadata, results) {
1022
+ const nextMetadata = {
1023
+ ...metadata,
1024
+ managedFiles: {
1025
+ ...metadata.managedFiles
1026
+ }
1027
+ };
1028
+
1029
+ let appliedCount = 0;
1030
+
1031
+ for (const entry of results) {
1032
+ if (!["safe-update", "new-file"].includes(entry.status)) {
1033
+ continue;
1034
+ }
1035
+
1036
+ const absolutePath = path.join(targetDirectory, entry.relativePath);
1037
+ fs.mkdirSync(path.dirname(absolutePath), { recursive: true });
1038
+ fs.writeFileSync(absolutePath, entry.latestContent, "utf8");
1039
+ nextMetadata.managedFiles[entry.relativePath] = {
1040
+ baselineHash: entry.latestHash
1041
+ };
1042
+ appliedCount += 1;
1043
+ }
1044
+
1045
+ const remainingConflicts = results.filter((entry) => entry.status === "conflict").length;
1046
+ if (remainingConflicts === 0) {
1047
+ nextMetadata.templateVersion = CLI_PACKAGE.version;
1048
+ }
1049
+
1050
+ fs.writeFileSync(getMetadataPath(targetDirectory), `${JSON.stringify(nextMetadata, null, 2)}\n`, "utf8");
1051
+
1052
+ process.stdout.write(`\n${colors.bold("Upgrade apply")}\n`);
1053
+ process.stdout.write(` Applied safe updates: ${appliedCount}\n`);
1054
+ process.stdout.write(` Remaining conflicts: ${remainingConflicts}\n`);
1055
+ process.stdout.write("\n");
1056
+ }
1057
+
657
1058
  function printSummary(summary) {
658
1059
  process.stdout.write(`\n${colors.bold("Summary")}\n`);
659
1060
  process.stdout.write(` Template: ${summary.template.id}\n`);
@@ -670,20 +1071,22 @@ function printSummary(summary) {
670
1071
  }
671
1072
 
672
1073
  async function runPostGenerateActions(template, targetDirectory, summary) {
673
- if (!process.stdin.isTTY || !process.stdout.isTTY) {
674
- return;
675
- }
676
-
677
1074
  const prerequisites = collectPrerequisites();
1075
+ const options = summary.options;
1076
+ const canPrompt = process.stdin.isTTY && process.stdout.isTTY;
678
1077
 
679
1078
  if (prerequisites.npm) {
680
- const shouldInstallDependencies = await askYesNo("Run npm install now?", true);
681
-
682
- if (shouldInstallDependencies) {
683
- await runCommand("npm", ["install"], targetDirectory);
684
- summary.npmInstall = "completed";
685
- } else {
1079
+ if (options.noInstall) {
686
1080
  summary.npmInstall = "skipped";
1081
+ } else {
1082
+ const shouldInstallDependencies = options.yes ? true : canPrompt ? await askYesNo("Run npm install now?", true) : false;
1083
+
1084
+ if (shouldInstallDependencies) {
1085
+ await runCommand("npm", ["install"], targetDirectory);
1086
+ summary.npmInstall = "completed";
1087
+ } else {
1088
+ summary.npmInstall = canPrompt ? "skipped" : "not-run";
1089
+ }
687
1090
  }
688
1091
  } else {
689
1092
  process.stdout.write(`${colors.yellow("Skipping")} npm install prompt because npm is not available.\n`);
@@ -691,8 +1094,10 @@ async function runPostGenerateActions(template, targetDirectory, summary) {
691
1094
  }
692
1095
 
693
1096
  if (template.setup) {
694
- if (prerequisites[template.setup.availability]) {
695
- const shouldRunExtraSetup = await askYesNo(template.setup.prompt, true);
1097
+ if (options.noSetup) {
1098
+ summary.extraSetup = "skipped";
1099
+ } else if (prerequisites[template.setup.availability]) {
1100
+ const shouldRunExtraSetup = options.yes ? true : canPrompt ? await askYesNo(template.setup.prompt, true) : false;
696
1101
 
697
1102
  if (shouldRunExtraSetup) {
698
1103
  try {
@@ -711,7 +1116,7 @@ async function runPostGenerateActions(template, targetDirectory, summary) {
711
1116
  }
712
1117
  }
713
1118
  } else {
714
- summary.extraSetup = "skipped";
1119
+ summary.extraSetup = canPrompt ? "skipped" : "not-run";
715
1120
  }
716
1121
  } else {
717
1122
  process.stdout.write(
@@ -722,13 +1127,17 @@ async function runPostGenerateActions(template, targetDirectory, summary) {
722
1127
  }
723
1128
 
724
1129
  if (prerequisites.npm) {
725
- const shouldRunTests = await askYesNo("Run npm test now?", false);
726
-
727
- if (shouldRunTests) {
728
- await runCommand("npm", ["test"], targetDirectory);
729
- summary.testRun = "completed";
730
- } else {
1130
+ if (options.noTest) {
731
1131
  summary.testRun = "skipped";
1132
+ } else {
1133
+ const shouldRunTests = options.yes ? true : canPrompt ? await askYesNo("Run npm test now?", false) : false;
1134
+
1135
+ if (shouldRunTests) {
1136
+ await runCommand("npm", ["test"], targetDirectory);
1137
+ summary.testRun = "completed";
1138
+ } else {
1139
+ summary.testRun = canPrompt ? "skipped" : "not-run";
1140
+ }
732
1141
  }
733
1142
  } else {
734
1143
  process.stdout.write(`${colors.yellow("Skipping")} npm test prompt because npm is not available.\n`);
@@ -736,17 +1145,66 @@ async function runPostGenerateActions(template, targetDirectory, summary) {
736
1145
  }
737
1146
  }
738
1147
 
1148
+ function resolveUpgradeTarget(args) {
1149
+ if (args.length > 1) {
1150
+ throw new Error("Too many arguments for upgrade. Use `create-qa-patterns upgrade check [target-directory]`.");
1151
+ }
1152
+
1153
+ return path.resolve(process.cwd(), args[0] || ".");
1154
+ }
1155
+
1156
+ function runUpgradeCommand(rawArgs) {
1157
+ const [subcommand = "check", ...rest] = rawArgs;
1158
+ const options = parseCliOptions(rest);
1159
+ const targetDirectory = resolveUpgradeTarget(options.positionalArgs);
1160
+ const metadata = readProjectMetadata(targetDirectory);
1161
+ const templateId = metadata.template || detectTemplateFromProject(targetDirectory);
1162
+ const template = getTemplate(templateId);
1163
+
1164
+ if (!template) {
1165
+ throw new Error(`Unsupported template "${templateId}".`);
1166
+ }
1167
+
1168
+ const results = analyzeUpgrade(template, targetDirectory, metadata);
1169
+
1170
+ if (subcommand === "check" || subcommand === "report") {
1171
+ printUpgradeReport(targetDirectory, metadata, results);
1172
+ return;
1173
+ }
1174
+
1175
+ if (subcommand === "apply") {
1176
+ if (!options.safe) {
1177
+ throw new Error("Upgrade apply requires --safe. Only safe managed-file updates are supported.");
1178
+ }
1179
+
1180
+ printUpgradeReport(targetDirectory, metadata, results);
1181
+ applySafeUpdates(targetDirectory, metadata, results);
1182
+ return;
1183
+ }
1184
+
1185
+ throw new Error(`Unsupported upgrade command "${subcommand}". Use check, report, or apply --safe.`);
1186
+ }
1187
+
739
1188
  async function main() {
740
- const args = process.argv.slice(2);
1189
+ const rawArgs = process.argv.slice(2);
741
1190
 
742
1191
  assertSupportedNodeVersion();
743
1192
 
744
- if (args.includes("--help") || args.includes("-h")) {
1193
+ if (rawArgs[0] === "upgrade") {
1194
+ runUpgradeCommand(rawArgs.slice(1));
1195
+ return;
1196
+ }
1197
+
1198
+ if (rawArgs.includes("--help") || rawArgs.includes("-h")) {
745
1199
  printHelp();
746
1200
  return;
747
1201
  }
748
1202
 
749
- const { templateName, targetDirectory, generatedInCurrentDirectory } = await resolveScaffoldArgs(args);
1203
+ const options = parseCliOptions(rawArgs);
1204
+ const args = options.positionalArgs;
1205
+ const { templateName, targetDirectory, generatedInCurrentDirectory } = options.templateName
1206
+ ? resolveNonInteractiveArgs(args, options)
1207
+ : await resolveScaffoldArgs(args);
750
1208
  const template = getTemplate(templateName);
751
1209
 
752
1210
  if (!template) {
@@ -755,11 +1213,14 @@ async function main() {
755
1213
 
756
1214
  const prerequisites = collectPrerequisites();
757
1215
  const summary = createSummary(template, targetDirectory, generatedInCurrentDirectory);
1216
+ summary.options = options;
758
1217
  printPrerequisiteWarnings(prerequisites);
759
1218
  await scaffoldProject(template, targetDirectory, prerequisites);
1219
+ writeProjectMetadata(template, targetDirectory);
760
1220
  summary.gitInit = prerequisites.git ? "completed" : "unavailable";
761
1221
  printSuccess(template, targetDirectory, generatedInCurrentDirectory);
762
1222
  await runPostGenerateActions(template, targetDirectory, summary);
1223
+ writeProjectMetadata(template, targetDirectory, readProjectMetadata(targetDirectory));
763
1224
  printSummary(summary);
764
1225
  printNextSteps(summary);
765
1226
  }