firebase-tools 15.10.0 → 15.11.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 (32) hide show
  1. package/lib/appdistribution/client.js +17 -0
  2. package/lib/apphosting/utils.js +14 -0
  3. package/lib/apptesting/parseTestFiles.js +25 -13
  4. package/lib/commands/apptesting.js +35 -16
  5. package/lib/commands/index.js +5 -5
  6. package/lib/commands/studio-export.js +2 -2
  7. package/lib/deploy/apphosting/util.js +1 -1
  8. package/lib/deploy/firestore/prepare.js +17 -0
  9. package/lib/deploy/functions/prepareFunctionsUpload.js +1 -1
  10. package/lib/deploy/functions/runtimes/dart/index.js +282 -0
  11. package/lib/deploy/functions/runtimes/index.js +1 -1
  12. package/lib/deploy/functions/runtimes/supported/index.js +4 -0
  13. package/lib/emulator/apphosting/serve.js +13 -15
  14. package/lib/emulator/downloadableEmulatorInfo.json +37 -37
  15. package/lib/emulator/functionsEmulator.js +103 -24
  16. package/lib/emulator/functionsRuntimeWorker.js +21 -18
  17. package/lib/firebase_studio/migrate.js +274 -95
  18. package/lib/init/features/functions/dart.js +31 -0
  19. package/lib/init/features/functions/index.js +14 -0
  20. package/lib/mcp/index.js +18 -6
  21. package/lib/tsconfig.publish.tsbuildinfo +1 -1
  22. package/package.json +1 -1
  23. package/schema/firebase-config.json +7 -0
  24. package/templates/firebase-studio-export/readme_template.md +13 -7
  25. package/templates/firebase-studio-export/system_instructions_template.md +14 -0
  26. package/templates/firebase-studio-export/workflows/cleanup.md +20 -0
  27. package/templates/init/apphosting/apphosting.yaml +1 -0
  28. package/templates/init/functions/dart/_gitignore +11 -0
  29. package/templates/init/functions/dart/pubspec.yaml +14 -0
  30. package/templates/init/functions/dart/server.dart +15 -0
  31. package/lib/deploy/functions/runtimes/dart.js +0 -42
  32. package/templates/firebase-studio-export/workflows/startup_workflow.md +0 -12
@@ -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,7 @@ 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) {
20
+ async function setupAntigravityMcpServer(rootPath, appType) {
20
21
  const mcpConfigDir = path.join(os.homedir(), ".gemini", "antigravity");
21
22
  const mcpConfigPath = path.join(mcpConfigDir, "mcp_config.json");
22
23
  let mcpConfig = { mcpServers: {} };
@@ -36,16 +37,56 @@ async function setupAntigravityMcpServer(rootPath) {
36
37
  mcpConfig.mcpServers = {};
37
38
  }
38
39
  }
39
- if (mcpConfig.mcpServers["firebase"]) {
40
+ let updated = false;
41
+ if (!mcpConfig.mcpServers["firebase"]) {
42
+ if (utils.commandExistsSync("npx")) {
43
+ const confirmFirebase = await prompt.confirm({
44
+ message: "Would you like to enable the Firebase MCP server for Antigravity?",
45
+ default: true,
46
+ });
47
+ if (confirmFirebase) {
48
+ mcpConfig.mcpServers["firebase"] = {
49
+ command: "npx",
50
+ args: ["-y", "firebase-tools@latest", "mcp", "--dir", path.resolve(rootPath)],
51
+ };
52
+ updated = true;
53
+ logger_1.logger.info(`✅ Configured Firebase MCP server in ${mcpConfigPath}`);
54
+ }
55
+ }
56
+ else {
57
+ logger_1.logger.info("ℹ️ npx not found on PATH, skipping Firebase MCP server configuration.");
58
+ }
59
+ }
60
+ else {
40
61
  logger_1.logger.info("ℹ️ Firebase MCP server already configured in Antigravity, skipping.");
41
- return;
42
62
  }
43
- mcpConfig.mcpServers["firebase"] = {
44
- command: "npx",
45
- args: ["-y", "firebase-tools@latest", "mcp", "--dir", path.resolve(rootPath)],
46
- };
47
- await fs.writeFile(mcpConfigPath, JSON.stringify(mcpConfig, null, 2));
48
- logger_1.logger.info(`✅ Configured Firebase MCP server in ${mcpConfigPath}`);
63
+ if (appType === "FLUTTER") {
64
+ if (utils.commandExistsSync("dart")) {
65
+ if (!mcpConfig.mcpServers["dart"]) {
66
+ const confirmDart = await prompt.confirm({
67
+ message: "Would you like to enable the Dart MCP server for Antigravity?",
68
+ default: true,
69
+ });
70
+ if (confirmDart) {
71
+ mcpConfig.mcpServers["dart"] = {
72
+ command: "dart",
73
+ args: ["mcp-server"],
74
+ };
75
+ updated = true;
76
+ logger_1.logger.info(`✅ Configured Dart MCP server in ${mcpConfigPath}`);
77
+ }
78
+ }
79
+ else {
80
+ logger_1.logger.info("ℹ️ Dart MCP server already configured in Antigravity, skipping.");
81
+ }
82
+ }
83
+ else {
84
+ utils.logWarning("Couldn't find Dart/Flutter on PATH. Install Flutter by following the instruction at https://docs.flutter.dev/install.");
85
+ }
86
+ }
87
+ if (updated) {
88
+ await fs.writeFile(mcpConfigPath, JSON.stringify(mcpConfig, null, 2));
89
+ }
49
90
  }
50
91
  catch (err) {
51
92
  const message = err instanceof Error ? err.message : String(err);
@@ -89,40 +130,23 @@ async function detectAppType(rootPath) {
89
130
  }
90
131
  return "OTHER";
91
132
  }
92
- async function downloadGitHubDir(apiUrl, localPath) {
93
- const response = await fetch(apiUrl);
94
- if (!response.ok) {
95
- throw new Error(`Failed to fetch directory listing: ${apiUrl}`);
96
- }
97
- const items = (await response.json());
98
- await fs.mkdir(localPath, { recursive: true });
99
- for (const item of items) {
100
- const itemLocalPath = path.join(localPath, item.name);
101
- if (item.type === "dir") {
102
- await downloadGitHubDir(item.url, itemLocalPath);
103
- }
104
- else if (item.type === "file") {
105
- const fileResponse = await fetch(item.download_url);
106
- if (fileResponse.ok) {
107
- const content = await fileResponse.arrayBuffer();
108
- await fs.writeFile(itemLocalPath, Buffer.from(content));
109
- }
110
- }
111
- }
112
- }
113
133
  const isValidFirebaseProjectId = (projectId) => {
114
134
  const projectIdRegex = /^[a-z][a-z0-9-]{4,28}[a-z0-9]$/;
115
135
  return projectIdRegex.test(projectId);
116
136
  };
117
137
  async function extractMetadata(rootPath, overrideProjectId) {
138
+ const studioJsonPath = path.join(rootPath, "studio.json");
118
139
  const metadataPath = path.join(rootPath, "metadata.json");
119
140
  let metadata = {};
120
- try {
121
- const metadataContent = await fs.readFile(metadataPath, "utf8");
122
- metadata = JSON.parse(metadataContent);
123
- }
124
- catch (err) {
125
- logger_1.logger.debug(`Could not read metadata.json at ${metadataPath}: ${err}`);
141
+ for (const metadataFile of [metadataPath, studioJsonPath]) {
142
+ try {
143
+ const metadataContent = await fs.readFile(metadataFile, "utf8");
144
+ metadata = JSON.parse(metadataContent);
145
+ logger_1.logger.info(`✅ Read ${metadataFile}`);
146
+ }
147
+ catch (err) {
148
+ logger_1.logger.debug(`Could not read metadata at ${metadataFile}: ${err}`);
149
+ }
126
150
  }
127
151
  logger_1.logger.debug(`overrideProjectId ${overrideProjectId}`);
128
152
  logger_1.logger.debug(`metadata.projectId ${metadata.projectId}`);
@@ -146,14 +170,13 @@ async function extractMetadata(rootPath, overrideProjectId) {
146
170
  logger_1.logger.info(`✅ Using Firebase Project: ${projectId}`);
147
171
  }
148
172
  else {
149
- logger_1.logger.info(`❌ Failed to determine the Firebase Project ID. You can set a project later with 'firebase use <project-id>' or by setting the '--project' flag.`);
173
+ logger_1.logger.debug(`❌ Failed to determine the Firebase Project ID. You can set a project later by setting the '--project' flag.`);
150
174
  }
151
175
  let appName = "firebase-studio-export";
152
- let blueprintContent = "";
153
176
  const blueprintPath = path.join(rootPath, "docs", "blueprint.md");
154
177
  try {
155
- blueprintContent = await fs.readFile(blueprintPath, "utf8");
156
- const nameMatch = blueprintContent.match(/# \*\*App Name\*\*: (.*)/);
178
+ const content = await fs.readFile(blueprintPath, "utf8");
179
+ const nameMatch = content.match(/# \*\*App Name\*\*: (.*)/);
157
180
  if (nameMatch && nameMatch[1]) {
158
181
  appName = nameMatch[1].trim();
159
182
  }
@@ -164,15 +187,34 @@ async function extractMetadata(rootPath, overrideProjectId) {
164
187
  if (appName !== "firebase-studio-export") {
165
188
  logger_1.logger.info(`✅ Detected App Name: ${appName}`);
166
189
  }
167
- return { projectId, appName, blueprintContent };
190
+ return { projectId, appName };
168
191
  }
169
- async function updateReadme(rootPath, blueprintContent, appName) {
192
+ async function updateReadme(rootPath, framework) {
170
193
  const readmePath = path.join(rootPath, "README.md");
171
194
  const readmeTemplate = await (0, templates_1.readTemplate)("firebase-studio-export/readme_template.md");
172
- const newReadme = readmeTemplate
173
- .replace(/\${appName}/g, appName)
195
+ const frameworkConfigs = {
196
+ NEXT_JS: { startCommand: "npm run dev", localUrl: "http://localhost:9002" },
197
+ ANGULAR: { startCommand: "npm run start", localUrl: "http://localhost:4200" },
198
+ FLUTTER: {
199
+ startCommand: "flutter run -d chrome --web-port=8080",
200
+ localUrl: "http://localhost:8080",
201
+ },
202
+ OTHER: { startCommand: "npm run dev", localUrl: "http://localhost:9002" },
203
+ };
204
+ const { startCommand, localUrl } = frameworkConfigs[framework];
205
+ let existingReadme = "";
206
+ try {
207
+ existingReadme = await fs.readFile(readmePath, "utf8");
208
+ }
209
+ catch (err) {
210
+ }
211
+ let newReadme = readmeTemplate
174
212
  .replace("${exportDate}", new Date().toISOString().split("T")[0])
175
- .replace("${blueprintContent}", blueprintContent.replace(/# \*\*App Name\*\*: .*/, "").trim());
213
+ .replace("${startCommand}", startCommand)
214
+ .replace("${localUrl}", localUrl);
215
+ if (existingReadme.trim()) {
216
+ newReadme += `\n\n---\n\n## Previous README.md contents:\n\n${existingReadme}`;
217
+ }
176
218
  await fs.writeFile(readmePath, newReadme);
177
219
  logger_1.logger.info("✅ Updated README.md with project details and origin info");
178
220
  }
@@ -184,39 +226,54 @@ async function injectAntigravityContext(rootPath, projectId, appName) {
184
226
  await fs.mkdir(rulesDir, { recursive: true });
185
227
  await fs.mkdir(workflowsDir, { recursive: true });
186
228
  await fs.mkdir(skillsDir, { recursive: true });
187
- logger_1.logger.info("⏳ Fetching Antigravity skills from firebase/agent-skills...");
229
+ const installLocation = await prompt.select({
230
+ message: "Where would you like to install Firebase project skills?",
231
+ choices: [
232
+ { name: "Locally in the project", value: "local" },
233
+ { name: "Globally for all projects", value: "global" },
234
+ ],
235
+ default: "local",
236
+ nonInteractive: process.env.NODE_ENV === "test",
237
+ });
238
+ logger_1.logger.info("⏳ Adding Antigravity skills...");
188
239
  try {
189
- const skillsResponse = await fetch("https://api.github.com/repos/firebase/agent-skills/contents/skills");
190
- if (!skillsResponse.ok) {
191
- throw new Error(`GitHub API returned ${skillsResponse.status}`);
192
- }
193
- const skillsData = (await skillsResponse.json());
194
- if (Array.isArray(skillsData)) {
195
- for (const item of skillsData) {
196
- if (item.type === "dir") {
197
- const skillName = item.name;
198
- const skillDir = path.join(skillsDir, skillName);
199
- await downloadGitHubDir(item.url, skillDir);
200
- }
201
- }
240
+ const args = [
241
+ "-y",
242
+ "skills",
243
+ "add",
244
+ "firebase/agent-skills",
245
+ "-a",
246
+ "gemini-cli",
247
+ "--skill",
248
+ "*",
249
+ "-y",
250
+ ];
251
+ if (installLocation === "global") {
252
+ args.push("-g");
202
253
  }
203
- else {
204
- utils.logWarning("GitHub API response for skills is not an array.");
254
+ const result = (0, child_process_1.spawnSync)("npx", args, {
255
+ cwd: rootPath,
256
+ stdio: "ignore",
257
+ shell: process.platform === "win32",
258
+ });
259
+ if (result.error) {
260
+ throw result.error;
261
+ }
262
+ if (result.status !== 0) {
263
+ throw new Error(`npx skills add exited with code ${result.status}`);
205
264
  }
206
- logger_1.logger.info(`✅ Downloaded Firebase skills`);
265
+ logger_1.logger.info(`✅ Added Antigravity skills`);
207
266
  }
208
267
  catch (err) {
209
- utils.logWarning(`Could not download Antigravity skills, skipping. ${err}`);
268
+ utils.logWarning(`Could not add Antigravity skills, skipping. ${err}`);
210
269
  }
211
270
  const systemInstructionsTemplate = await (0, templates_1.readTemplate)("firebase-studio-export/system_instructions_template.md");
212
- const systemInstructions = systemInstructionsTemplate
213
- .replace("${projectId}", projectId || "None")
214
- .replace("${appName}", appName);
271
+ const systemInstructions = systemInstructionsTemplate.replace("${appName}", appName);
215
272
  await fs.writeFile(path.join(rulesDir, "migration-context.md"), systemInstructions);
216
273
  logger_1.logger.info("✅ Injected Antigravity rules");
217
274
  try {
218
- const startupWorkflow = await (0, templates_1.readTemplate)("firebase-studio-export/workflows/startup_workflow.md");
219
- await fs.writeFile(path.join(workflowsDir, "startup.md"), startupWorkflow);
275
+ const cleanupWorkflow = await (0, templates_1.readTemplate)("firebase-studio-export/workflows/cleanup.md");
276
+ await fs.writeFile(path.join(workflowsDir, "cleanup.md"), cleanupWorkflow);
220
277
  logger_1.logger.info("✅ Created Antigravity startup workflow");
221
278
  }
222
279
  catch (err) {
@@ -282,12 +339,35 @@ async function createFirebaseConfigs(rootPath, projectId) {
282
339
  const backendsData = await apphosting.listBackends(projectId, "-");
283
340
  const backends = backendsData.backends || [];
284
341
  if (backends.length > 0) {
342
+ const backendIds = backends.map((b) => b.name.split("/").pop());
285
343
  const studioBackend = backends.find((b) => b.name.endsWith("/studio") || b.name.toLowerCase().includes("studio"));
344
+ let selectedBackendId = "";
286
345
  if (studioBackend) {
287
- backendId = studioBackend.name.split("/").pop();
346
+ selectedBackendId = studioBackend.name.split("/").pop();
288
347
  }
289
348
  else {
290
- backendId = backends[0].name.split("/").pop();
349
+ selectedBackendId = backendIds[0];
350
+ }
351
+ const confirmBackend = await prompt.confirm({
352
+ message: `Would you like to use the App Hosting backend "${selectedBackendId}"?`,
353
+ default: true,
354
+ nonInteractive: process.env.NODE_ENV === "test",
355
+ });
356
+ if (confirmBackend) {
357
+ backendId = selectedBackendId;
358
+ }
359
+ else {
360
+ logger_1.logger.info("Available App Hosting backends:");
361
+ for (const id of backendIds) {
362
+ logger_1.logger.info(` - ${id}`);
363
+ }
364
+ const inputBackendId = await prompt.input({
365
+ message: "Please enter the name of the backend you would like to use:",
366
+ });
367
+ if (!backendIds.includes(inputBackendId)) {
368
+ throw new error_1.FirebaseError(`Invalid backend selected: ${inputBackendId}`, { exit: 1 });
369
+ }
370
+ backendId = inputBackendId;
291
371
  }
292
372
  logger_1.logger.info(`✅ Selected App Hosting backend: ${backendId}`);
293
373
  }
@@ -317,20 +397,33 @@ async function createFirebaseConfigs(rootPath, projectId) {
317
397
  logger_1.logger.info(`✅ Created firebase.json with backendId: ${backendId}`);
318
398
  }
319
399
  }
320
- async function writeAntigravityConfigs(rootPath) {
400
+ async function writeAntigravityConfigs(rootPath, framework) {
321
401
  const vscodeDir = path.join(rootPath, ".vscode");
322
402
  await fs.mkdir(vscodeDir, { recursive: true });
323
403
  const tasksJson = {
324
404
  version: "2.0.0",
325
- tasks: [
326
- {
327
- label: "npm-install",
328
- type: "shell",
329
- command: "npm install",
330
- problemMatcher: [],
331
- },
332
- ],
405
+ tasks: [],
333
406
  };
407
+ if (framework === "FLUTTER") {
408
+ tasksJson.tasks.push({
409
+ label: "flutter-pub-get",
410
+ type: "shell",
411
+ command: "flutter pub get",
412
+ problemMatcher: [],
413
+ group: {
414
+ kind: "build",
415
+ isDefault: true,
416
+ },
417
+ });
418
+ }
419
+ else {
420
+ tasksJson.tasks.push({
421
+ label: "npm-install",
422
+ type: "shell",
423
+ command: "npm install",
424
+ problemMatcher: [],
425
+ });
426
+ }
334
427
  await fs.writeFile(path.join(vscodeDir, "tasks.json"), JSON.stringify(tasksJson, null, 2));
335
428
  logger_1.logger.info("✅ Created .vscode/tasks.json");
336
429
  const settingsPath = path.join(vscodeDir, "settings.json");
@@ -354,19 +447,43 @@ async function writeAntigravityConfigs(rootPath) {
354
447
  logger_1.logger.info("✅ Updated .vscode/settings.json with startup preferences");
355
448
  const launchJson = {
356
449
  version: "0.2.0",
357
- configurations: [
358
- {
359
- type: "node",
360
- request: "launch",
361
- name: "Next.js: debug server-side",
362
- runtimeExecutable: "npm",
363
- runtimeArgs: ["run", "dev"],
364
- port: 9002,
365
- console: "integratedTerminal",
366
- preLaunchTask: "npm-install",
367
- },
368
- ],
450
+ configurations: [],
369
451
  };
452
+ if (framework === "ANGULAR") {
453
+ launchJson.configurations.push({
454
+ type: "node",
455
+ request: "launch",
456
+ name: "Angular: debug server-side",
457
+ runtimeExecutable: "npm",
458
+ runtimeArgs: ["run", "start"],
459
+ port: 4200,
460
+ console: "integratedTerminal",
461
+ preLaunchTask: "npm-install",
462
+ });
463
+ }
464
+ else if (framework === "NEXT_JS") {
465
+ launchJson.configurations.push({
466
+ type: "node",
467
+ request: "launch",
468
+ name: "Next.js: debug server-side",
469
+ runtimeExecutable: "npm",
470
+ runtimeArgs: ["run", "dev"],
471
+ port: 9002,
472
+ console: "integratedTerminal",
473
+ preLaunchTask: "npm-install",
474
+ });
475
+ }
476
+ else if (framework === "FLUTTER") {
477
+ launchJson.configurations.push({
478
+ name: "Flutter",
479
+ request: "launch",
480
+ type: "dart",
481
+ preLaunchTask: "flutter-pub-get",
482
+ });
483
+ }
484
+ else {
485
+ return;
486
+ }
370
487
  await fs.writeFile(path.join(vscodeDir, "launch.json"), JSON.stringify(launchJson, null, 2));
371
488
  logger_1.logger.info("✅ Created .vscode/launch.json");
372
489
  }
@@ -392,6 +509,48 @@ async function cleanupUnusedFiles(rootPath) {
392
509
  const message = err instanceof Error ? err.message : String(err);
393
510
  logger_1.logger.debug(`Could not delete ${modifiedPath}: ${message}`);
394
511
  }
512
+ const mcpJsonPath = path.join(rootPath, ".idx", "mcp.json");
513
+ try {
514
+ await fs.unlink(mcpJsonPath);
515
+ logger_1.logger.info("✅ Cleaned up .idx/mcp.json");
516
+ }
517
+ catch (err) {
518
+ const message = err instanceof Error ? err.message : String(err);
519
+ logger_1.logger.debug(`Could not delete ${mcpJsonPath}: ${message}`);
520
+ }
521
+ }
522
+ async function upgradeGenkitVersion(rootPath) {
523
+ const packageJsonPath = path.join(rootPath, "package.json");
524
+ try {
525
+ const packageJsonContent = await fs.readFile(packageJsonPath, "utf8");
526
+ const packageJson = JSON.parse(packageJsonContent);
527
+ let modified = false;
528
+ const targetVersion = "1.29.0";
529
+ const checkAndUpgrade = (deps) => {
530
+ if (!deps || !deps["genkit-cli"]) {
531
+ return;
532
+ }
533
+ const currentVersion = deps["genkit-cli"];
534
+ if (currentVersion.startsWith("^")) {
535
+ return;
536
+ }
537
+ const coerced = semver.coerce(currentVersion);
538
+ if (coerced && semver.lt(coerced, targetVersion)) {
539
+ deps["genkit-cli"] = "^1.29";
540
+ modified = true;
541
+ }
542
+ };
543
+ checkAndUpgrade(packageJson.dependencies);
544
+ checkAndUpgrade(packageJson.devDependencies);
545
+ if (modified) {
546
+ await fs.writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2) + "\n");
547
+ logger_1.logger.info("✅ Upgraded genkit-cli version to 1.29 in package.json");
548
+ }
549
+ }
550
+ catch (err) {
551
+ const message = err instanceof Error ? err.message : String(err);
552
+ logger_1.logger.debug(`Could not upgrade Genkit version: ${message}`);
553
+ }
395
554
  }
396
555
  async function uploadSecrets(rootPath, projectId) {
397
556
  if (!projectId) {
@@ -453,17 +612,37 @@ async function askToOpenAntigravity(rootPath, appName, startAntigravity) {
453
612
  }
454
613
  }
455
614
  }
615
+ async function checkDirectoryExists(dir) {
616
+ try {
617
+ const stat = await fs.stat(dir);
618
+ if (!stat.isDirectory()) {
619
+ throw new error_1.FirebaseError(`The path ${dir} is not a directory.`, { exit: 1 });
620
+ }
621
+ }
622
+ catch (err) {
623
+ if (err.code === "ENOENT") {
624
+ throw new error_1.FirebaseError(`The directory ${dir} does not exist.`, { exit: 1 });
625
+ }
626
+ throw err;
627
+ }
628
+ }
456
629
  async function migrate(rootPath, options = { startAntigravity: true }) {
630
+ await checkDirectoryExists(rootPath);
457
631
  const appType = await detectAppType(rootPath);
458
632
  void track.trackGA4("firebase_studio_migrate", { app_type: appType, result: "started" });
459
633
  logger_1.logger.info("🚀 Starting Firebase Studio to Antigravity migration...");
460
- const { projectId, appName, blueprintContent } = await extractMetadata(rootPath, options.project);
461
- await updateReadme(rootPath, blueprintContent, appName);
634
+ logger_1.logger.info("\nFile any bugs at https://github.com/firebase/firebase-tools/issues");
635
+ const { projectId, appName } = await extractMetadata(rootPath, options.project);
636
+ if (appType) {
637
+ logger_1.logger.info(`✅ Detected framework: ${appType}`);
638
+ }
639
+ await updateReadme(rootPath, appType);
462
640
  await createFirebaseConfigs(rootPath, projectId);
463
641
  await uploadSecrets(rootPath, projectId);
642
+ await upgradeGenkitVersion(rootPath);
464
643
  await injectAntigravityContext(rootPath, projectId, appName);
465
- await writeAntigravityConfigs(rootPath);
466
- await setupAntigravityMcpServer(rootPath);
644
+ await writeAntigravityConfigs(rootPath, appType);
645
+ await setupAntigravityMcpServer(rootPath, appType);
467
646
  await cleanupUnusedFiles(rootPath);
468
647
  const currentFolderName = path.basename(rootPath);
469
648
  if (currentFolderName === "download") {
@@ -0,0 +1,31 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.setup = setup;
4
+ const spawn = require("cross-spawn");
5
+ const prompt_1 = require("../../../prompt");
6
+ const supported_1 = require("../../../deploy/functions/runtimes/supported");
7
+ const templates_1 = require("../../../templates");
8
+ const PUBSPEC_TEMPLATE = (0, templates_1.readTemplateSync)("init/functions/dart/pubspec.yaml");
9
+ const MAIN_TEMPLATE = (0, templates_1.readTemplateSync)("init/functions/dart/server.dart");
10
+ const GITIGNORE_TEMPLATE = (0, templates_1.readTemplateSync)("init/functions/dart/_gitignore");
11
+ async function setup(setup, config) {
12
+ await config.askWriteProjectFile(`${setup.functions.source}/pubspec.yaml`, PUBSPEC_TEMPLATE);
13
+ await config.askWriteProjectFile(`${setup.functions.source}/.gitignore`, GITIGNORE_TEMPLATE);
14
+ await config.askWriteProjectFile(`${setup.functions.source}/bin/server.dart`, MAIN_TEMPLATE);
15
+ config.set("functions.runtime", (0, supported_1.latest)("dart"));
16
+ config.set("functions.ignore", [".dart_tool", "build"]);
17
+ const install = await (0, prompt_1.confirm)({
18
+ message: "Do you want to install dependencies now?",
19
+ default: true,
20
+ });
21
+ if (install) {
22
+ const installProcess = spawn("dart", ["pub", "get"], {
23
+ cwd: config.path(setup.functions.source),
24
+ stdio: ["inherit", "inherit", "inherit"],
25
+ });
26
+ await new Promise((resolve, reject) => {
27
+ installProcess.on("exit", resolve);
28
+ installProcess.on("error", reject);
29
+ });
30
+ }
31
+ }
@@ -144,6 +144,10 @@ async function languageSetup(setup) {
144
144
  value: "python",
145
145
  });
146
146
  }
147
+ choices.push({
148
+ name: "Dart",
149
+ value: "dart",
150
+ });
147
151
  const language = await (0, prompt_1.select)({
148
152
  message: "What language would you like to use to write Cloud Functions?",
149
153
  default: "javascript",
@@ -173,6 +177,16 @@ async function languageSetup(setup) {
173
177
  cbconfig.ignore = ["venv", ".git", "firebase-debug.log", "firebase-debug.*.log", "*.local"];
174
178
  cbconfig.runtime = supported.latest("python");
175
179
  break;
180
+ case "dart":
181
+ cbconfig.ignore = [
182
+ ".dart_tool",
183
+ ".git",
184
+ "firebase-debug.log",
185
+ "firebase-debug.*.log",
186
+ "*.local",
187
+ ];
188
+ cbconfig.runtime = supported.latest("dart");
189
+ break;
176
190
  }
177
191
  setup.functions.languageChoice = language;
178
192
  }
package/lib/mcp/index.js CHANGED
@@ -135,7 +135,7 @@ class FirebaseMcpServer {
135
135
  this.logger.debug("detecting active features of Firebase MCP server...");
136
136
  const projectId = (await this.getProjectId()) || "";
137
137
  const accountEmail = await this.getAuthenticatedUser();
138
- const isBillingEnabled = projectId ? await (0, cloudbilling_1.checkBillingEnabled)(projectId) : false;
138
+ const isBillingEnabled = projectId ? await this.safeCheckBillingEnabled(projectId) : false;
139
139
  const ctx = this._createMcpContext(projectId, accountEmail, isBillingEnabled);
140
140
  const detected = await Promise.all(types_1.SERVER_FEATURES.map(async (f) => {
141
141
  const availabilityCheck = (0, availability_1.getDefaultFeatureAvailabilityCheck)(f);
@@ -171,7 +171,7 @@ class FirebaseMcpServer {
171
171
  async getAvailableTools() {
172
172
  const projectId = (await this.getProjectId()) || "";
173
173
  const accountEmail = await this.getAuthenticatedUser();
174
- const isBillingEnabled = projectId ? await (0, cloudbilling_1.checkBillingEnabled)(projectId) : false;
174
+ const isBillingEnabled = projectId ? await this.safeCheckBillingEnabled(projectId) : false;
175
175
  const ctx = this._createMcpContext(projectId, accountEmail, isBillingEnabled);
176
176
  return (0, index_2.availableTools)(ctx, this.activeFeatures, this.detectedFeatures, this.enabledTools);
177
177
  }
@@ -182,7 +182,7 @@ class FirebaseMcpServer {
182
182
  async getAvailablePrompts() {
183
183
  const projectId = (await this.getProjectId()) || "";
184
184
  const accountEmail = await this.getAuthenticatedUser();
185
- const isBillingEnabled = projectId ? await (0, cloudbilling_1.checkBillingEnabled)(projectId) : false;
185
+ const isBillingEnabled = projectId ? await this.safeCheckBillingEnabled(projectId) : false;
186
186
  const ctx = this._createMcpContext(projectId, accountEmail, isBillingEnabled);
187
187
  return (0, index_1.availablePrompts)(ctx, this.activeFeatures, this.detectedFeatures);
188
188
  }
@@ -276,7 +276,7 @@ class FirebaseMcpServer {
276
276
  if (tool.mcp._meta?.requiresAuth && !accountEmail) {
277
277
  return (0, errors_1.mcpAuthError)(skipAutoAuthForStudio);
278
278
  }
279
- const isBillingEnabled = projectId ? await (0, cloudbilling_1.checkBillingEnabled)(projectId) : false;
279
+ const isBillingEnabled = projectId ? await this.safeCheckBillingEnabled(projectId) : false;
280
280
  const toolsCtx = this._createMcpContext(projectId, accountEmail, isBillingEnabled);
281
281
  try {
282
282
  const res = await tool.fn(toolArgs, toolsCtx);
@@ -327,7 +327,7 @@ class FirebaseMcpServer {
327
327
  projectId = projectId || "";
328
328
  const skipAutoAuthForStudio = (0, env_1.isFirebaseStudio)();
329
329
  const accountEmail = await this.getAuthenticatedUser(skipAutoAuthForStudio);
330
- const isBillingEnabled = projectId ? await (0, cloudbilling_1.checkBillingEnabled)(projectId) : false;
330
+ const isBillingEnabled = projectId ? await this.safeCheckBillingEnabled(projectId) : false;
331
331
  const promptsCtx = this._createMcpContext(projectId, accountEmail, isBillingEnabled);
332
332
  try {
333
333
  const messages = await prompt.fn(promptArgs, promptsCtx);
@@ -362,7 +362,7 @@ class FirebaseMcpServer {
362
362
  projectId = projectId || "";
363
363
  const skipAutoAuthForStudio = (0, env_1.isFirebaseStudio)();
364
364
  const accountEmail = await this.getAuthenticatedUser(skipAutoAuthForStudio);
365
- const isBillingEnabled = projectId ? await (0, cloudbilling_1.checkBillingEnabled)(projectId) : false;
365
+ const isBillingEnabled = projectId ? await this.safeCheckBillingEnabled(projectId) : false;
366
366
  const resourceCtx = this._createMcpContext(projectId, accountEmail, isBillingEnabled);
367
367
  const resolved = await (0, resources_1.resolveResource)(req.params.uri, resourceCtx);
368
368
  if (!resolved) {
@@ -376,6 +376,18 @@ class FirebaseMcpServer {
376
376
  : new stdio_js_1.StdioServerTransport();
377
377
  await this.server.connect(transport);
378
378
  }
379
+ async safeCheckBillingEnabled(projectId) {
380
+ try {
381
+ return await (0, cloudbilling_1.checkBillingEnabled)(projectId);
382
+ }
383
+ catch (e) {
384
+ this.logger.debug("[mcp] Error on billingInfo for " +
385
+ projectId +
386
+ ", failing open (assuming false): " +
387
+ (e.message || e));
388
+ return false;
389
+ }
390
+ }
379
391
  get logger() {
380
392
  const logAtLevel = (level, message) => {
381
393
  let data = message;