firebase-dataconnect-bootstrap 1.1.0 → 1.2.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/dist/main.js CHANGED
@@ -50,10 +50,7 @@ function appendIfMissing(content, appendLine) {
50
50
  }
51
51
  function detectModuleStyle(entryPath, packageJson) {
52
52
  const ext = path.extname(entryPath);
53
- if (ext === ".ts") {
54
- return "esm";
55
- }
56
- if (ext === ".mjs") {
53
+ if (ext === ".ts" || ext === ".mjs") {
57
54
  return "esm";
58
55
  }
59
56
  if (ext === ".cjs") {
@@ -64,6 +61,22 @@ function detectModuleStyle(entryPath, packageJson) {
64
61
  }
65
62
  return "cjs";
66
63
  }
64
+ function buildGeneratedImportPath(moduleStyle, extension, moduleBaseName) {
65
+ if (moduleStyle === "cjs") {
66
+ if (extension === ".cjs") {
67
+ return `./${moduleBaseName}.cjs`;
68
+ }
69
+ return `./${moduleBaseName}`;
70
+ }
71
+ if (extension === ".ts") {
72
+ return `./${moduleBaseName}.js`;
73
+ }
74
+ return `./${moduleBaseName}${extension}`;
75
+ }
76
+ function toSafeFileSegment(value) {
77
+ const normalized = value.replace(/[^A-Za-z0-9_-]/g, "_");
78
+ return normalized || "generated";
79
+ }
67
80
  function buildDataconnectYaml(config) {
68
81
  return `specVersion: "v1alpha"
69
82
  serviceId: "${config.serviceId}"
@@ -124,13 +137,36 @@ function buildMutationsGql() {
124
137
  }
125
138
  `;
126
139
  }
127
- function buildFunctionModule({ style, functionName, documentPath, region }) {
140
+ function buildEmbeddingModule(style) {
128
141
  if (style === "cjs") {
129
- return `const { onDocumentWritten } = require("firebase-functions/v2/firestore");
142
+ return `/**
143
+ * Replace this function with your embedding provider implementation.
144
+ * Return a numeric vector (number[]) for the given text.
145
+ */
146
+ async function embedText(_text) {
147
+ return null;
148
+ }
149
+
150
+ module.exports = { embedText };
151
+ `;
152
+ }
153
+ return `/**
154
+ * Replace this function with your embedding provider implementation.
155
+ * Return a numeric vector (number[]) for the given text.
156
+ */
157
+ export async function embedText(_text) {
158
+ return null;
159
+ }
160
+ `;
161
+ }
162
+ function buildFunctionModule({ style, config, embeddingImportPath }) {
163
+ if (style === "cjs") {
164
+ if (!config.vectorSearch.enabled) {
165
+ return `const { onDocumentWritten } = require("firebase-functions/v2/firestore");
130
166
  const logger = require("firebase-functions/logger");
131
167
 
132
- const ${functionName} = onDocumentWritten(
133
- { document: "${documentPath}", region: "${region}" },
168
+ const ${config.functionName} = onDocumentWritten(
169
+ { document: "${config.documentPath}", region: "${config.region}" },
134
170
  async (event) => {
135
171
  logger.info("Firestore onDocumentWritten fired", {
136
172
  params: event.params,
@@ -142,14 +178,61 @@ const ${functionName} = onDocumentWritten(
142
178
  }
143
179
  );
144
180
 
145
- module.exports = { ${functionName} };
181
+ module.exports = { ${config.functionName} };
182
+ `;
183
+ }
184
+ return `const { onDocumentWritten } = require("firebase-functions/v2/firestore");
185
+ const logger = require("firebase-functions/logger");
186
+ const { embedText } = require("${embeddingImportPath}");
187
+
188
+ const SOURCE_TEXT_FIELD = ${JSON.stringify(config.vectorSearch.sourceTextField)};
189
+ const VECTOR_FIELD = ${JSON.stringify(config.vectorSearch.vectorField)};
190
+
191
+ const ${config.functionName} = onDocumentWritten(
192
+ { document: "${config.documentPath}", region: "${config.region}" },
193
+ async (event) => {
194
+ if (!event.data?.after?.exists) {
195
+ return;
196
+ }
197
+
198
+ const afterData = event.data.after.data();
199
+ const sourceText = String(afterData?.[SOURCE_TEXT_FIELD] ?? "").trim();
200
+ if (!sourceText) {
201
+ logger.info("Skip embedding update because source text field is empty.", {
202
+ field: SOURCE_TEXT_FIELD,
203
+ document: event.data.after.ref.path
204
+ });
205
+ return;
206
+ }
207
+
208
+ const vector = await embedText(sourceText);
209
+ const validVector =
210
+ Array.isArray(vector) && vector.length > 0 && vector.every((value) => Number.isFinite(value));
211
+ if (!validVector) {
212
+ logger.warn("Embedding provider returned an invalid vector.", {
213
+ field: SOURCE_TEXT_FIELD,
214
+ document: event.data.after.ref.path
215
+ });
216
+ return;
217
+ }
218
+
219
+ await event.data.after.ref.set({ [VECTOR_FIELD]: vector }, { merge: true });
220
+ logger.info("Updated vector field from source text.", {
221
+ field: VECTOR_FIELD,
222
+ document: event.data.after.ref.path
223
+ });
224
+ }
225
+ );
226
+
227
+ module.exports = { ${config.functionName} };
146
228
  `;
147
229
  }
148
- return `import { onDocumentWritten } from "firebase-functions/v2/firestore";
230
+ if (!config.vectorSearch.enabled) {
231
+ return `import { onDocumentWritten } from "firebase-functions/v2/firestore";
149
232
  import * as logger from "firebase-functions/logger";
150
233
 
151
- export const ${functionName} = onDocumentWritten(
152
- { document: "${documentPath}", region: "${region}" },
234
+ export const ${config.functionName} = onDocumentWritten(
235
+ { document: "${config.documentPath}", region: "${config.region}" },
153
236
  async (event) => {
154
237
  logger.info("Firestore onDocumentWritten fired", {
155
238
  params: event.params,
@@ -160,6 +243,266 @@ export const ${functionName} = onDocumentWritten(
160
243
  return;
161
244
  }
162
245
  );
246
+ `;
247
+ }
248
+ return `import { onDocumentWritten } from "firebase-functions/v2/firestore";
249
+ import * as logger from "firebase-functions/logger";
250
+ import { embedText } from "${embeddingImportPath}";
251
+
252
+ const SOURCE_TEXT_FIELD = ${JSON.stringify(config.vectorSearch.sourceTextField)};
253
+ const VECTOR_FIELD = ${JSON.stringify(config.vectorSearch.vectorField)};
254
+
255
+ export const ${config.functionName} = onDocumentWritten(
256
+ { document: "${config.documentPath}", region: "${config.region}" },
257
+ async (event) => {
258
+ if (!event.data?.after?.exists) {
259
+ return;
260
+ }
261
+
262
+ const afterData = event.data.after.data();
263
+ const sourceText = String(afterData?.[SOURCE_TEXT_FIELD] ?? "").trim();
264
+ if (!sourceText) {
265
+ logger.info("Skip embedding update because source text field is empty.", {
266
+ field: SOURCE_TEXT_FIELD,
267
+ document: event.data.after.ref.path
268
+ });
269
+ return;
270
+ }
271
+
272
+ const vector = await embedText(sourceText);
273
+ const validVector =
274
+ Array.isArray(vector) && vector.length > 0 && vector.every((value) => Number.isFinite(value));
275
+ if (!validVector) {
276
+ logger.warn("Embedding provider returned an invalid vector.", {
277
+ field: SOURCE_TEXT_FIELD,
278
+ document: event.data.after.ref.path
279
+ });
280
+ return;
281
+ }
282
+
283
+ await event.data.after.ref.set({ [VECTOR_FIELD]: vector }, { merge: true });
284
+ logger.info("Updated vector field from source text.", {
285
+ field: VECTOR_FIELD,
286
+ document: event.data.after.ref.path
287
+ });
288
+ }
289
+ );
290
+ `;
291
+ }
292
+ function buildVectorSearchOnRequestModule(style, config, embeddingImportPath) {
293
+ const returnFields = JSON.stringify(config.vectorSearch.returnFields);
294
+ if (style === "cjs") {
295
+ return `const { onRequest } = require("firebase-functions/v2/https");
296
+ const logger = require("firebase-functions/logger");
297
+ const admin = require("firebase-admin");
298
+ const { embedText } = require("${embeddingImportPath}");
299
+
300
+ if (admin.apps.length === 0) {
301
+ admin.initializeApp();
302
+ }
303
+
304
+ const db = admin.firestore();
305
+ const COLLECTION_PATH = ${JSON.stringify(config.vectorSearch.collectionPath)};
306
+ const VECTOR_FIELD = ${JSON.stringify(config.vectorSearch.vectorField)};
307
+ const RETURN_FIELDS = ${returnFields};
308
+ const DEFAULT_TOP_K = ${config.vectorSearch.defaultTopK};
309
+
310
+ function normalizeVector(raw) {
311
+ if (!Array.isArray(raw)) {
312
+ return null;
313
+ }
314
+ if (raw.length === 0) {
315
+ return null;
316
+ }
317
+ const vector = raw.map((value) => Number(value));
318
+ if (vector.some((value) => !Number.isFinite(value))) {
319
+ return null;
320
+ }
321
+ return vector;
322
+ }
323
+
324
+ function cosineSimilarity(left, right) {
325
+ let dot = 0;
326
+ let leftNorm = 0;
327
+ let rightNorm = 0;
328
+ for (let i = 0; i < left.length; i += 1) {
329
+ dot += left[i] * right[i];
330
+ leftNorm += left[i] * left[i];
331
+ rightNorm += right[i] * right[i];
332
+ }
333
+ const denom = Math.sqrt(leftNorm) * Math.sqrt(rightNorm);
334
+ if (denom === 0) {
335
+ return Number.NaN;
336
+ }
337
+ return dot / denom;
338
+ }
339
+
340
+ function pickFields(data) {
341
+ const picked = {};
342
+ for (const field of RETURN_FIELDS) {
343
+ if (field in data) {
344
+ picked[field] = data[field];
345
+ }
346
+ }
347
+ return picked;
348
+ }
349
+
350
+ const ${config.vectorSearch.functionName} = onRequest(
351
+ { region: ${JSON.stringify(config.region)}, cors: true },
352
+ async (req, res) => {
353
+ try {
354
+ const payload = req.method === "GET" ? req.query : req.body ?? {};
355
+ let queryVector = normalizeVector(payload.vector);
356
+ const queryText = typeof payload.query === "string" ? payload.query.trim() : "";
357
+ const rawTopK = Number.parseInt(String(payload.topK ?? DEFAULT_TOP_K), 10);
358
+ const topK = Number.isFinite(rawTopK) && rawTopK > 0 ? rawTopK : DEFAULT_TOP_K;
359
+
360
+ if (!queryVector && queryText) {
361
+ queryVector = normalizeVector(await embedText(queryText));
362
+ }
363
+ if (!queryVector) {
364
+ res.status(400).json({
365
+ error: "Provide 'vector' (number[]) or implement embedText and send 'query'."
366
+ });
367
+ return;
368
+ }
369
+
370
+ const snapshot = await db.collection(COLLECTION_PATH).get();
371
+ const scored = [];
372
+ snapshot.forEach((doc) => {
373
+ const data = doc.data();
374
+ const candidate = normalizeVector(data[VECTOR_FIELD]);
375
+ if (!candidate || candidate.length !== queryVector.length) {
376
+ return;
377
+ }
378
+ const score = cosineSimilarity(queryVector, candidate);
379
+ if (!Number.isFinite(score)) {
380
+ return;
381
+ }
382
+ scored.push({
383
+ id: doc.id,
384
+ score,
385
+ data: pickFields(data)
386
+ });
387
+ });
388
+
389
+ scored.sort((a, b) => b.score - a.score);
390
+ res.status(200).json({
391
+ total: scored.length,
392
+ results: scored.slice(0, topK)
393
+ });
394
+ } catch (error) {
395
+ logger.error("Vector search request failed", error);
396
+ res.status(500).json({ error: "Vector search failed." });
397
+ }
398
+ }
399
+ );
400
+
401
+ module.exports = { ${config.vectorSearch.functionName} };
402
+ `;
403
+ }
404
+ return `import { onRequest } from "firebase-functions/v2/https";
405
+ import * as logger from "firebase-functions/logger";
406
+ import admin from "firebase-admin";
407
+ import { embedText } from "${embeddingImportPath}";
408
+
409
+ if (admin.apps.length === 0) {
410
+ admin.initializeApp();
411
+ }
412
+
413
+ const db = admin.firestore();
414
+ const COLLECTION_PATH = ${JSON.stringify(config.vectorSearch.collectionPath)};
415
+ const VECTOR_FIELD = ${JSON.stringify(config.vectorSearch.vectorField)};
416
+ const RETURN_FIELDS = ${returnFields};
417
+ const DEFAULT_TOP_K = ${config.vectorSearch.defaultTopK};
418
+
419
+ function normalizeVector(raw) {
420
+ if (!Array.isArray(raw) || raw.length === 0) {
421
+ return null;
422
+ }
423
+ const vector = raw.map((value) => Number(value));
424
+ if (vector.some((value) => !Number.isFinite(value))) {
425
+ return null;
426
+ }
427
+ return vector;
428
+ }
429
+
430
+ function cosineSimilarity(left, right) {
431
+ let dot = 0;
432
+ let leftNorm = 0;
433
+ let rightNorm = 0;
434
+ for (let i = 0; i < left.length; i += 1) {
435
+ dot += left[i] * right[i];
436
+ leftNorm += left[i] * left[i];
437
+ rightNorm += right[i] * right[i];
438
+ }
439
+ const denom = Math.sqrt(leftNorm) * Math.sqrt(rightNorm);
440
+ if (denom === 0) {
441
+ return Number.NaN;
442
+ }
443
+ return dot / denom;
444
+ }
445
+
446
+ function pickFields(data) {
447
+ const picked = {};
448
+ for (const field of RETURN_FIELDS) {
449
+ if (field in data) {
450
+ picked[field] = data[field];
451
+ }
452
+ }
453
+ return picked;
454
+ }
455
+
456
+ export const ${config.vectorSearch.functionName} = onRequest(
457
+ { region: ${JSON.stringify(config.region)}, cors: true },
458
+ async (req, res) => {
459
+ try {
460
+ const payload = req.method === "GET" ? req.query : req.body ?? {};
461
+ let queryVector = normalizeVector(payload.vector);
462
+ const queryText = typeof payload.query === "string" ? payload.query.trim() : "";
463
+ const rawTopK = Number.parseInt(String(payload.topK ?? DEFAULT_TOP_K), 10);
464
+ const topK = Number.isFinite(rawTopK) && rawTopK > 0 ? rawTopK : DEFAULT_TOP_K;
465
+
466
+ if (!queryVector && queryText) {
467
+ queryVector = normalizeVector(await embedText(queryText));
468
+ }
469
+ if (!queryVector) {
470
+ res.status(400).json({
471
+ error: "Provide 'vector' (number[]) or implement embedText and send 'query'."
472
+ });
473
+ return;
474
+ }
475
+
476
+ const snapshot = await db.collection(COLLECTION_PATH).get();
477
+ const scored = [];
478
+ snapshot.forEach((doc) => {
479
+ const data = doc.data();
480
+ const candidate = normalizeVector(data[VECTOR_FIELD]);
481
+ if (!candidate || candidate.length !== queryVector.length) {
482
+ return;
483
+ }
484
+ const score = cosineSimilarity(queryVector, candidate);
485
+ if (!Number.isFinite(score)) {
486
+ return;
487
+ }
488
+ scored.push({
489
+ id: doc.id,
490
+ score,
491
+ data: pickFields(data)
492
+ });
493
+ });
494
+
495
+ scored.sort((a, b) => b.score - a.score);
496
+ res.status(200).json({
497
+ total: scored.length,
498
+ results: scored.slice(0, topK)
499
+ });
500
+ } catch (error) {
501
+ logger.error("Vector search request failed", error);
502
+ res.status(500).json({ error: "Vector search failed." });
503
+ }
504
+ }
505
+ );
163
506
  `;
164
507
  }
165
508
  async function ensureFirebaseRc(targetDir, projectId) {
@@ -266,41 +609,107 @@ admin.initializeApp();
266
609
  `, "utf8");
267
610
  return { entryPath: defaultEntry, created: defaultEntry };
268
611
  }
269
- async function ensureFunctionExport(targetDir, config, entryRelativePath) {
612
+ async function resolveEntryContext(targetDir, entryRelativePath) {
270
613
  const functionsPackage = await readJsonIfExists(path.join(targetDir, "functions", "package.json"));
271
614
  const entryFullPath = path.join(targetDir, entryRelativePath);
272
615
  const moduleStyle = detectModuleStyle(entryFullPath, functionsPackage);
273
616
  const extension = path.extname(entryRelativePath) || ".js";
274
- const generatedFileName = `firestoreOnDocumentWritten${extension}`;
275
- const generatedRelativePath = path.join(path.dirname(entryRelativePath), generatedFileName);
276
- const generatedBaseName = path.basename(generatedFileName, extension);
277
- const generatedImportPath = moduleStyle === "esm"
278
- ? extension === ".ts"
279
- ? `./${generatedBaseName}.js`
280
- : `./${generatedFileName}`
281
- : `./${generatedBaseName}`;
617
+ return {
618
+ moduleStyle,
619
+ extension,
620
+ entryRelativePath,
621
+ entryFullPath
622
+ };
623
+ }
624
+ async function ensureFunctionExport(targetDir, config, context) {
625
+ const onWriteModuleBaseName = `onDocumentWritten_${toSafeFileSegment(config.functionName)}`;
626
+ const generatedFileName = `${onWriteModuleBaseName}${context.extension}`;
627
+ const generatedRelativePath = path.join(path.dirname(context.entryRelativePath), generatedFileName);
628
+ const generatedImportPath = buildGeneratedImportPath(context.moduleStyle, context.extension, onWriteModuleBaseName);
629
+ const embeddingModuleBaseName = `vectorSearchEmbedding_${toSafeFileSegment(config.functionName)}`;
630
+ const embeddingImportPath = buildGeneratedImportPath(context.moduleStyle, context.extension, embeddingModuleBaseName);
282
631
  const generatedFullPath = path.join(targetDir, generatedRelativePath);
283
632
  const functionModuleText = buildFunctionModule({
284
- style: moduleStyle,
285
- functionName: config.functionName,
286
- documentPath: config.documentPath,
287
- region: config.region
633
+ style: context.moduleStyle,
634
+ config,
635
+ embeddingImportPath
288
636
  });
289
637
  await writeFile(generatedFullPath, functionModuleText, "utf8");
290
- const entryText = (await readFile(entryFullPath, "utf8")).trimEnd();
291
- const exportLine = moduleStyle === "cjs"
638
+ const entryText = (await readFile(context.entryFullPath, "utf8")).trimEnd();
639
+ const exportLine = context.moduleStyle === "cjs"
292
640
  ? `exports.${config.functionName} = require("${generatedImportPath}").${config.functionName};`
293
641
  : `export { ${config.functionName} } from "${generatedImportPath}";`;
294
642
  const updated = appendIfMissing(entryText, exportLine);
295
643
  if (updated.changed) {
296
- await writeFile(entryFullPath, `${updated.content}`, "utf8");
644
+ await writeFile(context.entryFullPath, updated.content, "utf8");
297
645
  }
298
646
  return {
299
647
  generatedRelativePath: toPosixPath(generatedRelativePath),
300
- entryRelativePath: toPosixPath(entryRelativePath),
648
+ entryRelativePath: toPosixPath(context.entryRelativePath),
301
649
  entryUpdated: updated.changed
302
650
  };
303
651
  }
652
+ async function ensureVectorSearchScaffold(targetDir, config, context) {
653
+ if (!config.vectorSearch.enabled) {
654
+ return null;
655
+ }
656
+ const folder = path.dirname(context.entryRelativePath);
657
+ const onRequestModuleBaseName = `vectorSearchOnRequest_${toSafeFileSegment(config.vectorSearch.functionName)}`;
658
+ const embeddingModuleBaseName = `vectorSearchEmbedding_${toSafeFileSegment(config.functionName)}`;
659
+ const onRequestFileName = `${onRequestModuleBaseName}${context.extension}`;
660
+ const embeddingFileName = `${embeddingModuleBaseName}${context.extension}`;
661
+ const onRequestRelativePath = path.join(folder, onRequestFileName);
662
+ const embeddingRelativePath = path.join(folder, embeddingFileName);
663
+ const onRequestFullPath = path.join(targetDir, onRequestRelativePath);
664
+ const embeddingFullPath = path.join(targetDir, embeddingRelativePath);
665
+ const embeddingImportPath = buildGeneratedImportPath(context.moduleStyle, context.extension, embeddingModuleBaseName);
666
+ const onRequestImportPath = buildGeneratedImportPath(context.moduleStyle, context.extension, onRequestModuleBaseName);
667
+ await writeFile(onRequestFullPath, buildVectorSearchOnRequestModule(context.moduleStyle, config, embeddingImportPath), "utf8");
668
+ const embeddingCreated = await writeTextIfMissing(embeddingFullPath, buildEmbeddingModule(context.moduleStyle));
669
+ const entryText = (await readFile(context.entryFullPath, "utf8")).trimEnd();
670
+ const exportLine = context.moduleStyle === "cjs"
671
+ ? `exports.${config.vectorSearch.functionName} = require("${onRequestImportPath}").${config.vectorSearch.functionName};`
672
+ : `export { ${config.vectorSearch.functionName} } from "${onRequestImportPath}";`;
673
+ const updated = appendIfMissing(entryText, exportLine);
674
+ if (updated.changed) {
675
+ await writeFile(context.entryFullPath, updated.content, "utf8");
676
+ }
677
+ return {
678
+ generatedRelativePath: toPosixPath(onRequestRelativePath),
679
+ embeddingRelativePath: toPosixPath(embeddingRelativePath),
680
+ entryRelativePath: toPosixPath(context.entryRelativePath),
681
+ entryUpdated: updated.changed,
682
+ embeddingCreated
683
+ };
684
+ }
685
+ async function persistBootstrapConfig(targetDir, config) {
686
+ const filePath = path.join(targetDir, config.configFileName);
687
+ const currentTarget = {
688
+ documentPath: config.documentPath,
689
+ functionName: config.functionName,
690
+ vectorSearch: config.vectorSearch
691
+ };
692
+ const mergedTargets = [...config.existingTargets];
693
+ const existingIndex = mergedTargets.findIndex((target) => target.functionName === currentTarget.functionName);
694
+ if (existingIndex >= 0) {
695
+ mergedTargets[existingIndex] = currentTarget;
696
+ }
697
+ else {
698
+ mergedTargets.push(currentTarget);
699
+ }
700
+ const persisted = {
701
+ projectId: config.projectId,
702
+ region: config.region,
703
+ serviceId: config.serviceId,
704
+ location: config.location,
705
+ installDependencies: config.installDependencies,
706
+ configFileName: config.configFileName,
707
+ targets: mergedTargets,
708
+ lastTargetFunctionName: currentTarget.functionName
709
+ };
710
+ await writeJson(filePath, persisted);
711
+ return toPosixPath(config.configFileName);
712
+ }
304
713
  async function runNpmInstall(functionsDir) {
305
714
  await new Promise((resolvePromise, rejectPromise) => {
306
715
  const child = spawn("npm", ["install"], {
@@ -350,11 +759,26 @@ export async function run(config) {
350
759
  else {
351
760
  summaryLines.push(`Using existing ${entryPath}`);
352
761
  }
353
- const exportResult = await ensureFunctionExport(targetDir, config, entryPath);
354
- summaryLines.push(`Updated ${exportResult.generatedRelativePath}`);
355
- summaryLines.push(exportResult.entryUpdated
356
- ? `Appended export in ${exportResult.entryRelativePath}`
357
- : `Export already present in ${exportResult.entryRelativePath}`);
762
+ const context = await resolveEntryContext(targetDir, entryPath);
763
+ const documentWriteResult = await ensureFunctionExport(targetDir, config, context);
764
+ summaryLines.push(`Updated ${documentWriteResult.generatedRelativePath}`);
765
+ summaryLines.push(documentWriteResult.entryUpdated
766
+ ? `Appended export in ${documentWriteResult.entryRelativePath}`
767
+ : `Export already present in ${documentWriteResult.entryRelativePath}`);
768
+ const vectorSearchResult = await ensureVectorSearchScaffold(targetDir, config, context);
769
+ if (vectorSearchResult) {
770
+ summaryLines.push(`Updated ${vectorSearchResult.generatedRelativePath}`);
771
+ summaryLines.push(vectorSearchResult.embeddingCreated
772
+ ? `Created ${vectorSearchResult.embeddingRelativePath}`
773
+ : `Using existing ${vectorSearchResult.embeddingRelativePath}`);
774
+ summaryLines.push(vectorSearchResult.entryUpdated
775
+ ? `Appended export in ${vectorSearchResult.entryRelativePath}`
776
+ : `Vector search export already present in ${vectorSearchResult.entryRelativePath}`);
777
+ }
778
+ else {
779
+ summaryLines.push("Skipped vector search scaffolding");
780
+ }
781
+ summaryLines.push(`Updated ${await persistBootstrapConfig(targetDir, config)}`);
358
782
  if (config.installDependencies) {
359
783
  const functionsDir = path.join(targetDir, "functions");
360
784
  await runNpmInstall(functionsDir);
@@ -0,0 +1,2 @@
1
+ import type { CliArgs, SetupConfig } from "./types.js";
2
+ export declare function collectConfig(parsed: CliArgs): Promise<SetupConfig>;