@toolstackhq/create-qa-patterns 1.0.13 → 1.0.15

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 (123) hide show
  1. package/README.md +23 -0
  2. package/index.js +282 -738
  3. package/lib/args.js +139 -0
  4. package/lib/constants.js +115 -0
  5. package/lib/interactive.js +131 -0
  6. package/lib/local-env.js +65 -0
  7. package/lib/metadata.js +329 -0
  8. package/lib/output.js +326 -0
  9. package/lib/prereqs.js +72 -0
  10. package/lib/scaffold.js +120 -0
  11. package/lib/templates.js +40 -0
  12. package/package.json +5 -3
  13. package/templates/cypress-template/.env.example +2 -2
  14. package/templates/cypress-template/.github/workflows/cypress-tests.yml +2 -2
  15. package/templates/cypress-template/README.md +29 -6
  16. package/templates/cypress-template/allurerc.mjs +1 -1
  17. package/templates/cypress-template/config/environments.ts +13 -11
  18. package/templates/cypress-template/config/runtime-config.ts +17 -12
  19. package/templates/cypress-template/config/secret-manager.ts +1 -1
  20. package/templates/cypress-template/config/test-env.ts +3 -3
  21. package/templates/cypress-template/cypress/e2e/ui-journey.cy.ts +12 -10
  22. package/templates/cypress-template/cypress/support/app-config.ts +5 -5
  23. package/templates/cypress-template/cypress/support/commands.ts +7 -7
  24. package/templates/cypress-template/cypress/support/data/data-factory.ts +6 -4
  25. package/templates/cypress-template/cypress/support/data/id-generator.ts +1 -1
  26. package/templates/cypress-template/cypress/support/data/seeded-faker.ts +2 -2
  27. package/templates/cypress-template/cypress/support/e2e.ts +2 -2
  28. package/templates/cypress-template/cypress/support/pages/login-page.ts +4 -4
  29. package/templates/cypress-template/cypress/support/pages/people-page.ts +10 -10
  30. package/templates/cypress-template/cypress.config.ts +9 -9
  31. package/templates/cypress-template/demo-apps/ui-demo-app/public/styles.css +1 -1
  32. package/templates/cypress-template/demo-apps/ui-demo-app/src/server.js +44 -41
  33. package/templates/cypress-template/demo-apps/ui-demo-app/src/store.js +31 -3
  34. package/templates/cypress-template/demo-apps/ui-demo-app/src/templates.js +5 -5
  35. package/templates/cypress-template/eslint.config.mjs +53 -45
  36. package/templates/cypress-template/package.json +6 -5
  37. package/templates/cypress-template/scripts/ensure-local-env.mjs +36 -0
  38. package/templates/cypress-template/scripts/generate-allure-report.mjs +16 -10
  39. package/templates/cypress-template/scripts/run-cypress.mjs +33 -24
  40. package/templates/cypress-template/scripts/run-tests.sh +1 -0
  41. package/templates/cypress-template/tsconfig.json +7 -1
  42. package/templates/playwright-template/.env.example +6 -6
  43. package/templates/playwright-template/.github/workflows/playwright-tests.yml +14 -5
  44. package/templates/playwright-template/README.md +25 -5
  45. package/templates/playwright-template/allurerc.mjs +1 -1
  46. package/templates/playwright-template/components/flash-message.ts +2 -2
  47. package/templates/playwright-template/config/environments.ts +16 -14
  48. package/templates/playwright-template/config/runtime-config.ts +17 -12
  49. package/templates/playwright-template/config/secret-manager.ts +1 -1
  50. package/templates/playwright-template/config/test-env.ts +3 -3
  51. package/templates/playwright-template/data/factories/data-factory.ts +6 -4
  52. package/templates/playwright-template/data/generators/id-generator.ts +1 -1
  53. package/templates/playwright-template/data/generators/seeded-faker.ts +2 -2
  54. package/templates/playwright-template/demo-apps/api-demo-server/src/server.js +9 -9
  55. package/templates/playwright-template/demo-apps/api-demo-server/src/store.js +1 -1
  56. package/templates/playwright-template/demo-apps/ui-demo-app/public/styles.css +1 -1
  57. package/templates/playwright-template/demo-apps/ui-demo-app/src/server.js +44 -41
  58. package/templates/playwright-template/demo-apps/ui-demo-app/src/store.js +31 -3
  59. package/templates/playwright-template/demo-apps/ui-demo-app/src/templates.js +5 -5
  60. package/templates/playwright-template/eslint.config.mjs +40 -40
  61. package/templates/playwright-template/fixtures/test-fixtures.ts +27 -12
  62. package/templates/playwright-template/lint/architecture-plugin.cjs +36 -31
  63. package/templates/playwright-template/package.json +7 -6
  64. package/templates/playwright-template/pages/base-page.ts +4 -4
  65. package/templates/playwright-template/pages/login-page.ts +9 -9
  66. package/templates/playwright-template/pages/people-page.ts +21 -17
  67. package/templates/playwright-template/playwright.config.ts +22 -19
  68. package/templates/playwright-template/reporters/structured-reporter.ts +11 -8
  69. package/templates/playwright-template/scripts/ensure-local-env.mjs +37 -0
  70. package/templates/playwright-template/scripts/generate-allure-report.mjs +16 -10
  71. package/templates/playwright-template/scripts/run-tests.sh +1 -0
  72. package/templates/playwright-template/tests/api-people.spec.ts +8 -6
  73. package/templates/playwright-template/tests/ui-journey.spec.ts +13 -8
  74. package/templates/playwright-template/tsconfig.json +3 -11
  75. package/templates/playwright-template/utils/logger.ts +12 -8
  76. package/templates/playwright-template/utils/test-step.ts +5 -5
  77. package/templates/wdio-template/.env.example +14 -0
  78. package/templates/wdio-template/.github/workflows/wdio-tests.yml +46 -0
  79. package/templates/wdio-template/README.md +241 -0
  80. package/templates/wdio-template/allurerc.mjs +10 -0
  81. package/templates/wdio-template/components/README.md +5 -0
  82. package/templates/wdio-template/components/flash-message.ts +16 -0
  83. package/templates/wdio-template/config/README.md +5 -0
  84. package/templates/wdio-template/config/environments.ts +40 -0
  85. package/templates/wdio-template/config/runtime-config.ts +53 -0
  86. package/templates/wdio-template/config/secret-manager.ts +29 -0
  87. package/templates/wdio-template/config/test-env.ts +9 -0
  88. package/templates/wdio-template/data/README.md +9 -0
  89. package/templates/wdio-template/data/factories/README.md +6 -0
  90. package/templates/wdio-template/data/factories/data-factory.ts +36 -0
  91. package/templates/wdio-template/data/generators/README.md +5 -0
  92. package/templates/wdio-template/data/generators/id-generator.ts +18 -0
  93. package/templates/wdio-template/data/generators/seeded-faker.ts +14 -0
  94. package/templates/wdio-template/demo-apps/ui-demo-app/public/styles.css +120 -0
  95. package/templates/wdio-template/demo-apps/ui-demo-app/src/server.js +152 -0
  96. package/templates/wdio-template/demo-apps/ui-demo-app/src/store.js +71 -0
  97. package/templates/wdio-template/demo-apps/ui-demo-app/src/templates.js +121 -0
  98. package/templates/wdio-template/eslint.config.mjs +86 -0
  99. package/templates/wdio-template/lint/architecture-plugin.cjs +123 -0
  100. package/templates/wdio-template/package-lock.json +11058 -0
  101. package/templates/wdio-template/package.json +44 -0
  102. package/templates/wdio-template/pages/README.md +6 -0
  103. package/templates/wdio-template/pages/base-page.ts +15 -0
  104. package/templates/wdio-template/pages/login-page.ts +27 -0
  105. package/templates/wdio-template/pages/people-page.ts +54 -0
  106. package/templates/wdio-template/reporters/README.md +5 -0
  107. package/templates/wdio-template/reporters/structured-reporter.ts +78 -0
  108. package/templates/wdio-template/scripts/README.md +5 -0
  109. package/templates/wdio-template/scripts/ensure-local-env.mjs +36 -0
  110. package/templates/wdio-template/scripts/generate-allure-report.mjs +72 -0
  111. package/templates/wdio-template/scripts/run-tests.sh +7 -0
  112. package/templates/wdio-template/scripts/run-wdio.mjs +114 -0
  113. package/templates/wdio-template/tests/README.md +7 -0
  114. package/templates/wdio-template/tests/ui-journey.spec.ts +52 -0
  115. package/templates/wdio-template/tsconfig.json +22 -0
  116. package/templates/wdio-template/utils/README.md +5 -0
  117. package/templates/wdio-template/utils/logger.ts +60 -0
  118. package/templates/wdio-template/utils/test-step.ts +20 -0
  119. package/templates/wdio-template/wdio.conf.ts +58 -0
  120. package/tests/args.test.js +58 -0
  121. package/tests/local-env.test.js +70 -0
  122. package/tests/metadata.test.js +147 -0
  123. package/tests/templates.test.js +44 -0
package/index.js CHANGED
@@ -1,492 +1,131 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- const fs = require("node:fs");
4
- const path = require("node:path");
5
- const readline = require("node:readline");
6
- const { spawn, spawnSync } = require("node:child_process");
7
-
8
- const DEFAULT_TEMPLATE = "playwright-template";
9
- const MIN_NODE_VERSION = {
10
- major: 18,
11
- minor: 18,
12
- patch: 0
13
- };
14
- const COLOR_ENABLED = Boolean(process.stdout.isTTY) && !("NO_COLOR" in process.env);
15
- const DEFAULT_GITIGNORE = `node_modules/
16
-
17
- .env
18
- .env.*
19
- !.env.example
20
-
21
- .DS_Store
22
- *.log
23
- *.tgz
24
- .idea/
25
- .vscode/
26
- .nyc_output/
27
- coverage/
28
- dist/
29
- build/
30
- tmp/
31
- temp/
32
- downloads/
33
- cypress.env.json
34
- reports/
35
- cypress/screenshots/
36
- cypress/videos/
37
- reports/screenshots/
38
- reports/videos/
39
- allure-results/
40
- allure-report/
41
- test-results/
42
- playwright-report/
43
- `;
44
-
45
- const TEMPLATES = [
46
- {
47
- id: DEFAULT_TEMPLATE,
48
- aliases: ["playwright", "pw"],
49
- label: "Playwright Template",
50
- description: "TypeScript starter with page objects, fixtures, multi-environment config, reporting, linting, CI and Docker.",
51
- defaultPackageName: "playwright-template",
52
- demoAppsManagedByTemplate: true,
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+ const {
6
+ CLI_PACKAGE_VERSION,
7
+ DEFAULT_GITIGNORE,
8
+ DEFAULT_TEMPLATE,
9
+ MANAGED_FILE_PATTERNS,
10
+ METADATA_FILENAME,
11
+ MIN_NODE_VERSION,
12
+ TEMPLATE_CATALOG
13
+ } = require('./lib/constants');
14
+ const { parseCliOptions, resolveNonInteractiveArgs } = require('./lib/args');
15
+ const {
16
+ analyzeUpgrade,
17
+ applySafeUpdates,
18
+ detectTemplateFromProject,
19
+ readProjectMetadata,
20
+ renderTemplateFile,
21
+ writeProjectMetadata
22
+ } = require('./lib/metadata');
23
+ const {
24
+ createLocalCredentials,
25
+ writeGeneratedLocalEnv
26
+ } = require('./lib/local-env');
27
+ const {
28
+ askQuestion,
29
+ askYesNo,
30
+ selectTemplateInteractively
31
+ } = require('./lib/interactive');
32
+ const {
33
+ assertSupportedNodeVersion,
34
+ createColors,
35
+ printHelp,
36
+ printNextSteps,
37
+ printPlaywrightInstallRecovery,
38
+ printPrerequisiteWarnings,
39
+ printSuccess,
40
+ printSummary,
41
+ printUpgradeReport
42
+ } = require('./lib/output');
43
+ const {
44
+ collectPrerequisites,
45
+ initializeGitRepository,
46
+ runCommand
47
+ } = require('./lib/prereqs');
48
+ const { scaffoldProject } = require('./lib/scaffold');
49
+ const {
50
+ createTemplateAliases,
51
+ getTemplate,
52
+ getTemplateDirectory,
53
+ resolveTemplate,
54
+ toPackageName
55
+ } = require('./lib/templates');
56
+
57
+ const colors = createColors();
58
+ const TEMPLATES = TEMPLATE_CATALOG.map((template) => {
59
+ if (template.id !== 'playwright-template') {
60
+ return template;
61
+ }
62
+
63
+ return {
64
+ ...template,
53
65
  setup: {
54
- availability: "npx",
55
- prompt: "Run npx playwright install now?",
56
- summaryLabel: "Playwright browser install",
57
- nextStep: "npx playwright install",
66
+ ...template.setup,
58
67
  run(targetDirectory) {
59
- return runCommand("npx", ["playwright", "install"], targetDirectory);
68
+ return runCommand('npx', ['playwright', 'install'], targetDirectory);
60
69
  },
61
70
  recovery(targetDirectory) {
62
- printPlaywrightInstallRecovery(targetDirectory);
71
+ printPlaywrightInstallRecovery(targetDirectory, colors);
63
72
  }
64
73
  }
65
- },
66
- {
67
- id: "cypress-template",
68
- aliases: ["cypress", "cy"],
69
- label: "Cypress Template",
70
- description: "TypeScript starter with Cypress e2e specs, custom commands, page modules, env-based config, CI, and a bundled demo app.",
71
- defaultPackageName: "cypress-template",
72
- demoAppsManagedByTemplate: true
73
- }
74
- ];
75
-
76
- const TEMPLATE_ALIASES = new Map(
77
- TEMPLATES.flatMap((template) => [
78
- [template.id, template.id],
79
- ...template.aliases.map((alias) => [alias, template.id])
80
- ])
81
- );
82
-
83
- function style(text, ...codes) {
84
- if (!COLOR_ENABLED) {
85
- return text;
86
- }
87
-
88
- return `\u001b[${codes.join(";")}m${text}\u001b[0m`;
89
- }
90
-
91
- const colors = {
92
- bold(text) {
93
- return style(text, 1);
94
- },
95
- dim(text) {
96
- return style(text, 2);
97
- },
98
- cyan(text) {
99
- return style(text, 36);
100
- },
101
- green(text) {
102
- return style(text, 32);
103
- },
104
- yellow(text) {
105
- return style(text, 33);
106
- },
107
- red(text) {
108
- return style(text, 31);
109
- }
110
- };
111
-
112
- function printHelp() {
113
- const supportedTemplates = TEMPLATES.map((template) => ` ${template.id}${template.aliases.length > 0 ? ` (${template.aliases.join(", ")})` : ""}`).join("\n");
114
-
115
- process.stdout.write(`${colors.bold("create-qa-patterns")}
116
-
117
- Usage:
118
- create-qa-patterns
119
- create-qa-patterns <target-directory>
120
- create-qa-patterns <template> [target-directory]
121
- create-qa-patterns --template <template> [target-directory]
122
-
123
- Options:
124
- --yes Accept all post-generate prompts
125
- --no-install Skip npm install
126
- --no-setup Skip template-specific setup such as Playwright browser install
127
- --no-test Skip npm test
128
- --template Explicitly choose a template without using positional arguments
129
-
130
- Interactive mode:
131
- When run without an explicit template, the CLI shows an interactive template picker.
132
-
133
- Supported templates:
134
- ${supportedTemplates}
135
- `);
136
- }
137
-
138
- function parseCliOptions(args) {
139
- const options = {
140
- yes: false,
141
- noInstall: false,
142
- noSetup: false,
143
- noTest: false,
144
- templateName: null,
145
- positionalArgs: []
146
74
  };
75
+ });
147
76
 
148
- for (let index = 0; index < args.length; index += 1) {
149
- const arg = args[index];
150
-
151
- switch (arg) {
152
- case "--yes":
153
- options.yes = true;
154
- break;
155
- case "--no-install":
156
- options.noInstall = true;
157
- break;
158
- case "--no-setup":
159
- options.noSetup = true;
160
- break;
161
- case "--no-test":
162
- options.noTest = true;
163
- break;
164
- case "--template": {
165
- const templateValue = args[index + 1];
166
- if (!templateValue) {
167
- throw new Error("Missing value for --template.");
168
- }
169
-
170
- const templateName = resolveTemplate(templateValue);
171
- if (!templateName) {
172
- throw new Error(
173
- `Unsupported template "${templateValue}". Supported templates: ${TEMPLATES.map((template) => template.id).join(", ")}.`
174
- );
175
- }
176
-
177
- options.templateName = templateName;
178
- index += 1;
179
- break;
180
- }
181
- default:
182
- options.positionalArgs.push(arg);
183
- break;
184
- }
185
- }
186
-
187
- return options;
188
- }
189
-
190
- function parseNodeVersion(version) {
191
- const normalized = version.replace(/^v/, "");
192
- const [major = "0", minor = "0", patch = "0"] = normalized.split(".");
193
-
194
- return {
195
- major: Number.parseInt(major, 10),
196
- minor: Number.parseInt(minor, 10),
197
- patch: Number.parseInt(patch, 10)
198
- };
199
- }
200
-
201
- function isNodeVersionSupported(version) {
202
- if (version.major !== MIN_NODE_VERSION.major) {
203
- return version.major > MIN_NODE_VERSION.major;
204
- }
205
-
206
- if (version.minor !== MIN_NODE_VERSION.minor) {
207
- return version.minor > MIN_NODE_VERSION.minor;
208
- }
209
-
210
- return version.patch >= MIN_NODE_VERSION.patch;
211
- }
212
-
213
- function assertSupportedNodeVersion() {
214
- const currentVersion = parseNodeVersion(process.version);
215
-
216
- if (!isNodeVersionSupported(currentVersion)) {
217
- throw new Error(
218
- `Node ${MIN_NODE_VERSION.major}.${MIN_NODE_VERSION.minor}.${MIN_NODE_VERSION.patch}+ is required. Current version: ${process.version}`
219
- );
220
- }
221
- }
222
-
223
- function resolveTemplate(value) {
224
- return TEMPLATE_ALIASES.get(value);
225
- }
226
-
227
- function getTemplate(templateId) {
228
- return TEMPLATES.find((template) => template.id === templateId);
229
- }
230
-
231
- function sleep(ms) {
232
- return new Promise((resolve) => setTimeout(resolve, ms));
233
- }
234
-
235
- function commandExists(command) {
236
- const result = spawnSync(getCommandName(command), ["--version"], {
237
- stdio: "ignore"
238
- });
239
-
240
- return !result.error && result.status === 0;
241
- }
77
+ const TEMPLATE_ALIASES = createTemplateAliases(TEMPLATES);
78
+ const SUPPORTED_TEMPLATE_IDS = TEMPLATES.map((template) => template.id);
242
79
 
243
- function collectPrerequisites() {
80
+ function createMetadataOptions() {
244
81
  return {
245
- npm: commandExists("npm"),
246
- npx: commandExists("npx"),
247
- docker: commandExists("docker"),
248
- git: commandExists("git")
82
+ cliPackageVersion: CLI_PACKAGE_VERSION,
83
+ defaultGitignore: DEFAULT_GITIGNORE,
84
+ getTemplateDirectory: (templateId) =>
85
+ getTemplateDirectory(__dirname, templateId),
86
+ managedPatterns: MANAGED_FILE_PATTERNS,
87
+ metadataFilename: METADATA_FILENAME,
88
+ toPackageName
249
89
  };
250
90
  }
251
91
 
252
- function printPrerequisiteWarnings(prerequisites) {
253
- if (!prerequisites.npm) {
254
- process.stdout.write(`${colors.yellow("Warning:")} npm was not found. Automated install and test steps will be unavailable.\n`);
255
- }
256
-
257
- if (!prerequisites.npx) {
258
- process.stdout.write(`${colors.yellow("Warning:")} npx was not found. Template setup steps that depend on npx will be unavailable.\n`);
259
- }
260
-
261
- if (!prerequisites.docker) {
262
- process.stdout.write(`${colors.yellow("Warning:")} docker was not found. Docker-based template flows will not run until Docker is installed.\n`);
263
- }
264
-
265
- if (!prerequisites.git) {
266
- process.stdout.write(`${colors.yellow("Warning:")} git was not found. The generated project cannot be initialized as a repository automatically.\n`);
267
- }
268
-
269
- if (!prerequisites.npm || !prerequisites.npx || !prerequisites.docker || !prerequisites.git) {
270
- process.stdout.write("\n");
271
- }
272
- }
273
-
274
- function createLineInterface() {
275
- return readline.createInterface({
276
- input: process.stdin,
277
- output: process.stdout
278
- });
279
- }
280
-
281
92
  function createSummary(template, targetDirectory, generatedInCurrentDirectory) {
282
93
  return {
283
94
  template,
284
95
  targetDirectory,
96
+ targetRelativePath: path.relative(process.cwd(), targetDirectory) || '.',
285
97
  generatedInCurrentDirectory,
286
98
  demoAppsManagedByTemplate: Boolean(template.demoAppsManagedByTemplate),
287
- gitInit: "not-run",
288
- npmInstall: "not-run",
289
- extraSetup: template.setup ? "not-run" : null,
290
- testRun: "not-run"
99
+ localCredentials: null,
100
+ gitInit: 'not-run',
101
+ npmInstall: 'not-run',
102
+ extraSetup: template.setup ? 'not-run' : null,
103
+ testRun: 'not-run'
291
104
  };
292
105
  }
293
106
 
294
- function askQuestion(prompt) {
295
- const lineInterface = createLineInterface();
296
-
297
- return new Promise((resolve) => {
298
- lineInterface.question(prompt, (answer) => {
299
- lineInterface.close();
300
- resolve(answer.trim());
301
- });
302
- });
303
- }
304
-
305
- async function askYesNo(prompt, defaultValue = true) {
306
- const suffix = defaultValue ? " [Y/n] " : " [y/N] ";
307
-
308
- while (true) {
309
- const answer = (await askQuestion(`${prompt}${suffix}`)).toLowerCase();
310
-
311
- if (!answer) {
312
- return defaultValue;
313
- }
314
-
315
- if (["y", "yes"].includes(answer)) {
316
- return true;
317
- }
318
-
319
- if (["n", "no"].includes(answer)) {
320
- return false;
321
- }
322
-
323
- process.stdout.write("Please answer yes or no.\n");
324
- }
325
- }
326
-
327
- async function selectTemplateInteractively() {
328
- if (!process.stdin.isTTY || !process.stdout.isTTY) {
329
- return DEFAULT_TEMPLATE;
330
- }
331
-
332
- readline.emitKeypressEvents(process.stdin);
333
-
334
- if (typeof process.stdin.setRawMode === "function") {
335
- process.stdin.setRawMode(true);
336
- }
337
-
338
- let selectedIndex = 0;
339
- let renderedLines = 0;
340
-
341
- const render = () => {
342
- if (renderedLines > 0) {
343
- readline.moveCursor(process.stdout, 0, -renderedLines);
344
- readline.clearScreenDown(process.stdout);
345
- }
346
-
347
- const lines = [
348
- "Select a template",
349
- "Use ↑/↓ to choose and press Enter to continue.",
350
- ""
351
- ];
352
-
353
- for (let index = 0; index < TEMPLATES.length; index += 1) {
354
- const template = TEMPLATES[index];
355
- const marker = index === selectedIndex ? ">" : " ";
356
- lines.push(`${marker} ${template.label}`);
357
- lines.push(` ${template.description}`);
358
- lines.push("");
359
- }
360
-
361
- renderedLines = lines.length;
362
- process.stdout.write(`${lines.join("\n")}\n`);
363
- };
364
-
365
- render();
366
-
367
- return new Promise((resolve) => {
368
- const handleKeypress = (_, key) => {
369
- if (!key) {
370
- return;
371
- }
372
-
373
- if (key.name === "up") {
374
- selectedIndex = (selectedIndex - 1 + TEMPLATES.length) % TEMPLATES.length;
375
- render();
376
- return;
377
- }
378
-
379
- if (key.name === "down") {
380
- selectedIndex = (selectedIndex + 1) % TEMPLATES.length;
381
- render();
382
- return;
383
- }
384
-
385
- if (key.name === "return") {
386
- process.stdin.off("keypress", handleKeypress);
387
- if (typeof process.stdin.setRawMode === "function") {
388
- process.stdin.setRawMode(false);
389
- }
390
- readline.clearScreenDown(process.stdout);
391
- process.stdout.write(`Selected: ${TEMPLATES[selectedIndex].label}\n\n`);
392
- resolve(TEMPLATES[selectedIndex].id);
393
- return;
394
- }
395
-
396
- if (key.ctrl && key.name === "c") {
397
- process.stdin.off("keypress", handleKeypress);
398
- if (typeof process.stdin.setRawMode === "function") {
399
- process.stdin.setRawMode(false);
400
- }
401
- process.stdout.write("\n");
402
- process.exit(1);
403
- }
404
- };
405
-
406
- process.stdin.on("keypress", handleKeypress);
407
- });
408
- }
409
-
410
- function resolveNonInteractiveArgs(args, options = {}) {
411
- if (options.templateName) {
412
- if (args.length > 1) {
413
- throw new Error("Too many arguments. Run `create-qa-patterns --help` for usage.");
414
- }
415
-
416
- if (args.length === 0) {
417
- return {
418
- templateName: options.templateName,
419
- targetDirectory: process.cwd(),
420
- generatedInCurrentDirectory: true
421
- };
422
- }
423
-
424
- return {
425
- templateName: options.templateName,
426
- targetDirectory: path.resolve(process.cwd(), args[0]),
427
- generatedInCurrentDirectory: false
428
- };
429
- }
430
-
431
- if (args.length === 0) {
432
- return {
433
- templateName: DEFAULT_TEMPLATE,
434
- targetDirectory: process.cwd(),
435
- generatedInCurrentDirectory: true
436
- };
437
- }
438
-
439
- if (args.length === 1) {
440
- const templateName = resolveTemplate(args[0]);
441
-
442
- if (templateName) {
443
- return {
444
- templateName,
445
- targetDirectory: process.cwd(),
446
- generatedInCurrentDirectory: true
447
- };
448
- }
449
-
450
- return {
451
- templateName: DEFAULT_TEMPLATE,
452
- targetDirectory: path.resolve(process.cwd(), args[0]),
453
- generatedInCurrentDirectory: false
454
- };
455
- }
456
-
457
- if (args.length === 2) {
458
- const templateName = resolveTemplate(args[0]);
459
-
460
- if (!templateName) {
461
- throw new Error(
462
- `Unsupported template "${args[0]}". Supported templates: ${TEMPLATES.map((template) => template.id).join(", ")}.`
463
- );
464
- }
465
-
466
- return {
467
- templateName,
468
- targetDirectory: path.resolve(process.cwd(), args[1]),
469
- generatedInCurrentDirectory: false
470
- };
471
- }
472
-
473
- throw new Error("Too many arguments. Run `create-qa-patterns --help` for usage.");
474
- }
475
-
476
107
  async function resolveScaffoldArgs(args) {
477
- const explicitTemplate = args[0] && resolveTemplate(args[0]);
108
+ const explicitTemplate =
109
+ args[0] && resolveTemplate(TEMPLATE_ALIASES, args[0]);
110
+ const nonInteractiveOptions = {
111
+ defaultTemplate: DEFAULT_TEMPLATE,
112
+ resolveTemplate: (value) => resolveTemplate(TEMPLATE_ALIASES, value),
113
+ supportedTemplateIds: SUPPORTED_TEMPLATE_IDS
114
+ };
478
115
 
479
116
  if (explicitTemplate) {
480
- return resolveNonInteractiveArgs(args);
117
+ return resolveNonInteractiveArgs(args, nonInteractiveOptions);
481
118
  }
482
119
 
483
120
  if (!process.stdin.isTTY || !process.stdout.isTTY) {
484
- return resolveNonInteractiveArgs(args);
121
+ return resolveNonInteractiveArgs(args, nonInteractiveOptions);
485
122
  }
486
123
 
487
- const templateName = await selectTemplateInteractively();
488
- const defaultTarget = args[0] ? args[0] : ".";
489
- const targetAnswer = await askQuestion(`Target directory (${defaultTarget}): `);
124
+ const templateName = await selectTemplateInteractively(TEMPLATES);
125
+ const defaultTarget = args[0] ? args[0] : '.';
126
+ const targetAnswer = await askQuestion(
127
+ `Target directory (${defaultTarget}): `
128
+ );
490
129
  const targetValue = targetAnswer || defaultTarget;
491
130
  const targetDirectory = path.resolve(process.cwd(), targetValue);
492
131
 
@@ -497,256 +136,21 @@ async function resolveScaffoldArgs(args) {
497
136
  };
498
137
  }
499
138
 
500
- function ensureScaffoldTarget(targetDirectory) {
501
- if (!fs.existsSync(targetDirectory)) {
502
- fs.mkdirSync(targetDirectory, { recursive: true });
503
- return;
504
- }
505
-
506
- const entries = fs
507
- .readdirSync(targetDirectory)
508
- .filter((entry) => ![".git", ".DS_Store"].includes(entry));
509
-
510
- if (entries.length > 0) {
511
- throw new Error(`Target directory is not empty: ${targetDirectory}`);
512
- }
513
- }
514
-
515
- function toPackageName(targetDirectory, template) {
516
- const baseName = path.basename(targetDirectory).toLowerCase();
517
- const normalized = baseName
518
- .replace(/[^a-z0-9-_]+/g, "-")
519
- .replace(/^-+|-+$/g, "")
520
- .replace(/-{2,}/g, "-");
521
-
522
- return normalized || template.defaultPackageName || "qa-patterns-template";
523
- }
524
-
525
- function updateJsonFile(filePath, update) {
526
- const current = JSON.parse(fs.readFileSync(filePath, "utf8"));
527
- const next = update(current);
528
- fs.writeFileSync(filePath, `${JSON.stringify(next, null, 2)}\n`, "utf8");
529
- }
530
-
531
- function customizeProject(targetDirectory, template) {
532
- const packageName = toPackageName(targetDirectory, template);
533
- const packageJsonPath = path.join(targetDirectory, "package.json");
534
- const packageLockPath = path.join(targetDirectory, "package-lock.json");
535
- const gitignorePath = path.join(targetDirectory, ".gitignore");
536
-
537
- if (fs.existsSync(packageJsonPath)) {
538
- updateJsonFile(packageJsonPath, (pkg) => ({
539
- ...pkg,
540
- name: packageName
541
- }));
542
- }
543
-
544
- if (fs.existsSync(packageLockPath)) {
545
- updateJsonFile(packageLockPath, (lock) => ({
546
- ...lock,
547
- name: packageName,
548
- packages: lock.packages
549
- ? {
550
- ...lock.packages,
551
- "": {
552
- ...lock.packages[""],
553
- name: packageName
554
- }
555
- }
556
- : lock.packages
557
- }));
558
- }
559
-
560
- if (!fs.existsSync(gitignorePath)) {
561
- fs.writeFileSync(gitignorePath, DEFAULT_GITIGNORE, "utf8");
562
- }
563
- }
564
-
565
- function initializeGitRepository(targetDirectory) {
566
- if (fs.existsSync(path.join(targetDirectory, ".git"))) {
567
- return;
568
- }
569
-
570
- const result = spawnSync(getCommandName("git"), ["init"], {
571
- cwd: targetDirectory,
572
- encoding: "utf8"
573
- });
574
-
575
- if (result.status !== 0) {
576
- throw new Error(result.stderr || "git init failed.");
577
- }
578
- }
139
+ let lastProgressLineLength = 0;
579
140
 
580
141
  function renderProgress(completed, total, label) {
581
142
  const width = 24;
582
143
  const filled = Math.round((completed / total) * width);
583
144
  const empty = width - filled;
584
- const bar = `${"=".repeat(filled)}${" ".repeat(empty)}`;
585
- const percentage = `${Math.round((completed / total) * 100)}`.padStart(3, " ");
586
- process.stdout.write(`\r[${bar}] ${percentage}% ${label}`);
587
- }
588
-
589
- async function scaffoldProject(template, targetDirectory, prerequisites) {
590
- const templateName = template.id;
591
- const templateDirectory = path.resolve(__dirname, "templates", templateName);
592
-
593
- if (!fs.existsSync(templateDirectory)) {
594
- throw new Error(`Template files are missing for "${templateName}".`);
595
- }
596
-
597
- const steps = [
598
- "Validating target directory",
599
- "Copying template files",
600
- "Customizing project files",
601
- "Finalizing scaffold"
602
- ];
603
-
604
- renderProgress(0, steps.length, "Preparing scaffold");
605
- ensureScaffoldTarget(targetDirectory);
606
- await sleep(60);
607
-
608
- renderProgress(1, steps.length, steps[0]);
609
- await sleep(80);
610
-
611
- fs.cpSync(templateDirectory, targetDirectory, { recursive: true });
612
- renderProgress(2, steps.length, steps[1]);
613
- await sleep(80);
614
-
615
- customizeProject(targetDirectory, template);
616
- renderProgress(3, steps.length, steps[2]);
617
- await sleep(80);
618
-
619
- if (prerequisites.git) {
620
- initializeGitRepository(targetDirectory);
621
- }
622
-
623
- renderProgress(4, steps.length, steps[3]);
624
- await sleep(60);
625
- process.stdout.write("\n");
626
- }
627
-
628
- function getCommandName(base) {
629
- if (process.platform === "win32") {
630
- return `${base}.cmd`;
631
- }
632
-
633
- return base;
634
- }
635
-
636
- function printPlaywrightInstallRecovery(targetDirectory) {
637
- process.stdout.write(`
638
- ${colors.yellow("Playwright browser installation did not complete.")}
639
-
640
- Common cause:
641
- Missing OS packages required to run Playwright browsers.
642
-
643
- Recommended next steps:
644
- cd ${path.relative(process.cwd(), targetDirectory) || "."}
645
- sudo npx playwright install-deps
646
- npx playwright install
647
-
648
- If you already know the missing package name, install it with your system package manager and then rerun:
649
- npx playwright install
650
-
651
- The template was generated successfully. You can complete browser setup later.
652
-
653
- `);
654
- }
655
-
656
- function runCommand(command, args, cwd) {
657
- return new Promise((resolve, reject) => {
658
- const child = spawn(getCommandName(command), args, {
659
- cwd,
660
- stdio: "inherit"
661
- });
662
-
663
- child.on("close", (code) => {
664
- if (code === 0) {
665
- resolve();
666
- return;
667
- }
668
-
669
- reject(new Error(`${command} ${args.join(" ")} exited with code ${code}`));
670
- });
671
-
672
- child.on("error", reject);
673
- });
674
- }
675
-
676
- function printSuccess(template, targetDirectory, generatedInCurrentDirectory) {
677
- process.stdout.write(`\n${colors.green(colors.bold("Success"))}
678
- Generated ${template ? template.label : template.id} in ${targetDirectory}
679
- \n`);
680
-
681
- if (!generatedInCurrentDirectory) {
682
- process.stdout.write(`${colors.cyan("Change directory first:")}\n cd ${path.relative(process.cwd(), targetDirectory) || "."}\n\n`);
683
- }
684
- }
685
-
686
- function printNextSteps(summary) {
687
- const steps = [];
688
-
689
- if (!summary.generatedInCurrentDirectory) {
690
- steps.push(`cd ${path.relative(process.cwd(), summary.targetDirectory) || "."}`);
691
- }
692
-
693
- if (summary.npmInstall !== "completed") {
694
- steps.push("npm install");
695
- }
696
-
697
- if (summary.template.setup && summary.extraSetup !== "completed") {
698
- steps.push(summary.template.setup.nextStep);
699
- }
700
-
701
- if (summary.testRun !== "completed") {
702
- steps.push("npm test");
703
- }
704
-
705
- if (steps.length > 0) {
706
- process.stdout.write(`${colors.cyan("Next steps:")}\n`);
707
- for (const step of steps) {
708
- process.stdout.write(` ${step}\n`);
709
- }
710
- process.stdout.write("\n");
711
- }
712
-
713
- if (summary.demoAppsManagedByTemplate) {
714
- process.stdout.write(
715
- `${colors.yellow(colors.bold("Demo apps included:"))} sample tests run against bundled demo apps in local ${colors.bold("dev")}. Delete or replace ${colors.bold("demo-apps/")} if you do not want them.\n`
716
- );
717
- }
718
-
719
- process.stdout.write(`${colors.green(colors.bold("Happy testing."))}\n`);
720
- }
721
-
722
- function formatStatus(status) {
723
- switch (status) {
724
- case "completed":
725
- return colors.green("completed");
726
- case "skipped":
727
- return colors.dim("skipped");
728
- case "unavailable":
729
- return colors.yellow("unavailable");
730
- case "manual-recovery":
731
- return colors.yellow("manual recovery required");
732
- default:
733
- return colors.dim("not run");
734
- }
735
- }
736
-
737
- function printSummary(summary) {
738
- process.stdout.write(`\n${colors.bold("Summary")}\n`);
739
- process.stdout.write(` Template: ${summary.template.id}\n`);
740
- process.stdout.write(` Target: ${summary.targetDirectory}\n`);
741
- process.stdout.write(` Git repository: ${formatStatus(summary.gitInit)}\n`);
742
- process.stdout.write(
743
- ` Demo apps: ${summary.demoAppsManagedByTemplate ? "bundled and auto-started in dev when using default local URLs" : "external application required"}\n`
145
+ const bar = `${'='.repeat(filled)}${' '.repeat(empty)}`;
146
+ const percentage = `${Math.round((completed / total) * 100)}`.padStart(
147
+ 3,
148
+ ' '
744
149
  );
745
- process.stdout.write(` npm install: ${formatStatus(summary.npmInstall)}\n`);
746
- if (summary.template.setup) {
747
- process.stdout.write(` ${summary.template.setup.summaryLabel}: ${formatStatus(summary.extraSetup)}\n`);
748
- }
749
- process.stdout.write(` npm test: ${formatStatus(summary.testRun)}\n`);
150
+ const line = `[${bar}] ${percentage}% ${label}`;
151
+ const paddingLength = Math.max(0, lastProgressLineLength - line.length);
152
+ process.stdout.write(`\r${line}${' '.repeat(paddingLength)}`);
153
+ lastProgressLineLength = line.length;
750
154
  }
751
155
 
752
156
  async function runPostGenerateActions(template, targetDirectory, summary) {
@@ -756,72 +160,171 @@ async function runPostGenerateActions(template, targetDirectory, summary) {
756
160
 
757
161
  if (prerequisites.npm) {
758
162
  if (options.noInstall) {
759
- summary.npmInstall = "skipped";
163
+ summary.npmInstall = 'skipped';
760
164
  } else {
761
- const shouldInstallDependencies = options.yes ? true : canPrompt ? await askYesNo("Run npm install now?", true) : false;
165
+ const shouldInstallDependencies = options.yes
166
+ ? true
167
+ : canPrompt
168
+ ? await askYesNo('Run npm install now?', true)
169
+ : false;
762
170
 
763
171
  if (shouldInstallDependencies) {
764
- await runCommand("npm", ["install"], targetDirectory);
765
- summary.npmInstall = "completed";
172
+ await runCommand('npm', ['install'], targetDirectory);
173
+ summary.npmInstall = 'completed';
766
174
  } else {
767
- summary.npmInstall = canPrompt ? "skipped" : "not-run";
175
+ summary.npmInstall = canPrompt ? 'skipped' : 'not-run';
768
176
  }
769
177
  }
770
178
  } else {
771
- process.stdout.write(`${colors.yellow("Skipping")} npm install prompt because npm is not available.\n`);
772
- summary.npmInstall = "unavailable";
179
+ process.stdout.write(
180
+ `${colors.yellow('Skipping')} npm install prompt because npm is not available.\n`
181
+ );
182
+ summary.npmInstall = 'unavailable';
773
183
  }
774
184
 
775
185
  if (template.setup) {
776
186
  if (options.noSetup) {
777
- summary.extraSetup = "skipped";
187
+ summary.extraSetup = 'skipped';
778
188
  } else if (prerequisites[template.setup.availability]) {
779
- const shouldRunExtraSetup = options.yes ? true : canPrompt ? await askYesNo(template.setup.prompt, true) : false;
189
+ const shouldRunExtraSetup = options.yes
190
+ ? true
191
+ : canPrompt
192
+ ? await askYesNo(template.setup.prompt, true)
193
+ : false;
780
194
 
781
195
  if (shouldRunExtraSetup) {
782
196
  try {
783
197
  await template.setup.run(targetDirectory);
784
- summary.extraSetup = "completed";
198
+ summary.extraSetup = 'completed';
785
199
  } catch (error) {
786
- summary.extraSetup = "manual-recovery";
787
- if (typeof template.setup.recovery === "function") {
200
+ summary.extraSetup = 'manual-recovery';
201
+ if (typeof template.setup.recovery === 'function') {
788
202
  template.setup.recovery(targetDirectory);
789
203
  }
790
204
 
791
- const shouldContinue = await askYesNo("Continue without completing setup?", true);
205
+ const shouldContinue = await askYesNo(
206
+ 'Continue without completing setup?',
207
+ true
208
+ );
792
209
 
793
210
  if (!shouldContinue) {
794
211
  throw error;
795
212
  }
796
213
  }
797
214
  } else {
798
- summary.extraSetup = canPrompt ? "skipped" : "not-run";
215
+ summary.extraSetup = canPrompt ? 'skipped' : 'not-run';
799
216
  }
800
217
  } else {
801
218
  process.stdout.write(
802
- `${colors.yellow("Skipping")} ${template.setup.summaryLabel.toLowerCase()} prompt because ${template.setup.availability} is not available.\n`
219
+ `${colors.yellow('Skipping')} ${template.setup.summaryLabel.toLowerCase()} prompt because ${template.setup.availability} is not available.\n`
803
220
  );
804
- summary.extraSetup = "unavailable";
221
+ summary.extraSetup = 'unavailable';
805
222
  }
806
223
  }
807
224
 
808
225
  if (prerequisites.npm) {
809
226
  if (options.noTest) {
810
- summary.testRun = "skipped";
227
+ summary.testRun = 'skipped';
811
228
  } else {
812
- const shouldRunTests = options.yes ? true : canPrompt ? await askYesNo("Run npm test now?", false) : false;
229
+ const shouldRunTests = options.yes
230
+ ? true
231
+ : canPrompt
232
+ ? await askYesNo('Run npm test now?', false)
233
+ : false;
813
234
 
814
235
  if (shouldRunTests) {
815
- await runCommand("npm", ["test"], targetDirectory);
816
- summary.testRun = "completed";
236
+ await runCommand('npm', ['test'], targetDirectory);
237
+ summary.testRun = 'completed';
817
238
  } else {
818
- summary.testRun = canPrompt ? "skipped" : "not-run";
239
+ summary.testRun = canPrompt ? 'skipped' : 'not-run';
819
240
  }
820
241
  }
821
242
  } else {
822
- process.stdout.write(`${colors.yellow("Skipping")} npm test prompt because npm is not available.\n`);
823
- summary.testRun = "unavailable";
243
+ process.stdout.write(
244
+ `${colors.yellow('Skipping')} npm test prompt because npm is not available.\n`
245
+ );
246
+ summary.testRun = 'unavailable';
247
+ }
248
+ }
249
+
250
+ function resolveUpgradeTarget(args) {
251
+ if (args.length > 1) {
252
+ throw new Error(
253
+ 'Too many arguments for upgrade. Use `create-qa-patterns upgrade check [target-directory]`.'
254
+ );
255
+ }
256
+
257
+ return path.resolve(process.cwd(), args[0] || '.');
258
+ }
259
+
260
+ function runUpgradeCommand(rawArgs) {
261
+ const [subcommand = 'check', ...rest] = rawArgs;
262
+ const metadataOptions = createMetadataOptions();
263
+ const options = parseCliOptions(rest, {
264
+ resolveTemplate: (value) => resolveTemplate(TEMPLATE_ALIASES, value),
265
+ supportedTemplateIds: SUPPORTED_TEMPLATE_IDS
266
+ });
267
+ const targetDirectory = resolveUpgradeTarget(options.positionalArgs);
268
+ const metadata = readProjectMetadata(targetDirectory, METADATA_FILENAME);
269
+ const templateId =
270
+ metadata.template ||
271
+ detectTemplateFromProject(targetDirectory, METADATA_FILENAME);
272
+ const template = getTemplate(TEMPLATES, templateId);
273
+
274
+ if (!template) {
275
+ throw new Error(`Unsupported template "${templateId}".`);
276
+ }
277
+
278
+ const results = analyzeUpgrade(
279
+ template,
280
+ targetDirectory,
281
+ metadata,
282
+ metadataOptions
283
+ );
284
+
285
+ if (subcommand === 'check' || subcommand === 'report') {
286
+ printUpgradeReport(
287
+ targetDirectory,
288
+ metadata,
289
+ results,
290
+ CLI_PACKAGE_VERSION,
291
+ colors
292
+ );
293
+ return;
294
+ }
295
+
296
+ if (subcommand === 'apply') {
297
+ if (!options.safe) {
298
+ throw new Error(
299
+ 'Upgrade apply requires --safe. Only safe managed-file updates are supported.'
300
+ );
301
+ }
302
+
303
+ printUpgradeReport(
304
+ targetDirectory,
305
+ metadata,
306
+ results,
307
+ CLI_PACKAGE_VERSION,
308
+ colors
309
+ );
310
+ const outcome = applySafeUpdates(
311
+ targetDirectory,
312
+ metadata,
313
+ results,
314
+ metadataOptions
315
+ );
316
+ process.stdout.write(`\n${colors.bold('Upgrade apply')}\n`);
317
+ process.stdout.write(` Applied safe updates: ${outcome.appliedCount}\n`);
318
+ process.stdout.write(
319
+ ` Remaining conflicts: ${outcome.remainingConflicts}\n`
320
+ );
321
+ process.stdout.write('\n');
322
+ return;
824
323
  }
324
+
325
+ throw new Error(
326
+ `Unsupported upgrade command "${subcommand}". Use check, report, or apply --safe.`
327
+ );
825
328
  }
826
329
 
827
330
  async function main() {
@@ -829,32 +332,73 @@ async function main() {
829
332
 
830
333
  assertSupportedNodeVersion();
831
334
 
832
- if (rawArgs.includes("--help") || rawArgs.includes("-h")) {
833
- printHelp();
335
+ if (rawArgs[0] === 'upgrade') {
336
+ runUpgradeCommand(rawArgs.slice(1));
834
337
  return;
835
338
  }
836
339
 
837
- const options = parseCliOptions(rawArgs);
340
+ if (rawArgs.includes('--help') || rawArgs.includes('-h')) {
341
+ printHelp(TEMPLATES, colors, DEFAULT_TEMPLATE);
342
+ return;
343
+ }
344
+
345
+ const metadataOptions = createMetadataOptions();
346
+ const options = parseCliOptions(rawArgs, {
347
+ resolveTemplate: (value) => resolveTemplate(TEMPLATE_ALIASES, value),
348
+ supportedTemplateIds: SUPPORTED_TEMPLATE_IDS
349
+ });
838
350
  const args = options.positionalArgs;
839
- const { templateName, targetDirectory, generatedInCurrentDirectory } = options.templateName
840
- ? resolveNonInteractiveArgs(args, options)
841
- : await resolveScaffoldArgs(args);
842
- const template = getTemplate(templateName);
351
+ const { templateName, targetDirectory, generatedInCurrentDirectory } =
352
+ options.templateName
353
+ ? resolveNonInteractiveArgs(args, {
354
+ ...options,
355
+ defaultTemplate: DEFAULT_TEMPLATE,
356
+ resolveTemplate: (value) => resolveTemplate(TEMPLATE_ALIASES, value),
357
+ supportedTemplateIds: SUPPORTED_TEMPLATE_IDS
358
+ })
359
+ : await resolveScaffoldArgs(args);
360
+ const template = getTemplate(TEMPLATES, templateName);
843
361
 
844
362
  if (!template) {
845
363
  throw new Error(`Unsupported template "${templateName}".`);
846
364
  }
847
365
 
848
366
  const prerequisites = collectPrerequisites();
849
- const summary = createSummary(template, targetDirectory, generatedInCurrentDirectory);
367
+ const summary = createSummary(
368
+ template,
369
+ targetDirectory,
370
+ generatedInCurrentDirectory
371
+ );
850
372
  summary.options = options;
851
- printPrerequisiteWarnings(prerequisites);
852
- await scaffoldProject(template, targetDirectory, prerequisites);
853
- summary.gitInit = prerequisites.git ? "completed" : "unavailable";
854
- printSuccess(template, targetDirectory, generatedInCurrentDirectory);
373
+ printPrerequisiteWarnings(prerequisites, colors);
374
+ const localEnv = await scaffoldProject(
375
+ template,
376
+ targetDirectory,
377
+ prerequisites,
378
+ {
379
+ createLocalCredentials,
380
+ defaultGitignore: DEFAULT_GITIGNORE,
381
+ getTemplateDirectory: (templateId) =>
382
+ getTemplateDirectory(__dirname, templateId),
383
+ initializeGitRepository,
384
+ renderProgress,
385
+ toPackageName,
386
+ writeGeneratedLocalEnv
387
+ }
388
+ );
389
+ summary.localCredentials = localEnv.credentials;
390
+ writeProjectMetadata(template, targetDirectory, undefined, metadataOptions);
391
+ summary.gitInit = prerequisites.git ? 'completed' : 'unavailable';
392
+ printSuccess(template, targetDirectory, generatedInCurrentDirectory, colors);
855
393
  await runPostGenerateActions(template, targetDirectory, summary);
856
- printSummary(summary);
857
- printNextSteps(summary);
394
+ writeProjectMetadata(
395
+ template,
396
+ targetDirectory,
397
+ readProjectMetadata(targetDirectory, METADATA_FILENAME),
398
+ metadataOptions
399
+ );
400
+ printSummary(summary, colors);
401
+ printNextSteps(summary, colors);
858
402
  }
859
403
 
860
404
  main().catch((error) => {