backend-manager 5.2.18 → 5.2.19

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.
@@ -11,6 +11,7 @@ const bem_allRulesRegex = /(\/\/\/---backend-manager---\/\/\/)(.*?)(\/\/\/------
11
11
  class SetupCommand extends BaseCommand {
12
12
  async execute() {
13
13
  const self = this.main;
14
+ const ui = this.ui;
14
15
 
15
16
  // Load config
16
17
  await this.loadConfig();
@@ -24,7 +25,7 @@ class SetupCommand extends BaseCommand {
24
25
  attempt++;
25
26
 
26
27
  if (maxAttempts > 1) {
27
- this.logSuccess(`\n==== SETUP ATTEMPT ${attempt}/${maxAttempts} ====`);
28
+ ui.section(`Attempt ${attempt}/${maxAttempts}`);
28
29
  }
29
30
 
30
31
  // Reset counters so each attempt starts fresh
@@ -35,16 +36,13 @@ class SetupCommand extends BaseCommand {
35
36
 
36
37
  const allPassed = self.testCount === self.testTotal;
37
38
  if (allPassed) {
38
- if (maxAttempts > 1 && attempt > 1) {
39
- this.logSuccess(`\nAll checks passed on attempt ${attempt}/${maxAttempts}.`);
40
- }
41
39
  return;
42
40
  }
43
41
 
44
42
  if (attempt < maxAttempts) {
45
- this.logWarning(`\nAttempt ${attempt}/${maxAttempts} had failures. Retrying...`);
43
+ ui.status('warn', `Attempt ${attempt}/${maxAttempts} had failures — retrying…`);
46
44
  } else if (maxAttempts > 1) {
47
- this.logWarning(`\nReached retry limit (${maxAttempts}). Some checks still failing.`);
45
+ ui.status('warn', `Reached retry limit (${maxAttempts}) some checks still failing`);
48
46
  }
49
47
  }
50
48
  }
@@ -61,9 +59,15 @@ class SetupCommand extends BaseCommand {
61
59
 
62
60
  async runSetup() {
63
61
  const self = this.main;
62
+ const ui = this.ui;
64
63
  let cwd = jetpack.cwd();
65
64
 
66
- this.logSuccess(`\n---- RUNNING SETUP v${self.default.version} ----`);
65
+ // OMEGA-style banner. Replaces the old `---- RUNNING SETUP ---- ` line.
66
+ ui.banner(`Backend Manager ${chalk.dim(`v${self.default.version}`)}`);
67
+
68
+ // Fresh summary collector for this run (the test runner records into it and
69
+ // prints it on a hard failure; we print it here on success).
70
+ self.setupSummary = new ui.Summary().start();
67
71
 
68
72
  // Load files
69
73
  self.package = loadJSON(`${self.firebaseProjectPath}/functions/package.json`);
@@ -76,14 +80,16 @@ class SetupCommand extends BaseCommand {
76
80
 
77
81
  // Check if package exists
78
82
  if (!hasContent(self.package)) {
79
- this.logError(`Missing functions/package.json :(`);
80
- return;
83
+ ui.status('fail', `Missing ${chalk.bold('functions/package.json')}`);
84
+ ui.note(`Run ${chalk.bold('npx mgr setup')} from inside the ${chalk.bold('functions')} folder of a Firebase project.`);
85
+ process.exit(1);
81
86
  }
82
87
 
83
88
  // Check if we're running from the functions folder
84
89
  if (!cwd.endsWith('functions') && !cwd.endsWith('functions/')) {
85
- this.logError(`Please run ${chalk.bold('npx bm setup')} from the ${chalk.bold('functions')} folder. Run ${chalk.bold('cd functions')}.`);
86
- return;
90
+ ui.status('fail', `Wrong directory`);
91
+ ui.note(`Run ${chalk.bold('npx mgr setup')} from the ${chalk.bold('functions')} folder. Try ${chalk.bold('cd functions')} first.`);
92
+ process.exit(1);
87
93
  }
88
94
 
89
95
  // Load the rules files
@@ -96,9 +102,12 @@ class SetupCommand extends BaseCommand {
96
102
  self.projectUrl = `https://console.firebase.google.com/project/${self.projectId}`;
97
103
  self.apiUrl = `https://api.${(self.bemConfigJSON.brand?.url || '').replace(/^https?:\/\//, '')}`;
98
104
 
99
- // Log
100
- this.log(`ID: `, chalk.bold(`${self.projectId}`));
101
- this.log(`URL:`, chalk.bold(`${self.projectUrl}`));
105
+ // Divider-wrapped header with the project name + Firebase console link.
106
+ const brandName = self.bemConfigJSON.brand?.name || self.projectId;
107
+ ui.header(brandName, { subtitle: self.projectUrl });
108
+ ui.blank();
109
+ ui.field('Project', self.projectId, { pad: 9 });
110
+ ui.field('API', self.apiUrl, { pad: 9, valueColor: chalk.cyan });
102
111
 
103
112
  if (!self.package || !self.package.engines || !self.package.engines.node) {
104
113
  throw new Error('Missing <engines.node> in package.json');
@@ -109,26 +118,26 @@ class SetupCommand extends BaseCommand {
109
118
 
110
119
  // Copy / merge defaults into consumer project root (matches EM/BXM/UJM pattern).
111
120
  // Runs BEFORE tests so any test that inspects scaffolded files sees the merged state.
121
+ ui.section('Defaults');
112
122
  this.copyDefaults();
113
123
 
114
124
  // Run all tests
125
+ ui.section('Checks');
115
126
  await this.runTests();
116
127
 
117
- // Log if using local backend-manager
128
+ // Warn if using local backend-manager
118
129
  if (self.package.dependencies['backend-manager'].includes('file:')) {
119
- this.log('\n' + chalk.yellow(chalk.bold('Warning: ') + 'You are using the local ' + chalk.bold('backend-manager')));
120
- } else {
121
- this.log('\n');
130
+ ui.section('Notices');
131
+ ui.status('warn', `Using the local ${chalk.bold('backend-manager')} source (file: dependency)`, { level: 2 });
122
132
  }
123
133
 
124
134
  // Fetch stats
135
+ ui.section('Stats');
125
136
  await this.fetchStats();
126
137
 
127
- // Log results
128
- this.logSuccess(`Checks finished. Passed ${self.testCount}/${self.testTotal} tests.`);
129
- if (self.testCount !== self.testTotal) {
130
- this.logWarning(`You should continue to run ${chalk.bold('npx bm setup')} until you pass all tests and fix all errors.`);
131
- }
138
+ // Everything passed (a hard failure would have exited via haltSetup). Print
139
+ // the OMEGA-style summary block.
140
+ self.setupSummary.print();
132
141
 
133
142
  // Notify parent if exists
134
143
  if (process.send) {
@@ -162,10 +171,12 @@ class SetupCommand extends BaseCommand {
162
171
  // # ========== Custom Values ========== (consumer-owned)
163
172
  copyDefaults() {
164
173
  const self = this.main;
174
+ const ui = this.ui;
165
175
  const defaultsDir = path.resolve(`${__dirname}/../../defaults`);
166
176
 
167
177
  if (!jetpack.exists(defaultsDir)) {
168
178
  // Defaults dir is optional — older BEM versions didn't have one. If missing, skip silently.
179
+ ui.note('No defaults to scaffold', 2);
169
180
  return;
170
181
  }
171
182
 
@@ -175,6 +186,9 @@ class SetupCommand extends BaseCommand {
175
186
  // matches the EM/BXM/UJM contract if we ever add them.
176
187
  const MERGEABLE_BASENAMES = new Set(['.env', '.gitignore', 'CLAUDE.md']);
177
188
 
189
+ // Track whether we emitted any line so we can show an "up to date" note when nothing changed.
190
+ let touched = 0;
191
+
178
192
  const files = jetpack.find(defaultsDir, { matching: '**/*', recursive: true, files: true, directories: false });
179
193
 
180
194
  for (const src of files) {
@@ -201,10 +215,12 @@ class SetupCommand extends BaseCommand {
201
215
  const merged = mergeLineBasedFiles(existing, incoming, basename);
202
216
  if (merged !== existing) {
203
217
  jetpack.write(dest, merged);
204
- this.logSuccess(`Merged default → ${target}`);
218
+ ui.status('change', `Merged ${chalk.cyan(target)}`, { level: 2 });
219
+ touched++;
205
220
  }
206
221
  } catch (e) {
207
- this.logWarning(`Failed to merge ${target}: ${e.message}`);
222
+ ui.status('warn', `Failed to merge ${chalk.cyan(target)}`, { detail: e.message, level: 2 });
223
+ touched++;
208
224
  }
209
225
  continue;
210
226
  }
@@ -215,7 +231,12 @@ class SetupCommand extends BaseCommand {
215
231
 
216
232
  // First time: copy as-is.
217
233
  jetpack.copy(src, dest);
218
- this.logSuccess(`Copied default → ${target}`);
234
+ ui.status('add', `Copied ${chalk.cyan(target)}`, { level: 2 });
235
+ touched++;
236
+ }
237
+
238
+ if (touched === 0) {
239
+ ui.note('All defaults up to date', 2);
219
240
  }
220
241
  }
221
242
 
@@ -281,12 +302,15 @@ class SetupCommand extends BaseCommand {
281
302
  .then(json => json)
282
303
  .catch(e => e);
283
304
 
305
+ const ui = this.ui;
284
306
  if (statsFetchResult instanceof Error) {
285
- if (!statsFetchResult.message.includes('network timeout')) {
286
- this.logWarning(`Ran into error while fetching stats endpoint (${url})`, statsFetchResult);
307
+ if (statsFetchResult.message.includes('network timeout')) {
308
+ ui.status('skip', 'Skipped stats fetch', { detail: 'network timeout', level: 2 });
309
+ } else {
310
+ ui.status('warn', 'Could not fetch stats endpoint', { detail: statsFetchResult.message, level: 2 });
287
311
  }
288
312
  } else {
289
- this.logSuccess(`Stats fetched/created properly.`);
313
+ ui.status('pass', 'Stats fetched/created', { level: 2 });
290
314
  }
291
315
  }
292
316
  }
package/src/cli/index.js CHANGED
@@ -164,47 +164,76 @@ Main.prototype.process = async function (args) {
164
164
  // Test method for setup command
165
165
  Main.prototype.test = async function(name, fn, fix, args) {
166
166
  const self = this;
167
- let status;
168
167
  const chalk = require('chalk').default;
169
-
170
- return new Promise(async function(resolve, reject) {
171
- let passed = await fn();
172
-
173
- if (passed instanceof Error) {
174
- console.log(chalk.red(passed));
175
- process.exit(0);
176
- } else if (passed) {
177
- status = chalk.green('passed');
178
- self.testCount++;
179
- self.testTotal++;
180
- } else {
181
- status = chalk.red('failed');
182
- self.testTotal++;
183
- }
184
- console.log(chalk.bold(`[${self.testTotal}]`), `${name}:`, status);
185
- if (!passed) {
186
- console.log(chalk.yellow(`Fixing...`));
187
- fix(self, args)
188
- .then((r) => {
189
- console.log(chalk.green(`...done~!`));
190
- resolve();
191
- })
192
- .catch((e) => {
193
- console.log(chalk.red(`Failed to fix: ${e}`));
194
- if (self.options['--continue']) {
195
- console.log(chalk.yellow('⚠️ Continuing despite error because of --continue flag\n'));
196
- setTimeout(function () {
197
- resolve();
198
- }, 5000);
199
- } else {
200
- console.log(chalk.yellow('To force the setup to continue, run with the --continue flag\n'));
201
- reject();
202
- }
203
- });
204
- } else {
205
- resolve();
168
+ const ui = require('./utils/ui');
169
+
170
+ // Prints ` [N] <symbol> <name>` the OMEGA-style per-check status line
171
+ // (indented one level under the `[CHECKS]` section label).
172
+ const printLine = (index, kind, label) => {
173
+ const colorByKind = { pass: chalk.green, fail: chalk.red, warn: chalk.yellow };
174
+ const color = colorByKind[kind] || chalk.white;
175
+ const suffix = label ? ` ${chalk.dim(label)}` : '';
176
+ console.log(`${ui.indent(2)}${chalk.dim(`[${index}]`)} ${color(ui.SYMBOLS[kind])} ${name}${suffix}`);
177
+ };
178
+
179
+ let passed = await fn();
180
+
181
+ // A check that returns an Error is a hard, unrecoverable failure (e.g. wrong
182
+ // Node version). Print it cleanly and stop — no auto-fix is possible.
183
+ if (passed instanceof Error) {
184
+ self.testTotal++;
185
+ printLine(self.testTotal, 'fail', '');
186
+ const message = passed.message || String(passed);
187
+ ui.status('fail', chalk.red(message), { level: 3 });
188
+ if (self.setupSummary) { self.setupSummary.fail(name, [chalk.red(message)]); }
189
+ self.haltSetup();
190
+ return;
191
+ }
192
+
193
+ if (passed) {
194
+ self.testCount++;
195
+ self.testTotal++;
196
+ if (self.setupSummary) { self.setupSummary.pass(); }
197
+ printLine(self.testTotal, 'pass', '');
198
+ return;
199
+ }
200
+
201
+ // Failed → attempt a fix.
202
+ self.testTotal++;
203
+ printLine(self.testTotal, 'warn', '— fixing…');
204
+
205
+ try {
206
+ await fix(self, args);
207
+ self.testCount++;
208
+ if (self.setupSummary) { self.setupSummary.pass(); }
209
+ ui.status('pass', chalk.green('fixed'), { level: 3 });
210
+ } catch (e) {
211
+ // The fix couldn't complete. `e.summaryDetails` (if present) is an array of
212
+ // pre-formatted detail lines the failing check wants surfaced in the summary.
213
+ const message = e && e.message ? e.message : String(e);
214
+ const details = (e && Array.isArray(e.summaryDetails)) ? e.summaryDetails : [chalk.red(message)];
215
+ ui.status('fail', chalk.red(`Could not fix: ${message}`), { level: 3 });
216
+ if (self.setupSummary) { self.setupSummary.fail(name, details); }
217
+
218
+ if (self.options['--continue']) {
219
+ ui.status('warn', chalk.yellow('Continuing despite error (--continue flag)'), { level: 3 });
220
+ return;
206
221
  }
207
- });
222
+
223
+ self.haltSetup();
224
+ }
225
+ };
226
+
227
+ // Halt the setup run on an unfixable failure. The failing check must already be
228
+ // recorded via `setupSummary.fail(...)` by the caller; this prints the summary
229
+ // block + a re-run hint and exits with code 1 — instead of rejecting a promise
230
+ // with no reason (which surfaced as an ugly `UnhandledPromiseRejection: undefined`).
231
+ Main.prototype.haltSetup = function() {
232
+ if (this.setupSummary) {
233
+ this.setupSummary.print({ hint: `Fix the above, then run ${require('chalk').default.bold('npx mgr setup')} again.` });
234
+ }
235
+
236
+ process.exit(1);
208
237
  };
209
238
 
210
239
  module.exports = Main;
@@ -0,0 +1,270 @@
1
+ const chalk = require('chalk').default;
2
+
3
+ /**
4
+ * Shared CLI styling helpers — the SSOT for BEM's console output look.
5
+ *
6
+ * Mirrors the OMEGA Manager (omega-manager) styling conventions so every BEM
7
+ * command renders with the same dividers, indentation, timestamps, colors, and
8
+ * status symbols. Pull these helpers into any command (setup/serve/deploy/test/
9
+ * emulator/...) instead of hand-rolling chalk + console.log.
10
+ *
11
+ * Conventions (matching OMEGA):
12
+ * - 70-char `━` horizontal rules wrap section titles.
13
+ * - Indentation is 2 spaces per level (level 1 = 2 spaces, level 2 = 4, ...).
14
+ * - Dimmed labels (`ID:`, `URL:`, `Local:`) with normal-weight values.
15
+ * - Status symbols: → (running) ✓ (pass) ✗ (fail) ⊘ (skip) ⚠ (warn) ✅ (done).
16
+ * - Timestamps via `new Date().toLocaleTimeString()` (e.g. "7:47:12 PM").
17
+ */
18
+
19
+ // Divider width + character (OMEGA uses 70 × `━`).
20
+ const RULE_WIDTH = 70;
21
+ const RULE_CHAR = '━';
22
+
23
+ // Status symbols — the single source of truth for BEM's CLI iconography.
24
+ const SYMBOLS = {
25
+ running: '→',
26
+ pass: '✓',
27
+ fail: '✗',
28
+ skip: '⊘',
29
+ warn: '⚠',
30
+ done: '✅',
31
+ rocket: '🚀',
32
+ add: '+',
33
+ change: '↻',
34
+ };
35
+
36
+ /** Two-space-per-level indentation. */
37
+ function indent(level = 1) {
38
+ return ' '.repeat(level);
39
+ }
40
+
41
+ /** Locale time string, e.g. "7:47:12 PM". */
42
+ function timestamp() {
43
+ return new Date().toLocaleTimeString();
44
+ }
45
+
46
+ /** A full-width horizontal rule in the given chalk color (default cyan). */
47
+ function rule(color = chalk.cyan) {
48
+ return color(RULE_CHAR.repeat(RULE_WIDTH));
49
+ }
50
+
51
+ /** Print a blank line. */
52
+ function blank() {
53
+ console.log('');
54
+ }
55
+
56
+ /**
57
+ * Print the top-level program banner, e.g. `🚀 Backend Manager`.
58
+ * @param {string} title
59
+ */
60
+ function banner(title) {
61
+ blank();
62
+ console.log(chalk.bold.cyan(`${SYMBOLS.rocket} ${title}`));
63
+ blank();
64
+ }
65
+
66
+ /**
67
+ * Print a divider-wrapped section header with an optional subtitle + timestamp.
68
+ *
69
+ * ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
70
+ * Title subtitle @ 7:47:12 PM
71
+ * ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
72
+ *
73
+ * @param {string} title - Bold white title.
74
+ * @param {object} [opts]
75
+ * @param {string} [opts.subtitle] - Dimmed/colored text after the title (e.g. a URL).
76
+ * @param {Function} [opts.subtitleColor] - chalk fn for the subtitle (default cyan).
77
+ * @param {boolean} [opts.time=true] - Append `@ <time>`.
78
+ * @param {Function} [opts.color] - chalk fn for the rules (default cyan).
79
+ */
80
+ function header(title, opts = {}) {
81
+ const color = opts.color || chalk.cyan;
82
+ const subtitleColor = opts.subtitleColor || chalk.cyan;
83
+ const time = opts.time !== false;
84
+
85
+ const parts = [chalk.bold.white(title)];
86
+ if (opts.subtitle) {
87
+ parts.push(subtitleColor(opts.subtitle));
88
+ }
89
+ if (time) {
90
+ parts.push(chalk.dim(`@ ${timestamp()}`));
91
+ }
92
+
93
+ console.log(rule(color));
94
+ console.log(`${indent(1)}${parts.join(' ')}`);
95
+ console.log(rule(color));
96
+ }
97
+
98
+ /**
99
+ * Print a magenta section label like `[SETUP]`.
100
+ * @param {string} label
101
+ */
102
+ function section(label) {
103
+ blank();
104
+ console.log(`${indent(1)}${chalk.bold.magenta(`[${String(label).toUpperCase()}]`)}`);
105
+ }
106
+
107
+ /**
108
+ * Print a dimmed `Label: value` line at a given indent level.
109
+ * @param {string} label
110
+ * @param {string} value
111
+ * @param {object} [opts]
112
+ * @param {number} [opts.level=1]
113
+ * @param {number} [opts.pad] - Pad the label (incl. trailing colon) to this width for alignment.
114
+ * @param {Function} [opts.valueColor] - chalk fn for the value (default none).
115
+ */
116
+ function field(label, value, opts = {}) {
117
+ const level = opts.level || 1;
118
+ let labelText = `${label}:`;
119
+ if (opts.pad) {
120
+ labelText = labelText.padEnd(opts.pad);
121
+ }
122
+ const valueText = opts.valueColor ? opts.valueColor(value) : value;
123
+ console.log(`${indent(level)}${chalk.dim(labelText)} ${valueText}`);
124
+ }
125
+
126
+ /**
127
+ * Print a status line: `<symbol> <text>` at a given indent level.
128
+ * @param {('running'|'pass'|'fail'|'skip'|'warn'|'add'|'change')} kind
129
+ * @param {string} text
130
+ * @param {object} [opts]
131
+ * @param {number} [opts.level=1]
132
+ * @param {string} [opts.detail] - Dimmed trailing detail.
133
+ */
134
+ function status(kind, text, opts = {}) {
135
+ const level = opts.level || 1;
136
+ const colorByKind = {
137
+ running: chalk.dim,
138
+ pass: chalk.green,
139
+ fail: chalk.red,
140
+ skip: chalk.dim,
141
+ warn: chalk.yellow,
142
+ add: chalk.green,
143
+ change: chalk.yellow,
144
+ };
145
+ const color = colorByKind[kind] || chalk.white;
146
+ const symbol = SYMBOLS[kind] || '';
147
+ const detail = opts.detail ? ` ${chalk.dim(opts.detail)}` : '';
148
+ console.log(`${indent(level)}${color(symbol)} ${text}${detail}`);
149
+ }
150
+
151
+ /** Print a plain dimmed note line. */
152
+ function note(text, level = 1) {
153
+ console.log(`${indent(level)}${chalk.dim(text)}`);
154
+ }
155
+
156
+ /**
157
+ * Collects setup-check results and prints an OMEGA-style summary block.
158
+ *
159
+ * Used by the setup command's test runner. `start()` stamps a wall-clock so the
160
+ * summary can report duration; `passed()` / `failed()` record outcomes; the
161
+ * failing check (if any) carries optional detail lines to surface in the block.
162
+ */
163
+ class Summary {
164
+ constructor() {
165
+ this.startTime = null;
166
+ this.passes = 0;
167
+ this.fails = [];
168
+ }
169
+
170
+ start() {
171
+ this.startTime = Date.now();
172
+ return this;
173
+ }
174
+
175
+ pass() {
176
+ this.passes++;
177
+ }
178
+
179
+ /**
180
+ * @param {string} name - Check name.
181
+ * @param {string[]} [details] - Pre-formatted detail lines to show under the failure.
182
+ */
183
+ fail(name, details = []) {
184
+ this.fails.push({ name, details });
185
+ }
186
+
187
+ get total() {
188
+ return this.passes + this.fails.length;
189
+ }
190
+
191
+ /**
192
+ * Format milliseconds into "Xs" / "Xm Ys" / "Xh Ym Zs".
193
+ * @param {number} ms
194
+ */
195
+ _formatDuration(ms) {
196
+ const seconds = Math.floor(ms / 1000);
197
+ if (seconds < 60) {
198
+ return `${seconds}s`;
199
+ }
200
+ const minutes = Math.floor(seconds / 60);
201
+ const remSeconds = seconds % 60;
202
+ if (minutes < 60) {
203
+ return `${minutes}m ${remSeconds}s`;
204
+ }
205
+ const hours = Math.floor(minutes / 60);
206
+ const remMinutes = minutes % 60;
207
+ return `${hours}h ${remMinutes}m ${remSeconds}s`;
208
+ }
209
+
210
+ /**
211
+ * Print the summary block. Green ✅ when everything passed, yellow ⚠ otherwise.
212
+ * @param {object} [opts]
213
+ * @param {string} [opts.hint] - A closing call-to-action line (e.g. how to re-run).
214
+ */
215
+ print(opts = {}) {
216
+ const hasErrors = this.fails.length > 0;
217
+ const headerColor = hasErrors ? chalk.yellow : chalk.green;
218
+ const icon = hasErrors ? SYMBOLS.warn : SYMBOLS.done;
219
+ const elapsed = this.startTime ? this._formatDuration(Date.now() - this.startTime) : '0s';
220
+
221
+ blank();
222
+ console.log(headerColor(RULE_CHAR.repeat(RULE_WIDTH)));
223
+ console.log(`${indent(1)}${headerColor.bold(`${icon} Summary`)}`);
224
+ console.log(headerColor(RULE_CHAR.repeat(RULE_WIDTH)));
225
+
226
+ blank();
227
+ field('Checks', String(this.total), { pad: 11 });
228
+ field('Duration', elapsed, { pad: 11 });
229
+ const failText = hasErrors
230
+ ? chalk.red(`${this.fails.length} failed`)
231
+ : chalk.dim('0 failed');
232
+ field('Results', `${chalk.green(`${this.passes} passed`)}${chalk.dim(', ')}${failText}`, { pad: 11 });
233
+
234
+ if (hasErrors) {
235
+ blank();
236
+ for (const { name, details } of this.fails) {
237
+ console.log(`${indent(1)}${chalk.red(SYMBOLS.fail)} ${chalk.bold(name)}`);
238
+ for (const line of details) {
239
+ console.log(`${indent(3)}${line}`);
240
+ }
241
+ }
242
+ }
243
+
244
+ if (opts.hint) {
245
+ blank();
246
+ console.log(`${indent(1)}${chalk.dim(SYMBOLS.running)} ${opts.hint}`);
247
+ }
248
+
249
+ blank();
250
+ console.log(headerColor(RULE_CHAR.repeat(RULE_WIDTH)));
251
+ }
252
+ }
253
+
254
+ module.exports = {
255
+ chalk,
256
+ RULE_WIDTH,
257
+ RULE_CHAR,
258
+ SYMBOLS,
259
+ indent,
260
+ timestamp,
261
+ rule,
262
+ blank,
263
+ banner,
264
+ header,
265
+ section,
266
+ field,
267
+ status,
268
+ note,
269
+ Summary,
270
+ };
@@ -1277,6 +1277,7 @@
1277
1277
  "doobb.com",
1278
1278
  "doojazz.com",
1279
1279
  "doquier.tk",
1280
+ "doreact.com",
1280
1281
  "dotapodemail.com",
1281
1282
  "dotman.de",
1282
1283
  "dotmsg.com",
@@ -61,8 +61,10 @@ function mergeLineBasedFiles(existingContent, newContent, fileName) {
61
61
  continue;
62
62
  }
63
63
  if (existingCustomKeys.has(key)) {
64
- // Key the user moved to custom leave it in custom; emit empty default value.
65
- emit(line);
64
+ // The framework newly adopted a key the user had in their Custom section.
65
+ // Promote it UP into Default with the user's value, and drop the Custom copy
66
+ // (handled below) so the key isn't duplicated / left empty.
67
+ emit(findKeyLine(existingCustom, key));
66
68
  continue;
67
69
  }
68
70
  }
@@ -97,10 +99,19 @@ function mergeLineBasedFiles(existingContent, newContent, fileName) {
97
99
  }
98
100
  }
99
101
 
100
- // The user's Custom section is preserved verbatim — except .env values get
101
- // normalized to double-quoted form so the file's quoting style is consistent.
102
+ // The user's Custom section is preserved — except (a) .env values get normalized
103
+ // to double-quoted form for consistent quoting, and (b) any key the framework just
104
+ // adopted into its Default section is dropped here, since it was promoted UP into
105
+ // Default above (otherwise the key would appear in both sections).
102
106
  const finalCustom = isEnvFile
103
- ? existingCustom.map((line) => normalizeEnvLine(line))
107
+ ? existingCustom
108
+ .filter((line) => {
109
+ const trimmed = line.trim();
110
+ if (!trimmed || trimmed.startsWith('#')) return true;
111
+ const key = trimmed.split('=')[0].trim();
112
+ return !(key && newDefaultKeys.has(key));
113
+ })
114
+ .map((line) => normalizeEnvLine(line))
104
115
  : existingCustom;
105
116
 
106
117
  const result = [];
package/templates/_.env CHANGED
@@ -34,6 +34,7 @@ SENDGRID_API_KEY=""
34
34
  BEEHIIV_API_KEY=""
35
35
  ZEROBOUNCE_API_KEY=""
36
36
  UNSUBSCRIBE_HMAC_KEY=""
37
+ APOLLO_API_KEY=""
37
38
 
38
39
  # ========== Custom Values ==========
39
40
  # Add your custom environment variables below this line