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.
- package/README.md +13 -5
- package/lib/apiv2.js +12 -2
- package/lib/appdistribution/client.js +62 -16
- package/lib/appdistribution/types.js +1 -12
- package/lib/appdistribution/yaml_helper.js +69 -0
- package/lib/commands/appdistribution-groups-list.js +3 -5
- package/lib/commands/appdistribution-testcases-export.js +32 -0
- package/lib/commands/appdistribution-testcases-import.js +34 -0
- package/lib/commands/appdistribution-testers-list.js +3 -5
- package/lib/commands/deploy.js +6 -4
- package/lib/commands/hosting-sites-create.js +4 -3
- package/lib/commands/index.js +8 -5
- package/lib/commands/init.js +5 -7
- package/lib/deploy/functions/params.js +7 -11
- package/lib/emulator/auth/operations.js +7 -1
- package/lib/emulator/downloadableEmulatorInfo.json +25 -25
- package/lib/emulator/functionsEmulatorRuntime.js +8 -0
- package/lib/emulator/taskQueue.js +5 -0
- package/lib/experiments.js +0 -6
- package/lib/firestore/api.js +22 -4
- package/lib/frameworks/angular/index.js +1 -1
- package/lib/frameworks/flutter/index.js +1 -1
- package/lib/frameworks/next/index.js +1 -1
- package/lib/frameworks/nuxt/index.js +1 -1
- package/lib/frameworks/vite/index.js +5 -2
- package/lib/hosting/interactive.js +14 -19
- package/lib/init/features/hosting/github.js +6 -6
- package/lib/init/features/hosting/index.js +89 -86
- package/lib/init/features/index.js +3 -2
- package/lib/init/index.js +5 -1
- package/lib/management/provisioning/errorHandler.js +54 -0
- package/lib/management/provisioning/provision.js +2 -5
- package/lib/mcp/resources/guides/init_backend.js +3 -26
- package/lib/mcp/resources/guides/init_hosting.js +15 -10
- package/lib/mcp/tools/core/init.js +27 -0
- package/lib/mcp/tools/functions/index.js +2 -1
- package/lib/mcp/tools/functions/list_functions.js +48 -0
- package/lib/utils.js +4 -1
- package/package.json +2 -2
- 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] [](https://cursor.com/en/install-mcp?name=firebase&config=
|
|
1
|
+
# Firebase CLI and MCP Server [![Actions Status][gh-actions-badge]][gh-actions] [![Node Version][node-badge]][npm] [![NPM version][npm-badge]][npm] [](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
|
|
91
|
-
|
|
|
92
|
-
| **appdistribution:distribute**
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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(
|
|
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.
|
|
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
|
|
20
|
+
let groups;
|
|
22
21
|
const spinner = ora("Preparing the list of your App Distribution Groups").start();
|
|
23
22
|
try {
|
|
24
|
-
|
|
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
|
|
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
|
|
19
|
+
let testers;
|
|
21
20
|
const spinner = ora("Preparing the list of your App Distribution testers").start();
|
|
22
21
|
try {
|
|
23
|
-
|
|
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
|
|
34
|
+
return { testers };
|
|
37
35
|
});
|
|
38
36
|
function printTestersTable(testers) {
|
|
39
37
|
var _a, _b;
|
package/lib/commands/deploy.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
123
|
+
shouldCreateSite = true;
|
|
123
124
|
}
|
|
124
125
|
}
|
|
125
|
-
if (!
|
|
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.
|
|
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
|
-
|
|
24
|
-
|
|
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) {
|
package/lib/commands/index.js
CHANGED
|
@@ -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.
|
|
24
|
-
client.appdistribution.
|
|
25
|
-
client.appdistribution.
|
|
26
|
-
client.appdistribution.
|
|
27
|
-
client.appdistribution.
|
|
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");
|
package/lib/commands/init.js
CHANGED
|
@@ -105,13 +105,11 @@ if ((0, experiments_1.isEnabled)("apptesting")) {
|
|
|
105
105
|
checked: false,
|
|
106
106
|
});
|
|
107
107
|
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
|
2067
|
+
signal,
|
|
2062
2068
|
});
|
|
2063
2069
|
ok = res.ok;
|
|
2064
2070
|
status = res.status;
|