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.
@@ -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
  }
@@ -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) {