@toolstackhq/create-qa-patterns 1.0.13 → 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.
- package/README.md +18 -0
- package/index.js +386 -18
- package/package.json +1 -1
- package/templates/cypress-template/README.md +19 -0
- package/templates/playwright-template/README.md +19 -0
package/README.md
CHANGED
|
@@ -40,6 +40,24 @@ Generate without post-create prompts, which is useful for CI or scripted setup:
|
|
|
40
40
|
create-qa-patterns playwright-template my-project --yes --no-install --no-setup --no-test
|
|
41
41
|
```
|
|
42
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
|
+
|
|
43
61
|
## Supported templates
|
|
44
62
|
|
|
45
63
|
- `playwright-template`
|
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
|
|
|
@@ -119,6 +362,8 @@ Usage:
|
|
|
119
362
|
create-qa-patterns <target-directory>
|
|
120
363
|
create-qa-patterns <template> [target-directory]
|
|
121
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]
|
|
122
367
|
|
|
123
368
|
Options:
|
|
124
369
|
--yes Accept all post-generate prompts
|
|
@@ -126,6 +371,7 @@ Options:
|
|
|
126
371
|
--no-setup Skip template-specific setup such as Playwright browser install
|
|
127
372
|
--no-test Skip npm test
|
|
128
373
|
--template Explicitly choose a template without using positional arguments
|
|
374
|
+
--safe Required with upgrade apply; only updates unchanged managed files
|
|
129
375
|
|
|
130
376
|
Interactive mode:
|
|
131
377
|
When run without an explicit template, the CLI shows an interactive template picker.
|
|
@@ -141,6 +387,7 @@ function parseCliOptions(args) {
|
|
|
141
387
|
noInstall: false,
|
|
142
388
|
noSetup: false,
|
|
143
389
|
noTest: false,
|
|
390
|
+
safe: false,
|
|
144
391
|
templateName: null,
|
|
145
392
|
positionalArgs: []
|
|
146
393
|
};
|
|
@@ -161,6 +408,9 @@ function parseCliOptions(args) {
|
|
|
161
408
|
case "--no-test":
|
|
162
409
|
options.noTest = true;
|
|
163
410
|
break;
|
|
411
|
+
case "--safe":
|
|
412
|
+
options.safe = true;
|
|
413
|
+
break;
|
|
164
414
|
case "--template": {
|
|
165
415
|
const templateValue = args[index + 1];
|
|
166
416
|
if (!templateValue) {
|
|
@@ -529,32 +779,24 @@ function updateJsonFile(filePath, update) {
|
|
|
529
779
|
}
|
|
530
780
|
|
|
531
781
|
function customizeProject(targetDirectory, template) {
|
|
532
|
-
const packageName = toPackageName(targetDirectory, template);
|
|
533
782
|
const packageJsonPath = path.join(targetDirectory, "package.json");
|
|
534
783
|
const packageLockPath = path.join(targetDirectory, "package-lock.json");
|
|
535
784
|
const gitignorePath = path.join(targetDirectory, ".gitignore");
|
|
536
785
|
|
|
537
786
|
if (fs.existsSync(packageJsonPath)) {
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
787
|
+
fs.writeFileSync(
|
|
788
|
+
packageJsonPath,
|
|
789
|
+
transformTemplateFile("package.json", fs.readFileSync(packageJsonPath, "utf8"), targetDirectory, template),
|
|
790
|
+
"utf8"
|
|
791
|
+
);
|
|
542
792
|
}
|
|
543
793
|
|
|
544
794
|
if (fs.existsSync(packageLockPath)) {
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
...lock.packages,
|
|
551
|
-
"": {
|
|
552
|
-
...lock.packages[""],
|
|
553
|
-
name: packageName
|
|
554
|
-
}
|
|
555
|
-
}
|
|
556
|
-
: lock.packages
|
|
557
|
-
}));
|
|
795
|
+
fs.writeFileSync(
|
|
796
|
+
packageLockPath,
|
|
797
|
+
transformTemplateFile("package-lock.json", fs.readFileSync(packageLockPath, "utf8"), targetDirectory, template),
|
|
798
|
+
"utf8"
|
|
799
|
+
);
|
|
558
800
|
}
|
|
559
801
|
|
|
560
802
|
if (!fs.existsSync(gitignorePath)) {
|
|
@@ -734,6 +976,85 @@ function formatStatus(status) {
|
|
|
734
976
|
}
|
|
735
977
|
}
|
|
736
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
|
+
|
|
737
1058
|
function printSummary(summary) {
|
|
738
1059
|
process.stdout.write(`\n${colors.bold("Summary")}\n`);
|
|
739
1060
|
process.stdout.write(` Template: ${summary.template.id}\n`);
|
|
@@ -824,11 +1145,56 @@ async function runPostGenerateActions(template, targetDirectory, summary) {
|
|
|
824
1145
|
}
|
|
825
1146
|
}
|
|
826
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
|
+
|
|
827
1188
|
async function main() {
|
|
828
1189
|
const rawArgs = process.argv.slice(2);
|
|
829
1190
|
|
|
830
1191
|
assertSupportedNodeVersion();
|
|
831
1192
|
|
|
1193
|
+
if (rawArgs[0] === "upgrade") {
|
|
1194
|
+
runUpgradeCommand(rawArgs.slice(1));
|
|
1195
|
+
return;
|
|
1196
|
+
}
|
|
1197
|
+
|
|
832
1198
|
if (rawArgs.includes("--help") || rawArgs.includes("-h")) {
|
|
833
1199
|
printHelp();
|
|
834
1200
|
return;
|
|
@@ -850,9 +1216,11 @@ async function main() {
|
|
|
850
1216
|
summary.options = options;
|
|
851
1217
|
printPrerequisiteWarnings(prerequisites);
|
|
852
1218
|
await scaffoldProject(template, targetDirectory, prerequisites);
|
|
1219
|
+
writeProjectMetadata(template, targetDirectory);
|
|
853
1220
|
summary.gitInit = prerequisites.git ? "completed" : "unavailable";
|
|
854
1221
|
printSuccess(template, targetDirectory, generatedInCurrentDirectory);
|
|
855
1222
|
await runPostGenerateActions(template, targetDirectory, summary);
|
|
1223
|
+
writeProjectMetadata(template, targetDirectory, readProjectMetadata(targetDirectory));
|
|
856
1224
|
printSummary(summary);
|
|
857
1225
|
printNextSteps(summary);
|
|
858
1226
|
}
|
package/package.json
CHANGED
|
@@ -13,6 +13,7 @@ This is a Cypress + TypeScript automation framework template for a small determi
|
|
|
13
13
|
- [Reports and artifacts](#reports-and-artifacts)
|
|
14
14
|
- [Add a new test](#add-a-new-test)
|
|
15
15
|
- [Extend the framework](#extend-the-framework)
|
|
16
|
+
- [Template upgrades](#template-upgrades)
|
|
16
17
|
- [CI](#ci)
|
|
17
18
|
|
|
18
19
|
## Feature set
|
|
@@ -197,6 +198,24 @@ Recommended rules:
|
|
|
197
198
|
- use Cypress commands for workflows, not giant helper classes
|
|
198
199
|
- keep the data layer generic until the project really needs domain-specific factories
|
|
199
200
|
|
|
201
|
+
## Template upgrades
|
|
202
|
+
|
|
203
|
+
This project includes a `.qa-patterns.json` metadata file so future CLI versions can compare the current project against the managed template baseline.
|
|
204
|
+
|
|
205
|
+
Check for available safe updates:
|
|
206
|
+
|
|
207
|
+
```bash
|
|
208
|
+
npx -y @toolstackhq/create-qa-patterns upgrade check .
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
Apply only safe managed-file updates:
|
|
212
|
+
|
|
213
|
+
```bash
|
|
214
|
+
npx -y @toolstackhq/create-qa-patterns upgrade apply --safe .
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
The upgrade flow is conservative. It updates framework infrastructure such as config, scripts, workflows, and package metadata when those files are still unchanged from the generated baseline. If you changed a managed file yourself, the CLI reports a conflict instead of overwriting it.
|
|
218
|
+
|
|
200
219
|
## CI
|
|
201
220
|
|
|
202
221
|
The included workflow lives at:
|
|
@@ -13,6 +13,7 @@ This is a Playwright + TypeScript automation framework template for UI and API t
|
|
|
13
13
|
- [Reports and artifacts](#reports-and-artifacts)
|
|
14
14
|
- [Add a new test](#add-a-new-test)
|
|
15
15
|
- [Extend the framework](#extend-the-framework)
|
|
16
|
+
- [Template upgrades](#template-upgrades)
|
|
16
17
|
- [CI and Docker](#ci-and-docker)
|
|
17
18
|
|
|
18
19
|
## Feature set
|
|
@@ -228,6 +229,24 @@ Recommended rules:
|
|
|
228
229
|
- prefer semantic selectors such as `getByRole`, `getByLabel`, and `data-testid`
|
|
229
230
|
- keep the data layer generic until the project really needs domain-specific factories
|
|
230
231
|
|
|
232
|
+
## Template upgrades
|
|
233
|
+
|
|
234
|
+
This project includes a `.qa-patterns.json` metadata file so future CLI versions can compare the current project against the managed template baseline.
|
|
235
|
+
|
|
236
|
+
Check for available safe updates:
|
|
237
|
+
|
|
238
|
+
```bash
|
|
239
|
+
npx -y @toolstackhq/create-qa-patterns upgrade check .
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
Apply only safe managed-file updates:
|
|
243
|
+
|
|
244
|
+
```bash
|
|
245
|
+
npx -y @toolstackhq/create-qa-patterns upgrade apply --safe .
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
The upgrade flow is conservative. It updates framework infrastructure such as config, scripts, workflows, and package metadata when those files are still unchanged from the generated baseline. If you changed a managed file yourself, the CLI reports a conflict instead of overwriting it.
|
|
249
|
+
|
|
231
250
|
## CI and Docker
|
|
232
251
|
|
|
233
252
|
The CI entrypoint is:
|