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