firebase-tools 15.7.0 ā 15.8.1-ct-studioexport.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/apiv2.js +13 -10
- package/lib/appdistribution/client.js +2 -2
- package/lib/appdistribution/yaml_helper.js +24 -16
- package/lib/apphosting/secrets/dialogs.js +3 -3
- package/lib/apphosting/secrets/index.js +59 -0
- package/lib/apptesting/parseTestFiles.js +2 -1
- package/lib/bin/mcp.js +2 -1
- package/lib/commands/apphosting-secrets-set.js +2 -53
- package/lib/commands/apptesting.js +2 -2
- package/lib/commands/index.js +4 -0
- package/lib/commands/studio-export.js +21 -0
- package/lib/dataconnect/schemaMigration.js +12 -1
- package/lib/deploy/apphosting/release.js +7 -1
- package/lib/deploy/extensions/prepare.js +13 -4
- package/lib/downloadUtils.js +3 -1
- package/lib/emulator/download.js +15 -1
- package/lib/emulator/downloadableEmulatorInfo.json +24 -24
- package/lib/emulator/downloadableEmulators.js +33 -6
- package/lib/env.js +6 -1
- package/lib/experiments.js +5 -0
- package/lib/firebase_studio/migrate.js +391 -0
- package/lib/functionsConfig.js +3 -3
- package/lib/gcp/cloudsql/cloudsqladmin.js +8 -3
- package/lib/gcp/cloudsql/fbToolsAuthClient.js +1 -1
- package/lib/gcp/runv2.js +3 -2
- package/lib/mcp/onemcp/index.js +7 -1
- package/lib/mcp/onemcp/onemcp_server.js +3 -4
- package/lib/mcp/prompts/apptesting/run_test.js +6 -5
- package/lib/mcp/tools/apptesting/tests.js +4 -4
- package/lib/mcp/tools/firestore/index.js +1 -4
- package/lib/mcp/tools/index.js +12 -12
- package/lib/tsconfig.publish.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/templates/firebase-studio-export/readme_template.md +11 -0
- package/templates/firebase-studio-export/system_instructions_template.md +14 -0
- package/templates/firebase-studio-export/workflows/startup_workflow.md +16 -0
- package/lib/mcp/tools/firestore/delete_document.js +0 -50
- package/lib/mcp/tools/firestore/get_documents.js +0 -58
- package/lib/mcp/tools/firestore/list_collections.js +0 -33
|
@@ -11,6 +11,7 @@ exports.stop = stop;
|
|
|
11
11
|
exports.downloadIfNecessary = downloadIfNecessary;
|
|
12
12
|
exports.start = start;
|
|
13
13
|
exports.isIncomaptibleArchError = isIncomaptibleArchError;
|
|
14
|
+
exports.emulatorVersionOverride = emulatorVersionOverride;
|
|
14
15
|
const lsofi = require("lsofi");
|
|
15
16
|
const types_1 = require("./types");
|
|
16
17
|
const constants_1 = require("./constants");
|
|
@@ -41,9 +42,11 @@ function generateDownloadDetails(emulator) {
|
|
|
41
42
|
: process.platform === "win32"
|
|
42
43
|
? EMULATOR_UPDATE_DETAILS.dataconnect.win32
|
|
43
44
|
: EMULATOR_UPDATE_DETAILS.dataconnect.linux;
|
|
45
|
+
let details;
|
|
46
|
+
const overrideVersion = emulatorVersionOverride(emulator);
|
|
44
47
|
switch (emulator) {
|
|
45
48
|
case "database":
|
|
46
|
-
|
|
49
|
+
details = {
|
|
47
50
|
downloadPath: path.join(CACHE_DIR, EMULATOR_UPDATE_DETAILS.database.downloadPathRelativeToCacheDir),
|
|
48
51
|
version: EMULATOR_UPDATE_DETAILS.database.version,
|
|
49
52
|
opts: {
|
|
@@ -52,8 +55,9 @@ function generateDownloadDetails(emulator) {
|
|
|
52
55
|
namePrefix: "firebase-database-emulator",
|
|
53
56
|
},
|
|
54
57
|
};
|
|
58
|
+
break;
|
|
55
59
|
case "firestore":
|
|
56
|
-
|
|
60
|
+
details = {
|
|
57
61
|
downloadPath: path.join(CACHE_DIR, EMULATOR_UPDATE_DETAILS.firestore.downloadPathRelativeToCacheDir),
|
|
58
62
|
version: EMULATOR_UPDATE_DETAILS.firestore.version,
|
|
59
63
|
opts: {
|
|
@@ -62,8 +66,9 @@ function generateDownloadDetails(emulator) {
|
|
|
62
66
|
namePrefix: "cloud-firestore-emulator",
|
|
63
67
|
},
|
|
64
68
|
};
|
|
69
|
+
break;
|
|
65
70
|
case "storage":
|
|
66
|
-
|
|
71
|
+
details = {
|
|
67
72
|
downloadPath: path.join(CACHE_DIR, EMULATOR_UPDATE_DETAILS.storage.downloadPathRelativeToCacheDir),
|
|
68
73
|
version: EMULATOR_UPDATE_DETAILS.storage.version,
|
|
69
74
|
opts: {
|
|
@@ -72,8 +77,9 @@ function generateDownloadDetails(emulator) {
|
|
|
72
77
|
namePrefix: "cloud-storage-rules-emulator",
|
|
73
78
|
},
|
|
74
79
|
};
|
|
80
|
+
break;
|
|
75
81
|
case "ui":
|
|
76
|
-
|
|
82
|
+
details = {
|
|
77
83
|
version: emulatorUiDetails.version,
|
|
78
84
|
downloadPath: path.join(CACHE_DIR, emulatorUiDetails.downloadPathRelativeToCacheDir),
|
|
79
85
|
unzipDir: path.join(CACHE_DIR, `ui-v${emulatorUiDetails.version}`),
|
|
@@ -86,8 +92,9 @@ function generateDownloadDetails(emulator) {
|
|
|
86
92
|
namePrefix: "ui",
|
|
87
93
|
},
|
|
88
94
|
};
|
|
95
|
+
break;
|
|
89
96
|
case "pubsub":
|
|
90
|
-
|
|
97
|
+
details = {
|
|
91
98
|
downloadPath: path.join(CACHE_DIR, EMULATOR_UPDATE_DETAILS.pubsub.downloadPathRelativeToCacheDir),
|
|
92
99
|
version: EMULATOR_UPDATE_DETAILS.pubsub.version,
|
|
93
100
|
unzipDir: path.join(CACHE_DIR, `pubsub-emulator-${EMULATOR_UPDATE_DETAILS.pubsub.version}`),
|
|
@@ -98,8 +105,9 @@ function generateDownloadDetails(emulator) {
|
|
|
98
105
|
namePrefix: "pubsub-emulator",
|
|
99
106
|
},
|
|
100
107
|
};
|
|
108
|
+
break;
|
|
101
109
|
case "dataconnect":
|
|
102
|
-
|
|
110
|
+
details = {
|
|
103
111
|
downloadPath: path.join(CACHE_DIR, dataconnectDetails.downloadPathRelativeToCacheDir),
|
|
104
112
|
version: dataconnectDetails.version,
|
|
105
113
|
binaryPath: path.join(CACHE_DIR, dataconnectDetails.downloadPathRelativeToCacheDir),
|
|
@@ -111,9 +119,25 @@ function generateDownloadDetails(emulator) {
|
|
|
111
119
|
auth: false,
|
|
112
120
|
},
|
|
113
121
|
};
|
|
122
|
+
break;
|
|
114
123
|
default:
|
|
115
124
|
throw new Error(`Invalid downloadable emulator: ${emulator}`);
|
|
116
125
|
}
|
|
126
|
+
if (overrideVersion && overrideVersion !== details.version) {
|
|
127
|
+
const oldVersion = details.version;
|
|
128
|
+
const replaceVersion = (s) => s.split(oldVersion).join(overrideVersion);
|
|
129
|
+
details.version = overrideVersion;
|
|
130
|
+
details.downloadPath = replaceVersion(details.downloadPath);
|
|
131
|
+
if (details.unzipDir) {
|
|
132
|
+
details.unzipDir = replaceVersion(details.unzipDir);
|
|
133
|
+
}
|
|
134
|
+
if (details.binaryPath) {
|
|
135
|
+
details.binaryPath = replaceVersion(details.binaryPath);
|
|
136
|
+
}
|
|
137
|
+
details.opts.remoteUrl = replaceVersion(details.opts.remoteUrl);
|
|
138
|
+
details.opts.skipChecksumAndSize = true;
|
|
139
|
+
}
|
|
140
|
+
return details;
|
|
117
141
|
}
|
|
118
142
|
const EmulatorDetails = {
|
|
119
143
|
database: {
|
|
@@ -437,3 +461,6 @@ function isIncomaptibleArchError(err) {
|
|
|
437
461
|
/Unknown system error/.test(err.message ?? "") &&
|
|
438
462
|
process.platform === "darwin");
|
|
439
463
|
}
|
|
464
|
+
function emulatorVersionOverride(emulator) {
|
|
465
|
+
return process.env[`${emulator.toUpperCase()}_EMULATOR_VERSION`];
|
|
466
|
+
}
|
package/lib/env.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.isFirebaseStudio = isFirebaseStudio;
|
|
4
4
|
exports.isFirebaseMcp = isFirebaseMcp;
|
|
5
|
+
exports.setFirebaseMcp = setFirebaseMcp;
|
|
5
6
|
exports.detectAIAgent = detectAIAgent;
|
|
6
7
|
const fsutils_1 = require("./fsutils");
|
|
7
8
|
let googleIdxFolderExists;
|
|
@@ -13,8 +14,12 @@ function isFirebaseStudio() {
|
|
|
13
14
|
googleIdxFolderExists = (0, fsutils_1.dirExistsSync)("/google/idx");
|
|
14
15
|
return googleIdxFolderExists;
|
|
15
16
|
}
|
|
17
|
+
let isFirebaseMcpFlag = false;
|
|
16
18
|
function isFirebaseMcp() {
|
|
17
|
-
return
|
|
19
|
+
return isFirebaseMcpFlag;
|
|
20
|
+
}
|
|
21
|
+
function setFirebaseMcp(value) {
|
|
22
|
+
isFirebaseMcpFlag = value;
|
|
18
23
|
}
|
|
19
24
|
function detectAIAgent() {
|
|
20
25
|
if (process.env.ANTIGRAVITY_CLI_ALIAS)
|
package/lib/experiments.js
CHANGED
|
@@ -145,6 +145,11 @@ exports.ALL_EXPERIMENTS = experiments({
|
|
|
145
145
|
default: false,
|
|
146
146
|
public: false,
|
|
147
147
|
},
|
|
148
|
+
studioexport: {
|
|
149
|
+
shortDescription: "Enable the experimental studio:export command.",
|
|
150
|
+
default: false,
|
|
151
|
+
public: false,
|
|
152
|
+
},
|
|
148
153
|
});
|
|
149
154
|
function isValidExperiment(name) {
|
|
150
155
|
return Object.keys(exports.ALL_EXPERIMENTS).includes(name);
|
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.extractMetadata = extractMetadata;
|
|
4
|
+
exports.uploadSecrets = uploadSecrets;
|
|
5
|
+
exports.migrate = migrate;
|
|
6
|
+
const fs = require("fs/promises");
|
|
7
|
+
const path = require("path");
|
|
8
|
+
const child_process_1 = require("child_process");
|
|
9
|
+
const logger_1 = require("../logger");
|
|
10
|
+
const prompt = require("../prompt");
|
|
11
|
+
const apphosting = require("../gcp/apphosting");
|
|
12
|
+
const utils = require("../utils");
|
|
13
|
+
const templates_1 = require("../templates");
|
|
14
|
+
const secrets_1 = require("../apphosting/secrets");
|
|
15
|
+
const env = require("../functions/env");
|
|
16
|
+
async function downloadGitHubDir(apiUrl, localPath) {
|
|
17
|
+
const response = await fetch(apiUrl);
|
|
18
|
+
if (!response.ok) {
|
|
19
|
+
throw new Error(`Failed to fetch directory listing: ${apiUrl}`);
|
|
20
|
+
}
|
|
21
|
+
const items = (await response.json());
|
|
22
|
+
await fs.mkdir(localPath, { recursive: true });
|
|
23
|
+
for (const item of items) {
|
|
24
|
+
const itemLocalPath = path.join(localPath, item.name);
|
|
25
|
+
if (item.type === "dir") {
|
|
26
|
+
await downloadGitHubDir(item.url, itemLocalPath);
|
|
27
|
+
}
|
|
28
|
+
else if (item.type === "file") {
|
|
29
|
+
const fileResponse = await fetch(item.download_url);
|
|
30
|
+
if (fileResponse.ok) {
|
|
31
|
+
const content = await fileResponse.arrayBuffer();
|
|
32
|
+
await fs.writeFile(itemLocalPath, Buffer.from(content));
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
async function extractMetadata(rootPath, overrideProjectId) {
|
|
38
|
+
const metadataPath = path.join(rootPath, "metadata.json");
|
|
39
|
+
let metadata = {};
|
|
40
|
+
try {
|
|
41
|
+
const metadataContent = await fs.readFile(metadataPath, "utf8");
|
|
42
|
+
metadata = JSON.parse(metadataContent);
|
|
43
|
+
}
|
|
44
|
+
catch (err) {
|
|
45
|
+
logger_1.logger.debug(`Could not read metadata.json at ${metadataPath}: ${err}`);
|
|
46
|
+
}
|
|
47
|
+
logger_1.logger.debug(`overrideProjectId ${overrideProjectId}`);
|
|
48
|
+
logger_1.logger.debug(`metadata.projectId ${metadata.projectId}`);
|
|
49
|
+
let projectId = overrideProjectId || metadata.projectId;
|
|
50
|
+
if (!projectId) {
|
|
51
|
+
try {
|
|
52
|
+
const firebasercContent = await fs.readFile(path.join(rootPath, ".firebaserc"), "utf8");
|
|
53
|
+
const firebaserc = JSON.parse(firebasercContent);
|
|
54
|
+
projectId = firebaserc.projects?.default;
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
logger_1.logger.debug(`Could not read .firebaserc at ${rootPath}: ${err}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (projectId) {
|
|
61
|
+
logger_1.logger.info(`ā
Detected Firebase Project: ${projectId}`);
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
logger_1.logger.info(`ā
Failed to determine the Firebase Project ID`);
|
|
65
|
+
}
|
|
66
|
+
let appName = "firebase-studio-export";
|
|
67
|
+
let blueprintContent = "";
|
|
68
|
+
const blueprintPath = path.join(rootPath, "docs", "blueprint.md");
|
|
69
|
+
try {
|
|
70
|
+
blueprintContent = await fs.readFile(blueprintPath, "utf8");
|
|
71
|
+
const nameMatch = blueprintContent.match(/# \*\*App Name\*\*: (.*)/);
|
|
72
|
+
if (nameMatch && nameMatch[1]) {
|
|
73
|
+
appName = nameMatch[1].trim();
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
catch (err) {
|
|
77
|
+
logger_1.logger.debug(`Could not read blueprint.md at ${blueprintPath}: ${err}`);
|
|
78
|
+
}
|
|
79
|
+
if (appName !== "firebase-studio-export") {
|
|
80
|
+
logger_1.logger.info(`ā
Detected App Name: ${appName}`);
|
|
81
|
+
}
|
|
82
|
+
return { projectId, appName, blueprintContent };
|
|
83
|
+
}
|
|
84
|
+
async function updateReadme(rootPath, blueprintContent, appName) {
|
|
85
|
+
const readmePath = path.join(rootPath, "README.md");
|
|
86
|
+
const readmeTemplate = await (0, templates_1.readTemplate)("firebase-studio-export/readme_template.md");
|
|
87
|
+
const newReadme = readmeTemplate
|
|
88
|
+
.replace(/\${appName}/g, appName)
|
|
89
|
+
.replace("${exportDate}", new Date().toISOString().split("T")[0])
|
|
90
|
+
.replace("${blueprintContent}", blueprintContent.replace(/# \*\*App Name\*\*: .*/, "").trim());
|
|
91
|
+
await fs.writeFile(readmePath, newReadme);
|
|
92
|
+
logger_1.logger.info("ā
Updated README.md with project details and origin info");
|
|
93
|
+
}
|
|
94
|
+
async function injectAgyContext(rootPath, projectId, appName) {
|
|
95
|
+
const agentDir = path.join(rootPath, ".agent");
|
|
96
|
+
const rulesDir = path.join(agentDir, "rules");
|
|
97
|
+
const workflowsDir = path.join(agentDir, "workflows");
|
|
98
|
+
const skillsDir = path.join(agentDir, "skills");
|
|
99
|
+
await fs.mkdir(rulesDir, { recursive: true });
|
|
100
|
+
await fs.mkdir(workflowsDir, { recursive: true });
|
|
101
|
+
await fs.mkdir(skillsDir, { recursive: true });
|
|
102
|
+
logger_1.logger.info("ā³ Fetching AGY skills from firebase/agent-skills...");
|
|
103
|
+
try {
|
|
104
|
+
const skillsResponse = await fetch("https://api.github.com/repos/firebase/agent-skills/contents/skills");
|
|
105
|
+
if (!skillsResponse.ok) {
|
|
106
|
+
throw new Error(`GitHub API returned ${skillsResponse.status}`);
|
|
107
|
+
}
|
|
108
|
+
const skillsData = (await skillsResponse.json());
|
|
109
|
+
if (Array.isArray(skillsData)) {
|
|
110
|
+
for (const item of skillsData) {
|
|
111
|
+
if (item.type === "dir") {
|
|
112
|
+
const skillName = item.name;
|
|
113
|
+
const skillDir = path.join(skillsDir, skillName);
|
|
114
|
+
await downloadGitHubDir(item.url, skillDir);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
utils.logWarning("GitHub API response for skills is not an array.");
|
|
120
|
+
}
|
|
121
|
+
logger_1.logger.info(`ā
Downloaded Firebase skills`);
|
|
122
|
+
}
|
|
123
|
+
catch (err) {
|
|
124
|
+
utils.logWarning(`Could not download AGY skills, skipping. ${err}`);
|
|
125
|
+
}
|
|
126
|
+
logger_1.logger.info("ā³ Fetching Genkit skill...");
|
|
127
|
+
try {
|
|
128
|
+
const genkitSkillDir = path.join(skillsDir, "developing-genkit-js");
|
|
129
|
+
await downloadGitHubDir("https://api.github.com/repos/genkit-ai/skills/contents/skills/developing-genkit-js?ref=main", genkitSkillDir);
|
|
130
|
+
logger_1.logger.info(`ā
Downloaded Genkit skill`);
|
|
131
|
+
}
|
|
132
|
+
catch (err) {
|
|
133
|
+
utils.logWarning(`Could not download Genkit skill, skipping. ${err}`);
|
|
134
|
+
}
|
|
135
|
+
const systemInstructionsTemplate = await (0, templates_1.readTemplate)("firebase-studio-export/system_instructions_template.md");
|
|
136
|
+
const systemInstructions = systemInstructionsTemplate
|
|
137
|
+
.replace("${projectId}", projectId || "None")
|
|
138
|
+
.replace("${appName}", appName);
|
|
139
|
+
await fs.writeFile(path.join(rulesDir, "migration-context.md"), systemInstructions);
|
|
140
|
+
logger_1.logger.info("ā
Injected AGY rules");
|
|
141
|
+
try {
|
|
142
|
+
const startupWorkflow = await (0, templates_1.readTemplate)("firebase-studio-export/workflows/startup_workflow.md");
|
|
143
|
+
await fs.writeFile(path.join(workflowsDir, "startup.md"), startupWorkflow);
|
|
144
|
+
logger_1.logger.info("ā
Created AGY startup workflow");
|
|
145
|
+
}
|
|
146
|
+
catch (err) {
|
|
147
|
+
logger_1.logger.debug(`Could not read or write startup workflow: ${err}`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
async function getAgyCommand(startAgy) {
|
|
151
|
+
if (!startAgy) {
|
|
152
|
+
return undefined;
|
|
153
|
+
}
|
|
154
|
+
const commands = ["agy", "antigravity"];
|
|
155
|
+
for (const cmd of commands) {
|
|
156
|
+
if (utils.commandExistsSync(cmd)) {
|
|
157
|
+
logger_1.logger.info(`ā
Antigravity IDE CLI (${cmd}) detected`);
|
|
158
|
+
return cmd;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (process.platform === "darwin") {
|
|
162
|
+
const macPath = "/Applications/Antigravity.app/Contents/Resources/app/bin/agy";
|
|
163
|
+
try {
|
|
164
|
+
await fs.access(macPath);
|
|
165
|
+
logger_1.logger.info(`ā
Antigravity IDE CLI detected at ${macPath}`);
|
|
166
|
+
return macPath;
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
const downloadLink = "https://antigravity.google/download";
|
|
172
|
+
logger_1.logger.info(`ā ļø Antigravity IDE CLI (agy) not found in your PATH. To ensure a seamless migration, please download and install Antigravity: ${downloadLink}`);
|
|
173
|
+
return undefined;
|
|
174
|
+
}
|
|
175
|
+
async function createFirebaseConfigs(rootPath, projectId) {
|
|
176
|
+
if (!projectId) {
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
const firebaserc = {
|
|
180
|
+
projects: {
|
|
181
|
+
default: projectId,
|
|
182
|
+
},
|
|
183
|
+
};
|
|
184
|
+
await fs.writeFile(path.join(rootPath, ".firebaserc"), JSON.stringify(firebaserc, null, 2));
|
|
185
|
+
logger_1.logger.info("ā
Created .firebaserc");
|
|
186
|
+
const firebaseJsonPath = path.join(rootPath, "firebase.json");
|
|
187
|
+
try {
|
|
188
|
+
await fs.access(firebaseJsonPath);
|
|
189
|
+
logger_1.logger.info("ā¹ļø firebase.json already exists, skipping creation.");
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
let backendId = "studio";
|
|
193
|
+
try {
|
|
194
|
+
logger_1.logger.info(`ā³ Fetching App Hosting backends for project ${projectId}...`);
|
|
195
|
+
const backendsData = await apphosting.listBackends(projectId, "-");
|
|
196
|
+
const backends = backendsData.backends || [];
|
|
197
|
+
if (backends.length > 0) {
|
|
198
|
+
const studioBackend = backends.find((b) => b.name.endsWith("/studio") || b.name.toLowerCase().includes("studio"));
|
|
199
|
+
if (studioBackend) {
|
|
200
|
+
backendId = studioBackend.name.split("/").pop();
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
backendId = backends[0].name.split("/").pop();
|
|
204
|
+
}
|
|
205
|
+
logger_1.logger.info(`ā
Selected App Hosting backend: ${backendId}`);
|
|
206
|
+
}
|
|
207
|
+
else {
|
|
208
|
+
utils.logWarning('No App Hosting backends found, using default "studio"');
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
catch (err) {
|
|
212
|
+
utils.logWarning(`Could not fetch backends from Firebase CLI, using default "studio". ${err}`);
|
|
213
|
+
}
|
|
214
|
+
const firebaseJson = {
|
|
215
|
+
apphosting: {
|
|
216
|
+
backendId: backendId,
|
|
217
|
+
ignore: [
|
|
218
|
+
"node_modules",
|
|
219
|
+
".git",
|
|
220
|
+
".agent",
|
|
221
|
+
".idx",
|
|
222
|
+
"firebase-debug.log",
|
|
223
|
+
"firebase-debug.*.log",
|
|
224
|
+
"functions",
|
|
225
|
+
],
|
|
226
|
+
},
|
|
227
|
+
};
|
|
228
|
+
await fs.writeFile(firebaseJsonPath, JSON.stringify(firebaseJson, null, 2));
|
|
229
|
+
logger_1.logger.info(`ā
Created firebase.json with backendId: ${backendId}`);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
async function writeAgyConfigs(rootPath) {
|
|
233
|
+
const vscodeDir = path.join(rootPath, ".vscode");
|
|
234
|
+
await fs.mkdir(vscodeDir, { recursive: true });
|
|
235
|
+
const tasksJson = {
|
|
236
|
+
version: "2.0.0",
|
|
237
|
+
tasks: [
|
|
238
|
+
{
|
|
239
|
+
label: "npm-install",
|
|
240
|
+
type: "shell",
|
|
241
|
+
command: "npm install",
|
|
242
|
+
problemMatcher: [],
|
|
243
|
+
},
|
|
244
|
+
],
|
|
245
|
+
};
|
|
246
|
+
await fs.writeFile(path.join(vscodeDir, "tasks.json"), JSON.stringify(tasksJson, null, 2));
|
|
247
|
+
logger_1.logger.info("ā
Created .vscode/tasks.json");
|
|
248
|
+
const settingsPath = path.join(vscodeDir, "settings.json");
|
|
249
|
+
let settings = {};
|
|
250
|
+
try {
|
|
251
|
+
const settingsContent = await fs.readFile(settingsPath, "utf8");
|
|
252
|
+
settings = JSON.parse(settingsContent);
|
|
253
|
+
}
|
|
254
|
+
catch (err) {
|
|
255
|
+
logger_1.logger.debug(`Could not read ${settingsPath}: ${err}`);
|
|
256
|
+
}
|
|
257
|
+
const cleanSettings = {};
|
|
258
|
+
for (const [key, value] of Object.entries(settings)) {
|
|
259
|
+
if (!key.startsWith("IDX.")) {
|
|
260
|
+
cleanSettings[key] = value;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
cleanSettings["workbench.startupEditor"] = "readme";
|
|
264
|
+
await fs.writeFile(settingsPath, JSON.stringify(cleanSettings, null, 2));
|
|
265
|
+
logger_1.logger.info("ā
Updated .vscode/settings.json with startup preferences");
|
|
266
|
+
const launchJson = {
|
|
267
|
+
version: "0.2.0",
|
|
268
|
+
configurations: [
|
|
269
|
+
{
|
|
270
|
+
type: "node",
|
|
271
|
+
request: "launch",
|
|
272
|
+
name: "Next.js: debug server-side",
|
|
273
|
+
runtimeExecutable: "npm",
|
|
274
|
+
runtimeArgs: ["run", "dev"],
|
|
275
|
+
port: 9002,
|
|
276
|
+
console: "integratedTerminal",
|
|
277
|
+
preLaunchTask: "npm-install",
|
|
278
|
+
},
|
|
279
|
+
],
|
|
280
|
+
};
|
|
281
|
+
await fs.writeFile(path.join(vscodeDir, "launch.json"), JSON.stringify(launchJson, null, 2));
|
|
282
|
+
logger_1.logger.info("ā
Created .vscode/launch.json");
|
|
283
|
+
}
|
|
284
|
+
async function cleanupUnusedFiles(rootPath) {
|
|
285
|
+
const docsDir = path.join(rootPath, "docs");
|
|
286
|
+
const blueprintPath = path.join(docsDir, "blueprint.md");
|
|
287
|
+
try {
|
|
288
|
+
await fs.unlink(blueprintPath);
|
|
289
|
+
logger_1.logger.info("ā
Cleaned up docs/blueprint.md");
|
|
290
|
+
}
|
|
291
|
+
catch (err) {
|
|
292
|
+
logger_1.logger.debug(`Could not delete ${blueprintPath}: ${err}`);
|
|
293
|
+
}
|
|
294
|
+
try {
|
|
295
|
+
const files = await fs.readdir(docsDir);
|
|
296
|
+
if (files.length === 0) {
|
|
297
|
+
await fs.rmdir(docsDir);
|
|
298
|
+
logger_1.logger.info("ā
Removed empty docs directory");
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
catch (err) {
|
|
302
|
+
logger_1.logger.debug(`Could not remove ${docsDir}: ${err}`);
|
|
303
|
+
}
|
|
304
|
+
const metadataPath = path.join(rootPath, "metadata.json");
|
|
305
|
+
try {
|
|
306
|
+
await fs.unlink(metadataPath);
|
|
307
|
+
logger_1.logger.info("ā
Cleaned up metadata.json");
|
|
308
|
+
}
|
|
309
|
+
catch (err) {
|
|
310
|
+
logger_1.logger.debug(`Could not delete ${metadataPath}: ${err}`);
|
|
311
|
+
}
|
|
312
|
+
const modifiedPath = path.join(rootPath, ".modified");
|
|
313
|
+
try {
|
|
314
|
+
await fs.unlink(modifiedPath);
|
|
315
|
+
logger_1.logger.info("ā
Cleaned up .modified");
|
|
316
|
+
}
|
|
317
|
+
catch (err) {
|
|
318
|
+
logger_1.logger.debug(`Could not delete ${modifiedPath}: ${err}`);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
async function uploadSecrets(rootPath, projectId) {
|
|
322
|
+
if (!projectId) {
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
const envPath = path.join(rootPath, ".env");
|
|
326
|
+
try {
|
|
327
|
+
await fs.access(envPath);
|
|
328
|
+
}
|
|
329
|
+
catch {
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
try {
|
|
333
|
+
const envContent = await fs.readFile(envPath, "utf8");
|
|
334
|
+
const parsedEnv = env.parse(envContent);
|
|
335
|
+
const geminiApiKey = parsedEnv.envs["GEMINI_API_KEY"];
|
|
336
|
+
if (geminiApiKey && geminiApiKey.trim().length > 0) {
|
|
337
|
+
logger_1.logger.info("ā³ Uploading GEMINI_API_KEY from .env to App Hosting secrets...");
|
|
338
|
+
await (0, secrets_1.apphostingSecretsSetAction)("GEMINI_API_KEY", projectId, undefined, undefined, envPath, true);
|
|
339
|
+
logger_1.logger.info("ā
Uploaded GEMINI_API_KEY secret");
|
|
340
|
+
}
|
|
341
|
+
else {
|
|
342
|
+
logger_1.logger.debug("Skipping GEMINI_API_KEY upload: key is missing or blank in .env");
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
catch (err) {
|
|
346
|
+
utils.logWarning(`Failed to upload GEMINI_API_KEY secret: ${err}`);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
async function askToOpenAntigravity(rootPath, appName, startAgy) {
|
|
350
|
+
const agyCommand = await getAgyCommand(startAgy);
|
|
351
|
+
if (!startAgy || !agyCommand) {
|
|
352
|
+
logger_1.logger.info('\nš Next steps: Open this folder in Antigravity and run the "Initial Project Setup" workflow.');
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
const answer = await prompt.confirm({
|
|
356
|
+
message: `Migration complete for ${appName}! Would you like to open it in Antigravity now?`,
|
|
357
|
+
default: true,
|
|
358
|
+
});
|
|
359
|
+
if (answer) {
|
|
360
|
+
logger_1.logger.info(`ā³ Opening ${appName} in Antigravity...`);
|
|
361
|
+
try {
|
|
362
|
+
const agyProcess = (0, child_process_1.spawn)(agyCommand, ["."], {
|
|
363
|
+
cwd: rootPath,
|
|
364
|
+
stdio: "ignore",
|
|
365
|
+
detached: true,
|
|
366
|
+
});
|
|
367
|
+
agyProcess.unref();
|
|
368
|
+
}
|
|
369
|
+
catch (err) {
|
|
370
|
+
utils.logWarning("Could not open Antigravity IDE automatically. Please open it manually.");
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
else {
|
|
374
|
+
logger_1.logger.info('\nš Next steps: Open this folder in Antigravity and run the "Initial Project Setup" workflow.');
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
async function migrate(rootPath, options = { startAgy: true }) {
|
|
378
|
+
logger_1.logger.info("š Starting Firebase Studio to Antigravity migration...");
|
|
379
|
+
const { projectId, appName, blueprintContent } = await extractMetadata(rootPath, options.project);
|
|
380
|
+
await updateReadme(rootPath, blueprintContent, appName);
|
|
381
|
+
await createFirebaseConfigs(rootPath, projectId);
|
|
382
|
+
await uploadSecrets(rootPath, projectId);
|
|
383
|
+
await injectAgyContext(rootPath, projectId, appName);
|
|
384
|
+
await writeAgyConfigs(rootPath);
|
|
385
|
+
await cleanupUnusedFiles(rootPath);
|
|
386
|
+
const currentFolderName = path.basename(rootPath);
|
|
387
|
+
if (currentFolderName === "download") {
|
|
388
|
+
logger_1.logger.info(`\nš” Tip: You might want to rename this folder to "${appName.toLowerCase().replace(/\s+/g, "-")}"`);
|
|
389
|
+
}
|
|
390
|
+
await askToOpenAntigravity(rootPath, appName, options.startAgy);
|
|
391
|
+
}
|
package/lib/functionsConfig.js
CHANGED
|
@@ -27,11 +27,11 @@ const utils_1 = require("./utils");
|
|
|
27
27
|
exports.RESERVED_NAMESPACES = ["firebase"];
|
|
28
28
|
const apiClient = new apiv2_1.Client({ urlPrefix: (0, api_1.firebaseApiOrigin)() });
|
|
29
29
|
const LEGACY_RUNTIME_CONFIG_EXPERIMENT = "legacyRuntimeConfigCommands";
|
|
30
|
-
const FUNCTIONS_CONFIG_DEPRECATION_MESSAGE = `DEPRECATION NOTICE: Action required before March
|
|
30
|
+
const FUNCTIONS_CONFIG_DEPRECATION_MESSAGE = `DEPRECATION NOTICE: Action required before March 2027
|
|
31
31
|
|
|
32
|
-
The functions.config() API and the Cloud Runtime Config service are deprecated. Deploys that rely on functions.config() will fail once Runtime Config shuts down in March
|
|
32
|
+
The functions.config() API and the Cloud Runtime Config service are deprecated. Deploys that rely on functions.config() will fail once Runtime Config shuts down in March 2027.
|
|
33
33
|
|
|
34
|
-
The legacy functions:config:* CLI commands are deprecated and will be removed before March
|
|
34
|
+
The legacy functions:config:* CLI commands are deprecated and will be removed before March 2027.
|
|
35
35
|
|
|
36
36
|
Learn how to migrate from functions.config() to the params package:
|
|
37
37
|
|
|
@@ -23,6 +23,7 @@ const projectUtils_1 = require("../../projectUtils");
|
|
|
23
23
|
const logger_1 = require("../../logger");
|
|
24
24
|
const iam_1 = require("../iam");
|
|
25
25
|
const error_1 = require("../../error");
|
|
26
|
+
const freeTrial_1 = require("../../dataconnect/freeTrial");
|
|
26
27
|
const API_VERSION = "v1";
|
|
27
28
|
const client = new apiv2_1.Client({
|
|
28
29
|
urlPrefix: (0, api_1.cloudSQLAdminOrigin)(),
|
|
@@ -91,7 +92,7 @@ async function createInstance(args) {
|
|
|
91
92
|
return;
|
|
92
93
|
}
|
|
93
94
|
catch (err) {
|
|
94
|
-
|
|
95
|
+
await handleCreateInstanceError(err, args.location, args.projectId);
|
|
95
96
|
throw err;
|
|
96
97
|
}
|
|
97
98
|
}
|
|
@@ -119,10 +120,14 @@ async function updateInstanceForDataConnect(instance, enableGoogleMlIntegration)
|
|
|
119
120
|
});
|
|
120
121
|
return pollRes;
|
|
121
122
|
}
|
|
122
|
-
function
|
|
123
|
-
if (err
|
|
123
|
+
async function handleCreateInstanceError(err, region, projectId) {
|
|
124
|
+
if (err?.message?.includes("Not allowed to set system label: firebase-data-connect")) {
|
|
124
125
|
throw new error_1.FirebaseError(`Cloud SQL free trial instances are not yet available in ${region}. Please check https://firebase.google.com/docs/data-connect/ for a full list of available regions.`);
|
|
125
126
|
}
|
|
127
|
+
if (err?.message?.includes("The billing account is not in good standing") &&
|
|
128
|
+
(await (0, freeTrial_1.checkFreeTrialInstanceUsed)(projectId))) {
|
|
129
|
+
throw new error_1.FirebaseError(`You have already used your Cloud SQL free trial. To create more instances, you need to attach a billing account to project ${projectId}.`);
|
|
130
|
+
}
|
|
126
131
|
}
|
|
127
132
|
function setDatabaseFlag(flag, flags = []) {
|
|
128
133
|
const temp = flags.filter((f) => f.name !== flag.name);
|
|
@@ -36,7 +36,7 @@ class FBToolsAuthClient extends google_auth_library_1.AuthClient {
|
|
|
36
36
|
async getRequestHeaders() {
|
|
37
37
|
const token = await this.getAccessToken();
|
|
38
38
|
return {
|
|
39
|
-
...apiv2.
|
|
39
|
+
...apiv2.standardHeaders(),
|
|
40
40
|
Authorization: `Bearer ${token.token}`,
|
|
41
41
|
};
|
|
42
42
|
}
|
package/lib/gcp/runv2.js
CHANGED
|
@@ -42,7 +42,7 @@ async function submitBuild(projectId, location, build) {
|
|
|
42
42
|
async function updateService(service) {
|
|
43
43
|
const fieldMask = proto.fieldMasks(service, "labels", "annotations", "tags");
|
|
44
44
|
fieldMask.push("template.revision");
|
|
45
|
-
const res = await client.
|
|
45
|
+
const res = await client.patch(service.name, service, {
|
|
46
46
|
queryParams: {
|
|
47
47
|
updateMask: fieldMask.join(","),
|
|
48
48
|
},
|
|
@@ -155,7 +155,8 @@ function endpointFromService(service) {
|
|
|
155
155
|
: service.annotations?.["run.googleapis.com/ingress"] === "internal-and-cloud-load-balancing"
|
|
156
156
|
? "ALLOW_INTERNAL_AND_GCLB"
|
|
157
157
|
: "ALLOW_ALL"),
|
|
158
|
-
...(service.annotations?.[exports.TRIGGER_TYPE_ANNOTATION]
|
|
158
|
+
...(!service.annotations?.[exports.TRIGGER_TYPE_ANNOTATION] ||
|
|
159
|
+
service.annotations?.[exports.TRIGGER_TYPE_ANNOTATION] === "HTTP_TRIGGER"
|
|
159
160
|
? { httpsTrigger: {} }
|
|
160
161
|
: {
|
|
161
162
|
eventTrigger: {
|
package/lib/mcp/onemcp/index.js
CHANGED
|
@@ -4,5 +4,11 @@ exports.ONEMCP_SERVERS = void 0;
|
|
|
4
4
|
const api_1 = require("../../api");
|
|
5
5
|
const onemcp_server_1 = require("./onemcp_server");
|
|
6
6
|
exports.ONEMCP_SERVERS = {
|
|
7
|
-
developerknowledge: new onemcp_server_1.OneMcpServer("developerknowledge", (0, api_1.developerKnowledgeOrigin)()
|
|
7
|
+
developerknowledge: new onemcp_server_1.OneMcpServer("developerknowledge", (0, api_1.developerKnowledgeOrigin)(), {
|
|
8
|
+
requiresAuth: true,
|
|
9
|
+
}),
|
|
10
|
+
firestore: new onemcp_server_1.OneMcpServer("firestore", (0, api_1.firestoreOrigin)(), {
|
|
11
|
+
requiresAuth: true,
|
|
12
|
+
requiresProject: true,
|
|
13
|
+
}),
|
|
8
14
|
};
|
|
@@ -6,9 +6,10 @@ const apiv2_1 = require("../../apiv2");
|
|
|
6
6
|
const error_1 = require("../../error");
|
|
7
7
|
const ensureApiEnabled_1 = require("../../ensureApiEnabled");
|
|
8
8
|
class OneMcpServer {
|
|
9
|
-
constructor(feature, serverUrl) {
|
|
9
|
+
constructor(feature, serverUrl, meta) {
|
|
10
10
|
this.feature = feature;
|
|
11
11
|
this.serverUrl = serverUrl;
|
|
12
|
+
this.meta = meta;
|
|
12
13
|
this.listClient = new apiv2_1.Client({
|
|
13
14
|
urlPrefix: this.serverUrl,
|
|
14
15
|
auth: false,
|
|
@@ -30,9 +31,7 @@ class OneMcpServer {
|
|
|
30
31
|
mcp: {
|
|
31
32
|
...mcpTool,
|
|
32
33
|
name: `${this.feature}_${mcpTool.name}`,
|
|
33
|
-
_meta: {
|
|
34
|
-
requiresAuth: true,
|
|
35
|
-
},
|
|
34
|
+
_meta: { ...this.meta, feature: this.feature },
|
|
36
35
|
},
|
|
37
36
|
fn: (args, ctx) => this.callTool(mcpTool.name, args, ctx),
|
|
38
37
|
isAvailable: () => Promise.resolve(true),
|