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
@@ -8,6 +8,8 @@ const devConnect_1 = require("../../gcp/devConnect");
8
8
  const projectUtils_1 = require("../../projectUtils");
9
9
  const prompt_1 = require("../../prompt");
10
10
  const utils_1 = require("../../utils");
11
+ const localbuilds_1 = require("../../apphosting/localbuilds");
12
+ const error_1 = require("../../error");
11
13
  async function default_1(context, options) {
12
14
  var _a;
13
15
  const projectId = (0, projectUtils_1.needProjectId)(options);
@@ -17,6 +19,7 @@ async function default_1(context, options) {
17
19
  context.backendConfigs = {};
18
20
  context.backendLocations = {};
19
21
  context.backendStorageUris = {};
22
+ context.backendLocalBuilds = {};
20
23
  const configs = getBackendConfigs(options);
21
24
  const { backends } = await (0, apphosting_1.listBackends)(projectId, "-");
22
25
  const foundBackends = [];
@@ -105,7 +108,26 @@ async function default_1(context, options) {
105
108
  if (skippedBackends.length > 0) {
106
109
  (0, utils_1.logLabeledWarning)("apphosting", `Skipping deployment of backend(s) ${skippedBackends.map((cfg) => cfg.backendId).join(", ")}.`);
107
110
  }
108
- return;
111
+ for (const cfg of Object.values(context.backendConfigs)) {
112
+ if (!cfg.localBuild) {
113
+ continue;
114
+ }
115
+ (0, utils_1.logLabeledBullet)("apphosting", `Starting local build for backend ${cfg.backendId}`);
116
+ try {
117
+ const { outputFiles, annotations, buildConfig } = await (0, localbuilds_1.localBuild)(options.projectRoot || "./", "nextjs");
118
+ if (outputFiles.length !== 1) {
119
+ throw new error_1.FirebaseError(`Local build for backend ${cfg.backendId} failed: No output files found.`);
120
+ }
121
+ context.backendLocalBuilds[cfg.backendId] = {
122
+ buildDir: outputFiles[0],
123
+ buildConfig,
124
+ annotations,
125
+ };
126
+ }
127
+ catch (e) {
128
+ throw new error_1.FirebaseError(`Local Build for backend ${cfg.backendId} failed: ${e}`);
129
+ }
130
+ }
109
131
  }
110
132
  exports.default = default_1;
111
133
  function getBackendConfigs(options) {
@@ -13,6 +13,11 @@ async function default_1(context, options) {
13
13
  (0, utils_1.logLabeledWarning)("apphosting", `Failed to find metadata for backend(s) ${backendIds.join(", ")}. Please contact support with the contents of your firebase-debug.log to report your issue.`);
14
14
  backendIds = backendIds.filter((id) => !missingBackends.includes(id));
15
15
  }
16
+ const localBuildBackends = backendIds.filter((id) => context.backendLocalBuilds[id]);
17
+ if (localBuildBackends.length > 0) {
18
+ (0, utils_1.logLabeledWarning)("apphosting", `Skipping backend(s) ${localBuildBackends.join(", ")}. Local Builds are not supported yet.`);
19
+ backendIds = backendIds.filter((id) => !localBuildBackends.includes(id));
20
+ }
16
21
  if (backendIds.length === 0) {
17
22
  return;
18
23
  }
@@ -7,20 +7,21 @@ const path = require("path");
7
7
  const tmp = require("tmp");
8
8
  const error_1 = require("../../error");
9
9
  const fsAsync = require("../../fsAsync");
10
- async function createArchive(config, rootDir) {
10
+ async function createArchive(config, rootDir, targetSubDir) {
11
11
  const tmpFile = tmp.fileSync({ prefix: `${config.backendId}-`, postfix: ".zip" }).name;
12
12
  const fileStream = fs.createWriteStream(tmpFile, {
13
13
  flags: "w",
14
14
  encoding: "binary",
15
15
  });
16
16
  const archive = archiver("zip");
17
+ const targetDir = targetSubDir ? path.join(rootDir, targetSubDir) : rootDir;
17
18
  const ignore = config.ignore || ["node_modules", ".git"];
18
19
  ignore.push("firebase-debug.log", "firebase-debug.*.log");
19
- const gitIgnorePatterns = parseGitIgnorePatterns(rootDir);
20
+ const gitIgnorePatterns = parseGitIgnorePatterns(targetDir);
20
21
  ignore.push(...gitIgnorePatterns);
21
22
  try {
22
23
  const files = await fsAsync.readdirRecursive({
23
- path: rootDir,
24
+ path: targetDir,
24
25
  ignore: ignore,
25
26
  isGitIgnore: true,
26
27
  });
@@ -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 bucketName = `firebase-functions-src-${projectNumber}`;
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 ${bucketName}...`,
66
+ createMessage: `Creating Cloud Storage bucket in ${region} to store Functions source code uploads at ${baseName}...`,
67
67
  req: {
68
- name: bucketName,
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 = 11;
388
- exports.JAVA_DEPRECATION_WARNING = "firebase-tools no longer supports Java versions before 11. " +
389
- "Please install a JDK at version 11 or above to get a compatible runtime.";
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
- throw new error_1.FirebaseError(commandUtils_1.JAVA_DEPRECATION_WARNING);
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) {
@@ -60,7 +60,7 @@ async function createInstance(args) {
60
60
  await client.post(`projects/${args.projectId}/instances`, {
61
61
  name: args.instanceId,
62
62
  region: args.location,
63
- databaseVersion: "POSTGRES_17",
63
+ databaseVersion: "POSTGRES_15",
64
64
  settings: {
65
65
  tier: "db-f1-micro",
66
66
  edition: "ENTERPRISE",
@@ -1,14 +1,16 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.getDownloadUrl = exports.getServiceAccount = exports.listBuckets = exports.upsertBucket = exports.createBucket = exports.getBucket = exports.deleteObject = exports.getObject = exports.uploadObject = exports.upload = exports.getDefaultBucket = void 0;
3
+ exports.getDownloadUrl = exports.getServiceAccount = exports.listBuckets = exports.upsertBucket = exports.randomString = exports.patchBucket = exports.createBucket = exports.getBucket = exports.deleteObject = exports.getObject = exports.uploadObject = exports.upload = exports.getDefaultBucket = void 0;
4
4
  const path = require("path");
5
5
  const clc = require("colorette");
6
+ const crypto_1 = require("crypto");
6
7
  const api_1 = require("../api");
7
8
  const apiv2_1 = require("../apiv2");
8
9
  const error_1 = require("../error");
9
10
  const logger_1 = require("../logger");
10
11
  const ensureApiEnabled_1 = require("../ensureApiEnabled");
11
12
  const utils = require("../utils");
13
+ const proto_1 = require("./proto");
12
14
  async function getDefaultBucket(projectId) {
13
15
  var _a;
14
16
  await (0, ensureApiEnabled_1.ensure)(projectId, (0, api_1.firebaseStorageOrigin)(), "storage", false);
@@ -120,37 +122,88 @@ async function createBucket(projectId, req, projectPrivate) {
120
122
  }
121
123
  }
122
124
  exports.createBucket = createBucket;
123
- async function upsertBucket(opts) {
125
+ async function patchBucket(bucketName, metadata) {
124
126
  try {
125
- await exports.getBucket(opts.req.name);
126
- return;
127
+ const localAPIClient = new apiv2_1.Client({ urlPrefix: (0, api_1.storageOrigin)() });
128
+ const mask = (0, proto_1.fieldMasks)(metadata, "labels", "acl", "defaultObjectAcl", "lifecycle");
129
+ const result = await localAPIClient.patch(`/storage/v1/b/${bucketName}`, metadata, { queryParams: { updateMask: mask.join(",") } });
130
+ return result.body;
127
131
  }
128
132
  catch (err) {
129
- const errStatus = (0, error_1.getErrStatus)(err.original);
130
- if (errStatus !== 403 && errStatus !== 404) {
131
- throw err;
132
- }
133
+ logger_1.logger.debug(err);
134
+ throw new error_1.FirebaseError("Failed to patch the storage bucket", {
135
+ original: err,
136
+ });
133
137
  }
134
- utils.logLabeledBullet(opts.product, opts.createMessage);
135
- try {
136
- await exports.createBucket(opts.projectId, opts.req, true);
138
+ }
139
+ exports.patchBucket = patchBucket;
140
+ function randomString(length) {
141
+ const chars = "abcdefghijklmnopqrstuvwxyz0123456789";
142
+ let result = "";
143
+ for (let i = length; i > 0; --i) {
144
+ result += chars[(0, crypto_1.randomInt)(chars.length)];
145
+ }
146
+ return result;
147
+ }
148
+ exports.randomString = randomString;
149
+ const dynamicDispatch = exports;
150
+ async function upsertBucket(opts) {
151
+ const existingBuckets = await dynamicDispatch.listBuckets(opts.projectId);
152
+ const managedBucket = existingBuckets.find((b) => opts.req.purposeLabel in (b.labels || {}));
153
+ if (managedBucket) {
154
+ return managedBucket.name;
155
+ }
156
+ const existingUnmanaged = existingBuckets.find((b) => b.name === opts.req.baseName);
157
+ if (existingUnmanaged) {
158
+ logger_1.logger.debug(`Found existing bucket ${existingUnmanaged.name} without purpose label. Because it is known not to be squatted, we can use it.`);
159
+ const labels = Object.assign(Object.assign({}, existingUnmanaged.labels), { [opts.req.purposeLabel]: "true" });
160
+ await dynamicDispatch.patchBucket(existingUnmanaged.name, { labels });
161
+ return existingUnmanaged.name;
137
162
  }
138
- catch (err) {
139
- if ((0, error_1.getErrStatus)(err.original) === 403) {
140
- utils.logLabeledWarning(opts.product, "Failed to create Cloud Storage bucket because user does not have sufficient permissions. " +
141
- "See https://cloud.google.com/storage/docs/access-control/iam-roles for more details on " +
142
- "IAM roles that are able to create a Cloud Storage bucket, and ask your project administrator " +
143
- "to grant you one of those roles.");
163
+ utils.logLabeledBullet(opts.product, opts.createMessage);
164
+ for (let retryCount = 0; retryCount < 5; retryCount++) {
165
+ const name = retryCount === 0
166
+ ? opts.req.baseName
167
+ : `${opts.req.baseName}-${dynamicDispatch.randomString(6)}`;
168
+ try {
169
+ await dynamicDispatch.createBucket(opts.projectId, {
170
+ name,
171
+ location: opts.req.location,
172
+ lifecycle: opts.req.lifecycle,
173
+ labels: {
174
+ [opts.req.purposeLabel]: "true",
175
+ },
176
+ }, true);
177
+ return name;
178
+ }
179
+ catch (err) {
180
+ if ((0, error_1.getErrStatus)(err.original) === 409) {
181
+ utils.logLabeledBullet(opts.product, `Bucket ${name} already exists, creating a new bucket with a conflict-avoiding hash`);
182
+ continue;
183
+ }
184
+ if ((0, error_1.getErrStatus)(err.original) === 403) {
185
+ utils.logLabeledWarning(opts.product, "Failed to create Cloud Storage bucket because user does not have sufficient permissions. " +
186
+ "See https://cloud.google.com/storage/docs/access-control/iam-roles for more details on " +
187
+ "IAM roles that are able to create a Cloud Storage bucket, and ask your project administrator " +
188
+ "to grant you one of those roles.");
189
+ }
190
+ throw err;
144
191
  }
145
- throw err;
146
192
  }
193
+ throw new error_1.FirebaseError("Failed to create a unique Cloud Storage bucket name after 5 attempts.");
147
194
  }
148
195
  exports.upsertBucket = upsertBucket;
149
196
  async function listBuckets(projectId) {
150
197
  try {
198
+ let buckets = [];
151
199
  const localAPIClient = new apiv2_1.Client({ urlPrefix: (0, api_1.storageOrigin)() });
152
- const result = await localAPIClient.get(`/storage/v1/b?project=${projectId}`);
153
- return result.body.items.map((bucket) => bucket.name);
200
+ let pageToken;
201
+ do {
202
+ const result = await localAPIClient.get(`/storage/v1/b?project=${projectId}`, { queryParams: pageToken ? { pageToken } : {} });
203
+ buckets = buckets.concat(result.body.items || []);
204
+ pageToken = result.body.nextPageToken;
205
+ } while (pageToken);
206
+ return buckets;
154
207
  }
155
208
  catch (err) {
156
209
  logger_1.logger.debug(err);
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.newUniqueId = exports.toDNSCompatibleId = exports.actuate = exports.askQuestions = void 0;
3
+ exports.newUniqueId = exports.toDNSCompatibleId = exports.actuate = exports.askQuestions = exports.FDC_DEFAULT_REGION = void 0;
4
4
  const path_1 = require("path");
5
5
  const clc = require("colorette");
6
6
  const fs = require("fs-extra");
@@ -21,6 +21,7 @@ const sdk = require("./sdk");
21
21
  const fdcExperience_1 = require("../../../gemini/fdcExperience");
22
22
  const configstore_1 = require("../../../configstore");
23
23
  const track_1 = require("../../../track");
24
+ exports.FDC_DEFAULT_REGION = "us-east4";
24
25
  const DATACONNECT_YAML_TEMPLATE = (0, templates_1.readTemplateSync)("init/dataconnect/dataconnect.yaml");
25
26
  const CONNECTOR_YAML_TEMPLATE = (0, templates_1.readTemplateSync)("init/dataconnect/connector.yaml");
26
27
  const SCHEMA_TEMPLATE = (0, templates_1.readTemplateSync)("init/dataconnect/schema.gql");
@@ -76,6 +77,9 @@ async function askQuestions(setup) {
76
77
  if (hasBilling) {
77
78
  await promptForCloudSQL(setup, info);
78
79
  }
80
+ else if (info.appDescription) {
81
+ await promptForLocation(setup, info);
82
+ }
79
83
  }
80
84
  setup.featureInfo = setup.featureInfo || {};
81
85
  setup.featureInfo.dataconnect = info;
@@ -93,7 +97,7 @@ async function actuate(setup, config, options) {
93
97
  }
94
98
  info.serviceId = info.serviceId || defaultServiceId();
95
99
  info.cloudSqlInstanceId = info.cloudSqlInstanceId || `${info.serviceId.toLowerCase()}-fdc`;
96
- info.locationId = info.locationId || `us-central1`;
100
+ info.locationId = info.locationId || exports.FDC_DEFAULT_REGION;
97
101
  info.cloudSqlDatabase = info.cloudSqlDatabase || `fdcdb`;
98
102
  try {
99
103
  await actuateWithInfo(setup, config, info, options);
@@ -269,10 +273,10 @@ async function writeConnectorFiles(config, connectorInfo, options) {
269
273
  function subDataconnectYamlValues(replacementValues) {
270
274
  const replacements = {
271
275
  serviceId: "__serviceId__",
276
+ locationId: "__location__",
272
277
  cloudSqlDatabase: "__cloudSqlDatabase__",
273
278
  cloudSqlInstanceId: "__cloudSqlInstanceId__",
274
279
  connectorDirs: "__connectorDirs__",
275
- locationId: "__location__",
276
280
  };
277
281
  let replaced = DATACONNECT_YAML_TEMPLATE;
278
282
  for (const [k, v] of Object.entries(replacementValues)) {
@@ -422,12 +426,7 @@ async function promptForCloudSQL(setup, info) {
422
426
  }
423
427
  }
424
428
  if (info.locationId === "") {
425
- const choices = await locationChoices(setup);
426
- info.locationId = await (0, prompt_1.select)({
427
- message: "What location would like to use?",
428
- choices,
429
- default: "us-east4",
430
- });
429
+ await promptForLocation(setup, info);
431
430
  info.shouldProvisionCSQL = await (0, prompt_1.confirm)({
432
431
  message: `Would you like to provision your Cloud SQL instance and database now?`,
433
432
  default: true,
@@ -445,6 +444,16 @@ async function promptForCloudSQL(setup, info) {
445
444
  }
446
445
  return;
447
446
  }
447
+ async function promptForLocation(setup, info) {
448
+ if (info.locationId === "") {
449
+ const choices = await locationChoices(setup);
450
+ info.locationId = await (0, prompt_1.select)({
451
+ message: "What location should the new Cloud SQL instance be in?",
452
+ choices,
453
+ default: exports.FDC_DEFAULT_REGION,
454
+ });
455
+ }
456
+ }
448
457
  async function locationChoices(setup) {
449
458
  if (setup.projectId) {
450
459
  const locations = await (0, client_1.listLocations)(setup.projectId);
@@ -3,101 +3,92 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.doSetup = void 0;
4
4
  const clc = require("colorette");
5
5
  const _ = require("lodash");
6
- const error_1 = require("../../error");
7
6
  const projects_1 = require("../../management/projects");
8
7
  const logger_1 = require("../../logger");
9
8
  const utils = require("../../utils");
10
9
  const prompt = require("../../prompt");
10
+ const requireAuth_1 = require("../../requireAuth");
11
+ const constants_1 = require("../../emulator/constants");
12
+ const error_1 = require("../../error");
11
13
  const OPTION_NO_PROJECT = "Don't set up a default project";
12
14
  const OPTION_USE_PROJECT = "Use an existing project";
13
15
  const OPTION_NEW_PROJECT = "Create a new project";
14
16
  const OPTION_ADD_FIREBASE = "Add Firebase to an existing Google Cloud Platform project";
15
- function toInitProjectInfo(projectMetaData) {
16
- const { projectId, displayName, resources } = projectMetaData;
17
- return {
18
- id: projectId,
19
- label: `${projectId}` + (displayName ? ` (${displayName})` : ""),
20
- instance: resources === null || resources === void 0 ? void 0 : resources.realtimeDatabaseInstance,
21
- location: resources === null || resources === void 0 ? void 0 : resources.locationId,
22
- };
23
- }
24
- async function promptAndCreateNewProject(options) {
25
- utils.logBullet("If you want to create a project in a Google Cloud organization or folder, please use " +
26
- `"firebase projects:create" instead, and return to this command when you've created the project.`);
27
- const { projectId, displayName } = await (0, projects_1.promptProjectCreation)(options);
28
- if (!projectId) {
29
- throw new error_1.FirebaseError("Project ID cannot be empty");
30
- }
31
- return await (0, projects_1.createFirebaseProjectAndLog)(projectId, { displayName });
32
- }
33
- async function promptAndAddFirebaseToCloudProject() {
34
- const projectId = await (0, projects_1.promptAvailableProjectId)();
35
- if (!projectId) {
36
- throw new error_1.FirebaseError("Project ID cannot be empty");
37
- }
38
- return await (0, projects_1.addFirebaseToCloudProjectAndLog)(projectId);
39
- }
40
- async function projectChoicePrompt(options) {
41
- const choices = [OPTION_USE_PROJECT, OPTION_NEW_PROJECT, OPTION_ADD_FIREBASE, OPTION_NO_PROJECT];
42
- const projectSetupOption = await prompt.select({
43
- message: "Please select an option:",
44
- choices,
45
- });
46
- switch (projectSetupOption) {
47
- case OPTION_USE_PROJECT:
48
- return (0, projects_1.getOrPromptProject)(options);
49
- case OPTION_NEW_PROJECT:
50
- return promptAndCreateNewProject(options);
51
- case OPTION_ADD_FIREBASE:
52
- return promptAndAddFirebaseToCloudProject();
53
- default:
54
- return;
55
- }
56
- }
57
17
  async function doSetup(setup, config, options) {
58
- var _a, _b, _c;
18
+ var _a, _b;
59
19
  setup.project = {};
60
20
  logger_1.logger.info();
61
21
  logger_1.logger.info(`First, let's associate this project directory with a Firebase project.`);
62
22
  logger_1.logger.info(`You can create multiple project aliases by running ${clc.bold("firebase use --add")}, `);
63
- logger_1.logger.info(`but for now we'll just set up a default project.`);
64
23
  logger_1.logger.info();
24
+ if (options.project) {
25
+ if (constants_1.Constants.isDemoProject(options.project)) {
26
+ logger_1.logger.info(`Skipping Firebase project setup because a demo project is provided`);
27
+ return;
28
+ }
29
+ await (0, requireAuth_1.requireAuth)(options);
30
+ await usingProject(setup, config, options.project);
31
+ return;
32
+ }
65
33
  const projectFromRcFile = (_b = (_a = setup.rcfile) === null || _a === void 0 ? void 0 : _a.projects) === null || _b === void 0 ? void 0 : _b.default;
66
- if (projectFromRcFile && !options.project) {
67
- utils.logBullet(`.firebaserc already has a default project, using ${projectFromRcFile}.`);
68
- const rcProject = await (0, projects_1.getFirebaseProject)(projectFromRcFile);
69
- setup.projectId = rcProject.projectId;
70
- setup.projectLocation = (_c = rcProject === null || rcProject === void 0 ? void 0 : rcProject.resources) === null || _c === void 0 ? void 0 : _c.locationId;
34
+ if (projectFromRcFile) {
35
+ await (0, requireAuth_1.requireAuth)(options);
36
+ await usingProject(setup, config, projectFromRcFile, ".firebaserc");
71
37
  return;
72
38
  }
73
- let projectMetaData;
74
- if (options.project) {
75
- logger_1.logger.debug(`Using project from CLI flag: ${options.project}`);
76
- projectMetaData = await (0, projects_1.getFirebaseProject)(options.project);
39
+ const projectEnvVar = utils.envOverride("FIREBASE_PROJECT", "");
40
+ if (projectEnvVar) {
41
+ await (0, requireAuth_1.requireAuth)(options);
42
+ await usingProject(setup, config, projectEnvVar, "$FIREBASE_PROJECT");
43
+ return;
77
44
  }
78
- else {
79
- const projectEnvVar = utils.envOverride("FIREBASE_PROJECT", "");
80
- if (projectEnvVar) {
81
- logger_1.logger.debug(`Using project from $FIREBASE_PROJECT: ${projectEnvVar}`);
82
- projectMetaData = await (0, projects_1.getFirebaseProject)(projectEnvVar);
45
+ if (options.nonInteractive) {
46
+ logger_1.logger.info("No default project found. Continuing without a project in non interactive mode.");
47
+ return;
48
+ }
49
+ const choices = [OPTION_USE_PROJECT, OPTION_NEW_PROJECT, OPTION_ADD_FIREBASE, OPTION_NO_PROJECT];
50
+ const projectSetupOption = await prompt.select({
51
+ message: "Please select an option:",
52
+ choices,
53
+ });
54
+ switch (projectSetupOption) {
55
+ case OPTION_USE_PROJECT: {
56
+ await (0, requireAuth_1.requireAuth)(options);
57
+ const pm = await (0, projects_1.selectProjectInteractively)();
58
+ return await usingProjectMetadata(setup, config, pm);
83
59
  }
84
- else {
85
- if (options.nonInteractive) {
86
- logger_1.logger.info("No default project found. Continuing without a project in non interactive mode.");
87
- return;
88
- }
89
- projectMetaData = await projectChoicePrompt(options);
90
- if (!projectMetaData) {
91
- return;
92
- }
60
+ case OPTION_NEW_PROJECT: {
61
+ utils.logBullet("If you want to create a project in a Google Cloud organization or folder, please use " +
62
+ `"firebase projects:create" instead, and return to this command when you've created the project.`);
63
+ await (0, requireAuth_1.requireAuth)(options);
64
+ const { projectId, displayName } = await (0, projects_1.promptProjectCreation)(options);
65
+ const pm = await (0, projects_1.createFirebaseProjectAndLog)(projectId, { displayName });
66
+ return await usingProjectMetadata(setup, config, pm);
93
67
  }
68
+ case OPTION_ADD_FIREBASE: {
69
+ await (0, requireAuth_1.requireAuth)(options);
70
+ const pm = await (0, projects_1.addFirebaseToCloudProjectAndLog)(await (0, projects_1.promptAvailableProjectId)());
71
+ return await usingProjectMetadata(setup, config, pm);
72
+ }
73
+ default:
74
+ return;
94
75
  }
95
- const projectInfo = toInitProjectInfo(projectMetaData);
96
- utils.logBullet(`Using project ${projectInfo.label}`);
97
- _.set(setup.rcfile, "projects.default", projectInfo.id);
98
- setup.projectId = projectInfo.id;
99
- setup.instance = projectInfo.instance;
100
- setup.projectLocation = projectInfo.location;
101
- utils.makeActiveProject(config.projectDir, projectInfo.id);
102
76
  }
103
77
  exports.doSetup = doSetup;
78
+ async function usingProject(setup, config, projectId, from = "") {
79
+ const pm = await (0, projects_1.getFirebaseProject)(projectId);
80
+ const label = `${pm.projectId}` + (pm.displayName ? ` (${pm.displayName})` : "");
81
+ utils.logBullet(`Using project ${label} ${from ? "from ${from}" : ""}.`);
82
+ await usingProjectMetadata(setup, config, pm);
83
+ }
84
+ async function usingProjectMetadata(setup, config, pm) {
85
+ var _a, _b;
86
+ if (!pm) {
87
+ throw new error_1.FirebaseError("null FirebaseProjectMetadata");
88
+ }
89
+ _.set(setup.rcfile, "projects.default", pm.projectId);
90
+ setup.projectId = pm.projectId;
91
+ setup.instance = (_a = pm.resources) === null || _a === void 0 ? void 0 : _a.realtimeDatabaseInstance;
92
+ setup.projectLocation = (_b = pm.resources) === null || _b === void 0 ? void 0 : _b.locationId;
93
+ utils.makeActiveProject(config.projectDir, pm.projectId);
94
+ }
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.checkFirebaseEnabledForCloudProject = exports.getProject = exports.getFirebaseProject = exports.checkAndRecommendProjectId = exports.listFirebaseProjects = exports.getAvailableCloudProjectPage = exports.getFirebaseProjectPage = exports.addFirebaseToCloudProject = exports.createCloudProject = exports.promptAvailableProjectId = exports.getOrPromptProject = exports.addFirebaseToCloudProjectAndLog = exports.createFirebaseProjectAndLog = exports.promptProjectCreation = exports.ProjectParentResourceType = void 0;
3
+ exports.checkFirebaseEnabledForCloudProject = exports.getProject = exports.getFirebaseProject = exports.checkAndRecommendProjectId = exports.listFirebaseProjects = exports.getAvailableCloudProjectPage = exports.getFirebaseProjectPage = exports.addFirebaseToCloudProject = exports.createCloudProject = exports.promptAvailableProjectId = exports.selectProjectInteractively = exports.getOrPromptProject = exports.addFirebaseToCloudProjectAndLog = exports.createFirebaseProjectAndLog = exports.promptProjectCreation = exports.ProjectParentResourceType = void 0;
4
4
  const clc = require("colorette");
5
5
  const ora = require("ora");
6
6
  const apiv2_1 = require("../apiv2");
@@ -11,6 +11,7 @@ const api = require("../api");
11
11
  const logger_1 = require("../logger");
12
12
  const utils = require("../utils");
13
13
  const ensureApiEnabled_1 = require("../ensureApiEnabled");
14
+ const constants_1 = require("../emulator/constants");
14
15
  const TIMEOUT_MILLIS = 30000;
15
16
  const MAXIMUM_PROMPT_LIST = 100;
16
17
  const PROJECT_LIST_PAGE_SIZE = 1000;
@@ -33,6 +34,9 @@ async function promptProjectCreation(options) {
33
34
  else if (projectId.length > 30) {
34
35
  return "Project ID cannot be longer than 30 characters";
35
36
  }
37
+ if (constants_1.Constants.isDemoProject(projectId)) {
38
+ return "Project ID cannot starts with demo-";
39
+ }
36
40
  try {
37
41
  const { isAvailable, suggestedProjectId } = await checkAndRecommendProjectId(projectId);
38
42
  if (!isAvailable && suggestedProjectId) {
@@ -137,13 +141,20 @@ async function selectProjectInteractively(pageSize = MAXIMUM_PROMPT_LIST) {
137
141
  }
138
142
  if (nextPageToken) {
139
143
  logger_1.logger.debug(`Found more than ${projects.length} projects, selecting via prompt`);
140
- return selectProjectByPrompting();
144
+ return await getFirebaseProject(await selectProjectByPrompting());
141
145
  }
142
146
  return selectProjectFromList(projects);
143
147
  }
148
+ exports.selectProjectInteractively = selectProjectInteractively;
144
149
  async function selectProjectByPrompting() {
145
150
  const projectId = await prompt.input("Please input the project ID you would like to use:");
146
- return await getFirebaseProject(projectId);
151
+ if (!projectId) {
152
+ throw new error_1.FirebaseError("Project ID cannot be empty");
153
+ }
154
+ if (constants_1.Constants.isDemoProject(projectId)) {
155
+ throw new error_1.FirebaseError("Project ID cannot starts with demo-");
156
+ }
157
+ return projectId;
147
158
  }
148
159
  async function selectProjectFromList(projects = []) {
149
160
  const choices = projects
@@ -179,7 +190,8 @@ async function promptAvailableProjectId() {
179
190
  throw new error_1.FirebaseError("There are no available Google Cloud projects to add Firebase services.");
180
191
  }
181
192
  if (nextPageToken) {
182
- return await prompt.input("Please input the ID of the Google Cloud Project you would like to add Firebase:");
193
+ logger_1.logger.debug(`Found more than ${projects.length} projects, selecting via prompt`);
194
+ return await selectProjectByPrompting();
183
195
  }
184
196
  else {
185
197
  const choices = projects
package/lib/mcp/index.js CHANGED
@@ -22,6 +22,7 @@ const logging_transport_1 = require("./logging-transport");
22
22
  const env_1 = require("../env");
23
23
  const timeout_1 = require("../timeout");
24
24
  const resources_1 = require("./resources");
25
+ const crossSpawn = require("cross-spawn");
25
26
  const SERVER_VERSION = "0.3.0";
26
27
  const cmd = new command_1.Command("mcp");
27
28
  const orderedLogLevels = [
@@ -212,8 +213,16 @@ class FirebaseMcpServer {
212
213
  config: config_1.Config.load(options, true) || new config_1.Config({}, options),
213
214
  rc: (0, rc_1.loadRC)(options),
214
215
  accountEmail,
216
+ firebaseCliCommand: this._getFirebaseCliCommand(),
215
217
  };
216
218
  }
219
+ _getFirebaseCliCommand() {
220
+ if (!this.cliCommand) {
221
+ const testCommand = crossSpawn.sync("firebase --version");
222
+ this.cliCommand = testCommand.error ? "npx firebase-tools@latest" : "firebase";
223
+ }
224
+ return this.cliCommand;
225
+ }
217
226
  async mcpListTools() {
218
227
  await Promise.all([this.detectActiveFeatures(), this.detectProjectRoot()]);
219
228
  const hasActiveProject = !!(await this.getProjectId());