@toolstackhq/create-qa-patterns 1.0.14 → 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.
- package/README.md +5 -0
- package/index.js +252 -1076
- package/lib/args.js +139 -0
- package/lib/constants.js +115 -0
- package/lib/interactive.js +131 -0
- package/lib/local-env.js +65 -0
- package/lib/metadata.js +329 -0
- package/lib/output.js +326 -0
- package/lib/prereqs.js +72 -0
- package/lib/scaffold.js +120 -0
- package/lib/templates.js +40 -0
- package/package.json +5 -3
- package/templates/cypress-template/.env.example +2 -2
- package/templates/cypress-template/.github/workflows/cypress-tests.yml +2 -2
- package/templates/cypress-template/README.md +10 -6
- package/templates/cypress-template/allurerc.mjs +1 -1
- package/templates/cypress-template/config/environments.ts +13 -11
- package/templates/cypress-template/config/runtime-config.ts +17 -12
- package/templates/cypress-template/config/secret-manager.ts +1 -1
- package/templates/cypress-template/config/test-env.ts +3 -3
- package/templates/cypress-template/cypress/e2e/ui-journey.cy.ts +12 -10
- package/templates/cypress-template/cypress/support/app-config.ts +5 -5
- package/templates/cypress-template/cypress/support/commands.ts +7 -7
- package/templates/cypress-template/cypress/support/data/data-factory.ts +6 -4
- package/templates/cypress-template/cypress/support/data/id-generator.ts +1 -1
- package/templates/cypress-template/cypress/support/data/seeded-faker.ts +2 -2
- package/templates/cypress-template/cypress/support/e2e.ts +2 -2
- package/templates/cypress-template/cypress/support/pages/login-page.ts +4 -4
- package/templates/cypress-template/cypress/support/pages/people-page.ts +10 -10
- package/templates/cypress-template/cypress.config.ts +9 -9
- package/templates/cypress-template/demo-apps/ui-demo-app/public/styles.css +1 -1
- package/templates/cypress-template/demo-apps/ui-demo-app/src/server.js +44 -41
- package/templates/cypress-template/demo-apps/ui-demo-app/src/store.js +31 -3
- package/templates/cypress-template/demo-apps/ui-demo-app/src/templates.js +5 -5
- package/templates/cypress-template/eslint.config.mjs +53 -45
- package/templates/cypress-template/package.json +6 -5
- package/templates/cypress-template/scripts/ensure-local-env.mjs +36 -0
- package/templates/cypress-template/scripts/generate-allure-report.mjs +16 -10
- package/templates/cypress-template/scripts/run-cypress.mjs +33 -24
- package/templates/cypress-template/scripts/run-tests.sh +1 -0
- package/templates/cypress-template/tsconfig.json +7 -1
- package/templates/playwright-template/.env.example +6 -6
- package/templates/playwright-template/.github/workflows/playwright-tests.yml +14 -5
- package/templates/playwright-template/README.md +6 -5
- package/templates/playwright-template/allurerc.mjs +1 -1
- package/templates/playwright-template/components/flash-message.ts +2 -2
- package/templates/playwright-template/config/environments.ts +16 -14
- package/templates/playwright-template/config/runtime-config.ts +17 -12
- package/templates/playwright-template/config/secret-manager.ts +1 -1
- package/templates/playwright-template/config/test-env.ts +3 -3
- package/templates/playwright-template/data/factories/data-factory.ts +6 -4
- package/templates/playwright-template/data/generators/id-generator.ts +1 -1
- package/templates/playwright-template/data/generators/seeded-faker.ts +2 -2
- package/templates/playwright-template/demo-apps/api-demo-server/src/server.js +9 -9
- package/templates/playwright-template/demo-apps/api-demo-server/src/store.js +1 -1
- package/templates/playwright-template/demo-apps/ui-demo-app/public/styles.css +1 -1
- package/templates/playwright-template/demo-apps/ui-demo-app/src/server.js +44 -41
- package/templates/playwright-template/demo-apps/ui-demo-app/src/store.js +31 -3
- package/templates/playwright-template/demo-apps/ui-demo-app/src/templates.js +5 -5
- package/templates/playwright-template/eslint.config.mjs +40 -40
- package/templates/playwright-template/fixtures/test-fixtures.ts +27 -12
- package/templates/playwright-template/lint/architecture-plugin.cjs +36 -31
- package/templates/playwright-template/package.json +7 -6
- package/templates/playwright-template/pages/base-page.ts +4 -4
- package/templates/playwright-template/pages/login-page.ts +9 -9
- package/templates/playwright-template/pages/people-page.ts +21 -17
- package/templates/playwright-template/playwright.config.ts +22 -19
- package/templates/playwright-template/reporters/structured-reporter.ts +11 -8
- package/templates/playwright-template/scripts/ensure-local-env.mjs +37 -0
- package/templates/playwright-template/scripts/generate-allure-report.mjs +16 -10
- package/templates/playwright-template/scripts/run-tests.sh +1 -0
- package/templates/playwright-template/tests/api-people.spec.ts +8 -6
- package/templates/playwright-template/tests/ui-journey.spec.ts +13 -8
- package/templates/playwright-template/tsconfig.json +3 -11
- package/templates/playwright-template/utils/logger.ts +12 -8
- package/templates/playwright-template/utils/test-step.ts +5 -5
- package/templates/wdio-template/.env.example +14 -0
- package/templates/wdio-template/.github/workflows/wdio-tests.yml +46 -0
- package/templates/wdio-template/README.md +241 -0
- package/templates/wdio-template/allurerc.mjs +10 -0
- package/templates/wdio-template/components/README.md +5 -0
- package/templates/wdio-template/components/flash-message.ts +16 -0
- package/templates/wdio-template/config/README.md +5 -0
- package/templates/wdio-template/config/environments.ts +40 -0
- package/templates/wdio-template/config/runtime-config.ts +53 -0
- package/templates/wdio-template/config/secret-manager.ts +29 -0
- package/templates/wdio-template/config/test-env.ts +9 -0
- package/templates/wdio-template/data/README.md +9 -0
- package/templates/wdio-template/data/factories/README.md +6 -0
- package/templates/wdio-template/data/factories/data-factory.ts +36 -0
- package/templates/wdio-template/data/generators/README.md +5 -0
- package/templates/wdio-template/data/generators/id-generator.ts +18 -0
- package/templates/wdio-template/data/generators/seeded-faker.ts +14 -0
- package/templates/wdio-template/demo-apps/ui-demo-app/public/styles.css +120 -0
- package/templates/wdio-template/demo-apps/ui-demo-app/src/server.js +152 -0
- package/templates/wdio-template/demo-apps/ui-demo-app/src/store.js +71 -0
- package/templates/wdio-template/demo-apps/ui-demo-app/src/templates.js +121 -0
- package/templates/wdio-template/eslint.config.mjs +86 -0
- package/templates/wdio-template/lint/architecture-plugin.cjs +123 -0
- package/templates/wdio-template/package-lock.json +11058 -0
- package/templates/wdio-template/package.json +44 -0
- package/templates/wdio-template/pages/README.md +6 -0
- package/templates/wdio-template/pages/base-page.ts +15 -0
- package/templates/wdio-template/pages/login-page.ts +27 -0
- package/templates/wdio-template/pages/people-page.ts +54 -0
- package/templates/wdio-template/reporters/README.md +5 -0
- package/templates/wdio-template/reporters/structured-reporter.ts +78 -0
- package/templates/wdio-template/scripts/README.md +5 -0
- package/templates/wdio-template/scripts/ensure-local-env.mjs +36 -0
- package/templates/wdio-template/scripts/generate-allure-report.mjs +72 -0
- package/templates/wdio-template/scripts/run-tests.sh +7 -0
- package/templates/wdio-template/scripts/run-wdio.mjs +114 -0
- package/templates/wdio-template/tests/README.md +7 -0
- package/templates/wdio-template/tests/ui-journey.spec.ts +52 -0
- package/templates/wdio-template/tsconfig.json +22 -0
- package/templates/wdio-template/utils/README.md +5 -0
- package/templates/wdio-template/utils/logger.ts +60 -0
- package/templates/wdio-template/utils/test-step.ts +20 -0
- package/templates/wdio-template/wdio.conf.ts +58 -0
- package/tests/args.test.js +58 -0
- package/tests/local-env.test.js +70 -0
- package/tests/metadata.test.js +147 -0
- package/tests/templates.test.js +44 -0
package/index.js
CHANGED
|
@@ -1,742 +1,131 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
const fs = require(
|
|
4
|
-
const path = require(
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
};
|
|
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
|
+
}
|
|
71
62
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
id: DEFAULT_TEMPLATE,
|
|
75
|
-
aliases: ["playwright", "pw"],
|
|
76
|
-
label: "Playwright Template",
|
|
77
|
-
description: "TypeScript starter with page objects, fixtures, multi-environment config, reporting, linting, CI and Docker.",
|
|
78
|
-
defaultPackageName: "playwright-template",
|
|
79
|
-
demoAppsManagedByTemplate: true,
|
|
63
|
+
return {
|
|
64
|
+
...template,
|
|
80
65
|
setup: {
|
|
81
|
-
|
|
82
|
-
prompt: "Run npx playwright install now?",
|
|
83
|
-
summaryLabel: "Playwright browser install",
|
|
84
|
-
nextStep: "npx playwright install",
|
|
66
|
+
...template.setup,
|
|
85
67
|
run(targetDirectory) {
|
|
86
|
-
return runCommand(
|
|
68
|
+
return runCommand('npx', ['playwright', 'install'], targetDirectory);
|
|
87
69
|
},
|
|
88
70
|
recovery(targetDirectory) {
|
|
89
|
-
printPlaywrightInstallRecovery(targetDirectory);
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
},
|
|
93
|
-
{
|
|
94
|
-
id: "cypress-template",
|
|
95
|
-
aliases: ["cypress", "cy"],
|
|
96
|
-
label: "Cypress Template",
|
|
97
|
-
description: "TypeScript starter with Cypress e2e specs, custom commands, page modules, env-based config, CI, and a bundled demo app.",
|
|
98
|
-
defaultPackageName: "cypress-template",
|
|
99
|
-
demoAppsManagedByTemplate: true
|
|
100
|
-
}
|
|
101
|
-
];
|
|
102
|
-
|
|
103
|
-
const TEMPLATE_ALIASES = new Map(
|
|
104
|
-
TEMPLATES.flatMap((template) => [
|
|
105
|
-
[template.id, template.id],
|
|
106
|
-
...template.aliases.map((alias) => [alias, template.id])
|
|
107
|
-
])
|
|
108
|
-
);
|
|
109
|
-
|
|
110
|
-
function style(text, ...codes) {
|
|
111
|
-
if (!COLOR_ENABLED) {
|
|
112
|
-
return text;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
return `\u001b[${codes.join(";")}m${text}\u001b[0m`;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
const colors = {
|
|
119
|
-
bold(text) {
|
|
120
|
-
return style(text, 1);
|
|
121
|
-
},
|
|
122
|
-
dim(text) {
|
|
123
|
-
return style(text, 2);
|
|
124
|
-
},
|
|
125
|
-
cyan(text) {
|
|
126
|
-
return style(text, 36);
|
|
127
|
-
},
|
|
128
|
-
green(text) {
|
|
129
|
-
return style(text, 32);
|
|
130
|
-
},
|
|
131
|
-
yellow(text) {
|
|
132
|
-
return style(text, 33);
|
|
133
|
-
},
|
|
134
|
-
red(text) {
|
|
135
|
-
return style(text, 31);
|
|
136
|
-
}
|
|
137
|
-
};
|
|
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);
|
|
71
|
+
printPlaywrightInstallRecovery(targetDirectory, colors);
|
|
179
72
|
}
|
|
180
73
|
}
|
|
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
74
|
};
|
|
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
|
-
|
|
355
|
-
function printHelp() {
|
|
356
|
-
const supportedTemplates = TEMPLATES.map((template) => ` ${template.id}${template.aliases.length > 0 ? ` (${template.aliases.join(", ")})` : ""}`).join("\n");
|
|
357
|
-
|
|
358
|
-
process.stdout.write(`${colors.bold("create-qa-patterns")}
|
|
359
|
-
|
|
360
|
-
Usage:
|
|
361
|
-
create-qa-patterns
|
|
362
|
-
create-qa-patterns <target-directory>
|
|
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
|
|
375
|
-
|
|
376
|
-
Interactive mode:
|
|
377
|
-
When run without an explicit template, the CLI shows an interactive template picker.
|
|
378
|
-
|
|
379
|
-
Supported templates:
|
|
380
|
-
${supportedTemplates}
|
|
381
|
-
`);
|
|
382
|
-
}
|
|
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
|
-
}
|
|
75
|
+
});
|
|
439
76
|
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
const [major = "0", minor = "0", patch = "0"] = normalized.split(".");
|
|
77
|
+
const TEMPLATE_ALIASES = createTemplateAliases(TEMPLATES);
|
|
78
|
+
const SUPPORTED_TEMPLATE_IDS = TEMPLATES.map((template) => template.id);
|
|
443
79
|
|
|
80
|
+
function createMetadataOptions() {
|
|
444
81
|
return {
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
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
|
|
448
89
|
};
|
|
449
90
|
}
|
|
450
91
|
|
|
451
|
-
function isNodeVersionSupported(version) {
|
|
452
|
-
if (version.major !== MIN_NODE_VERSION.major) {
|
|
453
|
-
return version.major > MIN_NODE_VERSION.major;
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
if (version.minor !== MIN_NODE_VERSION.minor) {
|
|
457
|
-
return version.minor > MIN_NODE_VERSION.minor;
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
return version.patch >= MIN_NODE_VERSION.patch;
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
function assertSupportedNodeVersion() {
|
|
464
|
-
const currentVersion = parseNodeVersion(process.version);
|
|
465
|
-
|
|
466
|
-
if (!isNodeVersionSupported(currentVersion)) {
|
|
467
|
-
throw new Error(
|
|
468
|
-
`Node ${MIN_NODE_VERSION.major}.${MIN_NODE_VERSION.minor}.${MIN_NODE_VERSION.patch}+ is required. Current version: ${process.version}`
|
|
469
|
-
);
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
function resolveTemplate(value) {
|
|
474
|
-
return TEMPLATE_ALIASES.get(value);
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
function getTemplate(templateId) {
|
|
478
|
-
return TEMPLATES.find((template) => template.id === templateId);
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
function sleep(ms) {
|
|
482
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
function commandExists(command) {
|
|
486
|
-
const result = spawnSync(getCommandName(command), ["--version"], {
|
|
487
|
-
stdio: "ignore"
|
|
488
|
-
});
|
|
489
|
-
|
|
490
|
-
return !result.error && result.status === 0;
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
function collectPrerequisites() {
|
|
494
|
-
return {
|
|
495
|
-
npm: commandExists("npm"),
|
|
496
|
-
npx: commandExists("npx"),
|
|
497
|
-
docker: commandExists("docker"),
|
|
498
|
-
git: commandExists("git")
|
|
499
|
-
};
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
function printPrerequisiteWarnings(prerequisites) {
|
|
503
|
-
if (!prerequisites.npm) {
|
|
504
|
-
process.stdout.write(`${colors.yellow("Warning:")} npm was not found. Automated install and test steps will be unavailable.\n`);
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
if (!prerequisites.npx) {
|
|
508
|
-
process.stdout.write(`${colors.yellow("Warning:")} npx was not found. Template setup steps that depend on npx will be unavailable.\n`);
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
if (!prerequisites.docker) {
|
|
512
|
-
process.stdout.write(`${colors.yellow("Warning:")} docker was not found. Docker-based template flows will not run until Docker is installed.\n`);
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
if (!prerequisites.git) {
|
|
516
|
-
process.stdout.write(`${colors.yellow("Warning:")} git was not found. The generated project cannot be initialized as a repository automatically.\n`);
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
if (!prerequisites.npm || !prerequisites.npx || !prerequisites.docker || !prerequisites.git) {
|
|
520
|
-
process.stdout.write("\n");
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
function createLineInterface() {
|
|
525
|
-
return readline.createInterface({
|
|
526
|
-
input: process.stdin,
|
|
527
|
-
output: process.stdout
|
|
528
|
-
});
|
|
529
|
-
}
|
|
530
|
-
|
|
531
92
|
function createSummary(template, targetDirectory, generatedInCurrentDirectory) {
|
|
532
93
|
return {
|
|
533
94
|
template,
|
|
534
95
|
targetDirectory,
|
|
96
|
+
targetRelativePath: path.relative(process.cwd(), targetDirectory) || '.',
|
|
535
97
|
generatedInCurrentDirectory,
|
|
536
98
|
demoAppsManagedByTemplate: Boolean(template.demoAppsManagedByTemplate),
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
99
|
+
localCredentials: null,
|
|
100
|
+
gitInit: 'not-run',
|
|
101
|
+
npmInstall: 'not-run',
|
|
102
|
+
extraSetup: template.setup ? 'not-run' : null,
|
|
103
|
+
testRun: 'not-run'
|
|
541
104
|
};
|
|
542
105
|
}
|
|
543
106
|
|
|
544
|
-
function askQuestion(prompt) {
|
|
545
|
-
const lineInterface = createLineInterface();
|
|
546
|
-
|
|
547
|
-
return new Promise((resolve) => {
|
|
548
|
-
lineInterface.question(prompt, (answer) => {
|
|
549
|
-
lineInterface.close();
|
|
550
|
-
resolve(answer.trim());
|
|
551
|
-
});
|
|
552
|
-
});
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
async function askYesNo(prompt, defaultValue = true) {
|
|
556
|
-
const suffix = defaultValue ? " [Y/n] " : " [y/N] ";
|
|
557
|
-
|
|
558
|
-
while (true) {
|
|
559
|
-
const answer = (await askQuestion(`${prompt}${suffix}`)).toLowerCase();
|
|
560
|
-
|
|
561
|
-
if (!answer) {
|
|
562
|
-
return defaultValue;
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
if (["y", "yes"].includes(answer)) {
|
|
566
|
-
return true;
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
if (["n", "no"].includes(answer)) {
|
|
570
|
-
return false;
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
process.stdout.write("Please answer yes or no.\n");
|
|
574
|
-
}
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
async function selectTemplateInteractively() {
|
|
578
|
-
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
579
|
-
return DEFAULT_TEMPLATE;
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
readline.emitKeypressEvents(process.stdin);
|
|
583
|
-
|
|
584
|
-
if (typeof process.stdin.setRawMode === "function") {
|
|
585
|
-
process.stdin.setRawMode(true);
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
let selectedIndex = 0;
|
|
589
|
-
let renderedLines = 0;
|
|
590
|
-
|
|
591
|
-
const render = () => {
|
|
592
|
-
if (renderedLines > 0) {
|
|
593
|
-
readline.moveCursor(process.stdout, 0, -renderedLines);
|
|
594
|
-
readline.clearScreenDown(process.stdout);
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
const lines = [
|
|
598
|
-
"Select a template",
|
|
599
|
-
"Use ↑/↓ to choose and press Enter to continue.",
|
|
600
|
-
""
|
|
601
|
-
];
|
|
602
|
-
|
|
603
|
-
for (let index = 0; index < TEMPLATES.length; index += 1) {
|
|
604
|
-
const template = TEMPLATES[index];
|
|
605
|
-
const marker = index === selectedIndex ? ">" : " ";
|
|
606
|
-
lines.push(`${marker} ${template.label}`);
|
|
607
|
-
lines.push(` ${template.description}`);
|
|
608
|
-
lines.push("");
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
renderedLines = lines.length;
|
|
612
|
-
process.stdout.write(`${lines.join("\n")}\n`);
|
|
613
|
-
};
|
|
614
|
-
|
|
615
|
-
render();
|
|
616
|
-
|
|
617
|
-
return new Promise((resolve) => {
|
|
618
|
-
const handleKeypress = (_, key) => {
|
|
619
|
-
if (!key) {
|
|
620
|
-
return;
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
if (key.name === "up") {
|
|
624
|
-
selectedIndex = (selectedIndex - 1 + TEMPLATES.length) % TEMPLATES.length;
|
|
625
|
-
render();
|
|
626
|
-
return;
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
if (key.name === "down") {
|
|
630
|
-
selectedIndex = (selectedIndex + 1) % TEMPLATES.length;
|
|
631
|
-
render();
|
|
632
|
-
return;
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
if (key.name === "return") {
|
|
636
|
-
process.stdin.off("keypress", handleKeypress);
|
|
637
|
-
if (typeof process.stdin.setRawMode === "function") {
|
|
638
|
-
process.stdin.setRawMode(false);
|
|
639
|
-
}
|
|
640
|
-
readline.clearScreenDown(process.stdout);
|
|
641
|
-
process.stdout.write(`Selected: ${TEMPLATES[selectedIndex].label}\n\n`);
|
|
642
|
-
resolve(TEMPLATES[selectedIndex].id);
|
|
643
|
-
return;
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
if (key.ctrl && key.name === "c") {
|
|
647
|
-
process.stdin.off("keypress", handleKeypress);
|
|
648
|
-
if (typeof process.stdin.setRawMode === "function") {
|
|
649
|
-
process.stdin.setRawMode(false);
|
|
650
|
-
}
|
|
651
|
-
process.stdout.write("\n");
|
|
652
|
-
process.exit(1);
|
|
653
|
-
}
|
|
654
|
-
};
|
|
655
|
-
|
|
656
|
-
process.stdin.on("keypress", handleKeypress);
|
|
657
|
-
});
|
|
658
|
-
}
|
|
659
|
-
|
|
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
|
-
|
|
681
|
-
if (args.length === 0) {
|
|
682
|
-
return {
|
|
683
|
-
templateName: DEFAULT_TEMPLATE,
|
|
684
|
-
targetDirectory: process.cwd(),
|
|
685
|
-
generatedInCurrentDirectory: true
|
|
686
|
-
};
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
if (args.length === 1) {
|
|
690
|
-
const templateName = resolveTemplate(args[0]);
|
|
691
|
-
|
|
692
|
-
if (templateName) {
|
|
693
|
-
return {
|
|
694
|
-
templateName,
|
|
695
|
-
targetDirectory: process.cwd(),
|
|
696
|
-
generatedInCurrentDirectory: true
|
|
697
|
-
};
|
|
698
|
-
}
|
|
699
|
-
|
|
700
|
-
return {
|
|
701
|
-
templateName: DEFAULT_TEMPLATE,
|
|
702
|
-
targetDirectory: path.resolve(process.cwd(), args[0]),
|
|
703
|
-
generatedInCurrentDirectory: false
|
|
704
|
-
};
|
|
705
|
-
}
|
|
706
|
-
|
|
707
|
-
if (args.length === 2) {
|
|
708
|
-
const templateName = resolveTemplate(args[0]);
|
|
709
|
-
|
|
710
|
-
if (!templateName) {
|
|
711
|
-
throw new Error(
|
|
712
|
-
`Unsupported template "${args[0]}". Supported templates: ${TEMPLATES.map((template) => template.id).join(", ")}.`
|
|
713
|
-
);
|
|
714
|
-
}
|
|
715
|
-
|
|
716
|
-
return {
|
|
717
|
-
templateName,
|
|
718
|
-
targetDirectory: path.resolve(process.cwd(), args[1]),
|
|
719
|
-
generatedInCurrentDirectory: false
|
|
720
|
-
};
|
|
721
|
-
}
|
|
722
|
-
|
|
723
|
-
throw new Error("Too many arguments. Run `create-qa-patterns --help` for usage.");
|
|
724
|
-
}
|
|
725
|
-
|
|
726
107
|
async function resolveScaffoldArgs(args) {
|
|
727
|
-
const explicitTemplate =
|
|
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
|
+
};
|
|
728
115
|
|
|
729
116
|
if (explicitTemplate) {
|
|
730
|
-
return resolveNonInteractiveArgs(args);
|
|
117
|
+
return resolveNonInteractiveArgs(args, nonInteractiveOptions);
|
|
731
118
|
}
|
|
732
119
|
|
|
733
120
|
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
734
|
-
return resolveNonInteractiveArgs(args);
|
|
121
|
+
return resolveNonInteractiveArgs(args, nonInteractiveOptions);
|
|
735
122
|
}
|
|
736
123
|
|
|
737
|
-
const templateName = await selectTemplateInteractively();
|
|
738
|
-
const defaultTarget = args[0] ? args[0] :
|
|
739
|
-
const targetAnswer = await askQuestion(
|
|
124
|
+
const templateName = await selectTemplateInteractively(TEMPLATES);
|
|
125
|
+
const defaultTarget = args[0] ? args[0] : '.';
|
|
126
|
+
const targetAnswer = await askQuestion(
|
|
127
|
+
`Target directory (${defaultTarget}): `
|
|
128
|
+
);
|
|
740
129
|
const targetValue = targetAnswer || defaultTarget;
|
|
741
130
|
const targetDirectory = path.resolve(process.cwd(), targetValue);
|
|
742
131
|
|
|
@@ -747,327 +136,21 @@ async function resolveScaffoldArgs(args) {
|
|
|
747
136
|
};
|
|
748
137
|
}
|
|
749
138
|
|
|
750
|
-
|
|
751
|
-
if (!fs.existsSync(targetDirectory)) {
|
|
752
|
-
fs.mkdirSync(targetDirectory, { recursive: true });
|
|
753
|
-
return;
|
|
754
|
-
}
|
|
755
|
-
|
|
756
|
-
const entries = fs
|
|
757
|
-
.readdirSync(targetDirectory)
|
|
758
|
-
.filter((entry) => ![".git", ".DS_Store"].includes(entry));
|
|
759
|
-
|
|
760
|
-
if (entries.length > 0) {
|
|
761
|
-
throw new Error(`Target directory is not empty: ${targetDirectory}`);
|
|
762
|
-
}
|
|
763
|
-
}
|
|
764
|
-
|
|
765
|
-
function toPackageName(targetDirectory, template) {
|
|
766
|
-
const baseName = path.basename(targetDirectory).toLowerCase();
|
|
767
|
-
const normalized = baseName
|
|
768
|
-
.replace(/[^a-z0-9-_]+/g, "-")
|
|
769
|
-
.replace(/^-+|-+$/g, "")
|
|
770
|
-
.replace(/-{2,}/g, "-");
|
|
771
|
-
|
|
772
|
-
return normalized || template.defaultPackageName || "qa-patterns-template";
|
|
773
|
-
}
|
|
774
|
-
|
|
775
|
-
function updateJsonFile(filePath, update) {
|
|
776
|
-
const current = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
777
|
-
const next = update(current);
|
|
778
|
-
fs.writeFileSync(filePath, `${JSON.stringify(next, null, 2)}\n`, "utf8");
|
|
779
|
-
}
|
|
780
|
-
|
|
781
|
-
function customizeProject(targetDirectory, template) {
|
|
782
|
-
const packageJsonPath = path.join(targetDirectory, "package.json");
|
|
783
|
-
const packageLockPath = path.join(targetDirectory, "package-lock.json");
|
|
784
|
-
const gitignorePath = path.join(targetDirectory, ".gitignore");
|
|
785
|
-
|
|
786
|
-
if (fs.existsSync(packageJsonPath)) {
|
|
787
|
-
fs.writeFileSync(
|
|
788
|
-
packageJsonPath,
|
|
789
|
-
transformTemplateFile("package.json", fs.readFileSync(packageJsonPath, "utf8"), targetDirectory, template),
|
|
790
|
-
"utf8"
|
|
791
|
-
);
|
|
792
|
-
}
|
|
793
|
-
|
|
794
|
-
if (fs.existsSync(packageLockPath)) {
|
|
795
|
-
fs.writeFileSync(
|
|
796
|
-
packageLockPath,
|
|
797
|
-
transformTemplateFile("package-lock.json", fs.readFileSync(packageLockPath, "utf8"), targetDirectory, template),
|
|
798
|
-
"utf8"
|
|
799
|
-
);
|
|
800
|
-
}
|
|
801
|
-
|
|
802
|
-
if (!fs.existsSync(gitignorePath)) {
|
|
803
|
-
fs.writeFileSync(gitignorePath, DEFAULT_GITIGNORE, "utf8");
|
|
804
|
-
}
|
|
805
|
-
}
|
|
806
|
-
|
|
807
|
-
function initializeGitRepository(targetDirectory) {
|
|
808
|
-
if (fs.existsSync(path.join(targetDirectory, ".git"))) {
|
|
809
|
-
return;
|
|
810
|
-
}
|
|
811
|
-
|
|
812
|
-
const result = spawnSync(getCommandName("git"), ["init"], {
|
|
813
|
-
cwd: targetDirectory,
|
|
814
|
-
encoding: "utf8"
|
|
815
|
-
});
|
|
816
|
-
|
|
817
|
-
if (result.status !== 0) {
|
|
818
|
-
throw new Error(result.stderr || "git init failed.");
|
|
819
|
-
}
|
|
820
|
-
}
|
|
139
|
+
let lastProgressLineLength = 0;
|
|
821
140
|
|
|
822
141
|
function renderProgress(completed, total, label) {
|
|
823
142
|
const width = 24;
|
|
824
143
|
const filled = Math.round((completed / total) * width);
|
|
825
144
|
const empty = width - filled;
|
|
826
|
-
const bar = `${
|
|
827
|
-
const percentage = `${Math.round((completed / total) * 100)}`.padStart(
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
async function scaffoldProject(template, targetDirectory, prerequisites) {
|
|
832
|
-
const templateName = template.id;
|
|
833
|
-
const templateDirectory = path.resolve(__dirname, "templates", templateName);
|
|
834
|
-
|
|
835
|
-
if (!fs.existsSync(templateDirectory)) {
|
|
836
|
-
throw new Error(`Template files are missing for "${templateName}".`);
|
|
837
|
-
}
|
|
838
|
-
|
|
839
|
-
const steps = [
|
|
840
|
-
"Validating target directory",
|
|
841
|
-
"Copying template files",
|
|
842
|
-
"Customizing project files",
|
|
843
|
-
"Finalizing scaffold"
|
|
844
|
-
];
|
|
845
|
-
|
|
846
|
-
renderProgress(0, steps.length, "Preparing scaffold");
|
|
847
|
-
ensureScaffoldTarget(targetDirectory);
|
|
848
|
-
await sleep(60);
|
|
849
|
-
|
|
850
|
-
renderProgress(1, steps.length, steps[0]);
|
|
851
|
-
await sleep(80);
|
|
852
|
-
|
|
853
|
-
fs.cpSync(templateDirectory, targetDirectory, { recursive: true });
|
|
854
|
-
renderProgress(2, steps.length, steps[1]);
|
|
855
|
-
await sleep(80);
|
|
856
|
-
|
|
857
|
-
customizeProject(targetDirectory, template);
|
|
858
|
-
renderProgress(3, steps.length, steps[2]);
|
|
859
|
-
await sleep(80);
|
|
860
|
-
|
|
861
|
-
if (prerequisites.git) {
|
|
862
|
-
initializeGitRepository(targetDirectory);
|
|
863
|
-
}
|
|
864
|
-
|
|
865
|
-
renderProgress(4, steps.length, steps[3]);
|
|
866
|
-
await sleep(60);
|
|
867
|
-
process.stdout.write("\n");
|
|
868
|
-
}
|
|
869
|
-
|
|
870
|
-
function getCommandName(base) {
|
|
871
|
-
if (process.platform === "win32") {
|
|
872
|
-
return `${base}.cmd`;
|
|
873
|
-
}
|
|
874
|
-
|
|
875
|
-
return base;
|
|
876
|
-
}
|
|
877
|
-
|
|
878
|
-
function printPlaywrightInstallRecovery(targetDirectory) {
|
|
879
|
-
process.stdout.write(`
|
|
880
|
-
${colors.yellow("Playwright browser installation did not complete.")}
|
|
881
|
-
|
|
882
|
-
Common cause:
|
|
883
|
-
Missing OS packages required to run Playwright browsers.
|
|
884
|
-
|
|
885
|
-
Recommended next steps:
|
|
886
|
-
cd ${path.relative(process.cwd(), targetDirectory) || "."}
|
|
887
|
-
sudo npx playwright install-deps
|
|
888
|
-
npx playwright install
|
|
889
|
-
|
|
890
|
-
If you already know the missing package name, install it with your system package manager and then rerun:
|
|
891
|
-
npx playwright install
|
|
892
|
-
|
|
893
|
-
The template was generated successfully. You can complete browser setup later.
|
|
894
|
-
|
|
895
|
-
`);
|
|
896
|
-
}
|
|
897
|
-
|
|
898
|
-
function runCommand(command, args, cwd) {
|
|
899
|
-
return new Promise((resolve, reject) => {
|
|
900
|
-
const child = spawn(getCommandName(command), args, {
|
|
901
|
-
cwd,
|
|
902
|
-
stdio: "inherit"
|
|
903
|
-
});
|
|
904
|
-
|
|
905
|
-
child.on("close", (code) => {
|
|
906
|
-
if (code === 0) {
|
|
907
|
-
resolve();
|
|
908
|
-
return;
|
|
909
|
-
}
|
|
910
|
-
|
|
911
|
-
reject(new Error(`${command} ${args.join(" ")} exited with code ${code}`));
|
|
912
|
-
});
|
|
913
|
-
|
|
914
|
-
child.on("error", reject);
|
|
915
|
-
});
|
|
916
|
-
}
|
|
917
|
-
|
|
918
|
-
function printSuccess(template, targetDirectory, generatedInCurrentDirectory) {
|
|
919
|
-
process.stdout.write(`\n${colors.green(colors.bold("Success"))}
|
|
920
|
-
Generated ${template ? template.label : template.id} in ${targetDirectory}
|
|
921
|
-
\n`);
|
|
922
|
-
|
|
923
|
-
if (!generatedInCurrentDirectory) {
|
|
924
|
-
process.stdout.write(`${colors.cyan("Change directory first:")}\n cd ${path.relative(process.cwd(), targetDirectory) || "."}\n\n`);
|
|
925
|
-
}
|
|
926
|
-
}
|
|
927
|
-
|
|
928
|
-
function printNextSteps(summary) {
|
|
929
|
-
const steps = [];
|
|
930
|
-
|
|
931
|
-
if (!summary.generatedInCurrentDirectory) {
|
|
932
|
-
steps.push(`cd ${path.relative(process.cwd(), summary.targetDirectory) || "."}`);
|
|
933
|
-
}
|
|
934
|
-
|
|
935
|
-
if (summary.npmInstall !== "completed") {
|
|
936
|
-
steps.push("npm install");
|
|
937
|
-
}
|
|
938
|
-
|
|
939
|
-
if (summary.template.setup && summary.extraSetup !== "completed") {
|
|
940
|
-
steps.push(summary.template.setup.nextStep);
|
|
941
|
-
}
|
|
942
|
-
|
|
943
|
-
if (summary.testRun !== "completed") {
|
|
944
|
-
steps.push("npm test");
|
|
945
|
-
}
|
|
946
|
-
|
|
947
|
-
if (steps.length > 0) {
|
|
948
|
-
process.stdout.write(`${colors.cyan("Next steps:")}\n`);
|
|
949
|
-
for (const step of steps) {
|
|
950
|
-
process.stdout.write(` ${step}\n`);
|
|
951
|
-
}
|
|
952
|
-
process.stdout.write("\n");
|
|
953
|
-
}
|
|
954
|
-
|
|
955
|
-
if (summary.demoAppsManagedByTemplate) {
|
|
956
|
-
process.stdout.write(
|
|
957
|
-
`${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`
|
|
958
|
-
);
|
|
959
|
-
}
|
|
960
|
-
|
|
961
|
-
process.stdout.write(`${colors.green(colors.bold("Happy testing."))}\n`);
|
|
962
|
-
}
|
|
963
|
-
|
|
964
|
-
function formatStatus(status) {
|
|
965
|
-
switch (status) {
|
|
966
|
-
case "completed":
|
|
967
|
-
return colors.green("completed");
|
|
968
|
-
case "skipped":
|
|
969
|
-
return colors.dim("skipped");
|
|
970
|
-
case "unavailable":
|
|
971
|
-
return colors.yellow("unavailable");
|
|
972
|
-
case "manual-recovery":
|
|
973
|
-
return colors.yellow("manual recovery required");
|
|
974
|
-
default:
|
|
975
|
-
return colors.dim("not run");
|
|
976
|
-
}
|
|
977
|
-
}
|
|
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
|
-
|
|
1058
|
-
function printSummary(summary) {
|
|
1059
|
-
process.stdout.write(`\n${colors.bold("Summary")}\n`);
|
|
1060
|
-
process.stdout.write(` Template: ${summary.template.id}\n`);
|
|
1061
|
-
process.stdout.write(` Target: ${summary.targetDirectory}\n`);
|
|
1062
|
-
process.stdout.write(` Git repository: ${formatStatus(summary.gitInit)}\n`);
|
|
1063
|
-
process.stdout.write(
|
|
1064
|
-
` 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
|
+
' '
|
|
1065
149
|
);
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
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;
|
|
1071
154
|
}
|
|
1072
155
|
|
|
1073
156
|
async function runPostGenerateActions(template, targetDirectory, summary) {
|
|
@@ -1077,112 +160,171 @@ async function runPostGenerateActions(template, targetDirectory, summary) {
|
|
|
1077
160
|
|
|
1078
161
|
if (prerequisites.npm) {
|
|
1079
162
|
if (options.noInstall) {
|
|
1080
|
-
summary.npmInstall =
|
|
163
|
+
summary.npmInstall = 'skipped';
|
|
1081
164
|
} else {
|
|
1082
|
-
const shouldInstallDependencies = options.yes
|
|
165
|
+
const shouldInstallDependencies = options.yes
|
|
166
|
+
? true
|
|
167
|
+
: canPrompt
|
|
168
|
+
? await askYesNo('Run npm install now?', true)
|
|
169
|
+
: false;
|
|
1083
170
|
|
|
1084
171
|
if (shouldInstallDependencies) {
|
|
1085
|
-
await runCommand(
|
|
1086
|
-
summary.npmInstall =
|
|
172
|
+
await runCommand('npm', ['install'], targetDirectory);
|
|
173
|
+
summary.npmInstall = 'completed';
|
|
1087
174
|
} else {
|
|
1088
|
-
summary.npmInstall = canPrompt ?
|
|
175
|
+
summary.npmInstall = canPrompt ? 'skipped' : 'not-run';
|
|
1089
176
|
}
|
|
1090
177
|
}
|
|
1091
178
|
} else {
|
|
1092
|
-
process.stdout.write(
|
|
1093
|
-
|
|
179
|
+
process.stdout.write(
|
|
180
|
+
`${colors.yellow('Skipping')} npm install prompt because npm is not available.\n`
|
|
181
|
+
);
|
|
182
|
+
summary.npmInstall = 'unavailable';
|
|
1094
183
|
}
|
|
1095
184
|
|
|
1096
185
|
if (template.setup) {
|
|
1097
186
|
if (options.noSetup) {
|
|
1098
|
-
summary.extraSetup =
|
|
187
|
+
summary.extraSetup = 'skipped';
|
|
1099
188
|
} else if (prerequisites[template.setup.availability]) {
|
|
1100
|
-
const shouldRunExtraSetup = options.yes
|
|
189
|
+
const shouldRunExtraSetup = options.yes
|
|
190
|
+
? true
|
|
191
|
+
: canPrompt
|
|
192
|
+
? await askYesNo(template.setup.prompt, true)
|
|
193
|
+
: false;
|
|
1101
194
|
|
|
1102
195
|
if (shouldRunExtraSetup) {
|
|
1103
196
|
try {
|
|
1104
197
|
await template.setup.run(targetDirectory);
|
|
1105
|
-
summary.extraSetup =
|
|
198
|
+
summary.extraSetup = 'completed';
|
|
1106
199
|
} catch (error) {
|
|
1107
|
-
summary.extraSetup =
|
|
1108
|
-
if (typeof template.setup.recovery ===
|
|
200
|
+
summary.extraSetup = 'manual-recovery';
|
|
201
|
+
if (typeof template.setup.recovery === 'function') {
|
|
1109
202
|
template.setup.recovery(targetDirectory);
|
|
1110
203
|
}
|
|
1111
204
|
|
|
1112
|
-
const shouldContinue = await askYesNo(
|
|
205
|
+
const shouldContinue = await askYesNo(
|
|
206
|
+
'Continue without completing setup?',
|
|
207
|
+
true
|
|
208
|
+
);
|
|
1113
209
|
|
|
1114
210
|
if (!shouldContinue) {
|
|
1115
211
|
throw error;
|
|
1116
212
|
}
|
|
1117
213
|
}
|
|
1118
214
|
} else {
|
|
1119
|
-
summary.extraSetup = canPrompt ?
|
|
215
|
+
summary.extraSetup = canPrompt ? 'skipped' : 'not-run';
|
|
1120
216
|
}
|
|
1121
217
|
} else {
|
|
1122
218
|
process.stdout.write(
|
|
1123
|
-
`${colors.yellow(
|
|
219
|
+
`${colors.yellow('Skipping')} ${template.setup.summaryLabel.toLowerCase()} prompt because ${template.setup.availability} is not available.\n`
|
|
1124
220
|
);
|
|
1125
|
-
summary.extraSetup =
|
|
221
|
+
summary.extraSetup = 'unavailable';
|
|
1126
222
|
}
|
|
1127
223
|
}
|
|
1128
224
|
|
|
1129
225
|
if (prerequisites.npm) {
|
|
1130
226
|
if (options.noTest) {
|
|
1131
|
-
summary.testRun =
|
|
227
|
+
summary.testRun = 'skipped';
|
|
1132
228
|
} else {
|
|
1133
|
-
const shouldRunTests = options.yes
|
|
229
|
+
const shouldRunTests = options.yes
|
|
230
|
+
? true
|
|
231
|
+
: canPrompt
|
|
232
|
+
? await askYesNo('Run npm test now?', false)
|
|
233
|
+
: false;
|
|
1134
234
|
|
|
1135
235
|
if (shouldRunTests) {
|
|
1136
|
-
await runCommand(
|
|
1137
|
-
summary.testRun =
|
|
236
|
+
await runCommand('npm', ['test'], targetDirectory);
|
|
237
|
+
summary.testRun = 'completed';
|
|
1138
238
|
} else {
|
|
1139
|
-
summary.testRun = canPrompt ?
|
|
239
|
+
summary.testRun = canPrompt ? 'skipped' : 'not-run';
|
|
1140
240
|
}
|
|
1141
241
|
}
|
|
1142
242
|
} else {
|
|
1143
|
-
process.stdout.write(
|
|
1144
|
-
|
|
243
|
+
process.stdout.write(
|
|
244
|
+
`${colors.yellow('Skipping')} npm test prompt because npm is not available.\n`
|
|
245
|
+
);
|
|
246
|
+
summary.testRun = 'unavailable';
|
|
1145
247
|
}
|
|
1146
248
|
}
|
|
1147
249
|
|
|
1148
250
|
function resolveUpgradeTarget(args) {
|
|
1149
251
|
if (args.length > 1) {
|
|
1150
|
-
throw new Error(
|
|
252
|
+
throw new Error(
|
|
253
|
+
'Too many arguments for upgrade. Use `create-qa-patterns upgrade check [target-directory]`.'
|
|
254
|
+
);
|
|
1151
255
|
}
|
|
1152
256
|
|
|
1153
|
-
return path.resolve(process.cwd(), args[0] ||
|
|
257
|
+
return path.resolve(process.cwd(), args[0] || '.');
|
|
1154
258
|
}
|
|
1155
259
|
|
|
1156
260
|
function runUpgradeCommand(rawArgs) {
|
|
1157
|
-
const [subcommand =
|
|
1158
|
-
const
|
|
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
|
+
});
|
|
1159
267
|
const targetDirectory = resolveUpgradeTarget(options.positionalArgs);
|
|
1160
|
-
const metadata = readProjectMetadata(targetDirectory);
|
|
1161
|
-
const templateId =
|
|
1162
|
-
|
|
268
|
+
const metadata = readProjectMetadata(targetDirectory, METADATA_FILENAME);
|
|
269
|
+
const templateId =
|
|
270
|
+
metadata.template ||
|
|
271
|
+
detectTemplateFromProject(targetDirectory, METADATA_FILENAME);
|
|
272
|
+
const template = getTemplate(TEMPLATES, templateId);
|
|
1163
273
|
|
|
1164
274
|
if (!template) {
|
|
1165
275
|
throw new Error(`Unsupported template "${templateId}".`);
|
|
1166
276
|
}
|
|
1167
277
|
|
|
1168
|
-
const results = analyzeUpgrade(
|
|
278
|
+
const results = analyzeUpgrade(
|
|
279
|
+
template,
|
|
280
|
+
targetDirectory,
|
|
281
|
+
metadata,
|
|
282
|
+
metadataOptions
|
|
283
|
+
);
|
|
1169
284
|
|
|
1170
|
-
if (subcommand ===
|
|
1171
|
-
printUpgradeReport(
|
|
285
|
+
if (subcommand === 'check' || subcommand === 'report') {
|
|
286
|
+
printUpgradeReport(
|
|
287
|
+
targetDirectory,
|
|
288
|
+
metadata,
|
|
289
|
+
results,
|
|
290
|
+
CLI_PACKAGE_VERSION,
|
|
291
|
+
colors
|
|
292
|
+
);
|
|
1172
293
|
return;
|
|
1173
294
|
}
|
|
1174
295
|
|
|
1175
|
-
if (subcommand ===
|
|
296
|
+
if (subcommand === 'apply') {
|
|
1176
297
|
if (!options.safe) {
|
|
1177
|
-
throw new Error(
|
|
298
|
+
throw new Error(
|
|
299
|
+
'Upgrade apply requires --safe. Only safe managed-file updates are supported.'
|
|
300
|
+
);
|
|
1178
301
|
}
|
|
1179
302
|
|
|
1180
|
-
printUpgradeReport(
|
|
1181
|
-
|
|
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');
|
|
1182
322
|
return;
|
|
1183
323
|
}
|
|
1184
324
|
|
|
1185
|
-
throw new Error(
|
|
325
|
+
throw new Error(
|
|
326
|
+
`Unsupported upgrade command "${subcommand}". Use check, report, or apply --safe.`
|
|
327
|
+
);
|
|
1186
328
|
}
|
|
1187
329
|
|
|
1188
330
|
async function main() {
|
|
@@ -1190,39 +332,73 @@ async function main() {
|
|
|
1190
332
|
|
|
1191
333
|
assertSupportedNodeVersion();
|
|
1192
334
|
|
|
1193
|
-
if (rawArgs[0] ===
|
|
335
|
+
if (rawArgs[0] === 'upgrade') {
|
|
1194
336
|
runUpgradeCommand(rawArgs.slice(1));
|
|
1195
337
|
return;
|
|
1196
338
|
}
|
|
1197
339
|
|
|
1198
|
-
if (rawArgs.includes(
|
|
1199
|
-
printHelp();
|
|
340
|
+
if (rawArgs.includes('--help') || rawArgs.includes('-h')) {
|
|
341
|
+
printHelp(TEMPLATES, colors, DEFAULT_TEMPLATE);
|
|
1200
342
|
return;
|
|
1201
343
|
}
|
|
1202
344
|
|
|
1203
|
-
const
|
|
345
|
+
const metadataOptions = createMetadataOptions();
|
|
346
|
+
const options = parseCliOptions(rawArgs, {
|
|
347
|
+
resolveTemplate: (value) => resolveTemplate(TEMPLATE_ALIASES, value),
|
|
348
|
+
supportedTemplateIds: SUPPORTED_TEMPLATE_IDS
|
|
349
|
+
});
|
|
1204
350
|
const args = options.positionalArgs;
|
|
1205
|
-
const { templateName, targetDirectory, generatedInCurrentDirectory } =
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
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);
|
|
1209
361
|
|
|
1210
362
|
if (!template) {
|
|
1211
363
|
throw new Error(`Unsupported template "${templateName}".`);
|
|
1212
364
|
}
|
|
1213
365
|
|
|
1214
366
|
const prerequisites = collectPrerequisites();
|
|
1215
|
-
const summary = createSummary(
|
|
367
|
+
const summary = createSummary(
|
|
368
|
+
template,
|
|
369
|
+
targetDirectory,
|
|
370
|
+
generatedInCurrentDirectory
|
|
371
|
+
);
|
|
1216
372
|
summary.options = options;
|
|
1217
|
-
printPrerequisiteWarnings(prerequisites);
|
|
1218
|
-
await scaffoldProject(
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
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);
|
|
1222
393
|
await runPostGenerateActions(template, targetDirectory, summary);
|
|
1223
|
-
writeProjectMetadata(
|
|
1224
|
-
|
|
1225
|
-
|
|
394
|
+
writeProjectMetadata(
|
|
395
|
+
template,
|
|
396
|
+
targetDirectory,
|
|
397
|
+
readProjectMetadata(targetDirectory, METADATA_FILENAME),
|
|
398
|
+
metadataOptions
|
|
399
|
+
);
|
|
400
|
+
printSummary(summary, colors);
|
|
401
|
+
printNextSteps(summary, colors);
|
|
1226
402
|
}
|
|
1227
403
|
|
|
1228
404
|
main().catch((error) => {
|