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.
@@ -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
+ }