firebase-tools 14.18.0 → 14.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -5
- package/lib/appUtils.js +230 -0
- package/lib/bin/mcp.js +16 -1
- package/lib/commands/index.js +8 -0
- package/lib/commands/init.js +0 -2
- package/lib/commands/remoteconfig-experiments-delete.js +32 -0
- package/lib/commands/remoteconfig-experiments-get.js +20 -0
- package/lib/commands/remoteconfig-experiments-list.js +38 -0
- package/lib/commands/remoteconfig-rollouts-delete.js +32 -0
- package/lib/commands/remoteconfig-rollouts-get.js +20 -0
- package/lib/commands/remoteconfig-rollouts-list.js +38 -0
- package/lib/config.js +4 -2
- package/lib/deploy/apphosting/deploy.js +8 -5
- package/lib/deploy/functions/deploy.js +5 -4
- package/lib/deploy/functions/params.js +2 -2
- package/lib/emulator/commandUtils.js +3 -3
- package/lib/emulator/controller.js +3 -2
- package/lib/gcp/storage.js +73 -20
- package/lib/init/features/dataconnect/index.js +18 -9
- package/lib/init/features/project.js +66 -75
- package/lib/management/projects.js +16 -4
- package/lib/mcp/index.js +9 -0
- package/lib/mcp/prompts/core/deploy.js +8 -8
- package/lib/mcp/prompts/core/init.js +15 -18
- package/lib/mcp/prompts/crashlytics/connect.js +2 -2
- package/lib/mcp/prompts/index.js +30 -9
- package/lib/mcp/resources/guides/init_ai.js +8 -0
- package/lib/mcp/resources/guides/init_auth.js +4 -0
- package/lib/mcp/resources/guides/init_firestore_rules.js +2 -0
- package/lib/mcp/resources/index.js +16 -1
- package/lib/mcp/tools/apphosting/list_backends.js +1 -1
- package/lib/mcp/tools/core/get_environment.js +34 -17
- package/lib/mcp/tools/core/init.js +2 -1
- package/lib/mcp/tools/core/logout.js +1 -2
- package/lib/mcp/tools/functions/get_logs.js +9 -7
- package/lib/mcp/tools/index.js +3 -2
- package/lib/remoteconfig/deleteExperiment.js +32 -0
- package/lib/remoteconfig/deleteRollout.js +33 -0
- package/lib/remoteconfig/getExperiment.js +52 -0
- package/lib/remoteconfig/getRollout.js +43 -0
- package/lib/remoteconfig/interfaces.js +3 -1
- package/lib/remoteconfig/listExperiments.js +72 -0
- package/lib/remoteconfig/listRollouts.js +72 -0
- package/lib/remoteconfig/options.js +4 -0
- package/lib/track.js +6 -4
- package/package.json +1 -1
package/lib/mcp/prompts/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.availablePrompts = void 0;
|
|
3
|
+
exports.markdownDocsOfPrompts = exports.availablePrompts = void 0;
|
|
4
4
|
const core_1 = require("./core");
|
|
5
5
|
const dataconnect_1 = require("./dataconnect");
|
|
6
6
|
const crashlytics_1 = require("./crashlytics");
|
|
@@ -33,16 +33,37 @@ function namespacePrompts(promptsToNamespace, feature) {
|
|
|
33
33
|
return newPrompt;
|
|
34
34
|
});
|
|
35
35
|
}
|
|
36
|
-
function availablePrompts(
|
|
37
|
-
const allPrompts =
|
|
38
|
-
if (!
|
|
39
|
-
|
|
36
|
+
function availablePrompts(activeFeatures) {
|
|
37
|
+
const allPrompts = [];
|
|
38
|
+
if (!(activeFeatures === null || activeFeatures === void 0 ? void 0 : activeFeatures.length)) {
|
|
39
|
+
activeFeatures = Object.keys(prompts);
|
|
40
40
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
41
|
+
if (!activeFeatures.includes("core")) {
|
|
42
|
+
activeFeatures = ["core", ...activeFeatures];
|
|
43
|
+
}
|
|
44
|
+
for (const feature of activeFeatures) {
|
|
45
|
+
allPrompts.push(...namespacePrompts(prompts[feature], feature));
|
|
45
46
|
}
|
|
46
47
|
return allPrompts;
|
|
47
48
|
}
|
|
48
49
|
exports.availablePrompts = availablePrompts;
|
|
50
|
+
function markdownDocsOfPrompts() {
|
|
51
|
+
var _a, _b;
|
|
52
|
+
const allPrompts = availablePrompts();
|
|
53
|
+
let doc = `
|
|
54
|
+
| Prompt Name | Feature Group | Description |
|
|
55
|
+
| ----------- | ------------- | ----------- |`;
|
|
56
|
+
for (const prompt of allPrompts) {
|
|
57
|
+
const feature = ((_a = prompt.mcp._meta) === null || _a === void 0 ? void 0 : _a.feature) || "";
|
|
58
|
+
let description = prompt.mcp.description || "";
|
|
59
|
+
if ((_b = prompt.mcp.arguments) === null || _b === void 0 ? void 0 : _b.length) {
|
|
60
|
+
const argsList = prompt.mcp.arguments.map((arg) => ` <br><${arg.name}>${arg.required ? "" : " (optional)"}: ${arg.description || ""}`);
|
|
61
|
+
description += ` <br><br>Arguments:${argsList.join("")}`;
|
|
62
|
+
}
|
|
63
|
+
description = description.replaceAll("\n", "<br>");
|
|
64
|
+
doc += `
|
|
65
|
+
| ${prompt.mcp.name} | ${feature} | ${description} |`;
|
|
66
|
+
}
|
|
67
|
+
return doc;
|
|
68
|
+
}
|
|
69
|
+
exports.markdownDocsOfPrompts = markdownDocsOfPrompts;
|
|
@@ -60,6 +60,10 @@ Take the following actions depending on the language and platform or framework t
|
|
|
60
60
|
|
|
61
61
|
- Use the firebase_init tool to set up ailogic
|
|
62
62
|
|
|
63
|
+
- For Android, the Google Services Gradle plugin is required to prevent the app from crashing. You must add it in two files:
|
|
64
|
+
- 1. In your project-level \`/build.gradle.kts\` file, add the plugin to the plugins block: id("com.google.gms.google-services") version "4.4.2" apply false
|
|
65
|
+
- 2. In your **app-level** \`/app/build.gradle.kts\` file, apply the plugin: id("com.google.gms.google-services")
|
|
66
|
+
|
|
63
67
|
### 3\. Implement AI Features
|
|
64
68
|
|
|
65
69
|
#### Gather Building Blocks for Code Generation
|
|
@@ -77,6 +81,10 @@ Take the following actions depending on the language and platform or framework t
|
|
|
77
81
|
- Kotlin Only
|
|
78
82
|
- implementation(platform("com.google.firebase:firebase-bom:34.3.0")) or a higher bom version if it is available
|
|
79
83
|
- implementation("com.google.firebase:firebase-ai")
|
|
84
|
+
- CRITICAL: When initializing the Firebase AI model in Kotlin, you must explicitly specify the Google AI backend by calling the googleAI() function. The correct syntax is GenerativeBackend.googleAI().
|
|
85
|
+
- Correct Example: val model = Firebase.ai(backend = GenerativeBackend.googleAI()).generativeModel(...)
|
|
86
|
+
- Incorrect: Do not use the invalid constant GenerativeBackend.GOOGLE_AI.
|
|
87
|
+
- The Kotlin SDK public API makes extensive use of suspend functions and coroutines. Make sure the code you generate is based on that paradigm and avoid using callbacks unless absolutely necessary in Kotlin
|
|
80
88
|
- For Flutter apps, always include the following imports. do not forget or modify them
|
|
81
89
|
- import 'package:firebase_core/firebase_core.dart';
|
|
82
90
|
- import 'package:firebase_ai/firebase_ai.dart';
|
|
@@ -27,6 +27,10 @@ exports.init_auth = (0, resource_1.resource)({
|
|
|
27
27
|
**Testing & Deployment:**
|
|
28
28
|
- Test the complete sign-up and sign-in flow to verify authentication functionality
|
|
29
29
|
- Deploy the application to production once authentication is verified and working properly
|
|
30
|
+
|
|
31
|
+
**Next Steps:**
|
|
32
|
+
- **Security Rules**: If an app uses *Cloud Firestore database*, *Cloud Storage for Firebase*, or *Firebase Realtime Database*, then please update user-based Security Rules that are structured according to the app's specific requirements.
|
|
33
|
+
- **App Deployment**: Deploy the app to production after Security Rules are verified to be working properly.
|
|
30
34
|
`.trim(),
|
|
31
35
|
},
|
|
32
36
|
],
|
|
@@ -30,6 +30,8 @@ ${config.readProjectFile("firestore.rules", { fallback: "<FILE DOES NOT EXIST>"
|
|
|
30
30
|
|
|
31
31
|
For database entities that neatly fall into the "personal" and "public categories, you can use the personalData and publicData rules. Use the following firestore.rules file, and add a comment above 'personalData' and 'publicData' to note what entities apply to each rule.
|
|
32
32
|
|
|
33
|
+
**Next Steps:**
|
|
34
|
+
- **App Deployment**: Deploy the app to production after Security Rules are verified to be working properly.
|
|
33
35
|
\`\`\`
|
|
34
36
|
rules_version = '2';
|
|
35
37
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.resolveResource = exports.resourceTemplates = exports.resources = void 0;
|
|
3
|
+
exports.markdownDocsOfResources = exports.resolveResource = exports.resourceTemplates = exports.resources = void 0;
|
|
4
4
|
const docs_1 = require("./docs");
|
|
5
5
|
const init_ai_1 = require("./guides/init_ai");
|
|
6
6
|
const init_auth_1 = require("./guides/init_auth");
|
|
@@ -42,3 +42,18 @@ async function resolveResource(uri, ctx, track = true) {
|
|
|
42
42
|
return null;
|
|
43
43
|
}
|
|
44
44
|
exports.resolveResource = resolveResource;
|
|
45
|
+
function markdownDocsOfResources() {
|
|
46
|
+
const allResources = [...exports.resources, ...exports.resourceTemplates];
|
|
47
|
+
const headings = `
|
|
48
|
+
| Resource Name | Description |
|
|
49
|
+
| ------------- | ----------- |`;
|
|
50
|
+
const resourceRows = allResources.map((res) => {
|
|
51
|
+
let desc = res.mcp.title ? `${res.mcp.title}: ` : "";
|
|
52
|
+
desc += res.mcp.description || "";
|
|
53
|
+
desc = desc.replaceAll("\n", "<br>");
|
|
54
|
+
return `
|
|
55
|
+
| ${res.mcp.name} | ${desc} |`;
|
|
56
|
+
});
|
|
57
|
+
return headings + resourceRows.join("");
|
|
58
|
+
}
|
|
59
|
+
exports.markdownDocsOfResources = markdownDocsOfResources;
|
|
@@ -13,7 +13,7 @@ exports.list_backends = (0, tool_1.tool)({
|
|
|
13
13
|
"is the resource name of the Cloud Run service serving the App Hosting backend. The last segment of that name is the service ID. " +
|
|
14
14
|
"`domains` is the list of domains that are associated with the backend. They either have type `CUSTOM` or `DEFAULT`. " +
|
|
15
15
|
" Every backend should have a `DEFAULT` domain. " +
|
|
16
|
-
" The actual domain that a user would use to
|
|
16
|
+
" The actual domain that a user would use to connect to the backend is the last parameter of the domain resource name. " +
|
|
17
17
|
" If a custom domain is correctly set up, it will have statuses ending in `ACTIVE`.",
|
|
18
18
|
inputSchema: zod_1.z.object({
|
|
19
19
|
location: zod_1.z
|
|
@@ -8,6 +8,7 @@ const projectUtils_1 = require("../../../projectUtils");
|
|
|
8
8
|
const js_yaml_1 = require("js-yaml");
|
|
9
9
|
const auth_1 = require("../../../auth");
|
|
10
10
|
const configstore_1 = require("../../../configstore");
|
|
11
|
+
const appUtils_1 = require("../../../appUtils");
|
|
11
12
|
exports.get_environment = (0, tool_1.tool)({
|
|
12
13
|
name: "get_environment",
|
|
13
14
|
description: "Use this to retrieve the current Firebase **environment** configuration for the Firebase CLI and Firebase MCP server, including current authenticated user, project directory, active Firebase Project, and more.",
|
|
@@ -23,32 +24,48 @@ exports.get_environment = (0, tool_1.tool)({
|
|
|
23
24
|
}, async (_, { projectId, host, accountEmail, rc, config }) => {
|
|
24
25
|
const aliases = projectId ? (0, projectUtils_1.getAliases)({ rc }, projectId) : [];
|
|
25
26
|
const geminiTosAccepted = !!configstore_1.configstore.get("gemini");
|
|
27
|
+
const projectFileExists = config.projectFileExists("firebase.json");
|
|
28
|
+
const detectedApps = await (0, appUtils_1.detectApps)(process.cwd());
|
|
29
|
+
const allAccounts = (0, auth_1.getAllAccounts)().map((account) => account.user.email);
|
|
30
|
+
const hasOtherAccounts = allAccounts.filter((email) => email !== accountEmail).length > 0;
|
|
31
|
+
const projectConfigPathString = projectFileExists
|
|
32
|
+
? config.path("firebase.json")
|
|
33
|
+
: "<NO CONFIG PRESENT>";
|
|
34
|
+
const detectedAppsMap = detectedApps
|
|
35
|
+
.filter((app) => !!app.appId)
|
|
36
|
+
.reduce((map, app) => {
|
|
37
|
+
if (app.appId) {
|
|
38
|
+
map.set(app.appId, app.bundleId ? app.bundleId : "<UNKNOWN BUNDLE ID>");
|
|
39
|
+
}
|
|
40
|
+
return map;
|
|
41
|
+
}, new Map());
|
|
42
|
+
const activeProjectString = projectId
|
|
43
|
+
? `${projectId}${aliases.length ? ` (alias: ${aliases.join(",")})` : ""}`
|
|
44
|
+
: "<NONE>";
|
|
45
|
+
const acceptedGeminiTosString = geminiTosAccepted ? "Accepted" : "<NOT ACCEPTED>";
|
|
26
46
|
return (0, util_1.toContent)(`# Environment Information
|
|
27
47
|
|
|
28
48
|
Project Directory: ${host.cachedProjectDir}
|
|
29
|
-
Project Config Path: ${
|
|
30
|
-
Active Project ID: ${
|
|
49
|
+
Project Config Path: ${projectConfigPathString}
|
|
50
|
+
Active Project ID: ${activeProjectString}
|
|
51
|
+
Gemini in Firebase Terms of Service: ${acceptedGeminiTosString}
|
|
31
52
|
Authenticated User: ${accountEmail || "<NONE>"}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
${(0, js_yaml_1.dump)(rc.projects).trim()}
|
|
37
|
-
|
|
38
|
-
# Available Accounts:
|
|
39
|
-
|
|
40
|
-
${(0, js_yaml_1.dump)((0, auth_1.getAllAccounts)().map((account) => account.user.email)).trim()}
|
|
41
|
-
${config.projectFileExists("firebase.json")
|
|
42
|
-
? `
|
|
43
|
-
# firebase.json contents:
|
|
53
|
+
Detected App IDs: ${detectedAppsMap.size > 0 ? `\n\n${(0, js_yaml_1.dump)(Object.fromEntries(detectedAppsMap)).trim()}\n` : "<NONE>"}
|
|
54
|
+
Available Project Aliases (format: '[alias]: [projectId]'): ${Object.entries(rc.projects).length > 0 ? `\n\n${(0, js_yaml_1.dump)(rc.projects).trim()}\n` : "<NONE>"}${hasOtherAccounts ? `\nAvailable Accounts: \n\n${(0, js_yaml_1.dump)(allAccounts).trim()}` : ""}
|
|
55
|
+
${projectFileExists
|
|
56
|
+
? `\nfirebase.json contents:
|
|
44
57
|
|
|
45
58
|
\`\`\`json
|
|
46
59
|
${config.readProjectFile("firebase.json")}
|
|
47
60
|
\`\`\``
|
|
48
|
-
: `\
|
|
61
|
+
: `\nNo firebase.json file was found.
|
|
62
|
+
|
|
63
|
+
If this project does not use Firebase services that require a firebase.json file, no action is necessary.
|
|
49
64
|
|
|
50
|
-
|
|
65
|
+
If this project uses Firebase services that require a firebase.json file, the user will most likely want to:
|
|
51
66
|
|
|
52
67
|
a) Change the project directory using the 'firebase_update_environment' tool to select a directory with a 'firebase.json' file in it, or
|
|
53
|
-
b) Initialize a new Firebase project directory using the 'firebase_init' tool
|
|
68
|
+
b) Initialize a new Firebase project directory using the 'firebase_init' tool.
|
|
69
|
+
|
|
70
|
+
Confirm with the user before taking action.`}`);
|
|
54
71
|
});
|
|
@@ -11,6 +11,7 @@ const errors_1 = require("../../errors");
|
|
|
11
11
|
const error_1 = require("../../../error");
|
|
12
12
|
const utils_1 = require("../../../init/features/ailogic/utils");
|
|
13
13
|
const projects_1 = require("../../../management/projects");
|
|
14
|
+
const dataconnect_1 = require("../../../init/features/dataconnect");
|
|
14
15
|
exports.init = (0, tool_1.tool)({
|
|
15
16
|
name: "init",
|
|
16
17
|
description: "Use this to initialize selected Firebase services in the workspace (Cloud Firestore database, Firebase Data Connect, Firebase Realtime Database, Firebase AI Logic). All services are optional; specify only the products you want to set up. " +
|
|
@@ -70,7 +71,7 @@ exports.init = (0, tool_1.tool)({
|
|
|
70
71
|
location_id: zod_1.z
|
|
71
72
|
.string()
|
|
72
73
|
.optional()
|
|
73
|
-
.default(
|
|
74
|
+
.default(dataconnect_1.FDC_DEFAULT_REGION)
|
|
74
75
|
.describe("The GCP region ID to set up the Firebase Data Connect service."),
|
|
75
76
|
cloudsql_instance_id: zod_1.z
|
|
76
77
|
.string()
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.logout = void 0;
|
|
4
4
|
const zod_1 = require("zod");
|
|
5
|
-
const clc = require("colorette");
|
|
6
5
|
const tool_1 = require("../../tool");
|
|
7
6
|
const util_1 = require("../../util");
|
|
8
7
|
const auth_1 = require("../../../auth");
|
|
@@ -44,7 +43,7 @@ exports.logout = (0, tool_1.tool)({
|
|
|
44
43
|
if (token) {
|
|
45
44
|
try {
|
|
46
45
|
await (0, auth_1.logout)(token);
|
|
47
|
-
logoutMessages.push(`Logged out from ${
|
|
46
|
+
logoutMessages.push(`Logged out from ${account.user.email}`);
|
|
48
47
|
}
|
|
49
48
|
catch (e) {
|
|
50
49
|
if (e instanceof Error) {
|
|
@@ -6,6 +6,7 @@ const tool_1 = require("../../tool");
|
|
|
6
6
|
const util_1 = require("../../util");
|
|
7
7
|
const functionslog_1 = require("../../../functions/functionslog");
|
|
8
8
|
const cloudlogging_1 = require("../../../gcp/cloudlogging");
|
|
9
|
+
const error_1 = require("../../../error");
|
|
9
10
|
const SEVERITY_LEVELS = [
|
|
10
11
|
"DEFAULT",
|
|
11
12
|
"DEBUG",
|
|
@@ -39,12 +40,13 @@ function validateTimestamp(label, value) {
|
|
|
39
40
|
}
|
|
40
41
|
exports.get_logs = (0, tool_1.tool)({
|
|
41
42
|
name: "get_logs",
|
|
42
|
-
description: "
|
|
43
|
+
description: "Use this to retrieve a page of Cloud Functions log entries using Google Cloud Logging advanced filters.",
|
|
43
44
|
inputSchema: zod_1.z.object({
|
|
44
45
|
function_names: zod_1.z
|
|
45
|
-
.
|
|
46
|
+
.array(zod_1.z.string())
|
|
47
|
+
.min(1)
|
|
46
48
|
.optional()
|
|
47
|
-
.describe("Optional list of deployed Cloud Function
|
|
49
|
+
.describe("Optional list of deployed Cloud Function IDs to filter logs (e.g. ['fnA','fnB'])."),
|
|
48
50
|
page_size: zod_1.z
|
|
49
51
|
.number()
|
|
50
52
|
.int()
|
|
@@ -84,8 +86,8 @@ exports.get_logs = (0, tool_1.tool)({
|
|
|
84
86
|
requiresProject: true,
|
|
85
87
|
},
|
|
86
88
|
}, async ({ function_names, page_size, order, page_token, min_severity, start_time, end_time, filter }, { projectId }) => {
|
|
87
|
-
const resolvedOrder = order;
|
|
88
|
-
const resolvedPageSize = page_size;
|
|
89
|
+
const resolvedOrder = (order === null || order === void 0 ? void 0 : order.toLowerCase()) === "asc" ? "asc" : "desc";
|
|
90
|
+
const resolvedPageSize = page_size !== null && page_size !== void 0 ? page_size : 50;
|
|
89
91
|
const normalizedSelectors = normalizeFunctionSelectors(function_names);
|
|
90
92
|
const filterParts = [(0, functionslog_1.getApiFilter)(normalizedSelectors)];
|
|
91
93
|
if (min_severity) {
|
|
@@ -143,7 +145,7 @@ exports.get_logs = (0, tool_1.tool)({
|
|
|
143
145
|
return (0, util_1.toContent)(response);
|
|
144
146
|
}
|
|
145
147
|
catch (err) {
|
|
146
|
-
const
|
|
147
|
-
return (0, util_1.mcpError)(
|
|
148
|
+
const errMsg = (0, error_1.getErrMsg)((err === null || err === void 0 ? void 0 : err.original) || err, "Failed to retrieve Cloud Logging entries.");
|
|
149
|
+
return (0, util_1.mcpError)(errMsg);
|
|
148
150
|
}
|
|
149
151
|
});
|
package/lib/mcp/tools/index.js
CHANGED
|
@@ -18,7 +18,7 @@ function availableTools(activeFeatures) {
|
|
|
18
18
|
activeFeatures = Object.keys(tools);
|
|
19
19
|
}
|
|
20
20
|
if (!activeFeatures.includes("core")) {
|
|
21
|
-
activeFeatures
|
|
21
|
+
activeFeatures = ["core", ...activeFeatures];
|
|
22
22
|
}
|
|
23
23
|
for (const key of activeFeatures) {
|
|
24
24
|
toolDefs.push(...tools[key]);
|
|
@@ -53,8 +53,9 @@ function markdownDocsOfTools() {
|
|
|
53
53
|
if (feature === "firebase") {
|
|
54
54
|
feature = "core";
|
|
55
55
|
}
|
|
56
|
+
const description = (((_c = tool.mcp) === null || _c === void 0 ? void 0 : _c.description) || "").replaceAll("\n", "<br>");
|
|
56
57
|
doc += `
|
|
57
|
-
| ${tool.mcp.name} | ${feature} | ${
|
|
58
|
+
| ${tool.mcp.name} | ${feature} | ${description} |`;
|
|
58
59
|
}
|
|
59
60
|
return doc;
|
|
60
61
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.deleteExperiment = void 0;
|
|
4
|
+
const clc = require("colorette");
|
|
5
|
+
const api_1 = require("../api");
|
|
6
|
+
const apiv2_1 = require("../apiv2");
|
|
7
|
+
const error_1 = require("../error");
|
|
8
|
+
const utils_1 = require("../utils");
|
|
9
|
+
const TIMEOUT = 30000;
|
|
10
|
+
const apiClient = new apiv2_1.Client({
|
|
11
|
+
urlPrefix: (0, api_1.remoteConfigApiOrigin)(),
|
|
12
|
+
apiVersion: "v1",
|
|
13
|
+
});
|
|
14
|
+
async function deleteExperiment(projectId, namespace, experimentId) {
|
|
15
|
+
try {
|
|
16
|
+
await apiClient.request({
|
|
17
|
+
method: "DELETE",
|
|
18
|
+
path: `projects/${projectId}/namespaces/${namespace}/experiments/${experimentId}`,
|
|
19
|
+
timeout: TIMEOUT,
|
|
20
|
+
});
|
|
21
|
+
return clc.bold(`Successfully deleted experiment ${clc.yellow(experimentId)}`);
|
|
22
|
+
}
|
|
23
|
+
catch (err) {
|
|
24
|
+
const error = (0, error_1.getError)(err);
|
|
25
|
+
if (error.message.includes("is running and cannot be deleted")) {
|
|
26
|
+
const rcConsoleUrl = (0, utils_1.consoleUrl)(projectId, `/config/experiment/results/${experimentId}`);
|
|
27
|
+
throw new error_1.FirebaseError(`Experiment ${experimentId} is currently running and cannot be deleted. If you want to delete this experiment, stop it at ${rcConsoleUrl}`, { original: error });
|
|
28
|
+
}
|
|
29
|
+
throw new error_1.FirebaseError(`Failed to delete Remote Config experiment with ID ${experimentId} for project ${projectId}. Error: ${(0, error_1.getErrMsg)(err)}`, { original: error });
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
exports.deleteExperiment = deleteExperiment;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.deleteRollout = void 0;
|
|
4
|
+
const api_1 = require("../api");
|
|
5
|
+
const apiv2_1 = require("../apiv2");
|
|
6
|
+
const error_1 = require("../error");
|
|
7
|
+
const utils_1 = require("../utils");
|
|
8
|
+
const clc = require("colorette");
|
|
9
|
+
const TIMEOUT = 30000;
|
|
10
|
+
const apiClient = new apiv2_1.Client({
|
|
11
|
+
urlPrefix: (0, api_1.remoteConfigApiOrigin)(),
|
|
12
|
+
apiVersion: "v1",
|
|
13
|
+
});
|
|
14
|
+
async function deleteRollout(projectId, namespace, rolloutId) {
|
|
15
|
+
try {
|
|
16
|
+
await apiClient.request({
|
|
17
|
+
method: "DELETE",
|
|
18
|
+
path: `/projects/${projectId}/namespaces/${namespace}/rollouts/${rolloutId}`,
|
|
19
|
+
timeout: TIMEOUT,
|
|
20
|
+
});
|
|
21
|
+
return clc.bold(`Successfully deleted rollout ${clc.yellow(rolloutId)}`);
|
|
22
|
+
}
|
|
23
|
+
catch (err) {
|
|
24
|
+
const originalError = (0, error_1.getError)(err);
|
|
25
|
+
const errorMessage = (0, error_1.getErrMsg)(err);
|
|
26
|
+
if (errorMessage.includes("is running and cannot be deleted")) {
|
|
27
|
+
const rcConsoleUrl = (0, utils_1.consoleUrl)(projectId, `/config/env/firebase/rollout/${rolloutId}`);
|
|
28
|
+
throw new error_1.FirebaseError(`Rollout '${rolloutId}' is currently running and cannot be deleted. If you want to delete this rollout, stop it at ${rcConsoleUrl}`, { original: originalError });
|
|
29
|
+
}
|
|
30
|
+
throw new error_1.FirebaseError(`Failed to delete Remote Config rollout with ID ${rolloutId} for project ${projectId}. Error: ${errorMessage}`, { original: originalError });
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
exports.deleteRollout = deleteRollout;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getExperiment = exports.parseExperiment = void 0;
|
|
4
|
+
const Table = require("cli-table3");
|
|
5
|
+
const util = require("util");
|
|
6
|
+
const api_1 = require("../api");
|
|
7
|
+
const apiv2_1 = require("../apiv2");
|
|
8
|
+
const logger_1 = require("../logger");
|
|
9
|
+
const error_1 = require("../error");
|
|
10
|
+
const TIMEOUT = 30000;
|
|
11
|
+
const TABLE_HEAD = ["Entry Name", "Value"];
|
|
12
|
+
const apiClient = new apiv2_1.Client({
|
|
13
|
+
urlPrefix: (0, api_1.remoteConfigApiOrigin)(),
|
|
14
|
+
apiVersion: "v1",
|
|
15
|
+
});
|
|
16
|
+
const parseExperiment = (experiment) => {
|
|
17
|
+
const table = new Table({ head: TABLE_HEAD, style: { head: ["green"] } });
|
|
18
|
+
table.push(["Name", experiment.name]);
|
|
19
|
+
table.push(["Display Name", experiment.definition.displayName]);
|
|
20
|
+
table.push(["Service", experiment.definition.service]);
|
|
21
|
+
table.push([
|
|
22
|
+
"Objectives",
|
|
23
|
+
util.inspect(experiment.definition.objectives, { showHidden: false, depth: null }),
|
|
24
|
+
]);
|
|
25
|
+
table.push([
|
|
26
|
+
"Variants",
|
|
27
|
+
util.inspect(experiment.definition.variants, { showHidden: false, depth: null }),
|
|
28
|
+
]);
|
|
29
|
+
table.push(["State", experiment.state]);
|
|
30
|
+
table.push(["Start Time", experiment.startTime]);
|
|
31
|
+
table.push(["End Time", experiment.endTime]);
|
|
32
|
+
table.push(["Last Update Time", experiment.lastUpdateTime]);
|
|
33
|
+
table.push(["etag", experiment.etag]);
|
|
34
|
+
return table.toString();
|
|
35
|
+
};
|
|
36
|
+
exports.parseExperiment = parseExperiment;
|
|
37
|
+
async function getExperiment(projectId, namespace, experimentId) {
|
|
38
|
+
try {
|
|
39
|
+
const res = await apiClient.request({
|
|
40
|
+
method: "GET",
|
|
41
|
+
path: `projects/${projectId}/namespaces/${namespace}/experiments/${experimentId}`,
|
|
42
|
+
timeout: TIMEOUT,
|
|
43
|
+
});
|
|
44
|
+
return res.body;
|
|
45
|
+
}
|
|
46
|
+
catch (err) {
|
|
47
|
+
const error = (0, error_1.getError)(err);
|
|
48
|
+
logger_1.logger.debug(error.message);
|
|
49
|
+
throw new error_1.FirebaseError(`Failed to get Remote Config experiment with ID ${experimentId} for project ${projectId}. Error: ${error.message}`, { original: error });
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
exports.getExperiment = getExperiment;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getRollout = exports.parseRolloutIntoTable = void 0;
|
|
4
|
+
const api_1 = require("../api");
|
|
5
|
+
const apiv2_1 = require("../apiv2");
|
|
6
|
+
const logger_1 = require("../logger");
|
|
7
|
+
const error_1 = require("../error");
|
|
8
|
+
const Table = require("cli-table3");
|
|
9
|
+
const util = require("util");
|
|
10
|
+
const TIMEOUT = 30000;
|
|
11
|
+
const TABLE_HEAD = ["Entry Name", "Value"];
|
|
12
|
+
const apiClient = new apiv2_1.Client({
|
|
13
|
+
urlPrefix: (0, api_1.remoteConfigApiOrigin)(),
|
|
14
|
+
apiVersion: "v1",
|
|
15
|
+
});
|
|
16
|
+
const parseRolloutIntoTable = (rollout) => {
|
|
17
|
+
const table = new Table({ head: TABLE_HEAD, style: { head: ["green"] } });
|
|
18
|
+
table.push(["Name", rollout.name], ["Display Name", rollout.definition.displayName], ["Description", rollout.definition.description], ["State", rollout.state], ["Create Time", rollout.createTime], ["Start Time", rollout.startTime], ["End Time", rollout.endTime], ["Last Update Time", rollout.lastUpdateTime], [
|
|
19
|
+
"Control Variant",
|
|
20
|
+
util.inspect(rollout.definition.controlVariant, { showHidden: false, depth: null }),
|
|
21
|
+
], [
|
|
22
|
+
"Enabled Variant",
|
|
23
|
+
util.inspect(rollout.definition.enabledVariant, { showHidden: false, depth: null }),
|
|
24
|
+
], ["ETag", rollout.etag]);
|
|
25
|
+
return table.toString();
|
|
26
|
+
};
|
|
27
|
+
exports.parseRolloutIntoTable = parseRolloutIntoTable;
|
|
28
|
+
async function getRollout(projectId, namespace, rolloutId) {
|
|
29
|
+
try {
|
|
30
|
+
const res = await apiClient.request({
|
|
31
|
+
method: "GET",
|
|
32
|
+
path: `/projects/${projectId}/namespaces/${namespace}/rollouts/${rolloutId}`,
|
|
33
|
+
timeout: TIMEOUT,
|
|
34
|
+
});
|
|
35
|
+
return res.body;
|
|
36
|
+
}
|
|
37
|
+
catch (err) {
|
|
38
|
+
const error = (0, error_1.getError)(err);
|
|
39
|
+
logger_1.logger.debug(error.message);
|
|
40
|
+
throw new error_1.FirebaseError(`Failed to get Remote Config Rollout with ID ${rolloutId} for project ${projectId}. Error: ${error.message}`, { original: error });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
exports.getRollout = getRollout;
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.TagColor = void 0;
|
|
3
|
+
exports.TagColor = exports.DEFAULT_PAGE_SIZE = exports.NAMESPACE_FIREBASE = void 0;
|
|
4
|
+
exports.NAMESPACE_FIREBASE = "firebase";
|
|
5
|
+
exports.DEFAULT_PAGE_SIZE = "10";
|
|
4
6
|
var TagColor;
|
|
5
7
|
(function (TagColor) {
|
|
6
8
|
TagColor["BLUE"] = "Blue";
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.listExperiments = exports.parseExperimentList = void 0;
|
|
4
|
+
const Table = require("cli-table3");
|
|
5
|
+
const api_1 = require("../api");
|
|
6
|
+
const apiv2_1 = require("../apiv2");
|
|
7
|
+
const logger_1 = require("../logger");
|
|
8
|
+
const error_1 = require("../error");
|
|
9
|
+
const TIMEOUT = 30000;
|
|
10
|
+
const TABLE_HEAD = [
|
|
11
|
+
"Experiment ID",
|
|
12
|
+
"Display Name",
|
|
13
|
+
"Service",
|
|
14
|
+
"Description",
|
|
15
|
+
"State",
|
|
16
|
+
"Start Time",
|
|
17
|
+
"End Time",
|
|
18
|
+
"Last Update Time",
|
|
19
|
+
"etag",
|
|
20
|
+
];
|
|
21
|
+
const apiClient = new apiv2_1.Client({
|
|
22
|
+
urlPrefix: (0, api_1.remoteConfigApiOrigin)(),
|
|
23
|
+
apiVersion: "v1",
|
|
24
|
+
});
|
|
25
|
+
const parseExperimentList = (experiments) => {
|
|
26
|
+
if (experiments.length === 0)
|
|
27
|
+
return "\x1b[33mNo experiments found\x1b[0m";
|
|
28
|
+
const table = new Table({ head: TABLE_HEAD, style: { head: ["green"] } });
|
|
29
|
+
for (const experiment of experiments) {
|
|
30
|
+
table.push([
|
|
31
|
+
experiment.name.split("/").pop(),
|
|
32
|
+
experiment.definition.displayName,
|
|
33
|
+
experiment.definition.service,
|
|
34
|
+
experiment.definition.description,
|
|
35
|
+
experiment.state,
|
|
36
|
+
experiment.startTime,
|
|
37
|
+
experiment.endTime,
|
|
38
|
+
experiment.lastUpdateTime,
|
|
39
|
+
experiment.etag,
|
|
40
|
+
]);
|
|
41
|
+
}
|
|
42
|
+
return table.toString();
|
|
43
|
+
};
|
|
44
|
+
exports.parseExperimentList = parseExperimentList;
|
|
45
|
+
async function listExperiments(projectId, namespace, listExperimentOptions) {
|
|
46
|
+
try {
|
|
47
|
+
const params = new URLSearchParams();
|
|
48
|
+
if (listExperimentOptions.pageSize) {
|
|
49
|
+
params.set("page_size", listExperimentOptions.pageSize);
|
|
50
|
+
}
|
|
51
|
+
if (listExperimentOptions.filter) {
|
|
52
|
+
params.set("filter", listExperimentOptions.filter);
|
|
53
|
+
}
|
|
54
|
+
if (listExperimentOptions.pageToken) {
|
|
55
|
+
params.set("page_token", listExperimentOptions.pageToken);
|
|
56
|
+
}
|
|
57
|
+
logger_1.logger.debug(`Query parameters for listExperiments: ${params.toString()}`);
|
|
58
|
+
const res = await apiClient.request({
|
|
59
|
+
method: "GET",
|
|
60
|
+
path: `projects/${projectId}/namespaces/${namespace}/experiments`,
|
|
61
|
+
queryParams: params,
|
|
62
|
+
timeout: TIMEOUT,
|
|
63
|
+
});
|
|
64
|
+
return res.body;
|
|
65
|
+
}
|
|
66
|
+
catch (err) {
|
|
67
|
+
const error = (0, error_1.getError)(err);
|
|
68
|
+
logger_1.logger.debug(error.message);
|
|
69
|
+
throw new error_1.FirebaseError(`Failed to get Remote Config experiments for project ${projectId}. Error: ${error.message}`, { original: error });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
exports.listExperiments = listExperiments;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.listRollouts = exports.parseRolloutList = void 0;
|
|
4
|
+
const api_1 = require("../api");
|
|
5
|
+
const apiv2_1 = require("../apiv2");
|
|
6
|
+
const logger_1 = require("../logger");
|
|
7
|
+
const error_1 = require("../error");
|
|
8
|
+
const Table = require("cli-table3");
|
|
9
|
+
const TIMEOUT = 30000;
|
|
10
|
+
const apiClient = new apiv2_1.Client({
|
|
11
|
+
urlPrefix: (0, api_1.remoteConfigApiOrigin)(),
|
|
12
|
+
apiVersion: "v1",
|
|
13
|
+
});
|
|
14
|
+
const TABLE_HEAD = [
|
|
15
|
+
"Rollout ID",
|
|
16
|
+
"Display Name",
|
|
17
|
+
"Service",
|
|
18
|
+
"Description",
|
|
19
|
+
"State",
|
|
20
|
+
"Start Time",
|
|
21
|
+
"End Time",
|
|
22
|
+
"Last Update Time",
|
|
23
|
+
"ETag",
|
|
24
|
+
];
|
|
25
|
+
const parseRolloutList = (rollouts) => {
|
|
26
|
+
if (rollouts.length === 0) {
|
|
27
|
+
return "\x1b[33mNo rollouts found.\x1b[0m";
|
|
28
|
+
}
|
|
29
|
+
const table = new Table({ head: TABLE_HEAD, style: { head: ["green"] } });
|
|
30
|
+
for (const rollout of rollouts) {
|
|
31
|
+
table.push([
|
|
32
|
+
rollout.name.split("/").pop() || rollout.name,
|
|
33
|
+
rollout.definition.displayName,
|
|
34
|
+
rollout.definition.service,
|
|
35
|
+
rollout.definition.description,
|
|
36
|
+
rollout.state,
|
|
37
|
+
rollout.startTime,
|
|
38
|
+
rollout.endTime,
|
|
39
|
+
rollout.lastUpdateTime,
|
|
40
|
+
rollout.etag,
|
|
41
|
+
]);
|
|
42
|
+
}
|
|
43
|
+
return table.toString();
|
|
44
|
+
};
|
|
45
|
+
exports.parseRolloutList = parseRolloutList;
|
|
46
|
+
async function listRollouts(projectId, namespace, listRolloutOptions) {
|
|
47
|
+
try {
|
|
48
|
+
const params = new URLSearchParams();
|
|
49
|
+
if (listRolloutOptions.pageSize) {
|
|
50
|
+
params.set("page_size", listRolloutOptions.pageSize);
|
|
51
|
+
}
|
|
52
|
+
if (listRolloutOptions.filter) {
|
|
53
|
+
params.set("filter", listRolloutOptions.filter);
|
|
54
|
+
}
|
|
55
|
+
if (listRolloutOptions.pageToken) {
|
|
56
|
+
params.set("page_token", listRolloutOptions.pageToken);
|
|
57
|
+
}
|
|
58
|
+
const res = await apiClient.request({
|
|
59
|
+
method: "GET",
|
|
60
|
+
path: `/projects/${projectId}/namespaces/${namespace}/rollouts`,
|
|
61
|
+
queryParams: params,
|
|
62
|
+
timeout: TIMEOUT,
|
|
63
|
+
});
|
|
64
|
+
return res.body;
|
|
65
|
+
}
|
|
66
|
+
catch (err) {
|
|
67
|
+
const error = (0, error_1.getError)(err);
|
|
68
|
+
logger_1.logger.debug(error.message);
|
|
69
|
+
throw new error_1.FirebaseError(`Failed to get Remote Config rollouts for project ${projectId}. Error: ${error.message}`, { original: error });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
exports.listRollouts = listRollouts;
|