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