basebrick-bricklayer 1.0.1 → 1.2.0

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 CHANGED
@@ -8,10 +8,22 @@ Bricklayer is the core static site generator (JamBrick) for BaseBrick. It is des
8
8
  - **Dynamic Content Fetching**: Automatically fetches content from a headless CMS based on configuration (`components/generic.json`) and generates individual pages for each content item with clean, SEO-friendly slugs.
9
9
  - **Production Optimization**: In production mode (`--prod`), it minifies HTML and CSS, and compresses image assets using `sharp`.
10
10
  - **Tailwind Integration**: Seamlessly builds Tailwind CSS using `@tailwindcss/cli`, supporting development and production (minified) builds.
11
+ - **Environment Variables**: Natively loads `.env` and `.dev.vars` (Cloudflare) files, makes variables accessible globally in Nunjucks templates, and interpolates variables in `generic.json`.
12
+ - **Automated Deployments**: Scaffolding optionally generates tailored GitHub Actions (`deploy.yml`) workflows for seamless CI/CD.
13
+ - **Cloudflare Native**: Integrates native Cloudflare Workers support, automatically configuring `deploy:prod` and `deploy:preview` environments mapped to `wrangler.toml`.
14
+ - **Centralized Management**: Includes a `bricklayer manage` command to securely register and sync your local `.basebrick.config` settings with a central Manager API.
11
15
 
12
16
  ## Installation
13
17
 
14
- Since Bricklayer is a local module within the BaseBrick ecosystem, you can link it directly:
18
+ To create a new Bricklayer project with automatic scaffolding, run the `init` command:
19
+
20
+ ```bash
21
+ npx bricklayer init
22
+ ```
23
+
24
+ This interactive setup will create the default folder structure and ask if you'd like to scaffold a demo site with starter templates, as well as configure Sonic JS CMS integration automatically.
25
+
26
+ Alternatively, if you are working within the BaseBrick ecosystem locally, you can link it:
15
27
 
16
28
  ```bash
17
29
  cd bricklayer
@@ -37,6 +49,12 @@ To run a production build (minifies HTML/CSS, compresses images):
37
49
  bricklayer --prod
38
50
  ```
39
51
 
52
+ To sync your project settings with a central Bricklayer Manager instance:
53
+
54
+ ```bash
55
+ bricklayer manage
56
+ ```
57
+
40
58
  ### Module
41
59
 
42
60
  ```javascript
@@ -74,11 +92,39 @@ To enable remote CMS fetching, place a `generic.json` configuration file in `src
74
92
  - `indexPage`: The template (e.g., `blog.njk`) where an array of posts will be injected under the `posts` variable.
75
93
  - `postPage`: The template (e.g., `post.njk`) that will be used to generate individual pages for each item fetched from the API. The generated HTML will be placed in a directory matching the `postPage` name (e.g., `public/post/my-slug.html`).
76
94
 
95
+ ## Environment Variables
96
+
97
+ Bricklayer natively supports reading `.env` and `.dev.vars` (Cloudflare Pages) files from the project's root directory.
98
+
99
+ - **Nunjucks Templates:** Variables are automatically passed to Nunjucks templates under the global `env` object. E.g., `{{ env.MY_API_KEY }}`.
100
+ - **CMS Configuration:** Environment variables can be directly interpolated in `generic.json` strings using the `${VARIABLE_NAME}` syntax.
101
+
102
+ ## Bricklayer Manager
103
+
104
+ Bricklayer includes a centralized dashboard to help you track and administer all your statically generated sites.
105
+
106
+ The **Bricklayer Manager** allows you to:
107
+ - View all deployed Bricklayer sites via an elegant web UI.
108
+ - Automatically track and store Cloudflare Worker endpoints.
109
+ - Access deep links directly to Cloudflare Dashboards for easy debugging.
110
+
111
+ You can find the source code and instructions for deploying your own manager instance in the `manager/` directory of the core [BrickLayer GitHub Repository](https://github.com/cryptoskillz/BrickLayer/tree/main/manager).
112
+
113
+ Once your manager is deployed, use the CLI command to securely sync any of your projects:
114
+
115
+ ```bash
116
+ bricklayer manage
117
+ ```
118
+
77
119
  ## Directory Structure
78
120
 
79
121
  Bricklayer expects the following default structure (overridable via options):
80
122
 
81
- ```
123
+ ```text
124
+ ├── .github/
125
+ │ └── workflows/
126
+ │ └── deploy.yml # Auto-generated GitHub Actions
127
+ ├── cms/ # (Optional) Cloned Sonic JS CMS
82
128
  ├── src/
83
129
  │ ├── _includes/ # Nunjucks layouts
84
130
  │ ├── assets/ # Static assets (images, fonts, tailwind)
@@ -86,5 +132,8 @@ Bricklayer expects the following default structure (overridable via options):
86
132
  │ ├── index.njk # Pages
87
133
  │ └── post.njk
88
134
  ├── public/ # Build output
135
+ ├── .basebrick.config # Bricklayer project settings
136
+ ├── .gitignore # Version control exclusions
137
+ ├── wrangler.toml # (Optional) Cloudflare configuration
89
138
  └── package.json
90
139
  ```
package/bin/bricklayer.js CHANGED
@@ -1,6 +1,63 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { buildSite } from '../index.js';
4
+ import { initProject } from './init.js';
4
5
 
5
- const isProd = process.argv.includes('--prod');
6
- buildSite({ isProd, cwd: process.cwd() }).catch(console.error);
6
+ const args = process.argv.slice(2);
7
+
8
+ if (args.includes('help') || args.includes('--help') || args.includes('-h')) {
9
+ console.log(`
10
+ 🧱 Bricklayer CLI 🧱
11
+
12
+ Usage: bricklayer [command] [options]
13
+
14
+ Commands:
15
+ (empty) Run a standard development build
16
+ init Scaffold a new project (Demo, CMS, Cloudflare API)
17
+ manage Register this project with a Bricklayer Manager
18
+ deploy Deploy the project to Cloudflare and register its URL
19
+ help Show this help message
20
+
21
+ Options:
22
+ --prod Run a production build (minifies HTML/CSS, compresses images)
23
+
24
+ Examples:
25
+ bricklayer init Initialize a project with a demo site, CMS, & Cloudflare Workers API
26
+ bricklayer manage Send project configuration to your central Bricklayer Manager
27
+ bricklayer Builds the site in development mode
28
+ bricklayer --prod Builds the site with production optimizations
29
+
30
+ Generated NPM Scripts (after init):
31
+ npm run start Test your Cloudflare Worker locally
32
+ npm run deploy:preview Deploy to a Cloudflare preview environment
33
+ npm run deploy:prod Deploy to Cloudflare production
34
+ `);
35
+ process.exit(0);
36
+ }
37
+
38
+ if (args.includes('init')) {
39
+ initProject(process.cwd()).catch(console.error);
40
+ } else if (args.includes('deploy')) {
41
+ const preview = args.includes('--preview');
42
+ import('./deploy.js').then(m => m.deployProject(process.cwd(), { preview })).catch(console.error);
43
+ } else if (args.includes('manage')) {
44
+ const reconfigure = args.includes('-c') || args.includes('--config');
45
+ let url = null;
46
+ let token = null;
47
+
48
+ const uIndex = args.findIndex(a => a === '-u' || a === '--url');
49
+ if (uIndex !== -1 && args[uIndex + 1]) url = args[uIndex + 1];
50
+
51
+ const tIndex = args.findIndex(a => a === '-t' || a === '--token');
52
+ if (tIndex !== -1 && args[tIndex + 1]) token = args[tIndex + 1];
53
+
54
+ import('./manage.js').then(m => m.manageProject(process.cwd(), { reconfigure, url, token })).catch(console.error);
55
+ } else {
56
+ const isProd = args.includes('--prod');
57
+ buildSite({ isProd, cwd: process.cwd() })
58
+ .then(() => process.exit(0))
59
+ .catch(err => {
60
+ console.error(err);
61
+ process.exit(1);
62
+ });
63
+ }
package/bin/deploy.js ADDED
@@ -0,0 +1,93 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { spawn } from 'child_process';
4
+
5
+ export async function deployProject(cwd, options = {}) {
6
+ const isPreview = options.preview || false;
7
+
8
+ console.log(`\n🚀 Deploying to Cloudflare ${isPreview ? 'Preview' : 'Production'}...\n`);
9
+
10
+ const args = ['deploy'];
11
+ if (isPreview) {
12
+ args.push('--env', 'preview');
13
+ }
14
+
15
+ const tmpLog = path.join(cwd, '.wrangler-deploy.log');
16
+
17
+ let commandStr;
18
+ if (process.platform === 'darwin') {
19
+ commandStr = `script -q ${tmpLog} npx wrangler ${args.join(' ')}`;
20
+ } else if (process.platform === 'linux') {
21
+ commandStr = `script -q -c "npx wrangler ${args.join(' ')}" ${tmpLog}`;
22
+ } else {
23
+ // Windows fallback
24
+ commandStr = `npx wrangler ${args.join(' ')} > ${tmpLog} 2>&1`;
25
+ }
26
+
27
+ const child = spawn(commandStr, {
28
+ cwd,
29
+ stdio: 'inherit',
30
+ shell: true
31
+ });
32
+
33
+ child.on('close', async (code) => {
34
+ let outputData = '';
35
+ if (fs.existsSync(tmpLog)) {
36
+ outputData = fs.readFileSync(tmpLog, 'utf8');
37
+ // Remove ANSI color codes
38
+ outputData = outputData.replace(/\x1B\[[0-9;]*[a-zA-Z]/g, '');
39
+ // Clean up the log file
40
+ try { fs.unlinkSync(tmpLog); } catch (e) {}
41
+ }
42
+
43
+ if (code === 0) {
44
+ // Try to extract URL from wrangler output
45
+ const urlMatch = outputData.match(/https:\/\/[a-zA-Z0-9-]+\.[a-zA-Z0-9-]+\.workers\.dev/i) ||
46
+ outputData.match(/https:\/\/[a-zA-Z0-9-]+\.[a-zA-Z0-9-]+\.pages\.dev/i) ||
47
+ outputData.match(/https:\/\/[a-zA-Z0-9-]+\.[a-zA-Z0-9-]+\.cloudflare\.com/i) ||
48
+ outputData.match(/(https:\/\/[^\s"]+)/i);
49
+
50
+ let deployedUrl = null;
51
+ const lines = outputData.split('\n');
52
+ for (const line of lines) {
53
+ if (line.includes('workers.dev') || line.includes('pages.dev')) {
54
+ const match = line.match(/(https:\/\/[^\s"]+)/i);
55
+ if (match) deployedUrl = match[1];
56
+ }
57
+ }
58
+
59
+ // Fallback to the first URL we found if we didn't find a worker specific one
60
+ if (!deployedUrl && urlMatch) {
61
+ deployedUrl = urlMatch[1] || urlMatch[0];
62
+ }
63
+
64
+ if (deployedUrl) {
65
+ console.log(`\n✅ Deployed successfully to: ${deployedUrl}`);
66
+
67
+ const localConfigPath = path.join(cwd, '.basebrick.config');
68
+ if (fs.existsSync(localConfigPath)) {
69
+ try {
70
+ const projectConfig = JSON.parse(fs.readFileSync(localConfigPath, 'utf8'));
71
+ projectConfig.url = deployedUrl;
72
+ projectConfig.environment = isPreview ? 'Preview' : 'Production';
73
+
74
+ fs.writeFileSync(localConfigPath, JSON.stringify(projectConfig, null, 2));
75
+ console.log('✅ Added deployed URL to .basebrick.config');
76
+
77
+ // Automatically push the new config to the manager
78
+ console.log('\n🔄 Automatically registering new URL with Bricklayer Manager...');
79
+ const manageModule = await import('./manage.js');
80
+ await manageModule.manageProject(cwd, {});
81
+ } catch(e) {
82
+ console.error('❌ Failed to update .basebrick.config with URL:', e);
83
+ }
84
+ }
85
+ } else {
86
+ console.log('\n✅ Deployed successfully (Could not extract URL automatically)');
87
+ }
88
+ } else {
89
+ console.error(`\n❌ Deployment failed with exit code ${code}`);
90
+ process.exit(1);
91
+ }
92
+ });
93
+ }
package/bin/init.js ADDED
@@ -0,0 +1,399 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import readline from 'readline';
4
+ import { execSync } from 'child_process';
5
+
6
+ const rl = readline.createInterface({
7
+ input: process.stdin,
8
+ output: process.stdout
9
+ });
10
+
11
+ const question = (query) => new Promise((resolve) => rl.question(query, resolve));
12
+
13
+ export async function initProject(cwd) {
14
+ console.log('\n🧱 Welcome to Bricklayer Initialization 🧱\n');
15
+
16
+ const confirm = await question('Initialize a new Bricklayer project in the current directory? (Y/n) ');
17
+ if (confirm.toLowerCase() === 'n') {
18
+ console.log('Initialization cancelled.');
19
+ rl.close();
20
+ return;
21
+ }
22
+
23
+ const existingFiles = fs.readdirSync(cwd).filter(f => f !== '.git' && f !== '.DS_Store');
24
+ if (existingFiles.length > 0) {
25
+ const wipe = await question('Warning: This directory is not empty. Would you like to wipe existing files before scaffolding? (y/N) ');
26
+ if (wipe.toLowerCase() === 'y') {
27
+ console.log('Wiping directory...');
28
+ for (const file of existingFiles) {
29
+ fs.rmSync(path.join(cwd, file), { recursive: true, force: true });
30
+ }
31
+ console.log('Directory wiped.\n');
32
+ }
33
+ }
34
+
35
+ const projectNameRaw = await question('What is the name of your project? (bricklayer-site) ');
36
+ const projectName = projectNameRaw.trim() || 'bricklayer-site';
37
+
38
+ const projectDescriptionRaw = await question('Project description (optional): ');
39
+ const projectDescription = projectDescriptionRaw.trim() || '';
40
+
41
+ const githubRepoRaw = await question('What is the GitHub repository URL? (leave blank for none) ');
42
+ const githubRepo = githubRepoRaw.trim();
43
+
44
+ const demoSite = await question('Would you like to scaffold a demo site with starter templates? (Y/n) ');
45
+ const includeDemo = demoSite.toLowerCase() !== 'n';
46
+
47
+ const sonicJs = await question('Would you like to configure Sonic JS CMS integration? (Y/n) ');
48
+ const includeSonic = sonicJs.toLowerCase() !== 'n';
49
+
50
+ let pullSonic = false;
51
+ if (includeSonic) {
52
+ const pull = await question('Do you want to pull Sonic.js into the cms/ folder? (y/N) ');
53
+ pullSonic = pull.toLowerCase() === 'y';
54
+ }
55
+
56
+ const cloudflare = await question('Would you like to start with Cloudflare? (Y/n) ');
57
+ const includeCloudflare = cloudflare.toLowerCase() !== 'n';
58
+ let cfFramework = '';
59
+ if (includeCloudflare) {
60
+ const fw = await question('Which API framework? (1)hono or (2)vanilla? (1/2) ');
61
+ cfFramework = fw.trim() === '1' ? 'hono' : 'vanilla';
62
+ }
63
+
64
+ const deployYml = await question('Would you like to generate a GitHub Actions deploy.yml? (Y/n) ');
65
+ const createDeploy = deployYml.toLowerCase() !== 'n';
66
+
67
+ console.log('\nScaffolding project...');
68
+
69
+ // 1. Create directory structure
70
+ const dirs = [
71
+ 'src',
72
+ 'src/_includes',
73
+ 'src/assets',
74
+ 'src/assets/images',
75
+ 'src/assets/tailwind',
76
+ 'src/components',
77
+ 'public'
78
+ ];
79
+
80
+ dirs.forEach(dir => {
81
+ const dirPath = path.join(cwd, dir);
82
+ if (!fs.existsSync(dirPath)) {
83
+ fs.mkdirSync(dirPath, { recursive: true });
84
+ console.log(` Created ${dir}/`);
85
+ }
86
+ });
87
+
88
+ // Download dancing ninja
89
+ const ninjaPath = path.join(cwd, 'src/assets/images/ninja-dance.gif');
90
+ if (!fs.existsSync(ninjaPath)) {
91
+ try {
92
+ console.log(' Downloading dancing ninja asset...');
93
+ const response = await fetch('https://www.rfgeneration.com/images/collections/gamepopper101/bitdance.gif', {
94
+ headers: { 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)' }
95
+ });
96
+ const buffer = await response.arrayBuffer();
97
+ fs.writeFileSync(ninjaPath, Buffer.from(buffer));
98
+ } catch (e) {
99
+ console.log(' Failed to download ninja gif.');
100
+ }
101
+ }
102
+
103
+ // 2. Create base CSS
104
+ const cssPath = path.join(cwd, 'src/assets/tailwind/input.css');
105
+ if (!fs.existsSync(cssPath)) {
106
+ fs.writeFileSync(cssPath, `@import "tailwindcss";\n\n@source "../../**/*.njk";\n`);
107
+ console.log(' Created src/assets/tailwind/input.css');
108
+ }
109
+
110
+ if (includeDemo) {
111
+ // Base layout
112
+ const layoutPath = path.join(cwd, 'src/_includes/base.njk');
113
+ if (!fs.existsSync(layoutPath)) {
114
+ fs.writeFileSync(layoutPath, `<!DOCTYPE html>
115
+ <html lang="en">
116
+ <head>
117
+ <meta charset="UTF-8">
118
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
119
+ <title>{{ title | default("Bricklayer Site") }}</title>
120
+ <link rel="stylesheet" href="/assets/css/style.css">
121
+ </head>
122
+ <body class="bg-gray-50 text-gray-900 font-sans p-8">
123
+ <main class="max-w-4xl mx-auto">
124
+ {{ content | safe }}
125
+ </main>
126
+ </body>
127
+ </html>`);
128
+ console.log(' Created src/_includes/base.njk');
129
+ }
130
+
131
+ const indexPath = path.join(cwd, 'src/index.njk');
132
+ if (!fs.existsSync(indexPath)) {
133
+ fs.writeFileSync(indexPath, `---
134
+ title: Welcome to Bricklayer
135
+ layout: base
136
+ ---
137
+ <h1 class="text-4xl font-bold text-blue-600 mb-4">Welcome to Bricklayer</h1>
138
+ <p class="text-lg">Your statically generated JamBrick site is ready.</p>
139
+ ${includeSonic ? '<p class="mt-4 mb-4"><a href="/blog.html" class="text-blue-500 underline">View the CMS Blog</a></p>' : ''}
140
+ <div class="mt-8">
141
+ <img src="/assets/images/ninja-dance.gif" alt="Dancing Ninja" class="rounded-lg shadow-md max-w-xs" />
142
+ </div>
143
+ `);
144
+ console.log(' Created src/index.njk');
145
+ }
146
+ }
147
+
148
+ if (includeSonic) {
149
+ // generic.json
150
+ const genericPath = path.join(cwd, 'src/components/generic.json');
151
+ if (!fs.existsSync(genericPath)) {
152
+ fs.writeFileSync(genericPath, JSON.stringify({
153
+ name: "sonic.js",
154
+ apiUrl: "/v1/posts",
155
+ productionUrl: "${PROD_CMS_URL}",
156
+ locaLUrl: "http://localhost:3018",
157
+ indexPage: "blog",
158
+ postPage: "post"
159
+ }, null, 2));
160
+ console.log(' Created src/components/generic.json');
161
+ }
162
+
163
+ // Environment files
164
+ const envPath = path.join(cwd, '.env');
165
+ if (!fs.existsSync(envPath)) {
166
+ fs.writeFileSync(envPath, `PROD_CMS_URL=https://cms.basebrick.xyz\n`);
167
+ console.log(' Created .env file');
168
+ }
169
+
170
+ if (includeDemo) {
171
+ // Blog Listing
172
+ const blogPath = path.join(cwd, 'src/blog.njk');
173
+ if (!fs.existsSync(blogPath)) {
174
+ fs.writeFileSync(blogPath, `---
175
+ title: Blog
176
+ layout: base
177
+ ---
178
+ <h1 class="text-3xl font-bold mb-6">Blog Posts</h1>
179
+ <div class="grid gap-4">
180
+ {% for post in posts %}
181
+ <div class="p-4 bg-white shadow rounded">
182
+ <h2 class="text-xl font-semibold"><a href="/post/{{ post.slug }}.html" class="text-blue-600 hover:underline">{{ post.title }}</a></h2>
183
+ </div>
184
+ {% else %}
185
+ <p>No posts found. Ensure your CMS is running and the API URL is correct.</p>
186
+ {% endfor %}
187
+ </div>
188
+ <p class="mt-6"><a href="/index.html" class="text-blue-500 underline">&larr; Back home</a></p>`);
189
+ console.log(' Created src/blog.njk');
190
+ }
191
+
192
+ // Post detail
193
+ const postPath = path.join(cwd, 'src/post.njk');
194
+ if (!fs.existsSync(postPath)) {
195
+ fs.writeFileSync(postPath, `---
196
+ layout: base
197
+ url: post.title
198
+ ---
199
+ <article>
200
+ <h1 class="text-4xl font-bold mb-4">{{ post.title }}</h1>
201
+ <div class="prose">
202
+ {{ post.body | safe }}
203
+ </div>
204
+ <p class="mt-8"><a href="/blog.html" class="text-blue-500 underline">&larr; Back to blog</a></p>
205
+ </article>`);
206
+ console.log(' Created src/post.njk');
207
+ }
208
+ }
209
+ }
210
+
211
+ console.log('\nConfiguring project dependencies...');
212
+ const pkgPath = path.join(cwd, 'package.json');
213
+ let pkg = {
214
+ name: projectName,
215
+ description: projectDescription,
216
+ version: "1.0.0",
217
+ type: "module",
218
+ scripts: {
219
+ build: "bricklayer",
220
+ "build:prod": "bricklayer --prod",
221
+ manage: "bricklayer manage"
222
+ },
223
+ dependencies: {},
224
+ devDependencies: {
225
+ "basebrick-bricklayer": "^1.0.0",
226
+ "@tailwindcss/cli": "^4.0.0",
227
+ "tailwindcss": "^4.0.0"
228
+ }
229
+ };
230
+ if (githubRepo) {
231
+ pkg.repository = { type: "git", url: githubRepo };
232
+ }
233
+ if (fs.existsSync(pkgPath)) {
234
+ try {
235
+ const existing = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
236
+ pkg = { ...pkg, ...existing };
237
+ pkg.dependencies = { ...pkg.dependencies, ...existing.dependencies };
238
+ pkg.devDependencies = { ...pkg.devDependencies, ...existing.devDependencies };
239
+ } catch(e) {}
240
+ }
241
+
242
+ if (includeCloudflare) {
243
+ console.log('\nConfiguring Cloudflare...');
244
+ pkg.scripts.start = "wrangler dev";
245
+ pkg.scripts["deploy:preview"] = "npm run build:prod && bricklayer deploy --preview";
246
+ pkg.scripts["deploy:prod"] = "npm run build:prod && bricklayer deploy";
247
+ pkg.devDependencies.wrangler = "^3.0.0";
248
+
249
+ const wranglerPath = path.join(cwd, 'wrangler.toml');
250
+ if (!fs.existsSync(wranglerPath)) {
251
+ fs.writeFileSync(wranglerPath, `name = "${projectName}"\ncompatibility_date = "2024-05-12"\nmain = "api/worker.js"\n\n[assets]\ndirectory = "./public"\n\n[env.preview]\nname = "${projectName}-preview"\n`);
252
+ console.log(' Created wrangler.toml');
253
+ }
254
+
255
+ const apiDir = path.join(cwd, 'api');
256
+ if (!fs.existsSync(apiDir)) {
257
+ fs.mkdirSync(apiDir, { recursive: true });
258
+ console.log(' Created api/');
259
+ }
260
+
261
+ if (cfFramework === 'hono') {
262
+ pkg.dependencies.hono = "^4.3.0";
263
+ const workerPath = path.join(cwd, 'api/worker.js');
264
+ if (!fs.existsSync(workerPath)) {
265
+ fs.writeFileSync(workerPath, `import { Hono } from 'hono';\n\nconst app = new Hono();\n\napp.get('/api', (c) => {\n return c.json({ message: 'Hello from Hono API!' });\n});\n\nexport default app;\n`);
266
+ console.log(' Created api/worker.js (Hono API)');
267
+ }
268
+ } else {
269
+ const workerPath = path.join(cwd, 'api/worker.js');
270
+ if (!fs.existsSync(workerPath)) {
271
+ fs.writeFileSync(workerPath, `export default {\n async fetch(request, env, ctx) {\n const url = new URL(request.url);\n if (url.pathname.startsWith('/api')) {\n return new Response(JSON.stringify({ message: 'Hello from Vanilla API!' }), { headers: { 'Content-Type': 'application/json' } });\n }\n return new Response('Not found', { status: 404 });\n }\n};\n`);
272
+ console.log(' Created api/worker.js (Vanilla API)');
273
+ }
274
+ }
275
+ }
276
+
277
+ fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2));
278
+ console.log(' Created/Updated package.json');
279
+
280
+ if (pullSonic) {
281
+ console.log('\nPulling Sonic JS CMS into cms/ directory...');
282
+ try {
283
+ if (fs.existsSync(path.join(cwd, 'cms'))) {
284
+ console.log(' Directory cms/ already exists, skipping clone.');
285
+ } else {
286
+ execSync('git clone https://github.com/lane711/sonicjs.git cms', { stdio: 'inherit', cwd });
287
+ fs.rmSync(path.join(cwd, 'cms', '.git'), { recursive: true, force: true });
288
+ console.log(' Successfully cloned Sonic JS CMS.');
289
+ }
290
+ } catch (e) {
291
+ console.error(' Failed to pull Sonic JS:', e.message);
292
+ }
293
+ }
294
+
295
+ const gitignorePath = path.join(cwd, '.gitignore');
296
+ if (!fs.existsSync(gitignorePath)) {
297
+ fs.writeFileSync(gitignorePath, `node_modules/\n.DS_Store\n.env\n.dev.vars\n.wrangler/\n`);
298
+ console.log(' Created .gitignore');
299
+ }
300
+
301
+ if (createDeploy) {
302
+ console.log('\nConfiguring GitHub Actions deploy.yml...');
303
+ const githubDir = path.join(cwd, '.github', 'workflows');
304
+ if (!fs.existsSync(githubDir)) {
305
+ fs.mkdirSync(githubDir, { recursive: true });
306
+ console.log(' Created .github/workflows/');
307
+ }
308
+
309
+ let deploySteps = ` - name: Checkout repository
310
+ uses: actions/checkout@v4
311
+
312
+ - name: Setup Node.js
313
+ uses: actions/setup-node@v4
314
+ with:
315
+ node-version: '20'
316
+
317
+ - name: Install dependencies
318
+ run: npm install
319
+ `;
320
+
321
+ if (includeCloudflare) {
322
+ deploySteps += `
323
+ - name: Build and Deploy Site
324
+ run: npm run deploy:prod
325
+ env:
326
+ CLOUDFLARE_API_TOKEN: \${{ secrets.CLOUDFLARE_API_TOKEN }}
327
+ CLOUDFLARE_ACCOUNT_ID: \${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
328
+ `;
329
+ } else {
330
+ deploySteps += `
331
+ - name: Build Site
332
+ run: npm run build:prod
333
+ `;
334
+ }
335
+
336
+ if (includeSonic && pullSonic) {
337
+ deploySteps += `
338
+ - name: Install CMS dependencies
339
+ run: npm install
340
+ working-directory: ./cms
341
+
342
+ - name: Deploy Sonic CMS
343
+ run: npm run deploy
344
+ working-directory: ./cms
345
+ env:
346
+ CLOUDFLARE_API_TOKEN: \${{ secrets.CLOUDFLARE_API_TOKEN }}
347
+ CLOUDFLARE_ACCOUNT_ID: \${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
348
+ `;
349
+ }
350
+ const deployYmlContent = `name: Deploy
351
+
352
+ on:
353
+ push:
354
+ branches:
355
+ - main
356
+
357
+ jobs:
358
+ deploy:
359
+ runs-on: ubuntu-latest
360
+ steps:
361
+ ${deploySteps}`;
362
+
363
+ const deployYmlPath = path.join(githubDir, 'deploy.yml');
364
+ fs.writeFileSync(deployYmlPath, deployYmlContent);
365
+ console.log(' Created .github/workflows/deploy.yml');
366
+ if (includeCloudflare || (includeSonic && pullSonic)) {
367
+ console.log(' NOTE: Make sure to add CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID to your GitHub repository secrets.');
368
+ }
369
+ }
370
+
371
+ const basebrickConfigPath = path.join(cwd, '.basebrick.config');
372
+ const basebrickConfig = {
373
+ name: projectName,
374
+ description: projectDescription,
375
+ githubUrl: githubRepo,
376
+ includeDemo,
377
+ includeSonic,
378
+ pullSonic,
379
+ includeCloudflare,
380
+ cfFramework,
381
+ createDeploy
382
+ };
383
+ fs.writeFileSync(basebrickConfigPath, JSON.stringify(basebrickConfig, null, 2));
384
+ console.log(' Created .basebrick.config');
385
+
386
+ console.log('\n✅ Initialization complete!');
387
+ console.log('\nNext steps:');
388
+ console.log(' 1. Run `npm install` to install dependencies');
389
+ console.log(' 2. Run `npm run build` to build the static site (or `npm run build:prod` for production)');
390
+ if (includeCloudflare) {
391
+ console.log(' 3. Run `npm run start` to test your Cloudflare Worker locally');
392
+ console.log(' 4. Run `npm run deploy:preview` to deploy to a Cloudflare preview environment');
393
+ console.log(' 5. Run `npm run deploy:prod` to deploy to Cloudflare production\n');
394
+ } else {
395
+ console.log('');
396
+ }
397
+
398
+ rl.close();
399
+ }
package/bin/manage.js ADDED
@@ -0,0 +1,168 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import readline from 'readline';
4
+ import os from 'os';
5
+
6
+ const rl = readline.createInterface({
7
+ input: process.stdin,
8
+ output: process.stdout
9
+ });
10
+
11
+ const question = (query) => new Promise((resolve) => rl.question(query, resolve));
12
+
13
+ export async function manageProject(cwd, options = {}) {
14
+ const { reconfigure = false, url = null, token = null } = options;
15
+ const localConfigPath = path.join(cwd, '.basebrick.config');
16
+
17
+ if (!fs.existsSync(localConfigPath)) {
18
+ console.error('❌ No .basebrick.config found in this directory. Run `bricklayer init` first.');
19
+ rl.close();
20
+ return;
21
+ }
22
+
23
+ const globalConfigPath = path.join(os.homedir(), '.bricklayer-manager.json');
24
+ let managerConfig = {};
25
+
26
+ if (fs.existsSync(globalConfigPath)) {
27
+ try {
28
+ managerConfig = JSON.parse(fs.readFileSync(globalConfigPath, 'utf8'));
29
+ } catch (e) {
30
+ console.error('Warning: Could not parse global manager config.');
31
+ }
32
+ }
33
+
34
+ if (reconfigure) {
35
+ managerConfig = {};
36
+ }
37
+
38
+ if (url) {
39
+ managerConfig.url = url;
40
+ }
41
+ if (token) {
42
+ managerConfig.token = token;
43
+ }
44
+
45
+ if (managerConfig.url && !managerConfig.url.endsWith('/api/sites')) {
46
+ managerConfig.url = managerConfig.url.replace(/\/+$/, '') + '/api/sites';
47
+ }
48
+
49
+ if (!managerConfig.url) {
50
+ const urlRaw = await question('What is your Bricklayer Manager URL? ');
51
+ let url = urlRaw.trim();
52
+ if (!url.endsWith('/api/sites')) {
53
+ url = url.replace(/\/+$/, '') + '/api/sites';
54
+ }
55
+ managerConfig.url = url;
56
+ }
57
+
58
+ if (!managerConfig.token) {
59
+ const tokenRaw = await question('What is your transfer token? ');
60
+ managerConfig.token = tokenRaw.trim();
61
+ }
62
+
63
+ if (!managerConfig.url || !managerConfig.token) {
64
+ console.error('❌ URL and token are required.');
65
+ rl.close();
66
+ return;
67
+ }
68
+
69
+ // Save for next time
70
+ fs.writeFileSync(globalConfigPath, JSON.stringify(managerConfig, null, 2));
71
+
72
+ let projectConfig;
73
+ try {
74
+ projectConfig = JSON.parse(fs.readFileSync(localConfigPath, 'utf8'));
75
+ } catch(e) {
76
+ console.error('❌ Could not parse .basebrick.config');
77
+ rl.close();
78
+ return;
79
+ }
80
+
81
+ // Consolidate legacy keys to prefer user edits
82
+ if (projectConfig.projectName && projectConfig.projectName !== projectConfig.name) {
83
+ projectConfig.name = projectConfig.projectName;
84
+ }
85
+ if (projectConfig.projectDescription && projectConfig.projectDescription !== projectConfig.description) {
86
+ projectConfig.description = projectConfig.projectDescription;
87
+ }
88
+ if (projectConfig.githubRepo && projectConfig.githubRepo !== projectConfig.githubUrl) {
89
+ projectConfig.githubUrl = projectConfig.githubRepo;
90
+ }
91
+
92
+ // Clean up duplicate legacy keys
93
+ delete projectConfig.projectName;
94
+ delete projectConfig.projectDescription;
95
+ delete projectConfig.githubRepo;
96
+
97
+ // Normalize githubUrl
98
+ if (projectConfig.githubUrl) {
99
+ projectConfig.githubUrl = projectConfig.githubUrl.replace(/^git\+/, '').replace(/\.git$/, '');
100
+ }
101
+
102
+ // Extract description from package.json if not explicitly set
103
+ const pkgPath = path.join(cwd, 'package.json');
104
+ if (fs.existsSync(pkgPath)) {
105
+ try {
106
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
107
+ if (pkg.description && !projectConfig.description) {
108
+ projectConfig.description = pkg.description;
109
+ }
110
+ if (pkg.repository && pkg.repository.url && !projectConfig.githubUrl) {
111
+ let repoUrl = pkg.repository.url.replace(/^git\+/, '').replace(/\.git$/, '');
112
+ projectConfig.githubUrl = repoUrl;
113
+ }
114
+ } catch (e) {
115
+ console.error('Warning: Could not parse package.json');
116
+ }
117
+ }
118
+
119
+ // Extract name and account_id from wrangler.toml
120
+ const wranglerPath = path.join(cwd, 'wrangler.toml');
121
+ if (fs.existsSync(wranglerPath)) {
122
+ try {
123
+ const wranglerContent = fs.readFileSync(wranglerPath, 'utf8');
124
+ const nameMatch = wranglerContent.match(/^name\s*=\s*"([^"]+)"/m);
125
+ if (nameMatch && nameMatch[1] && !projectConfig.name) {
126
+ projectConfig.name = nameMatch[1];
127
+ }
128
+
129
+ const accountMatch = wranglerContent.match(/^account_id\s*=\s*"([^"]+)"/m);
130
+ if (accountMatch && accountMatch[1]) {
131
+ projectConfig.accountId = accountMatch[1];
132
+ }
133
+ } catch (e) {
134
+ console.error('Warning: Could not parse wrangler.toml');
135
+ }
136
+ }
137
+
138
+ // Save cleaned config back to .basebrick.config
139
+ fs.writeFileSync(localConfigPath, JSON.stringify(projectConfig, null, 2));
140
+
141
+ console.log(`\nSending project configuration to ${managerConfig.url}...`);
142
+
143
+ let endpoint = managerConfig.url;
144
+ if (!endpoint.startsWith('http')) {
145
+ endpoint = `https://${endpoint}`;
146
+ }
147
+
148
+ try {
149
+ const response = await fetch(endpoint, {
150
+ method: 'POST',
151
+ headers: {
152
+ 'Content-Type': 'application/json',
153
+ 'Authorization': `Bearer ${managerConfig.token}`
154
+ },
155
+ body: JSON.stringify(projectConfig)
156
+ });
157
+
158
+ if (response.ok) {
159
+ console.log('✅ Successfully registered with Bricklayer Manager!');
160
+ } else {
161
+ console.error(`❌ Manager returned error: ${response.status} ${response.statusText}`);
162
+ }
163
+ } catch(e) {
164
+ console.error(`❌ Failed to connect to manager: ${e.message}`);
165
+ }
166
+
167
+ rl.close();
168
+ }
package/history.md CHANGED
@@ -1,5 +1,27 @@
1
1
  # Version History
2
2
 
3
+ ## 1.2.0 (May 2026)
4
+
5
+ - **CLI Deployment Wrapper**: Introduced `bricklayer deploy` command using a pseudo-TTY to seamlessly wrap `wrangler deploy` across environments, automatically intercepting and capturing live Cloudflare URLs.
6
+ - **Automated Config Sync**: The `bricklayer manage` tool now seamlessly parses Cloudflare `account_id` from `wrangler.toml` and syncs it with your central configuration.
7
+ - **Manager Dashboard Upgrades**: Added real-time, silent dashboard polling and integrated direct Cloudflare Worker dashboard deep links for registered sites.
8
+ - **Site Management**: Implemented site deletion functionality within the manager UI and securely moved the API Configuration Token into an isolated Settings page.
9
+
10
+ ## 1.1.0 (May 2026)
11
+
12
+ - **Interactive Scaffolding**: Added `bricklayer init` command to scaffold new projects via an interactive wizard, generating `.gitignore` and `.basebrick.config` automatically.
13
+ - **Automated Deployments**: Scaffolding now optionally generates a tailored GitHub Actions `deploy.yml` workflow that orchestrates the site (and an isolated Sonic JS worker, if configured).
14
+ - **Cloudflare Native**: Integrated native Cloudflare Workers support (Vanilla or Hono) and static asset routing into the scaffolding workflow, automatically generating `deploy:prod` and `deploy:preview` environments in `wrangler.toml` and `package.json`.
15
+ - **Automated Sonic JS CMS**: Scaffolding now supports automatically cloning the Sonic JS repository into a local `/cms` folder and binding the frontend.
16
+ - **Centralized Management**: Added `bricklayer manage` command to securely register and sync local `.basebrick.config` data with a central Manager API.
17
+ - **Asset Enhancements**: The init command now automatically generates `src/assets/images` and downloads starter media directly.
18
+ - **Enhanced Logging**: Replaced intimidating Node.js fetch stack traces with clean, user-friendly warnings when remote CMS connections fail.
19
+ - **CLI Helpers**: The CLI help menu and post-init success output now clearly document the generated `npm run` workflow commands.
20
+
21
+ ## 1.0.1 (May 2026)
22
+
23
+ - **Environment Variables**: Added native support for loading `.env` and `.dev.vars` (Cloudflare Pages) files. Environment variables are now globally accessible in Nunjucks templates via the `env` object and can be interpolated directly within `generic.json` configurations.
24
+
3
25
  ## 1.0.0 (May 2026)
4
26
 
5
27
  - **Initial Extraction**: Extracted the build pipeline from the BaseBrick frontend into a standalone, reusable Node module (`bricklayer`).
package/index.js CHANGED
@@ -5,9 +5,34 @@ import nunjucks from 'nunjucks';
5
5
  import matter from 'gray-matter';
6
6
  import { execSync } from 'child_process';
7
7
 
8
+ // Simple environment variable loader (.env and .dev.vars)
9
+ function loadEnv(cwd) {
10
+ const envFiles = ['.env', '.dev.vars'];
11
+ for (const file of envFiles) {
12
+ const envPath = path.join(cwd, file);
13
+ if (fs.existsSync(envPath)) {
14
+ const lines = fs.readFileSync(envPath, 'utf8').split('\n');
15
+ for (const line of lines) {
16
+ const match = line.match(/^\s*([\w.-]+)\s*=\s*(.*)?\s*$/);
17
+ if (match) {
18
+ const key = match[1];
19
+ let value = match[2] || '';
20
+ value = value.replace(/(^['"]|['"]$)/g, '').trim();
21
+ if (process.env[key] === undefined) {
22
+ process.env[key] = value;
23
+ }
24
+ }
25
+ }
26
+ }
27
+ }
28
+ }
29
+
8
30
  export async function buildSite(options = {}) {
9
31
  const cwd = options.cwd || process.cwd();
10
32
  const isProd = options.isProd || false;
33
+
34
+ // Load environment variables from .env file
35
+ loadEnv(cwd);
11
36
 
12
37
  // Conditionally import minifiers so build doesn't fail if they aren't installed yet
13
38
  let minifyHtml;
@@ -102,6 +127,9 @@ export async function buildSite(options = {}) {
102
127
  autoescape: false,
103
128
  noCache: true
104
129
  });
130
+
131
+ // Support environment variables in Nunjucks templates
132
+ env.addGlobal('env', process.env);
105
133
 
106
134
  // Fetch remote content based on generic.json
107
135
  const genericJsonPath = path.join(config.srcDir, 'components', 'generic.json');
@@ -110,7 +138,12 @@ export async function buildSite(options = {}) {
110
138
 
111
139
  if (fs.existsSync(genericJsonPath)) {
112
140
  try {
113
- cmsConfig = JSON.parse(fs.readFileSync(genericJsonPath, 'utf-8'));
141
+ let jsonString = fs.readFileSync(genericJsonPath, 'utf-8');
142
+
143
+ // Substitute environment variables in the format ${VAR_NAME}
144
+ jsonString = jsonString.replace(/\$\{([^}]+)\}/g, (_, varName) => process.env[varName] || '');
145
+
146
+ cmsConfig = JSON.parse(jsonString);
114
147
  const baseUrl = isProd ? cmsConfig.productionUrl : cmsConfig.locaLUrl;
115
148
  const apiUrl = `${baseUrl.replace(/\/$/, '')}/${cmsConfig.apiUrl.replace(/^\//, '')}`;
116
149
 
@@ -157,7 +190,11 @@ export async function buildSite(options = {}) {
157
190
 
158
191
  console.log(`Successfully fetched ${remoteData.length} items from remote source.`);
159
192
  } catch (e) {
160
- console.error('Error fetching remote content:', e);
193
+ if ((e.cause && e.cause.code === 'ECONNREFUSED') || e.message.includes('fetch failed')) {
194
+ console.warn(`\n⚠️ CMS Not Found: Could not connect to remote API. Make sure your CMS is running.\n`);
195
+ } else {
196
+ console.error('Error fetching remote content:', e.message || e);
197
+ }
161
198
  }
162
199
  }
163
200
 
@@ -244,7 +281,7 @@ export async function buildSite(options = {}) {
244
281
  console.log('Building Tailwind CSS...');
245
282
  try {
246
283
  const tailwindArgs = isProd ? '--minify' : '';
247
- execSync(`npx tailwindcss -i "${config.css.input}" -o "${config.css.output}" ${tailwindArgs}`, { stdio: 'inherit' });
284
+ execSync(`npx @tailwindcss/cli -i "${config.css.input}" -o "${config.css.output}" ${tailwindArgs}`, { stdio: 'inherit' });
248
285
  console.log('Tailwind CSS built successfully!');
249
286
  } catch (error) {
250
287
  console.error('Failed to build Tailwind CSS:', error);
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "basebrick-bricklayer",
3
- "version": "1.0.1",
3
+ "version": "1.2.0",
4
4
  "type": "module",
5
5
  "description": "BaseBrick static site generator (JamBrick)",
6
6
  "main": "index.js",
7
7
  "bin": {
8
- "bricklayer": "./bin/bricklayer.js"
8
+ "bricklayer": "bin/bricklayer.js"
9
9
  },
10
10
  "keywords": [
11
11
  "jamstack",
@@ -21,4 +21,4 @@
21
21
  "gray-matter": "^4.0.3",
22
22
  "nunjucks": "^3.2.4"
23
23
  }
24
- }
24
+ }
package/todo.md ADDED
@@ -0,0 +1,3 @@
1
+ add preview instances for cloudflare
2
+ add the ability to add to the manager, it will ask you manager code and token
3
+ the manager is an idea i have had to manage sites he will send a payload to it with git url, sitemap, settings and this will let you manage the distrubation so everytime we build a new site it can be managed in the