firebase-tools 14.21.0 → 14.23.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.
Files changed (40) hide show
  1. package/README.md +13 -5
  2. package/lib/apiv2.js +12 -2
  3. package/lib/appdistribution/client.js +62 -16
  4. package/lib/appdistribution/types.js +1 -12
  5. package/lib/appdistribution/yaml_helper.js +69 -0
  6. package/lib/commands/appdistribution-groups-list.js +3 -5
  7. package/lib/commands/appdistribution-testcases-export.js +32 -0
  8. package/lib/commands/appdistribution-testcases-import.js +34 -0
  9. package/lib/commands/appdistribution-testers-list.js +3 -5
  10. package/lib/commands/deploy.js +6 -4
  11. package/lib/commands/hosting-sites-create.js +4 -3
  12. package/lib/commands/index.js +8 -5
  13. package/lib/commands/init.js +5 -7
  14. package/lib/deploy/functions/params.js +7 -11
  15. package/lib/emulator/auth/operations.js +7 -1
  16. package/lib/emulator/downloadableEmulatorInfo.json +25 -25
  17. package/lib/emulator/functionsEmulatorRuntime.js +8 -0
  18. package/lib/emulator/taskQueue.js +5 -0
  19. package/lib/experiments.js +0 -6
  20. package/lib/firestore/api.js +22 -4
  21. package/lib/frameworks/angular/index.js +1 -1
  22. package/lib/frameworks/flutter/index.js +1 -1
  23. package/lib/frameworks/next/index.js +1 -1
  24. package/lib/frameworks/nuxt/index.js +1 -1
  25. package/lib/frameworks/vite/index.js +5 -2
  26. package/lib/hosting/interactive.js +14 -19
  27. package/lib/init/features/hosting/github.js +6 -6
  28. package/lib/init/features/hosting/index.js +89 -86
  29. package/lib/init/features/index.js +3 -2
  30. package/lib/init/index.js +5 -1
  31. package/lib/management/provisioning/errorHandler.js +54 -0
  32. package/lib/management/provisioning/provision.js +2 -5
  33. package/lib/mcp/resources/guides/init_backend.js +3 -26
  34. package/lib/mcp/resources/guides/init_hosting.js +15 -10
  35. package/lib/mcp/tools/core/init.js +27 -0
  36. package/lib/mcp/tools/functions/index.js +2 -1
  37. package/lib/mcp/tools/functions/list_functions.js +48 -0
  38. package/lib/utils.js +4 -1
  39. package/package.json +2 -2
  40. package/schema/firebase-config.json +0 -1
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
- # Firebase CLI [![Actions Status][gh-actions-badge]][gh-actions] [![Node Version][node-badge]][npm] [![NPM version][npm-badge]][npm] [![Install MCP Server](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/en/install-mcp?name=firebase&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsImZpcmViYXNlLXRvb2xzIiwiZXhwZXJpbWVudGFsOm1jcCIsIi0tZGlyIiwiLiJdfQ==)
1
+ # Firebase CLI and MCP Server [![Actions Status][gh-actions-badge]][gh-actions] [![Node Version][node-badge]][npm] [![NPM version][npm-badge]][npm] [![Install MCP Server](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/en/install-mcp?name=firebase&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsImZpcmViYXNlLXRvb2xzIiwiZXhwZXBpbWVudGFsOm1jcCIsIi0tZGlyIiwiLiJdfQ==)
2
2
 
3
- The Firebase Command Line Interface (CLI) Tools can be used to test, manage, and deploy your Firebase project from the command line.
3
+ The Firebase Command Line Interface (CLI) Tools can be used to test, manage, and deploy your Firebase project from the command line. This repository is also the home of the official Firebase MCP Server. For more information, please see the [Firebase MCP Server documentation](./src/mcp).
4
4
 
5
5
  - Deploy code and assets to your Firebase projects
6
6
  - Run a local web server for your Firebase Hosting site
@@ -87,9 +87,17 @@ These commands let you deploy and interact with your Firebase services.
87
87
 
88
88
  ### App Distribution Commands
89
89
 
90
- | Command | Description |
91
- | ------------------------------ | ---------------------- |
92
- | **appdistribution:distribute** | Upload a distribution. |
90
+ | Command | Description |
91
+ | ------------------------------------ | ---------------------------------------------------------------------------------------- |
92
+ | **appdistribution:distribute** | Upload a release binary and optionally distribute it to testers and run automated tests. |
93
+ | **appdistribution:testers:list** | List testers in project. |
94
+ | **appdistribution:testers:add** | Add testers to project (and group, if specified via flag). |
95
+ | **appdistribution:testers:remove** | Remove testers from a project (or group, if specified via flag). |
96
+ | **appdistribution:groups:list** | List groups (of testers). |
97
+ | **appdistribution:groups:create** | Create a group (of testers). |
98
+ | **appdistribution:groups:delete** | Delete a group (of testers). |
99
+ | **appdistribution:testcases:export** | Export test cases as a YAML file. |
100
+ | **appdistribution:testcases:import** | Import test cases from YAML file. |
93
101
 
94
102
  ### Auth Commands
95
103
 
package/lib/apiv2.js CHANGED
@@ -205,7 +205,12 @@ class Client {
205
205
  fetchOptions.agent = new proxy_agent_1.ProxyAgent();
206
206
  }
207
207
  if (options.signal) {
208
- fetchOptions.signal = options.signal;
208
+ const signal = options.signal;
209
+ signal.reason = "";
210
+ signal.throwIfAborted = () => {
211
+ throw new error_1.FirebaseError("Aborted");
212
+ };
213
+ fetchOptions.signal = signal;
209
214
  }
210
215
  let reqTimeout;
211
216
  if (options.timeout) {
@@ -213,7 +218,12 @@ class Client {
213
218
  reqTimeout = setTimeout(() => {
214
219
  controller.abort();
215
220
  }, options.timeout);
216
- fetchOptions.signal = controller.signal;
221
+ const signal = controller.signal;
222
+ signal.reason = "";
223
+ signal.throwIfAborted = () => {
224
+ throw new error_1.FirebaseError("Aborted");
225
+ };
226
+ fetchOptions.signal = signal;
217
227
  }
218
228
  if (typeof options.body === "string" || isStream(options.body)) {
219
229
  fetchOptions.body = options.body;
@@ -6,7 +6,6 @@ const operationPoller = require("../operation-poller");
6
6
  const error_1 = require("../error");
7
7
  const apiv2_1 = require("../apiv2");
8
8
  const api_1 = require("../api");
9
- const types_1 = require("./types");
10
9
  class AppDistributionClient {
11
10
  constructor() {
12
11
  this.appDistroV1Client = new apiv2_1.Client({
@@ -84,7 +83,7 @@ class AppDistributionClient {
84
83
  await this.appDistroV1Client.post(`/${releaseName}:distribute`, data);
85
84
  }
86
85
  catch (err) {
87
- let errorMessage = err.message;
86
+ let errorMessage = (0, error_1.getErrMsg)(err);
88
87
  const errorStatus = (_c = (_b = (_a = err === null || err === void 0 ? void 0 : err.context) === null || _a === void 0 ? void 0 : _a.body) === null || _b === void 0 ? void 0 : _b.error) === null || _c === void 0 ? void 0 : _c.status;
89
88
  if (errorStatus === "FAILED_PRECONDITION") {
90
89
  errorMessage = "invalid testers";
@@ -100,12 +99,10 @@ class AppDistributionClient {
100
99
  }
101
100
  async listTesters(projectName, groupName) {
102
101
  var _a;
103
- const listTestersResponse = {
104
- testers: [],
105
- };
102
+ const testers = [];
106
103
  const client = this.appDistroV1Client;
107
- let pageToken;
108
104
  const filter = groupName ? `groups=${projectName}/groups/${groupName}` : null;
105
+ let pageToken;
109
106
  do {
110
107
  const queryParams = pageToken ? { pageToken } : {};
111
108
  if (filter != null) {
@@ -118,10 +115,10 @@ class AppDistributionClient {
118
115
  });
119
116
  }
120
117
  catch (err) {
121
- throw new error_1.FirebaseError(`Client request failed to list testers ${err}`);
118
+ throw new error_1.FirebaseError(`Client request failed to list testers ${(0, error_1.getErrMsg)(err)}`);
122
119
  }
123
120
  for (const t of (_a = apiResponse.body.testers) !== null && _a !== void 0 ? _a : []) {
124
- listTestersResponse.testers.push({
121
+ testers.push({
125
122
  name: t.name,
126
123
  displayName: t.displayName,
127
124
  groups: t.groups,
@@ -130,7 +127,7 @@ class AppDistributionClient {
130
127
  }
131
128
  pageToken = apiResponse.body.nextPageToken;
132
129
  } while (pageToken);
133
- return listTestersResponse;
130
+ return testers;
134
131
  }
135
132
  async addTesters(projectName, emails) {
136
133
  try {
@@ -161,9 +158,7 @@ class AppDistributionClient {
161
158
  }
162
159
  async listGroups(projectName) {
163
160
  var _a;
164
- const listGroupsResponse = {
165
- groups: [],
166
- };
161
+ const groups = [];
167
162
  const client = this.appDistroV1Client;
168
163
  let pageToken;
169
164
  do {
@@ -172,14 +167,14 @@ class AppDistributionClient {
172
167
  const apiResponse = await client.get(`${projectName}/groups`, {
173
168
  queryParams,
174
169
  });
175
- listGroupsResponse.groups.push(...((_a = apiResponse.body.groups) !== null && _a !== void 0 ? _a : []));
170
+ groups.push(...((_a = apiResponse.body.groups) !== null && _a !== void 0 ? _a : []));
176
171
  pageToken = apiResponse.body.nextPageToken;
177
172
  }
178
173
  catch (err) {
179
- throw new error_1.FirebaseError(`Client failed to list groups ${err}`);
174
+ throw new error_1.FirebaseError(`Client failed to list groups ${(0, error_1.getErrMsg)(err)}`);
180
175
  }
181
176
  } while (pageToken);
182
- return listGroupsResponse;
177
+ return groups;
183
178
  }
184
179
  async createGroup(projectName, displayName, alias) {
185
180
  let apiResponse;
@@ -239,7 +234,7 @@ class AppDistributionClient {
239
234
  method: "POST",
240
235
  path: `${releaseName}/tests`,
241
236
  body: {
242
- deviceExecutions: devices.map(types_1.mapDeviceToExecution),
237
+ deviceExecutions: devices.map((device) => ({ device })),
243
238
  loginCredential,
244
239
  testCase: testCaseName,
245
240
  },
@@ -254,5 +249,56 @@ class AppDistributionClient {
254
249
  const response = await this.appDistroV1AlphaClient.get(releaseTestName);
255
250
  return response.body;
256
251
  }
252
+ async listTestCases(appName) {
253
+ var _a;
254
+ const testCases = [];
255
+ const client = this.appDistroV1AlphaClient;
256
+ let pageToken;
257
+ do {
258
+ const queryParams = pageToken ? { pageToken } : {};
259
+ try {
260
+ const apiResponse = await client.get(`${appName}/testCases`, {
261
+ queryParams,
262
+ });
263
+ testCases.push(...((_a = apiResponse.body.testCases) !== null && _a !== void 0 ? _a : []));
264
+ pageToken = apiResponse.body.nextPageToken;
265
+ }
266
+ catch (err) {
267
+ throw new error_1.FirebaseError(`Client failed to list test cases ${(0, error_1.getErrMsg)(err)}`);
268
+ }
269
+ } while (pageToken);
270
+ return testCases;
271
+ }
272
+ async createTestCase(appName, testCase) {
273
+ try {
274
+ const response = await this.appDistroV1AlphaClient.request({
275
+ method: "POST",
276
+ path: `${appName}/testCases`,
277
+ body: testCase,
278
+ });
279
+ return response.body;
280
+ }
281
+ catch (err) {
282
+ throw new error_1.FirebaseError(`Failed to create test case ${(0, error_1.getErrMsg)(err)}`);
283
+ }
284
+ }
285
+ async batchUpsertTestCases(appName, testCases) {
286
+ try {
287
+ const response = await this.appDistroV1AlphaClient.request({
288
+ method: "POST",
289
+ path: `${appName}/testCases:batchUpdate`,
290
+ body: {
291
+ requests: testCases.map((tc) => ({
292
+ testCase: tc,
293
+ allowMissing: true,
294
+ })),
295
+ },
296
+ });
297
+ return response.body.testCases;
298
+ }
299
+ catch (err) {
300
+ throw new error_1.FirebaseError(`Failed to upsert test cases ${(0, error_1.getErrMsg)(err)}`);
301
+ }
302
+ }
257
303
  }
258
304
  exports.AppDistributionClient = AppDistributionClient;
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.mapDeviceToExecution = exports.UploadReleaseResult = exports.IntegrationState = void 0;
3
+ exports.UploadReleaseResult = exports.IntegrationState = void 0;
4
4
  var IntegrationState;
5
5
  (function (IntegrationState) {
6
6
  IntegrationState["AAB_INTEGRATION_STATE_UNSPECIFIED"] = "AAB_INTEGRATION_STATE_UNSPECIFIED";
@@ -18,14 +18,3 @@ var UploadReleaseResult;
18
18
  UploadReleaseResult["RELEASE_UPDATED"] = "RELEASE_UPDATED";
19
19
  UploadReleaseResult["RELEASE_UNMODIFIED"] = "RELEASE_UNMODIFIED";
20
20
  })(UploadReleaseResult = exports.UploadReleaseResult || (exports.UploadReleaseResult = {}));
21
- function mapDeviceToExecution(device) {
22
- return {
23
- device: {
24
- model: device.model,
25
- version: device.version,
26
- orientation: device.orientation,
27
- locale: device.locale,
28
- },
29
- };
30
- }
31
- exports.mapDeviceToExecution = mapDeviceToExecution;
@@ -0,0 +1,69 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.fromYaml = exports.toYaml = void 0;
4
+ const jsYaml = require("js-yaml");
5
+ const error_1 = require("../error");
6
+ const ALLOWED_YAML_STEP_KEYS = new Set(["goal", "hint", "successCriteria"]);
7
+ const ALLOWED_YAML_TEST_CASE_KEYS = new Set([
8
+ "displayName",
9
+ "id",
10
+ "prerequisiteTestCaseId",
11
+ "steps",
12
+ ]);
13
+ function extractIdFromResourceName(name) {
14
+ var _a;
15
+ return (_a = name.split("/").pop()) !== null && _a !== void 0 ? _a : "";
16
+ }
17
+ function toYamlTestCases(testCases) {
18
+ return testCases.map((testCase) => (Object.assign(Object.assign({ displayName: testCase.displayName, id: extractIdFromResourceName(testCase.name) }, (testCase.prerequisiteTestCase && {
19
+ prerequisiteTestCaseId: extractIdFromResourceName(testCase.prerequisiteTestCase),
20
+ })), { steps: testCase.aiInstructions.steps.map((step) => (Object.assign(Object.assign({ goal: step.goal }, (step.hint && { hint: step.hint })), (step.successCriteria && { successCriteria: step.successCriteria })))) })));
21
+ }
22
+ function toYaml(testCases) {
23
+ return jsYaml.safeDump(toYamlTestCases(testCases));
24
+ }
25
+ exports.toYaml = toYaml;
26
+ function castExists(it, thing) {
27
+ if (it == null) {
28
+ throw new error_1.FirebaseError(`"${thing}" is required`);
29
+ }
30
+ return it;
31
+ }
32
+ function checkAllowedKeys(allowedKeys, o) {
33
+ for (const key of Object.keys(o)) {
34
+ if (!allowedKeys.has(key)) {
35
+ throw new error_1.FirebaseError(`unexpected property "${key}"`);
36
+ }
37
+ }
38
+ }
39
+ function fromYamlTestCases(appName, yamlTestCases) {
40
+ return yamlTestCases.map((yamlTestCase) => {
41
+ checkAllowedKeys(ALLOWED_YAML_TEST_CASE_KEYS, yamlTestCase);
42
+ return Object.assign(Object.assign({ displayName: castExists(yamlTestCase.displayName, "displayName"), aiInstructions: {
43
+ steps: castExists(yamlTestCase.steps, "steps").map((yamlStep) => {
44
+ checkAllowedKeys(ALLOWED_YAML_STEP_KEYS, yamlStep);
45
+ return Object.assign(Object.assign({ goal: castExists(yamlStep.goal, "goal") }, (yamlStep.hint && { hint: yamlStep.hint })), (yamlStep.successCriteria && {
46
+ successCriteria: yamlStep.successCriteria,
47
+ }));
48
+ }),
49
+ } }, (yamlTestCase.id && {
50
+ name: `${appName}/testCases/${yamlTestCase.id}`,
51
+ })), (yamlTestCase.prerequisiteTestCaseId && {
52
+ prerequisiteTestCase: `${appName}/testCases/${yamlTestCase.prerequisiteTestCaseId}`,
53
+ }));
54
+ });
55
+ }
56
+ function fromYaml(appName, yaml) {
57
+ let parsedYaml;
58
+ try {
59
+ parsedYaml = jsYaml.safeLoad(yaml);
60
+ }
61
+ catch (err) {
62
+ throw new error_1.FirebaseError(`Failed to parse YAML: ${(0, error_1.getErrMsg)(err)}`);
63
+ }
64
+ if (!Array.isArray(parsedYaml)) {
65
+ throw new error_1.FirebaseError("YAML file must contain a list of test cases.");
66
+ }
67
+ return fromYamlTestCases(appName, parsedYaml);
68
+ }
69
+ exports.fromYaml = fromYaml;
@@ -15,13 +15,12 @@ exports.command = new command_1.Command("appdistribution:groups:list")
15
15
  .alias("appdistribution:group:list")
16
16
  .before(requireAuth_1.requireAuth)
17
17
  .action(async (options) => {
18
- var _a;
19
18
  const projectName = await (0, options_parser_util_1.getProjectName)(options);
20
19
  const appDistroClient = new client_1.AppDistributionClient();
21
- let groupsResponse;
20
+ let groups;
22
21
  const spinner = ora("Preparing the list of your App Distribution Groups").start();
23
22
  try {
24
- groupsResponse = await appDistroClient.listGroups(projectName);
23
+ groups = await appDistroClient.listGroups(projectName);
25
24
  }
26
25
  catch (err) {
27
26
  spinner.fail();
@@ -31,10 +30,9 @@ exports.command = new command_1.Command("appdistribution:groups:list")
31
30
  });
32
31
  }
33
32
  spinner.succeed();
34
- const groups = (_a = groupsResponse.groups) !== null && _a !== void 0 ? _a : [];
35
33
  printGroupsTable(groups);
36
34
  utils.logSuccess(`Groups listed successfully`);
37
- return groupsResponse;
35
+ return { groups };
38
36
  });
39
37
  function printGroupsTable(groups) {
40
38
  const tableHead = ["Group", "Display Name", "Tester Count", "Release Count", "Invite Link Count"];
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.command = void 0;
4
+ const fs = require("fs-extra");
5
+ const command_1 = require("../command");
6
+ const yaml_helper_1 = require("../appdistribution/yaml_helper");
7
+ const requireAuth_1 = require("../requireAuth");
8
+ const client_1 = require("../appdistribution/client");
9
+ const options_parser_util_1 = require("../appdistribution/options-parser-util");
10
+ const error_1 = require("../error");
11
+ const utils = require("../utils");
12
+ exports.command = new command_1.Command("appdistribution:testcases:export <test-cases-yaml-file>")
13
+ .description("export test cases as a YAML file")
14
+ .option("--app <app_id>", "the app id of your Firebase app")
15
+ .before(requireAuth_1.requireAuth)
16
+ .action(async (yamlFile, options) => {
17
+ const appName = (0, options_parser_util_1.getAppName)(options);
18
+ const appDistroClient = new client_1.AppDistributionClient();
19
+ let testCases;
20
+ try {
21
+ testCases = await appDistroClient.listTestCases(appName);
22
+ }
23
+ catch (err) {
24
+ throw new error_1.FirebaseError("Failed to list test cases.", {
25
+ exit: 1,
26
+ original: err,
27
+ });
28
+ }
29
+ fs.writeFileSync(yamlFile, (0, yaml_helper_1.toYaml)(testCases), "utf8");
30
+ utils.logSuccess(`Exported ${testCases.length} test cases to ${yamlFile}`);
31
+ return { testCases };
32
+ });
@@ -0,0 +1,34 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.command = void 0;
4
+ const fs = require("fs-extra");
5
+ const command_1 = require("../command");
6
+ const yaml_helper_1 = require("../appdistribution/yaml_helper");
7
+ const requireAuth_1 = require("../requireAuth");
8
+ const client_1 = require("../appdistribution/client");
9
+ const options_parser_util_1 = require("../appdistribution/options-parser-util");
10
+ const utils = require("../utils");
11
+ const error_1 = require("../error");
12
+ exports.command = new command_1.Command("appdistribution:testcases:import <test-cases-yaml-file>")
13
+ .description("import test cases from YAML file")
14
+ .option("--app <app_id>", "the app id of your Firebase app")
15
+ .before(requireAuth_1.requireAuth)
16
+ .action(async (yamlFile, options) => {
17
+ const appName = (0, options_parser_util_1.getAppName)(options);
18
+ const appDistroClient = new client_1.AppDistributionClient();
19
+ (0, options_parser_util_1.ensureFileExists)(yamlFile);
20
+ const testCases = (0, yaml_helper_1.fromYaml)(appName, fs.readFileSync(yamlFile, "utf8"));
21
+ const testCasesWithoutName = testCases.filter((tc) => !tc.name);
22
+ const creationResults = await Promise.allSettled(testCasesWithoutName.map((tc) => appDistroClient.createTestCase(appName, tc)));
23
+ const failed = creationResults.filter((r) => r.status === "rejected");
24
+ if (failed.length > 0) {
25
+ for (const f of failed) {
26
+ utils.logWarning(f.reason);
27
+ }
28
+ const succeeded = creationResults.length - failed.length;
29
+ throw new error_1.FirebaseError(`Created ${succeeded} test case(s), but failed to create ${failed.length}.`);
30
+ }
31
+ const testCasesWithName = testCases.filter((tc) => !!tc.name);
32
+ await appDistroClient.batchUpsertTestCases(appName, testCasesWithName);
33
+ utils.logSuccess(`Imported ${testCases.length} test cases from ${yamlFile}`);
34
+ });
@@ -14,13 +14,12 @@ exports.command = new command_1.Command("appdistribution:testers:list [group]")
14
14
  .description("list testers in project")
15
15
  .before(requireAuth_1.requireAuth)
16
16
  .action(async (group, options) => {
17
- var _a;
18
17
  const projectName = await (0, options_parser_util_1.getProjectName)(options);
19
18
  const appDistroClient = new client_1.AppDistributionClient();
20
- let testersResponse;
19
+ let testers;
21
20
  const spinner = ora("Preparing the list of your App Distribution testers").start();
22
21
  try {
23
- testersResponse = await appDistroClient.listTesters(projectName, group);
22
+ testers = await appDistroClient.listTesters(projectName, group);
24
23
  }
25
24
  catch (err) {
26
25
  spinner.fail();
@@ -30,10 +29,9 @@ exports.command = new command_1.Command("appdistribution:testers:list [group]")
30
29
  });
31
30
  }
32
31
  spinner.succeed();
33
- const testers = (_a = testersResponse.testers) !== null && _a !== void 0 ? _a : [];
34
32
  printTestersTable(testers);
35
33
  utils.logSuccess(`Testers listed successfully`);
36
- return testersResponse;
34
+ return { testers };
37
35
  });
38
36
  function printTestersTable(testers) {
39
37
  var _a, _b;
@@ -15,6 +15,7 @@ const error_1 = require("../error");
15
15
  const colorette_1 = require("colorette");
16
16
  const interactive_1 = require("../hosting/interactive");
17
17
  const utils_1 = require("../utils");
18
+ const api_1 = require("../hosting/api");
18
19
  exports.VALID_DEPLOY_TARGETS = [
19
20
  "database",
20
21
  "storage",
@@ -107,7 +108,7 @@ exports.command = new command_1.Command("deploy")
107
108
  await (0, requireDatabaseInstance_1.requireDatabaseInstance)(options);
108
109
  }
109
110
  if (options.filteredTargets.includes("hosting")) {
110
- let createSite = false;
111
+ let shouldCreateSite = false;
111
112
  try {
112
113
  await (0, requireHostingSite_1.requireHostingSite)(options);
113
114
  }
@@ -119,17 +120,18 @@ exports.command = new command_1.Command("deploy")
119
120
  throw err;
120
121
  }
121
122
  else if (err === getDefaultHostingSite_1.errNoDefaultSite) {
122
- createSite = true;
123
+ shouldCreateSite = true;
123
124
  }
124
125
  }
125
- if (!createSite) {
126
+ if (!shouldCreateSite) {
126
127
  return;
127
128
  }
128
129
  if (options.nonInteractive) {
129
130
  throw new error_1.FirebaseError(`Unable to deploy to Hosting as there is no Hosting site. Use ${(0, colorette_1.bold)("firebase hosting:sites:create")} to create a site.`);
130
131
  }
131
132
  (0, utils_1.logBullet)("No Hosting site detected.");
132
- await (0, interactive_1.interactiveCreateHostingSite)("", "", options);
133
+ const siteId = await (0, interactive_1.pickHostingSiteName)("", options);
134
+ await (0, api_1.createSite)(options.project, siteId);
133
135
  }
134
136
  })
135
137
  .before(checkValidTargetFilters_1.checkValidTargetFilters)
@@ -8,6 +8,7 @@ const utils_1 = require("../utils");
8
8
  const logger_1 = require("../logger");
9
9
  const projectUtils_1 = require("../projectUtils");
10
10
  const requirePermissions_1 = require("../requirePermissions");
11
+ const api_1 = require("../hosting/api");
11
12
  const error_1 = require("../error");
12
13
  const LOG_TAG = "hosting:sites";
13
14
  exports.command = new command_1.Command("hosting:sites:create [siteId]")
@@ -18,10 +19,10 @@ exports.command = new command_1.Command("hosting:sites:create [siteId]")
18
19
  const projectId = (0, projectUtils_1.needProjectId)(options);
19
20
  const appId = options.app;
20
21
  if (options.nonInteractive && !siteId) {
21
- throw new error_1.FirebaseError(`${(0, colorette_1.bold)(siteId)} is required in a non-interactive environment`);
22
+ throw new error_1.FirebaseError(`${(0, colorette_1.bold)("siteId")} is required in a non-interactive environment`);
22
23
  }
23
- const site = await (0, interactive_1.interactiveCreateHostingSite)(siteId, appId, options);
24
- siteId = (0, utils_1.last)(site.name.split("/"));
24
+ siteId = await (0, interactive_1.pickHostingSiteName)(siteId !== null && siteId !== void 0 ? siteId : "", options);
25
+ const site = await (0, api_1.createSite)(projectId, siteId, appId);
25
26
  logger_1.logger.info();
26
27
  (0, utils_1.logLabeledSuccess)(LOG_TAG, `Site ${(0, colorette_1.bold)(siteId)} has been created in project ${(0, colorette_1.bold)(projectId)}.`);
27
28
  if (appId) {
@@ -20,11 +20,14 @@ function load(client) {
20
20
  client.appdistribution.testers.list = loadCommand("appdistribution-testers-list");
21
21
  client.appdistribution.testers.add = loadCommand("appdistribution-testers-add");
22
22
  client.appdistribution.testers.delete = loadCommand("appdistribution-testers-remove");
23
- client.appdistribution.group = {};
24
- client.appdistribution.group.list = loadCommand("appdistribution-groups-list");
25
- client.appdistribution.group.create = loadCommand("appdistribution-groups-create");
26
- client.appdistribution.group.delete = loadCommand("appdistribution-groups-delete");
27
- client.appdistribution.groups = client.appdistribution.group;
23
+ client.appdistribution.groups = {};
24
+ client.appdistribution.groups.list = loadCommand("appdistribution-groups-list");
25
+ client.appdistribution.groups.create = loadCommand("appdistribution-groups-create");
26
+ client.appdistribution.groups.delete = loadCommand("appdistribution-groups-delete");
27
+ client.appdistribution.group = client.appdistribution.groups;
28
+ client.appdistribution.testCases = {};
29
+ client.appdistribution.testCases.export = loadCommand("appdistribution-testcases-export");
30
+ client.appdistribution.testCases.import = loadCommand("appdistribution-testcases-import");
28
31
  client.apps = {};
29
32
  client.apps.create = loadCommand("apps-create");
30
33
  client.apps.list = loadCommand("apps-list");
@@ -105,13 +105,11 @@ if ((0, experiments_1.isEnabled)("apptesting")) {
105
105
  checked: false,
106
106
  });
107
107
  }
108
- if ((0, experiments_1.isEnabled)("ailogic")) {
109
- choices.push({
110
- value: "ailogic",
111
- name: "AI Logic: Set up Firebase AI Logic with app provisioning",
112
- checked: false,
113
- });
114
- }
108
+ choices.push({
109
+ value: "ailogic",
110
+ name: "AI Logic: Set up Firebase AI Logic with app provisioning",
111
+ checked: false,
112
+ });
115
113
  choices.push({
116
114
  value: "aitools",
117
115
  name: "AI Tools: Configure AI coding assistants to work with your Firebase project",
@@ -168,18 +168,9 @@ async function resolveParams(params, firebaseConfig, userEnvs, nonInteractive, i
168
168
  paramValues[param.name] = userEnvs[param.name];
169
169
  }
170
170
  const [needSecret, needPrompt] = (0, functional_1.partition)(outstanding, (param) => param.type === "secret");
171
- if (nonInteractive && needSecret.length > 0) {
172
- const secretNames = needSecret.map((p) => p.name).join(", ");
173
- const commands = needSecret
174
- .map((p) => `\tfirebase functions:secrets:set ${p.name}${p.format === "json" ? " --format=json --data-file <file.json>" : ""}`)
175
- .join("\n");
176
- throw new error_1.FirebaseError(`In non-interactive mode but have no value for the following secrets: ${secretNames}\n\n` +
177
- "Set these secrets before deploying:\n" +
178
- commands);
179
- }
180
171
  if (!isEmulator) {
181
172
  for (const param of needSecret) {
182
- await handleSecret(param, firebaseConfig.projectId);
173
+ await handleSecret(param, firebaseConfig.projectId, nonInteractive);
183
174
  }
184
175
  }
185
176
  if (nonInteractive && needPrompt.length > 0) {
@@ -230,9 +221,14 @@ function populateDefaultParams(config) {
230
221
  }
231
222
  return defaultParams;
232
223
  }
233
- async function handleSecret(secretParam, projectId) {
224
+ async function handleSecret(secretParam, projectId, nonInteractive) {
234
225
  const metadata = await secretManager.getSecretMetadata(projectId, secretParam.name, "latest");
235
226
  if (!metadata.secret) {
227
+ if (nonInteractive) {
228
+ throw new error_1.FirebaseError(`In non-interactive mode but have no value for the secret: ${secretParam.name}\n\n` +
229
+ "Set this secret before deploying:\n" +
230
+ `\tfirebase functions:secrets:set ${secretParam.name}${secretParam.format === "json" ? " --format=json --data-file <file.json>" : ""}`);
231
+ }
236
232
  const promptMessage = `This secret will be stored in Cloud Secret Manager (https://cloud.google.com/secret-manager/pricing) as ${secretParam.name}. Enter ${secretParam.format === "json" ? "a JSON value" : "a value"} for ${secretParam.label || secretParam.name}:`;
237
233
  const secretValue = await (0, prompt_1.password)({
238
234
  message: promptMessage,
@@ -10,6 +10,7 @@ const errors_1 = require("./errors");
10
10
  const types_1 = require("../types");
11
11
  const emulatorLogger_1 = require("../emulatorLogger");
12
12
  const state_1 = require("./state");
13
+ const error_1 = require("../../error");
13
14
  exports.authOperations = {
14
15
  identitytoolkit: {
15
16
  getProjects,
@@ -2054,11 +2055,16 @@ async function fetchBlockingFunction(state, event, user, options = {}, oauthToke
2054
2055
  let status;
2055
2056
  let text;
2056
2057
  try {
2058
+ const signal = controller.signal;
2059
+ signal.reason = "";
2060
+ signal.throwIfAborted = () => {
2061
+ throw new error_1.FirebaseError("Aborted");
2062
+ };
2057
2063
  const res = await (0, node_fetch_1.default)(url, {
2058
2064
  method: "POST",
2059
2065
  headers: { "Content-Type": "application/json" },
2060
2066
  body: JSON.stringify(reqBody),
2061
- signal: controller.signal,
2067
+ signal,
2062
2068
  });
2063
2069
  ok = res.ok;
2064
2070
  status = res.status;