firebase-tools 15.19.0 → 15.20.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.
Files changed (38) hide show
  1. package/lib/agentSkills.js +2 -1
  2. package/lib/api.js +1 -3
  3. package/lib/appdistribution/client.js +6 -5
  4. package/lib/appdistribution/options-parser-util.js +21 -0
  5. package/lib/archiveFile.js +30 -0
  6. package/lib/command.js +7 -0
  7. package/lib/commands/appdistribution-distribute.js +13 -4
  8. package/lib/commands/apptesting.js +9 -3
  9. package/lib/commands/crashlytics-sourcemap-upload.js +61 -0
  10. package/lib/commands/index.js +4 -0
  11. package/lib/crashlytics/sourcemap.js +270 -0
  12. package/lib/dataconnect/ensureApis.js +0 -13
  13. package/lib/deploy/apphosting/prepare.js +6 -6
  14. package/lib/deploy/dataconnect/context.js +0 -2
  15. package/lib/deploy/dataconnect/deploy.js +0 -19
  16. package/lib/deploy/functions/prepare.js +8 -104
  17. package/lib/deploy/functions/services/ailogic.js +17 -10
  18. package/lib/deploy/functions/services/auth.js +3 -0
  19. package/lib/deploy/functions/services/database.js +18 -0
  20. package/lib/deploy/functions/services/dataconnect.js +20 -0
  21. package/lib/deploy/functions/services/firestore.js +12 -0
  22. package/lib/deploy/functions/services/index.js +18 -7
  23. package/lib/deploy/functions/services/storage.js +14 -0
  24. package/lib/deploy/functions/triggerRegionHelper.js +2 -4
  25. package/lib/emulator/downloadableEmulatorInfo.json +24 -24
  26. package/lib/experiments.js +8 -3
  27. package/lib/firebase_studio/migrate.js +8 -0
  28. package/lib/gcp/location.js +16 -1
  29. package/lib/gemini/fdcExperience.js +171 -26
  30. package/lib/init/features/dataconnect/create_app.js +3 -4
  31. package/lib/init/features/dataconnect/index.js +49 -15
  32. package/lib/init/features/dataconnect/sdk.js +2 -2
  33. package/lib/mcp/tools/apptesting/tests.js +3 -1
  34. package/lib/tsconfig.compile.tsbuildinfo +1 -1
  35. package/lib/tsconfig.publish.tsbuildinfo +1 -1
  36. package/lib/utils.js +48 -0
  37. package/package.json +1 -1
  38. 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 apiClient = new apiv2_1.Client({ urlPrefix: (0, api_1.cloudAiCompanionOrigin)(), auth: true });
11
- const SCHEMA_GENERATOR_EXPERIENCE = "/appeco/firebase/fdc-schema-generator";
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
- async function generateSchema(prompt, project, chatHistory = []) {
17
- const res = await apiClient.post(`/v1beta/projects/${project}/locations/global/instances/default:completeTask`, {
18
- input: { messages: [...chatHistory, { content: prompt, author: "USER" }] },
19
- experienceContext: {
20
- experience: SCHEMA_GENERATOR_EXPERIENCE,
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
- return extractCodeBlock(res.body.output.messages[0].content);
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 generateOperation(prompt, service, project, chatHistory = []) {
26
- const res = await apiClient.post(`/v1beta/projects/${project}/locations/global/instances/default:completeTask`, {
27
- input: { messages: [...chatHistory, { content: prompt, author: "USER" }] },
28
- experienceContext: {
29
- experience: OPERATION_GENERATION_EXPERIENCE,
30
- },
31
- clientContext: {
32
- additionalContext: {
33
- "@type": FIREBASE_CHAT_REQUEST_CONTEXT_TYPE_NAME,
34
- fdcInfo: { fdcServiceName: service, requiresQuery: true },
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 = text.match(regex);
184
+ const match = regex.exec(text);
43
185
  if (match && match[1]) {
44
186
  return match[1].trim();
45
187
  }
46
- throw new error_1.FirebaseError(`No code block found in the generated response: ${text}`);
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
  }
@@ -3,7 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.createReactApp = createReactApp;
4
4
  exports.createNextApp = createNextApp;
5
5
  exports.createFlutterApp = createFlutterApp;
6
- const child_process_1 = require("child_process");
6
+ const spawn = require("cross-spawn");
7
7
  const clc = require("colorette");
8
8
  const utils_1 = require("../../../utils");
9
9
  async function createReactApp(webAppId) {
@@ -33,16 +33,15 @@ async function createFlutterApp(webAppId) {
33
33
  async function executeCommand(command, args) {
34
34
  (0, utils_1.logLabeledBullet)("dataconnect", `> ${clc.bold(`${command} ${args.join(" ")}`)}`);
35
35
  return new Promise((resolve, reject) => {
36
- const childProcess = (0, child_process_1.spawn)(command, args, {
36
+ const childProcess = spawn(command, args, {
37
37
  stdio: "inherit",
38
- shell: true,
39
38
  });
40
39
  childProcess.on("close", (code) => {
41
40
  if (code === 0) {
42
41
  resolve();
43
42
  }
44
43
  else {
45
- reject(new Error(`Command failed with exit code ${code}`));
44
+ reject(new Error(`Command failed with exit code ${code ?? "null"}`));
46
45
  }
47
46
  });
48
47
  childProcess.on("error", (err) => {
@@ -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 [saveSchemaGql, waitForCloudSQLProvision] = schemasDeploySequence(projectId, info, schemaFiles, info.shouldProvisionCSQL);
180
- await (0, client_1.upsertSchema)(saveSchemaGql);
181
- if (waitForCloudSQLProvision) {
182
- void (0, client_1.upsertSchema)(waitForCloudSQLProvision);
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
- }, "Saving the SQL Connect Schema...");
185
- try {
186
- const [operationGql, seedDataGql] = await (0, utils_1.promiseWithSpinner)(() => Promise.all([
187
- (0, fdcExperience_1.generateOperation)(fdcExperience_1.PROMPT_GENERATE_CONNECTOR, serviceName, projectId),
188
- (0, fdcExperience_1.generateOperation)(fdcExperience_1.PROMPT_GENERATE_SEED_DATA, serviceName, projectId),
189
- ]), "Generating the SQL Connect Operations...");
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
- catch (err) {
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
- throw err;
241
+ }
242
+ if (genOperationsError) {
243
+ throw genOperationsError;
210
244
  }
211
245
  }
212
246
  function schemasDeploySequence(projectId, info, schemaFiles, linkToCloudSql) {
@@ -65,7 +65,7 @@ async function askQuestions(setup, config, options) {
65
65
  }
66
66
  }
67
67
  catch (err) {
68
- (0, utils_1.logLabeledError)("dataconnect", `Failed to create a ${choice} app template`);
68
+ (0, utils_1.logLabeledError)("dataconnect", `Failed to create a ${choice} app template: ${(0, error_1.getErrStack)(err)}`);
69
69
  }
70
70
  }
71
71
  setup.featureInfo = setup.featureInfo || {};
@@ -214,7 +214,7 @@ async function actuateWithInfo(setup, config, info) {
214
214
  });
215
215
  }
216
216
  catch (e) {
217
- (0, utils_1.logLabeledError)("dataconnect", `Failed to generate SQL Connect SDKs\n${e?.message}`);
217
+ (0, utils_1.logLabeledError)("dataconnect", `Failed to generate SQL Connect SDKs\n${(0, error_1.getErrMsg)(e)}`);
218
218
  }
219
219
  (0, utils_1.logLabeledSuccess)("dataconnect", `Installed generated SDKs for ${clc.bold(apps.map((a) => (0, appUtils_1.appDescription)(a)).join(", "))}`);
220
220
  if (apps.some((a) => a.platform === appUtils_1.Platform.IOS)) {
@@ -59,7 +59,9 @@ exports.run_tests = (0, tool_1.tool)("apptesting", {
59
59
  const devices = testDevices || defaultDevices;
60
60
  const client = new client_1.AppDistributionClient();
61
61
  const release = await (0, distribution_1.upload)(client, (0, options_parser_util_1.toAppName)(appId), new distribution_1.Distribution(releaseBinaryFile));
62
- return (0, util_1.toContent)(await client.createReleaseTest(release.name, devices, testCase));
62
+ return (0, util_1.toContent)(await client.createReleaseTest(release.name, devices, {
63
+ aiInstructions: testCase,
64
+ }));
63
65
  });
64
66
  exports.check_status = (0, tool_1.tool)("apptesting", {
65
67
  name: "check_status",