@pixelated-tech/components 3.9.16 → 3.9.17

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,590 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * create-pixelated-app.js
4
- *
5
- * Simple CLI to scaffold a new site from `pixelated-template`.
6
- * - copies the template to a destination folder
7
- * - clears out the .git history in the copy
8
- * - optionally initializes a fresh git repo and adds a remote
9
- *
10
- * TODOs (placeholders for later work):
11
- * - Create/patch pixelated.config.json and optionally run config:encrypt
12
- * - Run `npm ci` / `npm run lint` / `npm test` and optionally build
13
- * - Optionally create GitHub repo using API (with token from secure vault)
14
- *
15
- *
16
- 1) Recommended approach (short) āœ…
17
- Build a small Node-based CLI (easier cross-platform than a Bash script) called e.g. scripts/new-site.js or an npm create script.
18
- Make it interactive with sensible CLI flags (--name, --domain, --repo, --git, --encrypt, --no-install) and a non-interactive mode for CI.
19
-
20
- 2) High-level workflow the script should perform šŸ”
21
- Validate inputs (target path, site slug, repo name).
22
- Copy pixelated-template → ./<new-site-name> (preserve file modes).
23
- Replace placeholders in files (template tokens like {{SITE_NAME}}, {{PACKAGE_NAME}}, {{DOMAIN}}).
24
- Patch template-specific files (see list below).
25
- Remove template git metadata (rm -rf .git) and reset any CI state.
26
- Create/patch pixelated.config.json with site-specific values (ask for secrets). Optionally run npm run config:encrypt to write .enc.
27
- Run validation: npm ci (optional), npm run lint, npm test, npm run build.
28
- Init git, make initial commit, optionally create remote GitHub repo (if token available) and push.
29
- Print summary and next steps (e.g., configure hosting / DNS / deploy keys).
30
-
31
- 3) Files / fields to update in the template šŸ”§
32
- package.json — name, description, repository, author, homepage, version, scripts (if you want to change default scripts).
33
- README.md — project title and quick-start instructions.
34
- next.config.ts / amplify.yml — site-specific env entries and build steps.
35
- pixelated.config.json — update global/aws/contentful/google fields for this site (prefer *.enc workflow).
36
- src/app/(pages)/… and any site metadata files (site slug, default pagesDir).
37
- public/ assets and site-images.json — update site logos/URLs.
38
- certificates/ if using TLS — template may include placeholder paths.
39
- FEATURE_CHECKLIST.md — optionally update default checklist for new site.
40
- .gitignore — ensure pixelated.config.json is ignored if plaintext during setup.
41
-
42
- 4) Template hardening (recommended changes in pixelated-template) āš ļø
43
- Add placeholder tokens for the things above (e.g., {{PACKAGE_NAME}}, {{SITE_DOMAIN}}) rather than literal values.
44
- Add a template.json or template.meta that lists files & placeholders to auto-replace.
45
- Ensure the template does not include real secrets. Provide pixelated.config.json.example with placeholders and an example .env.local.example.
46
- Add a scripts/prepare-template.sh or test job that ensures no plaintext sensitive values remain.
47
- Document the creation process in TEMPLATE_README.md for maintainers.
48
-
49
- 5) Security & operational notes šŸ”
50
- Do not commit plaintext pixelated.config.json. If the CLI accepts secret values, optionally run npm run config:encrypt immediately and only commit the .enc if needed (prefer not to commit it; store in site secrets store).
51
- Add an option to write the encrypted file directly into dist/config when building a deployment bundle.
52
- Provide an audit step: scan resulting repo for secret patterns before final commit/push.
53
-
54
- 6) Helpful CLI implementation details (tools & libs) šŸ› ļø
55
- Use Node + these packages: fs-extra (copy), replace-in-file or simple .replace() for template tokens, inquirer (prompt), execa (run commands), simple-git (git ops), node-fetch or @octokit/rest (optional GitHub repo creation).
56
- Use safe file writes and atomic renames for config operations.
57
- Provide a --dry-run and --preview mode so users can verify changes before creating repo or pushing.
58
-
59
- 7) Validation & post-creation steps āœ…
60
- npm run lint && npm test && npm run build — fail fast and show errors for the new site.
61
- Optional: run npm run config:decrypt locally (with provided key) to confirm decryption works in your deploy workflow (BUT DO NOT store the key in the repo).
62
-
63
- 8) Example minimal CLI flow (pseudo)
64
- Prompt: site name, package name, repo URL (optional), domain, author, contentful tokens, aws keys (optional), encrypt?
65
- Copy template → replace tokens → create pixelated.config.json from pixelated.config.json.example → encrypt if requested → init git → run install/build → push to remote.
66
-
67
- */
68
-
69
- import fs from 'fs/promises';
70
- import path from 'path';
71
- import { exec as execCb } from 'child_process';
72
- import { promisify } from 'util';
73
- import readline from 'readline/promises';
74
- import { stdin as input, stdout as output } from 'process';
75
- import { fileURLToPath } from 'url';
76
- import { loadManifest, findTemplateForSlug, pruneTemplateDirs, printAvailableTemplates } from './create-pixelated-app-template-mapper.js';
77
-
78
- const __filename = fileURLToPath(import.meta.url);
79
- const __dirname = path.dirname(__filename);
80
-
81
- const exec = promisify(execCb);
82
- // Exportable exec wrapper so tests can stub it.
83
- export let _exec = exec;
84
-
85
- async function exists(p) {
86
- try {
87
- await fs.access(p);
88
- return true;
89
- } catch (e) {
90
- return false;
91
- }
92
- }
93
-
94
- async function countFiles(src) {
95
- let total = 0;
96
- async function walk(p) {
97
- const stat = await fs.lstat(p);
98
- if (stat.isDirectory()) {
99
- const items = await fs.readdir(p, { withFileTypes: true });
100
- for (const item of items) {
101
- await walk(path.join(p, item.name));
102
- }
103
- } else {
104
- total++;
105
- }
106
- }
107
- await walk(src);
108
- return total;
109
- }
110
-
111
- function startSpinner(messageFn) {
112
- if (!process.stdout.isTTY) return { stop: () => {} };
113
- const frames = ['-', '\\', '|', '/'];
114
- let i = 0;
115
- const interval = setInterval(() => {
116
- const msg = messageFn ? messageFn() : '';
117
- process.stdout.write(`\r${frames[i % frames.length]} ${msg}`);
118
- i++;
119
- }, 100);
120
- return {
121
- stop: () => {
122
- clearInterval(interval);
123
- process.stdout.write('\r');
124
- process.stdout.write('\n');
125
- }
126
- };
127
- }
128
-
129
- async function copyRecursive(src, dest, onFileCopied) {
130
- // Recursive copy that reports per-file progress via onFileCopied.
131
- await fs.mkdir(dest, { recursive: true });
132
- const items = await fs.readdir(src, { withFileTypes: true });
133
- for (const item of items) {
134
- const s = path.join(src, item.name);
135
- const d = path.join(dest, item.name);
136
- const stat = await fs.lstat(s);
137
- if (stat.isDirectory()) {
138
- await copyRecursive(s, d, onFileCopied);
139
- } else if (stat.isSymbolicLink()) {
140
- try {
141
- const link = await fs.readlink(s);
142
- await fs.symlink(link, d);
143
- if (onFileCopied) onFileCopied(d);
144
- } catch (e) {
145
- // ignore symlink failures
146
- if (onFileCopied) onFileCopied(d);
147
- }
148
- } else {
149
- // Regular file
150
- await fs.copyFile(s, d);
151
- if (onFileCopied) onFileCopied(d);
152
- }
153
- }
154
- }
155
-
156
- // Replace placeholders like {{SITE_NAME}}, {{SITE_URL}}, {{EMAIL_ADDRESS}} across the created site
157
- // This supports both literal tags and simple regex patterns (for cases where the bundler
158
- // transformed template tokens into JS expressions like {SITE_NAME}). Each replacement
159
- // entry may have { tag, value, isRegex } where isRegex indicates `tag` is a regex string.
160
- async function replacePlaceholders(rootDir, replacements) {
161
- const ignoreDirs = new Set(['.git', 'node_modules', 'dist', 'coverage']);
162
- let filesChanged = 0;
163
- async function walk(p) {
164
- const items = await fs.readdir(p, { withFileTypes: true });
165
- for (const item of items) {
166
- const entry = path.join(p, item.name);
167
- if (item.isDirectory()) {
168
- // Allow running against .next when explicitly targeting it, but skip when walking a template
169
- if (item.name === '.next' && rootDir !== '.next') continue;
170
- if (ignoreDirs.has(item.name)) continue;
171
- await walk(entry);
172
- } else {
173
- try {
174
- let content = await fs.readFile(entry, 'utf8');
175
- let newContent = content;
176
- for (const { tag, value, isRegex } of replacements) {
177
- let re;
178
- if (isRegex) {
179
- re = new RegExp(tag, 'g');
180
- } else {
181
- re = new RegExp(tag.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g');
182
- }
183
- newContent = newContent.replace(re, value);
184
- }
185
- if (newContent !== content) {
186
- await fs.writeFile(entry, newContent, 'utf8');
187
- filesChanged++;
188
- }
189
- } catch (e) {
190
- // Could be binary or unreadable - skip
191
- }
192
- }
193
- }
194
- }
195
- await walk(rootDir);
196
- return filesChanged;
197
- }
198
-
199
- // Token map used by the CLI: literal marker (e.g., "__SITE_NAME__") -> replacement value (populate during interactive prompts)
200
- export const TOKEN_MAP = {
201
- "__SITE_NAME__": '',
202
- "__SITE_URL__": '',
203
- "__EMAIL_ADDRESS__": ''
204
- };
205
-
206
- // Helper: add a route entry to the routes.json structure for a newly created page
207
- export function addRouteEntry(routesJson, pageSlug, displayName, rootDisplayName) {
208
- if (!routesJson || !Array.isArray(routesJson.routes)) return false;
209
- const candidatePath = `/${pageSlug}`;
210
- if (routesJson.routes.some(r => r.path === candidatePath)) return false;
211
- const name = displayName.split(/\s+/).map(s => s.charAt(0).toUpperCase() + s.slice(1)).join(' ');
212
- routesJson.routes.push({
213
- "name": name,
214
- "path": candidatePath,
215
- "title": `${rootDisplayName} - ${displayName}`,
216
- "description": "",
217
- "keywords": ""
218
- });
219
- return true;
220
- }
221
-
222
- // Copy a template page into the target directory, resolving to the expected location inside the workspace template.
223
- // Returns an object { used: 'template'|'fallback', src: <sourcePath> }
224
- export async function copyTemplateForPage(templatePathArg, templateSrc, templatePagesHome, targetDir) {
225
- const folderName = path.basename(templateSrc);
226
- const srcTemplatePath = path.join(templatePathArg, 'src', 'app', '(pages)', folderName);
227
- if (await exists(srcTemplatePath)) {
228
- await copyRecursive(srcTemplatePath, targetDir);
229
- return { used: 'template', src: srcTemplatePath };
230
- } else {
231
- await copyRecursive(templatePagesHome, targetDir);
232
- return { used: 'fallback', src: templatePagesHome };
233
- }
234
- }
235
-
236
-
237
- export async function createAndPushRemote(destPath, siteName, defaultOwner) {
238
- // Initialize a local git repo and make the initial commit
239
- await _exec('git init -b main', { cwd: destPath });
240
- await _exec('git add .', { cwd: destPath });
241
- await _exec('git commit -m "chore: initial commit from pixelated-template"', { cwd: destPath });
242
- console.log('āœ… Git initialized and initial commit created.');
243
-
244
- // If an encrypted config exists, attempt a non-fatal decrypt in the new site to ensure the token can be read
245
- const encCandidates = [
246
- path.join(destPath, 'src', 'app', 'config', 'pixelated.config.json.enc'),
247
- path.join(destPath, 'src', 'config', 'pixelated.config.json.enc'),
248
- path.join(destPath, 'src', 'pixelated.config.json.enc'),
249
- path.join(destPath, 'pixelated.config.json.enc'),
250
- path.join(destPath, 'dist', 'config', 'pixelated.config.json.enc')
251
- ];
252
- for (const p of encCandidates) {
253
- if (await exists(p)) {
254
- console.log(`Found encrypted config at ${p}. Attempting to run 'npm run config:decrypt' in the new site (non-fatal)`);
255
- try {
256
- await _exec('npm run config:decrypt', { cwd: destPath, timeout: 60_000 });
257
- console.log('Attempted config:decrypt (non-fatal)');
258
- } catch (err) {
259
- console.warn('config:decrypt failed or PIXELATED_CONFIG_KEY missing (non-fatal):', err?.message || err);
260
- }
261
- break;
262
- }
263
- }
264
-
265
- // Create a small temporary script inside the new site to reliably import the project's provider and print JSON to stdout
266
- const tmpDir = path.join(destPath, '.px-scripts');
267
- const tmpFile = path.join(tmpDir, 'get_github_token.ts');
268
- await fs.mkdir(tmpDir, { recursive: true });
269
- const tmpContent = `import('./src/components/config/config').then(m => {
270
- const cfg = m.getFullPixelatedConfig();
271
- // Only print the github object (or null) as JSON to stdout
272
- console.log(JSON.stringify(cfg?.github || null));
273
- }).catch(e => {
274
- console.error('ERR_IMPORT', e?.message || e);
275
- process.exit(2);
276
- });`;
277
- await fs.writeFile(tmpFile, tmpContent, 'utf8');
278
-
279
- let execOut = null;
280
- try {
281
- execOut = await _exec(`npx tsx ${tmpFile}`, { cwd: destPath, timeout: 60_000 });
282
- } catch (e) {
283
- // Provide a helpful error message and ensure cleanup happens below
284
- console.error('āŒ Failed to run config provider to obtain GitHub token. Ensure PIXELATED_CONFIG_KEY is available (e.g., in .env.local) and the site includes an encrypted pixelated.config.json.enc');
285
- throw e;
286
- } finally {
287
- // Always clean up the temporary script directory
288
- try { await fs.rm(tmpDir, { recursive: true, force: true }); } catch (_) { /* ignore cleanup errors */ }
289
- }
290
-
291
- const outStr = (execOut && execOut.stdout) ? String(execOut.stdout).trim() : '';
292
- if (!outStr) {
293
- console.error('āŒ No output from config provider; cannot locate github token');
294
- throw new Error('Missing provider output');
295
- }
296
-
297
- let githubInfo = null;
298
- try { githubInfo = JSON.parse(outStr); } catch (e) { console.error('āŒ Invalid JSON from config provider:', outStr); throw e; }
299
- const token = githubInfo?.token;
300
- const cfgOwner = githubInfo?.defaultOwner;
301
- if (!token) {
302
- console.error('āŒ github.token not found in decrypted config; cannot create remote repo.');
303
- throw new Error('Missing github.token');
304
- }
305
-
306
- const repoName = siteName;
307
- const ownerForMessage = cfgOwner || defaultOwner;
308
- console.log(`Creating GitHub repo: ${ownerForMessage}/${repoName} ...`);
309
-
310
- let resp;
311
- try {
312
- resp = await fetch('https://api.github.com/user/repos', {
313
- method: 'POST',
314
- headers: {
315
- 'Authorization': `token ${token}`,
316
- 'Content-Type': 'application/json',
317
- 'User-Agent': 'create-pixelated-app'
318
- },
319
- body: JSON.stringify({ name: repoName, private: false })
320
- });
321
- } catch (e) {
322
- console.error('āŒ Failed to call GitHub API', e?.message || e);
323
- throw e;
324
- }
325
-
326
- const body = await (async () => { try { return await resp.json(); } catch (e) { return null; } })();
327
- if (!resp.ok) {
328
- console.error(`āŒ Failed to create GitHub repo: ${resp.status} ${resp.statusText} ${body?.message || ''}`);
329
- throw new Error('GitHub repo creation failed');
330
- }
331
- const cloneUrl = body.clone_url;
332
- if (!cloneUrl) {
333
- console.error('āŒ GitHub returned unexpected response (no clone_url)');
334
- throw new Error('Invalid GitHub response');
335
- }
336
-
337
- // Add remote and push
338
- await _exec(`git remote add origin ${cloneUrl}`, { cwd: destPath });
339
- await _exec('git branch --show-current || git branch -M main', { cwd: destPath });
340
- await _exec('git push -u origin main', { cwd: destPath });
341
- console.log(`āœ… Remote created and initial commit pushed: ${cloneUrl}`);
342
- }
343
- async function main() {
344
- const rl = readline.createInterface({ input, output });
345
- try {
346
- console.log('\nšŸ“¦ Pixelated site creator — scaffold a new site from pixelated-template\n');
347
-
348
- const defaultName = 'my-site';
349
- const siteName = (await rl.question(`Root directory name (kebab-case) [${defaultName}]: `)) || defaultName;
350
-
351
- // Display name used in route titles: convert kebab to Title Case
352
- const rootDisplayName = siteName.split(/[-_]+/).map(s => s.charAt(0).toUpperCase() + s.slice(1)).join(' ');
353
-
354
- // Additional site metadata for placeholder substitution
355
- const siteUrl = (await rl.question('Site URL (e.g. https://example.com) [leave blank to skip]: ')).trim();
356
- const emailAddress = (await rl.question('Contact email address [leave blank to skip]: ')).trim();
357
-
358
- const workspaceRoot = path.resolve(__dirname, '..', '..', '..');
359
- const templatePath = path.resolve(workspaceRoot, 'pixelated-template');
360
- if (!(await exists(templatePath))) {
361
- console.error(`\nāŒ Template not found at ${templatePath}. Please ensure this tool is run inside the workspace that contains pixelated-template.`);
362
- process.exit(1);
363
- }
364
-
365
- // Load manifest (if present)
366
- const manifest = await loadManifest(__dirname);
367
- // Note: available templates will be printed later just before prompting for pages
368
-
369
-
370
- // Destination is implicitly the top-level Git folder + site name to avoid prompting for it
371
- const destPath = path.resolve(workspaceRoot, siteName);
372
- console.log(`\nThe new site will be created at: ${destPath}`);
373
- const proceed = (await rl.question('Proceed? (Y/n): ')) || 'y';
374
- if (proceed.toLowerCase() !== 'y' && proceed.toLowerCase() !== 'yes') {
375
- console.log('Aborting.');
376
- process.exit(0);
377
- }
378
-
379
- if (await exists(destPath)) {
380
- const shouldOverwrite = (await rl.question(`Destination ${destPath} already exists. Overwrite? (y/N): `)).toLowerCase();
381
- if (shouldOverwrite !== 'y' && shouldOverwrite !== 'yes') {
382
- console.log('Aborting. Choose another destination.');
383
- process.exit(0);
384
- }
385
- console.log(`Removing existing directory ${destPath}...`);
386
- await fs.rm(destPath, { recursive: true, force: true });
387
- }
388
-
389
- console.log(`\nCopying template from ${templatePath} -> ${destPath} ...`);
390
- const totalFiles = await countFiles(templatePath);
391
- let filesCopied = 0;
392
- let lastFile = '';
393
- const spinner = startSpinner(() => `Copying... ${filesCopied}/${totalFiles} ${lastFile ? '- ' + path.basename(lastFile) : ''}`);
394
- await copyRecursive(templatePath, destPath, (f) => { if (f) { filesCopied++; lastFile = f; } });
395
- // If fs.cp was used, per-file callbacks won't have been called; ensure we report the full total
396
- if (filesCopied < totalFiles) filesCopied = totalFiles;
397
- spinner.stop();
398
- console.log(`āœ… Template files copied (${filesCopied} files).`);
399
-
400
- // Remove git history
401
- const gitDir = path.join(destPath, '.git');
402
- if (await exists(gitDir)) {
403
- console.log('Removing .git directory from new site...');
404
- await fs.rm(gitDir, { recursive: true, force: true });
405
- }
406
-
407
- // Pages prompt: show available templates and ask which pages to create (comma-separated)
408
- if (manifest && Array.isArray(manifest.templates) && manifest.templates.length) {
409
- printAvailableTemplates(manifest);
410
- }
411
- const pagesInput = (await rl.question('Pages to create (comma-separated, e.g. about,contact) [leave blank to skip]: ')).trim();
412
- let pagesToCreate = [];
413
- let existingPages = [];
414
- if (pagesInput) {
415
- const raw = pagesInput.split(',').map(s => s.trim()).filter(Boolean);
416
- // sanitize and normalize
417
- const seen = new Set();
418
- for (const r of raw) {
419
- const lower = r.toLowerCase();
420
- if (lower === 'home' || lower === 'index') {
421
- existingPages.push('home');
422
- continue;
423
- }
424
- let slug = r.trim().toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, '');
425
- if (!slug) continue;
426
- if (seen.has(slug)) continue;
427
- seen.add(slug);
428
- const matchedTemplate = findTemplateForSlug(manifest, slug);
429
- pagesToCreate.push({ slug, displayName: r.trim(), template: matchedTemplate });
430
- }
431
-
432
- console.log('\nSummary of pages:');
433
- if (existingPages.length) console.log(` - Existing (skipped): ${existingPages.join(', ')}`);
434
- if (pagesToCreate.length) {
435
- console.log(' - To be created:');
436
- for (const p of pagesToCreate) {
437
- if (p.template) {
438
- console.log(` - ${p.slug} (mapped to template: ${p.template.name})`);
439
- } else {
440
- console.log(` - ${p.slug} (no template match)`);
441
- }
442
- }
443
- }
444
- const proceedPages = (await rl.question('Proceed to create these pages? (Y/n): ')) || 'y';
445
- if (proceedPages.toLowerCase() === 'y' || proceedPages.toLowerCase() === 'yes') {
446
- // perform creation
447
- const templatePagesHome = path.join(templatePath, 'src', 'app', '(pages)', '(home)');
448
- const siteRoutesFile = path.join(destPath, 'src', 'app', 'data', 'routes.json');
449
- let routesJson = null;
450
- try {
451
- routesJson = JSON.parse(await fs.readFile(siteRoutesFile, 'utf8'));
452
- // Ensure siteInfo exists and set its name to the root display name
453
- routesJson.siteInfo = routesJson.siteInfo || {};
454
- routesJson.siteInfo.name = rootDisplayName;
455
- } catch (e) {
456
- console.warn('āš ļø Could not read routes.json, routes will not be updated.');
457
- }
458
-
459
- for (const p of pagesToCreate) {
460
- const targetDir = path.join(destPath, 'src', 'app', '(pages)', p.slug);
461
- console.log(`Creating page ${p.slug} -> ${targetDir}`);
462
- let copyResult = null;
463
- if (p.template && p.template.src) {
464
- copyResult = await copyTemplateForPage(templatePath, p.template.src, templatePagesHome, targetDir);
465
- if (copyResult.used === 'template') {
466
- console.log(` - Copied template ${p.template.name} from ${copyResult.src}`);
467
- } else {
468
- console.warn(`āš ļø Template source ${path.join(templatePath, 'src', 'app', '(pages)', path.basename(p.template.src))} not found; using default page template instead.`);
469
- }
470
- } else {
471
- await copyRecursive(templatePagesHome, targetDir);
472
- }
473
- // rename component in page.tsx
474
- const pageFile = path.join(targetDir, 'page.tsx');
475
- try {
476
- let content = await fs.readFile(pageFile, 'utf8');
477
- const compName = p.displayName.replace(/[^a-zA-Z0-9]+/g,' ').split(/\s+/).map(s=>s.charAt(0).toUpperCase()+s.slice(1)).join('') + 'Page';
478
- content = content.replace(/export default function\s+\w+\s*\(/, `export default function ${compName}(`);
479
- await fs.writeFile(pageFile, content, 'utf8');
480
- console.log(` - Updated component name to ${compName} in ${path.relative(destPath, pageFile)}`);
481
- } catch (e) {
482
- console.warn(`āš ļø Failed to update component name for ${p.slug}:`, e?.message || e);
483
- }
484
-
485
- // update routes.json
486
- if (routesJson && Array.isArray(routesJson.routes)) {
487
- // Skip if route path already exists
488
- const candidatePath = `/${p.slug}`;
489
- if (!routesJson.routes.some(r => r.path === candidatePath)) {
490
- routesJson.routes.push({
491
- "name": p.displayName.split(/\s+/).map(s=>s.charAt(0).toUpperCase()+s.slice(1)).join(' '),
492
- "path": candidatePath,
493
- "title": `${rootDisplayName} - ${p.displayName}`,
494
- "description": "",
495
- "keywords": ""
496
- });
497
- } else {
498
- console.log(` - Route ${candidatePath} already exists; skipping route add.`);
499
- }
500
- }
501
- }
502
-
503
- if (manifest) {
504
- const removed = await pruneTemplateDirs(manifest, destPath, pagesToCreate.map(p=>p.slug));
505
- for (const r of removed) {
506
- console.log(`Removed unused template page ${r} from new site...`);
507
- }
508
- }
509
-
510
- if (routesJson) {
511
- try {
512
- await fs.writeFile(siteRoutesFile, JSON.stringify(routesJson, null, '\t'), 'utf8');
513
- console.log('āœ… routes.json updated.');
514
- } catch (e) {
515
- console.warn('āš ļø Failed to write routes.json:', e?.message || e);
516
- }
517
- }
518
- } else {
519
- console.log('Skipping page creation.');
520
- }
521
- }
522
- // Automatically replace double-underscore template placeholders (e.g., __SITE_NAME__) with provided values
523
- const replacements = {};
524
- if (rootDisplayName) replacements.SITE_NAME = rootDisplayName;
525
- if (siteUrl) replacements.SITE_URL = siteUrl;
526
- if (emailAddress) replacements.EMAIL_ADDRESS = emailAddress;
527
- if (Object.keys(replacements).length) {
528
- const replArray = [];
529
- for (const [t, valRaw] of Object.entries(replacements)) {
530
- const val = String(valRaw);
531
- // populate TOKEN_MAP so other code can inspect token->value mapping (keyed by literal marker)
532
- const marker = `__${t}__`;
533
- TOKEN_MAP[marker] = val;
534
- // First, replace expression occurrences like {__TOKEN__} with a quoted string expression to avoid bare identifiers
535
- replArray.push({ tag: `\\{${marker}\\}`, value: `{${JSON.stringify(val)}}`, isRegex: true });
536
- // Then, replace literal marker occurrences (e.g., __TOKEN__) with the plain value
537
- replArray.push({ tag: marker, value: val });
538
- }
539
- try {
540
- const changed = await replacePlaceholders(destPath, replArray);
541
- console.log(`āœ… Replaced template placeholders in ${changed} files under ${destPath}`);
542
- } catch (e) {
543
- console.warn('āš ļø Failed to replace placeholders in site copy:', e?.message || e);
544
- }
545
- }
546
- // Prompt about creating a new GitHub repository. Default owner is read from components config `github.defaultOwner` (fallback: 'brianwhaley')
547
- const componentsCfgPath = path.resolve(__dirname, '..', 'config', 'pixelated.config.json');
548
- let defaultOwner = 'brianwhaley';
549
- try {
550
- if (await exists(componentsCfgPath)) {
551
- const compCfgText = await fs.readFile(componentsCfgPath, 'utf8');
552
- const compCfg = JSON.parse(compCfgText);
553
- if (compCfg?.github?.defaultOwner) defaultOwner = compCfg.github.defaultOwner;
554
- }
555
- } catch (e) {
556
- // ignore and use fallback
557
- }
558
- const createRemoteAnswer = (await rl.question(`Create a new GitHub repository in '${defaultOwner}' and push the initial commit? (Y/n): `)) || 'y';
559
- if (createRemoteAnswer.toLowerCase() === 'y' || createRemoteAnswer.toLowerCase() === 'yes') {
560
- try {
561
- await createAndPushRemote(destPath, siteName, defaultOwner);
562
- } catch (e) {
563
- console.warn('āš ļø Repo creation or git push failed. Your local repository is still available at:', destPath);
564
- console.warn(e?.stderr || e?.message || e);
565
- }
566
- }
567
-
568
-
569
-
570
- console.log('\nšŸŽ‰ Done. Summary:');
571
- console.log(` - Site copied to: ${destPath}`);
572
- console.log('\nNote: A git remote was not set by this script. You can add one later with `git remote add origin <url>` if desired.');
573
- console.log('\nNext recommended steps (manual or to be automated in future):');
574
- console.log(' - Update pixelated.config.json for this site and encrypt it with your config tool');
575
- console.log(' - Run `npm run lint`, `npm test`, and `npm run build` inside the new site and fix any issues');
576
- console.log(' - Create GitHub repo (if not already created), push main branch, and set up CI/deploy secrets');
577
- } catch (err) {
578
- console.error('Unexpected error:', err);
579
- process.exit(1);
580
- } finally {
581
- rl.close();
582
- }
583
- }
584
-
585
- if (typeof process !== 'undefined' && new URL(import.meta.url).pathname === process.argv[1]) {
586
- // CLI entry point: run the interactive main flow
587
- main();
588
- }
589
-
590
- }