firebase-tools 14.18.0 → 14.19.1

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.
Files changed (52) hide show
  1. package/README.md +11 -5
  2. package/lib/appUtils.js +230 -0
  3. package/lib/apphosting/localbuilds.js +23 -0
  4. package/lib/bin/mcp.js +16 -1
  5. package/lib/commands/index.js +8 -0
  6. package/lib/commands/init.js +0 -2
  7. package/lib/commands/remoteconfig-experiments-delete.js +32 -0
  8. package/lib/commands/remoteconfig-experiments-get.js +20 -0
  9. package/lib/commands/remoteconfig-experiments-list.js +38 -0
  10. package/lib/commands/remoteconfig-rollouts-delete.js +32 -0
  11. package/lib/commands/remoteconfig-rollouts-get.js +20 -0
  12. package/lib/commands/remoteconfig-rollouts-list.js +38 -0
  13. package/lib/config.js +4 -2
  14. package/lib/deploy/apphosting/deploy.js +26 -10
  15. package/lib/deploy/apphosting/prepare.js +23 -1
  16. package/lib/deploy/apphosting/release.js +5 -0
  17. package/lib/deploy/apphosting/util.js +4 -3
  18. package/lib/deploy/functions/deploy.js +5 -4
  19. package/lib/deploy/functions/params.js +2 -2
  20. package/lib/emulator/commandUtils.js +3 -3
  21. package/lib/emulator/controller.js +3 -2
  22. package/lib/gcp/cloudsql/cloudsqladmin.js +1 -1
  23. package/lib/gcp/storage.js +73 -20
  24. package/lib/init/features/dataconnect/index.js +18 -9
  25. package/lib/init/features/project.js +66 -75
  26. package/lib/management/projects.js +16 -4
  27. package/lib/mcp/index.js +9 -0
  28. package/lib/mcp/prompts/core/deploy.js +8 -8
  29. package/lib/mcp/prompts/core/init.js +15 -18
  30. package/lib/mcp/prompts/crashlytics/connect.js +2 -2
  31. package/lib/mcp/prompts/index.js +30 -9
  32. package/lib/mcp/resources/guides/init_ai.js +8 -0
  33. package/lib/mcp/resources/guides/init_auth.js +4 -0
  34. package/lib/mcp/resources/guides/init_firestore_rules.js +2 -0
  35. package/lib/mcp/resources/index.js +16 -1
  36. package/lib/mcp/tools/apphosting/list_backends.js +1 -1
  37. package/lib/mcp/tools/core/get_environment.js +34 -17
  38. package/lib/mcp/tools/core/init.js +2 -1
  39. package/lib/mcp/tools/core/logout.js +1 -2
  40. package/lib/mcp/tools/functions/get_logs.js +9 -7
  41. package/lib/mcp/tools/index.js +3 -2
  42. package/lib/remoteconfig/deleteExperiment.js +32 -0
  43. package/lib/remoteconfig/deleteRollout.js +33 -0
  44. package/lib/remoteconfig/getExperiment.js +52 -0
  45. package/lib/remoteconfig/getRollout.js +43 -0
  46. package/lib/remoteconfig/interfaces.js +3 -1
  47. package/lib/remoteconfig/listExperiments.js +72 -0
  48. package/lib/remoteconfig/listRollouts.js +72 -0
  49. package/lib/remoteconfig/options.js +4 -0
  50. package/lib/track.js +6 -4
  51. package/package.json +3 -1
  52. package/schema/firebase-config.json +6 -0
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 | 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. |
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
 
@@ -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
+ }
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.localBuild = void 0;
4
+ const build_1 = require("@apphosting/build");
5
+ async function localBuild(projectRoot, framework) {
6
+ var _a, _b, _c;
7
+ const apphostingBuildOutput = await (0, build_1.localBuild)(projectRoot, framework);
8
+ const annotations = Object.fromEntries(Object.entries(apphostingBuildOutput.metadata).map(([key, value]) => [key, String(value)]));
9
+ const env = (_a = apphostingBuildOutput.runConfig.environmentVariables) === null || _a === void 0 ? void 0 : _a.map(({ variable, value, availability }) => ({
10
+ variable,
11
+ value,
12
+ availability,
13
+ }));
14
+ return {
15
+ outputFiles: (_c = (_b = apphostingBuildOutput.outputFiles) === null || _b === void 0 ? void 0 : _b.serverApp.include) !== null && _c !== void 0 ? _c : [],
16
+ annotations,
17
+ buildConfig: {
18
+ runCommand: apphostingBuildOutput.runConfig.runCommand,
19
+ env: env !== null && env !== void 0 ? env : [],
20
+ },
21
+ };
22
+ }
23
+ exports.localBuild = localBuild;
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
- return;
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 || "")
@@ -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 = {};
@@ -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
- const data = cjson.load(filePath);
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,20 @@ async function default_1(context, options) {
17
17
  if (!context.backendConfigs) {
18
18
  return;
19
19
  }
20
- await Promise.all(Object.values(context.backendLocations).map(async (loc) => {
21
- const bucketName = `firebaseapphosting-sources-${options.projectNumber}-${loc.toLowerCase()}`;
22
- await gcs.upsertBucket({
20
+ const bucketsPerLocation = {};
21
+ await Promise.all(Object.entries(context.backendLocations).map(async ([backendId, loc]) => {
22
+ const cfg = context.backendConfigs[backendId];
23
+ if (!cfg) {
24
+ throw new error_1.FirebaseError(`Failed to find config for backend ${backendId}. Please contact support with the contents of your firebase-debug.log to report your issue.`);
25
+ }
26
+ const baseName = `firebaseapphosting-sources-${options.projectNumber}-${loc.toLowerCase()}`;
27
+ const resolvedName = await gcs.upsertBucket({
23
28
  product: "apphosting",
24
- createMessage: `Creating Cloud Storage bucket in ${loc} to store App Hosting source code uploads at ${bucketName}...`,
29
+ createMessage: `Creating Cloud Storage bucket in ${loc} to store App Hosting source code uploads at ${baseName}...`,
25
30
  projectId,
26
31
  req: {
27
- name: bucketName,
32
+ baseName,
33
+ purposeLabel: `apphosting-source-${loc.toLowerCase()}`,
28
34
  location: loc,
29
35
  lifecycle: {
30
36
  rule: [
@@ -40,21 +46,31 @@ async function default_1(context, options) {
40
46
  },
41
47
  },
42
48
  });
49
+ bucketsPerLocation[loc] = resolvedName;
43
50
  }));
44
51
  await Promise.all(Object.values(context.backendConfigs).map(async (cfg) => {
45
- const projectSourcePath = options.projectRoot ? options.projectRoot : process.cwd();
46
- const zippedSourcePath = await (0, util_1.createArchive)(cfg, projectSourcePath);
52
+ var _a;
53
+ const rootDir = (_a = options.projectRoot) !== null && _a !== void 0 ? _a : process.cwd();
54
+ let builtAppDir;
55
+ if (cfg.localBuild) {
56
+ builtAppDir = context.backendLocalBuilds[cfg.backendId].buildDir;
57
+ if (!builtAppDir) {
58
+ throw new error_1.FirebaseError(`No local build dir found for ${cfg.backendId}`);
59
+ }
60
+ }
61
+ const zippedSourcePath = await (0, util_1.createArchive)(cfg, rootDir, builtAppDir);
62
+ (0, utils_1.logLabeledBullet)("apphosting", `Zipped ${cfg.localBuild ? "built app" : "source"} for backend ${cfg.backendId}`);
47
63
  const backendLocation = context.backendLocations[cfg.backendId];
48
64
  if (!backendLocation) {
49
65
  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
66
  }
51
- (0, utils_1.logLabeledBullet)("apphosting", `Uploading source code at ${projectSourcePath} for backend ${cfg.backendId}...`);
52
- const bucketName = `firebaseapphosting-sources-${options.projectNumber}-${backendLocation.toLowerCase()}`;
67
+ (0, utils_1.logLabeledBullet)("apphosting", `Uploading ${cfg.localBuild ? "built app" : "source"} for backend ${cfg.backendId}...`);
68
+ const bucketName = bucketsPerLocation[backendLocation];
53
69
  const { bucket, object } = await gcs.uploadObject({
54
70
  file: zippedSourcePath,
55
71
  stream: fs.createReadStream(zippedSourcePath),
56
72
  }, bucketName);
57
- (0, utils_1.logLabeledBullet)("apphosting", `Source code uploaded at gs://${bucket}/${object}`);
73
+ (0, utils_1.logLabeledBullet)("apphosting", `Uploaded at gs://${bucket}/${object}`);
58
74
  context.backendStorageUris[cfg.backendId] =
59
75
  `gs://${bucketName}/${path.basename(zippedSourcePath)}`;
60
76
  }));