backend-manager 5.9.11 → 5.9.14

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
@@ -92,7 +92,7 @@ See [docs/cli-firestore-auth.md](docs/cli-firestore-auth.md) and [docs/cli-logs.
92
92
  ## Development Workflow
93
93
 
94
94
  - **🚫 NEVER run `npx mgr serve` / `npx mgr emulator`** — they're the user's long-running dev processes. Assume they're already running; if they aren't, **instruct the user to run them** rather than running them yourself (running them again kills theirs). To see output, **read the `functions/*.log` files** (`dev.log`, `emulator.log`, `test.log`) — never tail/attach to the process. Running `npx mgr test` is fine (it auto-starts its own emulator if needed).
95
- - **Where the output logs live:** BEM CLI commands tee output to `<projectDir>/functions/` (not `logs/` — BEM's deliberate exception, co-located with firebase-tools' `*-debug.log`): `dev.log` (`npx mgr serve`), `emulator.log` (`npx mgr emulator` / test with own emulator), `test.log` (`npx mgr test`), `production.log` (`npx mgr logs`). The `dev`/`test` names match EM/BXM/UJM; see [docs/test-framework.md](docs/test-framework.md#log-files).
95
+ - **Where the output logs live:** BEM CLI commands tee output to `<projectDir>/functions/` (not `logs/` — BEM's deliberate exception, co-located with firebase-tools' `*-debug.log`): `dev.log` (`npx mgr serve`), `deploy.log` (`npx mgr deploy`), `emulator.log` (`npx mgr emulator` / test with own emulator), `test.log` (`npx mgr test`), `production.log` (`npx mgr logs`). The `dev`/`test` names match EM/BXM/UJM; see [docs/logging.md](docs/logging.md).
96
96
  - **If the user reports an error**, check the emulator/test output for the root cause before guessing.
97
97
  - **Live-test UI changes via CDP.** When working on admin dashboards or browser-facing endpoints, use the `chrome-devtools` MCP tools (screenshots, click, evaluate JS, console logs) to verify the change works in the running browser. See `~/.claude/mcp-server/servers/chrome-devtools/CLAUDE.md`.
98
98
 
package/docs/logging.md CHANGED
@@ -9,12 +9,32 @@ All in `<projectDir>/functions/`:
9
9
  | File | Source | Lifetime |
10
10
  |---|---|---|
11
11
  | `dev.log` | `npx mgr serve` — BEM's local dev server (Firebase serve) | Overwritten each run |
12
+ | `deploy.log` | `npx mgr deploy` — Firebase deployment output (function uploads, hosting deploys, errors) | Overwritten each run |
12
13
  | `emulator.log` | `npx mgr emulator` — full emulator output (Firebase emulator + Cloud Functions logs); also `npx mgr test` when it starts its own emulator | Overwritten each run |
13
14
  | `test.log` | `npx mgr test` runner output when running against an already-running emulator | Overwritten each run |
14
15
  | `production.log` | `npx mgr logs:read` / `npx mgr logs:tail` — production Cloud Function logs from Google Cloud Logging (raw JSON for `read`, streaming text for `tail`) | Overwritten each run |
15
16
 
16
17
  The `dev`/`test` names match EM/BXM/UJM for cross-framework parity.
17
18
 
19
+ ## attach-log-file utility
20
+
21
+ `src/cli/utils/attach-log-file.js` — shared DRY utility (same pattern as BXM/UJM/EM). Intercepts `process.stdout.write` / `process.stderr.write` to tee all output to a log file while preserving console display. ANSI codes stripped from file output for grep-friendliness.
22
+
23
+ ```js
24
+ const attachLogFile = require('../utils/attach-log-file');
25
+
26
+ attachLogFile(this.getLogsPath('deploy.log'));
27
+ // ... run command — all stdout/stderr is now teed to the log file ...
28
+ await attachLogFile.detach();
29
+ ```
30
+
31
+ - **Singleton**: default export is a process-wide singleton (one file at a time)
32
+ - **Factory**: `attachLogFile.createTee()` returns an independent tee for stacking
33
+ - **Idempotent**: attaching the same path twice returns the existing handle
34
+ - **Flush-safe**: `detach()` returns a Promise — await before `process.exit()`
35
+
36
+ Used by `deploy.js`. The `serve`/`emulator`/`test` commands use inline stream management (they need reset-sentinel polling and reload detection that the basic tee doesn't cover).
37
+
18
38
  ## What gets captured
19
39
 
20
40
  When `npx mgr test` starts its own emulator, logs go to `emulator.log` (it delegates to the emulator command). When running against an already-running emulator, logs go to `test.log`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "backend-manager",
3
- "version": "5.9.11",
3
+ "version": "5.9.14",
4
4
  "description": "Quick tools for developing Firebase functions",
5
5
  "main": "src/manager/index.js",
6
6
  "files": [
@@ -70,6 +70,7 @@ class BaseCommand {
70
70
  sweepStaleLogs() {
71
71
  const logFiles = [
72
72
  'dev.log',
73
+ 'deploy.log',
73
74
  'emulator.log',
74
75
  'test.log',
75
76
  'production.log',
@@ -1,5 +1,17 @@
1
1
  const BaseCommand = require('./base-command');
2
+ const chalk = require('chalk').default;
2
3
  const powertools = require('node-powertools');
4
+ const attachLogFile = require('../utils/attach-log-file');
5
+ const { execSync } = require('child_process');
6
+ const path = require('path');
7
+ const jetpack = require('fs-jetpack');
8
+
9
+ const DEFAULT_REGION = 'us-central1';
10
+ const PUBLIC_HTTP_FUNCTIONS = [
11
+ 'bm_api',
12
+ 'bm_authBeforeCreate',
13
+ 'bm_authBeforeSignIn',
14
+ ];
3
15
 
4
16
  class DeployCommand extends BaseCommand {
5
17
  async execute() {
@@ -12,9 +24,120 @@ class DeployCommand extends BaseCommand {
12
24
  return;
13
25
  }
14
26
 
15
- // Execute
16
- await powertools.execute('firebase deploy', { log: true, config: { cwd: self.firebaseProjectPath } });
27
+ const logPath = this.getLogsPath('deploy.log');
28
+ attachLogFile(logPath);
29
+ this.log(chalk.gray(` Logs saving to: ${logPath}\n`));
30
+
31
+ try {
32
+ await powertools.execute('firebase deploy', {
33
+ log: false,
34
+ config: {
35
+ cwd: self.firebaseProjectPath,
36
+ stdio: ['inherit', 'pipe', 'pipe'],
37
+ env: { ...process.env, FORCE_COLOR: '1' },
38
+ },
39
+ }, (child) => {
40
+ child.stdout.on('data', (data) => process.stdout.write(data));
41
+ child.stderr.on('data', (data) => process.stderr.write(data));
42
+ });
43
+
44
+ // After successful deploy, ensure HTTP functions are publicly invocable
45
+ await this.ensurePublicInvoker();
46
+ } finally {
47
+ await attachLogFile.detach();
48
+ }
49
+ }
50
+
51
+ /**
52
+ * Ensure HTTP-triggered functions have allUsers as cloudfunctions.invoker.
53
+ *
54
+ * Firebase CLI used to set this automatically but stopped around the Node 10
55
+ * runtime transition. Without it, HTTP requests get a 403 at the IAM level
56
+ * before BEM's application-level auth (backendManagerKey) can run.
57
+ */
58
+ async ensurePublicInvoker() {
59
+ const projectId = this.getProjectId();
60
+
61
+ if (!projectId) {
62
+ return;
63
+ }
64
+
65
+ const gcloud = this.findGcloud();
66
+
67
+ if (!gcloud) {
68
+ this.log(chalk.gray('\n Skipping public invoker check (gcloud not found)\n'));
69
+ return;
70
+ }
71
+
72
+ this.log(chalk.gray('\n Ensuring HTTP functions are publicly invocable...\n'));
73
+
74
+ let fixed = 0;
75
+ let ok = 0;
76
+
77
+ for (const fnName of PUBLIC_HTTP_FUNCTIONS) {
78
+ try {
79
+ // Check current IAM policy
80
+ const policyOutput = execSync(
81
+ `${gcloud} functions get-iam-policy ${fnName} --project ${projectId} --region ${DEFAULT_REGION} --format=json`,
82
+ { encoding: 'utf8', timeout: 15000, stdio: ['pipe', 'pipe', 'pipe'] },
83
+ );
84
+
85
+ const policy = JSON.parse(policyOutput);
86
+ const hasAllUsers = (policy.bindings || []).some(
87
+ (b) => b.role === 'roles/cloudfunctions.invoker'
88
+ && (b.members || []).includes('allUsers'),
89
+ );
90
+
91
+ if (hasAllUsers) {
92
+ ok++;
93
+ continue;
94
+ }
95
+
96
+ // Add allUsers as invoker
97
+ execSync(
98
+ `${gcloud} functions add-iam-policy-binding ${fnName} --project ${projectId} --region ${DEFAULT_REGION} --member="allUsers" --role="roles/cloudfunctions.invoker"`,
99
+ { encoding: 'utf8', timeout: 30000, stdio: ['pipe', 'pipe', 'pipe'] },
100
+ );
101
+
102
+ this.log(` ${chalk.green('✓')} Set public invoker on ${chalk.cyan(fnName)}`);
103
+ fixed++;
104
+ } catch {
105
+ // Function might not exist (not all projects use all functions) — skip silently
106
+ }
107
+ }
108
+
109
+ if (fixed > 0) {
110
+ this.log(` ${chalk.green('✓')} Public invoker: ${ok + fixed} functions accessible (${fixed} just fixed)\n`);
111
+ } else if (ok > 0) {
112
+ this.log(` ${chalk.green('✓')} Public invoker: ${ok} functions accessible\n`);
113
+ }
114
+ }
115
+
116
+ getProjectId() {
117
+ try {
118
+ const firebaserc = jetpack.read(path.join(this.main.firebaseProjectPath, '.firebaserc'), 'json');
119
+ return firebaserc?.projects?.default;
120
+ } catch {
121
+ return null;
122
+ }
123
+ }
124
+
125
+ findGcloud() {
126
+ const homeDir = require('os').homedir();
127
+ const sdkPath = path.join(homeDir, 'google-cloud-sdk', 'bin', 'gcloud');
128
+
129
+ if (jetpack.exists(sdkPath)) {
130
+ return sdkPath;
131
+ }
132
+
133
+ // Try PATH
134
+ try {
135
+ execSync('which gcloud', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] });
136
+ return 'gcloud';
137
+ } catch {
138
+ return null;
139
+ }
17
140
  }
18
141
  }
19
142
 
20
- module.exports = DeployCommand;
143
+ module.exports = DeployCommand;
@@ -16,6 +16,11 @@ class FirebaseAdminTest extends BaseTest {
16
16
  const mine = this.context.package.dependencies[pkg];
17
17
  const bemv = this.context.packageJSON.peerDependencies[pkg];
18
18
 
19
+ // Not in package.json at all — must install
20
+ if (!mine) {
21
+ return false;
22
+ }
23
+
19
24
  // Get level difference
20
25
  const levelDifference = wonderfulVersion.levelDifference(latest, mine);
21
26
 
@@ -16,6 +16,11 @@ class FirebaseFunctionsTest extends BaseTest {
16
16
  const mine = this.context.package.dependencies[pkg];
17
17
  const bemv = this.context.packageJSON.peerDependencies[pkg];
18
18
 
19
+ // Not in package.json at all — must install
20
+ if (!mine) {
21
+ return false;
22
+ }
23
+
19
24
  // Get level difference
20
25
  const levelDifference = wonderfulVersion.levelDifference(latest, mine);
21
26
 
@@ -48,7 +48,7 @@ class RootPackageJsonTest extends BaseTest {
48
48
  scripts[name] = `cd functions && ${command}`;
49
49
  }
50
50
 
51
- scripts.preinstall = `cd functions && npm install && echo "\\n ✓ Dependencies installed in functions/ (not project root)\\n" && exit 1`;
51
+ scripts.preinstall = `cd functions && npm install && echo "\\n ✓ Dependencies installed in functions/ (not project root)\\n"`;
52
52
 
53
53
  return scripts;
54
54
  }
@@ -255,29 +255,13 @@ class SetupCommand extends BaseCommand {
255
255
  scaffoldPackageJson() {
256
256
  const self = this.main;
257
257
  const ui = this.ui;
258
- let changed = false;
259
-
260
- // Ensure backend-manager is in dependencies (Cloud Functions only installs deps, not devDeps)
261
- if (self.package.devDependencies?.['backend-manager'] && !self.package.dependencies?.['backend-manager']) {
262
- const version = self.package.devDependencies['backend-manager'];
263
- self.package.dependencies = self.package.dependencies || {};
264
- self.package.dependencies['backend-manager'] = version;
265
- delete self.package.devDependencies['backend-manager'];
266
- if (Object.keys(self.package.devDependencies).length === 0) {
267
- delete self.package.devDependencies;
268
- }
269
- changed = true;
270
- }
271
258
 
272
259
  if (!self.package.engines || !self.package.engines.node) {
273
260
  const nodeVer = String(parseInt(process.versions.node, 10));
274
261
  self.package.engines = self.package.engines || {};
275
262
  self.package.engines.node = nodeVer;
276
- changed = true;
277
- }
278
-
279
- if (changed) {
280
263
  jetpack.write(`${self.firebaseProjectPath}/functions/package.json`, JSON.stringify(self.package, null, 2));
264
+ ui.status('add', `Added ${chalk.cyan('engines.node')} = ${chalk.bold(nodeVer)} to package.json`, { level: 2 });
281
265
  }
282
266
  }
283
267
 
@@ -0,0 +1,92 @@
1
+ // attachLogFile(filePath) — duplicate process.stdout + process.stderr writes to a log file.
2
+ //
3
+ // Mirrors BXM/UJM/EM's attach-log-file pattern. Lets devs (and Claude) `tail -f` a log file to
4
+ // see every line of output a process produces — child process stdout/stderr, console.log calls,
5
+ // the works.
6
+ //
7
+ // ANSI color codes are stripped from the file output so it's grep-friendly. The console continues
8
+ // to receive the original colored output unchanged.
9
+ //
10
+ // The default export is a process-wide SINGLETON (the common case: a CLI command tees its whole
11
+ // run to one file). `attachLogFile.createTee()` returns an INDEPENDENT tee with its own state.
12
+ // Tees STACK: a later attach() captures the CURRENT `process.stdout.write` (which may already be
13
+ // an outer tee) as its "original", so writes fan out through every layer and detach() restores
14
+ // the exact prior writer in LIFO order.
15
+ //
16
+ // Idempotent: calling attach() twice with the same path on one tee returns the existing handle.
17
+
18
+ const fs = require('fs');
19
+ const path = require('path');
20
+
21
+ const ANSI_PATTERN = /\x1B\[[0-9;]*[a-zA-Z]/g;
22
+
23
+ function stripAnsi(s) {
24
+ return String(s).replace(ANSI_PATTERN, '');
25
+ }
26
+
27
+ function createTee() {
28
+ let activeStream = null;
29
+ let activePath = null;
30
+ let originalStdoutWrite = null;
31
+ let originalStderrWrite = null;
32
+
33
+ function attach(filePath) {
34
+ if (!filePath) return null;
35
+ const abs = path.resolve(filePath);
36
+
37
+ if (activeStream && activePath === abs) return activeStream;
38
+ if (activeStream) detach();
39
+
40
+ fs.mkdirSync(path.dirname(abs), { recursive: true });
41
+ const stream = fs.createWriteStream(abs, { flags: 'w' });
42
+
43
+ stream.write(`# bem log — ${new Date().toISOString()} — pid=${process.pid}\n`);
44
+
45
+ originalStdoutWrite = process.stdout.write.bind(process.stdout);
46
+ originalStderrWrite = process.stderr.write.bind(process.stderr);
47
+
48
+ process.stdout.write = function (chunk, ...rest) {
49
+ try { stream.write(stripAnsi(String(chunk))); } catch (e) { /* ignore */ }
50
+ return originalStdoutWrite(chunk, ...rest);
51
+ };
52
+ process.stderr.write = function (chunk, ...rest) {
53
+ try { stream.write(stripAnsi(String(chunk))); } catch (e) { /* ignore */ }
54
+ return originalStderrWrite(chunk, ...rest);
55
+ };
56
+
57
+ activeStream = stream;
58
+ activePath = abs;
59
+
60
+ return stream;
61
+ }
62
+
63
+ function detach() {
64
+ if (originalStdoutWrite) process.stdout.write = originalStdoutWrite;
65
+ if (originalStderrWrite) process.stderr.write = originalStderrWrite;
66
+ const stream = activeStream;
67
+ activeStream = null;
68
+ activePath = null;
69
+ originalStdoutWrite = null;
70
+ originalStderrWrite = null;
71
+
72
+ return new Promise((resolve) => {
73
+ if (!stream) {
74
+ return resolve();
75
+ }
76
+ stream.end(resolve);
77
+ });
78
+ }
79
+
80
+ return { attach, detach };
81
+ }
82
+
83
+ const singleton = createTee();
84
+
85
+ function attachLogFile(filePath) {
86
+ return singleton.attach(filePath);
87
+ }
88
+
89
+ module.exports = attachLogFile;
90
+ module.exports.detach = singleton.detach;
91
+ module.exports.stripAnsi = stripAnsi;
92
+ module.exports.createTee = createTee;
@@ -1566,6 +1566,7 @@
1566
1566
  "cuirushi.org",
1567
1567
  "cumnnm.us.ci",
1568
1568
  "cuoly.com",
1569
+ "cuongaquarium.com",
1569
1570
  "cupbest.com",
1570
1571
  "curlhph.tk",
1571
1572
  "currentmail.com",
@@ -1883,6 +1884,7 @@
1883
1884
  "dotman.de",
1884
1885
  "dotmsg.com",
1885
1886
  "dotool8.top",
1887
+ "dotos.dev",
1886
1888
  "dotslashrage.com",
1887
1889
  "dotvu.net",
1888
1890
  "dotzi.net",
@@ -3540,6 +3542,7 @@
3540
3542
  "jajxz.com",
3541
3543
  "jakarta.io.vn",
3542
3544
  "jakemsr.com",
3545
+ "jakl.li",
3543
3546
  "jamesjfattorini.rocks",
3544
3547
  "jamesthynnealmshouse.co.uk",
3545
3548
  "jamyjmkqdsc.com",
@@ -5293,6 +5296,7 @@
5293
5296
  "pazard.com",
5294
5297
  "pazuric.com",
5295
5298
  "pbhak.dev",
5299
+ "pcaa.lol",
5296
5300
  "pcbb.lol",
5297
5301
  "pckage.com",
5298
5302
  "pdf-cutter.com",
@@ -6136,6 +6140,7 @@
6136
6140
  "somelora.com",
6137
6141
  "somoj.com",
6138
6142
  "son2d.site",
6143
+ "songsan.business",
6139
6144
  "songsign.com",
6140
6145
  "soniot.fr",
6141
6146
  "soniot.info",
@@ -7068,6 +7073,7 @@
7068
7073
  "verityemail.site",
7069
7074
  "vermutlich.net",
7070
7075
  "vernonbourne.uk",
7076
+ "veronicamira.info",
7071
7077
  "vertexinbox.com",
7072
7078
  "vertexium.net",
7073
7079
  "veruvercomail.com",