firebase-tools 14.18.0 → 14.19.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/README.md +11 -5
- package/lib/appUtils.js +230 -0
- package/lib/bin/mcp.js +16 -1
- package/lib/commands/index.js +8 -0
- package/lib/commands/init.js +0 -2
- package/lib/commands/remoteconfig-experiments-delete.js +32 -0
- package/lib/commands/remoteconfig-experiments-get.js +20 -0
- package/lib/commands/remoteconfig-experiments-list.js +38 -0
- package/lib/commands/remoteconfig-rollouts-delete.js +32 -0
- package/lib/commands/remoteconfig-rollouts-get.js +20 -0
- package/lib/commands/remoteconfig-rollouts-list.js +38 -0
- package/lib/config.js +4 -2
- package/lib/deploy/apphosting/deploy.js +8 -5
- package/lib/deploy/functions/deploy.js +5 -4
- package/lib/deploy/functions/params.js +2 -2
- package/lib/emulator/commandUtils.js +3 -3
- package/lib/emulator/controller.js +3 -2
- package/lib/gcp/storage.js +73 -20
- package/lib/init/features/dataconnect/index.js +18 -9
- package/lib/init/features/project.js +66 -75
- package/lib/management/projects.js +16 -4
- package/lib/mcp/index.js +9 -0
- package/lib/mcp/prompts/core/deploy.js +8 -8
- package/lib/mcp/prompts/core/init.js +15 -18
- package/lib/mcp/prompts/crashlytics/connect.js +2 -2
- package/lib/mcp/prompts/index.js +30 -9
- package/lib/mcp/resources/guides/init_ai.js +8 -0
- package/lib/mcp/resources/guides/init_auth.js +4 -0
- package/lib/mcp/resources/guides/init_firestore_rules.js +2 -0
- package/lib/mcp/resources/index.js +16 -1
- package/lib/mcp/tools/apphosting/list_backends.js +1 -1
- package/lib/mcp/tools/core/get_environment.js +34 -17
- package/lib/mcp/tools/core/init.js +2 -1
- package/lib/mcp/tools/core/logout.js +1 -2
- package/lib/mcp/tools/functions/get_logs.js +9 -7
- package/lib/mcp/tools/index.js +3 -2
- package/lib/remoteconfig/deleteExperiment.js +32 -0
- package/lib/remoteconfig/deleteRollout.js +33 -0
- package/lib/remoteconfig/getExperiment.js +52 -0
- package/lib/remoteconfig/getRollout.js +43 -0
- package/lib/remoteconfig/interfaces.js +3 -1
- package/lib/remoteconfig/listExperiments.js +72 -0
- package/lib/remoteconfig/listRollouts.js +72 -0
- package/lib/remoteconfig/options.js +4 -0
- package/lib/track.js +6 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -161,11 +161,17 @@ Detailed doc is [here](https://firebase.google.com/docs/cli/auth).
|
|
|
161
161
|
|
|
162
162
|
### Remote Config Commands
|
|
163
163
|
|
|
164
|
-
| Command
|
|
165
|
-
|
|
|
166
|
-
| **remoteconfig:get**
|
|
167
|
-
| **remoteconfig:versions:list**
|
|
168
|
-
| **remoteconfig:rollback**
|
|
164
|
+
| Command | Description |
|
|
165
|
+
| ----------------------------------- | ---------------------------------------------------------------------------------------------------------- |
|
|
166
|
+
| **remoteconfig:get** | Get a Firebase project's Remote Config template. |
|
|
167
|
+
| **remoteconfig:versions:list** | Get a list of the most recent Firebase Remote Config template versions that have been published. |
|
|
168
|
+
| **remoteconfig:rollback** | Roll back a project's published Remote Config template to the version provided by `--version_number` flag. |
|
|
169
|
+
| **remoteconfig:experiments:get** | Get a Remote Config experiment. |
|
|
170
|
+
| **remoteconfig:experiments:list** | Get a list of Remote Config experiments |
|
|
171
|
+
| **remoteconfig:experiments:delete** | Delete a Remote Config experiment. |
|
|
172
|
+
| **remoteconfig:rollouts:get** | Get a Remote Config rollout. |
|
|
173
|
+
| **remoteconfig:rollouts:list** | Get a list of Remote Config rollouts. |
|
|
174
|
+
| **remoteconfig:rollouts:delete** | Delete a Remote Config rollout. |
|
|
169
175
|
|
|
170
176
|
Use `firebase:deploy --only remoteconfig` to update and publish a project's Firebase Remote Config template.
|
|
171
177
|
|
package/lib/appUtils.js
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.extractAppIdentifiersAndroid = exports.extractAppIdentifierIos = exports.extractAppIdentifiersFlutter = exports.detectApps = exports.getPlatformsFromFolder = exports.appDescription = exports.Framework = exports.Platform = void 0;
|
|
4
|
+
const fs = require("fs-extra");
|
|
5
|
+
const path = require("path");
|
|
6
|
+
const glob_1 = require("glob");
|
|
7
|
+
var Platform;
|
|
8
|
+
(function (Platform) {
|
|
9
|
+
Platform["ANDROID"] = "ANDROID";
|
|
10
|
+
Platform["WEB"] = "WEB";
|
|
11
|
+
Platform["IOS"] = "IOS";
|
|
12
|
+
Platform["FLUTTER"] = "FLUTTER";
|
|
13
|
+
})(Platform = exports.Platform || (exports.Platform = {}));
|
|
14
|
+
var Framework;
|
|
15
|
+
(function (Framework) {
|
|
16
|
+
Framework["REACT"] = "REACT";
|
|
17
|
+
Framework["ANGULAR"] = "ANGULAR";
|
|
18
|
+
})(Framework = exports.Framework || (exports.Framework = {}));
|
|
19
|
+
function appDescription(a) {
|
|
20
|
+
return `${a.directory} (${a.platform.toLowerCase()})`;
|
|
21
|
+
}
|
|
22
|
+
exports.appDescription = appDescription;
|
|
23
|
+
async function getPlatformsFromFolder(dirPath) {
|
|
24
|
+
const apps = await detectApps(dirPath);
|
|
25
|
+
return [...new Set(apps.map((app) => app.platform))];
|
|
26
|
+
}
|
|
27
|
+
exports.getPlatformsFromFolder = getPlatformsFromFolder;
|
|
28
|
+
async function detectApps(dirPath) {
|
|
29
|
+
const packageJsonFiles = await detectFiles(dirPath, "package.json");
|
|
30
|
+
const pubSpecYamlFiles = await detectFiles(dirPath, "pubspec.yaml");
|
|
31
|
+
const srcMainFolders = await detectFiles(dirPath, "src/main/");
|
|
32
|
+
const xCodeProjects = await detectFiles(dirPath, "*.xcodeproj/");
|
|
33
|
+
const webApps = await Promise.all(packageJsonFiles.map((p) => packageJsonToWebApp(dirPath, p)));
|
|
34
|
+
const flutterAppPromises = await Promise.all(pubSpecYamlFiles.map((f) => processFlutterDir(dirPath, f)));
|
|
35
|
+
const flutterApps = flutterAppPromises.flat();
|
|
36
|
+
const androidAppPromises = await Promise.all(srcMainFolders.map((f) => processAndroidDir(dirPath, f)));
|
|
37
|
+
const androidApps = androidAppPromises
|
|
38
|
+
.flat()
|
|
39
|
+
.filter((a) => !flutterApps.some((f) => isPathInside(f.directory, a.directory)));
|
|
40
|
+
const iosAppPromises = await Promise.all(xCodeProjects.map((f) => processIosDir(dirPath, f)));
|
|
41
|
+
const iosApps = iosAppPromises
|
|
42
|
+
.flat()
|
|
43
|
+
.filter((a) => !flutterApps.some((f) => isPathInside(f.directory, a.directory)));
|
|
44
|
+
return [...webApps, ...flutterApps, ...androidApps, ...iosApps];
|
|
45
|
+
}
|
|
46
|
+
exports.detectApps = detectApps;
|
|
47
|
+
async function processIosDir(dirPath, filePath) {
|
|
48
|
+
const iosDir = path.dirname(filePath);
|
|
49
|
+
const iosAppIds = await detectAppIdsForPlatform(dirPath, Platform.IOS);
|
|
50
|
+
if (iosAppIds.length === 0) {
|
|
51
|
+
return [
|
|
52
|
+
{
|
|
53
|
+
platform: Platform.IOS,
|
|
54
|
+
directory: iosDir,
|
|
55
|
+
},
|
|
56
|
+
];
|
|
57
|
+
}
|
|
58
|
+
const iosApps = await Promise.all(iosAppIds.map((app) => ({
|
|
59
|
+
platform: Platform.IOS,
|
|
60
|
+
directory: iosDir,
|
|
61
|
+
appId: app.appId,
|
|
62
|
+
bundleId: app.bundleId,
|
|
63
|
+
})));
|
|
64
|
+
return iosApps.flat();
|
|
65
|
+
}
|
|
66
|
+
async function processAndroidDir(dirPath, filePath) {
|
|
67
|
+
const androidDir = path.dirname(path.dirname(filePath));
|
|
68
|
+
const androidAppIds = await detectAppIdsForPlatform(dirPath, Platform.ANDROID);
|
|
69
|
+
if (androidAppIds.length === 0) {
|
|
70
|
+
return [
|
|
71
|
+
{
|
|
72
|
+
platform: Platform.ANDROID,
|
|
73
|
+
directory: androidDir,
|
|
74
|
+
},
|
|
75
|
+
];
|
|
76
|
+
}
|
|
77
|
+
const androidApps = await Promise.all(androidAppIds.map((app) => ({
|
|
78
|
+
platform: Platform.ANDROID,
|
|
79
|
+
directory: androidDir,
|
|
80
|
+
appId: app.appId,
|
|
81
|
+
bundleId: app.bundleId,
|
|
82
|
+
})));
|
|
83
|
+
return androidApps.flat();
|
|
84
|
+
}
|
|
85
|
+
async function processFlutterDir(dirPath, filePath) {
|
|
86
|
+
const flutterDir = path.dirname(filePath);
|
|
87
|
+
const flutterAppIds = await detectAppIdsForPlatform(dirPath, Platform.FLUTTER);
|
|
88
|
+
if (flutterAppIds.length === 0) {
|
|
89
|
+
return [
|
|
90
|
+
{
|
|
91
|
+
platform: Platform.FLUTTER,
|
|
92
|
+
directory: flutterDir,
|
|
93
|
+
},
|
|
94
|
+
];
|
|
95
|
+
}
|
|
96
|
+
const flutterApps = await Promise.all(flutterAppIds.map((app) => {
|
|
97
|
+
const flutterApp = {
|
|
98
|
+
platform: Platform.FLUTTER,
|
|
99
|
+
directory: flutterDir,
|
|
100
|
+
appId: app.appId,
|
|
101
|
+
bundleId: app.bundleId,
|
|
102
|
+
};
|
|
103
|
+
return flutterApp;
|
|
104
|
+
}));
|
|
105
|
+
return flutterApps.flat();
|
|
106
|
+
}
|
|
107
|
+
function isPathInside(parent, child) {
|
|
108
|
+
const relativePath = path.relative(parent, child);
|
|
109
|
+
return !relativePath.startsWith(`..`);
|
|
110
|
+
}
|
|
111
|
+
async function packageJsonToWebApp(dirPath, packageJsonFile) {
|
|
112
|
+
const fullPath = path.join(dirPath, packageJsonFile);
|
|
113
|
+
const packageJson = JSON.parse((await fs.readFile(fullPath)).toString());
|
|
114
|
+
return {
|
|
115
|
+
platform: Platform.WEB,
|
|
116
|
+
directory: path.dirname(packageJsonFile),
|
|
117
|
+
frameworks: getFrameworksFromPackageJson(packageJson),
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
const WEB_FRAMEWORKS = Object.values(Framework);
|
|
121
|
+
const WEB_FRAMEWORKS_SIGNALS = {
|
|
122
|
+
REACT: ["react", "next"],
|
|
123
|
+
ANGULAR: ["@angular/core"],
|
|
124
|
+
};
|
|
125
|
+
async function detectAppIdsForPlatform(dirPath, platform) {
|
|
126
|
+
let appIdFiles;
|
|
127
|
+
let extractFunc;
|
|
128
|
+
switch (platform) {
|
|
129
|
+
case Platform.ANDROID:
|
|
130
|
+
appIdFiles = await detectFiles(dirPath, "google-services*.json*");
|
|
131
|
+
extractFunc = extractAppIdentifiersAndroid;
|
|
132
|
+
break;
|
|
133
|
+
case Platform.IOS:
|
|
134
|
+
appIdFiles = await detectFiles(dirPath, "GoogleService-*.plist*");
|
|
135
|
+
extractFunc = extractAppIdentifierIos;
|
|
136
|
+
break;
|
|
137
|
+
case Platform.FLUTTER:
|
|
138
|
+
appIdFiles = await detectFiles(dirPath, "firebase_options.dart");
|
|
139
|
+
extractFunc = extractAppIdentifiersFlutter;
|
|
140
|
+
break;
|
|
141
|
+
default:
|
|
142
|
+
return [];
|
|
143
|
+
}
|
|
144
|
+
const allAppIds = await Promise.all(appIdFiles.map(async (file) => {
|
|
145
|
+
const fileContent = (await fs.readFile(path.join(dirPath, file))).toString();
|
|
146
|
+
return extractFunc(fileContent);
|
|
147
|
+
}));
|
|
148
|
+
return allAppIds.flat();
|
|
149
|
+
}
|
|
150
|
+
function getFrameworksFromPackageJson(packageJson) {
|
|
151
|
+
var _a, _b;
|
|
152
|
+
const devDependencies = Object.keys((_a = packageJson.devDependencies) !== null && _a !== void 0 ? _a : {});
|
|
153
|
+
const dependencies = Object.keys((_b = packageJson.dependencies) !== null && _b !== void 0 ? _b : {});
|
|
154
|
+
const allDeps = Array.from(new Set([...devDependencies, ...dependencies]));
|
|
155
|
+
return WEB_FRAMEWORKS.filter((framework) => WEB_FRAMEWORKS_SIGNALS[framework].find((dep) => allDeps.includes(dep)));
|
|
156
|
+
}
|
|
157
|
+
function extractAppIdentifiersFlutter(fileContent) {
|
|
158
|
+
const optionsRegex = /FirebaseOptions\(([^)]*)\)/g;
|
|
159
|
+
const appIdRegex = /appId: '([^']*)'/;
|
|
160
|
+
const bundleIdRegex = /iosBundleId: '([^']*)'/;
|
|
161
|
+
const matches = fileContent.matchAll(optionsRegex);
|
|
162
|
+
const identifiers = [];
|
|
163
|
+
for (const match of matches) {
|
|
164
|
+
const optionsContent = match[1];
|
|
165
|
+
const appIdMatch = appIdRegex.exec(optionsContent);
|
|
166
|
+
const bundleIdMatch = bundleIdRegex.exec(optionsContent);
|
|
167
|
+
if (appIdMatch === null || appIdMatch === void 0 ? void 0 : appIdMatch[1]) {
|
|
168
|
+
identifiers.push({
|
|
169
|
+
appId: appIdMatch[1],
|
|
170
|
+
bundleId: bundleIdMatch === null || bundleIdMatch === void 0 ? void 0 : bundleIdMatch[1],
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return identifiers;
|
|
175
|
+
}
|
|
176
|
+
exports.extractAppIdentifiersFlutter = extractAppIdentifiersFlutter;
|
|
177
|
+
function extractAppIdentifierIos(fileContent) {
|
|
178
|
+
const appIdRegex = /<key>GOOGLE_APP_ID<\/key>\s*<string>([^<]*)<\/string>/;
|
|
179
|
+
const bundleIdRegex = /<key>BUNDLE_ID<\/key>\s*<string>([^<]*)<\/string>/;
|
|
180
|
+
const appIdMatch = fileContent.match(appIdRegex);
|
|
181
|
+
const bundleIdMatch = fileContent.match(bundleIdRegex);
|
|
182
|
+
if (appIdMatch === null || appIdMatch === void 0 ? void 0 : appIdMatch[1]) {
|
|
183
|
+
return [
|
|
184
|
+
{
|
|
185
|
+
appId: appIdMatch[1],
|
|
186
|
+
bundleId: bundleIdMatch === null || bundleIdMatch === void 0 ? void 0 : bundleIdMatch[1],
|
|
187
|
+
},
|
|
188
|
+
];
|
|
189
|
+
}
|
|
190
|
+
return [];
|
|
191
|
+
}
|
|
192
|
+
exports.extractAppIdentifierIos = extractAppIdentifierIos;
|
|
193
|
+
function extractAppIdentifiersAndroid(fileContent) {
|
|
194
|
+
var _a, _b;
|
|
195
|
+
const identifiers = [];
|
|
196
|
+
try {
|
|
197
|
+
const config = JSON.parse(fileContent);
|
|
198
|
+
if (config.client && Array.isArray(config.client)) {
|
|
199
|
+
for (const client of config.client) {
|
|
200
|
+
if ((_a = client.client_info) === null || _a === void 0 ? void 0 : _a.mobilesdk_app_id) {
|
|
201
|
+
identifiers.push({
|
|
202
|
+
appId: client.client_info.mobilesdk_app_id,
|
|
203
|
+
bundleId: (_b = client.client_info.android_client_info) === null || _b === void 0 ? void 0 : _b.package_name,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
catch (e) {
|
|
210
|
+
console.error("Error parsing google-services.json:", e);
|
|
211
|
+
}
|
|
212
|
+
return identifiers;
|
|
213
|
+
}
|
|
214
|
+
exports.extractAppIdentifiersAndroid = extractAppIdentifiersAndroid;
|
|
215
|
+
async function detectFiles(dirPath, filePattern) {
|
|
216
|
+
const options = {
|
|
217
|
+
cwd: dirPath,
|
|
218
|
+
ignore: [
|
|
219
|
+
"**/dataconnect*/**",
|
|
220
|
+
"**/node_modules/**",
|
|
221
|
+
"**/dist/**",
|
|
222
|
+
"**/build/**",
|
|
223
|
+
"**/out/**",
|
|
224
|
+
"**/.next/**",
|
|
225
|
+
"**/coverage/**",
|
|
226
|
+
],
|
|
227
|
+
absolute: false,
|
|
228
|
+
};
|
|
229
|
+
return (0, glob_1.glob)(`**/${filePattern}`, options);
|
|
230
|
+
}
|
package/lib/bin/mcp.js
CHANGED
|
@@ -7,6 +7,8 @@ const index_1 = require("../mcp/index");
|
|
|
7
7
|
const util_1 = require("util");
|
|
8
8
|
const types_1 = require("../mcp/types");
|
|
9
9
|
const index_js_1 = require("../mcp/tools/index.js");
|
|
10
|
+
const index_js_2 = require("../mcp/prompts/index.js");
|
|
11
|
+
const index_js_3 = require("../mcp/resources/index.js");
|
|
10
12
|
const path_1 = require("path");
|
|
11
13
|
const STARTUP_MESSAGE = `
|
|
12
14
|
This is a running process of the Firebase MCP server. This command should only be executed by an MCP client. An example MCP client configuration might be:
|
|
@@ -26,13 +28,26 @@ async function mcp() {
|
|
|
26
28
|
only: { type: "string", default: "" },
|
|
27
29
|
dir: { type: "string" },
|
|
28
30
|
"generate-tool-list": { type: "boolean", default: false },
|
|
31
|
+
"generate-prompt-list": { type: "boolean", default: false },
|
|
32
|
+
"generate-resource-list": { type: "boolean", default: false },
|
|
29
33
|
},
|
|
30
34
|
allowPositionals: true,
|
|
31
35
|
});
|
|
36
|
+
let earlyExit = false;
|
|
32
37
|
if (values["generate-tool-list"]) {
|
|
33
38
|
console.log((0, index_js_1.markdownDocsOfTools)());
|
|
34
|
-
|
|
39
|
+
earlyExit = true;
|
|
40
|
+
}
|
|
41
|
+
if (values["generate-prompt-list"]) {
|
|
42
|
+
console.log((0, index_js_2.markdownDocsOfPrompts)());
|
|
43
|
+
earlyExit = true;
|
|
35
44
|
}
|
|
45
|
+
if (values["generate-resource-list"]) {
|
|
46
|
+
console.log((0, index_js_3.markdownDocsOfResources)());
|
|
47
|
+
earlyExit = true;
|
|
48
|
+
}
|
|
49
|
+
if (earlyExit)
|
|
50
|
+
return;
|
|
36
51
|
process.env.IS_FIREBASE_MCP = "true";
|
|
37
52
|
(0, logger_1.useFileLogger)();
|
|
38
53
|
const activeFeatures = (values.only || "")
|
package/lib/commands/index.js
CHANGED
|
@@ -208,6 +208,14 @@ function load(client) {
|
|
|
208
208
|
client.remoteconfig.rollback = loadCommand("remoteconfig-rollback");
|
|
209
209
|
client.remoteconfig.versions = {};
|
|
210
210
|
client.remoteconfig.versions.list = loadCommand("remoteconfig-versions-list");
|
|
211
|
+
client.remoteconfig.rollouts = {};
|
|
212
|
+
client.remoteconfig.rollouts.get = loadCommand("remoteconfig-rollouts-get");
|
|
213
|
+
client.remoteconfig.rollouts.list = loadCommand("remoteconfig-rollouts-list");
|
|
214
|
+
client.remoteconfig.rollouts.delete = loadCommand("remoteconfig-rollouts-delete");
|
|
215
|
+
client.remoteconfig.experiments = {};
|
|
216
|
+
client.remoteconfig.experiments.get = loadCommand("remoteconfig-experiments-get");
|
|
217
|
+
client.remoteconfig.experiments.list = loadCommand("remoteconfig-experiments-list");
|
|
218
|
+
client.remoteconfig.experiments.delete = loadCommand("remoteconfig-experiments-delete");
|
|
211
219
|
client.serve = loadCommand("serve");
|
|
212
220
|
client.setup = {};
|
|
213
221
|
client.setup.emulators = {};
|
package/lib/commands/init.js
CHANGED
|
@@ -10,7 +10,6 @@ const auth_1 = require("../auth");
|
|
|
10
10
|
const init_1 = require("../init");
|
|
11
11
|
const logger_1 = require("../logger");
|
|
12
12
|
const prompt_1 = require("../prompt");
|
|
13
|
-
const requireAuth_1 = require("../requireAuth");
|
|
14
13
|
const fsutils = require("../fsutils");
|
|
15
14
|
const utils = require("../utils");
|
|
16
15
|
const experiments_1 = require("../experiments");
|
|
@@ -132,7 +131,6 @@ ${[...featureNames]
|
|
|
132
131
|
exports.command = new command_1.Command("init [feature]")
|
|
133
132
|
.description("interactively configure the current directory as a Firebase project directory")
|
|
134
133
|
.help(HELP)
|
|
135
|
-
.before(requireAuth_1.requireAuth)
|
|
136
134
|
.action(initAction);
|
|
137
135
|
async function initAction(feature, options) {
|
|
138
136
|
var _a;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.command = void 0;
|
|
4
|
+
const command_1 = require("../command");
|
|
5
|
+
const requireAuth_1 = require("../requireAuth");
|
|
6
|
+
const requirePermissions_1 = require("../requirePermissions");
|
|
7
|
+
const logger_1 = require("../logger");
|
|
8
|
+
const projectUtils_1 = require("../projectUtils");
|
|
9
|
+
const interfaces_1 = require("../remoteconfig/interfaces");
|
|
10
|
+
const rcExperiment = require("../remoteconfig/deleteExperiment");
|
|
11
|
+
const getExperiment_1 = require("../remoteconfig/getExperiment");
|
|
12
|
+
const prompt_1 = require("../prompt");
|
|
13
|
+
exports.command = new command_1.Command("remoteconfig:experiments:delete <experimentId>")
|
|
14
|
+
.description("delete a Remote Config experiment.")
|
|
15
|
+
.before(requireAuth_1.requireAuth)
|
|
16
|
+
.before(requirePermissions_1.requirePermissions, [
|
|
17
|
+
"firebaseabt.experiments.delete",
|
|
18
|
+
"firebaseanalytics.resources.googleAnalyticsEdit",
|
|
19
|
+
])
|
|
20
|
+
.action(async (experimentId, options) => {
|
|
21
|
+
const projectNumber = await (0, projectUtils_1.needProjectNumber)(options);
|
|
22
|
+
const experiment = await (0, getExperiment_1.getExperiment)(projectNumber, interfaces_1.NAMESPACE_FIREBASE, experimentId);
|
|
23
|
+
logger_1.logger.info((0, getExperiment_1.parseExperiment)(experiment));
|
|
24
|
+
const confirmDeletion = await (0, prompt_1.confirm)({
|
|
25
|
+
message: "Are you sure you want to delete this experiment? This cannot be undone.",
|
|
26
|
+
default: false,
|
|
27
|
+
});
|
|
28
|
+
if (!confirmDeletion) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
logger_1.logger.info(await rcExperiment.deleteExperiment(projectNumber, interfaces_1.NAMESPACE_FIREBASE, experimentId));
|
|
32
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.command = void 0;
|
|
4
|
+
const command_1 = require("../command");
|
|
5
|
+
const requireAuth_1 = require("../requireAuth");
|
|
6
|
+
const requirePermissions_1 = require("../requirePermissions");
|
|
7
|
+
const logger_1 = require("../logger");
|
|
8
|
+
const projectUtils_1 = require("../projectUtils");
|
|
9
|
+
const interfaces_1 = require("../remoteconfig/interfaces");
|
|
10
|
+
const rcExperiment = require("../remoteconfig/getExperiment");
|
|
11
|
+
exports.command = new command_1.Command("remoteconfig:experiments:get <experimentId>")
|
|
12
|
+
.description("get a Remote Config experiment.")
|
|
13
|
+
.before(requireAuth_1.requireAuth)
|
|
14
|
+
.before(requirePermissions_1.requirePermissions, ["firebaseabt.experiments.get"])
|
|
15
|
+
.action(async (experimentId, options) => {
|
|
16
|
+
const projectNumber = await (0, projectUtils_1.needProjectNumber)(options);
|
|
17
|
+
const experiment = await rcExperiment.getExperiment(projectNumber, interfaces_1.NAMESPACE_FIREBASE, experimentId);
|
|
18
|
+
logger_1.logger.info(rcExperiment.parseExperiment(experiment));
|
|
19
|
+
return experiment;
|
|
20
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.command = void 0;
|
|
4
|
+
const rcExperiment = require("../remoteconfig/listExperiments");
|
|
5
|
+
const interfaces_1 = require("../remoteconfig/interfaces");
|
|
6
|
+
const command_1 = require("../command");
|
|
7
|
+
const requireAuth_1 = require("../requireAuth");
|
|
8
|
+
const requirePermissions_1 = require("../requirePermissions");
|
|
9
|
+
const logger_1 = require("../logger");
|
|
10
|
+
const projectUtils_1 = require("../projectUtils");
|
|
11
|
+
exports.command = new command_1.Command("remoteconfig:experiments:list")
|
|
12
|
+
.description("get a list of Remote Config experiments")
|
|
13
|
+
.option("--pageSize <pageSize>", "Maximum number of experiments to return per page. Defaults to 10. Pass '0' to fetch all experiments")
|
|
14
|
+
.option("--pageToken <pageToken>", "Token from a previous list operation to retrieve the next page of results. Listing starts from the beginning if omitted.")
|
|
15
|
+
.option("--filter <filter>", "Filters experiments by their full resource name. Format: `name:projects/{project_number}/namespaces/{namespace}/experiments/{experiment_id}`")
|
|
16
|
+
.before(requireAuth_1.requireAuth)
|
|
17
|
+
.before(requirePermissions_1.requirePermissions, [
|
|
18
|
+
"firebaseabt.experiments.list",
|
|
19
|
+
"firebaseanalytics.resources.googleAnalyticsReadAndAnalyze",
|
|
20
|
+
])
|
|
21
|
+
.action(async (options) => {
|
|
22
|
+
var _a;
|
|
23
|
+
const projectNumber = await (0, projectUtils_1.needProjectNumber)(options);
|
|
24
|
+
const listExperimentOptions = {
|
|
25
|
+
pageSize: (_a = options.pageSize) !== null && _a !== void 0 ? _a : interfaces_1.DEFAULT_PAGE_SIZE,
|
|
26
|
+
pageToken: options.pageToken,
|
|
27
|
+
filter: options.filter,
|
|
28
|
+
};
|
|
29
|
+
const { experiments, nextPageToken } = await rcExperiment.listExperiments(projectNumber, interfaces_1.NAMESPACE_FIREBASE, listExperimentOptions);
|
|
30
|
+
logger_1.logger.info(rcExperiment.parseExperimentList(experiments !== null && experiments !== void 0 ? experiments : []));
|
|
31
|
+
if (nextPageToken) {
|
|
32
|
+
logger_1.logger.info(`\nNext Page Token: \x1b[32m${nextPageToken}\x1b[0m\n`);
|
|
33
|
+
}
|
|
34
|
+
return {
|
|
35
|
+
experiments,
|
|
36
|
+
nextPageToken,
|
|
37
|
+
};
|
|
38
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.command = void 0;
|
|
4
|
+
const command_1 = require("../command");
|
|
5
|
+
const requireAuth_1 = require("../requireAuth");
|
|
6
|
+
const requirePermissions_1 = require("../requirePermissions");
|
|
7
|
+
const logger_1 = require("../logger");
|
|
8
|
+
const projectUtils_1 = require("../projectUtils");
|
|
9
|
+
const interfaces_1 = require("../remoteconfig/interfaces");
|
|
10
|
+
const rcRollout = require("../remoteconfig/deleteRollout");
|
|
11
|
+
const getRollout_1 = require("../remoteconfig/getRollout");
|
|
12
|
+
const prompt_1 = require("../prompt");
|
|
13
|
+
exports.command = new command_1.Command("remoteconfig:rollouts:delete <rolloutId>")
|
|
14
|
+
.description("delete a Remote Config rollout.")
|
|
15
|
+
.before(requireAuth_1.requireAuth)
|
|
16
|
+
.before(requirePermissions_1.requirePermissions, [
|
|
17
|
+
"cloud.configs.update",
|
|
18
|
+
"firebaseanalytics.resources.googleAnalyticsEdit",
|
|
19
|
+
])
|
|
20
|
+
.action(async (rolloutId, options) => {
|
|
21
|
+
const projectNumber = await (0, projectUtils_1.needProjectNumber)(options);
|
|
22
|
+
const rollout = await (0, getRollout_1.getRollout)(projectNumber, interfaces_1.NAMESPACE_FIREBASE, rolloutId);
|
|
23
|
+
logger_1.logger.info((0, getRollout_1.parseRolloutIntoTable)(rollout));
|
|
24
|
+
const confirmDeletion = await (0, prompt_1.confirm)({
|
|
25
|
+
message: "Are you sure you want to delete this rollout? This cannot be undone.",
|
|
26
|
+
default: false,
|
|
27
|
+
});
|
|
28
|
+
if (!confirmDeletion) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
logger_1.logger.info(await rcRollout.deleteRollout(projectNumber, interfaces_1.NAMESPACE_FIREBASE, rolloutId));
|
|
32
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.command = void 0;
|
|
4
|
+
const command_1 = require("../command");
|
|
5
|
+
const requireAuth_1 = require("../requireAuth");
|
|
6
|
+
const requirePermissions_1 = require("../requirePermissions");
|
|
7
|
+
const logger_1 = require("../logger");
|
|
8
|
+
const projectUtils_1 = require("../projectUtils");
|
|
9
|
+
const interfaces_1 = require("../remoteconfig/interfaces");
|
|
10
|
+
const rcRollout = require("../remoteconfig/getRollout");
|
|
11
|
+
exports.command = new command_1.Command("remoteconfig:rollouts:get <rolloutId>")
|
|
12
|
+
.description("get a Remote Config rollout")
|
|
13
|
+
.before(requireAuth_1.requireAuth)
|
|
14
|
+
.before(requirePermissions_1.requirePermissions, ["cloud.configs.get"])
|
|
15
|
+
.action(async (rolloutId, options) => {
|
|
16
|
+
const projectNumber = await (0, projectUtils_1.needProjectNumber)(options);
|
|
17
|
+
const rollout = await rcRollout.getRollout(projectNumber, interfaces_1.NAMESPACE_FIREBASE, rolloutId);
|
|
18
|
+
logger_1.logger.info(rcRollout.parseRolloutIntoTable(rollout));
|
|
19
|
+
return rollout;
|
|
20
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.command = void 0;
|
|
4
|
+
const command_1 = require("../command");
|
|
5
|
+
const requireAuth_1 = require("../requireAuth");
|
|
6
|
+
const requirePermissions_1 = require("../requirePermissions");
|
|
7
|
+
const logger_1 = require("../logger");
|
|
8
|
+
const projectUtils_1 = require("../projectUtils");
|
|
9
|
+
const interfaces_1 = require("../remoteconfig/interfaces");
|
|
10
|
+
const rcRollout = require("../remoteconfig/listRollouts");
|
|
11
|
+
exports.command = new command_1.Command("remoteconfig:rollouts:list")
|
|
12
|
+
.description("get a list of Remote Config rollouts.")
|
|
13
|
+
.option("--pageSize <pageSize>", "Maximum number of rollouts to return per page. Defaults to 10. Pass '0' to fetch all rollouts")
|
|
14
|
+
.option("--pageToken <pageToken>", "Token from a previous list operation to retrieve the next page of results. Listing starts from the beginning if omitted.")
|
|
15
|
+
.option("--filter <filter>", "Filters rollouts by their full resource name. Format: `name:projects/{project_id}/namespaces/{namespace}/rollouts/{rollout_id}`")
|
|
16
|
+
.before(requireAuth_1.requireAuth)
|
|
17
|
+
.before(requirePermissions_1.requirePermissions, [
|
|
18
|
+
"cloud.configs.get",
|
|
19
|
+
"firebaseanalytics.resources.googleAnalyticsReadAndAnalyze",
|
|
20
|
+
])
|
|
21
|
+
.action(async (options) => {
|
|
22
|
+
var _a;
|
|
23
|
+
const projectNumber = await (0, projectUtils_1.needProjectNumber)(options);
|
|
24
|
+
const listRolloutOptions = {
|
|
25
|
+
pageSize: (_a = options.pageSize) !== null && _a !== void 0 ? _a : interfaces_1.DEFAULT_PAGE_SIZE,
|
|
26
|
+
pageToken: options.pageToken,
|
|
27
|
+
filter: options.filter,
|
|
28
|
+
};
|
|
29
|
+
const { rollouts, nextPageToken } = await rcRollout.listRollouts(projectNumber, interfaces_1.NAMESPACE_FIREBASE, listRolloutOptions);
|
|
30
|
+
logger_1.logger.info(rcRollout.parseRolloutList(rollouts !== null && rollouts !== void 0 ? rollouts : []));
|
|
31
|
+
if (nextPageToken) {
|
|
32
|
+
logger_1.logger.info(`\nNext Page Token: \x1b[32m${nextPageToken}\x1b[0m\n`);
|
|
33
|
+
}
|
|
34
|
+
return {
|
|
35
|
+
rollouts,
|
|
36
|
+
nextPageToken,
|
|
37
|
+
};
|
|
38
|
+
});
|
package/lib/config.js
CHANGED
|
@@ -5,7 +5,6 @@ const _ = require("lodash");
|
|
|
5
5
|
const clc = require("colorette");
|
|
6
6
|
const fs = require("fs-extra");
|
|
7
7
|
const path = require("path");
|
|
8
|
-
const cjson = require("cjson");
|
|
9
8
|
const detectProjectRoot_1 = require("./detectProjectRoot");
|
|
10
9
|
const error_1 = require("./error");
|
|
11
10
|
const fsutils = require("./fsutils");
|
|
@@ -225,7 +224,10 @@ class Config {
|
|
|
225
224
|
if (pd) {
|
|
226
225
|
try {
|
|
227
226
|
const filePath = path.resolve(pd, path.basename(filename));
|
|
228
|
-
|
|
227
|
+
let data = {};
|
|
228
|
+
if (fs.statSync(filePath).size > 0) {
|
|
229
|
+
data = (0, loadCJSON_1.loadCJSON)(filePath);
|
|
230
|
+
}
|
|
229
231
|
const validator = (0, firebaseConfigValidate_1.getValidator)();
|
|
230
232
|
const valid = validator(data);
|
|
231
233
|
if (!valid && validator.errors) {
|
|
@@ -17,14 +17,16 @@ async function default_1(context, options) {
|
|
|
17
17
|
if (!context.backendConfigs) {
|
|
18
18
|
return;
|
|
19
19
|
}
|
|
20
|
+
const bucketsPerLocation = {};
|
|
20
21
|
await Promise.all(Object.values(context.backendLocations).map(async (loc) => {
|
|
21
|
-
const
|
|
22
|
-
await gcs.upsertBucket({
|
|
22
|
+
const baseName = `firebaseapphosting-sources-${options.projectNumber}-${loc.toLowerCase()}`;
|
|
23
|
+
const resolvedName = await gcs.upsertBucket({
|
|
23
24
|
product: "apphosting",
|
|
24
|
-
createMessage: `Creating Cloud Storage bucket in ${loc} to store App Hosting source code uploads at ${
|
|
25
|
+
createMessage: `Creating Cloud Storage bucket in ${loc} to store App Hosting source code uploads at ${baseName}...`,
|
|
25
26
|
projectId,
|
|
26
27
|
req: {
|
|
27
|
-
|
|
28
|
+
baseName,
|
|
29
|
+
purposeLabel: `apphosting-source-${loc.toLowerCase()}`,
|
|
28
30
|
location: loc,
|
|
29
31
|
lifecycle: {
|
|
30
32
|
rule: [
|
|
@@ -40,6 +42,7 @@ async function default_1(context, options) {
|
|
|
40
42
|
},
|
|
41
43
|
},
|
|
42
44
|
});
|
|
45
|
+
bucketsPerLocation[loc] = resolvedName;
|
|
43
46
|
}));
|
|
44
47
|
await Promise.all(Object.values(context.backendConfigs).map(async (cfg) => {
|
|
45
48
|
const projectSourcePath = options.projectRoot ? options.projectRoot : process.cwd();
|
|
@@ -49,7 +52,7 @@ async function default_1(context, options) {
|
|
|
49
52
|
throw new error_1.FirebaseError(`Failed to find location for backend ${cfg.backendId}. Please contact support with the contents of your firebase-debug.log to report your issue.`);
|
|
50
53
|
}
|
|
51
54
|
(0, utils_1.logLabeledBullet)("apphosting", `Uploading source code at ${projectSourcePath} for backend ${cfg.backendId}...`);
|
|
52
|
-
const bucketName =
|
|
55
|
+
const bucketName = bucketsPerLocation[backendLocation];
|
|
53
56
|
const { bucket, object } = await gcs.uploadObject({
|
|
54
57
|
file: zippedSourcePath,
|
|
55
58
|
stream: fs.createReadStream(zippedSourcePath),
|
|
@@ -59,14 +59,15 @@ async function uploadSourceV2(projectId, projectNumber, source, wantBackend) {
|
|
|
59
59
|
await gcs.upload(uploadOpts, res.uploadUrl, undefined, true);
|
|
60
60
|
return res.storageSource;
|
|
61
61
|
}
|
|
62
|
-
const
|
|
63
|
-
await gcs.upsertBucket({
|
|
62
|
+
const baseName = `firebase-functions-src-${projectNumber}`;
|
|
63
|
+
const bucketName = await gcs.upsertBucket({
|
|
64
64
|
product: "functions",
|
|
65
65
|
projectId,
|
|
66
|
-
createMessage: `Creating Cloud Storage bucket in ${region} to store Functions source code uploads at ${
|
|
66
|
+
createMessage: `Creating Cloud Storage bucket in ${region} to store Functions source code uploads at ${baseName}...`,
|
|
67
67
|
req: {
|
|
68
|
-
|
|
68
|
+
baseName,
|
|
69
69
|
location: region,
|
|
70
|
+
purposeLabel: `functions-source-${region.toLowerCase()}`,
|
|
70
71
|
lifecycle: {
|
|
71
72
|
rule: [
|
|
72
73
|
{
|
|
@@ -415,7 +415,7 @@ async function promptResourceString(prompt, input, projectId, resolvedDefault) {
|
|
|
415
415
|
const notFound = new error_1.FirebaseError(`No instances of ${input.resource.type} found.`);
|
|
416
416
|
switch (input.resource.type) {
|
|
417
417
|
case "storage.googleapis.com/Bucket":
|
|
418
|
-
const buckets = await (0, storage_1.listBuckets)(projectId);
|
|
418
|
+
const buckets = (await (0, storage_1.listBuckets)(projectId)).map((b) => b.name);
|
|
419
419
|
if (buckets.length === 0) {
|
|
420
420
|
throw notFound;
|
|
421
421
|
}
|
|
@@ -436,7 +436,7 @@ async function promptResourceStrings(prompt, input, projectId) {
|
|
|
436
436
|
const notFound = new error_1.FirebaseError(`No instances of ${input.resource.type} found.`);
|
|
437
437
|
switch (input.resource.type) {
|
|
438
438
|
case "storage.googleapis.com/Bucket":
|
|
439
|
-
const buckets = await (0, storage_1.listBuckets)(projectId);
|
|
439
|
+
const buckets = (await (0, storage_1.listBuckets)(projectId)).map((b) => b.name);
|
|
440
440
|
if (buckets.length === 0) {
|
|
441
441
|
throw notFound;
|
|
442
442
|
}
|
|
@@ -384,6 +384,6 @@ async function checkJavaMajorVersion() {
|
|
|
384
384
|
});
|
|
385
385
|
}
|
|
386
386
|
exports.checkJavaMajorVersion = checkJavaMajorVersion;
|
|
387
|
-
exports.MIN_SUPPORTED_JAVA_MAJOR_VERSION =
|
|
388
|
-
exports.JAVA_DEPRECATION_WARNING = "firebase-tools
|
|
389
|
-
"Please install a JDK at version
|
|
387
|
+
exports.MIN_SUPPORTED_JAVA_MAJOR_VERSION = 21;
|
|
388
|
+
exports.JAVA_DEPRECATION_WARNING = "firebase-tools will drop support for Java version < 21 soon in firebase-tools@15. " +
|
|
389
|
+
"Please install a JDK at version 21 or above to get a compatible runtime.";
|
|
@@ -177,10 +177,11 @@ async function startAll(options, showUI = true, runningTestScript = false) {
|
|
|
177
177
|
if (targets.length === 0) {
|
|
178
178
|
throw new error_1.FirebaseError(`No emulators to start, run ${clc.bold("firebase init emulators")} to get started.`);
|
|
179
179
|
}
|
|
180
|
+
const deprecationNotices = [];
|
|
180
181
|
if (targets.some(downloadableEmulators_1.requiresJava)) {
|
|
181
182
|
if ((await commandUtils.checkJavaMajorVersion()) < commandUtils_1.MIN_SUPPORTED_JAVA_MAJOR_VERSION) {
|
|
182
183
|
utils.logLabeledError("emulators", commandUtils_1.JAVA_DEPRECATION_WARNING, "warn");
|
|
183
|
-
|
|
184
|
+
deprecationNotices.push(commandUtils_1.JAVA_DEPRECATION_WARNING);
|
|
184
185
|
}
|
|
185
186
|
}
|
|
186
187
|
if (options.logVerbosity) {
|
|
@@ -685,7 +686,7 @@ async function startAll(options, showUI = true, runningTestScript = false) {
|
|
|
685
686
|
count_all: running.length,
|
|
686
687
|
is_demo_project: String(isDemoProject),
|
|
687
688
|
});
|
|
688
|
-
return { deprecationNotices
|
|
689
|
+
return { deprecationNotices };
|
|
689
690
|
}
|
|
690
691
|
exports.startAll = startAll;
|
|
691
692
|
function getListenConfig(options, emulator) {
|