firebase-tools 15.10.0 → 15.10.1

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.
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.getEnvironmentName = getEnvironmentName;
4
4
  exports.promptForAppHostingYaml = promptForAppHostingYaml;
5
+ exports.getAutoinitEnvVars = getAutoinitEnvVars;
5
6
  const error_1 = require("../error");
6
7
  const config_1 = require("./config");
7
8
  const prompt = require("../prompt");
@@ -36,3 +37,16 @@ async function promptForAppHostingYaml(apphostingFileNameToPathMap, promptMessag
36
37
  });
37
38
  return fileToExportPath;
38
39
  }
40
+ function getAutoinitEnvVars(webappConfig) {
41
+ if (!webappConfig) {
42
+ return {};
43
+ }
44
+ return {
45
+ FIREBASE_WEBAPP_CONFIG: JSON.stringify(webappConfig),
46
+ FIREBASE_CONFIG: JSON.stringify({
47
+ databaseURL: webappConfig.databaseURL,
48
+ storageBucket: webappConfig.storageBucket,
49
+ projectId: webappConfig.projectId,
50
+ }),
51
+ };
52
+ }
@@ -27,8 +27,8 @@ async function parseTestFiles(dir, targetUri, filePattern, namePattern) {
27
27
  }
28
28
  return accumulator;
29
29
  }, {});
30
- const fileFilterFn = createFilter(filePattern);
31
- const nameFilterFn = createFilter(namePattern);
30
+ const fileFilterFn = createFilter(filePattern, "file pattern");
31
+ const nameFilterFn = createFilter(namePattern, "test name pattern");
32
32
  const filteredInvocations = files
33
33
  .filter((file) => fileFilterFn(file.path))
34
34
  .flatMap((file) => file.invocations)
@@ -61,9 +61,18 @@ async function parseTestFiles(dir, targetUri, filePattern, namePattern) {
61
61
  };
62
62
  });
63
63
  }
64
- function createFilter(pattern) {
65
- const regex = pattern ? new RegExp(pattern) : undefined;
66
- return (s) => !regex || regex.test(s);
64
+ function createFilter(pattern, context) {
65
+ try {
66
+ const regex = pattern ? new RegExp(pattern) : undefined;
67
+ return (s) => !regex || regex.test(s);
68
+ }
69
+ catch (ex) {
70
+ if (ex instanceof SyntaxError) {
71
+ const errMsg = context ? `Invalid ${context} regex: ${pattern}` : `Invalid regex: ${pattern}`;
72
+ throw new error_1.FirebaseError(errMsg, { original: (0, error_1.getError)(ex) });
73
+ }
74
+ throw ex;
75
+ }
67
76
  }
68
77
  async function parseTestFilesRecursive(params) {
69
78
  const testDir = params.testDir;
@@ -3,14 +3,15 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.command = void 0;
4
4
  const requireAuth_1 = require("../requireAuth");
5
5
  const command_1 = require("../command");
6
- const logger_1 = require("../logger");
7
- const clc = require("colorette");
8
6
  const parseTestFiles_1 = require("../apptesting/parseTestFiles");
9
7
  const ora = require("ora");
10
8
  const error_1 = require("../error");
11
9
  const client_1 = require("../appdistribution/client");
12
10
  const distribution_1 = require("../appdistribution/distribution");
13
11
  const options_parser_util_1 = require("../appdistribution/options-parser-util");
12
+ const utils = require("../utils");
13
+ const fsutils_1 = require("../fsutils");
14
+ const path = require("path");
14
15
  const defaultDevices = [
15
16
  {
16
17
  model: "MediumPhone.arm",
@@ -24,35 +25,44 @@ exports.command = new command_1.Command("apptesting:execute <release-binary-file
24
25
  .option("--app <app_id>", "The app id of your Firebase web app. Optional if the project contains exactly one web app.")
25
26
  .option("--test-file-pattern <pattern>", "Test file pattern. Only tests contained in files that match this pattern will be executed.")
26
27
  .option("--test-name-pattern <pattern>", "Test name pattern. Only tests with names that match this pattern will be executed.")
27
- .option("--test-dir <test_dir>", "Directory where tests can be found.")
28
- .option("--test-devices <string>", "semicolon-separated list of devices to run automated tests on, in the format 'model=<model-id>,version=<os-version-id>,locale=<locale>,orientation=<orientation>'. Run 'gcloud firebase test android|ios models list' to see available devices. Note: This feature is in beta.")
29
- .option("--test-devices-file <string>", "path to file containing a list of semicolon- or newline-separated devices to run automated tests on, in the format 'model=<model-id>,version=<os-version-id>,locale=<locale>,orientation=<orientation>'. Run 'gcloud firebase test android|ios models list' to see available devices. Note: This feature is in beta.")
28
+ .option("--test-dir <test_dir>", "Directory where tests can be found. Defaults to './tests'.")
29
+ .option("--test-devices <string>", "Semicolon-separated list of devices to run automated tests on, in the format 'model=<model-id>,version=<os-version-id>,locale=<locale>,orientation=<orientation>'. Run 'gcloud firebase test android|ios models list' to see available devices. Note: This feature is in beta.")
30
+ .option("--test-devices-file <string>", "Path to file containing a list of semicolon- or newline-separated devices to run automated tests on, in the format 'model=<model-id>,version=<os-version-id>,locale=<locale>,orientation=<orientation>'. Run 'gcloud firebase test android|ios models list' to see available devices. Note: This feature is in beta.")
31
+ .option("--test-non-blocking", "Run automated tests without waiting for them to complete. Visit the Firebase console for the test results.")
30
32
  .before(requireAuth_1.requireAuth)
31
33
  .action(async (target, options) => {
32
34
  const appName = (0, options_parser_util_1.getAppName)(options);
33
- const testDir = options.testDir || "tests";
35
+ const testDir = path.resolve(options.testDir || "tests");
36
+ if (!(0, fsutils_1.dirExistsSync)(testDir)) {
37
+ throw new error_1.FirebaseError(`Tests directory not found: ${testDir}. Use the --test-dir flag to choose a different directory.`);
38
+ }
34
39
  const tests = await (0, parseTestFiles_1.parseTestFiles)(testDir, undefined, options.testFilePattern, options.testNamePattern);
35
40
  const testDevices = (0, options_parser_util_1.parseTestDevices)(options.testDevices, options.testDevicesFile);
36
41
  if (!tests.length) {
37
42
  throw new error_1.FirebaseError("No tests found");
38
43
  }
39
44
  const invokeSpinner = ora("Requesting test execution");
45
+ const client = new client_1.AppDistributionClient();
40
46
  let releaseTests;
41
47
  let release;
42
48
  try {
43
- const client = new client_1.AppDistributionClient();
44
49
  release = await (0, distribution_1.upload)(client, appName, new distribution_1.Distribution(target));
45
50
  invokeSpinner.start();
46
51
  releaseTests = await invokeTests(client, release.name, tests, !testDevices.length ? defaultDevices : testDevices);
47
- invokeSpinner.text = "Test execution requested";
52
+ invokeSpinner.text = `${pluralizeTests(releaseTests.length)} started successfully!`;
48
53
  invokeSpinner.succeed();
49
54
  }
50
55
  catch (ex) {
51
56
  invokeSpinner.fail("Failed to request test execution");
52
57
  throw ex;
53
58
  }
54
- logger_1.logger.info(clc.bold(`\n${clc.white("===")} Running ${pluralizeTests(releaseTests.length)}`));
55
- logger_1.logger.info(`View progress and results in the Firebase Console:\n${release.firebaseConsoleUri}`);
59
+ if (options.testNonBlocking) {
60
+ utils.logBullet(`View progress and results in the Firebase Console:\n${release.firebaseConsoleUri}`);
61
+ }
62
+ else {
63
+ await (0, distribution_1.awaitTestResults)(releaseTests, client);
64
+ utils.logBullet(`View detailed results in the Firebase Console:\n${release.firebaseConsoleUri}`);
65
+ }
56
66
  });
57
67
  function pluralizeTests(numTests) {
58
68
  return `${numTests} test${numTests === 1 ? "" : "s"}`;
@@ -33,6 +33,11 @@ function load(client) {
33
33
  client.appdistribution.testCases = {};
34
34
  client.appdistribution.testCases.export = loadCommand("appdistribution-testcases-export");
35
35
  client.appdistribution.testCases.import = loadCommand("appdistribution-testcases-import");
36
+ client.apptesting = {};
37
+ client.apptesting.execute = loadCommand("apptesting");
38
+ if (experiments.isEnabled("apptesting")) {
39
+ client.apptesting.wata = loadCommand("apptesting-wata");
40
+ }
36
41
  client.apps = {};
37
42
  client.apps.create = loadCommand("apps-create");
38
43
  client.apps.list = loadCommand("apps-list");
@@ -254,11 +259,6 @@ function load(client) {
254
259
  client.target.clear = loadCommand("target-clear");
255
260
  client.target.remove = loadCommand("target-remove");
256
261
  client.use = loadCommand("use");
257
- client.apptesting = {};
258
- client.apptesting.execute = loadCommand("apptesting");
259
- if (experiments.isEnabled("apptesting")) {
260
- client.apptesting.wata = loadCommand("apptesting-wata");
261
- }
262
262
  const t1 = process.hrtime.bigint();
263
263
  const diffMS = (t1 - t0) / BigInt(1e6);
264
264
  if (diffMS > 100) {
@@ -35,7 +35,7 @@ async function createArchive(config, rootDir, targetSubDir) {
35
35
  await pipeAsync(archive, fileStream);
36
36
  }
37
37
  catch (err) {
38
- throw new error_1.FirebaseError("Could not read source directory. Remove links and shortcuts and try again.", { original: err, exit: 1 });
38
+ throw new error_1.FirebaseError(`Could not read source directory. Remove links and shortcuts and try again. Original: ${err}`, { original: err, exit: 1 });
39
39
  }
40
40
  return tmpFile;
41
41
  }
@@ -98,7 +98,7 @@ async function packageSource(projectDir, sourceDir, config, additionalSources, r
98
98
  if (err instanceof error_1.FirebaseError) {
99
99
  throw err;
100
100
  }
101
- throw new error_1.FirebaseError("Could not read source directory. Remove links and shortcuts and try again.", {
101
+ throw new error_1.FirebaseError(`Could not read source directory. Remove links and shortcuts and try again. Original: ${err}`, {
102
102
  original: err,
103
103
  exit: 1,
104
104
  });
@@ -20,9 +20,10 @@ const utils_1 = require("../../utils");
20
20
  const apphosting = require("../../gcp/apphosting");
21
21
  const constants_2 = require("../constants");
22
22
  const fetchWebSetup_1 = require("../../fetchWebSetup");
23
- const apps_1 = require("../../management/apps");
24
23
  const child_process_1 = require("child_process");
25
24
  const semver_1 = require("semver");
25
+ const utils_2 = require("../../apphosting/utils");
26
+ const apps_1 = require("../../management/apps");
26
27
  const secretResourceRegex = /^projects\/([^/]+)\/secrets\/([^/]+)(?:\/versions\/((?:latest)|\d+))?$/;
27
28
  const secretShorthandRegex = /^([^/@]+)(?:@((?:latest)|\d+))?$/;
28
29
  async function loadSecret(project, name) {
@@ -81,6 +82,15 @@ async function start(options) {
81
82
  startCommand = await (0, developmentServer_1.detectPackageManagerStartCommand)(backendRoot);
82
83
  developmentServer_2.logger.logLabeled("BULLET", types_1.Emulators.APPHOSTING, `starting app with: '${startCommand}'`);
83
84
  }
85
+ const packageManager = await (0, developmentServer_1.detectPackageManager)(backendRoot).catch(() => undefined);
86
+ let autoinitEnvVars = {};
87
+ if (packageManager === "pnpm") {
88
+ (0, utils_1.logLabeledWarning)("apphosting", "Firebase JS SDK autoinit does not currently support PNPM.");
89
+ }
90
+ else {
91
+ const webappConfig = await getBackendAppConfig(options?.projectId, options?.backendId);
92
+ autoinitEnvVars = (0, utils_2.getAutoinitEnvVars)(webappConfig);
93
+ }
84
94
  const apphostingLocalConfig = await (0, config_1.getLocalAppHostingConfiguration)(backendRoot);
85
95
  const resolveEnv = Object.entries(apphostingLocalConfig.env).map(async ([key, value]) => [
86
96
  key,
@@ -88,6 +98,7 @@ async function start(options) {
88
98
  ]);
89
99
  const environmentVariablesToInject = {
90
100
  NODE_ENV: process.env.NODE_ENV,
101
+ ...autoinitEnvVars,
91
102
  ...getEmulatorEnvs(),
92
103
  ...Object.fromEntries(await Promise.all(resolveEnv)),
93
104
  FIREBASE_APP_HOSTING: "1",
@@ -96,20 +107,7 @@ async function start(options) {
96
107
  PROJECT_ID: options?.projectId,
97
108
  PORT: port.toString(),
98
109
  };
99
- const packageManager = await (0, developmentServer_1.detectPackageManager)(backendRoot).catch(() => undefined);
100
- if (packageManager === "pnpm") {
101
- (0, utils_1.logLabeledWarning)("apphosting", `Firebase JS SDK autoinit does not currently support PNPM.`);
102
- }
103
- else {
104
- const webappConfig = await getBackendAppConfig(options?.projectId, options?.backendId);
105
- if (webappConfig) {
106
- environmentVariablesToInject["FIREBASE_WEBAPP_CONFIG"] || (environmentVariablesToInject["FIREBASE_WEBAPP_CONFIG"] = JSON.stringify(webappConfig));
107
- environmentVariablesToInject["FIREBASE_CONFIG"] || (environmentVariablesToInject["FIREBASE_CONFIG"] = JSON.stringify({
108
- databaseURL: webappConfig.databaseURL,
109
- storageBucket: webappConfig.storageBucket,
110
- projectId: webappConfig.projectId,
111
- }));
112
- }
110
+ if (packageManager !== "pnpm") {
113
111
  await tripFirebasePostinstall(backendRoot, environmentVariablesToInject);
114
112
  }
115
113
  (0, spawn_1.spawnWithCommandString)(startCommand, backendRoot, environmentVariablesToInject)
@@ -44,13 +44,13 @@
44
44
  }
45
45
  },
46
46
  "pubsub": {
47
- "version": "0.8.27",
48
- "expectedSize": 52924291,
49
- "expectedChecksum": "cb0d35db6aa1bb5e3f7e2a5a690c631d",
50
- "expectedChecksumSHA256": "0b793b420b608b68c200a0d15123c63967ac2863bbd9545ecb087d5b28871339",
51
- "remoteUrl": "https://storage.googleapis.com/firebase-preview-drop/emulator/pubsub-emulator-0.8.27.zip",
52
- "downloadPathRelativeToCacheDir": "pubsub-emulator-0.8.27.zip",
53
- "binaryPathRelativeToCacheDir": "pubsub-emulator-0.8.27/pubsub-emulator/bin/cloud-pubsub-emulator"
47
+ "version": "0.8.29",
48
+ "expectedSize": 52906482,
49
+ "expectedChecksum": "8345b0a923dda5634dd56a25f913cf37",
50
+ "expectedChecksumSHA256": "31228112fb95a6818d13a88e801f4be6fb5ea5b601175b74528cc61823ea5507",
51
+ "remoteUrl": "https://storage.googleapis.com/firebase-preview-drop/emulator/pubsub-emulator-0.8.29.zip",
52
+ "downloadPathRelativeToCacheDir": "pubsub-emulator-0.8.29.zip",
53
+ "binaryPathRelativeToCacheDir": "pubsub-emulator-0.8.29/pubsub-emulator/bin/cloud-pubsub-emulator"
54
54
  },
55
55
  "dataconnect": {
56
56
  "darwin": {
@@ -16,7 +16,7 @@ const secrets_1 = require("../apphosting/secrets");
16
16
  const env = require("../functions/env");
17
17
  const error_1 = require("../error");
18
18
  const os = require("os");
19
- async function setupAntigravityMcpServer(rootPath) {
19
+ async function setupAntigravityMcpServer(rootPath, appType) {
20
20
  const mcpConfigDir = path.join(os.homedir(), ".gemini", "antigravity");
21
21
  const mcpConfigPath = path.join(mcpConfigDir, "mcp_config.json");
22
22
  let mcpConfig = { mcpServers: {} };
@@ -36,16 +36,39 @@ async function setupAntigravityMcpServer(rootPath) {
36
36
  mcpConfig.mcpServers = {};
37
37
  }
38
38
  }
39
- if (mcpConfig.mcpServers["firebase"]) {
39
+ let updated = false;
40
+ 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}`);
47
+ }
48
+ else {
40
49
  logger_1.logger.info("ℹ️ Firebase MCP server already configured in Antigravity, skipping.");
41
- return;
42
50
  }
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}`);
51
+ if (appType === "FLUTTER") {
52
+ if (utils.commandExistsSync("dart")) {
53
+ 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}`);
60
+ }
61
+ else {
62
+ logger_1.logger.info("ℹ️ Dart MCP server already configured in Antigravity, skipping.");
63
+ }
64
+ }
65
+ else {
66
+ utils.logWarning("Couldn't find Dart/Flutter on PATH. Install Flutter by following the instruction at https://docs.flutter.dev/install.");
67
+ }
68
+ }
69
+ if (updated) {
70
+ await fs.writeFile(mcpConfigPath, JSON.stringify(mcpConfig, null, 2));
71
+ }
49
72
  }
50
73
  catch (err) {
51
74
  const message = err instanceof Error ? err.message : String(err);
@@ -89,40 +112,23 @@ async function detectAppType(rootPath) {
89
112
  }
90
113
  return "OTHER";
91
114
  }
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
115
  const isValidFirebaseProjectId = (projectId) => {
114
116
  const projectIdRegex = /^[a-z][a-z0-9-]{4,28}[a-z0-9]$/;
115
117
  return projectIdRegex.test(projectId);
116
118
  };
117
119
  async function extractMetadata(rootPath, overrideProjectId) {
120
+ const studioJsonPath = path.join(rootPath, "studio.json");
118
121
  const metadataPath = path.join(rootPath, "metadata.json");
119
122
  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}`);
123
+ for (const metadataFile of [metadataPath, studioJsonPath]) {
124
+ try {
125
+ const metadataContent = await fs.readFile(metadataFile, "utf8");
126
+ metadata = JSON.parse(metadataContent);
127
+ logger_1.logger.info(`✅ Read ${metadataFile}`);
128
+ }
129
+ catch (err) {
130
+ logger_1.logger.debug(`Could not read metadata at ${metadataFile}: ${err}`);
131
+ }
126
132
  }
127
133
  logger_1.logger.debug(`overrideProjectId ${overrideProjectId}`);
128
134
  logger_1.logger.debug(`metadata.projectId ${metadata.projectId}`);
@@ -146,14 +152,13 @@ async function extractMetadata(rootPath, overrideProjectId) {
146
152
  logger_1.logger.info(`✅ Using Firebase Project: ${projectId}`);
147
153
  }
148
154
  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.`);
155
+ logger_1.logger.debug(`❌ Failed to determine the Firebase Project ID. You can set a project later by setting the '--project' flag.`);
150
156
  }
151
157
  let appName = "firebase-studio-export";
152
- let blueprintContent = "";
153
158
  const blueprintPath = path.join(rootPath, "docs", "blueprint.md");
154
159
  try {
155
- blueprintContent = await fs.readFile(blueprintPath, "utf8");
156
- const nameMatch = blueprintContent.match(/# \*\*App Name\*\*: (.*)/);
160
+ const content = await fs.readFile(blueprintPath, "utf8");
161
+ const nameMatch = content.match(/# \*\*App Name\*\*: (.*)/);
157
162
  if (nameMatch && nameMatch[1]) {
158
163
  appName = nameMatch[1].trim();
159
164
  }
@@ -164,15 +169,34 @@ async function extractMetadata(rootPath, overrideProjectId) {
164
169
  if (appName !== "firebase-studio-export") {
165
170
  logger_1.logger.info(`✅ Detected App Name: ${appName}`);
166
171
  }
167
- return { projectId, appName, blueprintContent };
172
+ return { projectId, appName };
168
173
  }
169
- async function updateReadme(rootPath, blueprintContent, appName) {
174
+ async function updateReadme(rootPath, framework) {
170
175
  const readmePath = path.join(rootPath, "README.md");
171
176
  const readmeTemplate = await (0, templates_1.readTemplate)("firebase-studio-export/readme_template.md");
172
- const newReadme = readmeTemplate
173
- .replace(/\${appName}/g, appName)
177
+ const frameworkConfigs = {
178
+ NEXT_JS: { startCommand: "npm run dev", localUrl: "http://localhost:9002" },
179
+ ANGULAR: { startCommand: "npm run start", localUrl: "http://localhost:4200" },
180
+ FLUTTER: {
181
+ startCommand: "flutter run -d chrome --web-port=8080",
182
+ localUrl: "http://localhost:8080",
183
+ },
184
+ OTHER: { startCommand: "npm run dev", localUrl: "http://localhost:9002" },
185
+ };
186
+ const { startCommand, localUrl } = frameworkConfigs[framework];
187
+ let existingReadme = "";
188
+ try {
189
+ existingReadme = await fs.readFile(readmePath, "utf8");
190
+ }
191
+ catch (err) {
192
+ }
193
+ let newReadme = readmeTemplate
174
194
  .replace("${exportDate}", new Date().toISOString().split("T")[0])
175
- .replace("${blueprintContent}", blueprintContent.replace(/# \*\*App Name\*\*: .*/, "").trim());
195
+ .replace("${startCommand}", startCommand)
196
+ .replace("${localUrl}", localUrl);
197
+ if (existingReadme.trim()) {
198
+ newReadme += `\n\n---\n\n## Previous README.md contents:\n\n${existingReadme}`;
199
+ }
176
200
  await fs.writeFile(readmePath, newReadme);
177
201
  logger_1.logger.info("✅ Updated README.md with project details and origin info");
178
202
  }
@@ -184,39 +208,31 @@ async function injectAntigravityContext(rootPath, projectId, appName) {
184
208
  await fs.mkdir(rulesDir, { recursive: true });
185
209
  await fs.mkdir(workflowsDir, { recursive: true });
186
210
  await fs.mkdir(skillsDir, { recursive: true });
187
- logger_1.logger.info("⏳ Fetching Antigravity skills from firebase/agent-skills...");
211
+ logger_1.logger.info("⏳ Adding Antigravity skills...");
188
212
  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
- }
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;
202
220
  }
203
- else {
204
- utils.logWarning("GitHub API response for skills is not an array.");
221
+ if (result.status !== 0) {
222
+ throw new Error(`npx skills add exited with code ${result.status}`);
205
223
  }
206
- logger_1.logger.info(`✅ Downloaded Firebase skills`);
224
+ logger_1.logger.info(`✅ Added Antigravity skills`);
207
225
  }
208
226
  catch (err) {
209
- utils.logWarning(`Could not download Antigravity skills, skipping. ${err}`);
227
+ utils.logWarning(`Could not add Antigravity skills, skipping. ${err}`);
210
228
  }
211
229
  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);
230
+ const systemInstructions = systemInstructionsTemplate.replace("${appName}", appName);
215
231
  await fs.writeFile(path.join(rulesDir, "migration-context.md"), systemInstructions);
216
232
  logger_1.logger.info("✅ Injected Antigravity rules");
217
233
  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);
234
+ const cleanupWorkflow = await (0, templates_1.readTemplate)("firebase-studio-export/workflows/cleanup.md");
235
+ await fs.writeFile(path.join(workflowsDir, "cleanup.md"), cleanupWorkflow);
220
236
  logger_1.logger.info("✅ Created Antigravity startup workflow");
221
237
  }
222
238
  catch (err) {
@@ -282,12 +298,35 @@ async function createFirebaseConfigs(rootPath, projectId) {
282
298
  const backendsData = await apphosting.listBackends(projectId, "-");
283
299
  const backends = backendsData.backends || [];
284
300
  if (backends.length > 0) {
301
+ const backendIds = backends.map((b) => b.name.split("/").pop());
285
302
  const studioBackend = backends.find((b) => b.name.endsWith("/studio") || b.name.toLowerCase().includes("studio"));
303
+ let selectedBackendId = "";
286
304
  if (studioBackend) {
287
- backendId = studioBackend.name.split("/").pop();
305
+ selectedBackendId = studioBackend.name.split("/").pop();
306
+ }
307
+ else {
308
+ selectedBackendId = backendIds[0];
309
+ }
310
+ const confirmBackend = await prompt.confirm({
311
+ message: `Would you like to use the App Hosting backend "${selectedBackendId}"?`,
312
+ default: true,
313
+ nonInteractive: process.env.NODE_ENV === "test",
314
+ });
315
+ if (confirmBackend) {
316
+ backendId = selectedBackendId;
288
317
  }
289
318
  else {
290
- backendId = backends[0].name.split("/").pop();
319
+ logger_1.logger.info("Available App Hosting backends:");
320
+ for (const id of backendIds) {
321
+ logger_1.logger.info(` - ${id}`);
322
+ }
323
+ const inputBackendId = await prompt.input({
324
+ message: "Please enter the name of the backend you would like to use:",
325
+ });
326
+ if (!backendIds.includes(inputBackendId)) {
327
+ throw new error_1.FirebaseError(`Invalid backend selected: ${inputBackendId}`, { exit: 1 });
328
+ }
329
+ backendId = inputBackendId;
291
330
  }
292
331
  logger_1.logger.info(`✅ Selected App Hosting backend: ${backendId}`);
293
332
  }
@@ -317,20 +356,33 @@ async function createFirebaseConfigs(rootPath, projectId) {
317
356
  logger_1.logger.info(`✅ Created firebase.json with backendId: ${backendId}`);
318
357
  }
319
358
  }
320
- async function writeAntigravityConfigs(rootPath) {
359
+ async function writeAntigravityConfigs(rootPath, framework) {
321
360
  const vscodeDir = path.join(rootPath, ".vscode");
322
361
  await fs.mkdir(vscodeDir, { recursive: true });
323
362
  const tasksJson = {
324
363
  version: "2.0.0",
325
- tasks: [
326
- {
327
- label: "npm-install",
328
- type: "shell",
329
- command: "npm install",
330
- problemMatcher: [],
331
- },
332
- ],
364
+ tasks: [],
333
365
  };
366
+ if (framework === "FLUTTER") {
367
+ tasksJson.tasks.push({
368
+ label: "flutter-pub-get",
369
+ type: "shell",
370
+ command: "flutter pub get",
371
+ problemMatcher: [],
372
+ group: {
373
+ kind: "build",
374
+ isDefault: true,
375
+ },
376
+ });
377
+ }
378
+ else {
379
+ tasksJson.tasks.push({
380
+ label: "npm-install",
381
+ type: "shell",
382
+ command: "npm install",
383
+ problemMatcher: [],
384
+ });
385
+ }
334
386
  await fs.writeFile(path.join(vscodeDir, "tasks.json"), JSON.stringify(tasksJson, null, 2));
335
387
  logger_1.logger.info("✅ Created .vscode/tasks.json");
336
388
  const settingsPath = path.join(vscodeDir, "settings.json");
@@ -354,19 +406,43 @@ async function writeAntigravityConfigs(rootPath) {
354
406
  logger_1.logger.info("✅ Updated .vscode/settings.json with startup preferences");
355
407
  const launchJson = {
356
408
  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
- ],
409
+ configurations: [],
369
410
  };
411
+ if (framework === "ANGULAR") {
412
+ launchJson.configurations.push({
413
+ type: "node",
414
+ request: "launch",
415
+ name: "Angular: debug server-side",
416
+ runtimeExecutable: "npm",
417
+ runtimeArgs: ["run", "start"],
418
+ port: 4200,
419
+ console: "integratedTerminal",
420
+ preLaunchTask: "npm-install",
421
+ });
422
+ }
423
+ else if (framework === "NEXT_JS") {
424
+ launchJson.configurations.push({
425
+ type: "node",
426
+ request: "launch",
427
+ name: "Next.js: debug server-side",
428
+ runtimeExecutable: "npm",
429
+ runtimeArgs: ["run", "dev"],
430
+ port: 9002,
431
+ console: "integratedTerminal",
432
+ preLaunchTask: "npm-install",
433
+ });
434
+ }
435
+ else if (framework === "FLUTTER") {
436
+ launchJson.configurations.push({
437
+ name: "Flutter",
438
+ request: "launch",
439
+ type: "dart",
440
+ preLaunchTask: "flutter-pub-get",
441
+ });
442
+ }
443
+ else {
444
+ return;
445
+ }
370
446
  await fs.writeFile(path.join(vscodeDir, "launch.json"), JSON.stringify(launchJson, null, 2));
371
447
  logger_1.logger.info("✅ Created .vscode/launch.json");
372
448
  }
@@ -392,6 +468,46 @@ async function cleanupUnusedFiles(rootPath) {
392
468
  const message = err instanceof Error ? err.message : String(err);
393
469
  logger_1.logger.debug(`Could not delete ${modifiedPath}: ${message}`);
394
470
  }
471
+ const mcpJsonPath = path.join(rootPath, ".idx", "mcp.json");
472
+ try {
473
+ await fs.unlink(mcpJsonPath);
474
+ logger_1.logger.info("✅ Cleaned up .idx/mcp.json");
475
+ }
476
+ catch (err) {
477
+ const message = err instanceof Error ? err.message : String(err);
478
+ logger_1.logger.debug(`Could not delete ${mcpJsonPath}: ${message}`);
479
+ }
480
+ }
481
+ async function upgradeGenkitVersion(rootPath) {
482
+ const packageJsonPath = path.join(rootPath, "package.json");
483
+ try {
484
+ const packageJsonContent = await fs.readFile(packageJsonPath, "utf8");
485
+ const packageJson = JSON.parse(packageJsonContent);
486
+ let modified = false;
487
+ const upgradeDeps = (deps) => {
488
+ if (!deps) {
489
+ return;
490
+ }
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
+ }
498
+ }
499
+ };
500
+ upgradeDeps(packageJson.dependencies);
501
+ upgradeDeps(packageJson.devDependencies);
502
+ if (modified) {
503
+ await fs.writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2) + "\n");
504
+ logger_1.logger.info("✅ Upgraded Genkit version to 1.29 in package.json");
505
+ }
506
+ }
507
+ catch (err) {
508
+ const message = err instanceof Error ? err.message : String(err);
509
+ logger_1.logger.debug(`Could not upgrade Genkit version: ${message}`);
510
+ }
395
511
  }
396
512
  async function uploadSecrets(rootPath, projectId) {
397
513
  if (!projectId) {
@@ -453,17 +569,36 @@ async function askToOpenAntigravity(rootPath, appName, startAntigravity) {
453
569
  }
454
570
  }
455
571
  }
572
+ async function checkDirectoryExists(dir) {
573
+ try {
574
+ const stat = await fs.stat(dir);
575
+ if (!stat.isDirectory()) {
576
+ throw new error_1.FirebaseError(`The path ${dir} is not a directory.`, { exit: 1 });
577
+ }
578
+ }
579
+ catch (err) {
580
+ if (err.code === "ENOENT") {
581
+ throw new error_1.FirebaseError(`The directory ${dir} does not exist.`, { exit: 1 });
582
+ }
583
+ throw err;
584
+ }
585
+ }
456
586
  async function migrate(rootPath, options = { startAntigravity: true }) {
587
+ await checkDirectoryExists(rootPath);
457
588
  const appType = await detectAppType(rootPath);
458
589
  void track.trackGA4("firebase_studio_migrate", { app_type: appType, result: "started" });
459
590
  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);
591
+ const { projectId, appName } = await extractMetadata(rootPath, options.project);
592
+ if (appType) {
593
+ logger_1.logger.info(`✅ Detected framework: ${appType}`);
594
+ }
595
+ await updateReadme(rootPath, appType);
462
596
  await createFirebaseConfigs(rootPath, projectId);
463
597
  await uploadSecrets(rootPath, projectId);
598
+ await upgradeGenkitVersion(rootPath);
464
599
  await injectAntigravityContext(rootPath, projectId, appName);
465
- await writeAntigravityConfigs(rootPath);
466
- await setupAntigravityMcpServer(rootPath);
600
+ await writeAntigravityConfigs(rootPath, appType);
601
+ await setupAntigravityMcpServer(rootPath, appType);
467
602
  await cleanupUnusedFiles(rootPath);
468
603
  const currentFolderName = path.basename(rootPath);
469
604
  if (currentFolderName === "download") {
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "firebase-tools",
3
- "version": "15.10.0",
3
+ "version": "15.10.1",
4
4
  "description": "Command-Line Interface for Firebase",
5
5
  "main": "./lib/index.js",
6
6
  "mcpName": "io.github.firebase/firebase-mcp",
@@ -1,11 +1,15 @@
1
- # ${appName}
1
+ # Welcome to Antigravity!
2
2
 
3
- This project was migrated from Firebase Studio.
4
- **Previous Name:** ${appName}
5
- **Export Date:** ${exportDate}
3
+ Welcome to your new developer home! Your Firebase Studio project has been successfully migrated to Antigravity.
6
4
 
7
- ${blueprintContent}
5
+ Antigravity is our next-generation, agent-first IDE designed for high-velocity, autonomous development. Because Antigravity runs locally on your machine, you now have access to powerful local workflows and fully integrated AI editing capabilities that go beyond a cloud-based web IDE.
8
6
 
9
- ---
7
+ ## Getting Started
8
+ - **Run Locally**: Use the **Run and Debug** menu on the left sidebar to start your local development server.
9
+ - Or in a terminal run `${startCommand}` and visit `${localUrl}`.
10
+ - **Deploy**: You can deploy your changes to Firebase App Hosting by using the integrated terminal and standard Firebase CLI commands, just as you did in Firebase Studio.
11
+ - **Cleanup**: Cleanup unused artifacts with the @cleanup workflow.
10
12
 
11
- To get started, run \`npm run dev\` and visit \`http://localhost:9002\`.
13
+ Enjoy the next era of AI-driven development!
14
+
15
+ **Firebase Studio Export Date:** ${exportDate}
@@ -20,6 +20,20 @@ DO NOT try to deploy to Firebase Hosting.
20
20
 
21
21
  - When asked to add AI features, use Genkit (as already configured in `src/ai/`).
22
22
 
23
+ # Understanding backend.json
24
+
25
+ - `docs/backend.json` if it exists, is a description of the data structure of this application used by the model at the time it was generated.
26
+ - The `backend.json` file is **NOT** a deployment script, a configuration file, or a source of truth for live cloud resources.
27
+ * **It is a BLUEPRINT, not the building.** It describes a *plan* for the data structure. It does **NOT** represent the currently deployed state. Think of it as an architect's drawing, not the physical house.
28
+ * **It has NO EFFECT on the backend.** The file's existence or content does not deploy, modify, or secure any cloud resources. Its sole purpose is to act as a static data source to ensure consistency during code generation.
29
+ * **It is NOT like Terraform.** Unlike a Terraform state file, this IR can be out of sync with the actual deployed resources. You must treat it as a self-contained, isolated definition and never assume it reflects reality.
30
+ - **What's inside:**
31
+ * `entities`: JSON Schema definitions for the application's data models.
32
+ * `auth`: Supported authentication providers.
33
+ * `firestore.structure`: Maps database paths to entities, including path wildcards (e.g., `{userId}`).
34
+ * `firestore.reasoning`: Explains the architectural decisions, such as denormalization and security rule strategies.
35
+ - **How to use it:** Use this file as a reference when creating or modifying Firestore security rules, data fetching logic, or when you need to understand the intended relationships between data entities.
36
+
23
37
  # Important
24
38
 
25
39
  Never display, log, or commit sensitive credentials, .env files, or service account keys.
@@ -0,0 +1,20 @@
1
+ ---
2
+ name: Cleanup
3
+ description: Run initial checks and clean up dead files.
4
+ ---
5
+
6
+ # Step 1: Check Compilation
7
+
8
+ Run \`npm run build\` (depending on the app type) to ensure the project is in a healthy state.
9
+
10
+ # Step 2: Cleanup Genkit config
11
+
12
+ Check to see if genkit is otherwise unused in this project, ask the user if they'd like to remove the configuration in src/ai/genkit.ts and remove related dependencies in package.json.
13
+
14
+ # Step 3: Cleanup dev.nix
15
+
16
+ Ask the developer if they'd like to remove the .idx/dev.nix file. Most times this file will be unused but the developer may have added some configurations that they'd like to keep.
17
+
18
+ # Step 4: Cleanup GEMINI.md
19
+
20
+ Check for the existence of GEMINI.md. The GEMINI.md (if it exists) was written for a Firebase Studio context. Much of this information is no longer relevant. Ask the user if they'd like you to update the contents or remove the file entirely so they can start with a fresh GEMINI.md.
@@ -18,6 +18,7 @@ runConfig:
18
18
  # - RUNTIME
19
19
 
20
20
  # Grant access to secrets in Cloud Secret Manager.
21
+ # You can create secrets with the CLI command `firebase apphosting:secrets:set <secretName>`.
21
22
  # See https://firebase.google.com/docs/app-hosting/configure#secret-parameters
22
23
  # - variable: MY_SECRET
23
24
  # secret: mySecretRef
@@ -1,12 +0,0 @@
1
- ---
2
- name: Initial Project Setup
3
- description: Run initial checks and fix common migration issues
4
- ---
5
-
6
- # Step 1: Check Compilation
7
-
8
- Run \`npm run typecheck\` and \`npm run build\` (depending on the app type) to ensure the project is in a healthy state.
9
-
10
- # Step 2: Cleanup Genkit config
11
-
12
- If genkit is otherwise unused in this project, ask the user if they'd like to remove the configuration in src/ai/genkit.ts and remove related dependencies in package.json.