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.
- package/lib/appdistribution/client.js +17 -0
- package/lib/apphosting/utils.js +14 -0
- package/lib/apptesting/parseTestFiles.js +25 -13
- package/lib/commands/apptesting.js +35 -16
- package/lib/commands/index.js +5 -5
- package/lib/commands/studio-export.js +2 -2
- package/lib/deploy/apphosting/util.js +1 -1
- package/lib/deploy/firestore/prepare.js +17 -0
- package/lib/deploy/functions/prepareFunctionsUpload.js +1 -1
- package/lib/deploy/functions/runtimes/dart/index.js +282 -0
- package/lib/deploy/functions/runtimes/index.js +1 -1
- package/lib/deploy/functions/runtimes/supported/index.js +4 -0
- package/lib/emulator/apphosting/serve.js +13 -15
- package/lib/emulator/downloadableEmulatorInfo.json +37 -37
- package/lib/emulator/functionsEmulator.js +103 -24
- package/lib/emulator/functionsRuntimeWorker.js +21 -18
- package/lib/firebase_studio/migrate.js +274 -95
- package/lib/init/features/functions/dart.js +31 -0
- package/lib/init/features/functions/index.js +14 -0
- package/lib/mcp/index.js +18 -6
- package/lib/tsconfig.publish.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/schema/firebase-config.json +7 -0
- package/templates/firebase-studio-export/readme_template.md +13 -7
- package/templates/firebase-studio-export/system_instructions_template.md +14 -0
- package/templates/firebase-studio-export/workflows/cleanup.md +20 -0
- package/templates/init/apphosting/apphosting.yaml +1 -0
- package/templates/init/functions/dart/_gitignore +11 -0
- package/templates/init/functions/dart/pubspec.yaml +14 -0
- package/templates/init/functions/dart/server.dart +15 -0
- package/lib/deploy/functions/runtimes/dart.js +0 -42
- 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
|
-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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.
|
|
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
|
-
|
|
156
|
-
const nameMatch =
|
|
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
|
|
190
|
+
return { projectId, appName };
|
|
168
191
|
}
|
|
169
|
-
async function updateReadme(rootPath,
|
|
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
|
|
173
|
-
|
|
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("${
|
|
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
|
-
|
|
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
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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
|
-
|
|
204
|
-
|
|
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(`✅
|
|
265
|
+
logger_1.logger.info(`✅ Added Antigravity skills`);
|
|
207
266
|
}
|
|
208
267
|
catch (err) {
|
|
209
|
-
utils.logWarning(`Could not
|
|
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
|
|
219
|
-
await fs.writeFile(path.join(workflowsDir, "
|
|
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
|
-
|
|
346
|
+
selectedBackendId = studioBackend.name.split("/").pop();
|
|
288
347
|
}
|
|
289
348
|
else {
|
|
290
|
-
|
|
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
|
-
|
|
461
|
-
await
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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;
|