basebrick-bricklayer 1.1.0 → 1.2.2

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
@@ -23,6 +23,18 @@ npx bricklayer init
23
23
 
24
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
25
 
26
+ ## Automated Deployment (GitHub Actions)
27
+
28
+ If you chose to generate a `deploy.yml` workflow during initialization, Bricklayer will automatically deploy your site to Cloudflare on every push to the `main` branch.
29
+
30
+ For Wrangler to authenticate correctly in a non-interactive CI/CD environment, you **must** configure your repository secrets.
31
+
32
+ 1. Go to [Cloudflare API Tokens](https://developers.cloudflare.com/fundamentals/api/get-started/create-token/) and create an API Token with `Edit Cloudflare Workers` permissions.
33
+ 2. In your GitHub repository, navigate to **Settings > Secrets and variables > Actions**.
34
+ 3. Create two new Repository Secrets:
35
+ - `CLOUDFLARE_API_TOKEN`: Your newly created API token.
36
+ - `CLOUDFLARE_ACCOUNT_ID`: Your Cloudflare Account ID (found on your Cloudflare dashboard overview).
37
+
26
38
  Alternatively, if you are working within the BaseBrick ecosystem locally, you can link it:
27
39
 
28
40
  ```bash
@@ -99,6 +111,23 @@ Bricklayer natively supports reading `.env` and `.dev.vars` (Cloudflare Pages) f
99
111
  - **Nunjucks Templates:** Variables are automatically passed to Nunjucks templates under the global `env` object. E.g., `{{ env.MY_API_KEY }}`.
100
112
  - **CMS Configuration:** Environment variables can be directly interpolated in `generic.json` strings using the `${VARIABLE_NAME}` syntax.
101
113
 
114
+ ## Bricklayer Manager
115
+
116
+ Bricklayer includes a centralized dashboard to help you track and administer all your statically generated sites.
117
+
118
+ The **Bricklayer Manager** allows you to:
119
+ - View all deployed Bricklayer sites via an elegant web UI.
120
+ - Automatically track and store Cloudflare Worker endpoints.
121
+ - Access deep links directly to Cloudflare Dashboards for easy debugging.
122
+
123
+ 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).
124
+
125
+ Once your manager is deployed, use the CLI command to securely sync any of your projects:
126
+
127
+ ```bash
128
+ bricklayer manage
129
+ ```
130
+
102
131
  ## Directory Structure
103
132
 
104
133
  Bricklayer expects the following default structure (overridable via options):
package/bin/bricklayer.js CHANGED
@@ -15,6 +15,7 @@ Commands:
15
15
  (empty) Run a standard development build
16
16
  init Scaffold a new project (Demo, CMS, Cloudflare API)
17
17
  manage Register this project with a Bricklayer Manager
18
+ deploy Deploy the project to Cloudflare and register its URL
18
19
  help Show this help message
19
20
 
20
21
  Options:
@@ -36,8 +37,21 @@ Generated NPM Scripts (after init):
36
37
 
37
38
  if (args.includes('init')) {
38
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);
39
43
  } else if (args.includes('manage')) {
40
- import('./manage.js').then(m => m.manageProject(process.cwd())).catch(console.error);
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);
41
55
  } else {
42
56
  const isProd = args.includes('--prod');
43
57
  buildSite({ isProd, cwd: process.cwd() })
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 CHANGED
@@ -35,6 +35,9 @@ export async function initProject(cwd) {
35
35
  const projectNameRaw = await question('What is the name of your project? (bricklayer-site) ');
36
36
  const projectName = projectNameRaw.trim() || 'bricklayer-site';
37
37
 
38
+ const projectDescriptionRaw = await question('Project description (optional): ');
39
+ const projectDescription = projectDescriptionRaw.trim() || '';
40
+
38
41
  const githubRepoRaw = await question('What is the GitHub repository URL? (leave blank for none) ');
39
42
  const githubRepo = githubRepoRaw.trim();
40
43
 
@@ -100,7 +103,7 @@ export async function initProject(cwd) {
100
103
  // 2. Create base CSS
101
104
  const cssPath = path.join(cwd, 'src/assets/tailwind/input.css');
102
105
  if (!fs.existsSync(cssPath)) {
103
- fs.writeFileSync(cssPath, `@tailwind base;\n@tailwind components;\n@tailwind utilities;\n`);
106
+ fs.writeFileSync(cssPath, `@import "tailwindcss";\n\n@source "../../**/*.njk";\n`);
104
107
  console.log(' Created src/assets/tailwind/input.css');
105
108
  }
106
109
 
@@ -209,11 +212,13 @@ url: post.title
209
212
  const pkgPath = path.join(cwd, 'package.json');
210
213
  let pkg = {
211
214
  name: projectName,
215
+ description: projectDescription,
212
216
  version: "1.0.0",
213
217
  type: "module",
214
218
  scripts: {
215
219
  build: "bricklayer",
216
- "build:prod": "bricklayer --prod"
220
+ "build:prod": "bricklayer --prod",
221
+ manage: "bricklayer manage"
217
222
  },
218
223
  dependencies: {},
219
224
  devDependencies: {
@@ -229,16 +234,17 @@ url: post.title
229
234
  try {
230
235
  const existing = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
231
236
  pkg = { ...pkg, ...existing };
232
- pkg.dependencies = { ...pkg.dependencies, ...existing.dependencies };
233
- pkg.devDependencies = { ...pkg.devDependencies, ...existing.devDependencies };
237
+ pkg.scripts = { ...(existing.scripts || {}), ...pkg.scripts };
238
+ pkg.dependencies = { ...(existing.dependencies || {}), ...pkg.dependencies };
239
+ pkg.devDependencies = { ...(existing.devDependencies || {}), ...pkg.devDependencies };
234
240
  } catch(e) {}
235
241
  }
236
242
 
237
243
  if (includeCloudflare) {
238
244
  console.log('\nConfiguring Cloudflare...');
239
245
  pkg.scripts.start = "wrangler dev";
240
- pkg.scripts["deploy:preview"] = "npm run build:prod && wrangler deploy --env preview";
241
- pkg.scripts["deploy:prod"] = "npm run build:prod && wrangler deploy";
246
+ pkg.scripts["deploy:preview"] = "npm run build:prod && bricklayer deploy --preview";
247
+ pkg.scripts["deploy:prod"] = "npm run build:prod && bricklayer deploy";
242
248
  pkg.devDependencies.wrangler = "^3.0.0";
243
249
 
244
250
  const wranglerPath = path.join(cwd, 'wrangler.toml');
@@ -365,8 +371,9 @@ ${deploySteps}`;
365
371
 
366
372
  const basebrickConfigPath = path.join(cwd, '.basebrick.config');
367
373
  const basebrickConfig = {
368
- projectName,
369
- githubRepo,
374
+ name: projectName,
375
+ description: projectDescription,
376
+ githubUrl: githubRepo,
370
377
  includeDemo,
371
378
  includeSonic,
372
379
  pullSonic,
package/bin/manage.js CHANGED
@@ -10,7 +10,8 @@ const rl = readline.createInterface({
10
10
 
11
11
  const question = (query) => new Promise((resolve) => rl.question(query, resolve));
12
12
 
13
- export async function manageProject(cwd) {
13
+ export async function manageProject(cwd, options = {}) {
14
+ const { reconfigure = false, url = null, token = null } = options;
14
15
  const localConfigPath = path.join(cwd, '.basebrick.config');
15
16
 
16
17
  if (!fs.existsSync(localConfigPath)) {
@@ -30,9 +31,28 @@ export async function manageProject(cwd) {
30
31
  }
31
32
  }
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
+
33
49
  if (!managerConfig.url) {
34
50
  const urlRaw = await question('What is your Bricklayer Manager URL? ');
35
- managerConfig.url = urlRaw.trim();
51
+ let url = urlRaw.trim();
52
+ if (!url.endsWith('/api/sites')) {
53
+ url = url.replace(/\/+$/, '') + '/api/sites';
54
+ }
55
+ managerConfig.url = url;
36
56
  }
37
57
 
38
58
  if (!managerConfig.token) {
@@ -58,6 +78,81 @@ export async function manageProject(cwd) {
58
78
  return;
59
79
  }
60
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
+ } else if (process.env.CLOUDFLARE_ACCOUNT_ID) {
133
+ projectConfig.accountId = process.env.CLOUDFLARE_ACCOUNT_ID;
134
+ }
135
+ } catch (e) {
136
+ console.error('Warning: Could not parse wrangler.toml');
137
+ }
138
+ }
139
+
140
+ if (!projectConfig.accountId) {
141
+ try {
142
+ const { execSync } = await import('child_process');
143
+ const whoamiOutput = execSync('npx wrangler whoami', { stdio: 'pipe', encoding: 'utf8' });
144
+ const match = whoamiOutput.match(/Account ID[^\n]*?([a-f0-9]{32})/i);
145
+ if (match && match[1]) {
146
+ projectConfig.accountId = match[1];
147
+ }
148
+ } catch (e) {
149
+ // Ignore if whoami fails
150
+ }
151
+ }
152
+
153
+ // Save cleaned config back to .basebrick.config
154
+ fs.writeFileSync(localConfigPath, JSON.stringify(projectConfig, null, 2));
155
+
61
156
  console.log(`\nSending project configuration to ${managerConfig.url}...`);
62
157
 
63
158
  let endpoint = managerConfig.url;
package/history.md CHANGED
@@ -1,5 +1,12 @@
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
+
3
10
  ## 1.1.0 (May 2026)
4
11
 
5
12
  - **Interactive Scaffolding**: Added `bricklayer init` command to scaffold new projects via an interactive wizard, generating `.gitignore` and `.basebrick.config` automatically.
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "basebrick-bricklayer",
3
- "version": "1.1.0",
3
+ "version": "1.2.2",
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
+ }