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
|
@@ -6,6 +6,7 @@ exports.migrate = migrate;
|
|
|
6
6
|
const fs = require("fs/promises");
|
|
7
7
|
const path = require("path");
|
|
8
8
|
const child_process_1 = require("child_process");
|
|
9
|
+
const semver = require("semver");
|
|
9
10
|
const logger_1 = require("../logger");
|
|
10
11
|
const prompt = require("../prompt");
|
|
11
12
|
const apphosting = require("../gcp/apphosting");
|
|
@@ -16,7 +17,8 @@ const secrets_1 = require("../apphosting/secrets");
|
|
|
16
17
|
const env = require("../functions/env");
|
|
17
18
|
const error_1 = require("../error");
|
|
18
19
|
const os = require("os");
|
|
19
|
-
|
|
20
|
+
const agentSkills_1 = require("../agentSkills");
|
|
21
|
+
async function setupAntigravityMcpServer(rootPath, appType, nonInteractive) {
|
|
20
22
|
const mcpConfigDir = path.join(os.homedir(), ".gemini", "antigravity");
|
|
21
23
|
const mcpConfigPath = path.join(mcpConfigDir, "mcp_config.json");
|
|
22
24
|
let mcpConfig = { mcpServers: {} };
|
|
@@ -38,12 +40,24 @@ async function setupAntigravityMcpServer(rootPath, appType) {
|
|
|
38
40
|
}
|
|
39
41
|
let updated = false;
|
|
40
42
|
if (!mcpConfig.mcpServers["firebase"]) {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
43
|
+
if (utils.commandExistsSync("npx")) {
|
|
44
|
+
const confirmFirebase = await prompt.confirm({
|
|
45
|
+
message: "Would you like to enable the Firebase MCP server for Antigravity?",
|
|
46
|
+
default: true,
|
|
47
|
+
nonInteractive,
|
|
48
|
+
});
|
|
49
|
+
if (confirmFirebase) {
|
|
50
|
+
mcpConfig.mcpServers["firebase"] = {
|
|
51
|
+
command: "npx",
|
|
52
|
+
args: ["-y", "firebase-tools@latest", "mcp", "--dir", path.resolve(rootPath)],
|
|
53
|
+
};
|
|
54
|
+
updated = true;
|
|
55
|
+
logger_1.logger.info(`✅ Configured Firebase MCP server in ${mcpConfigPath}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
logger_1.logger.info("ℹ️ npx not found on PATH, skipping Firebase MCP server configuration.");
|
|
60
|
+
}
|
|
47
61
|
}
|
|
48
62
|
else {
|
|
49
63
|
logger_1.logger.info("ℹ️ Firebase MCP server already configured in Antigravity, skipping.");
|
|
@@ -51,12 +65,19 @@ async function setupAntigravityMcpServer(rootPath, appType) {
|
|
|
51
65
|
if (appType === "FLUTTER") {
|
|
52
66
|
if (utils.commandExistsSync("dart")) {
|
|
53
67
|
if (!mcpConfig.mcpServers["dart"]) {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
68
|
+
const confirmDart = await prompt.confirm({
|
|
69
|
+
message: "Would you like to enable the Dart MCP server for Antigravity?",
|
|
70
|
+
default: true,
|
|
71
|
+
nonInteractive,
|
|
72
|
+
});
|
|
73
|
+
if (confirmDart) {
|
|
74
|
+
mcpConfig.mcpServers["dart"] = {
|
|
75
|
+
command: "dart",
|
|
76
|
+
args: ["mcp-server"],
|
|
77
|
+
};
|
|
78
|
+
updated = true;
|
|
79
|
+
logger_1.logger.info(`✅ Configured Dart MCP server in ${mcpConfigPath}`);
|
|
80
|
+
}
|
|
60
81
|
}
|
|
61
82
|
else {
|
|
62
83
|
logger_1.logger.info("ℹ️ Dart MCP server already configured in Antigravity, skipping.");
|
|
@@ -71,8 +92,7 @@ async function setupAntigravityMcpServer(rootPath, appType) {
|
|
|
71
92
|
}
|
|
72
93
|
}
|
|
73
94
|
catch (err) {
|
|
74
|
-
|
|
75
|
-
utils.logWarning(`Could not configure Antigravity MCP server: ${message}`);
|
|
95
|
+
utils.logWarning(`Could not configure Antigravity MCP server: ${(0, error_1.getErrMsg)(err)}`);
|
|
76
96
|
}
|
|
77
97
|
}
|
|
78
98
|
async function detectAppType(rootPath) {
|
|
@@ -200,7 +220,7 @@ async function updateReadme(rootPath, framework) {
|
|
|
200
220
|
await fs.writeFile(readmePath, newReadme);
|
|
201
221
|
logger_1.logger.info("✅ Updated README.md with project details and origin info");
|
|
202
222
|
}
|
|
203
|
-
async function injectAntigravityContext(rootPath, projectId, appName) {
|
|
223
|
+
async function injectAntigravityContext(rootPath, projectId, appName, nonInteractive) {
|
|
204
224
|
const agentDir = path.join(rootPath, ".agents");
|
|
205
225
|
const rulesDir = path.join(agentDir, "rules");
|
|
206
226
|
const workflowsDir = path.join(agentDir, "workflows");
|
|
@@ -208,24 +228,21 @@ async function injectAntigravityContext(rootPath, projectId, appName) {
|
|
|
208
228
|
await fs.mkdir(rulesDir, { recursive: true });
|
|
209
229
|
await fs.mkdir(workflowsDir, { recursive: true });
|
|
210
230
|
await fs.mkdir(skillsDir, { recursive: true });
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
}
|
|
226
|
-
catch (err) {
|
|
227
|
-
utils.logWarning(`Could not add Antigravity skills, skipping. ${err}`);
|
|
228
|
-
}
|
|
231
|
+
const installLocation = await prompt.select({
|
|
232
|
+
message: "Where would you like to install Firebase project skills?",
|
|
233
|
+
choices: [
|
|
234
|
+
{ name: "Locally in the project", value: "local" },
|
|
235
|
+
{ name: "Globally for all projects", value: "global" },
|
|
236
|
+
],
|
|
237
|
+
default: "local",
|
|
238
|
+
nonInteractive: nonInteractive || process.env.NODE_ENV === "test",
|
|
239
|
+
});
|
|
240
|
+
await (0, agentSkills_1.installAgentSkills)({
|
|
241
|
+
cwd: rootPath,
|
|
242
|
+
global: installLocation === "global",
|
|
243
|
+
background: false,
|
|
244
|
+
agentName: "gemini-cli",
|
|
245
|
+
});
|
|
229
246
|
const systemInstructionsTemplate = await (0, templates_1.readTemplate)("firebase-studio-export/system_instructions_template.md");
|
|
230
247
|
const systemInstructions = systemInstructionsTemplate.replace("${appName}", appName);
|
|
231
248
|
await fs.writeFile(path.join(rulesDir, "migration-context.md"), systemInstructions);
|
|
@@ -236,8 +253,7 @@ async function injectAntigravityContext(rootPath, projectId, appName) {
|
|
|
236
253
|
logger_1.logger.info("✅ Created Antigravity startup workflow");
|
|
237
254
|
}
|
|
238
255
|
catch (err) {
|
|
239
|
-
|
|
240
|
-
logger_1.logger.debug(`Could not read or write startup workflow: ${message}`);
|
|
256
|
+
logger_1.logger.debug(`Could not read or write startup workflow: ${(0, error_1.getErrMsg)(err)}`);
|
|
241
257
|
}
|
|
242
258
|
}
|
|
243
259
|
async function getAgyCommand(startAgy) {
|
|
@@ -275,7 +291,7 @@ async function getAgyCommand(startAgy) {
|
|
|
275
291
|
logger_1.logger.info(`⚠️ Antigravity IDE not found in your PATH. To ensure a seamless migration, please download and install Antigravity: ${downloadLink}`);
|
|
276
292
|
return undefined;
|
|
277
293
|
}
|
|
278
|
-
async function createFirebaseConfigs(rootPath, projectId) {
|
|
294
|
+
async function createFirebaseConfigs(rootPath, projectId, nonInteractive) {
|
|
279
295
|
if (!projectId) {
|
|
280
296
|
return;
|
|
281
297
|
}
|
|
@@ -310,7 +326,7 @@ async function createFirebaseConfigs(rootPath, projectId) {
|
|
|
310
326
|
const confirmBackend = await prompt.confirm({
|
|
311
327
|
message: `Would you like to use the App Hosting backend "${selectedBackendId}"?`,
|
|
312
328
|
default: true,
|
|
313
|
-
nonInteractive: process.env.NODE_ENV === "test",
|
|
329
|
+
nonInteractive: nonInteractive || process.env.NODE_ENV === "test",
|
|
314
330
|
});
|
|
315
331
|
if (confirmBackend) {
|
|
316
332
|
backendId = selectedBackendId;
|
|
@@ -335,8 +351,7 @@ async function createFirebaseConfigs(rootPath, projectId) {
|
|
|
335
351
|
}
|
|
336
352
|
}
|
|
337
353
|
catch (err) {
|
|
338
|
-
|
|
339
|
-
utils.logWarning(`Could not fetch backends from Firebase CLI, using default "studio". ${message}`);
|
|
354
|
+
utils.logWarning(`Could not fetch backends from Firebase CLI, using default "studio". ${(0, error_1.getErrMsg)(err)}`);
|
|
340
355
|
}
|
|
341
356
|
const firebaseJson = {
|
|
342
357
|
apphosting: {
|
|
@@ -392,8 +407,7 @@ async function writeAntigravityConfigs(rootPath, framework) {
|
|
|
392
407
|
settings = JSON.parse(settingsContent);
|
|
393
408
|
}
|
|
394
409
|
catch (err) {
|
|
395
|
-
|
|
396
|
-
logger_1.logger.debug(`Could not read ${settingsPath}: ${message}`);
|
|
410
|
+
logger_1.logger.debug(`Could not read ${settingsPath}: ${(0, error_1.getErrMsg)(err)}`);
|
|
397
411
|
}
|
|
398
412
|
const cleanSettings = {};
|
|
399
413
|
for (const [key, value] of Object.entries(settings)) {
|
|
@@ -456,8 +470,7 @@ async function cleanupUnusedFiles(rootPath) {
|
|
|
456
470
|
}
|
|
457
471
|
}
|
|
458
472
|
catch (err) {
|
|
459
|
-
|
|
460
|
-
logger_1.logger.debug(`Could not remove ${docsDir}: ${message}`);
|
|
473
|
+
logger_1.logger.debug(`Could not remove ${docsDir}: ${(0, error_1.getErrMsg)(err)}`);
|
|
461
474
|
}
|
|
462
475
|
const modifiedPath = path.join(rootPath, ".modified");
|
|
463
476
|
try {
|
|
@@ -465,8 +478,7 @@ async function cleanupUnusedFiles(rootPath) {
|
|
|
465
478
|
logger_1.logger.info("✅ Cleaned up .modified");
|
|
466
479
|
}
|
|
467
480
|
catch (err) {
|
|
468
|
-
|
|
469
|
-
logger_1.logger.debug(`Could not delete ${modifiedPath}: ${message}`);
|
|
481
|
+
logger_1.logger.debug(`Could not delete ${modifiedPath}: ${(0, error_1.getErrMsg)(err)}`);
|
|
470
482
|
}
|
|
471
483
|
const mcpJsonPath = path.join(rootPath, ".idx", "mcp.json");
|
|
472
484
|
try {
|
|
@@ -474,8 +486,7 @@ async function cleanupUnusedFiles(rootPath) {
|
|
|
474
486
|
logger_1.logger.info("✅ Cleaned up .idx/mcp.json");
|
|
475
487
|
}
|
|
476
488
|
catch (err) {
|
|
477
|
-
|
|
478
|
-
logger_1.logger.debug(`Could not delete ${mcpJsonPath}: ${message}`);
|
|
489
|
+
logger_1.logger.debug(`Could not delete ${mcpJsonPath}: ${(0, error_1.getErrMsg)(err)}`);
|
|
479
490
|
}
|
|
480
491
|
}
|
|
481
492
|
async function upgradeGenkitVersion(rootPath) {
|
|
@@ -484,29 +495,30 @@ async function upgradeGenkitVersion(rootPath) {
|
|
|
484
495
|
const packageJsonContent = await fs.readFile(packageJsonPath, "utf8");
|
|
485
496
|
const packageJson = JSON.parse(packageJsonContent);
|
|
486
497
|
let modified = false;
|
|
487
|
-
const
|
|
488
|
-
|
|
498
|
+
const targetVersion = "1.29.0";
|
|
499
|
+
const checkAndUpgrade = (deps) => {
|
|
500
|
+
if (!deps || !deps["genkit-cli"]) {
|
|
489
501
|
return;
|
|
490
502
|
}
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
503
|
+
const currentVersion = deps["genkit-cli"];
|
|
504
|
+
if (currentVersion.startsWith("^")) {
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
const coerced = semver.coerce(currentVersion);
|
|
508
|
+
if (coerced && semver.lt(coerced, targetVersion)) {
|
|
509
|
+
deps["genkit-cli"] = "^1.29";
|
|
510
|
+
modified = true;
|
|
498
511
|
}
|
|
499
512
|
};
|
|
500
|
-
|
|
501
|
-
|
|
513
|
+
checkAndUpgrade(packageJson.dependencies);
|
|
514
|
+
checkAndUpgrade(packageJson.devDependencies);
|
|
502
515
|
if (modified) {
|
|
503
516
|
await fs.writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2) + "\n");
|
|
504
|
-
logger_1.logger.info("✅ Upgraded
|
|
517
|
+
logger_1.logger.info("✅ Upgraded genkit-cli version to 1.29 in package.json");
|
|
505
518
|
}
|
|
506
519
|
}
|
|
507
520
|
catch (err) {
|
|
508
|
-
|
|
509
|
-
logger_1.logger.debug(`Could not upgrade Genkit version: ${message}`);
|
|
521
|
+
logger_1.logger.debug(`Could not upgrade Genkit version: ${(0, error_1.getErrMsg)(err)}`);
|
|
510
522
|
}
|
|
511
523
|
}
|
|
512
524
|
async function uploadSecrets(rootPath, projectId) {
|
|
@@ -534,11 +546,10 @@ async function uploadSecrets(rootPath, projectId) {
|
|
|
534
546
|
}
|
|
535
547
|
}
|
|
536
548
|
catch (err) {
|
|
537
|
-
|
|
538
|
-
utils.logWarning(`Failed to upload GEMINI_API_KEY secret: ${message}`);
|
|
549
|
+
utils.logWarning(`Failed to upload GEMINI_API_KEY secret: ${(0, error_1.getErrMsg)(err)}`);
|
|
539
550
|
}
|
|
540
551
|
}
|
|
541
|
-
async function askToOpenAntigravity(rootPath, appName, startAntigravity) {
|
|
552
|
+
async function askToOpenAntigravity(rootPath, appName, startAntigravity, nonInteractive) {
|
|
542
553
|
const agyCommand = await getAgyCommand(startAntigravity);
|
|
543
554
|
logger_1.logger.info(`\n🎉 Your Firebase Studio project "${appName}" is now ready for Antigravity!`);
|
|
544
555
|
logger_1.logger.info("Antigravity is Google's agentic IDE, where you can collaborate with AI agents to build, test, and deploy your application.");
|
|
@@ -552,6 +563,7 @@ async function askToOpenAntigravity(rootPath, appName, startAntigravity) {
|
|
|
552
563
|
const answer = await prompt.confirm({
|
|
553
564
|
message: "Would you like to open it in Antigravity now?",
|
|
554
565
|
default: true,
|
|
566
|
+
nonInteractive,
|
|
555
567
|
});
|
|
556
568
|
if (answer) {
|
|
557
569
|
logger_1.logger.info(`⏳ Opening ${appName} in Antigravity...`);
|
|
@@ -586,24 +598,25 @@ async function checkDirectoryExists(dir) {
|
|
|
586
598
|
async function migrate(rootPath, options = { startAntigravity: true }) {
|
|
587
599
|
await checkDirectoryExists(rootPath);
|
|
588
600
|
const appType = await detectAppType(rootPath);
|
|
589
|
-
|
|
601
|
+
await track.trackGA4("firebase_studio_migrate", { app_type: appType, result: "started" });
|
|
590
602
|
logger_1.logger.info("🚀 Starting Firebase Studio to Antigravity migration...");
|
|
603
|
+
logger_1.logger.info("\nFile any bugs at https://github.com/firebase/firebase-tools/issues");
|
|
591
604
|
const { projectId, appName } = await extractMetadata(rootPath, options.project);
|
|
592
605
|
if (appType) {
|
|
593
606
|
logger_1.logger.info(`✅ Detected framework: ${appType}`);
|
|
594
607
|
}
|
|
595
608
|
await updateReadme(rootPath, appType);
|
|
596
|
-
await createFirebaseConfigs(rootPath, projectId);
|
|
609
|
+
await createFirebaseConfigs(rootPath, projectId, options.nonInteractive);
|
|
597
610
|
await uploadSecrets(rootPath, projectId);
|
|
598
611
|
await upgradeGenkitVersion(rootPath);
|
|
599
|
-
await injectAntigravityContext(rootPath, projectId, appName);
|
|
612
|
+
await injectAntigravityContext(rootPath, projectId, appName, options.nonInteractive);
|
|
600
613
|
await writeAntigravityConfigs(rootPath, appType);
|
|
601
|
-
await setupAntigravityMcpServer(rootPath, appType);
|
|
614
|
+
await setupAntigravityMcpServer(rootPath, appType, options.nonInteractive);
|
|
602
615
|
await cleanupUnusedFiles(rootPath);
|
|
603
616
|
const currentFolderName = path.basename(rootPath);
|
|
604
617
|
if (currentFolderName === "download") {
|
|
605
618
|
logger_1.logger.info(`\n💡 Tip: You may want to rename this folder to "${appName.toLowerCase().replace(/\s+/g, "-")}"`);
|
|
606
619
|
}
|
|
607
620
|
await track.trackGA4("firebase_studio_migrate", { app_type: appType, result: "success" });
|
|
608
|
-
await askToOpenAntigravity(rootPath, appName, options.startAntigravity);
|
|
621
|
+
await askToOpenAntigravity(rootPath, appName, options.startAntigravity, options.nonInteractive);
|
|
609
622
|
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getInternalIac = getInternalIac;
|
|
4
|
+
const runtimes = require("../../deploy/functions/runtimes");
|
|
5
|
+
const supported = require("../../deploy/functions/runtimes/supported");
|
|
6
|
+
const functionsConfig = require("../../functionsConfig");
|
|
7
|
+
const functionsEnv = require("../../functions/env");
|
|
8
|
+
const logger_1 = require("../../logger");
|
|
9
|
+
const yaml = require("js-yaml");
|
|
10
|
+
const projectUtils_1 = require("../../projectUtils");
|
|
11
|
+
const error_1 = require("../../error");
|
|
12
|
+
async function getInternalIac(options, codebase) {
|
|
13
|
+
if (!codebase.source) {
|
|
14
|
+
throw new error_1.FirebaseError("Cannot export a codebase with no source");
|
|
15
|
+
}
|
|
16
|
+
const projectId = (0, projectUtils_1.needProjectId)(options);
|
|
17
|
+
const firebaseConfig = await functionsConfig.getFirebaseConfig(options);
|
|
18
|
+
const firebaseEnvs = functionsEnv.loadFirebaseEnvs(firebaseConfig, projectId);
|
|
19
|
+
const delegateContext = {
|
|
20
|
+
projectId,
|
|
21
|
+
sourceDir: options.config.path(codebase.source),
|
|
22
|
+
projectDir: options.config.projectDir,
|
|
23
|
+
runtime: codebase.runtime,
|
|
24
|
+
};
|
|
25
|
+
const runtimeDelegate = await runtimes.getRuntimeDelegate(delegateContext);
|
|
26
|
+
logger_1.logger.debug(`Validating ${runtimeDelegate.language} source`);
|
|
27
|
+
supported.guardVersionSupport(runtimeDelegate.runtime);
|
|
28
|
+
await runtimeDelegate.validate();
|
|
29
|
+
logger_1.logger.debug(`Building ${runtimeDelegate.language} source`);
|
|
30
|
+
await runtimeDelegate.build();
|
|
31
|
+
logger_1.logger.debug(`Discovering ${runtimeDelegate.language} source`);
|
|
32
|
+
const build = await runtimeDelegate.discoverBuild({}, firebaseEnvs);
|
|
33
|
+
return {
|
|
34
|
+
"functions.yaml": yaml.dump(build),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.expr = expr;
|
|
4
|
+
exports.copyField = copyField;
|
|
5
|
+
exports.renameField = renameField;
|
|
6
|
+
exports.serviceAccount = serviceAccount;
|
|
7
|
+
exports.serializeValue = serializeValue;
|
|
8
|
+
exports.blockToString = blockToString;
|
|
9
|
+
const utils = require("../../utils");
|
|
10
|
+
const error_1 = require("../../error");
|
|
11
|
+
function expr(string) {
|
|
12
|
+
return { "@type": "HCLExpression", value: string };
|
|
13
|
+
}
|
|
14
|
+
function copyField(attributes, source, field, transform = (v) => v) {
|
|
15
|
+
renameField(attributes, source, utils.toLowerSnakeCase(field), field, transform);
|
|
16
|
+
}
|
|
17
|
+
function renameField(attributes, source, attributeField, sourceField, transform = (v) => v) {
|
|
18
|
+
const val = source[sourceField];
|
|
19
|
+
if (val === undefined) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
attributes[attributeField] = val === null ? null : transform(val);
|
|
23
|
+
}
|
|
24
|
+
function serviceAccount(sa) {
|
|
25
|
+
if (sa.endsWith("@")) {
|
|
26
|
+
return `${sa}\${var.project}.iam.gserviceaccount.com`;
|
|
27
|
+
}
|
|
28
|
+
return sa;
|
|
29
|
+
}
|
|
30
|
+
function serializeValue(value, indentation = 0) {
|
|
31
|
+
if (typeof value === "string") {
|
|
32
|
+
value = value.replace(/{{ *params\.PROJECT_ID *}}/g, "${var.project}");
|
|
33
|
+
if (value.includes("{{ ")) {
|
|
34
|
+
throw new error_1.FirebaseError("Generalized parameterized fields are not supported in terraform yet");
|
|
35
|
+
}
|
|
36
|
+
return JSON.stringify(value);
|
|
37
|
+
}
|
|
38
|
+
else if (typeof value === "number" || typeof value === "boolean") {
|
|
39
|
+
return value.toString();
|
|
40
|
+
}
|
|
41
|
+
else if (value === null || value === undefined) {
|
|
42
|
+
return "null";
|
|
43
|
+
}
|
|
44
|
+
else if (Array.isArray(value)) {
|
|
45
|
+
if (value.some((e) => e !== null &&
|
|
46
|
+
typeof e === "object" &&
|
|
47
|
+
(Array.isArray(e) || e["@type"] !== "HCLExpression"))) {
|
|
48
|
+
return `[\n${value.map((v) => " ".repeat(indentation + 1) + serializeValue(v, indentation + 1)).join(",\n")}\n${" ".repeat(indentation)}]`;
|
|
49
|
+
}
|
|
50
|
+
return `[${value.map((v) => serializeValue(v)).join(", ")}]`;
|
|
51
|
+
}
|
|
52
|
+
else if (typeof value === "object") {
|
|
53
|
+
if (value["@type"] === "HCLExpression") {
|
|
54
|
+
return value.value;
|
|
55
|
+
}
|
|
56
|
+
const entries = Object.entries(value).map(([k, v]) => `${" ".repeat(indentation + 1)}${k} = ${serializeValue(v, indentation + 1)}`);
|
|
57
|
+
return `{\n${entries.join("\n")}\n${" ".repeat(indentation)}}`;
|
|
58
|
+
}
|
|
59
|
+
throw new error_1.FirebaseError(`Unsupported terraform value type ${typeof value}`, { exit: 1 });
|
|
60
|
+
}
|
|
61
|
+
const PREFIX_ARGUMENTS = new Set(["count", "for_each", "provider"]);
|
|
62
|
+
const SUFFIX_ARGUMENTS = new Set(["lifecycle", "depends_on"]);
|
|
63
|
+
const NON_BLOCK_PARAMETERS = {
|
|
64
|
+
google_cloudfunctions_function: new Set([
|
|
65
|
+
"name",
|
|
66
|
+
"runtime",
|
|
67
|
+
"description",
|
|
68
|
+
"available_memory_mb",
|
|
69
|
+
"timeout",
|
|
70
|
+
"entry_point",
|
|
71
|
+
"source_archive_bucket",
|
|
72
|
+
"source_archive_object",
|
|
73
|
+
"trigger_http",
|
|
74
|
+
"environment_variables",
|
|
75
|
+
"vpc_connector",
|
|
76
|
+
"service_account_email",
|
|
77
|
+
"max_instances",
|
|
78
|
+
"min_instances",
|
|
79
|
+
"project",
|
|
80
|
+
"region",
|
|
81
|
+
]),
|
|
82
|
+
google_cloudfunctions2_function: new Set(["name", "location", "description", "project"]),
|
|
83
|
+
google_cloud_scheduler_job: new Set([
|
|
84
|
+
"name",
|
|
85
|
+
"description",
|
|
86
|
+
"schedule",
|
|
87
|
+
"time_zone",
|
|
88
|
+
"paused",
|
|
89
|
+
"attempt_deadline",
|
|
90
|
+
"region",
|
|
91
|
+
"project",
|
|
92
|
+
]),
|
|
93
|
+
google_cloud_tasks_queue: new Set(["name", "location", "desired_state", "project"]),
|
|
94
|
+
google_eventarc_trigger: new Set(["name", "location", "project", "service_account"]),
|
|
95
|
+
google_pubsub_topic: new Set([
|
|
96
|
+
"name",
|
|
97
|
+
"project",
|
|
98
|
+
"labels",
|
|
99
|
+
"kms_key_name",
|
|
100
|
+
"message_retention_duration",
|
|
101
|
+
]),
|
|
102
|
+
google_pubsub_subscription: new Set([
|
|
103
|
+
"name",
|
|
104
|
+
"topic",
|
|
105
|
+
"project",
|
|
106
|
+
"labels",
|
|
107
|
+
"ack_deadline_seconds",
|
|
108
|
+
"message_retention_duration",
|
|
109
|
+
"retain_acked_messages",
|
|
110
|
+
"enable_message_ordering",
|
|
111
|
+
"filter",
|
|
112
|
+
]),
|
|
113
|
+
};
|
|
114
|
+
function serializeResourceAttributes(attributes, resourceType, indentation = 0) {
|
|
115
|
+
const nonBlockParams = NON_BLOCK_PARAMETERS[resourceType] || new Set();
|
|
116
|
+
const prefixGroup = [];
|
|
117
|
+
const nonBlockGroup = [];
|
|
118
|
+
const blockGroup = [];
|
|
119
|
+
const suffixGroup = [];
|
|
120
|
+
for (const [k, v] of Object.entries(attributes)) {
|
|
121
|
+
const serialized = `${" ".repeat(indentation + 1)}${k} = ${serializeValue(v, indentation + 1)}`;
|
|
122
|
+
if (PREFIX_ARGUMENTS.has(k)) {
|
|
123
|
+
prefixGroup.push(serialized);
|
|
124
|
+
}
|
|
125
|
+
else if (nonBlockParams.has(k)) {
|
|
126
|
+
nonBlockGroup.push(serialized);
|
|
127
|
+
}
|
|
128
|
+
else if (SUFFIX_ARGUMENTS.has(k)) {
|
|
129
|
+
suffixGroup.push(serialized);
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
blockGroup.push(serialized);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
const nonemptyGroups = [prefixGroup, nonBlockGroup, blockGroup, suffixGroup].filter((g) => g.length > 0);
|
|
136
|
+
const joinedGroups = nonemptyGroups.map((g) => g.join("\n")).join("\n\n");
|
|
137
|
+
return `{\n${joinedGroups}\n${" ".repeat(indentation)}}`;
|
|
138
|
+
}
|
|
139
|
+
function blockToString(block, indentation = 0) {
|
|
140
|
+
const labels = (block.labels || []).map((l) => `"${l}"`).join(" ");
|
|
141
|
+
if (block.type === "resource" && block.labels?.length) {
|
|
142
|
+
const resourceType = block.labels[0];
|
|
143
|
+
return `${" ".repeat(indentation)}${block.type} ${labels ? labels + " " : ""}${serializeResourceAttributes(block.attributes, resourceType, indentation)}`;
|
|
144
|
+
}
|
|
145
|
+
return `${" ".repeat(indentation)}${block.type} ${labels ? labels + " " : ""}${serializeValue(block.attributes, indentation)}`;
|
|
146
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.client = exports.AI_LOGIC_TRIGGERS_TO_EVENTS = exports.AI_LOGIC_EVENTS_TO_TRIGGER = exports.API_VERSION = void 0;
|
|
4
|
+
exports.createTrigger = createTrigger;
|
|
5
|
+
exports.getTrigger = getTrigger;
|
|
6
|
+
exports.updateTrigger = updateTrigger;
|
|
7
|
+
exports.deleteTrigger = deleteTrigger;
|
|
8
|
+
exports.listTriggers = listTriggers;
|
|
9
|
+
exports.upsertBlockingFunction = upsertBlockingFunction;
|
|
10
|
+
exports.deleteBlockingFunction = deleteBlockingFunction;
|
|
11
|
+
const apiv2_1 = require("../apiv2");
|
|
12
|
+
const api_1 = require("../api");
|
|
13
|
+
const ailogic_1 = require("../deploy/functions/services/ailogic");
|
|
14
|
+
exports.API_VERSION = "v1beta";
|
|
15
|
+
exports.AI_LOGIC_EVENTS_TO_TRIGGER = {
|
|
16
|
+
[ailogic_1.AI_LOGIC_BEFORE_GENERATE_CONTENT]: "before-generate-content",
|
|
17
|
+
[ailogic_1.AI_LOGIC_AFTER_GENERATE_CONTENT]: "after-generate-content",
|
|
18
|
+
};
|
|
19
|
+
exports.AI_LOGIC_TRIGGERS_TO_EVENTS = {
|
|
20
|
+
"before-generate-content": ailogic_1.AI_LOGIC_BEFORE_GENERATE_CONTENT,
|
|
21
|
+
"after-generate-content": ailogic_1.AI_LOGIC_AFTER_GENERATE_CONTENT,
|
|
22
|
+
};
|
|
23
|
+
exports.client = new apiv2_1.Client({
|
|
24
|
+
urlPrefix: (0, api_1.aiLogicProxyOrigin)(),
|
|
25
|
+
auth: true,
|
|
26
|
+
apiVersion: exports.API_VERSION,
|
|
27
|
+
});
|
|
28
|
+
async function createTrigger(projectId, location, triggerId, trigger, validateOnly = false) {
|
|
29
|
+
const parent = `projects/${projectId}/locations/${location}`;
|
|
30
|
+
const res = await exports.client.post(`${parent}/triggers`, trigger, {
|
|
31
|
+
queryParams: {
|
|
32
|
+
triggerId,
|
|
33
|
+
validateOnly: validateOnly ? "true" : "false",
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
return res.body;
|
|
37
|
+
}
|
|
38
|
+
async function getTrigger(projectId, location, triggerId) {
|
|
39
|
+
const name = `projects/${projectId}/locations/${location}/triggers/${triggerId}`;
|
|
40
|
+
const res = await exports.client.get(name);
|
|
41
|
+
return res.body;
|
|
42
|
+
}
|
|
43
|
+
async function updateTrigger(projectId, location, triggerId, trigger, updateMask, allowMissing = false, validateOnly = false) {
|
|
44
|
+
const name = `projects/${projectId}/locations/${location}/triggers/${triggerId}`;
|
|
45
|
+
const queryParams = {
|
|
46
|
+
allowMissing: allowMissing ? "true" : "false",
|
|
47
|
+
validateOnly: validateOnly ? "true" : "false",
|
|
48
|
+
};
|
|
49
|
+
if (updateMask && updateMask.length > 0) {
|
|
50
|
+
queryParams.updateMask = updateMask.join(",");
|
|
51
|
+
}
|
|
52
|
+
const res = await exports.client.patch(name, trigger, { queryParams });
|
|
53
|
+
return res.body;
|
|
54
|
+
}
|
|
55
|
+
async function deleteTrigger(projectId, location, triggerId, allowMissing = false, validateOnly = false, etag) {
|
|
56
|
+
const name = `projects/${projectId}/locations/${location}/triggers/${triggerId}`;
|
|
57
|
+
const queryParams = {
|
|
58
|
+
allowMissing: allowMissing ? "true" : "false",
|
|
59
|
+
validateOnly: validateOnly ? "true" : "false",
|
|
60
|
+
};
|
|
61
|
+
if (etag) {
|
|
62
|
+
queryParams.etag = etag;
|
|
63
|
+
}
|
|
64
|
+
await exports.client.delete(name, { queryParams });
|
|
65
|
+
}
|
|
66
|
+
async function listTriggers(projectId, location, filter) {
|
|
67
|
+
const parent = `projects/${projectId}/locations/${location}`;
|
|
68
|
+
let pageToken;
|
|
69
|
+
const triggers = [];
|
|
70
|
+
do {
|
|
71
|
+
const queryParams = pageToken ? { pageToken } : {};
|
|
72
|
+
if (filter) {
|
|
73
|
+
queryParams.filter = filter;
|
|
74
|
+
}
|
|
75
|
+
const res = await exports.client.get(`${parent}/triggers`, { queryParams });
|
|
76
|
+
if (res.body.triggers) {
|
|
77
|
+
triggers.push(...res.body.triggers);
|
|
78
|
+
}
|
|
79
|
+
pageToken = res.body.nextPageToken;
|
|
80
|
+
} while (pageToken);
|
|
81
|
+
return triggers;
|
|
82
|
+
}
|
|
83
|
+
async function upsertBlockingFunction(endpoint) {
|
|
84
|
+
const eventType = endpoint.blockingTrigger.eventType;
|
|
85
|
+
const triggerId = exports.AI_LOGIC_EVENTS_TO_TRIGGER[eventType];
|
|
86
|
+
const location = endpoint.blockingTrigger.options?.regionalWebhook ? endpoint.region : "global";
|
|
87
|
+
const triggerBody = {
|
|
88
|
+
cloudFunction: {
|
|
89
|
+
id: endpoint.id,
|
|
90
|
+
locationId: endpoint.region,
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
try {
|
|
94
|
+
return await createTrigger(endpoint.project, location, triggerId, triggerBody);
|
|
95
|
+
}
|
|
96
|
+
catch (err) {
|
|
97
|
+
if (err && typeof err === "object" && "status" in err && err.status === 409) {
|
|
98
|
+
return await updateTrigger(endpoint.project, location, triggerId, triggerBody);
|
|
99
|
+
}
|
|
100
|
+
throw err;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
async function deleteBlockingFunction(endpoint) {
|
|
104
|
+
const eventType = endpoint.blockingTrigger.eventType;
|
|
105
|
+
const triggerId = exports.AI_LOGIC_EVENTS_TO_TRIGGER[eventType];
|
|
106
|
+
const location = endpoint.blockingTrigger.options?.regionalWebhook ? endpoint.region : "global";
|
|
107
|
+
await deleteTrigger(endpoint.project, location, triggerId, true);
|
|
108
|
+
}
|
|
@@ -21,6 +21,8 @@ const utils = require("../utils");
|
|
|
21
21
|
const projectConfig = require("../functions/projectConfig");
|
|
22
22
|
const constants_1 = require("../functions/constants");
|
|
23
23
|
const cloudfunctions_1 = require("./cloudfunctions");
|
|
24
|
+
const ailogic_1 = require("./ailogic");
|
|
25
|
+
const ailogic_2 = require("../deploy/functions/services/ailogic");
|
|
24
26
|
const k8s_1 = require("./k8s");
|
|
25
27
|
exports.API_VERSION = "v2";
|
|
26
28
|
const DEFAULT_MAX_INSTANCE_COUNT = 100;
|
|
@@ -263,6 +265,13 @@ function functionFromEndpoint(endpoint) {
|
|
|
263
265
|
else if (backend.isDataConnectGraphqlTriggered(endpoint)) {
|
|
264
266
|
gcfFunction.labels = { ...gcfFunction.labels, "deployment-fdcgraphql": "true" };
|
|
265
267
|
}
|
|
268
|
+
else if ((0, ailogic_2.isAILogicEvent)(endpoint)) {
|
|
269
|
+
gcfFunction.labels = {
|
|
270
|
+
...gcfFunction.labels,
|
|
271
|
+
"ailogic-event-type": ailogic_1.AI_LOGIC_EVENTS_TO_TRIGGER[endpoint.blockingTrigger.eventType],
|
|
272
|
+
"ailogic-locality": endpoint.blockingTrigger.options?.regionalWebhook ? "regional" : "global",
|
|
273
|
+
};
|
|
274
|
+
}
|
|
266
275
|
else if (backend.isBlockingTriggered(endpoint)) {
|
|
267
276
|
gcfFunction.labels = {
|
|
268
277
|
...gcfFunction.labels,
|
|
@@ -310,6 +319,21 @@ function endpointFromFunction(gcfFunction) {
|
|
|
310
319
|
dataConnectGraphqlTrigger: {},
|
|
311
320
|
};
|
|
312
321
|
}
|
|
322
|
+
else if (gcfFunction.labels?.["ailogic-event-type"]) {
|
|
323
|
+
const triggerType = gcfFunction.labels["ailogic-event-type"];
|
|
324
|
+
const eventType = ailogic_1.AI_LOGIC_TRIGGERS_TO_EVENTS[triggerType];
|
|
325
|
+
if (!eventType) {
|
|
326
|
+
throw new error_1.FirebaseError(`Unrecognized ailogic-event-type label: ${triggerType}`);
|
|
327
|
+
}
|
|
328
|
+
trigger = {
|
|
329
|
+
blockingTrigger: {
|
|
330
|
+
eventType,
|
|
331
|
+
options: {
|
|
332
|
+
regionalWebhook: gcfFunction.labels["ailogic-locality"] === "regional",
|
|
333
|
+
},
|
|
334
|
+
},
|
|
335
|
+
};
|
|
336
|
+
}
|
|
313
337
|
else if (gcfFunction.labels?.[constants_1.BLOCKING_LABEL]) {
|
|
314
338
|
trigger = {
|
|
315
339
|
blockingTrigger: {
|