create-contractor-site 2.1.1
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/bin/create-contractor-site.mjs +293 -0
- package/package.json +42 -0
- package/scripts/smoke-test.mjs +502 -0
- package/src/copy-template.mjs +286 -0
- package/src/prompts.mjs +121 -0
- package/src/replace-data.mjs +465 -0
- package/src/run-command.mjs +399 -0
- package/src/validate-target.mjs +96 -0
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* create-contractor-site — scaffold a client contractor site from the template.
|
|
4
|
+
* Uses pnpm only for install/validate/build. Never invokes npm or npx.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import {
|
|
9
|
+
assertRequiredCopied,
|
|
10
|
+
copyTemplate,
|
|
11
|
+
resolveTemplateRoot,
|
|
12
|
+
} from '../src/copy-template.mjs';
|
|
13
|
+
import { collectClientAnswers, buildAnswers } from '../src/prompts.mjs';
|
|
14
|
+
import { replaceTargetData } from '../src/replace-data.mjs';
|
|
15
|
+
import {
|
|
16
|
+
assertGitAvailable,
|
|
17
|
+
assertPnpmAvailable,
|
|
18
|
+
runGitInit,
|
|
19
|
+
runPnpmSetup,
|
|
20
|
+
} from '../src/run-command.mjs';
|
|
21
|
+
import {
|
|
22
|
+
TargetValidationError,
|
|
23
|
+
validateTarget,
|
|
24
|
+
} from '../src/validate-target.mjs';
|
|
25
|
+
|
|
26
|
+
const USAGE = `Usage: create-contractor-site [options] <target-dir>
|
|
27
|
+
|
|
28
|
+
Scaffold a client contractor website from the placeholder multipage template.
|
|
29
|
+
|
|
30
|
+
Options:
|
|
31
|
+
-y, --yes Non-interactive mode using built-in sample answers
|
|
32
|
+
-h, --help Show this help
|
|
33
|
+
|
|
34
|
+
Environment:
|
|
35
|
+
CREATE_CONTRACTOR_SITE_ANSWERS_JSON
|
|
36
|
+
JSON object with at least businessName and primaryServices[].
|
|
37
|
+
When set, skips prompts (and overrides --yes sample answers).
|
|
38
|
+
|
|
39
|
+
CREATE_CONTRACTOR_TEMPLATE_ROOT
|
|
40
|
+
Absolute/relative path to a local template checkout (preferred for
|
|
41
|
+
local/dev and monorepo use).
|
|
42
|
+
|
|
43
|
+
CREATE_CONTRACTOR_TEMPLATE_REPO
|
|
44
|
+
Git URL used when the CLI is published and no local template is found.
|
|
45
|
+
Default: https://github.com/glacayo/website-multipages.git
|
|
46
|
+
|
|
47
|
+
CREATE_CONTRACTOR_TEMPLATE_REF
|
|
48
|
+
Git branch/tag/ref to clone for the published fallback. Default: v2.1.1
|
|
49
|
+
|
|
50
|
+
Answer precedence (highest first):
|
|
51
|
+
1. CREATE_CONTRACTOR_SITE_ANSWERS_JSON
|
|
52
|
+
2. --yes / -y sample answers
|
|
53
|
+
3. Interactive prompts (default)
|
|
54
|
+
|
|
55
|
+
Template source precedence (highest first):
|
|
56
|
+
1. CREATE_CONTRACTOR_TEMPLATE_ROOT
|
|
57
|
+
2. Local monorepo root discovered from this package
|
|
58
|
+
3. Temporary git clone of CREATE_CONTRACTOR_TEMPLATE_REPO @ REF
|
|
59
|
+
|
|
60
|
+
Requirements:
|
|
61
|
+
- Node.js 22+
|
|
62
|
+
- pnpm >= 11.1.2
|
|
63
|
+
- git
|
|
64
|
+
|
|
65
|
+
The CLI copies the template (denylist excludes node_modules, dist, .git, openspec,
|
|
66
|
+
packages/, etc.), applies client values only in src/data/*.json, then runs:
|
|
67
|
+
pnpm install
|
|
68
|
+
pnpm run validate:data
|
|
69
|
+
pnpm run build
|
|
70
|
+
git init && git add -A && git commit (only after validate + build succeed)
|
|
71
|
+
|
|
72
|
+
If install, validate:data, or build fails, git init/add/commit are intentionally
|
|
73
|
+
skipped so a broken scaffold is never committed.
|
|
74
|
+
|
|
75
|
+
Package-runner note: you may invoke this binary via pnpm create / compatible runners,
|
|
76
|
+
but the CLI itself always uses pnpm internally — never npm install or npx for project setup.
|
|
77
|
+
`;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Parse CLI args. Supports:
|
|
81
|
+
* create-contractor-site <target-dir>
|
|
82
|
+
* create-contractor-site --yes <target-dir> (non-interactive defaults; testing)
|
|
83
|
+
* CREATE_CONTRACTOR_SITE_ANSWERS_JSON env for scripted answers
|
|
84
|
+
*/
|
|
85
|
+
function parseArgs(argv) {
|
|
86
|
+
/** @type {string[]} */
|
|
87
|
+
const positionals = [];
|
|
88
|
+
let nonInteractive = false;
|
|
89
|
+
|
|
90
|
+
for (const arg of argv) {
|
|
91
|
+
if (arg === '--help' || arg === '-h') {
|
|
92
|
+
return { help: true, positionals, nonInteractive };
|
|
93
|
+
}
|
|
94
|
+
if (arg === '--yes' || arg === '-y') {
|
|
95
|
+
nonInteractive = true;
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (arg.startsWith('-')) {
|
|
99
|
+
throw new Error(`Unknown option: ${arg}\n\n${USAGE}`);
|
|
100
|
+
}
|
|
101
|
+
positionals.push(arg);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return { help: false, positionals, nonInteractive };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* @returns {Promise<import('../src/prompts.mjs').ScaffoldAnswers>}
|
|
109
|
+
*/
|
|
110
|
+
async function resolveAnswers(nonInteractive) {
|
|
111
|
+
const fromEnv = process.env.CREATE_CONTRACTOR_SITE_ANSWERS_JSON;
|
|
112
|
+
if (fromEnv) {
|
|
113
|
+
const parsed = JSON.parse(fromEnv);
|
|
114
|
+
if (!parsed.businessName || !Array.isArray(parsed.primaryServices)) {
|
|
115
|
+
throw new Error(
|
|
116
|
+
'CREATE_CONTRACTOR_SITE_ANSWERS_JSON must include businessName and primaryServices[]',
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
return buildAnswers(parsed);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (nonInteractive) {
|
|
123
|
+
return buildAnswers({
|
|
124
|
+
businessName: 'Acme Masonry',
|
|
125
|
+
legalName: 'Acme Masonry LLC',
|
|
126
|
+
tagline: 'Stonework that lasts.',
|
|
127
|
+
phone: '(757) 555-0199',
|
|
128
|
+
email: 'hello@acmemasonry.example',
|
|
129
|
+
street: '100 Scaffold Ave',
|
|
130
|
+
city: 'Virginia Beach',
|
|
131
|
+
state: 'VA',
|
|
132
|
+
zip: '23451',
|
|
133
|
+
serviceArea: 'Virginia Beach and Hampton Roads',
|
|
134
|
+
foundedYear: '2015',
|
|
135
|
+
primaryServices: ['Masonry', 'Patios', 'Retaining Walls'],
|
|
136
|
+
siteUrl: 'https://acmemasonry.example',
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return collectClientAnswers();
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* @param {string} stage
|
|
145
|
+
* @param {string} targetDir
|
|
146
|
+
* @param {unknown} err
|
|
147
|
+
*/
|
|
148
|
+
function printRecoveryGuidance(stage, targetDir, err) {
|
|
149
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
150
|
+
console.error(`\nScaffold failed during: ${stage}`);
|
|
151
|
+
console.error(message);
|
|
152
|
+
console.error(`
|
|
153
|
+
Recovery guidance:
|
|
154
|
+
Target path: ${targetDir || '(not created yet)'}
|
|
155
|
+
|
|
156
|
+
1. Inspect the target directory. Partial files may remain after a failed
|
|
157
|
+
copy, JSON replace, install, build, or git step.
|
|
158
|
+
2. Fix the underlying issue (pnpm/git install, network, disk space, data).
|
|
159
|
+
3. Remove the incomplete target directory if you want a clean retry:
|
|
160
|
+
rm -rf "${targetDir || '<target-dir>'}" # or Delete on Windows
|
|
161
|
+
4. Re-run create-contractor-site with the same arguments.
|
|
162
|
+
|
|
163
|
+
Notes:
|
|
164
|
+
- The source template is never modified (writes are target-only).
|
|
165
|
+
- Install/build always use pnpm — never npm install / npx.
|
|
166
|
+
- git init / git add / git commit run ONLY after validate:data and build succeed.
|
|
167
|
+
If this failure was during install, validate:data, or build, those git steps
|
|
168
|
+
were intentionally skipped (a broken scaffold is never committed).
|
|
169
|
+
- If a temp template clone was used, it is cleaned up automatically when possible.
|
|
170
|
+
`);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async function main() {
|
|
174
|
+
const { help, positionals, nonInteractive } = parseArgs(
|
|
175
|
+
process.argv.slice(2),
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
if (help) {
|
|
179
|
+
console.log(USAGE);
|
|
180
|
+
process.exit(0);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const targetArg = positionals[0];
|
|
184
|
+
if (!targetArg) {
|
|
185
|
+
console.error('Missing target directory.\n');
|
|
186
|
+
console.error(USAGE);
|
|
187
|
+
process.exit(1);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
console.log('create-contractor-site — Contractor multipage template scaffold');
|
|
191
|
+
console.log('pnpm-only installs. Placeholder template → client project.\n');
|
|
192
|
+
|
|
193
|
+
// Preconditions before any filesystem writes.
|
|
194
|
+
try {
|
|
195
|
+
assertPnpmAvailable();
|
|
196
|
+
assertGitAvailable();
|
|
197
|
+
} catch (err) {
|
|
198
|
+
console.error(err instanceof Error ? err.message : err);
|
|
199
|
+
process.exit(1);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** @type {(() => void) | null} */
|
|
203
|
+
let cleanupTemplate = null;
|
|
204
|
+
/** @type {string} */
|
|
205
|
+
let targetDir = '';
|
|
206
|
+
/** @type {string} */
|
|
207
|
+
let stage = 'init';
|
|
208
|
+
|
|
209
|
+
try {
|
|
210
|
+
stage = 'resolve-template';
|
|
211
|
+
const template = resolveTemplateRoot();
|
|
212
|
+
cleanupTemplate = template.cleanup;
|
|
213
|
+
const templateRoot = template.root;
|
|
214
|
+
|
|
215
|
+
stage = 'validate-target';
|
|
216
|
+
try {
|
|
217
|
+
targetDir = validateTarget(targetArg, { templateRoot });
|
|
218
|
+
} catch (err) {
|
|
219
|
+
if (err instanceof TargetValidationError) {
|
|
220
|
+
console.error(err.message);
|
|
221
|
+
process.exitCode = 1;
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
throw err;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
console.log(
|
|
228
|
+
`Template root: ${templateRoot} (${template.source}${template.source === 'clone' ? ', temp' : ''})`,
|
|
229
|
+
);
|
|
230
|
+
console.log(`Target: ${targetDir}`);
|
|
231
|
+
|
|
232
|
+
stage = 'collect-answers';
|
|
233
|
+
const answers = await resolveAnswers(nonInteractive);
|
|
234
|
+
|
|
235
|
+
stage = 'copy-template';
|
|
236
|
+
console.log('\n→ Copying template files…');
|
|
237
|
+
const { copiedFiles, skipped } = copyTemplate(templateRoot, targetDir);
|
|
238
|
+
console.log(
|
|
239
|
+
` Copied ${copiedFiles} files (skipped ${skipped.length} denied paths).`,
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
stage = 'assert-copy';
|
|
243
|
+
const missing = assertRequiredCopied(targetDir);
|
|
244
|
+
if (missing.length > 0) {
|
|
245
|
+
throw new Error(
|
|
246
|
+
`Copy incomplete. Missing required paths:\n${missing.map((m) => ` - ${m}`).join('\n')}`,
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
stage = 'replace-data';
|
|
251
|
+
console.log('→ Replacing JSON values in src/data (shape preserved)…');
|
|
252
|
+
replaceTargetData(targetDir, answers);
|
|
253
|
+
|
|
254
|
+
stage = 'pnpm-setup';
|
|
255
|
+
runPnpmSetup(targetDir);
|
|
256
|
+
|
|
257
|
+
stage = 'git-init';
|
|
258
|
+
runGitInit(targetDir);
|
|
259
|
+
|
|
260
|
+
console.log(`
|
|
261
|
+
✓ Scaffold complete
|
|
262
|
+
|
|
263
|
+
Path: ${targetDir}
|
|
264
|
+
|
|
265
|
+
Next steps:
|
|
266
|
+
cd ${path.relative(process.cwd(), targetDir) || '.'}
|
|
267
|
+
pnpm run dev
|
|
268
|
+
|
|
269
|
+
Remember:
|
|
270
|
+
- Customize remaining copy in src/data/*.json
|
|
271
|
+
- Keep pnpm only (never npm install / npx for this project)
|
|
272
|
+
- Run pnpm run validate:data and pnpm run build before deploy
|
|
273
|
+
`);
|
|
274
|
+
} catch (err) {
|
|
275
|
+
printRecoveryGuidance(stage, targetDir, err);
|
|
276
|
+
process.exitCode = 1;
|
|
277
|
+
} finally {
|
|
278
|
+
if (cleanupTemplate) {
|
|
279
|
+
try {
|
|
280
|
+
cleanupTemplate();
|
|
281
|
+
} catch {
|
|
282
|
+
console.error(
|
|
283
|
+
'Warning: could not remove temporary template clone. You may delete it manually from your system temp directory.',
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
main().catch((err) => {
|
|
291
|
+
console.error(err instanceof Error ? err.message : err);
|
|
292
|
+
process.exit(1);
|
|
293
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-contractor-site",
|
|
3
|
+
"version": "2.1.1",
|
|
4
|
+
"description": "Scaffold a client contractor website from the placeholder multipage template",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"create-contractor-site": "./bin/create-contractor-site.mjs"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin",
|
|
11
|
+
"src",
|
|
12
|
+
"scripts"
|
|
13
|
+
],
|
|
14
|
+
"keywords": [
|
|
15
|
+
"contractor",
|
|
16
|
+
"astro",
|
|
17
|
+
"scaffold",
|
|
18
|
+
"create",
|
|
19
|
+
"pnpm"
|
|
20
|
+
],
|
|
21
|
+
"license": "ISC",
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "git+https://github.com/glacayo/website-multipages.git",
|
|
25
|
+
"directory": "packages/create-contractor-site"
|
|
26
|
+
},
|
|
27
|
+
"homepage": "https://github.com/glacayo/website-multipages/tree/main/packages/create-contractor-site",
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=22.12.0",
|
|
30
|
+
"pnpm": ">=11.1.2"
|
|
31
|
+
},
|
|
32
|
+
"devEngines": {
|
|
33
|
+
"packageManager": {
|
|
34
|
+
"name": "pnpm",
|
|
35
|
+
"version": ">=11.1.2",
|
|
36
|
+
"onFail": "error"
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"scripts": {
|
|
40
|
+
"test:smoke": "node ./scripts/smoke-test.mjs"
|
|
41
|
+
}
|
|
42
|
+
}
|