firebase-tools 15.8.0 → 15.9.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 (38) hide show
  1. package/lib/apiv2.js +13 -10
  2. package/lib/appdistribution/client.js +2 -2
  3. package/lib/appdistribution/yaml_helper.js +24 -16
  4. package/lib/apphosting/secrets/dialogs.js +3 -3
  5. package/lib/apphosting/secrets/index.js +59 -0
  6. package/lib/apptesting/parseTestFiles.js +2 -1
  7. package/lib/bin/mcp.js +2 -1
  8. package/lib/commands/apphosting-secrets-set.js +2 -53
  9. package/lib/commands/apptesting.js +2 -2
  10. package/lib/commands/studio-export.js +13 -4
  11. package/lib/dataconnect/schemaMigration.js +12 -1
  12. package/lib/deploy/apphosting/release.js +7 -1
  13. package/lib/deploy/extensions/prepare.js +13 -4
  14. package/lib/downloadUtils.js +3 -1
  15. package/lib/emulator/download.js +15 -1
  16. package/lib/emulator/downloadableEmulatorInfo.json +24 -24
  17. package/lib/emulator/downloadableEmulators.js +33 -6
  18. package/lib/env.js +6 -1
  19. package/lib/experiments.js +1 -1
  20. package/lib/firebase_studio/migrate.js +378 -0
  21. package/lib/functionsConfig.js +3 -3
  22. package/lib/gcp/cloudsql/cloudsqladmin.js +8 -3
  23. package/lib/gcp/cloudsql/fbToolsAuthClient.js +1 -1
  24. package/lib/mcp/prompts/apptesting/run_test.js +6 -5
  25. package/lib/mcp/tools/apptesting/tests.js +4 -4
  26. package/lib/mcp/tools/firestore/converter.js +101 -0
  27. package/lib/mcp/tools/firestore/index.js +5 -0
  28. package/lib/mcp/tools/firestore/query_collection.js +132 -0
  29. package/lib/mcp/tools/index.js +2 -1
  30. package/lib/tsconfig.publish.tsbuildinfo +1 -1
  31. package/package.json +1 -1
  32. package/templates/firebase-studio-export/readme_template.md +11 -0
  33. package/templates/firebase-studio-export/system_instructions_template.md +14 -0
  34. package/templates/firebase-studio-export/workflows/startup_workflow.md +16 -0
  35. package/templates/init/functions/javascript/package-ongraphrequest.lint.json +1 -1
  36. package/templates/init/functions/javascript/package-ongraphrequest.nolint.json +1 -1
  37. package/templates/init/functions/typescript/package-ongraphrequest.lint.json +1 -1
  38. package/templates/init/functions/typescript/package-ongraphrequest.nolint.json +1 -1
@@ -11,6 +11,7 @@ exports.stop = stop;
11
11
  exports.downloadIfNecessary = downloadIfNecessary;
12
12
  exports.start = start;
13
13
  exports.isIncomaptibleArchError = isIncomaptibleArchError;
14
+ exports.emulatorVersionOverride = emulatorVersionOverride;
14
15
  const lsofi = require("lsofi");
15
16
  const types_1 = require("./types");
16
17
  const constants_1 = require("./constants");
@@ -41,9 +42,11 @@ function generateDownloadDetails(emulator) {
41
42
  : process.platform === "win32"
42
43
  ? EMULATOR_UPDATE_DETAILS.dataconnect.win32
43
44
  : EMULATOR_UPDATE_DETAILS.dataconnect.linux;
45
+ let details;
46
+ const overrideVersion = emulatorVersionOverride(emulator);
44
47
  switch (emulator) {
45
48
  case "database":
46
- return {
49
+ details = {
47
50
  downloadPath: path.join(CACHE_DIR, EMULATOR_UPDATE_DETAILS.database.downloadPathRelativeToCacheDir),
48
51
  version: EMULATOR_UPDATE_DETAILS.database.version,
49
52
  opts: {
@@ -52,8 +55,9 @@ function generateDownloadDetails(emulator) {
52
55
  namePrefix: "firebase-database-emulator",
53
56
  },
54
57
  };
58
+ break;
55
59
  case "firestore":
56
- return {
60
+ details = {
57
61
  downloadPath: path.join(CACHE_DIR, EMULATOR_UPDATE_DETAILS.firestore.downloadPathRelativeToCacheDir),
58
62
  version: EMULATOR_UPDATE_DETAILS.firestore.version,
59
63
  opts: {
@@ -62,8 +66,9 @@ function generateDownloadDetails(emulator) {
62
66
  namePrefix: "cloud-firestore-emulator",
63
67
  },
64
68
  };
69
+ break;
65
70
  case "storage":
66
- return {
71
+ details = {
67
72
  downloadPath: path.join(CACHE_DIR, EMULATOR_UPDATE_DETAILS.storage.downloadPathRelativeToCacheDir),
68
73
  version: EMULATOR_UPDATE_DETAILS.storage.version,
69
74
  opts: {
@@ -72,8 +77,9 @@ function generateDownloadDetails(emulator) {
72
77
  namePrefix: "cloud-storage-rules-emulator",
73
78
  },
74
79
  };
80
+ break;
75
81
  case "ui":
76
- return {
82
+ details = {
77
83
  version: emulatorUiDetails.version,
78
84
  downloadPath: path.join(CACHE_DIR, emulatorUiDetails.downloadPathRelativeToCacheDir),
79
85
  unzipDir: path.join(CACHE_DIR, `ui-v${emulatorUiDetails.version}`),
@@ -86,8 +92,9 @@ function generateDownloadDetails(emulator) {
86
92
  namePrefix: "ui",
87
93
  },
88
94
  };
95
+ break;
89
96
  case "pubsub":
90
- return {
97
+ details = {
91
98
  downloadPath: path.join(CACHE_DIR, EMULATOR_UPDATE_DETAILS.pubsub.downloadPathRelativeToCacheDir),
92
99
  version: EMULATOR_UPDATE_DETAILS.pubsub.version,
93
100
  unzipDir: path.join(CACHE_DIR, `pubsub-emulator-${EMULATOR_UPDATE_DETAILS.pubsub.version}`),
@@ -98,8 +105,9 @@ function generateDownloadDetails(emulator) {
98
105
  namePrefix: "pubsub-emulator",
99
106
  },
100
107
  };
108
+ break;
101
109
  case "dataconnect":
102
- return {
110
+ details = {
103
111
  downloadPath: path.join(CACHE_DIR, dataconnectDetails.downloadPathRelativeToCacheDir),
104
112
  version: dataconnectDetails.version,
105
113
  binaryPath: path.join(CACHE_DIR, dataconnectDetails.downloadPathRelativeToCacheDir),
@@ -111,9 +119,25 @@ function generateDownloadDetails(emulator) {
111
119
  auth: false,
112
120
  },
113
121
  };
122
+ break;
114
123
  default:
115
124
  throw new Error(`Invalid downloadable emulator: ${emulator}`);
116
125
  }
126
+ if (overrideVersion && overrideVersion !== details.version) {
127
+ const oldVersion = details.version;
128
+ const replaceVersion = (s) => s.split(oldVersion).join(overrideVersion);
129
+ details.version = overrideVersion;
130
+ details.downloadPath = replaceVersion(details.downloadPath);
131
+ if (details.unzipDir) {
132
+ details.unzipDir = replaceVersion(details.unzipDir);
133
+ }
134
+ if (details.binaryPath) {
135
+ details.binaryPath = replaceVersion(details.binaryPath);
136
+ }
137
+ details.opts.remoteUrl = replaceVersion(details.opts.remoteUrl);
138
+ details.opts.skipChecksumAndSize = true;
139
+ }
140
+ return details;
117
141
  }
118
142
  const EmulatorDetails = {
119
143
  database: {
@@ -437,3 +461,6 @@ function isIncomaptibleArchError(err) {
437
461
  /Unknown system error/.test(err.message ?? "") &&
438
462
  process.platform === "darwin");
439
463
  }
464
+ function emulatorVersionOverride(emulator) {
465
+ return process.env[`${emulator.toUpperCase()}_EMULATOR_VERSION`];
466
+ }
package/lib/env.js CHANGED
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.isFirebaseStudio = isFirebaseStudio;
4
4
  exports.isFirebaseMcp = isFirebaseMcp;
5
+ exports.setFirebaseMcp = setFirebaseMcp;
5
6
  exports.detectAIAgent = detectAIAgent;
6
7
  const fsutils_1 = require("./fsutils");
7
8
  let googleIdxFolderExists;
@@ -13,8 +14,12 @@ function isFirebaseStudio() {
13
14
  googleIdxFolderExists = (0, fsutils_1.dirExistsSync)("/google/idx");
14
15
  return googleIdxFolderExists;
15
16
  }
17
+ let isFirebaseMcpFlag = false;
16
18
  function isFirebaseMcp() {
17
- return !!process.env.IS_FIREBASE_MCP;
19
+ return isFirebaseMcpFlag;
20
+ }
21
+ function setFirebaseMcp(value) {
22
+ isFirebaseMcpFlag = value;
18
23
  }
19
24
  function detectAIAgent() {
20
25
  if (process.env.ANTIGRAVITY_CLI_ALIAS)
@@ -142,7 +142,7 @@ exports.ALL_EXPERIMENTS = experiments({
142
142
  },
143
143
  fdcwebhooks: {
144
144
  shortDescription: "Enable Firebase Data Connect webhooks feature.",
145
- default: false,
145
+ default: true,
146
146
  public: false,
147
147
  },
148
148
  studioexport: {
@@ -0,0 +1,378 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.extractMetadata = extractMetadata;
4
+ exports.uploadSecrets = uploadSecrets;
5
+ exports.migrate = migrate;
6
+ const fs = require("fs/promises");
7
+ const path = require("path");
8
+ const child_process_1 = require("child_process");
9
+ const logger_1 = require("../logger");
10
+ const error_1 = require("../error");
11
+ const prompt = require("../prompt");
12
+ const apphosting = require("../gcp/apphosting");
13
+ const utils = require("../utils");
14
+ const templates_1 = require("../templates");
15
+ const secrets_1 = require("../apphosting/secrets");
16
+ const env = require("../functions/env");
17
+ async function downloadGitHubDir(apiUrl, localPath) {
18
+ const response = await fetch(apiUrl);
19
+ if (!response.ok) {
20
+ throw new Error(`Failed to fetch directory listing: ${apiUrl}`);
21
+ }
22
+ const items = (await response.json());
23
+ await fs.mkdir(localPath, { recursive: true });
24
+ for (const item of items) {
25
+ const itemLocalPath = path.join(localPath, item.name);
26
+ if (item.type === "dir") {
27
+ await downloadGitHubDir(item.url, itemLocalPath);
28
+ }
29
+ else if (item.type === "file") {
30
+ const fileResponse = await fetch(item.download_url);
31
+ if (fileResponse.ok) {
32
+ const content = await fileResponse.arrayBuffer();
33
+ await fs.writeFile(itemLocalPath, Buffer.from(content));
34
+ }
35
+ }
36
+ }
37
+ }
38
+ async function extractMetadata(rootPath, overrideProjectId) {
39
+ const metadataPath = path.join(rootPath, "metadata.json");
40
+ let metadata = {};
41
+ try {
42
+ const metadataContent = await fs.readFile(metadataPath, "utf8");
43
+ metadata = JSON.parse(metadataContent);
44
+ }
45
+ catch (err) {
46
+ logger_1.logger.debug(`Could not read metadata.json at ${metadataPath}: ${err}`);
47
+ }
48
+ let projectId = overrideProjectId || metadata.projectId;
49
+ if (!projectId) {
50
+ try {
51
+ const firebasercContent = await fs.readFile(path.join(rootPath, ".firebaserc"), "utf8");
52
+ const firebaserc = JSON.parse(firebasercContent);
53
+ projectId = firebaserc.projects?.default;
54
+ }
55
+ catch (err) {
56
+ logger_1.logger.debug(`Could not read .firebaserc at ${rootPath}: ${err}`);
57
+ }
58
+ }
59
+ if (projectId) {
60
+ logger_1.logger.info(`āœ… Detected Firebase Project: ${projectId}`);
61
+ }
62
+ else {
63
+ logger_1.logger.info(`āœ… Failed to determine the Firebase Project ID`);
64
+ }
65
+ let appName = "firebase-studio-export";
66
+ let blueprintContent = "";
67
+ const blueprintPath = path.join(rootPath, "docs", "blueprint.md");
68
+ try {
69
+ blueprintContent = await fs.readFile(blueprintPath, "utf8");
70
+ const nameMatch = blueprintContent.match(/# \*\*App Name\*\*: (.*)/);
71
+ if (nameMatch && nameMatch[1]) {
72
+ appName = nameMatch[1].trim();
73
+ }
74
+ }
75
+ catch (err) {
76
+ logger_1.logger.debug(`Could not read blueprint.md at ${blueprintPath}: ${err}`);
77
+ }
78
+ if (appName !== "firebase-studio-export") {
79
+ logger_1.logger.info(`āœ… Detected App Name: ${appName}`);
80
+ }
81
+ return { projectId, appName, blueprintContent };
82
+ }
83
+ async function updateReadme(rootPath, blueprintContent, appName) {
84
+ const readmePath = path.join(rootPath, "README.md");
85
+ const readmeTemplate = await (0, templates_1.readTemplate)("firebase-studio-export/readme_template.md");
86
+ const newReadme = readmeTemplate
87
+ .replace(/\${appName}/g, appName)
88
+ .replace("${exportDate}", new Date().toISOString().split("T")[0])
89
+ .replace("${blueprintContent}", blueprintContent.replace(/# \*\*App Name\*\*: .*/, "").trim());
90
+ await fs.writeFile(readmePath, newReadme);
91
+ logger_1.logger.info("āœ… Updated README.md with project details and origin info");
92
+ }
93
+ async function injectAgyContext(rootPath, projectId, appName) {
94
+ const agentDir = path.join(rootPath, ".agent");
95
+ const rulesDir = path.join(agentDir, "rules");
96
+ const workflowsDir = path.join(agentDir, "workflows");
97
+ const skillsDir = path.join(agentDir, "skills");
98
+ await fs.mkdir(rulesDir, { recursive: true });
99
+ await fs.mkdir(workflowsDir, { recursive: true });
100
+ await fs.mkdir(skillsDir, { recursive: true });
101
+ logger_1.logger.info("ā³ Fetching AGY skills from firebase/agent-skills...");
102
+ try {
103
+ const skillsResponse = await fetch("https://api.github.com/repos/firebase/agent-skills/contents/skills");
104
+ if (!skillsResponse.ok) {
105
+ throw new Error(`GitHub API returned ${skillsResponse.status}`);
106
+ }
107
+ const skillsData = (await skillsResponse.json());
108
+ if (Array.isArray(skillsData)) {
109
+ for (const item of skillsData) {
110
+ if (item.type === "dir") {
111
+ const skillName = item.name;
112
+ const skillDir = path.join(skillsDir, skillName);
113
+ await downloadGitHubDir(item.url, skillDir);
114
+ }
115
+ }
116
+ }
117
+ else {
118
+ utils.logWarning("GitHub API response for skills is not an array.");
119
+ }
120
+ logger_1.logger.info(`āœ… Downloaded Firebase skills`);
121
+ }
122
+ catch (err) {
123
+ utils.logWarning(`Could not download AGY skills, skipping. ${err}`);
124
+ }
125
+ logger_1.logger.info("ā³ Fetching Genkit skill...");
126
+ try {
127
+ const genkitSkillDir = path.join(skillsDir, "developing-genkit-js");
128
+ await downloadGitHubDir("https://api.github.com/repos/genkit-ai/skills/contents/skills/developing-genkit-js?ref=main", genkitSkillDir);
129
+ logger_1.logger.info(`āœ… Downloaded Genkit skill`);
130
+ }
131
+ catch (err) {
132
+ utils.logWarning(`Could not download Genkit skill, skipping. ${err}`);
133
+ }
134
+ const systemInstructionsTemplate = await (0, templates_1.readTemplate)("firebase-studio-export/system_instructions_template.md");
135
+ const systemInstructions = systemInstructionsTemplate
136
+ .replace("${projectId}", projectId || "None")
137
+ .replace("${appName}", appName);
138
+ await fs.writeFile(path.join(rulesDir, "migration-context.md"), systemInstructions);
139
+ logger_1.logger.info("āœ… Injected AGY rules");
140
+ try {
141
+ const startupWorkflow = await (0, templates_1.readTemplate)("firebase-studio-export/workflows/startup_workflow.md");
142
+ await fs.writeFile(path.join(workflowsDir, "startup.md"), startupWorkflow);
143
+ logger_1.logger.info("āœ… Created AGY startup workflow");
144
+ }
145
+ catch (err) {
146
+ logger_1.logger.debug(`Could not read or write startup workflow: ${err}`);
147
+ }
148
+ }
149
+ async function assertSystemState(startAgy) {
150
+ if (startAgy === false) {
151
+ return;
152
+ }
153
+ try {
154
+ (0, child_process_1.execSync)("agy --version", { stdio: "ignore" });
155
+ logger_1.logger.info("āœ… Antigravity IDE CLI (agy) detected");
156
+ }
157
+ catch (err) {
158
+ const downloadLink = "https://antigravity.google/download";
159
+ throw new error_1.FirebaseError(`Antigravity IDE CLI (agy) not found in your PATH. To ensure a seamless migration, please download and install Antigravity: ${downloadLink}`, { exit: 1 });
160
+ }
161
+ }
162
+ async function createFirebaseConfigs(rootPath, projectId) {
163
+ if (!projectId) {
164
+ return;
165
+ }
166
+ const firebaserc = {
167
+ projects: {
168
+ default: projectId,
169
+ },
170
+ };
171
+ await fs.writeFile(path.join(rootPath, ".firebaserc"), JSON.stringify(firebaserc, null, 2));
172
+ logger_1.logger.info("āœ… Created .firebaserc");
173
+ const firebaseJsonPath = path.join(rootPath, "firebase.json");
174
+ try {
175
+ await fs.access(firebaseJsonPath);
176
+ logger_1.logger.info("ā„¹ļø firebase.json already exists, skipping creation.");
177
+ }
178
+ catch {
179
+ let backendId = "studio";
180
+ try {
181
+ logger_1.logger.info(`ā³ Fetching App Hosting backends for project ${projectId}...`);
182
+ const backendsData = await apphosting.listBackends(projectId, "-");
183
+ const backends = backendsData.backends || [];
184
+ if (backends.length > 0) {
185
+ const studioBackend = backends.find((b) => b.name.endsWith("/studio") || b.name.toLowerCase().includes("studio"));
186
+ if (studioBackend) {
187
+ backendId = studioBackend.name.split("/").pop();
188
+ }
189
+ else {
190
+ backendId = backends[0].name.split("/").pop();
191
+ }
192
+ logger_1.logger.info(`āœ… Selected App Hosting backend: ${backendId}`);
193
+ }
194
+ else {
195
+ utils.logWarning('No App Hosting backends found, using default "studio"');
196
+ }
197
+ }
198
+ catch (err) {
199
+ utils.logWarning(`Could not fetch backends from Firebase CLI, using default "studio". ${err}`);
200
+ }
201
+ const firebaseJson = {
202
+ apphosting: {
203
+ backendId: backendId,
204
+ ignore: [
205
+ "node_modules",
206
+ ".git",
207
+ ".agent",
208
+ ".idx",
209
+ "firebase-debug.log",
210
+ "firebase-debug.*.log",
211
+ "functions",
212
+ ],
213
+ },
214
+ };
215
+ await fs.writeFile(firebaseJsonPath, JSON.stringify(firebaseJson, null, 2));
216
+ logger_1.logger.info(`āœ… Created firebase.json with backendId: ${backendId}`);
217
+ }
218
+ }
219
+ async function writeAgyConfigs(rootPath) {
220
+ const vscodeDir = path.join(rootPath, ".vscode");
221
+ await fs.mkdir(vscodeDir, { recursive: true });
222
+ const tasksJson = {
223
+ version: "2.0.0",
224
+ tasks: [
225
+ {
226
+ label: "npm-install",
227
+ type: "shell",
228
+ command: "npm install",
229
+ problemMatcher: [],
230
+ },
231
+ ],
232
+ };
233
+ await fs.writeFile(path.join(vscodeDir, "tasks.json"), JSON.stringify(tasksJson, null, 2));
234
+ logger_1.logger.info("āœ… Created .vscode/tasks.json");
235
+ const settingsPath = path.join(vscodeDir, "settings.json");
236
+ let settings = {};
237
+ try {
238
+ const settingsContent = await fs.readFile(settingsPath, "utf8");
239
+ settings = JSON.parse(settingsContent);
240
+ }
241
+ catch (err) {
242
+ logger_1.logger.debug(`Could not read ${settingsPath}: ${err}`);
243
+ }
244
+ const cleanSettings = {};
245
+ for (const [key, value] of Object.entries(settings)) {
246
+ if (!key.startsWith("IDX.")) {
247
+ cleanSettings[key] = value;
248
+ }
249
+ }
250
+ cleanSettings["workbench.startupEditor"] = "readme";
251
+ await fs.writeFile(settingsPath, JSON.stringify(cleanSettings, null, 2));
252
+ logger_1.logger.info("āœ… Updated .vscode/settings.json with startup preferences");
253
+ const launchJson = {
254
+ version: "0.2.0",
255
+ configurations: [
256
+ {
257
+ type: "node",
258
+ request: "launch",
259
+ name: "Next.js: debug server-side",
260
+ runtimeExecutable: "npm",
261
+ runtimeArgs: ["run", "dev"],
262
+ port: 9002,
263
+ console: "integratedTerminal",
264
+ preLaunchTask: "npm-install",
265
+ },
266
+ ],
267
+ };
268
+ await fs.writeFile(path.join(vscodeDir, "launch.json"), JSON.stringify(launchJson, null, 2));
269
+ logger_1.logger.info("āœ… Created .vscode/launch.json");
270
+ }
271
+ async function cleanupUnusedFiles(rootPath) {
272
+ const docsDir = path.join(rootPath, "docs");
273
+ const blueprintPath = path.join(docsDir, "blueprint.md");
274
+ try {
275
+ await fs.unlink(blueprintPath);
276
+ logger_1.logger.info("āœ… Cleaned up docs/blueprint.md");
277
+ }
278
+ catch (err) {
279
+ logger_1.logger.debug(`Could not delete ${blueprintPath}: ${err}`);
280
+ }
281
+ try {
282
+ const files = await fs.readdir(docsDir);
283
+ if (files.length === 0) {
284
+ await fs.rmdir(docsDir);
285
+ logger_1.logger.info("āœ… Removed empty docs directory");
286
+ }
287
+ }
288
+ catch (err) {
289
+ logger_1.logger.debug(`Could not remove ${docsDir}: ${err}`);
290
+ }
291
+ const metadataPath = path.join(rootPath, "metadata.json");
292
+ try {
293
+ await fs.unlink(metadataPath);
294
+ logger_1.logger.info("āœ… Cleaned up metadata.json");
295
+ }
296
+ catch (err) {
297
+ logger_1.logger.debug(`Could not delete ${metadataPath}: ${err}`);
298
+ }
299
+ const modifiedPath = path.join(rootPath, ".modified");
300
+ try {
301
+ await fs.unlink(modifiedPath);
302
+ logger_1.logger.info("āœ… Cleaned up .modified");
303
+ }
304
+ catch (err) {
305
+ logger_1.logger.debug(`Could not delete ${modifiedPath}: ${err}`);
306
+ }
307
+ }
308
+ async function uploadSecrets(rootPath, projectId) {
309
+ if (!projectId) {
310
+ return;
311
+ }
312
+ const envPath = path.join(rootPath, ".env");
313
+ try {
314
+ await fs.access(envPath);
315
+ }
316
+ catch {
317
+ return;
318
+ }
319
+ try {
320
+ const envContent = await fs.readFile(envPath, "utf8");
321
+ const parsedEnv = env.parse(envContent);
322
+ const geminiApiKey = parsedEnv.envs["GEMINI_API_KEY"];
323
+ if (geminiApiKey && geminiApiKey.trim().length > 0) {
324
+ logger_1.logger.info("ā³ Uploading GEMINI_API_KEY from .env to App Hosting secrets...");
325
+ await (0, secrets_1.apphostingSecretsSetAction)("GEMINI_API_KEY", projectId, undefined, undefined, envPath, true);
326
+ logger_1.logger.info("āœ… Uploaded GEMINI_API_KEY secret");
327
+ }
328
+ else {
329
+ logger_1.logger.debug("Skipping GEMINI_API_KEY upload: key is missing or blank in .env");
330
+ }
331
+ }
332
+ catch (err) {
333
+ utils.logWarning(`Failed to upload GEMINI_API_KEY secret: ${err}`);
334
+ }
335
+ }
336
+ async function askToOpenAntigravity(rootPath, appName, startAgy) {
337
+ if (startAgy === false) {
338
+ logger_1.logger.info('\nšŸ‘‰ Next steps: Open this folder in Antigravity and run the "Initial Project Setup" workflow.');
339
+ return;
340
+ }
341
+ const answer = await prompt.confirm({
342
+ message: `Migration complete for ${appName}! Would you like to open it in Antigravity now?`,
343
+ default: true,
344
+ });
345
+ if (answer) {
346
+ logger_1.logger.info(`ā³ Opening ${appName} in Antigravity...`);
347
+ try {
348
+ const agyProcess = (0, child_process_1.spawn)("agy", ["."], {
349
+ cwd: rootPath,
350
+ stdio: "ignore",
351
+ detached: true,
352
+ });
353
+ agyProcess.unref();
354
+ }
355
+ catch (err) {
356
+ utils.logWarning("Could not open Antigravity IDE automatically. Please open it manually.");
357
+ }
358
+ }
359
+ else {
360
+ logger_1.logger.info('\nšŸ‘‰ Next steps: Open this folder in Antigravity and run the "Initial Project Setup" workflow.');
361
+ }
362
+ }
363
+ async function migrate(rootPath, options = { startAgy: true }) {
364
+ logger_1.logger.info("šŸš€ Starting Firebase Studio to Antigravity migration...");
365
+ await assertSystemState(options.startAgy);
366
+ const { projectId, appName, blueprintContent } = await extractMetadata(rootPath, options.project);
367
+ await updateReadme(rootPath, blueprintContent, appName);
368
+ await createFirebaseConfigs(rootPath, projectId);
369
+ await uploadSecrets(rootPath, projectId);
370
+ await injectAgyContext(rootPath, projectId, appName);
371
+ await writeAgyConfigs(rootPath);
372
+ await cleanupUnusedFiles(rootPath);
373
+ const currentFolderName = path.basename(rootPath);
374
+ if (currentFolderName === "download") {
375
+ logger_1.logger.info(`\nšŸ’” Tip: You might want to rename this folder to "${appName.toLowerCase().replace(/\s+/g, "-")}"`);
376
+ }
377
+ await askToOpenAntigravity(rootPath, appName, options.startAgy);
378
+ }
@@ -27,11 +27,11 @@ const utils_1 = require("./utils");
27
27
  exports.RESERVED_NAMESPACES = ["firebase"];
28
28
  const apiClient = new apiv2_1.Client({ urlPrefix: (0, api_1.firebaseApiOrigin)() });
29
29
  const LEGACY_RUNTIME_CONFIG_EXPERIMENT = "legacyRuntimeConfigCommands";
30
- const FUNCTIONS_CONFIG_DEPRECATION_MESSAGE = `DEPRECATION NOTICE: Action required before March 2026
30
+ const FUNCTIONS_CONFIG_DEPRECATION_MESSAGE = `DEPRECATION NOTICE: Action required before March 2027
31
31
 
32
- The functions.config() API and the Cloud Runtime Config service are deprecated. Deploys that rely on functions.config() will fail once Runtime Config shuts down in March 2026.
32
+ The functions.config() API and the Cloud Runtime Config service are deprecated. Deploys that rely on functions.config() will fail once Runtime Config shuts down in March 2027.
33
33
 
34
- The legacy functions:config:* CLI commands are deprecated and will be removed before March 2026.
34
+ The legacy functions:config:* CLI commands are deprecated and will be removed before March 2027.
35
35
 
36
36
  Learn how to migrate from functions.config() to the params package:
37
37
 
@@ -23,6 +23,7 @@ const projectUtils_1 = require("../../projectUtils");
23
23
  const logger_1 = require("../../logger");
24
24
  const iam_1 = require("../iam");
25
25
  const error_1 = require("../../error");
26
+ const freeTrial_1 = require("../../dataconnect/freeTrial");
26
27
  const API_VERSION = "v1";
27
28
  const client = new apiv2_1.Client({
28
29
  urlPrefix: (0, api_1.cloudSQLAdminOrigin)(),
@@ -91,7 +92,7 @@ async function createInstance(args) {
91
92
  return;
92
93
  }
93
94
  catch (err) {
94
- handleAllowlistError(err, args.location);
95
+ await handleCreateInstanceError(err, args.location, args.projectId);
95
96
  throw err;
96
97
  }
97
98
  }
@@ -119,10 +120,14 @@ async function updateInstanceForDataConnect(instance, enableGoogleMlIntegration)
119
120
  });
120
121
  return pollRes;
121
122
  }
122
- function handleAllowlistError(err, region) {
123
- if (err.message.includes("Not allowed to set system label: firebase-data-connect")) {
123
+ async function handleCreateInstanceError(err, region, projectId) {
124
+ if (err?.message?.includes("Not allowed to set system label: firebase-data-connect")) {
124
125
  throw new error_1.FirebaseError(`Cloud SQL free trial instances are not yet available in ${region}. Please check https://firebase.google.com/docs/data-connect/ for a full list of available regions.`);
125
126
  }
127
+ if (err?.message?.includes("The billing account is not in good standing") &&
128
+ (await (0, freeTrial_1.checkFreeTrialInstanceUsed)(projectId))) {
129
+ throw new error_1.FirebaseError(`You have already used your Cloud SQL free trial. To create more instances, you need to attach a billing account to project ${projectId}.`);
130
+ }
126
131
  }
127
132
  function setDatabaseFlag(flag, flags = []) {
128
133
  const temp = flags.filter((f) => f.name !== flag.name);
@@ -36,7 +36,7 @@ class FBToolsAuthClient extends google_auth_library_1.AuthClient {
36
36
  async getRequestHeaders() {
37
37
  const token = await this.getAccessToken();
38
38
  return {
39
- ...apiv2.STANDARD_HEADERS,
39
+ ...apiv2.standardHeaders(),
40
40
  Authorization: `Bearer ${token.token}`,
41
41
  };
42
42
  }
@@ -64,8 +64,9 @@ Here are a list of prerequisite steps that must be completed before running a te
64
64
 
65
65
  * Goal (required): In one sentence or less, describe what you want the agent to do in this step.
66
66
  * Hint (optional): Provide additional information to help Gemini understand and navigate your app.
67
- * Success Criteria (optional): Your success criteria should be phrased as an observation, such as 'The screen shows a
68
- success message' or 'The checkout page is visible'.
67
+ * Final Screen Assertion (required for last step): Your final screen assertion should be phrased as an observation, such
68
+ as 'The screen shows a success message' or 'The checkout page is visible'. You can think of these as test assertions
69
+ that are checked at the end of the step. Optional except for the last step, for which it is required.
69
70
 
70
71
  The developer has optionally specified the following description for their test:
71
72
  * ${testDescription}
@@ -79,8 +80,8 @@ Here are a list of prerequisite steps that must be completed before running a te
79
80
  For example, if a step has a list in it, it should probably be broken up into multiple steps. Steps do not need
80
81
  to be too small though. The test case should provide a good balance between strict guidance and flexibility. As a
81
82
  rule of thumb, each step should require between 2-5 actions.
82
- * Include a hint and success criteria whenever possible. Specifically, try to always include a success criteria to help
83
- the agent determine when the goal has been completed.
83
+ * Include a hint and final screen assertion whenever possible. Specifically, try to always include a final screen assertion
84
+ to help the agent determine when the goal has been completed.
84
85
  * Avoid functionality that the app testing agent struggles with. The app testing agent struggles with the following:
85
86
  * Journeys that require specific timing (like observing that something should be visible for a certain number of
86
87
  seconds), interacting with moving or transient elements, etc.
@@ -96,7 +97,7 @@ Here are a list of prerequisite steps that must be completed before running a te
96
97
  above, convert the test description provided by the user to make it easier for the agent to follow so that the tests can
97
98
  be re-run reliably. If there is no test description, generate a test case that you think will be useful given the functionality
98
99
  of the app. Generate an explanation on why you generated the new test case the way you did, and then generate the
99
- new test case, which again is an array of steps where each step contains a goal, hint, and success criteria. Show this
100
+ new test case, which again is an array of steps where each step contains a goal, hint, and final screen assertion. Show this
100
101
  to the user and have them confirm before moving forward.
101
102
 
102
103
  ## Run Test
@@ -17,17 +17,17 @@ const TestDeviceSchema = zod_1.z
17
17
  orientation: zod_1.z.string(),
18
18
  })
19
19
  .describe(`Device to run automated test on. Can run 'gcloud firebase test android|ios models list' to see available devices.`);
20
- const AIStepSchema = zod_1.z
20
+ const AiStepSchema = zod_1.z
21
21
  .object({
22
22
  goal: zod_1.z.string().describe("A goal to be accomplished during the test."),
23
23
  hint: zod_1.z
24
24
  .string()
25
25
  .optional()
26
26
  .describe("Hint text containing suggestions to help the agent accomplish the goal."),
27
- successCriteria: zod_1.z
27
+ finalScreenAssertion: zod_1.z
28
28
  .string()
29
29
  .optional()
30
- .describe("A description of criteria the agent should use to determine if the goal has been successfully completed."),
30
+ .describe("A description of what the screen should look like at the end of the step, to determine if the goal has been successfully completed."),
31
31
  })
32
32
  .describe("Step within a test case; run during the execution of the test.");
33
33
  const defaultDevices = [
@@ -47,7 +47,7 @@ exports.run_tests = (0, tool_1.tool)("apptesting", {
47
47
  testDevices: zod_1.z.array(TestDeviceSchema).default(defaultDevices),
48
48
  testCase: zod_1.z.object({
49
49
  steps: zod_1.z
50
- .array(AIStepSchema)
50
+ .array(AiStepSchema)
51
51
  .describe("Test case containing the steps that are run during its execution."),
52
52
  }),
53
53
  }),