firebase-tools 15.9.1 ā 15.10.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/distribution.js +1 -1
- package/lib/appdistribution/options-parser-util.js +7 -0
- package/lib/apphosting/config.js +24 -1
- package/lib/commands/appdistribution-distribute.js +5 -5
- package/lib/commands/apptesting.js +9 -10
- package/lib/commands/dataconnect-sql-setup.js +3 -4
- package/lib/commands/dataconnect-sql-shell.js +2 -1
- package/lib/commands/index.js +4 -6
- package/lib/commands/studio-export.js +24 -6
- package/lib/dataconnect/load.js +24 -1
- package/lib/dataconnect/schemaMigration.js +25 -17
- package/lib/dataconnect/types.js +1 -0
- package/lib/deploy/functions/build.js +24 -9
- package/lib/deploy/functions/runtimes/discovery/v1alpha1.js +8 -2
- package/lib/emulator/apphosting/config.js +1 -21
- package/lib/emulator/controller.js +2 -1
- package/lib/emulator/hub.js +2 -0
- package/lib/emulator/hubExport.js +7 -6
- package/lib/experiments.js +5 -5
- package/lib/firebase_studio/migrate.js +162 -66
- package/lib/gcp/cloudfunctionsv2.js +23 -2
- package/lib/gcp/cloudsql/permissionsSetup.js +4 -4
- package/lib/init/features/hosting/index.js +71 -101
- package/lib/mcp/tools/apptesting/tests.js +2 -2
- package/lib/track.js +1 -1
- package/package.json +1 -1
- package/schema/dataconnect-yaml.json +4 -0
- package/templates/firebase-studio-export/system_instructions_template.md +14 -3
- package/templates/firebase-studio-export/workflows/startup_workflow.md +3 -7
|
@@ -19,6 +19,7 @@ class HubExport {
|
|
|
19
19
|
this.projectId = projectId;
|
|
20
20
|
this.options = options;
|
|
21
21
|
this.exportPath = options.path;
|
|
22
|
+
this.exportTargets = options.targets ?? [...types_1.IMPORT_EXPORT_EMULATORS];
|
|
22
23
|
this.tmpDir = fs.mkdtempSync(`firebase-export-${new Date().getTime()}`);
|
|
23
24
|
}
|
|
24
25
|
static readMetadata(exportPath) {
|
|
@@ -36,14 +37,14 @@ class HubExport {
|
|
|
36
37
|
}
|
|
37
38
|
}
|
|
38
39
|
async exportAll() {
|
|
39
|
-
const toExport = types_1.ALL_EMULATORS.filter(shouldExport);
|
|
40
|
+
const toExport = types_1.ALL_EMULATORS.filter(shouldExport).filter((e) => this.exportTargets.includes(e));
|
|
40
41
|
if (toExport.length === 0) {
|
|
41
42
|
throw new error_1.FirebaseError("No running emulators support import/export.");
|
|
42
43
|
}
|
|
43
44
|
const metadata = {
|
|
44
45
|
version: hub_1.EmulatorHub.CLI_VERSION,
|
|
45
46
|
};
|
|
46
|
-
if (shouldExport(types_1.Emulators.FIRESTORE)) {
|
|
47
|
+
if (shouldExport(types_1.Emulators.FIRESTORE) && toExport.includes(types_1.Emulators.FIRESTORE)) {
|
|
47
48
|
metadata.firestore = {
|
|
48
49
|
version: (0, downloadableEmulators_1.getDownloadDetails)(types_1.Emulators.FIRESTORE).version,
|
|
49
50
|
path: "firestore_export",
|
|
@@ -51,28 +52,28 @@ class HubExport {
|
|
|
51
52
|
};
|
|
52
53
|
await this.exportFirestore(metadata);
|
|
53
54
|
}
|
|
54
|
-
if (shouldExport(types_1.Emulators.DATABASE)) {
|
|
55
|
+
if (shouldExport(types_1.Emulators.DATABASE) && toExport.includes(types_1.Emulators.DATABASE)) {
|
|
55
56
|
metadata.database = {
|
|
56
57
|
version: (0, downloadableEmulators_1.getDownloadDetails)(types_1.Emulators.DATABASE).version,
|
|
57
58
|
path: "database_export",
|
|
58
59
|
};
|
|
59
60
|
await this.exportDatabase(metadata);
|
|
60
61
|
}
|
|
61
|
-
if (shouldExport(types_1.Emulators.AUTH)) {
|
|
62
|
+
if (shouldExport(types_1.Emulators.AUTH) && toExport.includes(types_1.Emulators.AUTH)) {
|
|
62
63
|
metadata.auth = {
|
|
63
64
|
version: hub_1.EmulatorHub.CLI_VERSION,
|
|
64
65
|
path: "auth_export",
|
|
65
66
|
};
|
|
66
67
|
await this.exportAuth(metadata);
|
|
67
68
|
}
|
|
68
|
-
if (shouldExport(types_1.Emulators.STORAGE)) {
|
|
69
|
+
if (shouldExport(types_1.Emulators.STORAGE) && toExport.includes(types_1.Emulators.STORAGE)) {
|
|
69
70
|
metadata.storage = {
|
|
70
71
|
version: hub_1.EmulatorHub.CLI_VERSION,
|
|
71
72
|
path: "storage_export",
|
|
72
73
|
};
|
|
73
74
|
await this.exportStorage(metadata);
|
|
74
75
|
}
|
|
75
|
-
if (shouldExport(types_1.Emulators.DATACONNECT)) {
|
|
76
|
+
if (shouldExport(types_1.Emulators.DATACONNECT) && toExport.includes(types_1.Emulators.DATACONNECT)) {
|
|
76
77
|
metadata.dataconnect = {
|
|
77
78
|
version: hub_1.EmulatorHub.CLI_VERSION,
|
|
78
79
|
path: "dataconnect_export",
|
package/lib/experiments.js
CHANGED
|
@@ -104,6 +104,11 @@ exports.ALL_EXPERIMENTS = experiments({
|
|
|
104
104
|
default: true,
|
|
105
105
|
public: false,
|
|
106
106
|
},
|
|
107
|
+
apphostinglocalbuilds: {
|
|
108
|
+
shortDescription: "Enable App Hosting local builds",
|
|
109
|
+
default: false,
|
|
110
|
+
public: false,
|
|
111
|
+
},
|
|
107
112
|
dataconnect: {
|
|
108
113
|
shortDescription: "Deprecated. Previosuly, enabled Data Connect related features.",
|
|
109
114
|
fullDescription: "Deprecated. Previously, enabled Data Connect related features.",
|
|
@@ -145,11 +150,6 @@ exports.ALL_EXPERIMENTS = experiments({
|
|
|
145
150
|
default: true,
|
|
146
151
|
public: false,
|
|
147
152
|
},
|
|
148
|
-
studioexport: {
|
|
149
|
-
shortDescription: "Enable the experimental studio:export command.",
|
|
150
|
-
default: false,
|
|
151
|
-
public: false,
|
|
152
|
-
},
|
|
153
153
|
});
|
|
154
154
|
function isValidExperiment(name) {
|
|
155
155
|
return Object.keys(exports.ALL_EXPERIMENTS).includes(name);
|
|
@@ -7,13 +7,88 @@ const fs = require("fs/promises");
|
|
|
7
7
|
const path = require("path");
|
|
8
8
|
const child_process_1 = require("child_process");
|
|
9
9
|
const logger_1 = require("../logger");
|
|
10
|
-
const error_1 = require("../error");
|
|
11
10
|
const prompt = require("../prompt");
|
|
12
11
|
const apphosting = require("../gcp/apphosting");
|
|
13
12
|
const utils = require("../utils");
|
|
14
13
|
const templates_1 = require("../templates");
|
|
14
|
+
const track = require("../track");
|
|
15
15
|
const secrets_1 = require("../apphosting/secrets");
|
|
16
16
|
const env = require("../functions/env");
|
|
17
|
+
const error_1 = require("../error");
|
|
18
|
+
const os = require("os");
|
|
19
|
+
async function setupAntigravityMcpServer(rootPath) {
|
|
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
|
+
if (mcpConfig.mcpServers["firebase"]) {
|
|
40
|
+
logger_1.logger.info("ā¹ļø Firebase MCP server already configured in Antigravity, skipping.");
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
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}`);
|
|
49
|
+
}
|
|
50
|
+
catch (err) {
|
|
51
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
52
|
+
utils.logWarning(`Could not configure Antigravity MCP server: ${message}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
async function detectAppType(rootPath) {
|
|
56
|
+
try {
|
|
57
|
+
await fs.access(path.join(rootPath, "pubspec.yaml"));
|
|
58
|
+
return "FLUTTER";
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
}
|
|
62
|
+
try {
|
|
63
|
+
await fs.access(path.join(rootPath, "angular.json"));
|
|
64
|
+
return "ANGULAR";
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
}
|
|
68
|
+
try {
|
|
69
|
+
const packageJsonPath = path.join(rootPath, "package.json");
|
|
70
|
+
const packageJsonContent = await fs.readFile(packageJsonPath, "utf8");
|
|
71
|
+
const packageJson = JSON.parse(packageJsonContent);
|
|
72
|
+
const deps = { ...packageJson.dependencies, ...packageJson.devDependencies };
|
|
73
|
+
if (deps.next) {
|
|
74
|
+
return "NEXT_JS";
|
|
75
|
+
}
|
|
76
|
+
if (deps["@angular/core"]) {
|
|
77
|
+
return "ANGULAR";
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
}
|
|
82
|
+
for (const configFile of ["next.config.js", "next.config.mjs"]) {
|
|
83
|
+
try {
|
|
84
|
+
await fs.access(path.join(rootPath, configFile));
|
|
85
|
+
return "NEXT_JS";
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return "OTHER";
|
|
91
|
+
}
|
|
17
92
|
async function downloadGitHubDir(apiUrl, localPath) {
|
|
18
93
|
const response = await fetch(apiUrl);
|
|
19
94
|
if (!response.ok) {
|
|
@@ -35,6 +110,10 @@ async function downloadGitHubDir(apiUrl, localPath) {
|
|
|
35
110
|
}
|
|
36
111
|
}
|
|
37
112
|
}
|
|
113
|
+
const isValidFirebaseProjectId = (projectId) => {
|
|
114
|
+
const projectIdRegex = /^[a-z][a-z0-9-]{4,28}[a-z0-9]$/;
|
|
115
|
+
return projectIdRegex.test(projectId);
|
|
116
|
+
};
|
|
38
117
|
async function extractMetadata(rootPath, overrideProjectId) {
|
|
39
118
|
const metadataPath = path.join(rootPath, "metadata.json");
|
|
40
119
|
let metadata = {};
|
|
@@ -45,6 +124,8 @@ async function extractMetadata(rootPath, overrideProjectId) {
|
|
|
45
124
|
catch (err) {
|
|
46
125
|
logger_1.logger.debug(`Could not read metadata.json at ${metadataPath}: ${err}`);
|
|
47
126
|
}
|
|
127
|
+
logger_1.logger.debug(`overrideProjectId ${overrideProjectId}`);
|
|
128
|
+
logger_1.logger.debug(`metadata.projectId ${metadata.projectId}`);
|
|
48
129
|
let projectId = overrideProjectId || metadata.projectId;
|
|
49
130
|
if (!projectId) {
|
|
50
131
|
try {
|
|
@@ -57,10 +138,15 @@ async function extractMetadata(rootPath, overrideProjectId) {
|
|
|
57
138
|
}
|
|
58
139
|
}
|
|
59
140
|
if (projectId) {
|
|
60
|
-
|
|
141
|
+
if (!isValidFirebaseProjectId(projectId)) {
|
|
142
|
+
throw new error_1.FirebaseError(`Invalid project ID: ${projectId}.`, {
|
|
143
|
+
exit: 1,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
logger_1.logger.info(`ā
Using Firebase Project: ${projectId}`);
|
|
61
147
|
}
|
|
62
148
|
else {
|
|
63
|
-
logger_1.logger.info(
|
|
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.`);
|
|
64
150
|
}
|
|
65
151
|
let appName = "firebase-studio-export";
|
|
66
152
|
let blueprintContent = "";
|
|
@@ -90,15 +176,15 @@ async function updateReadme(rootPath, blueprintContent, appName) {
|
|
|
90
176
|
await fs.writeFile(readmePath, newReadme);
|
|
91
177
|
logger_1.logger.info("ā
Updated README.md with project details and origin info");
|
|
92
178
|
}
|
|
93
|
-
async function
|
|
94
|
-
const agentDir = path.join(rootPath, ".
|
|
179
|
+
async function injectAntigravityContext(rootPath, projectId, appName) {
|
|
180
|
+
const agentDir = path.join(rootPath, ".agents");
|
|
95
181
|
const rulesDir = path.join(agentDir, "rules");
|
|
96
182
|
const workflowsDir = path.join(agentDir, "workflows");
|
|
97
183
|
const skillsDir = path.join(agentDir, "skills");
|
|
98
184
|
await fs.mkdir(rulesDir, { recursive: true });
|
|
99
185
|
await fs.mkdir(workflowsDir, { recursive: true });
|
|
100
186
|
await fs.mkdir(skillsDir, { recursive: true });
|
|
101
|
-
logger_1.logger.info("ā³ Fetching
|
|
187
|
+
logger_1.logger.info("ā³ Fetching Antigravity skills from firebase/agent-skills...");
|
|
102
188
|
try {
|
|
103
189
|
const skillsResponse = await fetch("https://api.github.com/repos/firebase/agent-skills/contents/skills");
|
|
104
190
|
if (!skillsResponse.ok) {
|
|
@@ -120,44 +206,58 @@ async function injectAgyContext(rootPath, projectId, appName) {
|
|
|
120
206
|
logger_1.logger.info(`ā
Downloaded Firebase skills`);
|
|
121
207
|
}
|
|
122
208
|
catch (err) {
|
|
123
|
-
utils.logWarning(`Could not download
|
|
124
|
-
}
|
|
125
|
-
logger_1.logger.info("ā³ Fetching Genkit skill...");
|
|
126
|
-
try {
|
|
127
|
-
const genkitSkillDir = path.join(skillsDir, "developing-genkit-js");
|
|
128
|
-
await downloadGitHubDir("https://api.github.com/repos/genkit-ai/skills/contents/skills/developing-genkit-js?ref=main", genkitSkillDir);
|
|
129
|
-
logger_1.logger.info(`ā
Downloaded Genkit skill`);
|
|
130
|
-
}
|
|
131
|
-
catch (err) {
|
|
132
|
-
utils.logWarning(`Could not download Genkit skill, skipping. ${err}`);
|
|
209
|
+
utils.logWarning(`Could not download Antigravity skills, skipping. ${err}`);
|
|
133
210
|
}
|
|
134
211
|
const systemInstructionsTemplate = await (0, templates_1.readTemplate)("firebase-studio-export/system_instructions_template.md");
|
|
135
212
|
const systemInstructions = systemInstructionsTemplate
|
|
136
213
|
.replace("${projectId}", projectId || "None")
|
|
137
214
|
.replace("${appName}", appName);
|
|
138
215
|
await fs.writeFile(path.join(rulesDir, "migration-context.md"), systemInstructions);
|
|
139
|
-
logger_1.logger.info("ā
Injected
|
|
216
|
+
logger_1.logger.info("ā
Injected Antigravity rules");
|
|
140
217
|
try {
|
|
141
218
|
const startupWorkflow = await (0, templates_1.readTemplate)("firebase-studio-export/workflows/startup_workflow.md");
|
|
142
219
|
await fs.writeFile(path.join(workflowsDir, "startup.md"), startupWorkflow);
|
|
143
|
-
logger_1.logger.info("ā
Created
|
|
220
|
+
logger_1.logger.info("ā
Created Antigravity startup workflow");
|
|
144
221
|
}
|
|
145
222
|
catch (err) {
|
|
146
|
-
|
|
223
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
224
|
+
logger_1.logger.debug(`Could not read or write startup workflow: ${message}`);
|
|
147
225
|
}
|
|
148
226
|
}
|
|
149
|
-
async function
|
|
150
|
-
if (startAgy
|
|
151
|
-
return;
|
|
227
|
+
async function getAgyCommand(startAgy) {
|
|
228
|
+
if (!startAgy) {
|
|
229
|
+
return undefined;
|
|
230
|
+
}
|
|
231
|
+
const commands = ["agy", "antigravity"];
|
|
232
|
+
for (const cmd of commands) {
|
|
233
|
+
if (utils.commandExistsSync(cmd)) {
|
|
234
|
+
logger_1.logger.info(`ā
Antigravity IDE detected`);
|
|
235
|
+
return cmd;
|
|
236
|
+
}
|
|
152
237
|
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
238
|
+
if (process.platform === "darwin") {
|
|
239
|
+
const macPath = "/Applications/Antigravity.app/Contents/Resources/app/bin/agy";
|
|
240
|
+
try {
|
|
241
|
+
await fs.access(macPath);
|
|
242
|
+
logger_1.logger.info(`ā
Antigravity IDE detected at ${macPath}`);
|
|
243
|
+
return macPath;
|
|
244
|
+
}
|
|
245
|
+
catch {
|
|
246
|
+
}
|
|
156
247
|
}
|
|
157
|
-
|
|
158
|
-
const
|
|
159
|
-
|
|
248
|
+
if (process.platform === "win32") {
|
|
249
|
+
const winPath = path.join(process.env.LOCALAPPDATA || "", "Programs", "Antigravity", "bin", "agy.exe");
|
|
250
|
+
try {
|
|
251
|
+
await fs.access(winPath);
|
|
252
|
+
logger_1.logger.info(`ā
Antigravity IDE CLI detected at ${winPath}`);
|
|
253
|
+
return winPath;
|
|
254
|
+
}
|
|
255
|
+
catch {
|
|
256
|
+
}
|
|
160
257
|
}
|
|
258
|
+
const downloadLink = "https://antigravity.google/download";
|
|
259
|
+
logger_1.logger.info(`ā ļø Antigravity IDE not found in your PATH. To ensure a seamless migration, please download and install Antigravity: ${downloadLink}`);
|
|
260
|
+
return undefined;
|
|
161
261
|
}
|
|
162
262
|
async function createFirebaseConfigs(rootPath, projectId) {
|
|
163
263
|
if (!projectId) {
|
|
@@ -196,7 +296,8 @@ async function createFirebaseConfigs(rootPath, projectId) {
|
|
|
196
296
|
}
|
|
197
297
|
}
|
|
198
298
|
catch (err) {
|
|
199
|
-
|
|
299
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
300
|
+
utils.logWarning(`Could not fetch backends from Firebase CLI, using default "studio". ${message}`);
|
|
200
301
|
}
|
|
201
302
|
const firebaseJson = {
|
|
202
303
|
apphosting: {
|
|
@@ -204,7 +305,7 @@ async function createFirebaseConfigs(rootPath, projectId) {
|
|
|
204
305
|
ignore: [
|
|
205
306
|
"node_modules",
|
|
206
307
|
".git",
|
|
207
|
-
".
|
|
308
|
+
".agents",
|
|
208
309
|
".idx",
|
|
209
310
|
"firebase-debug.log",
|
|
210
311
|
"firebase-debug.*.log",
|
|
@@ -216,7 +317,7 @@ async function createFirebaseConfigs(rootPath, projectId) {
|
|
|
216
317
|
logger_1.logger.info(`ā
Created firebase.json with backendId: ${backendId}`);
|
|
217
318
|
}
|
|
218
319
|
}
|
|
219
|
-
async function
|
|
320
|
+
async function writeAntigravityConfigs(rootPath) {
|
|
220
321
|
const vscodeDir = path.join(rootPath, ".vscode");
|
|
221
322
|
await fs.mkdir(vscodeDir, { recursive: true });
|
|
222
323
|
const tasksJson = {
|
|
@@ -239,7 +340,8 @@ async function writeAgyConfigs(rootPath) {
|
|
|
239
340
|
settings = JSON.parse(settingsContent);
|
|
240
341
|
}
|
|
241
342
|
catch (err) {
|
|
242
|
-
|
|
343
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
344
|
+
logger_1.logger.debug(`Could not read ${settingsPath}: ${message}`);
|
|
243
345
|
}
|
|
244
346
|
const cleanSettings = {};
|
|
245
347
|
for (const [key, value] of Object.entries(settings)) {
|
|
@@ -270,14 +372,6 @@ async function writeAgyConfigs(rootPath) {
|
|
|
270
372
|
}
|
|
271
373
|
async function cleanupUnusedFiles(rootPath) {
|
|
272
374
|
const docsDir = path.join(rootPath, "docs");
|
|
273
|
-
const blueprintPath = path.join(docsDir, "blueprint.md");
|
|
274
|
-
try {
|
|
275
|
-
await fs.unlink(blueprintPath);
|
|
276
|
-
logger_1.logger.info("ā
Cleaned up docs/blueprint.md");
|
|
277
|
-
}
|
|
278
|
-
catch (err) {
|
|
279
|
-
logger_1.logger.debug(`Could not delete ${blueprintPath}: ${err}`);
|
|
280
|
-
}
|
|
281
375
|
try {
|
|
282
376
|
const files = await fs.readdir(docsDir);
|
|
283
377
|
if (files.length === 0) {
|
|
@@ -286,15 +380,8 @@ async function cleanupUnusedFiles(rootPath) {
|
|
|
286
380
|
}
|
|
287
381
|
}
|
|
288
382
|
catch (err) {
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
const metadataPath = path.join(rootPath, "metadata.json");
|
|
292
|
-
try {
|
|
293
|
-
await fs.unlink(metadataPath);
|
|
294
|
-
logger_1.logger.info("ā
Cleaned up metadata.json");
|
|
295
|
-
}
|
|
296
|
-
catch (err) {
|
|
297
|
-
logger_1.logger.debug(`Could not delete ${metadataPath}: ${err}`);
|
|
383
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
384
|
+
logger_1.logger.debug(`Could not remove ${docsDir}: ${message}`);
|
|
298
385
|
}
|
|
299
386
|
const modifiedPath = path.join(rootPath, ".modified");
|
|
300
387
|
try {
|
|
@@ -302,7 +389,8 @@ async function cleanupUnusedFiles(rootPath) {
|
|
|
302
389
|
logger_1.logger.info("ā
Cleaned up .modified");
|
|
303
390
|
}
|
|
304
391
|
catch (err) {
|
|
305
|
-
|
|
392
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
393
|
+
logger_1.logger.debug(`Could not delete ${modifiedPath}: ${message}`);
|
|
306
394
|
}
|
|
307
395
|
}
|
|
308
396
|
async function uploadSecrets(rootPath, projectId) {
|
|
@@ -330,49 +418,57 @@ async function uploadSecrets(rootPath, projectId) {
|
|
|
330
418
|
}
|
|
331
419
|
}
|
|
332
420
|
catch (err) {
|
|
333
|
-
|
|
421
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
422
|
+
utils.logWarning(`Failed to upload GEMINI_API_KEY secret: ${message}`);
|
|
334
423
|
}
|
|
335
424
|
}
|
|
336
|
-
async function askToOpenAntigravity(rootPath, appName,
|
|
337
|
-
|
|
338
|
-
|
|
425
|
+
async function askToOpenAntigravity(rootPath, appName, startAntigravity) {
|
|
426
|
+
const agyCommand = await getAgyCommand(startAntigravity);
|
|
427
|
+
logger_1.logger.info(`\nš Your Firebase Studio project "${appName}" is now ready for Antigravity!`);
|
|
428
|
+
logger_1.logger.info("Antigravity is Google's agentic IDE, where you can collaborate with AI agents to build, test, and deploy your application.");
|
|
429
|
+
logger_1.logger.info("\nWhat to do next inside Antigravity:");
|
|
430
|
+
logger_1.logger.info(" 1. Review the README.md: It has been updated with specifics about this migrated project.");
|
|
431
|
+
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.");
|
|
432
|
+
logger_1.logger.info("\nFile any bugs at https://github.com/firebase/firebase-tools/issues");
|
|
433
|
+
if (!startAntigravity || !agyCommand) {
|
|
339
434
|
return;
|
|
340
435
|
}
|
|
341
436
|
const answer = await prompt.confirm({
|
|
342
|
-
message:
|
|
437
|
+
message: "Would you like to open it in Antigravity now?",
|
|
343
438
|
default: true,
|
|
344
439
|
});
|
|
345
440
|
if (answer) {
|
|
346
441
|
logger_1.logger.info(`ā³ Opening ${appName} in Antigravity...`);
|
|
347
442
|
try {
|
|
348
|
-
const
|
|
443
|
+
const antigravityProcess = (0, child_process_1.spawn)(agyCommand, ["."], {
|
|
349
444
|
cwd: rootPath,
|
|
350
445
|
stdio: "ignore",
|
|
351
446
|
detached: true,
|
|
447
|
+
shell: process.platform === "win32",
|
|
352
448
|
});
|
|
353
|
-
|
|
449
|
+
antigravityProcess.unref();
|
|
354
450
|
}
|
|
355
451
|
catch (err) {
|
|
356
452
|
utils.logWarning("Could not open Antigravity IDE automatically. Please open it manually.");
|
|
357
453
|
}
|
|
358
454
|
}
|
|
359
|
-
else {
|
|
360
|
-
logger_1.logger.info('\nš Next steps: Open this folder in Antigravity and run the "Initial Project Setup" workflow.');
|
|
361
|
-
}
|
|
362
455
|
}
|
|
363
|
-
async function migrate(rootPath, options = {
|
|
456
|
+
async function migrate(rootPath, options = { startAntigravity: true }) {
|
|
457
|
+
const appType = await detectAppType(rootPath);
|
|
458
|
+
void track.trackGA4("firebase_studio_migrate", { app_type: appType, result: "started" });
|
|
364
459
|
logger_1.logger.info("š Starting Firebase Studio to Antigravity migration...");
|
|
365
|
-
await assertSystemState(options.startAgy);
|
|
366
460
|
const { projectId, appName, blueprintContent } = await extractMetadata(rootPath, options.project);
|
|
367
461
|
await updateReadme(rootPath, blueprintContent, appName);
|
|
368
462
|
await createFirebaseConfigs(rootPath, projectId);
|
|
369
463
|
await uploadSecrets(rootPath, projectId);
|
|
370
|
-
await
|
|
371
|
-
await
|
|
464
|
+
await injectAntigravityContext(rootPath, projectId, appName);
|
|
465
|
+
await writeAntigravityConfigs(rootPath);
|
|
466
|
+
await setupAntigravityMcpServer(rootPath);
|
|
372
467
|
await cleanupUnusedFiles(rootPath);
|
|
373
468
|
const currentFolderName = path.basename(rootPath);
|
|
374
469
|
if (currentFolderName === "download") {
|
|
375
|
-
logger_1.logger.info(`\nš” Tip: You
|
|
470
|
+
logger_1.logger.info(`\nš” Tip: You may want to rename this folder to "${appName.toLowerCase().replace(/\s+/g, "-")}"`);
|
|
376
471
|
}
|
|
377
|
-
await
|
|
472
|
+
await track.trackGA4("firebase_studio_migrate", { app_type: appType, result: "success" });
|
|
473
|
+
await askToOpenAntigravity(rootPath, appName, options.startAntigravity);
|
|
378
474
|
}
|
|
@@ -187,12 +187,22 @@ function functionFromEndpoint(endpoint) {
|
|
|
187
187
|
return String(cpu);
|
|
188
188
|
});
|
|
189
189
|
if (endpoint.vpc) {
|
|
190
|
-
|
|
191
|
-
|
|
190
|
+
if (endpoint.vpc.connector) {
|
|
191
|
+
gcfFunction.serviceConfig.vpcConnector = endpoint.vpc.connector;
|
|
192
|
+
gcfFunction.serviceConfig.vpcConnectorEgressSettings = endpoint.vpc.egressSettings || null;
|
|
193
|
+
}
|
|
194
|
+
else if (endpoint.vpc.networkInterfaces) {
|
|
195
|
+
gcfFunction.serviceConfig.directVpcNetworkInterface = endpoint.vpc.networkInterfaces;
|
|
196
|
+
gcfFunction.serviceConfig.directVpcEgress = endpoint.vpc.egressSettings
|
|
197
|
+
? `VPC_EGRESS_${endpoint.vpc.egressSettings}`
|
|
198
|
+
: null;
|
|
199
|
+
}
|
|
192
200
|
}
|
|
193
201
|
else if (endpoint.vpc === null) {
|
|
194
202
|
gcfFunction.serviceConfig.vpcConnector = null;
|
|
195
203
|
gcfFunction.serviceConfig.vpcConnectorEgressSettings = null;
|
|
204
|
+
gcfFunction.serviceConfig.directVpcNetworkInterface = null;
|
|
205
|
+
gcfFunction.serviceConfig.directVpcEgress = null;
|
|
196
206
|
}
|
|
197
207
|
if (backend.isEventTriggered(endpoint)) {
|
|
198
208
|
gcfFunction.eventTrigger = {
|
|
@@ -384,6 +394,17 @@ function endpointFromFunction(gcfFunction) {
|
|
|
384
394
|
endpoint.vpc = { connector: gcfFunction.serviceConfig.vpcConnector };
|
|
385
395
|
proto.renameIfPresent(endpoint.vpc, gcfFunction.serviceConfig, "egressSettings", "vpcConnectorEgressSettings");
|
|
386
396
|
}
|
|
397
|
+
else if (gcfFunction.serviceConfig.directVpcNetworkInterface) {
|
|
398
|
+
endpoint.vpc = { networkInterfaces: gcfFunction.serviceConfig.directVpcNetworkInterface };
|
|
399
|
+
if (gcfFunction.serviceConfig.directVpcEgress) {
|
|
400
|
+
if (!gcfFunction.serviceConfig.directVpcEgress.startsWith("VPC_EGRESS_")) {
|
|
401
|
+
throw new error_1.FirebaseError(`Unexpected VPC egress setting: ${gcfFunction.serviceConfig.directVpcEgress}`);
|
|
402
|
+
}
|
|
403
|
+
if (gcfFunction.serviceConfig.directVpcEgress !== "VPC_EGRESS_UNSPECIFIED") {
|
|
404
|
+
endpoint.vpc.egressSettings = gcfFunction.serviceConfig.directVpcEgress.substring("VPC_EGRESS_".length);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
387
408
|
const serviceName = gcfFunction.serviceConfig.service;
|
|
388
409
|
if (!serviceName) {
|
|
389
410
|
logger_1.logger.debug("Got a v2 function without a service name." +
|
|
@@ -125,9 +125,9 @@ async function setupSQLPermissions(instanceId, databaseId, schemaInfo, options,
|
|
|
125
125
|
}
|
|
126
126
|
async function greenFieldSchemaSetup(instanceId, databaseId, schema, options) {
|
|
127
127
|
const revokes = [];
|
|
128
|
-
if (await checkSQLRoleIsGranted(options, instanceId, databaseId, "cloudsqlsuperuser", (0, permissions_1.firebaseowner)(databaseId))) {
|
|
128
|
+
if (await checkSQLRoleIsGranted(options, instanceId, databaseId, "cloudsqlsuperuser", (0, permissions_1.firebaseowner)(databaseId, schema))) {
|
|
129
129
|
logger_1.logger.warn("Detected cloudsqlsuperuser was previously given to firebase owner, revoking to improve database security.");
|
|
130
|
-
revokes.push(`REVOKE "cloudsqlsuperuser" FROM "${(0, permissions_1.firebaseowner)(databaseId)}"`);
|
|
130
|
+
revokes.push(`REVOKE "cloudsqlsuperuser" FROM "${(0, permissions_1.firebaseowner)(databaseId, schema)}"`);
|
|
131
131
|
}
|
|
132
132
|
const user = (await (0, connect_1.getIAMUser)(options)).user;
|
|
133
133
|
const projectNumber = await (0, projectUtils_1.needProjectNumber)(options);
|
|
@@ -222,10 +222,10 @@ async function brownfieldSqlSetup(instanceId, databaseId, schemaInfo, options, s
|
|
|
222
222
|
];
|
|
223
223
|
await (0, connect_1.executeSqlCmdsAsSuperUser)(options, instanceId, databaseId, brownfieldSetupCmds, silent, true);
|
|
224
224
|
}
|
|
225
|
-
async function grantRoleTo(options, instanceId, databaseId, role, email) {
|
|
225
|
+
async function grantRoleTo(options, instanceId, databaseId, role, email, schema = permissions_1.DEFAULT_SCHEMA) {
|
|
226
226
|
const projectId = (0, projectUtils_1.needProjectId)(options);
|
|
227
227
|
const { user, mode } = (0, connect_2.toDatabaseUser)(email);
|
|
228
228
|
await cloudSqlAdminClient.createUser(projectId, instanceId, mode, user);
|
|
229
|
-
const fdcSqlRole = exports.fdcSqlRoleMap[role](databaseId);
|
|
229
|
+
const fdcSqlRole = exports.fdcSqlRoleMap[role](databaseId, schema);
|
|
230
230
|
await (0, connect_1.executeSqlCmdsAsSuperUser)(options, instanceId, databaseId, [`GRANT "${fdcSqlRole}" TO "${user}"`], false);
|
|
231
231
|
}
|