@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
@@ -0,0 +1,329 @@
1
+ const crypto = require('node:crypto');
2
+ const fs = require('node:fs');
3
+ const path = require('node:path');
4
+
5
+ function sha256(content) {
6
+ return crypto.createHash('sha256').update(content).digest('hex');
7
+ }
8
+
9
+ function normalizePath(value) {
10
+ return value.split(path.sep).join('/');
11
+ }
12
+
13
+ function pathMatchesPattern(relativePath, pattern) {
14
+ if (pattern.endsWith('/**')) {
15
+ const prefix = pattern.slice(0, -3);
16
+ return relativePath === prefix || relativePath.startsWith(`${prefix}/`);
17
+ }
18
+
19
+ return relativePath === pattern;
20
+ }
21
+
22
+ function collectRelativeFiles(rootDirectory) {
23
+ const results = [];
24
+
25
+ function visit(currentDirectory) {
26
+ const entries = fs.readdirSync(currentDirectory, { withFileTypes: true });
27
+
28
+ for (const entry of entries) {
29
+ const absolutePath = path.join(currentDirectory, entry.name);
30
+ const relativePath = normalizePath(
31
+ path.relative(rootDirectory, absolutePath)
32
+ );
33
+
34
+ if (entry.isDirectory()) {
35
+ visit(absolutePath);
36
+ } else {
37
+ results.push(relativePath);
38
+ }
39
+ }
40
+ }
41
+
42
+ visit(rootDirectory);
43
+
44
+ return results.sort();
45
+ }
46
+
47
+ function isManagedFile(template, relativePath, managedPatterns) {
48
+ const patterns = [
49
+ ...managedPatterns.common,
50
+ ...(managedPatterns[template.id] || [])
51
+ ];
52
+ return patterns.some((pattern) => pathMatchesPattern(relativePath, pattern));
53
+ }
54
+
55
+ function transformTemplateFile(
56
+ relativePath,
57
+ content,
58
+ targetDirectory,
59
+ template,
60
+ toPackageName
61
+ ) {
62
+ const packageName = toPackageName(targetDirectory, template);
63
+
64
+ if (relativePath === 'package.json') {
65
+ const pkg = JSON.parse(content);
66
+ return `${JSON.stringify({ ...pkg, name: packageName }, null, 2)}\n`;
67
+ }
68
+
69
+ if (relativePath === 'package-lock.json') {
70
+ const lock = JSON.parse(content);
71
+ return `${JSON.stringify(
72
+ {
73
+ ...lock,
74
+ name: packageName,
75
+ packages: lock.packages
76
+ ? {
77
+ ...lock.packages,
78
+ '': {
79
+ ...lock.packages[''],
80
+ name: packageName
81
+ }
82
+ }
83
+ : lock.packages
84
+ },
85
+ null,
86
+ 2
87
+ )}\n`;
88
+ }
89
+
90
+ return content;
91
+ }
92
+
93
+ function renderTemplateFile(template, relativePath, targetDirectory, options) {
94
+ const { defaultGitignore, getTemplateDirectory, toPackageName } = options;
95
+
96
+ if (relativePath === '.gitignore') {
97
+ const gitignorePath = path.join(
98
+ getTemplateDirectory(template.id),
99
+ '.gitignore'
100
+ );
101
+ const gitignoreContent = fs.existsSync(gitignorePath)
102
+ ? fs.readFileSync(gitignorePath, 'utf8')
103
+ : defaultGitignore;
104
+ return transformTemplateFile(
105
+ relativePath,
106
+ gitignoreContent,
107
+ targetDirectory,
108
+ template,
109
+ toPackageName
110
+ );
111
+ }
112
+
113
+ const sourcePath = path.join(getTemplateDirectory(template.id), relativePath);
114
+ const content = fs.readFileSync(sourcePath, 'utf8');
115
+ return transformTemplateFile(
116
+ relativePath,
117
+ content,
118
+ targetDirectory,
119
+ template,
120
+ toPackageName
121
+ );
122
+ }
123
+
124
+ function getManagedRelativePaths(template, options) {
125
+ const { getTemplateDirectory, managedPatterns, metadataFilename } = options;
126
+ const templateDirectory = getTemplateDirectory(template.id);
127
+ const templateFiles = collectRelativeFiles(templateDirectory).filter(
128
+ (relativePath) => isManagedFile(template, relativePath, managedPatterns)
129
+ );
130
+ const managedFiles = new Set(templateFiles);
131
+ managedFiles.add('.gitignore');
132
+ managedFiles.delete(metadataFilename);
133
+ return [...managedFiles].sort();
134
+ }
135
+
136
+ function getMetadataPath(targetDirectory, metadataFilename) {
137
+ return path.join(targetDirectory, metadataFilename);
138
+ }
139
+
140
+ function buildProjectMetadata(template, targetDirectory, options) {
141
+ const { cliPackageVersion, metadataFilename } = options;
142
+ const managedFiles = {};
143
+
144
+ for (const relativePath of getManagedRelativePaths(template, options)) {
145
+ const absolutePath = path.join(targetDirectory, relativePath);
146
+ if (!fs.existsSync(absolutePath)) {
147
+ continue;
148
+ }
149
+
150
+ managedFiles[relativePath] = {
151
+ baselineHash: sha256(fs.readFileSync(absolutePath, 'utf8'))
152
+ };
153
+ }
154
+
155
+ return {
156
+ schemaVersion: 1,
157
+ template: template.id,
158
+ templateVersion: cliPackageVersion,
159
+ packageName: options.toPackageName(targetDirectory, template),
160
+ generatedAt: new Date().toISOString(),
161
+ managedFiles,
162
+ metadataFilename
163
+ };
164
+ }
165
+
166
+ function writeProjectMetadata(
167
+ template,
168
+ targetDirectory,
169
+ existingMetadata,
170
+ options
171
+ ) {
172
+ const nextMetadata = buildProjectMetadata(template, targetDirectory, options);
173
+
174
+ if (existingMetadata) {
175
+ nextMetadata.generatedAt =
176
+ existingMetadata.generatedAt || nextMetadata.generatedAt;
177
+ nextMetadata.templateVersion =
178
+ existingMetadata.templateVersion || nextMetadata.templateVersion;
179
+ }
180
+
181
+ fs.writeFileSync(
182
+ getMetadataPath(targetDirectory, options.metadataFilename),
183
+ `${JSON.stringify(nextMetadata, null, 2)}\n`,
184
+ 'utf8'
185
+ );
186
+ return nextMetadata;
187
+ }
188
+
189
+ function readProjectMetadata(targetDirectory, metadataFilename) {
190
+ const metadataPath = getMetadataPath(targetDirectory, metadataFilename);
191
+
192
+ if (!fs.existsSync(metadataPath)) {
193
+ throw new Error(`No ${metadataFilename} file found in ${targetDirectory}.`);
194
+ }
195
+
196
+ return JSON.parse(fs.readFileSync(metadataPath, 'utf8'));
197
+ }
198
+
199
+ function detectTemplateFromProject(targetDirectory, metadataFilename) {
200
+ const metadataPath = getMetadataPath(targetDirectory, metadataFilename);
201
+ if (fs.existsSync(metadataPath)) {
202
+ const metadata = JSON.parse(fs.readFileSync(metadataPath, 'utf8'));
203
+ return metadata.template;
204
+ }
205
+
206
+ if (fs.existsSync(path.join(targetDirectory, 'playwright.config.ts'))) {
207
+ return 'playwright-template';
208
+ }
209
+
210
+ if (fs.existsSync(path.join(targetDirectory, 'cypress.config.ts'))) {
211
+ return 'cypress-template';
212
+ }
213
+
214
+ if (fs.existsSync(path.join(targetDirectory, 'wdio.conf.ts'))) {
215
+ return 'wdio-template';
216
+ }
217
+
218
+ throw new Error(`Could not detect the template used for ${targetDirectory}.`);
219
+ }
220
+
221
+ function analyzeUpgrade(template, targetDirectory, metadata, options) {
222
+ const managedPaths = getManagedRelativePaths(template, options);
223
+ const results = [];
224
+
225
+ for (const relativePath of managedPaths) {
226
+ const absolutePath = path.join(targetDirectory, relativePath);
227
+ const latestContent = renderTemplateFile(
228
+ template,
229
+ relativePath,
230
+ targetDirectory,
231
+ options
232
+ );
233
+ const latestHash = sha256(latestContent);
234
+ const baselineHash =
235
+ metadata.managedFiles?.[relativePath]?.baselineHash || null;
236
+ const currentExists = fs.existsSync(absolutePath);
237
+ const currentHash = currentExists
238
+ ? sha256(fs.readFileSync(absolutePath, 'utf8'))
239
+ : null;
240
+
241
+ let status = 'up-to-date';
242
+
243
+ if (!baselineHash) {
244
+ if (!currentExists) {
245
+ status = 'new-file';
246
+ } else if (currentHash === latestHash) {
247
+ status = 'new-file';
248
+ } else {
249
+ status = 'conflict';
250
+ }
251
+ } else if (!currentExists) {
252
+ status = 'conflict';
253
+ } else if (currentHash === latestHash) {
254
+ status = 'up-to-date';
255
+ } else if (currentHash === baselineHash) {
256
+ status = 'safe-update';
257
+ } else {
258
+ status = 'conflict';
259
+ }
260
+
261
+ results.push({
262
+ relativePath,
263
+ status,
264
+ latestContent,
265
+ latestHash,
266
+ currentHash,
267
+ baselineHash,
268
+ currentExists
269
+ });
270
+ }
271
+
272
+ return results;
273
+ }
274
+
275
+ function applySafeUpdates(targetDirectory, metadata, results, options) {
276
+ const nextMetadata = {
277
+ ...metadata,
278
+ managedFiles: {
279
+ ...metadata.managedFiles
280
+ }
281
+ };
282
+
283
+ let appliedCount = 0;
284
+
285
+ for (const entry of results) {
286
+ if (!['safe-update', 'new-file'].includes(entry.status)) {
287
+ continue;
288
+ }
289
+
290
+ const absolutePath = path.join(targetDirectory, entry.relativePath);
291
+ fs.mkdirSync(path.dirname(absolutePath), { recursive: true });
292
+ fs.writeFileSync(absolutePath, entry.latestContent, 'utf8');
293
+ nextMetadata.managedFiles[entry.relativePath] = {
294
+ baselineHash: entry.latestHash
295
+ };
296
+ appliedCount += 1;
297
+ }
298
+
299
+ const remainingConflicts = results.filter(
300
+ (entry) => entry.status === 'conflict'
301
+ ).length;
302
+ if (remainingConflicts === 0) {
303
+ nextMetadata.templateVersion = options.cliPackageVersion;
304
+ }
305
+
306
+ fs.writeFileSync(
307
+ getMetadataPath(targetDirectory, options.metadataFilename),
308
+ `${JSON.stringify(nextMetadata, null, 2)}\n`,
309
+ 'utf8'
310
+ );
311
+
312
+ return {
313
+ appliedCount,
314
+ remainingConflicts
315
+ };
316
+ }
317
+
318
+ module.exports = {
319
+ analyzeUpgrade,
320
+ applySafeUpdates,
321
+ buildProjectMetadata,
322
+ collectRelativeFiles,
323
+ detectTemplateFromProject,
324
+ getManagedRelativePaths,
325
+ readProjectMetadata,
326
+ renderTemplateFile,
327
+ sha256,
328
+ writeProjectMetadata
329
+ };
package/lib/output.js ADDED
@@ -0,0 +1,326 @@
1
+ const { MIN_NODE_VERSION } = require('./constants');
2
+
3
+ function createColors(processObject = process) {
4
+ const colorEnabled =
5
+ Boolean(processObject.stdout?.isTTY) && !('NO_COLOR' in processObject.env);
6
+
7
+ function style(text, ...codes) {
8
+ if (!colorEnabled) {
9
+ return text;
10
+ }
11
+
12
+ return `\u001b[${codes.join(';')}m${text}\u001b[0m`;
13
+ }
14
+
15
+ return {
16
+ bold(text) {
17
+ return style(text, 1);
18
+ },
19
+ dim(text) {
20
+ return style(text, 2);
21
+ },
22
+ cyan(text) {
23
+ return style(text, 36);
24
+ },
25
+ green(text) {
26
+ return style(text, 32);
27
+ },
28
+ yellow(text) {
29
+ return style(text, 33);
30
+ },
31
+ red(text) {
32
+ return style(text, 31);
33
+ }
34
+ };
35
+ }
36
+
37
+ function parseNodeVersion(version) {
38
+ const normalized = version.replace(/^v/, '');
39
+ const [major = '0', minor = '0', patch = '0'] = normalized.split('.');
40
+
41
+ return {
42
+ major: Number.parseInt(major, 10),
43
+ minor: Number.parseInt(minor, 10),
44
+ patch: Number.parseInt(patch, 10)
45
+ };
46
+ }
47
+
48
+ function isNodeVersionSupported(version) {
49
+ if (version.major !== MIN_NODE_VERSION.major) {
50
+ return version.major > MIN_NODE_VERSION.major;
51
+ }
52
+
53
+ if (version.minor !== MIN_NODE_VERSION.minor) {
54
+ return version.minor > MIN_NODE_VERSION.minor;
55
+ }
56
+
57
+ return version.patch >= MIN_NODE_VERSION.patch;
58
+ }
59
+
60
+ function assertSupportedNodeVersion(processVersion = process.version) {
61
+ const currentVersion = parseNodeVersion(processVersion);
62
+
63
+ if (!isNodeVersionSupported(currentVersion)) {
64
+ throw new Error(
65
+ `Node ${MIN_NODE_VERSION.major}.${MIN_NODE_VERSION.minor}.${MIN_NODE_VERSION.patch}+ is required. Current version: ${processVersion}`
66
+ );
67
+ }
68
+ }
69
+
70
+ function printHelp(templates, colors, defaultTemplate) {
71
+ const supportedTemplates = templates
72
+ .map(
73
+ (template) =>
74
+ ` ${template.id}${template.aliases.length > 0 ? ` (${template.aliases.join(', ')})` : ''}`
75
+ )
76
+ .join('\n');
77
+
78
+ process.stdout.write(`${colors.bold('create-qa-patterns')}
79
+
80
+ Usage:
81
+ create-qa-patterns
82
+ create-qa-patterns <target-directory>
83
+ create-qa-patterns <template> [target-directory]
84
+ create-qa-patterns --template <template> [target-directory]
85
+ create-qa-patterns upgrade check [target-directory]
86
+ create-qa-patterns upgrade apply --safe [target-directory]
87
+
88
+ Options:
89
+ --yes Accept all post-generate prompts
90
+ --no-install Skip npm install
91
+ --no-setup Skip template-specific setup such as Playwright browser install
92
+ --no-test Skip npm test
93
+ --template Explicitly choose a template without using positional arguments
94
+ --safe Required with upgrade apply; only updates unchanged managed files
95
+
96
+ Interactive mode:
97
+ When run without an explicit template, the CLI shows an interactive template picker.
98
+ Default template in non-interactive mode: ${defaultTemplate}
99
+
100
+ Supported templates:
101
+ ${supportedTemplates}
102
+ `);
103
+ }
104
+
105
+ function printPrerequisiteWarnings(prerequisites, colors) {
106
+ if (!prerequisites.npm) {
107
+ process.stdout.write(
108
+ `${colors.yellow('Warning:')} npm was not found. Automated install and test steps will be unavailable.\n`
109
+ );
110
+ }
111
+
112
+ if (!prerequisites.npx) {
113
+ process.stdout.write(
114
+ `${colors.yellow('Warning:')} npx was not found. Template setup steps that depend on npx will be unavailable.\n`
115
+ );
116
+ }
117
+
118
+ if (!prerequisites.docker) {
119
+ process.stdout.write(
120
+ `${colors.yellow('Warning:')} docker was not found. Docker-based template flows will not run until Docker is installed.\n`
121
+ );
122
+ }
123
+
124
+ if (!prerequisites.git) {
125
+ process.stdout.write(
126
+ `${colors.yellow('Warning:')} git was not found. The generated project cannot be initialized as a repository automatically.\n`
127
+ );
128
+ }
129
+
130
+ if (
131
+ !prerequisites.npm ||
132
+ !prerequisites.npx ||
133
+ !prerequisites.docker ||
134
+ !prerequisites.git
135
+ ) {
136
+ process.stdout.write('\n');
137
+ }
138
+ }
139
+
140
+ function printPlaywrightInstallRecovery(targetDirectory, colors) {
141
+ process.stdout.write(`
142
+ ${colors.yellow('Playwright browser installation did not complete.')}
143
+
144
+ Common cause:
145
+ Missing OS packages required to run Playwright browsers.
146
+
147
+ Recommended next steps:
148
+ cd ${targetDirectory}
149
+ sudo npx playwright install-deps
150
+ npx playwright install
151
+
152
+ If you already know the missing package name, install it with your system package manager and then rerun:
153
+ npx playwright install
154
+
155
+ The template was generated successfully. You can complete browser setup later.
156
+
157
+ `);
158
+ }
159
+
160
+ function printSuccess(
161
+ template,
162
+ targetDirectory,
163
+ generatedInCurrentDirectory,
164
+ colors
165
+ ) {
166
+ process.stdout.write(`\n${colors.green(colors.bold('Success'))}
167
+ Generated ${template.label} in ${targetDirectory}
168
+ \n`);
169
+
170
+ if (!generatedInCurrentDirectory) {
171
+ process.stdout.write(
172
+ `${colors.cyan('Your shell stays in the original directory. To work in the generated project, run:')}\n cd ${targetDirectory}\n\n`
173
+ );
174
+ }
175
+ }
176
+
177
+ function formatStatus(status, colors) {
178
+ switch (status) {
179
+ case 'completed':
180
+ return colors.green('completed');
181
+ case 'skipped':
182
+ return colors.dim('skipped');
183
+ case 'unavailable':
184
+ return colors.yellow('unavailable');
185
+ case 'manual-recovery':
186
+ return colors.yellow('manual recovery required');
187
+ default:
188
+ return colors.dim('not run');
189
+ }
190
+ }
191
+
192
+ function formatUpgradeStatus(status, colors) {
193
+ switch (status) {
194
+ case 'safe-update':
195
+ return colors.green('safe update available');
196
+ case 'new-file':
197
+ return colors.green('new managed file available');
198
+ case 'conflict':
199
+ return colors.yellow('manual review required');
200
+ default:
201
+ return colors.dim('up to date');
202
+ }
203
+ }
204
+
205
+ function printUpgradeReport(
206
+ targetDirectory,
207
+ metadata,
208
+ results,
209
+ cliPackageVersion,
210
+ colors
211
+ ) {
212
+ const safeCount = results.filter(
213
+ (entry) => entry.status === 'safe-update'
214
+ ).length;
215
+ const newCount = results.filter(
216
+ (entry) => entry.status === 'new-file'
217
+ ).length;
218
+ const conflictCount = results.filter(
219
+ (entry) => entry.status === 'conflict'
220
+ ).length;
221
+
222
+ process.stdout.write(`\n${colors.bold('Upgrade check')}\n`);
223
+ process.stdout.write(` Target: ${targetDirectory}\n`);
224
+ process.stdout.write(` Template: ${metadata.template}\n`);
225
+ process.stdout.write(
226
+ ` Current baseline version: ${metadata.templateVersion}\n`
227
+ );
228
+ process.stdout.write(` CLI template version: ${cliPackageVersion}\n`);
229
+ process.stdout.write(` Safe updates: ${safeCount}\n`);
230
+ process.stdout.write(` New managed files: ${newCount}\n`);
231
+ process.stdout.write(` Conflicts: ${conflictCount}\n\n`);
232
+
233
+ for (const entry of results) {
234
+ if (entry.status === 'up-to-date') {
235
+ continue;
236
+ }
237
+
238
+ process.stdout.write(
239
+ ` ${entry.relativePath}: ${formatUpgradeStatus(entry.status, colors)}\n`
240
+ );
241
+ }
242
+
243
+ if (safeCount === 0 && newCount === 0 && conflictCount === 0) {
244
+ process.stdout.write(
245
+ `${colors.green('Everything already matches the current managed template files.')}\n`
246
+ );
247
+ }
248
+
249
+ process.stdout.write('\n');
250
+ }
251
+
252
+ function printSummary(summary, colors) {
253
+ process.stdout.write(`\n${colors.bold('Summary')}\n`);
254
+ process.stdout.write(` Template: ${summary.template.id}\n`);
255
+ process.stdout.write(` Target: ${summary.targetDirectory}\n`);
256
+ process.stdout.write(
257
+ ` Git repository: ${formatStatus(summary.gitInit, colors)}\n`
258
+ );
259
+ process.stdout.write(
260
+ ` Demo apps: ${summary.demoAppsManagedByTemplate ? 'bundled and auto-started in dev when using default local URLs' : 'external application required'}\n`
261
+ );
262
+ if (summary.localCredentials) {
263
+ process.stdout.write(
264
+ ` Local credentials: ${summary.localCredentials.username} / ${summary.localCredentials.password}\n`
265
+ );
266
+ }
267
+ process.stdout.write(
268
+ ` npm install: ${formatStatus(summary.npmInstall, colors)}\n`
269
+ );
270
+ if (summary.template.setup) {
271
+ process.stdout.write(
272
+ ` ${summary.template.setup.summaryLabel}: ${formatStatus(summary.extraSetup, colors)}\n`
273
+ );
274
+ }
275
+ process.stdout.write(
276
+ ` npm test: ${formatStatus(summary.testRun, colors)}\n`
277
+ );
278
+ }
279
+
280
+ function printNextSteps(summary, colors) {
281
+ const steps = [];
282
+
283
+ if (!summary.generatedInCurrentDirectory) {
284
+ steps.push(`cd ${summary.targetRelativePath}`);
285
+ }
286
+
287
+ if (summary.npmInstall !== 'completed') {
288
+ steps.push('npm install');
289
+ }
290
+
291
+ if (summary.template.setup && summary.extraSetup !== 'completed') {
292
+ steps.push(summary.template.setup.nextStep);
293
+ }
294
+
295
+ if (summary.testRun !== 'completed') {
296
+ steps.push('npm test');
297
+ }
298
+
299
+ if (steps.length > 0) {
300
+ process.stdout.write(`${colors.cyan('Next steps:')}\n`);
301
+ for (const step of steps) {
302
+ process.stdout.write(` ${step}\n`);
303
+ }
304
+ process.stdout.write('\n');
305
+ }
306
+
307
+ if (summary.demoAppsManagedByTemplate) {
308
+ process.stdout.write(
309
+ `${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`
310
+ );
311
+ }
312
+
313
+ process.stdout.write(`${colors.green(colors.bold('Happy testing.'))}\n`);
314
+ }
315
+
316
+ module.exports = {
317
+ assertSupportedNodeVersion,
318
+ createColors,
319
+ printHelp,
320
+ printNextSteps,
321
+ printPlaywrightInstallRecovery,
322
+ printPrerequisiteWarnings,
323
+ printSuccess,
324
+ printSummary,
325
+ printUpgradeReport
326
+ };
package/lib/prereqs.js ADDED
@@ -0,0 +1,72 @@
1
+ const { spawn, spawnSync } = require('node:child_process');
2
+ const path = require('node:path');
3
+
4
+ function getCommandName(base) {
5
+ if (process.platform === 'win32') {
6
+ return `${base}.cmd`;
7
+ }
8
+
9
+ return base;
10
+ }
11
+
12
+ function commandExists(command) {
13
+ const result = spawnSync(getCommandName(command), ['--version'], {
14
+ stdio: 'ignore'
15
+ });
16
+
17
+ return !result.error && result.status === 0;
18
+ }
19
+
20
+ function collectPrerequisites() {
21
+ return {
22
+ npm: commandExists('npm'),
23
+ npx: commandExists('npx'),
24
+ docker: commandExists('docker'),
25
+ git: commandExists('git')
26
+ };
27
+ }
28
+
29
+ function initializeGitRepository(targetDirectory) {
30
+ if (require('node:fs').existsSync(path.join(targetDirectory, '.git'))) {
31
+ return;
32
+ }
33
+
34
+ const result = spawnSync(getCommandName('git'), ['init'], {
35
+ cwd: targetDirectory,
36
+ encoding: 'utf8'
37
+ });
38
+
39
+ if (result.status !== 0) {
40
+ throw new Error(result.stderr || 'git init failed.');
41
+ }
42
+ }
43
+
44
+ function runCommand(command, args, cwd) {
45
+ return new Promise((resolve, reject) => {
46
+ const child = spawn(getCommandName(command), args, {
47
+ cwd,
48
+ stdio: 'inherit'
49
+ });
50
+
51
+ child.on('close', (code) => {
52
+ if (code === 0) {
53
+ resolve();
54
+ return;
55
+ }
56
+
57
+ reject(
58
+ new Error(`${command} ${args.join(' ')} exited with code ${code}`)
59
+ );
60
+ });
61
+
62
+ child.on('error', reject);
63
+ });
64
+ }
65
+
66
+ module.exports = {
67
+ collectPrerequisites,
68
+ commandExists,
69
+ getCommandName,
70
+ initializeGitRepository,
71
+ runCommand
72
+ };