create-contractor-site 2.3.0 → 2.3.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.
@@ -1,296 +1,296 @@
1
- import fs from 'node:fs';
2
- import os from 'node:os';
3
- import path from 'node:path';
4
- import { fileURLToPath } from 'node:url';
5
- import { runCommand } from './run-command.mjs';
6
-
7
- /** Directory / file basenames that must never be copied into a client scaffold. */
8
- const DENY_DIR_NAMES = new Set([
9
- 'node_modules',
10
- 'dist',
11
- '.astro',
12
- '.git',
13
- '.codegraph',
14
- 'docs_trash',
15
- 'openspec',
16
- 'logs',
17
- // CLI package + internal agent tooling — not part of a client site
18
- 'packages',
19
- '.atl',
20
- // Image-tooling runtime state only (exact basename; never bare _out or tools)
21
- 'CUSTOMER-IMAGES',
22
- '.img-ia',
23
- ]);
24
-
25
- const DENY_FILE_NAMES = new Set(['package-lock.json']);
26
-
27
- /** Default remote template when the CLI is published outside the monorepo. */
28
- export const DEFAULT_TEMPLATE_REPO =
29
- process.env.CREATE_CONTRACTOR_TEMPLATE_REPO ||
30
- 'https://github.com/glacayo/website-multipages.git';
31
-
32
- export const DEFAULT_TEMPLATE_REF =
33
- process.env.CREATE_CONTRACTOR_TEMPLATE_REF || 'v2.3.0';
34
-
35
- /**
36
- * @param {string} name basename
37
- * @returns {boolean}
38
- */
39
- export function isDeniedName(name) {
40
- if (DENY_DIR_NAMES.has(name)) return true;
41
- if (DENY_FILE_NAMES.has(name)) return true;
42
- if (name.endsWith('.log')) return true;
43
- if (name.startsWith('.env')) return true;
44
- return false;
45
- }
46
-
47
- /**
48
- * @param {string} dir
49
- * @returns {boolean}
50
- */
51
- export function looksLikeTemplateRoot(dir) {
52
- return (
53
- fs.existsSync(path.join(dir, 'AGENTS.md')) &&
54
- fs.existsSync(path.join(dir, 'package.json')) &&
55
- fs.existsSync(path.join(dir, 'src', 'data', 'business.json')) &&
56
- fs.existsSync(path.join(dir, 'scripts', 'enforce-package-manager.cjs'))
57
- );
58
- }
59
-
60
- /**
61
- * @param {string} dir
62
- */
63
- function assertTemplateRoot(dir) {
64
- if (!looksLikeTemplateRoot(dir)) {
65
- throw new Error(
66
- `Template path does not look like the contractor template: ${dir}\n` +
67
- 'Expected AGENTS.md, package.json, src/data/business.json, and scripts/enforce-package-manager.cjs.',
68
- );
69
- }
70
- }
71
-
72
- /**
73
- * Walk parents looking for the monorepo template root.
74
- *
75
- * @param {string} [startDir]
76
- * @returns {string | null}
77
- */
78
- export function findLocalTemplateRoot(
79
- startDir = path.dirname(fileURLToPath(import.meta.url)),
80
- ) {
81
- let dir = path.resolve(startDir);
82
- for (let i = 0; i < 8; i += 1) {
83
- if (looksLikeTemplateRoot(dir)) {
84
- return dir;
85
- }
86
- const parent = path.dirname(dir);
87
- if (parent === dir) break;
88
- dir = parent;
89
- }
90
- return null;
91
- }
92
-
93
- /**
94
- * Clone the template repository into a temp directory (argv-based git only).
95
- *
96
- * @param {{ repo?: string, ref?: string }} [opts]
97
- * @returns {{ root: string, cleanup: () => void }}
98
- */
99
- export function cloneTemplateToTemp(opts = {}) {
100
- const repo = opts.repo || DEFAULT_TEMPLATE_REPO;
101
- const ref = opts.ref || DEFAULT_TEMPLATE_REF;
102
- const tempBase = fs.mkdtempSync(
103
- path.join(os.tmpdir(), 'create-contractor-site-'),
104
- );
105
- const root = path.join(tempBase, 'template');
106
-
107
- try {
108
- console.log(`→ Cloning template ${repo} @ ${ref} …`);
109
- runCommand(
110
- 'git',
111
- [
112
- 'clone',
113
- '--depth',
114
- '1',
115
- '--branch',
116
- ref,
117
- '--single-branch',
118
- repo,
119
- root,
120
- ],
121
- { stdio: 'pipe' },
122
- );
123
- assertTemplateRoot(root);
124
- } catch (err) {
125
- // Retry without --branch if the ref is a commit or default branch differs.
126
- try {
127
- if (fs.existsSync(root)) {
128
- fs.rmSync(root, { recursive: true, force: true });
129
- }
130
- runCommand('git', ['clone', '--depth', '1', repo, root], {
131
- stdio: 'pipe',
132
- });
133
- if (ref && ref !== 'HEAD') {
134
- runCommand('git', ['checkout', ref], { cwd: root, stdio: 'pipe' });
135
- }
136
- assertTemplateRoot(root);
137
- } catch (inner) {
138
- try {
139
- fs.rmSync(tempBase, { recursive: true, force: true });
140
- } catch {
141
- // ignore cleanup errors
142
- }
143
- throw new Error(
144
- `Failed to clone template repository.\n` +
145
- ` Repo: ${repo}\n` +
146
- ` Ref: ${ref}\n` +
147
- `${inner instanceof Error ? inner.message : String(inner)}\n\n` +
148
- `Recovery:\n` +
149
- ` - Set CREATE_CONTRACTOR_TEMPLATE_ROOT to a local checkout of the template, or\n` +
150
- ` - Set CREATE_CONTRACTOR_TEMPLATE_REPO / CREATE_CONTRACTOR_TEMPLATE_REF, or\n` +
151
- ` - Run the CLI from inside the monorepo workspace.`,
152
- );
153
- }
154
- }
155
-
156
- return {
157
- root,
158
- cleanup: () => {
159
- try {
160
- fs.rmSync(tempBase, { recursive: true, force: true });
161
- } catch {
162
- // Best-effort cleanup; leave a note if it fails later in the CLI.
163
- }
164
- },
165
- };
166
- }
167
-
168
- /**
169
- * Resolve the contractor template root for copy.
170
- *
171
- * Priority:
172
- * 1. CREATE_CONTRACTOR_TEMPLATE_ROOT (local/dev override)
173
- * 2. Walk parents from this package (monorepo / linked workspace)
174
- * 3. Clone DEFAULT_TEMPLATE_REPO @ DEFAULT_TEMPLATE_REF into a temp dir
175
- *
176
- * @param {string} [startDir]
177
- * @returns {{ root: string, cleanup: (() => void) | null, source: 'env' | 'local' | 'clone' }}
178
- */
179
- export function resolveTemplateRoot(
180
- startDir = path.dirname(fileURLToPath(import.meta.url)),
181
- ) {
182
- if (process.env.CREATE_CONTRACTOR_TEMPLATE_ROOT) {
183
- const fromEnv = path.resolve(process.env.CREATE_CONTRACTOR_TEMPLATE_ROOT);
184
- assertTemplateRoot(fromEnv);
185
- return { root: fromEnv, cleanup: null, source: 'env' };
186
- }
187
-
188
- const local = findLocalTemplateRoot(startDir);
189
- if (local) {
190
- return { root: local, cleanup: null, source: 'local' };
191
- }
192
-
193
- const cloned = cloneTemplateToTemp();
194
- return { root: cloned.root, cleanup: cloned.cleanup, source: 'clone' };
195
- }
196
-
197
- /**
198
- * Recursively copy template files into target using the denylist.
199
- *
200
- * @param {string} templateRoot
201
- * @param {string} targetDir
202
- * @returns {{ copiedFiles: number, skipped: string[] }}
203
- */
204
- export function copyTemplate(templateRoot, targetDir) {
205
- fs.mkdirSync(targetDir, { recursive: true });
206
-
207
- /** @type {string[]} */
208
- const skipped = [];
209
- let copiedFiles = 0;
210
-
211
- /**
212
- * @param {string} srcDir
213
- * @param {string} destDir
214
- * @param {string} relBase
215
- */
216
- function walk(srcDir, destDir, relBase) {
217
- const entries = fs.readdirSync(srcDir, { withFileTypes: true });
218
-
219
- for (const entry of entries) {
220
- const name = entry.name;
221
- const relPath = relBase ? path.join(relBase, name) : name;
222
-
223
- if (isDeniedName(name)) {
224
- skipped.push(relPath.replaceAll('\\', '/'));
225
- continue;
226
- }
227
-
228
- const srcPath = path.join(srcDir, name);
229
- const destPath = path.join(destDir, name);
230
-
231
- if (entry.isDirectory()) {
232
- fs.mkdirSync(destPath, { recursive: true });
233
- walk(srcPath, destPath, relPath);
234
- continue;
235
- }
236
-
237
- if (entry.isSymbolicLink()) {
238
- skipped.push(relPath.replaceAll('\\', '/'));
239
- continue;
240
- }
241
-
242
- if (entry.isFile()) {
243
- fs.mkdirSync(path.dirname(destPath), { recursive: true });
244
- fs.copyFileSync(srcPath, destPath);
245
- copiedFiles += 1;
246
- }
247
- }
248
- }
249
-
250
- walk(templateRoot, targetDir, '');
251
-
252
- return { copiedFiles, skipped };
253
- }
254
-
255
- /** Required paths that must exist after copy (relative, posix-style). */
256
- export const REQUIRED_AFTER_COPY = [
257
- 'src',
258
- 'public',
259
- 'scripts',
260
- 'package.json',
261
- 'pnpm-lock.yaml',
262
- '.npmrc',
263
- 'pnpm-workspace.yaml',
264
- 'astro.config.mjs',
265
- 'tsconfig.json',
266
- 'netlify.toml',
267
- 'README.md',
268
- 'AGENTS.md',
269
- 'SKILL.md',
270
- 'scripts/enforce-package-manager.cjs',
271
- 'src/assets/images',
272
- 'src/data/business.json',
273
- // Smart-image capsule + agent skill (must scaffold with every client site)
274
- 'tools/smart-image/package.json',
275
- 'tools/smart-image/pnpm-workspace.yaml',
276
- 'tools/smart-image/pnpm-lock.yaml',
277
- 'tools/smart-image/run.mjs',
278
- 'tools/smart-image/check.mjs',
279
- '.agents/skills/smart-image-cli/SKILL.md',
280
- ];
281
-
282
- /**
283
- * @param {string} targetDir
284
- * @returns {string[]} missing relative paths
285
- */
286
- export function assertRequiredCopied(targetDir) {
287
- /** @type {string[]} */
288
- const missing = [];
289
- for (const rel of REQUIRED_AFTER_COPY) {
290
- const full = path.join(targetDir, ...rel.split('/'));
291
- if (!fs.existsSync(full)) {
292
- missing.push(rel);
293
- }
294
- }
295
- return missing;
296
- }
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { runCommand } from './run-command.mjs';
6
+
7
+ /** Directory / file basenames that must never be copied into a client scaffold. */
8
+ const DENY_DIR_NAMES = new Set([
9
+ 'node_modules',
10
+ 'dist',
11
+ '.astro',
12
+ '.git',
13
+ '.codegraph',
14
+ 'docs_trash',
15
+ 'openspec',
16
+ 'logs',
17
+ // CLI package + internal agent tooling — not part of a client site
18
+ 'packages',
19
+ '.atl',
20
+ // Image-tooling runtime state only (exact basename; never bare _out or tools)
21
+ 'CUSTOMER-IMAGES',
22
+ '.img-ia',
23
+ ]);
24
+
25
+ const DENY_FILE_NAMES = new Set(['package-lock.json']);
26
+
27
+ /** Default remote template when the CLI is published outside the monorepo. */
28
+ export const DEFAULT_TEMPLATE_REPO =
29
+ process.env.CREATE_CONTRACTOR_TEMPLATE_REPO ||
30
+ 'https://github.com/glacayo/website-multipages.git';
31
+
32
+ export const DEFAULT_TEMPLATE_REF =
33
+ process.env.CREATE_CONTRACTOR_TEMPLATE_REF || 'v2.3.1';
34
+
35
+ /**
36
+ * @param {string} name basename
37
+ * @returns {boolean}
38
+ */
39
+ export function isDeniedName(name) {
40
+ if (DENY_DIR_NAMES.has(name)) return true;
41
+ if (DENY_FILE_NAMES.has(name)) return true;
42
+ if (name.endsWith('.log')) return true;
43
+ if (name.startsWith('.env')) return true;
44
+ return false;
45
+ }
46
+
47
+ /**
48
+ * @param {string} dir
49
+ * @returns {boolean}
50
+ */
51
+ export function looksLikeTemplateRoot(dir) {
52
+ return (
53
+ fs.existsSync(path.join(dir, 'AGENTS.md')) &&
54
+ fs.existsSync(path.join(dir, 'package.json')) &&
55
+ fs.existsSync(path.join(dir, 'src', 'data', 'business.json')) &&
56
+ fs.existsSync(path.join(dir, 'scripts', 'enforce-package-manager.cjs'))
57
+ );
58
+ }
59
+
60
+ /**
61
+ * @param {string} dir
62
+ */
63
+ function assertTemplateRoot(dir) {
64
+ if (!looksLikeTemplateRoot(dir)) {
65
+ throw new Error(
66
+ `Template path does not look like the contractor template: ${dir}\n` +
67
+ 'Expected AGENTS.md, package.json, src/data/business.json, and scripts/enforce-package-manager.cjs.',
68
+ );
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Walk parents looking for the monorepo template root.
74
+ *
75
+ * @param {string} [startDir]
76
+ * @returns {string | null}
77
+ */
78
+ export function findLocalTemplateRoot(
79
+ startDir = path.dirname(fileURLToPath(import.meta.url)),
80
+ ) {
81
+ let dir = path.resolve(startDir);
82
+ for (let i = 0; i < 8; i += 1) {
83
+ if (looksLikeTemplateRoot(dir)) {
84
+ return dir;
85
+ }
86
+ const parent = path.dirname(dir);
87
+ if (parent === dir) break;
88
+ dir = parent;
89
+ }
90
+ return null;
91
+ }
92
+
93
+ /**
94
+ * Clone the template repository into a temp directory (argv-based git only).
95
+ *
96
+ * @param {{ repo?: string, ref?: string }} [opts]
97
+ * @returns {{ root: string, cleanup: () => void }}
98
+ */
99
+ export function cloneTemplateToTemp(opts = {}) {
100
+ const repo = opts.repo || DEFAULT_TEMPLATE_REPO;
101
+ const ref = opts.ref || DEFAULT_TEMPLATE_REF;
102
+ const tempBase = fs.mkdtempSync(
103
+ path.join(os.tmpdir(), 'create-contractor-site-'),
104
+ );
105
+ const root = path.join(tempBase, 'template');
106
+
107
+ try {
108
+ console.log(`→ Cloning template ${repo} @ ${ref} …`);
109
+ runCommand(
110
+ 'git',
111
+ [
112
+ 'clone',
113
+ '--depth',
114
+ '1',
115
+ '--branch',
116
+ ref,
117
+ '--single-branch',
118
+ repo,
119
+ root,
120
+ ],
121
+ { stdio: 'pipe' },
122
+ );
123
+ assertTemplateRoot(root);
124
+ } catch (err) {
125
+ // Retry without --branch if the ref is a commit or default branch differs.
126
+ try {
127
+ if (fs.existsSync(root)) {
128
+ fs.rmSync(root, { recursive: true, force: true });
129
+ }
130
+ runCommand('git', ['clone', '--depth', '1', repo, root], {
131
+ stdio: 'pipe',
132
+ });
133
+ if (ref && ref !== 'HEAD') {
134
+ runCommand('git', ['checkout', ref], { cwd: root, stdio: 'pipe' });
135
+ }
136
+ assertTemplateRoot(root);
137
+ } catch (inner) {
138
+ try {
139
+ fs.rmSync(tempBase, { recursive: true, force: true });
140
+ } catch {
141
+ // ignore cleanup errors
142
+ }
143
+ throw new Error(
144
+ `Failed to clone template repository.\n` +
145
+ ` Repo: ${repo}\n` +
146
+ ` Ref: ${ref}\n` +
147
+ `${inner instanceof Error ? inner.message : String(inner)}\n\n` +
148
+ `Recovery:\n` +
149
+ ` - Set CREATE_CONTRACTOR_TEMPLATE_ROOT to a local checkout of the template, or\n` +
150
+ ` - Set CREATE_CONTRACTOR_TEMPLATE_REPO / CREATE_CONTRACTOR_TEMPLATE_REF, or\n` +
151
+ ` - Run the CLI from inside the monorepo workspace.`,
152
+ );
153
+ }
154
+ }
155
+
156
+ return {
157
+ root,
158
+ cleanup: () => {
159
+ try {
160
+ fs.rmSync(tempBase, { recursive: true, force: true });
161
+ } catch {
162
+ // Best-effort cleanup; leave a note if it fails later in the CLI.
163
+ }
164
+ },
165
+ };
166
+ }
167
+
168
+ /**
169
+ * Resolve the contractor template root for copy.
170
+ *
171
+ * Priority:
172
+ * 1. CREATE_CONTRACTOR_TEMPLATE_ROOT (local/dev override)
173
+ * 2. Walk parents from this package (monorepo / linked workspace)
174
+ * 3. Clone DEFAULT_TEMPLATE_REPO @ DEFAULT_TEMPLATE_REF into a temp dir
175
+ *
176
+ * @param {string} [startDir]
177
+ * @returns {{ root: string, cleanup: (() => void) | null, source: 'env' | 'local' | 'clone' }}
178
+ */
179
+ export function resolveTemplateRoot(
180
+ startDir = path.dirname(fileURLToPath(import.meta.url)),
181
+ ) {
182
+ if (process.env.CREATE_CONTRACTOR_TEMPLATE_ROOT) {
183
+ const fromEnv = path.resolve(process.env.CREATE_CONTRACTOR_TEMPLATE_ROOT);
184
+ assertTemplateRoot(fromEnv);
185
+ return { root: fromEnv, cleanup: null, source: 'env' };
186
+ }
187
+
188
+ const local = findLocalTemplateRoot(startDir);
189
+ if (local) {
190
+ return { root: local, cleanup: null, source: 'local' };
191
+ }
192
+
193
+ const cloned = cloneTemplateToTemp();
194
+ return { root: cloned.root, cleanup: cloned.cleanup, source: 'clone' };
195
+ }
196
+
197
+ /**
198
+ * Recursively copy template files into target using the denylist.
199
+ *
200
+ * @param {string} templateRoot
201
+ * @param {string} targetDir
202
+ * @returns {{ copiedFiles: number, skipped: string[] }}
203
+ */
204
+ export function copyTemplate(templateRoot, targetDir) {
205
+ fs.mkdirSync(targetDir, { recursive: true });
206
+
207
+ /** @type {string[]} */
208
+ const skipped = [];
209
+ let copiedFiles = 0;
210
+
211
+ /**
212
+ * @param {string} srcDir
213
+ * @param {string} destDir
214
+ * @param {string} relBase
215
+ */
216
+ function walk(srcDir, destDir, relBase) {
217
+ const entries = fs.readdirSync(srcDir, { withFileTypes: true });
218
+
219
+ for (const entry of entries) {
220
+ const name = entry.name;
221
+ const relPath = relBase ? path.join(relBase, name) : name;
222
+
223
+ if (isDeniedName(name)) {
224
+ skipped.push(relPath.replaceAll('\\', '/'));
225
+ continue;
226
+ }
227
+
228
+ const srcPath = path.join(srcDir, name);
229
+ const destPath = path.join(destDir, name);
230
+
231
+ if (entry.isDirectory()) {
232
+ fs.mkdirSync(destPath, { recursive: true });
233
+ walk(srcPath, destPath, relPath);
234
+ continue;
235
+ }
236
+
237
+ if (entry.isSymbolicLink()) {
238
+ skipped.push(relPath.replaceAll('\\', '/'));
239
+ continue;
240
+ }
241
+
242
+ if (entry.isFile()) {
243
+ fs.mkdirSync(path.dirname(destPath), { recursive: true });
244
+ fs.copyFileSync(srcPath, destPath);
245
+ copiedFiles += 1;
246
+ }
247
+ }
248
+ }
249
+
250
+ walk(templateRoot, targetDir, '');
251
+
252
+ return { copiedFiles, skipped };
253
+ }
254
+
255
+ /** Required paths that must exist after copy (relative, posix-style). */
256
+ export const REQUIRED_AFTER_COPY = [
257
+ 'src',
258
+ 'public',
259
+ 'scripts',
260
+ 'package.json',
261
+ 'pnpm-lock.yaml',
262
+ '.npmrc',
263
+ 'pnpm-workspace.yaml',
264
+ 'astro.config.mjs',
265
+ 'tsconfig.json',
266
+ 'netlify.toml',
267
+ 'README.md',
268
+ 'AGENTS.md',
269
+ 'SKILL.md',
270
+ 'scripts/enforce-package-manager.cjs',
271
+ 'src/assets/images',
272
+ 'src/data/business.json',
273
+ // Smart-image capsule + agent skill (must scaffold with every client site)
274
+ 'tools/smart-image/package.json',
275
+ 'tools/smart-image/pnpm-workspace.yaml',
276
+ 'tools/smart-image/pnpm-lock.yaml',
277
+ 'tools/smart-image/run.mjs',
278
+ 'tools/smart-image/check.mjs',
279
+ '.agents/skills/smart-image-cli/SKILL.md',
280
+ ];
281
+
282
+ /**
283
+ * @param {string} targetDir
284
+ * @returns {string[]} missing relative paths
285
+ */
286
+ export function assertRequiredCopied(targetDir) {
287
+ /** @type {string[]} */
288
+ const missing = [];
289
+ for (const rel of REQUIRED_AFTER_COPY) {
290
+ const full = path.join(targetDir, ...rel.split('/'));
291
+ if (!fs.existsSync(full)) {
292
+ missing.push(rel);
293
+ }
294
+ }
295
+ return missing;
296
+ }