backend-manager 5.2.18 → 5.3.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/CHANGELOG.md +35 -0
- package/CLAUDE.md +6 -4
- package/bin/backend-manager +10 -1
- package/docs/admin-post-route.md +2 -0
- package/docs/ai-library.md +29 -2
- package/docs/cli-output.md +122 -0
- package/docs/common-mistakes.md +1 -1
- package/docs/environment-detection.md +87 -3
- package/docs/marketing-campaigns.md +1 -1
- package/docs/payment-system.md +1 -1
- package/docs/testing.md +64 -17
- package/package.json +1 -1
- package/src/cli/commands/base-command.js +4 -0
- package/src/cli/commands/install.js +2 -2
- package/src/cli/commands/setup-tests/bem-config.js +22 -9
- package/src/cli/commands/setup-tests/helpers/merge-line-files.js +22 -168
- package/src/cli/commands/setup.js +65 -33
- package/src/cli/commands/test.js +1 -1
- package/src/cli/index.js +72 -39
- package/src/cli/utils/ui.js +270 -0
- package/src/defaults/CLAUDE.md +2 -2
- package/src/defaults/test/_init.js +14 -0
- package/src/manager/functions/_legacy/actions/create-post-handler.js +1 -1
- package/src/manager/functions/core/actions/api/admin/edit-post.js +1 -1
- package/src/manager/functions/core/actions/api/general/send-email.js +1 -1
- package/src/manager/functions/core/actions/api/user/delete.js +1 -1
- package/src/manager/functions/core/actions/api/user/oauth2.js +1 -1
- package/src/manager/helpers/analytics.js +3 -1
- package/src/manager/helpers/assistant.js +43 -38
- package/src/manager/index.js +76 -12
- package/src/manager/libraries/ai/index.js +33 -0
- package/src/manager/libraries/ai/providers/openai.js +105 -8
- package/src/manager/libraries/content/ghostii.js +76 -16
- package/src/manager/libraries/email/data/disposable-domains.json +24 -0
- package/src/manager/libraries/email/generators/lib/image-illustrator.js +154 -0
- package/src/manager/libraries/email/generators/newsletter.js +16 -2
- package/src/manager/libraries/payment/discount-codes.js +1 -0
- package/src/manager/routes/admin/post/put.js +1 -1
- package/src/manager/routes/handler/post/post.js +1 -1
- package/src/manager/routes/payments/intent/processors/test.js +1 -1
- package/src/manager/routes/user/delete.js +1 -1
- package/src/manager/routes/user/signup/post.js +4 -2
- package/src/test/runner.js +142 -20
- package/src/test/test-accounts.js +159 -102
- package/src/utils/merge-line-files.js +16 -5
- package/templates/_.env +1 -0
- package/test/events/payments/journey-payments-cancel-endpoint.js +6 -2
- package/test/events/payments/journey-payments-cancel.js +6 -3
- package/test/events/payments/journey-payments-failure.js +6 -3
- package/test/events/payments/journey-payments-plan-change.js +6 -3
- package/test/events/payments/journey-payments-refund-webhook.js +6 -2
- package/test/events/payments/journey-payments-suspend.js +6 -3
- package/test/events/payments/journey-payments-trial-cancel.js +6 -3
- package/test/events/payments/journey-payments-trial.js +10 -4
- package/test/events/payments/journey-payments-uid-resolution.js +6 -2
- package/test/events/payments/journey-payments-upgrade.js +6 -3
- package/test/helpers/content/ghostii-blocks.js +134 -0
- package/test/helpers/environment.js +230 -0
- package/test/helpers/merge-line-files.js +271 -0
- package/test/routes/marketing/webhook-forward.js +14 -7
- package/test/routes/payments/cancel.js +4 -1
- package/test/routes/payments/dispute-alert.js +9 -6
- package/test/routes/payments/intent.js +55 -14
- package/test/routes/payments/portal.js +4 -1
- package/test/routes/payments/refund.js +4 -1
|
@@ -38,31 +38,44 @@ class BemConfigTest extends BaseTest {
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
async fix() {
|
|
41
|
-
|
|
42
|
-
console.log(chalk.red(`You need to open backend-manager-config.json and set each of these keys:`));
|
|
41
|
+
const ui = require('../../utils/ui');
|
|
43
42
|
|
|
44
43
|
// Write if it doesn't exist
|
|
45
44
|
if (!this.context.hasContent(this.self.bemConfigJSON)) {
|
|
46
45
|
jetpack.write(`${this.self.firebaseProjectPath}/functions/backend-manager-config.json`, {});
|
|
47
46
|
}
|
|
48
47
|
|
|
49
|
-
//
|
|
48
|
+
// Collect the keys that are still missing (these are what the user must fill in).
|
|
49
|
+
const missing = [];
|
|
50
50
|
powertools.getKeys(bemConfigTemplate).forEach((key) => {
|
|
51
51
|
// Skip if an ancestor is explicitly set to a non-object value (e.g. stripe: false)
|
|
52
52
|
if (this._isAncestorDisabled(key)) {
|
|
53
53
|
return;
|
|
54
54
|
}
|
|
55
|
-
|
|
56
55
|
const userValue = _.get(this.self.bemConfigJSON, key, undefined);
|
|
57
|
-
|
|
58
56
|
if (typeof userValue === 'undefined') {
|
|
59
|
-
|
|
60
|
-
} else {
|
|
61
|
-
console.log(chalk.red(`${key} (${userValue})`));
|
|
57
|
+
missing.push(key);
|
|
62
58
|
}
|
|
63
59
|
});
|
|
64
60
|
|
|
65
|
-
|
|
61
|
+
ui.note(`Open ${chalk.bold('backend-manager-config.json')} and set the missing keys below:`, 3);
|
|
62
|
+
for (const key of missing) {
|
|
63
|
+
console.log(`${ui.indent(4)}${chalk.red('•')} ${key}`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Surface a compact version in the summary block (the full list printed above).
|
|
67
|
+
const preview = missing.slice(0, 8);
|
|
68
|
+
const summaryDetails = [
|
|
69
|
+
chalk.dim(`Set ${chalk.bold(missing.length)} missing key(s) in backend-manager-config.json:`),
|
|
70
|
+
...preview.map((key) => `${chalk.red('•')} ${key}`),
|
|
71
|
+
];
|
|
72
|
+
if (missing.length > preview.length) {
|
|
73
|
+
summaryDetails.push(chalk.dim(`…and ${missing.length - preview.length} more (see list above)`));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const error = new Error('Missing required backend-manager-config.json keys');
|
|
77
|
+
error.summaryDetails = summaryDetails;
|
|
78
|
+
throw error;
|
|
66
79
|
}
|
|
67
80
|
/**
|
|
68
81
|
* Check if any ancestor of a dot-notation key is a non-object value (e.g. false)
|
|
@@ -1,181 +1,35 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* SSOT shim for line-based file merging (.env / .gitignore / CLAUDE.md).
|
|
3
|
+
*
|
|
4
|
+
* The real merge logic lives in `src/utils/merge-line-files.js` — a key-based
|
|
5
|
+
* merge that keeps each KEY under its template header and promotes/migrates keys
|
|
6
|
+
* between the Default/Custom sections correctly. This file used to contain a
|
|
7
|
+
* SECOND, positional implementation that zipped comment lines and value lines by
|
|
8
|
+
* index; an off-by-one there is what historically scrambled consumers' `.env`
|
|
9
|
+
* files. That duplicate is gone — this module now just re-exports the canonical
|
|
10
|
+
* impl and adds the marker-name aliases + `hasSectionMarkers()` the setup tests
|
|
11
|
+
* (`env-file.js`, `gitignore.js`) consume.
|
|
4
12
|
*/
|
|
5
13
|
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
* @param {string} existingContent - Current file content
|
|
12
|
-
* @param {string} newContent - New template content
|
|
13
|
-
* @param {string} fileName - File name (.env or .gitignore)
|
|
14
|
-
* @returns {string} Merged content
|
|
15
|
-
*/
|
|
16
|
-
function mergeLineBasedFiles(existingContent, newContent, fileName) {
|
|
17
|
-
const existingLines = existingContent.split('\n');
|
|
18
|
-
const newLines = newContent.split('\n');
|
|
19
|
-
|
|
20
|
-
const isEnvFile = fileName === '.env';
|
|
21
|
-
|
|
22
|
-
// Parse existing file into default section and custom section
|
|
23
|
-
let defaultSection = [];
|
|
24
|
-
let customSection = [];
|
|
25
|
-
let inCustomSection = false;
|
|
26
|
-
let inDefaultSection = false;
|
|
27
|
-
|
|
28
|
-
const existingDefaultKeys = new Set();
|
|
29
|
-
const existingCustomKeys = new Set();
|
|
30
|
-
|
|
31
|
-
for (const line of existingLines) {
|
|
32
|
-
const trimmed = line.trim();
|
|
33
|
-
|
|
34
|
-
if (trimmed === DEFAULT_SECTION_MARKER) {
|
|
35
|
-
inDefaultSection = true;
|
|
36
|
-
inCustomSection = false;
|
|
37
|
-
continue;
|
|
38
|
-
}
|
|
39
|
-
if (trimmed === CUSTOM_SECTION_MARKER) {
|
|
40
|
-
inCustomSection = true;
|
|
41
|
-
inDefaultSection = false;
|
|
42
|
-
continue;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
if (inCustomSection) {
|
|
46
|
-
customSection.push(line);
|
|
47
|
-
if (isEnvFile && trimmed && !trimmed.startsWith('#')) {
|
|
48
|
-
const key = trimmed.split('=')[0].trim();
|
|
49
|
-
if (key) {
|
|
50
|
-
existingCustomKeys.add(key);
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
} else if (inDefaultSection) {
|
|
54
|
-
defaultSection.push(line);
|
|
55
|
-
if (isEnvFile && trimmed && !trimmed.startsWith('#')) {
|
|
56
|
-
const key = trimmed.split('=')[0].trim();
|
|
57
|
-
if (key) {
|
|
58
|
-
existingDefaultKeys.add(key);
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// Parse new content to build complete default section
|
|
65
|
-
const newDefaultSection = [];
|
|
66
|
-
const newDefaultKeys = new Set();
|
|
67
|
-
|
|
68
|
-
let inNewDefaultSection = false;
|
|
69
|
-
let inNewCustomSection = false;
|
|
70
|
-
|
|
71
|
-
for (const line of newLines) {
|
|
72
|
-
const trimmed = line.trim();
|
|
73
|
-
|
|
74
|
-
if (trimmed === DEFAULT_SECTION_MARKER) {
|
|
75
|
-
inNewDefaultSection = true;
|
|
76
|
-
inNewCustomSection = false;
|
|
77
|
-
continue;
|
|
78
|
-
}
|
|
79
|
-
if (trimmed === CUSTOM_SECTION_MARKER) {
|
|
80
|
-
inNewCustomSection = true;
|
|
81
|
-
inNewDefaultSection = false;
|
|
82
|
-
continue;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
if (inNewDefaultSection) {
|
|
86
|
-
if (isEnvFile && trimmed && !trimmed.startsWith('#')) {
|
|
87
|
-
const key = trimmed.split('=')[0].trim();
|
|
88
|
-
if (key) {
|
|
89
|
-
newDefaultKeys.add(key);
|
|
90
|
-
if (!existingDefaultKeys.has(key) && !existingCustomKeys.has(key)) {
|
|
91
|
-
newDefaultSection.push(line);
|
|
92
|
-
} else {
|
|
93
|
-
newDefaultSection.push(null); // Placeholder
|
|
94
|
-
}
|
|
95
|
-
} else {
|
|
96
|
-
newDefaultSection.push(line);
|
|
97
|
-
}
|
|
98
|
-
} else {
|
|
99
|
-
newDefaultSection.push(line);
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// Merge user's existing default values in the correct order
|
|
105
|
-
const mergedDefaultSection = [];
|
|
106
|
-
let defaultSectionIndex = 0;
|
|
107
|
-
|
|
108
|
-
for (const line of newDefaultSection) {
|
|
109
|
-
if (line === null) {
|
|
110
|
-
while (defaultSectionIndex < defaultSection.length) {
|
|
111
|
-
const userLine = defaultSection[defaultSectionIndex++];
|
|
112
|
-
const trimmed = userLine.trim();
|
|
113
|
-
if (trimmed && !trimmed.startsWith('#')) {
|
|
114
|
-
mergedDefaultSection.push(userLine);
|
|
115
|
-
break;
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
} else {
|
|
119
|
-
mergedDefaultSection.push(line);
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// Find user-added lines in default section that aren't in new defaults
|
|
124
|
-
const userAddedToDefaults = [];
|
|
125
|
-
|
|
126
|
-
for (const line of defaultSection) {
|
|
127
|
-
const trimmed = line.trim();
|
|
128
|
-
|
|
129
|
-
if (!trimmed || trimmed.startsWith('#')) {
|
|
130
|
-
continue;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
if (isEnvFile) {
|
|
134
|
-
const key = trimmed.split('=')[0].trim();
|
|
135
|
-
if (key && !newDefaultKeys.has(key) && !existingCustomKeys.has(key)) {
|
|
136
|
-
userAddedToDefaults.push(line);
|
|
137
|
-
}
|
|
138
|
-
} else {
|
|
139
|
-
const lineExistsInNewDefaults = newLines.some(newLine => {
|
|
140
|
-
return newLine.trim() === trimmed;
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
if (!lineExistsInNewDefaults) {
|
|
144
|
-
userAddedToDefaults.push(line);
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// Build final result
|
|
150
|
-
const result = [];
|
|
151
|
-
|
|
152
|
-
result.push(DEFAULT_SECTION_MARKER);
|
|
153
|
-
result.push(...mergedDefaultSection);
|
|
154
|
-
|
|
155
|
-
result.push('');
|
|
156
|
-
result.push(CUSTOM_SECTION_MARKER);
|
|
157
|
-
|
|
158
|
-
if (userAddedToDefaults.length > 0) {
|
|
159
|
-
result.push(...userAddedToDefaults);
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
result.push(...customSection);
|
|
163
|
-
|
|
164
|
-
return result.join('\n');
|
|
165
|
-
}
|
|
14
|
+
const {
|
|
15
|
+
mergeLineBasedFiles,
|
|
16
|
+
DEFAULT_MARKER,
|
|
17
|
+
CUSTOM_MARKER,
|
|
18
|
+
} = require('../../../../utils/merge-line-files.js');
|
|
166
19
|
|
|
167
20
|
/**
|
|
168
|
-
* Check if file has
|
|
169
|
-
* @param {string} content
|
|
21
|
+
* Check if a file already has the Default/Custom section markers.
|
|
22
|
+
* @param {string} content
|
|
170
23
|
* @returns {boolean}
|
|
171
24
|
*/
|
|
172
25
|
function hasSectionMarkers(content) {
|
|
173
|
-
return content.includes(
|
|
26
|
+
return content.includes(DEFAULT_MARKER) && content.includes(CUSTOM_MARKER);
|
|
174
27
|
}
|
|
175
28
|
|
|
176
29
|
module.exports = {
|
|
177
30
|
mergeLineBasedFiles,
|
|
178
31
|
hasSectionMarkers,
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
32
|
+
// Names the setup tests import (aliases of the canonical markers).
|
|
33
|
+
DEFAULT_SECTION_MARKER: DEFAULT_MARKER,
|
|
34
|
+
CUSTOM_SECTION_MARKER: CUSTOM_MARKER,
|
|
35
|
+
};
|
|
@@ -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
|
-
|
|
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
|
-
|
|
43
|
+
ui.status('warn', `Attempt ${attempt}/${maxAttempts} had failures — retrying…`);
|
|
46
44
|
} else if (maxAttempts > 1) {
|
|
47
|
-
|
|
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
|
-
|
|
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
|
-
|
|
80
|
-
|
|
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
|
-
|
|
86
|
-
|
|
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
|
-
//
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
//
|
|
128
|
+
// Warn if using local backend-manager
|
|
118
129
|
if (self.package.dependencies['backend-manager'].includes('file:')) {
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
//
|
|
128
|
-
|
|
129
|
-
|
|
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,16 +186,23 @@ 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) {
|
|
181
195
|
const rel = path.relative(defaultsDir, src);
|
|
182
196
|
const segments = rel.split(path.sep);
|
|
183
197
|
|
|
184
|
-
// Skip "archive"
|
|
185
|
-
// followed by a non-`.` character. Matches EM/BXM/UJM convention.
|
|
186
|
-
//
|
|
187
|
-
|
|
198
|
+
// Skip "archive" DIRECTORIES — any non-final path segment starting with `_` and
|
|
199
|
+
// followed by a non-`.` character (e.g. `_legacy/`). Matches EM/BXM/UJM convention.
|
|
200
|
+
// The check is restricted to directory segments (all but the last) so a `_`-prefixed
|
|
201
|
+
// FILENAME still ships — e.g. `test/_init.js` copies verbatim (the test runner skips
|
|
202
|
+
// it from discovery on its own). The `_.env` / `_.gitignore` files are likewise not
|
|
203
|
+
// skipped; their leading `_` strips on copy below.
|
|
204
|
+
const dirSegments = segments.slice(0, -1);
|
|
205
|
+
if (dirSegments.some((s) => s.startsWith('_') && !s.startsWith('_.'))) {
|
|
188
206
|
continue;
|
|
189
207
|
}
|
|
190
208
|
|
|
@@ -201,10 +219,12 @@ class SetupCommand extends BaseCommand {
|
|
|
201
219
|
const merged = mergeLineBasedFiles(existing, incoming, basename);
|
|
202
220
|
if (merged !== existing) {
|
|
203
221
|
jetpack.write(dest, merged);
|
|
204
|
-
|
|
222
|
+
ui.status('change', `Merged ${chalk.cyan(target)}`, { level: 2 });
|
|
223
|
+
touched++;
|
|
205
224
|
}
|
|
206
225
|
} catch (e) {
|
|
207
|
-
|
|
226
|
+
ui.status('warn', `Failed to merge ${chalk.cyan(target)}`, { detail: e.message, level: 2 });
|
|
227
|
+
touched++;
|
|
208
228
|
}
|
|
209
229
|
continue;
|
|
210
230
|
}
|
|
@@ -215,7 +235,12 @@ class SetupCommand extends BaseCommand {
|
|
|
215
235
|
|
|
216
236
|
// First time: copy as-is.
|
|
217
237
|
jetpack.copy(src, dest);
|
|
218
|
-
|
|
238
|
+
ui.status('add', `Copied ${chalk.cyan(target)}`, { level: 2 });
|
|
239
|
+
touched++;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (touched === 0) {
|
|
243
|
+
ui.note('All defaults up to date', 2);
|
|
219
244
|
}
|
|
220
245
|
}
|
|
221
246
|
|
|
@@ -253,6 +278,10 @@ class SetupCommand extends BaseCommand {
|
|
|
253
278
|
// Get all tests
|
|
254
279
|
const tests = testRegistry.getTests(testContext);
|
|
255
280
|
|
|
281
|
+
// Expose the total count so the per-check `[N]` prefix can right-align its
|
|
282
|
+
// width (single- vs double-digit indices stay aligned).
|
|
283
|
+
self.testTotalExpected = tests.length;
|
|
284
|
+
|
|
256
285
|
// Run each test
|
|
257
286
|
for (const test of tests) {
|
|
258
287
|
await self.test(
|
|
@@ -281,12 +310,15 @@ class SetupCommand extends BaseCommand {
|
|
|
281
310
|
.then(json => json)
|
|
282
311
|
.catch(e => e);
|
|
283
312
|
|
|
313
|
+
const ui = this.ui;
|
|
284
314
|
if (statsFetchResult instanceof Error) {
|
|
285
|
-
if (
|
|
286
|
-
|
|
315
|
+
if (statsFetchResult.message.includes('network timeout')) {
|
|
316
|
+
ui.status('skip', 'Skipped stats fetch', { detail: 'network timeout', level: 2 });
|
|
317
|
+
} else {
|
|
318
|
+
ui.status('warn', 'Could not fetch stats endpoint', { detail: statsFetchResult.message, level: 2 });
|
|
287
319
|
}
|
|
288
320
|
} else {
|
|
289
|
-
|
|
321
|
+
ui.status('pass', 'Stats fetched/created', { level: 2 });
|
|
290
322
|
}
|
|
291
323
|
}
|
|
292
324
|
}
|
package/src/cli/commands/test.js
CHANGED
|
@@ -36,7 +36,7 @@ class TestCommand extends BaseCommand {
|
|
|
36
36
|
const envSubset = captureSyncedEnv(process.env);
|
|
37
37
|
writeTestMode(projectDir, envSubset);
|
|
38
38
|
const extended = !!process.env.TEST_EXTENDED_MODE;
|
|
39
|
-
this.log(chalk.gray(` Test mode: ${extended ? '
|
|
39
|
+
this.log(chalk.gray(` Test mode: ${extended ? 'extended (real external APIs)' : 'normal (external APIs skipped)'}`));
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
// Load emulator ports from firebase.json
|
package/src/cli/index.js
CHANGED
|
@@ -164,47 +164,80 @@ 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
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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). The `[N]` bracket
|
|
172
|
+
// is padded to the width of the total check count so single- and double-digit
|
|
173
|
+
// numbers (`[1] ` … `[13]`) keep the symbols/names aligned.
|
|
174
|
+
const indexWidth = String(self.testTotalExpected || 99).length;
|
|
175
|
+
const printLine = (index, kind, label) => {
|
|
176
|
+
const colorByKind = { pass: chalk.green, fail: chalk.red, warn: chalk.yellow };
|
|
177
|
+
const color = colorByKind[kind] || chalk.white;
|
|
178
|
+
const suffix = label ? ` ${chalk.dim(label)}` : '';
|
|
179
|
+
const bracket = `[${index}]`.padEnd(indexWidth + 2, ' ');
|
|
180
|
+
console.log(`${ui.indent(2)}${chalk.dim(bracket)} ${color(ui.SYMBOLS[kind])} ${name}${suffix}`);
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
let passed = await fn();
|
|
184
|
+
|
|
185
|
+
// A check that returns an Error is a hard, unrecoverable failure (e.g. wrong
|
|
186
|
+
// Node version). Print it cleanly and stop — no auto-fix is possible.
|
|
187
|
+
if (passed instanceof Error) {
|
|
188
|
+
self.testTotal++;
|
|
189
|
+
printLine(self.testTotal, 'fail', '');
|
|
190
|
+
const message = passed.message || String(passed);
|
|
191
|
+
ui.status('fail', chalk.red(message), { level: 3 });
|
|
192
|
+
if (self.setupSummary) { self.setupSummary.fail(name, [chalk.red(message)]); }
|
|
193
|
+
self.haltSetup();
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (passed) {
|
|
198
|
+
self.testCount++;
|
|
199
|
+
self.testTotal++;
|
|
200
|
+
if (self.setupSummary) { self.setupSummary.pass(); }
|
|
201
|
+
printLine(self.testTotal, 'pass', '');
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Failed → attempt a fix.
|
|
206
|
+
self.testTotal++;
|
|
207
|
+
printLine(self.testTotal, 'warn', '— fixing…');
|
|
208
|
+
|
|
209
|
+
try {
|
|
210
|
+
await fix(self, args);
|
|
211
|
+
self.testCount++;
|
|
212
|
+
if (self.setupSummary) { self.setupSummary.pass(); }
|
|
213
|
+
ui.status('pass', chalk.green('fixed'), { level: 3 });
|
|
214
|
+
} catch (e) {
|
|
215
|
+
// The fix couldn't complete. `e.summaryDetails` (if present) is an array of
|
|
216
|
+
// pre-formatted detail lines the failing check wants surfaced in the summary.
|
|
217
|
+
const message = e && e.message ? e.message : String(e);
|
|
218
|
+
const details = (e && Array.isArray(e.summaryDetails)) ? e.summaryDetails : [chalk.red(message)];
|
|
219
|
+
ui.status('fail', chalk.red(`Could not fix: ${message}`), { level: 3 });
|
|
220
|
+
if (self.setupSummary) { self.setupSummary.fail(name, details); }
|
|
221
|
+
|
|
222
|
+
if (self.options['--continue']) {
|
|
223
|
+
ui.status('warn', chalk.yellow('Continuing despite error (--continue flag)'), { level: 3 });
|
|
224
|
+
return;
|
|
206
225
|
}
|
|
207
|
-
|
|
226
|
+
|
|
227
|
+
self.haltSetup();
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
// Halt the setup run on an unfixable failure. The failing check must already be
|
|
232
|
+
// recorded via `setupSummary.fail(...)` by the caller; this prints the summary
|
|
233
|
+
// block + a re-run hint and exits with code 1 — instead of rejecting a promise
|
|
234
|
+
// with no reason (which surfaced as an ugly `UnhandledPromiseRejection: undefined`).
|
|
235
|
+
Main.prototype.haltSetup = function() {
|
|
236
|
+
if (this.setupSummary) {
|
|
237
|
+
this.setupSummary.print({ hint: `Fix the above, then run ${require('chalk').default.bold('npx mgr setup')} again.` });
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
process.exit(1);
|
|
208
241
|
};
|
|
209
242
|
|
|
210
243
|
module.exports = Main;
|