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.
Files changed (54) hide show
  1. package/lib/agentSkills.js +70 -0
  2. package/lib/api.js +3 -1
  3. package/lib/appdistribution/client.js +17 -0
  4. package/lib/apphosting/backend.js +22 -3
  5. package/lib/apptesting/parseTestFiles.js +11 -8
  6. package/lib/bin/mcp.js +5 -1
  7. package/lib/commands/apphosting-backends-create.js +19 -2
  8. package/lib/commands/apphosting-backends-list.js +21 -5
  9. package/lib/commands/apptesting.js +16 -7
  10. package/lib/commands/functions-delete.js +1 -0
  11. package/lib/commands/functions-export.js +40 -0
  12. package/lib/commands/index.js +3 -0
  13. package/lib/commands/init.js +1 -0
  14. package/lib/commands/studio-export.js +2 -2
  15. package/lib/deploy/apphosting/deploy.js +11 -6
  16. package/lib/deploy/apphosting/prepare.js +21 -1
  17. package/lib/deploy/apphosting/release.js +2 -5
  18. package/lib/deploy/apphosting/util.js +45 -2
  19. package/lib/deploy/firestore/prepare.js +17 -0
  20. package/lib/deploy/functions/prepare.js +4 -1
  21. package/lib/deploy/functions/release/fabricator.js +4 -3
  22. package/lib/deploy/functions/release/index.js +5 -0
  23. package/lib/deploy/functions/runtimes/dart/index.js +282 -0
  24. package/lib/deploy/functions/runtimes/index.js +1 -1
  25. package/lib/deploy/functions/runtimes/supported/index.js +4 -0
  26. package/lib/deploy/functions/services/ailogic.js +68 -0
  27. package/lib/deploy/functions/services/index.js +4 -0
  28. package/lib/emulator/downloadableEmulatorInfo.json +30 -30
  29. package/lib/emulator/functionsEmulator.js +103 -24
  30. package/lib/emulator/functionsRuntimeWorker.js +21 -18
  31. package/lib/emulator/storage/rules/manager.js +10 -3
  32. package/lib/emulator/storage/rules/runtime.js +9 -7
  33. package/lib/experiments.js +22 -0
  34. package/lib/firebase_studio/migrate.js +83 -70
  35. package/lib/functions/iac/export.js +36 -0
  36. package/lib/functions/iac/terraform.js +146 -0
  37. package/lib/gcp/ailogic.js +108 -0
  38. package/lib/gcp/cloudfunctionsv2.js +24 -0
  39. package/lib/init/features/agentSkills.js +26 -0
  40. package/lib/init/features/dataconnect/sdk.js +26 -12
  41. package/lib/init/features/functions/dart.js +31 -0
  42. package/lib/init/features/functions/index.js +14 -0
  43. package/lib/init/features/index.js +4 -1
  44. package/lib/init/index.js +6 -0
  45. package/lib/tsconfig.publish.tsbuildinfo +1 -1
  46. package/lib/utils.js +8 -0
  47. package/package.json +5 -3
  48. package/schema/firebase-config.json +7 -0
  49. package/standalone/package.json +1 -1
  50. package/templates/firebase-studio-export/readme_template.md +2 -0
  51. package/templates/init/functions/dart/_gitignore +11 -0
  52. package/templates/init/functions/dart/pubspec.yaml +14 -0
  53. package/templates/init/functions/dart/server.dart +15 -0
  54. 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
- async function setupAntigravityMcpServer(rootPath, appType) {
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
- mcpConfig.mcpServers["firebase"] = {
42
- command: "npx",
43
- args: ["-y", "firebase-tools@latest", "mcp", "--dir", path.resolve(rootPath)],
44
- };
45
- updated = true;
46
- logger_1.logger.info(`✅ Configured Firebase MCP server in ${mcpConfigPath}`);
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
- mcpConfig.mcpServers["dart"] = {
55
- command: "dart",
56
- args: ["mcp-server"],
57
- };
58
- updated = true;
59
- logger_1.logger.info(`✅ Configured Dart MCP server in ${mcpConfigPath}`);
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
- const message = err instanceof Error ? err.message : String(err);
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
- logger_1.logger.info("⏳ Adding Antigravity skills...");
212
- try {
213
- const result = (0, child_process_1.spawnSync)("npx", ["-y", "skills", "add", "firebase/agent-skills", "-a", "antigravity", "--skill", "*", "-y"], {
214
- cwd: rootPath,
215
- stdio: "ignore",
216
- shell: process.platform === "win32",
217
- });
218
- if (result.error) {
219
- throw result.error;
220
- }
221
- if (result.status !== 0) {
222
- throw new Error(`npx skills add exited with code ${result.status}`);
223
- }
224
- logger_1.logger.info(`✅ Added Antigravity skills`);
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
- const message = err instanceof Error ? err.message : String(err);
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
- const message = err instanceof Error ? err.message : String(err);
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
- const message = err instanceof Error ? err.message : String(err);
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
- const message = err instanceof Error ? err.message : String(err);
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
- const message = err instanceof Error ? err.message : String(err);
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
- const message = err instanceof Error ? err.message : String(err);
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 upgradeDeps = (deps) => {
488
- if (!deps) {
498
+ const targetVersion = "1.29.0";
499
+ const checkAndUpgrade = (deps) => {
500
+ if (!deps || !deps["genkit-cli"]) {
489
501
  return;
490
502
  }
491
- for (const [name, version] of Object.entries(deps)) {
492
- if (name === "genkit" || name === "genkit-cli" || name.startsWith("@genkit-ai/")) {
493
- if (version !== "1.29") {
494
- deps[name] = "1.29";
495
- modified = true;
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
- upgradeDeps(packageJson.dependencies);
501
- upgradeDeps(packageJson.devDependencies);
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 Genkit version to 1.29 in package.json");
517
+ logger_1.logger.info("✅ Upgraded genkit-cli version to 1.29 in package.json");
505
518
  }
506
519
  }
507
520
  catch (err) {
508
- const message = err instanceof Error ? err.message : String(err);
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
- const message = err instanceof Error ? err.message : String(err);
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
- void track.trackGA4("firebase_studio_migrate", { app_type: appType, result: "started" });
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: {