firebase-tools 15.19.0 → 15.19.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/agentSkills.js +2 -1
- package/lib/api.js +1 -3
- package/lib/archiveFile.js +30 -0
- package/lib/command.js +7 -0
- package/lib/commands/crashlytics-sourcemap-upload.js +61 -0
- package/lib/commands/index.js +4 -0
- package/lib/crashlytics/sourcemap.js +270 -0
- package/lib/dataconnect/ensureApis.js +0 -13
- package/lib/deploy/apphosting/prepare.js +6 -6
- package/lib/emulator/downloadableEmulatorInfo.json +24 -24
- package/lib/experiments.js +8 -3
- package/lib/firebase_studio/migrate.js +8 -0
- package/lib/gemini/fdcExperience.js +171 -26
- package/lib/init/features/dataconnect/index.js +49 -15
- package/lib/tsconfig.compile.tsbuildinfo +1 -1
- package/lib/tsconfig.publish.tsbuildinfo +1 -1
- package/lib/utils.js +48 -0
- package/package.json +1 -1
- package/lib/dataconnect/cloudAICompanionTypes.js +0 -2
|
@@ -7,41 +7,186 @@ exports.extractCodeBlock = extractCodeBlock;
|
|
|
7
7
|
const apiv2_1 = require("../apiv2");
|
|
8
8
|
const api_1 = require("../api");
|
|
9
9
|
const error_1 = require("../error");
|
|
10
|
-
const
|
|
11
|
-
const
|
|
12
|
-
const OPERATION_GENERATION_EXPERIENCE = "/appeco/firebase/fdc-query-generator";
|
|
13
|
-
const FIREBASE_CHAT_REQUEST_CONTEXT_TYPE_NAME = "type.googleapis.com/google.cloud.cloudaicompanion.v1main.FirebaseChatRequestContext";
|
|
10
|
+
const logger_1 = require("../logger");
|
|
11
|
+
const apiClient = new apiv2_1.Client({ urlPrefix: (0, api_1.dataconnectOrigin)(), auth: true });
|
|
14
12
|
exports.PROMPT_GENERATE_CONNECTOR = "Create 4 operations for an app using the instance schema with proper authentication.";
|
|
15
13
|
exports.PROMPT_GENERATE_SEED_DATA = "Create a mutation to populate the database with some seed data.";
|
|
16
|
-
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
14
|
+
function logCurl(method, path, body) {
|
|
15
|
+
const url = `${(0, api_1.dataconnectOrigin)()}${path}`;
|
|
16
|
+
const headers = [
|
|
17
|
+
'-H "Content-Type: application/json"',
|
|
18
|
+
'-H "Authorization: Bearer $(gcloud auth print-access-token)"',
|
|
19
|
+
].join(" ");
|
|
20
|
+
const curl = `curl -X ${method} "${url}" ${headers} -d '${JSON.stringify(body)}'`;
|
|
21
|
+
logger_1.logger.debug(`[Agent Service] Reusable cURL command:\\n${curl}`);
|
|
22
|
+
}
|
|
23
|
+
async function generateSchema(prompt, project, location, onStatus) {
|
|
24
|
+
const path = `/v1/projects/${project}/locations/${location}/services/-:generateSchema`;
|
|
25
|
+
const body = {
|
|
26
|
+
name: `projects/${project}/locations/${location}/services/-`,
|
|
27
|
+
prompt,
|
|
28
|
+
};
|
|
29
|
+
logCurl("POST", path, body);
|
|
30
|
+
const res = await apiClient.request({
|
|
31
|
+
method: "POST",
|
|
32
|
+
path,
|
|
33
|
+
body,
|
|
34
|
+
responseType: "stream",
|
|
35
|
+
resolveOnHTTPError: true,
|
|
36
|
+
});
|
|
37
|
+
if (res.status >= 400) {
|
|
38
|
+
const errorText = await readStream(res.body);
|
|
39
|
+
throw new error_1.FirebaseError(`Failed to generate schema. Status: ${res.status}, Message: ${errorText}`);
|
|
40
|
+
}
|
|
41
|
+
return consumeStream(res.body, onStatus);
|
|
42
|
+
}
|
|
43
|
+
async function generateOperation(prompt, service, project, schemas, onStatus) {
|
|
44
|
+
let location = "us-central1";
|
|
45
|
+
let serviceId = service;
|
|
46
|
+
if (service.startsWith("projects/")) {
|
|
47
|
+
const parts = service.split("/");
|
|
48
|
+
project = parts[1];
|
|
49
|
+
location = parts[3];
|
|
50
|
+
serviceId = parts[5];
|
|
51
|
+
}
|
|
52
|
+
if (schemas && schemas.length > 0) {
|
|
53
|
+
serviceId = "-";
|
|
54
|
+
}
|
|
55
|
+
const path = `/v1/projects/${project}/locations/${location}/services/${serviceId}:generateQuery`;
|
|
56
|
+
const body = {
|
|
57
|
+
name: `projects/${project}/locations/${location}/services/${serviceId}`,
|
|
58
|
+
prompt,
|
|
59
|
+
schemas,
|
|
60
|
+
};
|
|
61
|
+
logCurl("POST", path, body);
|
|
62
|
+
const res = await apiClient.request({
|
|
63
|
+
method: "POST",
|
|
64
|
+
path,
|
|
65
|
+
body,
|
|
66
|
+
responseType: "stream",
|
|
67
|
+
resolveOnHTTPError: true,
|
|
22
68
|
});
|
|
23
|
-
|
|
69
|
+
if (res.status >= 400) {
|
|
70
|
+
const errorText = await readStream(res.body);
|
|
71
|
+
throw new error_1.FirebaseError(`Failed to generate operation. Status: ${res.status}, Message: ${errorText}`);
|
|
72
|
+
}
|
|
73
|
+
return consumeStream(res.body, onStatus);
|
|
24
74
|
}
|
|
25
|
-
async function
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
}
|
|
75
|
+
async function readStream(stream) {
|
|
76
|
+
return new Promise((resolve, reject) => {
|
|
77
|
+
let data = "";
|
|
78
|
+
stream.on("data", (chunk) => {
|
|
79
|
+
data += chunk.toString();
|
|
80
|
+
});
|
|
81
|
+
stream.on("end", () => {
|
|
82
|
+
resolve(data);
|
|
83
|
+
});
|
|
84
|
+
stream.on("error", (err) => {
|
|
85
|
+
reject(err);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
async function consumeStream(stream, onStatus) {
|
|
90
|
+
return new Promise((resolve, reject) => {
|
|
91
|
+
let buffer = "";
|
|
92
|
+
let fullText = "";
|
|
93
|
+
stream.on("data", (chunk) => {
|
|
94
|
+
const text = chunk.toString();
|
|
95
|
+
fullText += text;
|
|
96
|
+
buffer += text;
|
|
97
|
+
let newlineIndex;
|
|
98
|
+
while ((newlineIndex = buffer.indexOf("\n")) !== -1) {
|
|
99
|
+
const line = buffer.substring(0, newlineIndex).trim();
|
|
100
|
+
buffer = buffer.substring(newlineIndex + 1);
|
|
101
|
+
if (line) {
|
|
102
|
+
try {
|
|
103
|
+
const obj = JSON.parse(line);
|
|
104
|
+
if (obj.status && onStatus) {
|
|
105
|
+
onStatus(obj.status);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
catch (err) {
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
stream.on("end", () => {
|
|
114
|
+
try {
|
|
115
|
+
const response = JSON.parse(fullText);
|
|
116
|
+
if (Array.isArray(response)) {
|
|
117
|
+
let code = "";
|
|
118
|
+
for (const item of response) {
|
|
119
|
+
if (item.status && onStatus) {
|
|
120
|
+
onStatus(item.status);
|
|
121
|
+
}
|
|
122
|
+
if (item.part?.textChunk?.text) {
|
|
123
|
+
code += item.part.textChunk.text;
|
|
124
|
+
}
|
|
125
|
+
if (item.part?.codeChunk?.code) {
|
|
126
|
+
code += item.part.codeChunk.code;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
if (code) {
|
|
130
|
+
resolve(extractCodeBlock(code));
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
resolve(fullText);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
const resObj = response;
|
|
138
|
+
if (resObj.part?.codeChunk?.code) {
|
|
139
|
+
resolve(extractCodeBlock(resObj.part.codeChunk.code));
|
|
140
|
+
}
|
|
141
|
+
else if (resObj.part?.textChunk?.text) {
|
|
142
|
+
resolve(extractCodeBlock(resObj.part.textChunk.text));
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
resolve(fullText);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
catch (e) {
|
|
150
|
+
const lines = fullText.trim().split("\n");
|
|
151
|
+
let code = "";
|
|
152
|
+
for (const line of lines) {
|
|
153
|
+
try {
|
|
154
|
+
const obj = JSON.parse(line);
|
|
155
|
+
if (obj.part?.codeChunk?.code) {
|
|
156
|
+
code += obj.part.codeChunk.code;
|
|
157
|
+
}
|
|
158
|
+
else if (obj.part?.textChunk?.text) {
|
|
159
|
+
code += obj.part.textChunk.text;
|
|
160
|
+
}
|
|
161
|
+
if (obj.status && onStatus) {
|
|
162
|
+
onStatus(obj.status);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
catch (err) {
|
|
166
|
+
logger_1.logger.error("Failed to parse FSQL Generate response: ", err);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
if (code) {
|
|
170
|
+
resolve(extractCodeBlock(code));
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
resolve(fullText);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
stream.on("error", (err) => {
|
|
178
|
+
reject(err);
|
|
179
|
+
});
|
|
37
180
|
});
|
|
38
|
-
return extractCodeBlock(res.body.output.messages[0].content);
|
|
39
181
|
}
|
|
40
182
|
function extractCodeBlock(text) {
|
|
41
183
|
const regex = /```(?:[a-z]+\n)?([\s\S]*?)```/m;
|
|
42
|
-
const match =
|
|
184
|
+
const match = regex.exec(text);
|
|
43
185
|
if (match && match[1]) {
|
|
44
186
|
return match[1].trim();
|
|
45
187
|
}
|
|
46
|
-
|
|
188
|
+
if (!text.includes("{")) {
|
|
189
|
+
logger_1.logger.warn("[Agent Service] Response seems to be plain text, no GraphQL code block found.");
|
|
190
|
+
}
|
|
191
|
+
return text.trim();
|
|
47
192
|
}
|
|
@@ -79,7 +79,6 @@ async function askQuestions(setup, config, options) {
|
|
|
79
79
|
});
|
|
80
80
|
if (wantToGenerate) {
|
|
81
81
|
configstore_1.configstore.set("gemini", true);
|
|
82
|
-
await (0, ensureApis_1.ensureGIFApiTos)(setup.projectId);
|
|
83
82
|
info.appDescription = await (0, prompt_1.input)({
|
|
84
83
|
message: `Describe your app idea:`,
|
|
85
84
|
validate: async (s) => {
|
|
@@ -168,25 +167,59 @@ async function actuateWithInfo(setup, config, info, options) {
|
|
|
168
167
|
return await writeFiles(config, info, templateServiceInfo, options);
|
|
169
168
|
}
|
|
170
169
|
const serviceAlreadyExists = !(await (0, client_1.createService)(projectId, info.locationId, info.serviceId));
|
|
171
|
-
const schemaGql = await (0, utils_1.promiseWithSpinner)(() => (0, fdcExperience_1.generateSchema)(info.appDescription, projectId), "Generating the SQL Connect Schema...");
|
|
170
|
+
const schemaGql = await (0, utils_1.promiseWithSpinner)(() => (0, fdcExperience_1.generateSchema)(info.appDescription, projectId, info.locationId), "Generating the SQL Connect Schema...");
|
|
172
171
|
const schemaFiles = [{ path: "schema.gql", content: schemaGql }];
|
|
173
172
|
if (serviceAlreadyExists) {
|
|
174
173
|
(0, utils_1.logLabeledError)("dataconnect", `SQL Connect Service ${serviceName} already exists. Skip saving them...`);
|
|
175
174
|
info.flow += "_save_gemini_service_already_exists";
|
|
176
175
|
return await writeFiles(config, info, { schemaGql: schemaFiles, connectors: [] }, options);
|
|
177
176
|
}
|
|
177
|
+
const schemas = [
|
|
178
|
+
{
|
|
179
|
+
name: `${serviceName}/schemas/${types_1.MAIN_SCHEMA_ID}`,
|
|
180
|
+
datasources: [],
|
|
181
|
+
source: {
|
|
182
|
+
files: schemaFiles,
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
];
|
|
186
|
+
let operationGql = "";
|
|
187
|
+
let seedDataGql = "";
|
|
188
|
+
let genOperationsSuccess = false;
|
|
189
|
+
let genOperationsError = null;
|
|
178
190
|
await (0, utils_1.promiseWithSpinner)(async () => {
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
191
|
+
const deployPromise = (async () => {
|
|
192
|
+
const [saveSchemaGql, waitForCloudSQLProvision] = schemasDeploySequence(projectId, info, schemaFiles, info.shouldProvisionCSQL);
|
|
193
|
+
await (0, client_1.upsertSchema)(saveSchemaGql);
|
|
194
|
+
if (waitForCloudSQLProvision) {
|
|
195
|
+
void (0, client_1.upsertSchema)(waitForCloudSQLProvision);
|
|
196
|
+
}
|
|
197
|
+
})();
|
|
198
|
+
const opPromise = (async () => {
|
|
199
|
+
try {
|
|
200
|
+
const [op, seed] = await Promise.all([
|
|
201
|
+
(0, fdcExperience_1.generateOperation)(fdcExperience_1.PROMPT_GENERATE_CONNECTOR, serviceName, projectId, schemas),
|
|
202
|
+
(0, fdcExperience_1.generateOperation)(fdcExperience_1.PROMPT_GENERATE_SEED_DATA, serviceName, projectId, schemas),
|
|
203
|
+
]);
|
|
204
|
+
return { op, seed, success: true };
|
|
205
|
+
}
|
|
206
|
+
catch (err) {
|
|
207
|
+
(0, utils_1.logLabeledError)("dataconnect", `Deployment failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
208
|
+
return { success: false, error: err };
|
|
209
|
+
}
|
|
210
|
+
})();
|
|
211
|
+
const [, opResult] = await Promise.all([deployPromise, opPromise]);
|
|
212
|
+
if (opResult.success && opResult.op && opResult.seed) {
|
|
213
|
+
operationGql = opResult.op;
|
|
214
|
+
seedDataGql = opResult.seed;
|
|
215
|
+
genOperationsSuccess = true;
|
|
183
216
|
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
217
|
+
else {
|
|
218
|
+
(0, utils_1.logLabeledError)("dataconnect", `Operation Generation failed...`);
|
|
219
|
+
genOperationsError = opResult.error;
|
|
220
|
+
}
|
|
221
|
+
}, "Saving the SQL Connect Schema and generating operations...");
|
|
222
|
+
if (genOperationsSuccess) {
|
|
190
223
|
const connectors = [
|
|
191
224
|
{
|
|
192
225
|
id: "example",
|
|
@@ -202,11 +235,12 @@ async function actuateWithInfo(setup, config, info, options) {
|
|
|
202
235
|
info.flow += "_save_gemini";
|
|
203
236
|
await writeFiles(config, info, { schemaGql: schemaFiles, connectors: connectors, seedDataGql: seedDataGql }, options);
|
|
204
237
|
}
|
|
205
|
-
|
|
206
|
-
(0, utils_1.logLabeledError)("dataconnect", `Operation Generation failed...`);
|
|
238
|
+
else {
|
|
207
239
|
info.flow += "_save_gemini_operation_error";
|
|
208
240
|
await writeFiles(config, info, { schemaGql: schemaFiles, connectors: [] }, options);
|
|
209
|
-
|
|
241
|
+
}
|
|
242
|
+
if (genOperationsError) {
|
|
243
|
+
throw genOperationsError;
|
|
210
244
|
}
|
|
211
245
|
}
|
|
212
246
|
function schemasDeploySequence(projectId, info, schemaFiles, linkToCloudSql) {
|