firebase-tools 15.9.2-ct-studioexport3.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.
- package/lib/apphosting/utils.js +14 -0
- package/lib/apptesting/parseTestFiles.js +14 -5
- package/lib/commands/apptesting.js +20 -10
- package/lib/commands/index.js +5 -5
- package/lib/commands/studio-export.js +2 -2
- package/lib/dataconnect/load.js +24 -1
- package/lib/deploy/apphosting/util.js +1 -1
- package/lib/deploy/functions/prepareFunctionsUpload.js +1 -1
- package/lib/emulator/apphosting/serve.js +13 -15
- package/lib/emulator/downloadableEmulatorInfo.json +7 -7
- package/lib/firebase_studio/migrate.js +313 -142
- package/lib/init/features/hosting/index.js +71 -101
- package/lib/mcp/index.js +18 -6
- package/lib/track.js +1 -1
- package/package.json +1 -1
- package/templates/firebase-studio-export/readme_template.md +11 -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/firebase-studio-export/workflows/startup_workflow.md +0 -16
|
@@ -15,6 +15,66 @@ const track = require("../track");
|
|
|
15
15
|
const secrets_1 = require("../apphosting/secrets");
|
|
16
16
|
const env = require("../functions/env");
|
|
17
17
|
const error_1 = require("../error");
|
|
18
|
+
const os = require("os");
|
|
19
|
+
async function setupAntigravityMcpServer(rootPath, appType) {
|
|
20
|
+
const mcpConfigDir = path.join(os.homedir(), ".gemini", "antigravity");
|
|
21
|
+
const mcpConfigPath = path.join(mcpConfigDir, "mcp_config.json");
|
|
22
|
+
let mcpConfig = { mcpServers: {} };
|
|
23
|
+
try {
|
|
24
|
+
await fs.mkdir(mcpConfigDir, { recursive: true });
|
|
25
|
+
const content = await fs
|
|
26
|
+
.readFile(mcpConfigPath, "utf-8")
|
|
27
|
+
.catch((err) => {
|
|
28
|
+
if (err.code === "ENOENT") {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
throw err;
|
|
32
|
+
});
|
|
33
|
+
if (content) {
|
|
34
|
+
mcpConfig = JSON.parse(content);
|
|
35
|
+
if (!mcpConfig.mcpServers) {
|
|
36
|
+
mcpConfig.mcpServers = {};
|
|
37
|
+
}
|
|
38
|
+
}
|
|
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 {
|
|
49
|
+
logger_1.logger.info("ℹ️ Firebase MCP server already configured in Antigravity, skipping.");
|
|
50
|
+
}
|
|
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
|
+
}
|
|
72
|
+
}
|
|
73
|
+
catch (err) {
|
|
74
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
75
|
+
utils.logWarning(`Could not configure Antigravity MCP server: ${message}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
18
78
|
async function detectAppType(rootPath) {
|
|
19
79
|
try {
|
|
20
80
|
await fs.access(path.join(rootPath, "pubspec.yaml"));
|
|
@@ -52,36 +112,23 @@ async function detectAppType(rootPath) {
|
|
|
52
112
|
}
|
|
53
113
|
return "OTHER";
|
|
54
114
|
}
|
|
55
|
-
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
}
|
|
60
|
-
const items = (await response.json());
|
|
61
|
-
await fs.mkdir(localPath, { recursive: true });
|
|
62
|
-
for (const item of items) {
|
|
63
|
-
const itemLocalPath = path.join(localPath, item.name);
|
|
64
|
-
if (item.type === "dir") {
|
|
65
|
-
await downloadGitHubDir(item.url, itemLocalPath);
|
|
66
|
-
}
|
|
67
|
-
else if (item.type === "file") {
|
|
68
|
-
const fileResponse = await fetch(item.download_url);
|
|
69
|
-
if (fileResponse.ok) {
|
|
70
|
-
const content = await fileResponse.arrayBuffer();
|
|
71
|
-
await fs.writeFile(itemLocalPath, Buffer.from(content));
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
}
|
|
115
|
+
const isValidFirebaseProjectId = (projectId) => {
|
|
116
|
+
const projectIdRegex = /^[a-z][a-z0-9-]{4,28}[a-z0-9]$/;
|
|
117
|
+
return projectIdRegex.test(projectId);
|
|
118
|
+
};
|
|
76
119
|
async function extractMetadata(rootPath, overrideProjectId) {
|
|
120
|
+
const studioJsonPath = path.join(rootPath, "studio.json");
|
|
77
121
|
const metadataPath = path.join(rootPath, "metadata.json");
|
|
78
122
|
let metadata = {};
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
+
}
|
|
85
132
|
}
|
|
86
133
|
logger_1.logger.debug(`overrideProjectId ${overrideProjectId}`);
|
|
87
134
|
logger_1.logger.debug(`metadata.projectId ${metadata.projectId}`);
|
|
@@ -97,17 +144,21 @@ async function extractMetadata(rootPath, overrideProjectId) {
|
|
|
97
144
|
}
|
|
98
145
|
}
|
|
99
146
|
if (projectId) {
|
|
100
|
-
|
|
147
|
+
if (!isValidFirebaseProjectId(projectId)) {
|
|
148
|
+
throw new error_1.FirebaseError(`Invalid project ID: ${projectId}.`, {
|
|
149
|
+
exit: 1,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
logger_1.logger.info(`✅ Using Firebase Project: ${projectId}`);
|
|
101
153
|
}
|
|
102
154
|
else {
|
|
103
|
-
logger_1.logger.
|
|
155
|
+
logger_1.logger.debug(`❌ Failed to determine the Firebase Project ID. You can set a project later by setting the '--project' flag.`);
|
|
104
156
|
}
|
|
105
157
|
let appName = "firebase-studio-export";
|
|
106
|
-
let blueprintContent = "";
|
|
107
158
|
const blueprintPath = path.join(rootPath, "docs", "blueprint.md");
|
|
108
159
|
try {
|
|
109
|
-
|
|
110
|
-
const nameMatch =
|
|
160
|
+
const content = await fs.readFile(blueprintPath, "utf8");
|
|
161
|
+
const nameMatch = content.match(/# \*\*App Name\*\*: (.*)/);
|
|
111
162
|
if (nameMatch && nameMatch[1]) {
|
|
112
163
|
appName = nameMatch[1].trim();
|
|
113
164
|
}
|
|
@@ -118,72 +169,75 @@ async function extractMetadata(rootPath, overrideProjectId) {
|
|
|
118
169
|
if (appName !== "firebase-studio-export") {
|
|
119
170
|
logger_1.logger.info(`✅ Detected App Name: ${appName}`);
|
|
120
171
|
}
|
|
121
|
-
return { projectId, appName
|
|
172
|
+
return { projectId, appName };
|
|
122
173
|
}
|
|
123
|
-
async function updateReadme(rootPath,
|
|
174
|
+
async function updateReadme(rootPath, framework) {
|
|
124
175
|
const readmePath = path.join(rootPath, "README.md");
|
|
125
176
|
const readmeTemplate = await (0, templates_1.readTemplate)("firebase-studio-export/readme_template.md");
|
|
126
|
-
const
|
|
127
|
-
|
|
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
|
|
128
194
|
.replace("${exportDate}", new Date().toISOString().split("T")[0])
|
|
129
|
-
.replace("${
|
|
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
|
+
}
|
|
130
200
|
await fs.writeFile(readmePath, newReadme);
|
|
131
201
|
logger_1.logger.info("✅ Updated README.md with project details and origin info");
|
|
132
202
|
}
|
|
133
|
-
async function
|
|
134
|
-
const agentDir = path.join(rootPath, ".
|
|
203
|
+
async function injectAntigravityContext(rootPath, projectId, appName) {
|
|
204
|
+
const agentDir = path.join(rootPath, ".agents");
|
|
135
205
|
const rulesDir = path.join(agentDir, "rules");
|
|
136
206
|
const workflowsDir = path.join(agentDir, "workflows");
|
|
137
207
|
const skillsDir = path.join(agentDir, "skills");
|
|
138
208
|
await fs.mkdir(rulesDir, { recursive: true });
|
|
139
209
|
await fs.mkdir(workflowsDir, { recursive: true });
|
|
140
210
|
await fs.mkdir(skillsDir, { recursive: true });
|
|
141
|
-
logger_1.logger.info("⏳
|
|
211
|
+
logger_1.logger.info("⏳ Adding Antigravity skills...");
|
|
142
212
|
try {
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
if (
|
|
149
|
-
|
|
150
|
-
if (item.type === "dir") {
|
|
151
|
-
const skillName = item.name;
|
|
152
|
-
const skillDir = path.join(skillsDir, skillName);
|
|
153
|
-
await downloadGitHubDir(item.url, skillDir);
|
|
154
|
-
}
|
|
155
|
-
}
|
|
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;
|
|
156
220
|
}
|
|
157
|
-
|
|
158
|
-
|
|
221
|
+
if (result.status !== 0) {
|
|
222
|
+
throw new Error(`npx skills add exited with code ${result.status}`);
|
|
159
223
|
}
|
|
160
|
-
logger_1.logger.info(`✅
|
|
161
|
-
}
|
|
162
|
-
catch (err) {
|
|
163
|
-
utils.logWarning(`Could not download AGY skills, skipping. ${err}`);
|
|
164
|
-
}
|
|
165
|
-
logger_1.logger.info("⏳ Fetching Genkit skill...");
|
|
166
|
-
try {
|
|
167
|
-
const genkitSkillDir = path.join(skillsDir, "developing-genkit-js");
|
|
168
|
-
await downloadGitHubDir("https://api.github.com/repos/genkit-ai/skills/contents/skills/developing-genkit-js?ref=main", genkitSkillDir);
|
|
169
|
-
logger_1.logger.info(`✅ Downloaded Genkit skill`);
|
|
224
|
+
logger_1.logger.info(`✅ Added Antigravity skills`);
|
|
170
225
|
}
|
|
171
226
|
catch (err) {
|
|
172
|
-
utils.logWarning(`Could not
|
|
227
|
+
utils.logWarning(`Could not add Antigravity skills, skipping. ${err}`);
|
|
173
228
|
}
|
|
174
229
|
const systemInstructionsTemplate = await (0, templates_1.readTemplate)("firebase-studio-export/system_instructions_template.md");
|
|
175
|
-
const systemInstructions = systemInstructionsTemplate
|
|
176
|
-
.replace("${projectId}", projectId || "None")
|
|
177
|
-
.replace("${appName}", appName);
|
|
230
|
+
const systemInstructions = systemInstructionsTemplate.replace("${appName}", appName);
|
|
178
231
|
await fs.writeFile(path.join(rulesDir, "migration-context.md"), systemInstructions);
|
|
179
|
-
logger_1.logger.info("✅ Injected
|
|
232
|
+
logger_1.logger.info("✅ Injected Antigravity rules");
|
|
180
233
|
try {
|
|
181
|
-
const
|
|
182
|
-
await fs.writeFile(path.join(workflowsDir, "
|
|
183
|
-
logger_1.logger.info("✅ Created
|
|
234
|
+
const cleanupWorkflow = await (0, templates_1.readTemplate)("firebase-studio-export/workflows/cleanup.md");
|
|
235
|
+
await fs.writeFile(path.join(workflowsDir, "cleanup.md"), cleanupWorkflow);
|
|
236
|
+
logger_1.logger.info("✅ Created Antigravity startup workflow");
|
|
184
237
|
}
|
|
185
238
|
catch (err) {
|
|
186
|
-
|
|
239
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
240
|
+
logger_1.logger.debug(`Could not read or write startup workflow: ${message}`);
|
|
187
241
|
}
|
|
188
242
|
}
|
|
189
243
|
async function getAgyCommand(startAgy) {
|
|
@@ -193,7 +247,7 @@ async function getAgyCommand(startAgy) {
|
|
|
193
247
|
const commands = ["agy", "antigravity"];
|
|
194
248
|
for (const cmd of commands) {
|
|
195
249
|
if (utils.commandExistsSync(cmd)) {
|
|
196
|
-
logger_1.logger.info(`✅ Antigravity IDE
|
|
250
|
+
logger_1.logger.info(`✅ Antigravity IDE detected`);
|
|
197
251
|
return cmd;
|
|
198
252
|
}
|
|
199
253
|
}
|
|
@@ -201,14 +255,24 @@ async function getAgyCommand(startAgy) {
|
|
|
201
255
|
const macPath = "/Applications/Antigravity.app/Contents/Resources/app/bin/agy";
|
|
202
256
|
try {
|
|
203
257
|
await fs.access(macPath);
|
|
204
|
-
logger_1.logger.info(`✅ Antigravity IDE
|
|
258
|
+
logger_1.logger.info(`✅ Antigravity IDE detected at ${macPath}`);
|
|
205
259
|
return macPath;
|
|
206
260
|
}
|
|
207
261
|
catch {
|
|
208
262
|
}
|
|
209
263
|
}
|
|
264
|
+
if (process.platform === "win32") {
|
|
265
|
+
const winPath = path.join(process.env.LOCALAPPDATA || "", "Programs", "Antigravity", "bin", "agy.exe");
|
|
266
|
+
try {
|
|
267
|
+
await fs.access(winPath);
|
|
268
|
+
logger_1.logger.info(`✅ Antigravity IDE CLI detected at ${winPath}`);
|
|
269
|
+
return winPath;
|
|
270
|
+
}
|
|
271
|
+
catch {
|
|
272
|
+
}
|
|
273
|
+
}
|
|
210
274
|
const downloadLink = "https://antigravity.google/download";
|
|
211
|
-
logger_1.logger.info(`⚠️ Antigravity IDE
|
|
275
|
+
logger_1.logger.info(`⚠️ Antigravity IDE not found in your PATH. To ensure a seamless migration, please download and install Antigravity: ${downloadLink}`);
|
|
212
276
|
return undefined;
|
|
213
277
|
}
|
|
214
278
|
async function createFirebaseConfigs(rootPath, projectId) {
|
|
@@ -234,12 +298,35 @@ async function createFirebaseConfigs(rootPath, projectId) {
|
|
|
234
298
|
const backendsData = await apphosting.listBackends(projectId, "-");
|
|
235
299
|
const backends = backendsData.backends || [];
|
|
236
300
|
if (backends.length > 0) {
|
|
301
|
+
const backendIds = backends.map((b) => b.name.split("/").pop());
|
|
237
302
|
const studioBackend = backends.find((b) => b.name.endsWith("/studio") || b.name.toLowerCase().includes("studio"));
|
|
303
|
+
let selectedBackendId = "";
|
|
238
304
|
if (studioBackend) {
|
|
239
|
-
|
|
305
|
+
selectedBackendId = studioBackend.name.split("/").pop();
|
|
240
306
|
}
|
|
241
307
|
else {
|
|
242
|
-
|
|
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;
|
|
317
|
+
}
|
|
318
|
+
else {
|
|
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;
|
|
243
330
|
}
|
|
244
331
|
logger_1.logger.info(`✅ Selected App Hosting backend: ${backendId}`);
|
|
245
332
|
}
|
|
@@ -248,7 +335,8 @@ async function createFirebaseConfigs(rootPath, projectId) {
|
|
|
248
335
|
}
|
|
249
336
|
}
|
|
250
337
|
catch (err) {
|
|
251
|
-
|
|
338
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
339
|
+
utils.logWarning(`Could not fetch backends from Firebase CLI, using default "studio". ${message}`);
|
|
252
340
|
}
|
|
253
341
|
const firebaseJson = {
|
|
254
342
|
apphosting: {
|
|
@@ -256,7 +344,7 @@ async function createFirebaseConfigs(rootPath, projectId) {
|
|
|
256
344
|
ignore: [
|
|
257
345
|
"node_modules",
|
|
258
346
|
".git",
|
|
259
|
-
".
|
|
347
|
+
".agents",
|
|
260
348
|
".idx",
|
|
261
349
|
"firebase-debug.log",
|
|
262
350
|
"firebase-debug.*.log",
|
|
@@ -268,20 +356,33 @@ async function createFirebaseConfigs(rootPath, projectId) {
|
|
|
268
356
|
logger_1.logger.info(`✅ Created firebase.json with backendId: ${backendId}`);
|
|
269
357
|
}
|
|
270
358
|
}
|
|
271
|
-
async function
|
|
359
|
+
async function writeAntigravityConfigs(rootPath, framework) {
|
|
272
360
|
const vscodeDir = path.join(rootPath, ".vscode");
|
|
273
361
|
await fs.mkdir(vscodeDir, { recursive: true });
|
|
274
362
|
const tasksJson = {
|
|
275
363
|
version: "2.0.0",
|
|
276
|
-
tasks: [
|
|
277
|
-
{
|
|
278
|
-
label: "npm-install",
|
|
279
|
-
type: "shell",
|
|
280
|
-
command: "npm install",
|
|
281
|
-
problemMatcher: [],
|
|
282
|
-
},
|
|
283
|
-
],
|
|
364
|
+
tasks: [],
|
|
284
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
|
+
}
|
|
285
386
|
await fs.writeFile(path.join(vscodeDir, "tasks.json"), JSON.stringify(tasksJson, null, 2));
|
|
286
387
|
logger_1.logger.info("✅ Created .vscode/tasks.json");
|
|
287
388
|
const settingsPath = path.join(vscodeDir, "settings.json");
|
|
@@ -291,7 +392,8 @@ async function writeAgyConfigs(rootPath) {
|
|
|
291
392
|
settings = JSON.parse(settingsContent);
|
|
292
393
|
}
|
|
293
394
|
catch (err) {
|
|
294
|
-
|
|
395
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
396
|
+
logger_1.logger.debug(`Could not read ${settingsPath}: ${message}`);
|
|
295
397
|
}
|
|
296
398
|
const cleanSettings = {};
|
|
297
399
|
for (const [key, value] of Object.entries(settings)) {
|
|
@@ -304,32 +406,48 @@ async function writeAgyConfigs(rootPath) {
|
|
|
304
406
|
logger_1.logger.info("✅ Updated .vscode/settings.json with startup preferences");
|
|
305
407
|
const launchJson = {
|
|
306
408
|
version: "0.2.0",
|
|
307
|
-
configurations: [
|
|
308
|
-
{
|
|
309
|
-
type: "node",
|
|
310
|
-
request: "launch",
|
|
311
|
-
name: "Next.js: debug server-side",
|
|
312
|
-
runtimeExecutable: "npm",
|
|
313
|
-
runtimeArgs: ["run", "dev"],
|
|
314
|
-
port: 9002,
|
|
315
|
-
console: "integratedTerminal",
|
|
316
|
-
preLaunchTask: "npm-install",
|
|
317
|
-
},
|
|
318
|
-
],
|
|
409
|
+
configurations: [],
|
|
319
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
|
+
}
|
|
320
446
|
await fs.writeFile(path.join(vscodeDir, "launch.json"), JSON.stringify(launchJson, null, 2));
|
|
321
447
|
logger_1.logger.info("✅ Created .vscode/launch.json");
|
|
322
448
|
}
|
|
323
449
|
async function cleanupUnusedFiles(rootPath) {
|
|
324
450
|
const docsDir = path.join(rootPath, "docs");
|
|
325
|
-
const blueprintPath = path.join(docsDir, "blueprint.md");
|
|
326
|
-
try {
|
|
327
|
-
await fs.unlink(blueprintPath);
|
|
328
|
-
logger_1.logger.info("✅ Cleaned up docs/blueprint.md");
|
|
329
|
-
}
|
|
330
|
-
catch (err) {
|
|
331
|
-
logger_1.logger.debug(`Could not delete ${blueprintPath}: ${err}`);
|
|
332
|
-
}
|
|
333
451
|
try {
|
|
334
452
|
const files = await fs.readdir(docsDir);
|
|
335
453
|
if (files.length === 0) {
|
|
@@ -338,23 +456,57 @@ async function cleanupUnusedFiles(rootPath) {
|
|
|
338
456
|
}
|
|
339
457
|
}
|
|
340
458
|
catch (err) {
|
|
341
|
-
|
|
459
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
460
|
+
logger_1.logger.debug(`Could not remove ${docsDir}: ${message}`);
|
|
342
461
|
}
|
|
343
|
-
const
|
|
462
|
+
const modifiedPath = path.join(rootPath, ".modified");
|
|
463
|
+
try {
|
|
464
|
+
await fs.unlink(modifiedPath);
|
|
465
|
+
logger_1.logger.info("✅ Cleaned up .modified");
|
|
466
|
+
}
|
|
467
|
+
catch (err) {
|
|
468
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
469
|
+
logger_1.logger.debug(`Could not delete ${modifiedPath}: ${message}`);
|
|
470
|
+
}
|
|
471
|
+
const mcpJsonPath = path.join(rootPath, ".idx", "mcp.json");
|
|
344
472
|
try {
|
|
345
|
-
await fs.unlink(
|
|
346
|
-
logger_1.logger.info("✅ Cleaned up
|
|
473
|
+
await fs.unlink(mcpJsonPath);
|
|
474
|
+
logger_1.logger.info("✅ Cleaned up .idx/mcp.json");
|
|
347
475
|
}
|
|
348
476
|
catch (err) {
|
|
349
|
-
|
|
477
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
478
|
+
logger_1.logger.debug(`Could not delete ${mcpJsonPath}: ${message}`);
|
|
350
479
|
}
|
|
351
|
-
|
|
480
|
+
}
|
|
481
|
+
async function upgradeGenkitVersion(rootPath) {
|
|
482
|
+
const packageJsonPath = path.join(rootPath, "package.json");
|
|
352
483
|
try {
|
|
353
|
-
await fs.
|
|
354
|
-
|
|
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
|
+
}
|
|
355
506
|
}
|
|
356
507
|
catch (err) {
|
|
357
|
-
|
|
508
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
509
|
+
logger_1.logger.debug(`Could not upgrade Genkit version: ${message}`);
|
|
358
510
|
}
|
|
359
511
|
}
|
|
360
512
|
async function uploadSecrets(rootPath, projectId) {
|
|
@@ -382,57 +534,76 @@ async function uploadSecrets(rootPath, projectId) {
|
|
|
382
534
|
}
|
|
383
535
|
}
|
|
384
536
|
catch (err) {
|
|
385
|
-
|
|
537
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
538
|
+
utils.logWarning(`Failed to upload GEMINI_API_KEY secret: ${message}`);
|
|
386
539
|
}
|
|
387
540
|
}
|
|
388
|
-
async function askToOpenAntigravity(rootPath, appName,
|
|
389
|
-
const agyCommand = await getAgyCommand(
|
|
390
|
-
|
|
391
|
-
|
|
541
|
+
async function askToOpenAntigravity(rootPath, appName, startAntigravity) {
|
|
542
|
+
const agyCommand = await getAgyCommand(startAntigravity);
|
|
543
|
+
logger_1.logger.info(`\n🎉 Your Firebase Studio project "${appName}" is now ready for Antigravity!`);
|
|
544
|
+
logger_1.logger.info("Antigravity is Google's agentic IDE, where you can collaborate with AI agents to build, test, and deploy your application.");
|
|
545
|
+
logger_1.logger.info("\nWhat to do next inside Antigravity:");
|
|
546
|
+
logger_1.logger.info(" 1. Review the README.md: It has been updated with specifics about this migrated project.");
|
|
547
|
+
logger_1.logger.info(" 2. Open the Agent Chat: Use the side panel or press Cmd+L (Ctrl+L on Windows/Linux). This is your main interface with the AI.");
|
|
548
|
+
logger_1.logger.info("\nFile any bugs at https://github.com/firebase/firebase-tools/issues");
|
|
549
|
+
if (!startAntigravity || !agyCommand) {
|
|
392
550
|
return;
|
|
393
551
|
}
|
|
394
552
|
const answer = await prompt.confirm({
|
|
395
|
-
message:
|
|
553
|
+
message: "Would you like to open it in Antigravity now?",
|
|
396
554
|
default: true,
|
|
397
555
|
});
|
|
398
556
|
if (answer) {
|
|
399
557
|
logger_1.logger.info(`⏳ Opening ${appName} in Antigravity...`);
|
|
400
558
|
try {
|
|
401
|
-
const
|
|
559
|
+
const antigravityProcess = (0, child_process_1.spawn)(agyCommand, ["."], {
|
|
402
560
|
cwd: rootPath,
|
|
403
561
|
stdio: "ignore",
|
|
404
562
|
detached: true,
|
|
563
|
+
shell: process.platform === "win32",
|
|
405
564
|
});
|
|
406
|
-
|
|
565
|
+
antigravityProcess.unref();
|
|
407
566
|
}
|
|
408
567
|
catch (err) {
|
|
409
568
|
utils.logWarning("Could not open Antigravity IDE automatically. Please open it manually.");
|
|
410
569
|
}
|
|
411
570
|
}
|
|
412
|
-
else {
|
|
413
|
-
logger_1.logger.info('\n👉 Next steps: Open this folder in Antigravity and run the "Initial Project Setup" workflow.');
|
|
414
|
-
}
|
|
415
571
|
}
|
|
416
|
-
async function
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
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;
|
|
421
584
|
}
|
|
585
|
+
}
|
|
586
|
+
async function migrate(rootPath, options = { startAntigravity: true }) {
|
|
587
|
+
await checkDirectoryExists(rootPath);
|
|
422
588
|
const appType = await detectAppType(rootPath);
|
|
423
589
|
void track.trackGA4("firebase_studio_migrate", { app_type: appType, result: "started" });
|
|
424
590
|
logger_1.logger.info("🚀 Starting Firebase Studio to Antigravity migration...");
|
|
425
|
-
const { projectId, appName
|
|
426
|
-
|
|
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);
|
|
427
596
|
await createFirebaseConfigs(rootPath, projectId);
|
|
428
597
|
await uploadSecrets(rootPath, projectId);
|
|
429
|
-
await
|
|
430
|
-
await
|
|
598
|
+
await upgradeGenkitVersion(rootPath);
|
|
599
|
+
await injectAntigravityContext(rootPath, projectId, appName);
|
|
600
|
+
await writeAntigravityConfigs(rootPath, appType);
|
|
601
|
+
await setupAntigravityMcpServer(rootPath, appType);
|
|
431
602
|
await cleanupUnusedFiles(rootPath);
|
|
432
603
|
const currentFolderName = path.basename(rootPath);
|
|
433
604
|
if (currentFolderName === "download") {
|
|
434
|
-
logger_1.logger.info(`\n💡 Tip: You
|
|
605
|
+
logger_1.logger.info(`\n💡 Tip: You may want to rename this folder to "${appName.toLowerCase().replace(/\s+/g, "-")}"`);
|
|
435
606
|
}
|
|
436
607
|
await track.trackGA4("firebase_studio_migrate", { app_type: appType, result: "success" });
|
|
437
|
-
await askToOpenAntigravity(rootPath, appName, options.
|
|
608
|
+
await askToOpenAntigravity(rootPath, appName, options.startAntigravity);
|
|
438
609
|
}
|