backend-manager 5.0.133 → 5.0.135
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/CLAUDE.md
CHANGED
|
@@ -467,16 +467,16 @@ npx bm test
|
|
|
467
467
|
```
|
|
468
468
|
|
|
469
469
|
### Log Files
|
|
470
|
-
|
|
471
|
-
- **`
|
|
472
|
-
- **`
|
|
470
|
+
BEM CLI commands automatically save all output to log files in `functions/` while still streaming to the console:
|
|
471
|
+
- **`functions/serve.log`** — Output from `npx bm serve` (Firebase serve)
|
|
472
|
+
- **`functions/emulator.log`** — Full emulator output (Firebase emulator + Cloud Functions logs)
|
|
473
|
+
- **`functions/test.log`** — Test runner output (when running against an existing emulator)
|
|
474
|
+
- **`functions/logs.log`** — Cloud Function logs from `npx bm logs:read` or `npx bm logs:tail` (raw JSON for `read`, streaming text for `tail`)
|
|
473
475
|
|
|
474
476
|
When `npx bm test` starts its own emulator, logs go to `emulator.log` (since it delegates to the emulator command). When running against an already-running emulator, logs go to `test.log`.
|
|
475
477
|
|
|
476
478
|
These files are overwritten on each run and are gitignored (`*.log`). Use them to search for errors, debug webhook pipelines, or review full function output after a test run.
|
|
477
479
|
|
|
478
|
-
- **`logs.log`** — Cloud Function logs from `npx bm logs:read` or `npx bm logs:tail` (raw JSON for `read`, streaming text for `tail`)
|
|
479
|
-
|
|
480
480
|
### Filtering Tests
|
|
481
481
|
```bash
|
|
482
482
|
npx bm test rules/ # Run rules tests (both BEM and project)
|
|
@@ -630,7 +630,7 @@ npx bm logs:tail # Stream live logs
|
|
|
630
630
|
npx bm logs:tail --fn bm_paymentsWebhookOnWrite # Stream filtered live logs
|
|
631
631
|
```
|
|
632
632
|
|
|
633
|
-
Both commands save output to `logs.log`
|
|
633
|
+
Both commands save output to `functions/logs.log` (overwritten on each run). `logs:read` saves raw JSON; `logs:tail` streams text.
|
|
634
634
|
|
|
635
635
|
| Flag | Description | Default | Commands |
|
|
636
636
|
|------|-------------|---------|----------|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "backend-manager",
|
|
3
|
-
"version": "5.0.
|
|
3
|
+
"version": "5.0.135",
|
|
4
4
|
"description": "Quick tools for developing Firebase functions",
|
|
5
5
|
"main": "src/manager/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -18,11 +18,11 @@
|
|
|
18
18
|
"node": "22"
|
|
19
19
|
},
|
|
20
20
|
"projectScripts": {
|
|
21
|
-
"start": "npx
|
|
22
|
-
"deploy": "npx
|
|
23
|
-
"emulator": "npx
|
|
24
|
-
"test": "npx
|
|
25
|
-
"setup": "npx
|
|
21
|
+
"start": "npx mgr setup && npx mgr serve",
|
|
22
|
+
"deploy": "npx mgr setup && npx mgr deploy",
|
|
23
|
+
"emulator": "npx mgr setup && npx mgr emulator",
|
|
24
|
+
"test": "npx mgr setup && npx mgr test",
|
|
25
|
+
"setup": "npx mgr setup"
|
|
26
26
|
},
|
|
27
27
|
"repository": {
|
|
28
28
|
"type": "git",
|
|
@@ -62,7 +62,7 @@ class EmulatorCommand extends BaseCommand {
|
|
|
62
62
|
const emulatorCommand = `BEM_TESTING=true firebase emulators:exec --only functions,firestore,auth,database,hosting,pubsub --ui "${command}"`;
|
|
63
63
|
|
|
64
64
|
// Set up log file in the project directory
|
|
65
|
-
const logPath = path.join(projectDir, 'emulator.log');
|
|
65
|
+
const logPath = path.join(projectDir, 'functions', 'emulator.log');
|
|
66
66
|
const logStream = fs.createWriteStream(logPath, { flags: 'w' });
|
|
67
67
|
const stripAnsi = (str) => str.replace(/\x1B\[[0-9;]*[a-zA-Z]/g, '');
|
|
68
68
|
|
package/src/cli/commands/logs.js
CHANGED
|
@@ -73,7 +73,7 @@ class LogsCommand extends BaseCommand {
|
|
|
73
73
|
|
|
74
74
|
// Set up log file in the project directory
|
|
75
75
|
const projectDir = this.main.firebaseProjectPath;
|
|
76
|
-
const logPath = path.join(projectDir, 'logs.log');
|
|
76
|
+
const logPath = path.join(projectDir, 'functions', 'logs.log');
|
|
77
77
|
|
|
78
78
|
this.log(chalk.gray(` Filter: ${filter || '(none)'}`));
|
|
79
79
|
this.log(chalk.gray(` Limit: ${limit}`));
|
|
@@ -127,7 +127,7 @@ class LogsCommand extends BaseCommand {
|
|
|
127
127
|
|
|
128
128
|
// Set up log file in the project directory
|
|
129
129
|
const projectDir = this.main.firebaseProjectPath;
|
|
130
|
-
const logPath = path.join(projectDir, 'logs.log');
|
|
130
|
+
const logPath = path.join(projectDir, 'functions', 'logs.log');
|
|
131
131
|
const logStream = fs.createWriteStream(logPath, { flags: 'w' });
|
|
132
132
|
|
|
133
133
|
const filter = this.buildFilter(argv, { excludeTimestamp: true });
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
const BaseCommand = require('./base-command');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const chalk = require('chalk');
|
|
2
5
|
const powertools = require('node-powertools');
|
|
3
6
|
const WatchCommand = require('./watch');
|
|
4
7
|
|
|
@@ -6,6 +9,7 @@ class ServeCommand extends BaseCommand {
|
|
|
6
9
|
async execute() {
|
|
7
10
|
const self = this.main;
|
|
8
11
|
const port = self.argv.port || self.argv?._?.[1] || '5000';
|
|
12
|
+
const projectDir = self.firebaseProjectPath;
|
|
9
13
|
|
|
10
14
|
// Start BEM watcher in background
|
|
11
15
|
const watcher = new WatchCommand(self);
|
|
@@ -14,8 +18,44 @@ class ServeCommand extends BaseCommand {
|
|
|
14
18
|
// Start Stripe webhook forwarding in background
|
|
15
19
|
this.startStripeWebhookForwarding();
|
|
16
20
|
|
|
17
|
-
//
|
|
18
|
-
|
|
21
|
+
// Set up log file in the project directory
|
|
22
|
+
const logPath = path.join(projectDir, 'functions', 'serve.log');
|
|
23
|
+
const logStream = fs.createWriteStream(logPath, { flags: 'w' });
|
|
24
|
+
const stripAnsi = (str) => str.replace(/\x1B\[[0-9;]*[a-zA-Z]/g, '');
|
|
25
|
+
|
|
26
|
+
this.log(chalk.gray(` Logs saving to: ${logPath}\n`));
|
|
27
|
+
|
|
28
|
+
// Execute with tee to log file
|
|
29
|
+
try {
|
|
30
|
+
await powertools.execute(`firebase serve --port ${port}`, {
|
|
31
|
+
log: false,
|
|
32
|
+
cwd: projectDir,
|
|
33
|
+
config: {
|
|
34
|
+
stdio: ['inherit', 'pipe', 'pipe'],
|
|
35
|
+
env: { ...process.env, FORCE_COLOR: '1' },
|
|
36
|
+
},
|
|
37
|
+
}, (child) => {
|
|
38
|
+
// Tee stdout to both console and log file (strip ANSI codes for clean log)
|
|
39
|
+
child.stdout.on('data', (data) => {
|
|
40
|
+
process.stdout.write(data);
|
|
41
|
+
logStream.write(stripAnsi(data.toString()));
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// Tee stderr to both console and log file (strip ANSI codes for clean log)
|
|
45
|
+
child.stderr.on('data', (data) => {
|
|
46
|
+
process.stderr.write(data);
|
|
47
|
+
logStream.write(stripAnsi(data.toString()));
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// Clean up log stream when child exits
|
|
51
|
+
child.on('close', () => {
|
|
52
|
+
logStream.end();
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
} catch (error) {
|
|
56
|
+
// User pressed Ctrl+C - this is expected
|
|
57
|
+
this.log(chalk.gray('\n Server stopped.\n'));
|
|
58
|
+
}
|
|
19
59
|
}
|
|
20
60
|
}
|
|
21
61
|
|
package/src/cli/commands/test.js
CHANGED
|
@@ -185,7 +185,7 @@ class TestCommand extends BaseCommand {
|
|
|
185
185
|
this.log(chalk.gray(` UI: http://127.0.0.1:${emulatorPorts.ui}`));
|
|
186
186
|
|
|
187
187
|
// Set up log file in the project directory
|
|
188
|
-
const logPath = path.join(projectDir, 'test.log');
|
|
188
|
+
const logPath = path.join(projectDir, 'functions', 'test.log');
|
|
189
189
|
const logStream = fs.createWriteStream(logPath, { flags: 'w' });
|
|
190
190
|
const stripAnsi = (str) => str.replace(/\x1B\[[0-9;]*[a-zA-Z]/g, '');
|
|
191
191
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* POST /admin/post - Create blog post
|
|
3
3
|
* Admin/blogger endpoint to create blog posts via GitHub
|
|
4
|
+
* Uses Git Trees API to commit all files (images + post) in a single commit
|
|
4
5
|
*/
|
|
5
6
|
const moment = require('moment');
|
|
6
7
|
const jetpack = require('fs-jetpack');
|
|
@@ -15,7 +16,6 @@ const IMAGE_PATH_SRC = `src/assets/images/blog/post-{id}/`;
|
|
|
15
16
|
const IMAGE_REGEX = /(?:!\[(.*?)\]\((.*?)\))/img;
|
|
16
17
|
|
|
17
18
|
module.exports = async ({ assistant, Manager, user, settings, analytics }) => {
|
|
18
|
-
const fetch = Manager.require('wonderful-fetch');
|
|
19
19
|
|
|
20
20
|
// Require authentication
|
|
21
21
|
if (!user.authenticated) {
|
|
@@ -92,30 +92,47 @@ module.exports = async ({ assistant, Manager, user, settings, analytics }) => {
|
|
|
92
92
|
|
|
93
93
|
assistant.log('main(): Creating post...', settings);
|
|
94
94
|
|
|
95
|
-
//
|
|
96
|
-
const
|
|
97
|
-
if (
|
|
98
|
-
return assistant.respond(
|
|
95
|
+
// Download all images and collect file data
|
|
96
|
+
const imageFiles = await downloadImages(assistant, settings).catch(e => e);
|
|
97
|
+
if (imageFiles instanceof Error) {
|
|
98
|
+
return assistant.respond(imageFiles.message, { code: 400 });
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
// Rewrite body to use @post/ prefix for extracted images
|
|
102
|
-
for (const
|
|
103
|
-
|
|
102
|
+
for (const file of imageFiles) {
|
|
103
|
+
if (file.originalUrl) {
|
|
104
|
+
settings.body = settings.body.split(file.originalUrl).join(`@post/${file.filename}`);
|
|
105
|
+
}
|
|
104
106
|
}
|
|
105
107
|
|
|
106
|
-
//
|
|
108
|
+
// Generate post content from template
|
|
107
109
|
const formattedContent = powertools.template(
|
|
108
110
|
POST_TEMPLATE,
|
|
109
111
|
formatClone(settings),
|
|
110
112
|
);
|
|
111
113
|
|
|
112
|
-
//
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
114
|
+
// Build post file entry
|
|
115
|
+
const postFilename = `${settings.path}/${settings.date}-${settings.url}.md`;
|
|
116
|
+
const allFiles = [
|
|
117
|
+
...imageFiles.map(img => ({
|
|
118
|
+
path: img.githubPath,
|
|
119
|
+
content: img.base64,
|
|
120
|
+
encoding: 'base64',
|
|
121
|
+
})),
|
|
122
|
+
{
|
|
123
|
+
path: postFilename,
|
|
124
|
+
content: Buffer.from(formattedContent).toString('base64'),
|
|
125
|
+
encoding: 'base64',
|
|
126
|
+
},
|
|
127
|
+
];
|
|
128
|
+
|
|
129
|
+
// Commit all files in a single commit
|
|
130
|
+
const commitResult = await commitAll(assistant, octokit, settings, allFiles).catch(e => e);
|
|
131
|
+
if (commitResult instanceof Error) {
|
|
132
|
+
return assistant.respond(commitResult.message, { code: 500 });
|
|
116
133
|
}
|
|
117
134
|
|
|
118
|
-
assistant.log('main():
|
|
135
|
+
assistant.log('main(): commitAll', commitResult);
|
|
119
136
|
|
|
120
137
|
// Track analytics
|
|
121
138
|
analytics.event('admin/post', { action: 'create' });
|
|
@@ -123,9 +140,10 @@ module.exports = async ({ assistant, Manager, user, settings, analytics }) => {
|
|
|
123
140
|
return assistant.respond(settings);
|
|
124
141
|
};
|
|
125
142
|
|
|
126
|
-
// Helper:
|
|
127
|
-
async function
|
|
128
|
-
const
|
|
143
|
+
// Helper: Download all images and return file data (no GitHub uploads)
|
|
144
|
+
async function downloadImages(assistant, settings) {
|
|
145
|
+
const files = [];
|
|
146
|
+
const assetsPath = powertools.template(IMAGE_PATH_SRC, settings);
|
|
129
147
|
|
|
130
148
|
const matches = settings.body.matchAll(IMAGE_REGEX);
|
|
131
149
|
const images = Array.from(matches).map(match => ({
|
|
@@ -141,10 +159,10 @@ async function extractImages(assistant, octokit, settings) {
|
|
|
141
159
|
header: true,
|
|
142
160
|
});
|
|
143
161
|
|
|
144
|
-
assistant.log('
|
|
162
|
+
assistant.log('downloadImages(): images', images);
|
|
145
163
|
|
|
146
164
|
if (!images.length) {
|
|
147
|
-
return
|
|
165
|
+
return files;
|
|
148
166
|
}
|
|
149
167
|
|
|
150
168
|
for (let index = 0; index < images.length; index++) {
|
|
@@ -153,38 +171,29 @@ async function extractImages(assistant, octokit, settings) {
|
|
|
153
171
|
// Download image
|
|
154
172
|
const download = await downloadImage(assistant, image.src, image.alt).catch(e => e);
|
|
155
173
|
|
|
156
|
-
assistant.log('
|
|
174
|
+
assistant.log('downloadImages(): download', download);
|
|
157
175
|
|
|
158
176
|
if (download instanceof Error) {
|
|
159
177
|
if (image.header) {
|
|
160
178
|
throw download;
|
|
161
179
|
} else {
|
|
162
|
-
assistant.warn('
|
|
180
|
+
assistant.warn('downloadImages(): Skipping NON-HEADER image download due to error', download);
|
|
163
181
|
continue;
|
|
164
182
|
}
|
|
165
183
|
}
|
|
166
184
|
|
|
167
|
-
//
|
|
168
|
-
const
|
|
185
|
+
// Read file content as base64
|
|
186
|
+
const base64 = jetpack.read(download.path, 'buffer').toString('base64');
|
|
169
187
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
assistant.warn('extractImages(): Skipping NON-HEADER image upload due to error', upload);
|
|
177
|
-
continue;
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
// Track successfully uploaded non-header images for body rewriting
|
|
182
|
-
if (!image.header) {
|
|
183
|
-
urlMap.push({ originalUrl: image.src, localFilename: download.filename });
|
|
184
|
-
}
|
|
188
|
+
files.push({
|
|
189
|
+
githubPath: `${assetsPath}${download.filename}`,
|
|
190
|
+
filename: download.filename,
|
|
191
|
+
base64: base64,
|
|
192
|
+
originalUrl: image.header ? null : image.src,
|
|
193
|
+
});
|
|
185
194
|
}
|
|
186
195
|
|
|
187
|
-
return
|
|
196
|
+
return files;
|
|
188
197
|
}
|
|
189
198
|
|
|
190
199
|
// Helper: Download image
|
|
@@ -211,82 +220,95 @@ async function downloadImage(assistant, src, alt) {
|
|
|
211
220
|
return result;
|
|
212
221
|
}
|
|
213
222
|
|
|
214
|
-
// Helper:
|
|
215
|
-
async function
|
|
216
|
-
const filepath = image.path;
|
|
217
|
-
const filename = image.filename;
|
|
218
|
-
const assetsPath = powertools.template(IMAGE_PATH_SRC, settings);
|
|
223
|
+
// Helper: Commit all files (images + post) in a single commit using Git Trees API
|
|
224
|
+
async function commitAll(assistant, octokit, settings, files) {
|
|
219
225
|
const owner = settings.githubUser;
|
|
220
226
|
const repo = settings.githubRepo;
|
|
221
227
|
|
|
222
|
-
assistant.log('
|
|
223
|
-
assistant.log('uploadImage(): path', `${assetsPath}${filename}`);
|
|
228
|
+
assistant.log('commitAll(): Committing', files.length, 'files');
|
|
224
229
|
|
|
225
|
-
// Get
|
|
226
|
-
const
|
|
230
|
+
// Get the latest commit SHA on the default branch
|
|
231
|
+
const refResult = await octokit.rest.git.getRef({
|
|
227
232
|
owner: owner,
|
|
228
233
|
repo: repo,
|
|
229
|
-
|
|
230
|
-
}).catch(
|
|
234
|
+
ref: 'heads/master',
|
|
235
|
+
}).catch(() => {
|
|
236
|
+
// Try 'main' if 'master' fails
|
|
237
|
+
return octokit.rest.git.getRef({
|
|
238
|
+
owner: owner,
|
|
239
|
+
repo: repo,
|
|
240
|
+
ref: 'heads/main',
|
|
241
|
+
});
|
|
242
|
+
});
|
|
231
243
|
|
|
232
|
-
|
|
244
|
+
const latestCommitSha = refResult.data.object.sha;
|
|
245
|
+
const branch = refResult.data.ref;
|
|
233
246
|
|
|
234
|
-
|
|
235
|
-
throw existing;
|
|
236
|
-
}
|
|
247
|
+
assistant.log('commitAll(): Latest commit', latestCommitSha, 'on', branch);
|
|
237
248
|
|
|
238
|
-
//
|
|
239
|
-
const
|
|
249
|
+
// Get the tree SHA of the latest commit
|
|
250
|
+
const commitResult = await octokit.rest.git.getCommit({
|
|
240
251
|
owner: owner,
|
|
241
252
|
repo: repo,
|
|
242
|
-
|
|
243
|
-
sha: existing?.data?.sha || undefined,
|
|
244
|
-
message: `📦 admin/post:upload-image ${filename}`,
|
|
245
|
-
content: jetpack.read(filepath, 'buffer').toString('base64'),
|
|
253
|
+
commit_sha: latestCommitSha,
|
|
246
254
|
});
|
|
247
255
|
|
|
248
|
-
|
|
256
|
+
const baseTreeSha = commitResult.data.tree.sha;
|
|
249
257
|
|
|
250
|
-
|
|
251
|
-
|
|
258
|
+
// Create blobs for each file
|
|
259
|
+
const treeItems = [];
|
|
252
260
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
261
|
+
for (const file of files) {
|
|
262
|
+
const blob = await octokit.rest.git.createBlob({
|
|
263
|
+
owner: owner,
|
|
264
|
+
repo: repo,
|
|
265
|
+
content: file.content,
|
|
266
|
+
encoding: file.encoding,
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
assistant.log('commitAll(): Created blob for', file.path, blob.data.sha);
|
|
258
270
|
|
|
259
|
-
|
|
271
|
+
treeItems.push({
|
|
272
|
+
path: file.path,
|
|
273
|
+
mode: '100644',
|
|
274
|
+
type: 'blob',
|
|
275
|
+
sha: blob.data.sha,
|
|
276
|
+
});
|
|
277
|
+
}
|
|
260
278
|
|
|
261
|
-
//
|
|
262
|
-
const
|
|
279
|
+
// Create a new tree with all files
|
|
280
|
+
const newTree = await octokit.rest.git.createTree({
|
|
263
281
|
owner: owner,
|
|
264
282
|
repo: repo,
|
|
265
|
-
|
|
266
|
-
|
|
283
|
+
base_tree: baseTreeSha,
|
|
284
|
+
tree: treeItems,
|
|
285
|
+
});
|
|
267
286
|
|
|
268
|
-
assistant.log('
|
|
287
|
+
assistant.log('commitAll(): Created tree', newTree.data.sha);
|
|
269
288
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
289
|
+
// Create the commit
|
|
290
|
+
const postPath = files[files.length - 1].path;
|
|
291
|
+
const newCommit = await octokit.rest.git.createCommit({
|
|
292
|
+
owner: owner,
|
|
293
|
+
repo: repo,
|
|
294
|
+
message: `📦 admin/post:create ${postPath}`,
|
|
295
|
+
tree: newTree.data.sha,
|
|
296
|
+
parents: [latestCommitSha],
|
|
297
|
+
});
|
|
273
298
|
|
|
274
|
-
|
|
275
|
-
await powertools.wait(30000);
|
|
299
|
+
assistant.log('commitAll(): Created commit', newCommit.data.sha);
|
|
276
300
|
|
|
277
|
-
//
|
|
278
|
-
const
|
|
301
|
+
// Update the branch ref to point to the new commit
|
|
302
|
+
const updateResult = await octokit.rest.git.updateRef({
|
|
279
303
|
owner: owner,
|
|
280
304
|
repo: repo,
|
|
281
|
-
|
|
282
|
-
sha:
|
|
283
|
-
message: `📦 admin/post:upload-post ${filename}`,
|
|
284
|
-
content: Buffer.from(content).toString('base64'),
|
|
305
|
+
ref: branch.replace('refs/', ''),
|
|
306
|
+
sha: newCommit.data.sha,
|
|
285
307
|
});
|
|
286
308
|
|
|
287
|
-
assistant.log('
|
|
309
|
+
assistant.log('commitAll(): Updated ref', updateResult.data.object.sha);
|
|
288
310
|
|
|
289
|
-
return
|
|
311
|
+
return updateResult;
|
|
290
312
|
}
|
|
291
313
|
|
|
292
314
|
// Helper: Format clone for templating
|