firebase-tools 15.10.1 → 15.12.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/lib/agentSkills.js +70 -0
- package/lib/api.js +3 -1
- package/lib/appdistribution/client.js +17 -0
- package/lib/apphosting/backend.js +22 -3
- package/lib/apptesting/parseTestFiles.js +11 -8
- package/lib/bin/mcp.js +5 -1
- package/lib/commands/apphosting-backends-create.js +19 -2
- package/lib/commands/apphosting-backends-list.js +21 -5
- package/lib/commands/apptesting.js +16 -7
- package/lib/commands/functions-delete.js +1 -0
- package/lib/commands/functions-export.js +40 -0
- package/lib/commands/index.js +3 -0
- package/lib/commands/init.js +1 -0
- package/lib/commands/studio-export.js +2 -2
- package/lib/deploy/apphosting/deploy.js +11 -6
- package/lib/deploy/apphosting/prepare.js +21 -1
- package/lib/deploy/apphosting/release.js +2 -5
- package/lib/deploy/apphosting/util.js +45 -2
- package/lib/deploy/firestore/prepare.js +17 -0
- package/lib/deploy/functions/prepare.js +4 -1
- package/lib/deploy/functions/release/fabricator.js +4 -3
- package/lib/deploy/functions/release/index.js +5 -0
- package/lib/deploy/functions/runtimes/dart/index.js +282 -0
- package/lib/deploy/functions/runtimes/index.js +1 -1
- package/lib/deploy/functions/runtimes/supported/index.js +4 -0
- package/lib/deploy/functions/services/ailogic.js +68 -0
- package/lib/deploy/functions/services/index.js +4 -0
- package/lib/emulator/downloadableEmulatorInfo.json +30 -30
- package/lib/emulator/functionsEmulator.js +103 -24
- package/lib/emulator/functionsRuntimeWorker.js +21 -18
- package/lib/emulator/storage/rules/manager.js +10 -3
- package/lib/emulator/storage/rules/runtime.js +9 -7
- package/lib/experiments.js +22 -0
- package/lib/firebase_studio/migrate.js +83 -70
- package/lib/functions/iac/export.js +36 -0
- package/lib/functions/iac/terraform.js +146 -0
- package/lib/gcp/ailogic.js +108 -0
- package/lib/gcp/cloudfunctionsv2.js +24 -0
- package/lib/init/features/agentSkills.js +26 -0
- package/lib/init/features/dataconnect/sdk.js +26 -12
- package/lib/init/features/functions/dart.js +31 -0
- package/lib/init/features/functions/index.js +14 -0
- package/lib/init/features/index.js +4 -1
- package/lib/init/index.js +6 -0
- package/lib/tsconfig.publish.tsbuildinfo +1 -1
- package/lib/utils.js +8 -0
- package/package.json +5 -3
- package/schema/firebase-config.json +7 -0
- package/standalone/package.json +1 -1
- package/templates/firebase-studio-export/readme_template.md +2 -0
- package/templates/init/functions/dart/_gitignore +11 -0
- package/templates/init/functions/dart/pubspec.yaml +14 -0
- package/templates/init/functions/dart/server.dart +15 -0
- package/lib/deploy/functions/runtimes/dart.js +0 -42
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.promptForAgentSkills = promptForAgentSkills;
|
|
4
|
+
exports.installAgentSkills = installAgentSkills;
|
|
5
|
+
const child_process_1 = require("child_process");
|
|
6
|
+
const utils = require("./utils");
|
|
7
|
+
const prompt = require("./prompt");
|
|
8
|
+
const error_1 = require("./error");
|
|
9
|
+
async function promptForAgentSkills() {
|
|
10
|
+
return prompt.confirm({
|
|
11
|
+
message: "Would you like to install agent skills for Firebase?",
|
|
12
|
+
default: true,
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
async function installAgentSkills(options) {
|
|
16
|
+
if (!utils.commandExistsSync("npx")) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
const args = [
|
|
20
|
+
"-y",
|
|
21
|
+
"skills",
|
|
22
|
+
"add",
|
|
23
|
+
"firebase/agent-skills",
|
|
24
|
+
"--skill",
|
|
25
|
+
"*",
|
|
26
|
+
"-y",
|
|
27
|
+
];
|
|
28
|
+
if (options.agentName) {
|
|
29
|
+
args.push("-a", options.agentName);
|
|
30
|
+
}
|
|
31
|
+
if (options.global) {
|
|
32
|
+
args.push("-g");
|
|
33
|
+
}
|
|
34
|
+
if (options.background) {
|
|
35
|
+
utils.logBullet("Installing Agent skills in the background...");
|
|
36
|
+
try {
|
|
37
|
+
const child = (0, child_process_1.spawn)("npx", args, {
|
|
38
|
+
cwd: options.cwd,
|
|
39
|
+
stdio: "ignore",
|
|
40
|
+
detached: true,
|
|
41
|
+
shell: process.platform === "win32",
|
|
42
|
+
});
|
|
43
|
+
child.unref();
|
|
44
|
+
utils.logSuccess("Agent skills installation started");
|
|
45
|
+
}
|
|
46
|
+
catch (err) {
|
|
47
|
+
utils.logWarning(`Could not start Agent skills installation: ${(0, error_1.getErrMsg)(err)}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
utils.logBullet("Adding Agent skills...");
|
|
52
|
+
try {
|
|
53
|
+
const result = (0, child_process_1.spawnSync)("npx", args, {
|
|
54
|
+
cwd: options.cwd,
|
|
55
|
+
stdio: "ignore",
|
|
56
|
+
shell: process.platform === "win32",
|
|
57
|
+
});
|
|
58
|
+
if (result.error) {
|
|
59
|
+
throw result.error;
|
|
60
|
+
}
|
|
61
|
+
if (result.status !== 0) {
|
|
62
|
+
throw new Error(`npx skills add exited with code ${result.status}`);
|
|
63
|
+
}
|
|
64
|
+
utils.logSuccess("Added Agent skills");
|
|
65
|
+
}
|
|
66
|
+
catch (err) {
|
|
67
|
+
utils.logWarning(`Could not add Agent skills: ${(0, error_1.getErrMsg)(err)}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
package/lib/api.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.storageOrigin = exports.runtimeconfigOrigin = exports.rulesOrigin = exports.resourceManagerOrigin = exports.crashlyticsApiOrigin = exports.messagingApiOrigin = exports.remoteConfigApiOrigin = exports.rtdbMetadataOrigin = exports.rtdbManagementOrigin = exports.realtimeOrigin = exports.extensionsTOSOrigin = exports.extensionsPublisherOrigin = exports.extensionsOrigin = exports.iamOrigin = exports.identityOrigin = exports.hostingOrigin = exports.googleOrigin = exports.pubsubOrigin = exports.cloudTasksOrigin = exports.cloudschedulerOrigin = exports.cloudbuildOrigin = exports.functionsDefaultRegion = exports.runOrigin = exports.functionsV2Origin = exports.functionsOrigin = exports.firestoreOrigin = exports.firestoreOriginOrEmulator = exports.firedataOrigin = exports.firebaseExtensionsRegistryOrigin = exports.firebaseApiOrigin = exports.eventarcOrigin = exports.dynamicLinksKey = exports.dynamicLinksOrigin = exports.consoleOrigin = exports.authManagementOrigin = exports.authOrigin = exports.apphostingGitHubAppInstallationURL = exports.apphostingP4SADomain = exports.apphostingOrigin = exports.appDistributionOrigin = exports.artifactRegistryDomain = exports.developerConnectP4SADomain = exports.developerConnectOrigin = exports.containerRegistryDomain = exports.cloudMonitoringOrigin = exports.cloudloggingOrigin = exports.cloudbillingOrigin = exports.clientSecret = exports.clientId = exports.authProxyOrigin = void 0;
|
|
4
|
-
exports.developerKnowledgeOrigin = exports.cloudTestingOrigin = exports.appTestingOrigin = exports.cloudAiCompanionOrigin = exports.vertexAIOrigin = exports.cloudSQLAdminOrigin = exports.dataConnectLocalConnString = exports.dataconnectP4SADomain = exports.dataconnectOrigin = exports.githubClientSecret = exports.githubClientId = exports.computeOrigin = exports.secretManagerOrigin = exports.githubApiOrigin = exports.githubOrigin = exports.studioApiOrigin = exports.serviceUsageOrigin = exports.cloudRunApiOrigin = exports.hostingApiOrigin = exports.firebaseStorageOrigin = void 0;
|
|
4
|
+
exports.developerKnowledgeOrigin = exports.cloudTestingOrigin = exports.appTestingOrigin = exports.cloudAiCompanionOrigin = exports.aiLogicProxyOrigin = exports.vertexAIOrigin = exports.cloudSQLAdminOrigin = exports.dataConnectLocalConnString = exports.dataconnectP4SADomain = exports.dataconnectOrigin = exports.githubClientSecret = exports.githubClientId = exports.computeOrigin = exports.secretManagerOrigin = exports.githubApiOrigin = exports.githubOrigin = exports.studioApiOrigin = exports.serviceUsageOrigin = exports.cloudRunApiOrigin = exports.hostingApiOrigin = exports.firebaseStorageOrigin = void 0;
|
|
5
5
|
exports.getScopes = getScopes;
|
|
6
6
|
exports.setScopes = setScopes;
|
|
7
7
|
const constants_1 = require("./emulator/constants");
|
|
@@ -146,6 +146,8 @@ const cloudSQLAdminOrigin = () => utils.envOverride("CLOUD_SQL_URL", "https://sq
|
|
|
146
146
|
exports.cloudSQLAdminOrigin = cloudSQLAdminOrigin;
|
|
147
147
|
const vertexAIOrigin = () => utils.envOverride("VERTEX_AI_URL", "https://aiplatform.googleapis.com");
|
|
148
148
|
exports.vertexAIOrigin = vertexAIOrigin;
|
|
149
|
+
const aiLogicProxyOrigin = () => utils.envOverride("AI_LOGIC_PROXY_URL", "https://firebasevertexai.googleapis.com");
|
|
150
|
+
exports.aiLogicProxyOrigin = aiLogicProxyOrigin;
|
|
149
151
|
const cloudAiCompanionOrigin = () => utils.envOverride("CLOUD_AI_COMPANION_URL", "https://cloudaicompanion.googleapis.com");
|
|
150
152
|
exports.cloudAiCompanionOrigin = cloudAiCompanionOrigin;
|
|
151
153
|
const appTestingOrigin = () => utils.envOverride("FIREBASE_APP_TESTING_URL", "https://firebaseapptesting.googleapis.com");
|
|
@@ -298,5 +298,22 @@ class AppDistributionClient {
|
|
|
298
298
|
throw new error_1.FirebaseError(`Failed to upsert test cases ${(0, error_1.getErrMsg)(err)}`);
|
|
299
299
|
}
|
|
300
300
|
}
|
|
301
|
+
async getLatestRelease(appName) {
|
|
302
|
+
try {
|
|
303
|
+
const response = await this.appDistroV1Client.get(`/${appName}/releases`, {
|
|
304
|
+
queryParams: {
|
|
305
|
+
pageSize: "1",
|
|
306
|
+
orderBy: "createTime desc",
|
|
307
|
+
},
|
|
308
|
+
});
|
|
309
|
+
if (!response.body.releases?.length) {
|
|
310
|
+
return undefined;
|
|
311
|
+
}
|
|
312
|
+
return response.body.releases[0];
|
|
313
|
+
}
|
|
314
|
+
catch (err) {
|
|
315
|
+
throw new error_1.FirebaseError(`Failed to get latest release for app ${appName}: ${(0, error_1.getErrMsg)(err)}`);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
301
318
|
}
|
|
302
319
|
exports.AppDistributionClient = AppDistributionClient;
|
|
@@ -34,6 +34,8 @@ const ora = require("ora");
|
|
|
34
34
|
const node_fetch_1 = require("node-fetch");
|
|
35
35
|
const rollout_1 = require("./rollout");
|
|
36
36
|
const fuzzy = require("fuzzy");
|
|
37
|
+
const experiments_1 = require("../experiments");
|
|
38
|
+
const DEFAULT_RUNTIME = "nodejs";
|
|
37
39
|
const DEFAULT_COMPUTE_SERVICE_ACCOUNT_NAME = "firebase-app-hosting-compute";
|
|
38
40
|
const apphostingPollerOptions = {
|
|
39
41
|
apiOrigin: (0, api_1.apphostingOrigin)(),
|
|
@@ -64,7 +66,7 @@ async function awaitTlsReady(url) {
|
|
|
64
66
|
}
|
|
65
67
|
} while (!ready);
|
|
66
68
|
}
|
|
67
|
-
async function doSetup(projectId, nonInteractive, webAppName, backendId, serviceAccount, primaryRegion, rootDir) {
|
|
69
|
+
async function doSetup(projectId, nonInteractive, webAppName, backendId, serviceAccount, primaryRegion, rootDir, runtime, automaticBaseImageUpdatesDisabled) {
|
|
68
70
|
await ensureRequiredApisEnabled(projectId);
|
|
69
71
|
await ensureAppHostingComputeServiceAccount(projectId, serviceAccount ? serviceAccount : null);
|
|
70
72
|
let location = primaryRegion;
|
|
@@ -97,12 +99,27 @@ async function doSetup(projectId, nonInteractive, webAppName, backendId, service
|
|
|
97
99
|
if (!location || !backendId) {
|
|
98
100
|
throw new error_1.FirebaseError("Internal error: location or backendId is not defined.");
|
|
99
101
|
}
|
|
102
|
+
if (runtime === undefined && (0, experiments_1.isEnabled)("abiu")) {
|
|
103
|
+
if (nonInteractive) {
|
|
104
|
+
runtime = DEFAULT_RUNTIME;
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
runtime = await (0, prompt_1.select)({
|
|
108
|
+
message: "Which runtime do you want to use?",
|
|
109
|
+
choices: [
|
|
110
|
+
{ name: "Node.js (default)", value: DEFAULT_RUNTIME },
|
|
111
|
+
{ name: "Node.js 22", value: "nodejs22" },
|
|
112
|
+
],
|
|
113
|
+
default: DEFAULT_RUNTIME,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
}
|
|
100
117
|
const webApp = await app_1.webApps.getOrCreateWebApp(projectId, webAppName ? webAppName : null, backendId);
|
|
101
118
|
if (!webApp) {
|
|
102
119
|
(0, utils_1.logWarning)(`Firebase web app not set`);
|
|
103
120
|
}
|
|
104
121
|
const createBackendSpinner = ora("Creating your new backend...").start();
|
|
105
|
-
const backend = await createBackend(projectId, location, backendId, serviceAccount ? serviceAccount : null, gitRepositoryLink, webApp?.id, rootDir);
|
|
122
|
+
const backend = await createBackend(projectId, location, backendId, serviceAccount ? serviceAccount : null, gitRepositoryLink, webApp?.id, rootDir, runtime, automaticBaseImageUpdatesDisabled);
|
|
106
123
|
createBackendSpinner.succeed(`Successfully created backend!\n\t${backend.name}\n`);
|
|
107
124
|
if (nonInteractive) {
|
|
108
125
|
return;
|
|
@@ -227,7 +244,7 @@ async function promptNewBackendId(projectId, location) {
|
|
|
227
244
|
function defaultComputeServiceAccountEmail(projectId) {
|
|
228
245
|
return `${DEFAULT_COMPUTE_SERVICE_ACCOUNT_NAME}@${projectId}.iam.gserviceaccount.com`;
|
|
229
246
|
}
|
|
230
|
-
async function createBackend(projectId, location, backendId, serviceAccount, repository, webAppId, rootDir = "/") {
|
|
247
|
+
async function createBackend(projectId, location, backendId, serviceAccount, repository, webAppId, rootDir = "/", runtime, automaticBaseImageUpdatesDisabled) {
|
|
231
248
|
const defaultServiceAccount = defaultComputeServiceAccountEmail(projectId);
|
|
232
249
|
const backendReqBody = {
|
|
233
250
|
servingLocality: "GLOBAL_ACCESS",
|
|
@@ -240,6 +257,8 @@ async function createBackend(projectId, location, backendId, serviceAccount, rep
|
|
|
240
257
|
labels: deploymentTool.labels(),
|
|
241
258
|
serviceAccount: serviceAccount || defaultServiceAccount,
|
|
242
259
|
appId: webAppId,
|
|
260
|
+
runtime: { value: runtime ?? "" },
|
|
261
|
+
automaticBaseImageUpdatesDisabled,
|
|
243
262
|
};
|
|
244
263
|
async function createBackendAndPoll() {
|
|
245
264
|
const op = await apphosting.createBackend(projectId, location, backendReqBody, backendId);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.parseTestFiles = parseTestFiles;
|
|
4
|
+
exports.pluralizeTests = pluralizeTests;
|
|
4
5
|
const fsutils_1 = require("../fsutils");
|
|
5
6
|
const path_1 = require("path");
|
|
6
7
|
const logger_1 = require("../logger");
|
|
@@ -77,26 +78,25 @@ function createFilter(pattern, context) {
|
|
|
77
78
|
async function parseTestFilesRecursive(params) {
|
|
78
79
|
const testDir = params.testDir;
|
|
79
80
|
const targetUri = params.targetUri;
|
|
80
|
-
const
|
|
81
|
+
const filenames = (0, fsutils_1.listFiles)(testDir);
|
|
81
82
|
const results = [];
|
|
82
|
-
for (const
|
|
83
|
-
const path = (0, path_1.join)(testDir,
|
|
83
|
+
for (const filename of filenames) {
|
|
84
|
+
const path = (0, path_1.join)(testDir, filename);
|
|
84
85
|
if ((0, fsutils_1.dirExistsSync)(path)) {
|
|
85
86
|
results.push(...(await parseTestFilesRecursive({ testDir: path, targetUri })));
|
|
86
87
|
}
|
|
87
|
-
else if ((0, fsutils_1.fileExistsSync)(path)) {
|
|
88
|
+
else if ((0, fsutils_1.fileExistsSync)(path) && (path.endsWith(".yaml") || path.endsWith(".yml"))) {
|
|
88
89
|
try {
|
|
89
|
-
|
|
90
|
-
|
|
90
|
+
logger_1.logger.debug(`Reading ${path}.`);
|
|
91
|
+
const file = await (0, utils_1.readFileFromDirectory)(testDir, filename);
|
|
91
92
|
const parsedFile = (0, utils_1.wrappedSafeLoad)(file.source);
|
|
92
|
-
logger_1.logger.debug(`Parsed the file.`);
|
|
93
93
|
const tests = parsedFile.tests;
|
|
94
|
-
logger_1.logger.debug(`There are ${tests.length} tests.`);
|
|
95
94
|
const defaultConfig = parsedFile.defaultConfig;
|
|
96
95
|
if (!tests || !tests.length) {
|
|
97
96
|
logger_1.logger.debug(`No tests found in ${path}. Ignoring.`);
|
|
98
97
|
continue;
|
|
99
98
|
}
|
|
99
|
+
logger_1.logger.debug(`File contains ${pluralizeTests(tests.length)}.`);
|
|
100
100
|
const invocations = [];
|
|
101
101
|
for (const rawTestDef of tests) {
|
|
102
102
|
const invocation = toTestCaseInvocation(rawTestDef, targetUri, defaultConfig);
|
|
@@ -114,6 +114,9 @@ async function parseTestFilesRecursive(params) {
|
|
|
114
114
|
}
|
|
115
115
|
return results;
|
|
116
116
|
}
|
|
117
|
+
function pluralizeTests(numTests) {
|
|
118
|
+
return `${numTests} test${numTests === 1 ? "" : "s"}`;
|
|
119
|
+
}
|
|
117
120
|
function toTestCaseInvocation(testDef, targetUri, defaultConfig) {
|
|
118
121
|
const steps = testDef.steps ?? [];
|
|
119
122
|
const route = testDef.testConfig?.route ?? defaultConfig?.route ?? "";
|
package/lib/bin/mcp.js
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
4
|
exports.mcp = mcp;
|
|
5
5
|
const path_1 = require("path");
|
|
6
|
+
const promises_1 = require("fs/promises");
|
|
7
|
+
const os_1 = require("os");
|
|
6
8
|
const util_1 = require("util");
|
|
7
9
|
const logger_1 = require("../logger");
|
|
8
10
|
const index_1 = require("../mcp/index");
|
|
@@ -82,7 +84,9 @@ async function mcp() {
|
|
|
82
84
|
if (earlyExit)
|
|
83
85
|
return;
|
|
84
86
|
(0, env_1.setFirebaseMcp)(true);
|
|
85
|
-
(0,
|
|
87
|
+
const mcpLogDir = (0, path_1.join)((0, os_1.homedir)(), ".cache", "firebase");
|
|
88
|
+
await (0, promises_1.mkdir)(mcpLogDir, { recursive: true });
|
|
89
|
+
(0, logger_1.useFileLogger)((0, path_1.join)(mcpLogDir, "firebase-debug.log"));
|
|
86
90
|
const activeFeatures = (values.only || "")
|
|
87
91
|
.split(",")
|
|
88
92
|
.map((f) => f.trim())
|
|
@@ -7,6 +7,7 @@ const projectUtils_1 = require("../projectUtils");
|
|
|
7
7
|
const requireAuth_1 = require("../requireAuth");
|
|
8
8
|
const backend_1 = require("../apphosting/backend");
|
|
9
9
|
const apphosting_1 = require("../gcp/apphosting");
|
|
10
|
+
const experiments = require("../experiments");
|
|
10
11
|
const firedata_1 = require("../gcp/firedata");
|
|
11
12
|
const requireTosAcceptance_1 = require("../requireTosAcceptance");
|
|
12
13
|
exports.command = new command_1.Command("apphosting:backends:create")
|
|
@@ -15,7 +16,15 @@ exports.command = new command_1.Command("apphosting:backends:create")
|
|
|
15
16
|
.option("--backend <backend>", "specify the name of the new backend. Required with --non-interactive.")
|
|
16
17
|
.option("-s, --service-account <serviceAccount>", "specify the service account used to run the server", "")
|
|
17
18
|
.option("--primary-region <primaryRegion>", "specify the primary region for the backend. Required with --non-interactive.")
|
|
18
|
-
.option("--root-dir <rootDir>", "specify the root directory for the backend.")
|
|
19
|
+
.option("--root-dir <rootDir>", "specify the root directory for the backend.");
|
|
20
|
+
const abiuEnabled = experiments.isEnabled("abiu");
|
|
21
|
+
if (abiuEnabled) {
|
|
22
|
+
exports.command
|
|
23
|
+
.option("--runtime [runtime]", "specify the runtime for the backend (e.g., nodejs, nodejs22)")
|
|
24
|
+
.option("--automatic-base-image-updates", "enable automatic base image updates")
|
|
25
|
+
.option("--no-automatic-base-image-updates", "disable automatic base image updates");
|
|
26
|
+
}
|
|
27
|
+
exports.command
|
|
19
28
|
.before(requireAuth_1.requireAuth)
|
|
20
29
|
.before(apphosting_1.ensureApiEnabled)
|
|
21
30
|
.before((0, requireTosAcceptance_1.requireTosAcceptance)(firedata_1.APPHOSTING_TOS_ID))
|
|
@@ -24,5 +33,13 @@ exports.command = new command_1.Command("apphosting:backends:create")
|
|
|
24
33
|
if (options.nonInteractive && (options.backend == null || options.primaryRegion == null)) {
|
|
25
34
|
throw new error_1.FirebaseError(`--non-interactive option requires --backend and --primary-region`);
|
|
26
35
|
}
|
|
27
|
-
|
|
36
|
+
const abiuAllowed = experiments.isEnabled("abiu");
|
|
37
|
+
if (!abiuAllowed && (options.runtime || options.automaticBaseImageUpdates !== undefined)) {
|
|
38
|
+
throw new error_1.FirebaseError("The --runtime and --automatic-base-image-updates flags are only available when the 'abiu' experiment is enabled. To enable it, run 'firebase experiments:enable abiu'.");
|
|
39
|
+
}
|
|
40
|
+
const runtime = abiuAllowed && typeof options.runtime === "string" ? options.runtime : undefined;
|
|
41
|
+
const automaticBaseImageUpdatesDisabled = abiuAllowed
|
|
42
|
+
? options.automaticBaseImageUpdates === false
|
|
43
|
+
: undefined;
|
|
44
|
+
return (0, backend_1.doSetup)(projectId, options.nonInteractive, options.app, options.backend, options.serviceAccount, options.primaryRegion, options.rootDir, runtime, automaticBaseImageUpdatesDisabled);
|
|
28
45
|
});
|
|
@@ -9,8 +9,8 @@ const logger_1 = require("../logger");
|
|
|
9
9
|
const projectUtils_1 = require("../projectUtils");
|
|
10
10
|
const requireAuth_1 = require("../requireAuth");
|
|
11
11
|
const apphosting = require("../gcp/apphosting");
|
|
12
|
+
const experiments_1 = require("../experiments");
|
|
12
13
|
const Table = require("cli-table3");
|
|
13
|
-
const TABLE_HEAD = ["Backend", "Repository", "URL", "Primary Region", "Updated Date"];
|
|
14
14
|
exports.command = new command_1.Command("apphosting:backends:list")
|
|
15
15
|
.description("list Firebase App Hosting backends")
|
|
16
16
|
.before(requireAuth_1.requireAuth)
|
|
@@ -29,19 +29,35 @@ exports.command = new command_1.Command("apphosting:backends:list")
|
|
|
29
29
|
return backends;
|
|
30
30
|
});
|
|
31
31
|
function printBackendsTable(backends) {
|
|
32
|
+
const abiuEnabled = (0, experiments_1.isEnabled)("abiu");
|
|
33
|
+
const head = ["Backend", "Repository", "URL", "Primary Region"];
|
|
34
|
+
if (abiuEnabled) {
|
|
35
|
+
head.push("ABIU");
|
|
36
|
+
head.push("Runtime");
|
|
37
|
+
}
|
|
38
|
+
head.push("Updated Date");
|
|
32
39
|
const table = new Table({
|
|
33
|
-
head:
|
|
40
|
+
head: head,
|
|
34
41
|
style: { head: ["green"] },
|
|
35
42
|
});
|
|
36
43
|
for (const backend of backends) {
|
|
37
44
|
const { location, id } = apphosting.parseBackendName(backend.name);
|
|
38
|
-
|
|
45
|
+
const row = [
|
|
39
46
|
id,
|
|
40
47
|
backend.codebase?.repository?.split("/").pop() ?? "",
|
|
41
48
|
backend.uri.startsWith("https:") ? backend.uri : "https://" + backend.uri,
|
|
42
49
|
location,
|
|
43
|
-
|
|
44
|
-
|
|
50
|
+
];
|
|
51
|
+
if (abiuEnabled) {
|
|
52
|
+
let abiuStatus = "N/A";
|
|
53
|
+
if (backend.automaticBaseImageUpdatesDisabled !== undefined) {
|
|
54
|
+
abiuStatus = backend.automaticBaseImageUpdatesDisabled ? "Disabled" : "Enabled";
|
|
55
|
+
}
|
|
56
|
+
row.push(abiuStatus);
|
|
57
|
+
row.push(backend.runtime?.value ?? "N/A");
|
|
58
|
+
}
|
|
59
|
+
row.push((0, utils_1.datetimeString)(new Date(backend.updateTime)));
|
|
60
|
+
table.push(row);
|
|
45
61
|
}
|
|
46
62
|
logger_1.logger.info(table.toString());
|
|
47
63
|
}
|
|
@@ -20,7 +20,7 @@ const defaultDevices = [
|
|
|
20
20
|
orientation: "portrait",
|
|
21
21
|
},
|
|
22
22
|
];
|
|
23
|
-
exports.command = new command_1.Command("apptesting:execute
|
|
23
|
+
exports.command = new command_1.Command("apptesting:execute [release-binary-file]")
|
|
24
24
|
.description("Run mobile automated tests written in natural language driven by AI")
|
|
25
25
|
.option("--app <app_id>", "The app id of your Firebase web app. Optional if the project contains exactly one web app.")
|
|
26
26
|
.option("--test-file-pattern <pattern>", "Test file pattern. Only tests contained in files that match this pattern will be executed.")
|
|
@@ -39,17 +39,29 @@ exports.command = new command_1.Command("apptesting:execute <release-binary-file
|
|
|
39
39
|
const tests = await (0, parseTestFiles_1.parseTestFiles)(testDir, undefined, options.testFilePattern, options.testNamePattern);
|
|
40
40
|
const testDevices = (0, options_parser_util_1.parseTestDevices)(options.testDevices, options.testDevicesFile);
|
|
41
41
|
if (!tests.length) {
|
|
42
|
-
throw new error_1.FirebaseError(
|
|
42
|
+
throw new error_1.FirebaseError(`No tests found under test directory ${testDir}`);
|
|
43
43
|
}
|
|
44
|
+
utils.logBullet(`Found ${(0, parseTestFiles_1.pluralizeTests)(tests.length)} to run under test directory ${testDir}`);
|
|
44
45
|
const invokeSpinner = ora("Requesting test execution");
|
|
45
46
|
const client = new client_1.AppDistributionClient();
|
|
46
47
|
let releaseTests;
|
|
47
48
|
let release;
|
|
48
49
|
try {
|
|
49
|
-
|
|
50
|
+
if (target) {
|
|
51
|
+
release = await (0, distribution_1.upload)(client, appName, new distribution_1.Distribution(target));
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
utils.logBullet("release-binary-file not provided, using the latest App Distribution release.");
|
|
55
|
+
const latestRelease = await client.getLatestRelease(appName);
|
|
56
|
+
if (!latestRelease) {
|
|
57
|
+
throw new error_1.FirebaseError(`No app binary found for ${appName}. Call apptesting:execute with a local app binary file, or upload a release to App Distribution.`);
|
|
58
|
+
}
|
|
59
|
+
release = latestRelease;
|
|
60
|
+
utils.logBullet(`Using release ${release.displayVersion} created at ${release.createTime}`);
|
|
61
|
+
}
|
|
50
62
|
invokeSpinner.start();
|
|
51
63
|
releaseTests = await invokeTests(client, release.name, tests, !testDevices.length ? defaultDevices : testDevices);
|
|
52
|
-
invokeSpinner.text = `${pluralizeTests(releaseTests.length)} started successfully!`;
|
|
64
|
+
invokeSpinner.text = `${(0, parseTestFiles_1.pluralizeTests)(releaseTests.length)} started successfully!`;
|
|
53
65
|
invokeSpinner.succeed();
|
|
54
66
|
}
|
|
55
67
|
catch (ex) {
|
|
@@ -64,9 +76,6 @@ exports.command = new command_1.Command("apptesting:execute <release-binary-file
|
|
|
64
76
|
utils.logBullet(`View detailed results in the Firebase Console:\n${release.firebaseConsoleUri}`);
|
|
65
77
|
}
|
|
66
78
|
});
|
|
67
|
-
function pluralizeTests(numTests) {
|
|
68
|
-
return `${numTests} test${numTests === 1 ? "" : "s"}`;
|
|
69
|
-
}
|
|
70
79
|
async function invokeTests(client, releaseName, testDefs, devices) {
|
|
71
80
|
try {
|
|
72
81
|
const releaseTests = [];
|
|
@@ -75,6 +75,7 @@ exports.command = new command_1.Command("functions:delete [filters...]")
|
|
|
75
75
|
try {
|
|
76
76
|
const fab = new fabricator.Fabricator({
|
|
77
77
|
functionExecutor,
|
|
78
|
+
runFunctionExecutor: functionExecutor,
|
|
78
79
|
appEngineLocation,
|
|
79
80
|
executor: new executor.QueueExecutor({}),
|
|
80
81
|
sources: {},
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.command = void 0;
|
|
4
|
+
const command_1 = require("../command");
|
|
5
|
+
const error_1 = require("../error");
|
|
6
|
+
const iac = require("../functions/iac/export");
|
|
7
|
+
const projectConfig_1 = require("../functions/projectConfig");
|
|
8
|
+
const clc = require("colorette");
|
|
9
|
+
const logger_1 = require("../logger");
|
|
10
|
+
const EXPORTERS = {
|
|
11
|
+
internal: iac.getInternalIac,
|
|
12
|
+
};
|
|
13
|
+
exports.command = new command_1.Command("functions:export")
|
|
14
|
+
.description("export Cloud Functions code and configuration")
|
|
15
|
+
.option("--format <format>", `Format of the output. Can be ${Object.keys(EXPORTERS).join(", ")}.`)
|
|
16
|
+
.option("--codebase <codebase>", "Optional codebase to export. If not specified, exports the default or only codebase.")
|
|
17
|
+
.action(async (options) => {
|
|
18
|
+
if (!options.format || !Object.keys(EXPORTERS).includes(options.format)) {
|
|
19
|
+
throw new error_1.FirebaseError(`Must specify --format as ${Object.keys(EXPORTERS).join(", ")}.`);
|
|
20
|
+
}
|
|
21
|
+
const config = (0, projectConfig_1.normalizeAndValidate)(options.config?.src?.functions);
|
|
22
|
+
let codebaseConfig;
|
|
23
|
+
if (options.codebase) {
|
|
24
|
+
codebaseConfig = (0, projectConfig_1.configForCodebase)(config, options.codebase);
|
|
25
|
+
}
|
|
26
|
+
else if (config.length === 1) {
|
|
27
|
+
codebaseConfig = config[0];
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
codebaseConfig = (0, projectConfig_1.configForCodebase)(config, "default");
|
|
31
|
+
}
|
|
32
|
+
if (!codebaseConfig.source) {
|
|
33
|
+
throw new error_1.FirebaseError("Codebase does not have a local source directory.");
|
|
34
|
+
}
|
|
35
|
+
const manifest = await EXPORTERS[options.format](options, codebaseConfig);
|
|
36
|
+
for (const [file, contents] of Object.entries(manifest)) {
|
|
37
|
+
logger_1.logger.info(`Manifest file: ${clc.bold(file)}`);
|
|
38
|
+
logger_1.logger.info(contents);
|
|
39
|
+
}
|
|
40
|
+
});
|
package/lib/commands/index.js
CHANGED
|
@@ -141,6 +141,9 @@ function load(client) {
|
|
|
141
141
|
client.functions.config.set = loadCommand("functions-config-set");
|
|
142
142
|
client.functions.config.unset = loadCommand("functions-config-unset");
|
|
143
143
|
client.functions.delete = loadCommand("functions-delete");
|
|
144
|
+
if (experiments.isEnabled("functionsiac")) {
|
|
145
|
+
client.functions.export = loadCommand("functions-export");
|
|
146
|
+
}
|
|
144
147
|
client.functions.log = loadCommand("functions-log");
|
|
145
148
|
client.functions.shell = loadCommand("functions-shell");
|
|
146
149
|
client.functions.list = loadCommand("functions-list");
|
package/lib/commands/init.js
CHANGED
|
@@ -228,6 +228,7 @@ async function initAction(feature, options) {
|
|
|
228
228
|
if (setup.features.includes("dataconnect") && setup.features.includes("dataconnect:sdk")) {
|
|
229
229
|
setup.features = setup.features.filter((f) => f !== "dataconnect:sdk");
|
|
230
230
|
}
|
|
231
|
+
setup.features.push("agentSkills");
|
|
231
232
|
await (0, init_1.init)(setup, config, options);
|
|
232
233
|
await postInitSaves(setup, config);
|
|
233
234
|
if (setup.instructions.length) {
|
|
@@ -8,12 +8,12 @@ const path = require("path");
|
|
|
8
8
|
const error_1 = require("../error");
|
|
9
9
|
const unzip_1 = require("../unzip");
|
|
10
10
|
const fs = require("fs");
|
|
11
|
-
exports.command = new command_1.Command("studio:export
|
|
11
|
+
exports.command = new command_1.Command("studio:export [path]")
|
|
12
12
|
.description("Bootstrap Firebase Studio apps for migration to Antigravity. Run on the unzipped folder from the Firebase Studio download, or directly on the downloaded zip file.")
|
|
13
13
|
.option("--no-start-antigravity", "skip starting the Antigravity IDE after migration")
|
|
14
14
|
.action(async (exportPath, options) => {
|
|
15
15
|
if (!exportPath) {
|
|
16
|
-
throw new error_1.FirebaseError("Must specify
|
|
16
|
+
throw new error_1.FirebaseError("Must specify the path to the Firebase Studio downloaded zip file or the unzipped folder path.", { exit: 1 });
|
|
17
17
|
}
|
|
18
18
|
let rootPath = path.resolve(exportPath);
|
|
19
19
|
if (fs.existsSync(rootPath) && fs.statSync(rootPath).isFile() && rootPath.endsWith(".zip")) {
|
|
@@ -8,7 +8,8 @@ const gcs = require("../../gcp/storage");
|
|
|
8
8
|
const getProjectNumber_1 = require("../../getProjectNumber");
|
|
9
9
|
const projectUtils_1 = require("../../projectUtils");
|
|
10
10
|
const utils_1 = require("../../utils");
|
|
11
|
-
const
|
|
11
|
+
const util = require("./util");
|
|
12
|
+
const experiments = require("../../experiments");
|
|
12
13
|
async function default_1(context, options) {
|
|
13
14
|
if (Object.entries(context.backendConfigs).length === 0) {
|
|
14
15
|
return;
|
|
@@ -52,24 +53,28 @@ async function default_1(context, options) {
|
|
|
52
53
|
await Promise.all(Object.values(context.backendConfigs).map(async (cfg) => {
|
|
53
54
|
const rootDir = options.projectRoot ?? process.cwd();
|
|
54
55
|
let builtAppDir;
|
|
55
|
-
|
|
56
|
+
const isLocalBuild = !!cfg.localBuild;
|
|
57
|
+
if (isLocalBuild) {
|
|
58
|
+
experiments.assertEnabled("apphostinglocalbuilds", "App Hosting local builds");
|
|
56
59
|
builtAppDir = context.backendLocalBuilds[cfg.backendId].buildDir;
|
|
57
60
|
if (!builtAppDir) {
|
|
58
61
|
throw new error_1.FirebaseError(`No local build dir found for ${cfg.backendId}`);
|
|
59
62
|
}
|
|
60
63
|
}
|
|
61
|
-
const zippedSourcePath =
|
|
62
|
-
|
|
64
|
+
const zippedSourcePath = isLocalBuild
|
|
65
|
+
? await util.createLocalBuildTarArchive(cfg, rootDir, builtAppDir)
|
|
66
|
+
: await util.createSourceDeployArchive(cfg, rootDir);
|
|
67
|
+
(0, utils_1.logLabeledBullet)("apphosting", `Zipped ${isLocalBuild ? "built app" : "source"} for backend ${cfg.backendId}`);
|
|
63
68
|
const backendLocation = context.backendLocations[cfg.backendId];
|
|
64
69
|
if (!backendLocation) {
|
|
65
70
|
throw new error_1.FirebaseError(`Failed to find location for backend ${cfg.backendId}. Please contact support with the contents of your firebase-debug.log to report your issue.`);
|
|
66
71
|
}
|
|
67
|
-
(0, utils_1.logLabeledBullet)("apphosting", `Uploading ${
|
|
72
|
+
(0, utils_1.logLabeledBullet)("apphosting", `Uploading ${isLocalBuild ? "built app" : "source"} for backend ${cfg.backendId}...`);
|
|
68
73
|
const bucketName = bucketsPerLocation[backendLocation];
|
|
69
74
|
const { bucket, object } = await gcs.uploadObject({
|
|
70
75
|
file: zippedSourcePath,
|
|
71
76
|
stream: fs.createReadStream(zippedSourcePath),
|
|
72
|
-
}, bucketName);
|
|
77
|
+
}, bucketName, isLocalBuild ? gcs.ContentType.TAR : gcs.ContentType.ZIP);
|
|
73
78
|
(0, utils_1.logLabeledBullet)("apphosting", `Uploaded at gs://${bucket}/${object}`);
|
|
74
79
|
context.backendStorageUris[cfg.backendId] =
|
|
75
80
|
`gs://${bucketName}/${path.basename(zippedSourcePath)}`;
|
|
@@ -6,11 +6,15 @@ const path = require("path");
|
|
|
6
6
|
const backend_1 = require("../../apphosting/backend");
|
|
7
7
|
const apphosting_1 = require("../../gcp/apphosting");
|
|
8
8
|
const devConnect_1 = require("../../gcp/devConnect");
|
|
9
|
+
const resourceManager_1 = require("../../gcp/resourceManager");
|
|
9
10
|
const projectUtils_1 = require("../../projectUtils");
|
|
11
|
+
const getProjectNumber_1 = require("../../getProjectNumber");
|
|
10
12
|
const prompt_1 = require("../../prompt");
|
|
11
13
|
const utils_1 = require("../../utils");
|
|
12
14
|
const localbuilds_1 = require("../../apphosting/localbuilds");
|
|
13
15
|
const error_1 = require("../../error");
|
|
16
|
+
const experiments = require("../../experiments");
|
|
17
|
+
const logger_1 = require("../../logger");
|
|
14
18
|
async function default_1(context, options) {
|
|
15
19
|
const projectId = (0, projectUtils_1.needProjectId)(options);
|
|
16
20
|
await (0, apphosting_1.ensureApiEnabled)(options);
|
|
@@ -21,6 +25,10 @@ async function default_1(context, options) {
|
|
|
21
25
|
context.backendStorageUris = {};
|
|
22
26
|
context.backendLocalBuilds = {};
|
|
23
27
|
const configs = getBackendConfigs(options);
|
|
28
|
+
if (configs.some((cfg) => cfg.localBuild) && experiments.isEnabled("apphostinglocalbuilds")) {
|
|
29
|
+
const projectNumber = await (0, getProjectNumber_1.getProjectNumber)(options);
|
|
30
|
+
await ensureAppHostingServiceAgentRoles(projectId, projectNumber);
|
|
31
|
+
}
|
|
24
32
|
const { backends } = await (0, apphosting_1.listBackends)(projectId, "-");
|
|
25
33
|
const foundBackends = [];
|
|
26
34
|
const notFoundBackends = [];
|
|
@@ -112,6 +120,7 @@ async function default_1(context, options) {
|
|
|
112
120
|
if (!cfg.localBuild) {
|
|
113
121
|
continue;
|
|
114
122
|
}
|
|
123
|
+
experiments.assertEnabled("apphostinglocalbuilds", "locally build App Hosting backends");
|
|
115
124
|
(0, utils_1.logLabeledBullet)("apphosting", `Starting local build for backend ${cfg.backendId}`);
|
|
116
125
|
try {
|
|
117
126
|
const { outputFiles, annotations, buildConfig } = await (0, localbuilds_1.localBuild)(options.projectRoot || "./", "nextjs");
|
|
@@ -125,7 +134,8 @@ async function default_1(context, options) {
|
|
|
125
134
|
};
|
|
126
135
|
}
|
|
127
136
|
catch (e) {
|
|
128
|
-
|
|
137
|
+
const errorMsg = e instanceof Error ? e.message : String(e);
|
|
138
|
+
throw new error_1.FirebaseError(`Local Build for backend ${cfg.backendId} failed: ${errorMsg}`);
|
|
129
139
|
}
|
|
130
140
|
}
|
|
131
141
|
}
|
|
@@ -157,3 +167,13 @@ function getBackendConfigs(options) {
|
|
|
157
167
|
}
|
|
158
168
|
return backendConfigs.filter((cfg) => backendIds.includes(cfg.backendId));
|
|
159
169
|
}
|
|
170
|
+
async function ensureAppHostingServiceAgentRoles(projectId, projectNumber) {
|
|
171
|
+
const p4saEmail = (0, apphosting_1.serviceAgentEmail)(projectNumber);
|
|
172
|
+
try {
|
|
173
|
+
await (0, resourceManager_1.addServiceAccountToRoles)(projectId, p4saEmail, ["roles/storage.objectViewer"], true);
|
|
174
|
+
}
|
|
175
|
+
catch (err) {
|
|
176
|
+
logger_1.logger.debug(`Failed to grant storage.objectViewer to ${p4saEmail}: ${String(err)}`);
|
|
177
|
+
(0, utils_1.logLabeledWarning)("apphosting", `Unable to verify App Hosting service agent permissions for ${p4saEmail}. If you encounter a PERMISSION_DENIED error during rollout, please ensure the service agent has the "Storage Object Viewer" role.`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
@@ -15,11 +15,6 @@ async function default_1(context, options) {
|
|
|
15
15
|
(0, utils_1.logLabeledWarning)("apphosting", `Failed to find metadata for backend(s) ${backendIds.join(", ")}. Please contact support with the contents of your firebase-debug.log to report your issue.`);
|
|
16
16
|
backendIds = backendIds.filter((id) => !missingBackends.includes(id));
|
|
17
17
|
}
|
|
18
|
-
const localBuildBackends = backendIds.filter((id) => context.backendLocalBuilds[id]);
|
|
19
|
-
if (localBuildBackends.length > 0) {
|
|
20
|
-
(0, utils_1.logLabeledWarning)("apphosting", `Skipping backend(s) ${localBuildBackends.join(", ")}. Local Builds are not supported yet.`);
|
|
21
|
-
backendIds = backendIds.filter((id) => !localBuildBackends.includes(id));
|
|
22
|
-
}
|
|
23
18
|
if (backendIds.length === 0) {
|
|
24
19
|
return;
|
|
25
20
|
}
|
|
@@ -29,10 +24,12 @@ async function default_1(context, options) {
|
|
|
29
24
|
backendId,
|
|
30
25
|
location: context.backendLocations[backendId],
|
|
31
26
|
buildInput: {
|
|
27
|
+
config: context.backendLocalBuilds[backendId]?.buildConfig,
|
|
32
28
|
source: {
|
|
33
29
|
archive: {
|
|
34
30
|
userStorageUri: context.backendStorageUris[backendId],
|
|
35
31
|
rootDirectory: context.backendConfigs[backendId].rootDir,
|
|
32
|
+
locallyBuiltSource: !!context.backendLocalBuilds[backendId],
|
|
36
33
|
},
|
|
37
34
|
},
|
|
38
35
|
},
|