@dereekb/dbx-cli 13.15.0 → 13.17.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 (46) hide show
  1. package/eslint/index.cjs.default.js +1 -0
  2. package/eslint/index.cjs.js +1050 -0
  3. package/eslint/index.cjs.mjs +2 -0
  4. package/eslint/index.d.ts +1 -0
  5. package/eslint/index.esm.js +1046 -0
  6. package/eslint/package.json +25 -0
  7. package/eslint/rollup.alias-internal.config.d.ts +11 -0
  8. package/eslint/src/index.d.ts +1 -0
  9. package/eslint/src/lib/index.d.ts +2 -0
  10. package/eslint/src/lib/plugin.d.ts +22 -0
  11. package/eslint/src/lib/valid-dbx-route-model-tags.rule.d.ts +59 -0
  12. package/firebase-api-manifest/main.js +318 -228
  13. package/firebase-api-manifest/package.json +3 -3
  14. package/generate-firestore-indexes/main.js +37 -24
  15. package/generate-firestore-indexes/package.json +2 -2
  16. package/generate-mcp-manifest/main.js +57 -39
  17. package/generate-mcp-manifest/package.json +3 -3
  18. package/generate-route-manifest/main.js +1137 -0
  19. package/generate-route-manifest/package.json +10 -0
  20. package/index.cjs.js +4847 -1953
  21. package/index.esm.js +4827 -1954
  22. package/lint-cache/package.json +2 -2
  23. package/manifest-extract/index.cjs.js +175 -240
  24. package/manifest-extract/index.esm.js +174 -239
  25. package/manifest-extract/package.json +9 -4
  26. package/package.json +16 -6
  27. package/src/lib/index.d.ts +2 -0
  28. package/src/lib/manifest/types.d.ts +53 -0
  29. package/src/lib/mcp-scan/manifest/package-root.d.ts +17 -0
  30. package/src/lib/mcp-scan/manifest/tokens-schema.d.ts +5 -4
  31. package/src/lib/mcp-scan/scan/extract-models/assemble.d.ts +17 -0
  32. package/src/lib/route/component-resolve.d.ts +48 -0
  33. package/src/lib/route/index.d.ts +18 -0
  34. package/src/lib/route/route-build-tree.d.ts +31 -0
  35. package/src/lib/route/route-extract.d.ts +46 -0
  36. package/src/lib/route/route-load-tree.d.ts +17 -0
  37. package/src/lib/route/route-manifest.d.ts +132 -0
  38. package/src/lib/route/route-model-tag.d.ts +89 -0
  39. package/src/lib/route/route-models-extract.d.ts +22 -0
  40. package/src/lib/route/route-resolve-sources.d.ts +39 -0
  41. package/src/lib/route/route-types.d.ts +136 -0
  42. package/src/lib/route/url-match.d.ts +116 -0
  43. package/src/lib/scan-helpers/firestore-model-extract-utils.d.ts +43 -0
  44. package/test/index.cjs.js +1 -1
  45. package/test/index.esm.js +1 -1
  46. package/test/package.json +9 -9
@@ -284,29 +284,20 @@ function findFunctionsClassName(sourceFile) {
284
284
  return result;
285
285
  }
286
286
  function inferGroupName(sourceFile) {
287
+ return findTypeAliasStem(sourceFile, "ModelCrudFunctionsConfig") ?? findTypeAliasStem(sourceFile, "FunctionTypeMap");
288
+ }
289
+ function findTypeAliasStem(sourceFile, ending) {
287
290
  let result;
288
291
  for (const alias of sourceFile.getTypeAliases()) {
289
292
  const name = alias.getName();
290
- if (name.endsWith("ModelCrudFunctionsConfig")) {
291
- const stem = name.slice(0, -"ModelCrudFunctionsConfig".length);
293
+ if (name.endsWith(ending)) {
294
+ const stem = name.slice(0, -ending.length);
292
295
  if (stem.length > 0) {
293
296
  result = stem;
294
297
  break;
295
298
  }
296
299
  }
297
300
  }
298
- if (result === void 0) {
299
- for (const alias of sourceFile.getTypeAliases()) {
300
- const name = alias.getName();
301
- if (name.endsWith("FunctionTypeMap")) {
302
- const stem = name.slice(0, -"FunctionTypeMap".length);
303
- if (stem.length > 0) {
304
- result = stem;
305
- break;
306
- }
307
- }
308
- }
309
- }
310
301
  return result;
311
302
  }
312
303
  function isNullLiteralType(node) {
@@ -402,44 +393,44 @@ function typeNodeName(node) {
402
393
  return result;
403
394
  }
404
395
  function readTypeDocs(sourceFile, typeName) {
405
- let result;
406
396
  const interfaceDecl = sourceFile.getInterface(typeName);
397
+ let result;
407
398
  if (interfaceDecl) {
408
- const typeDescription = readJsDocSummary(interfaceDecl);
409
- const hasApiParamsTag = hasJsDocFlag(interfaceDecl, "dbxModelApiParams");
410
- const fields = [];
411
- for (const property of interfaceDecl.getProperties()) {
412
- const fieldName = property.getName();
413
- const description = readJsDocSummary(property);
414
- const typeNode = property.getTypeNode();
415
- const typeText = typeNode?.getText().trim() ?? "";
416
- const adminOnly = hasJsDocFlag(property, "dbxModelApiAdminOnly");
417
- const field = {
418
- name: fieldName,
419
- typeText,
420
- ...description ? { description } : {},
421
- ...adminOnly ? { accessLevel: "adminOnly" } : {}
422
- };
423
- fields.push(field);
424
- }
425
- if (typeDescription || fields.length > 0 || hasApiParamsTag) {
426
- result = {
427
- ...typeDescription ? { typeDescription } : {},
428
- ...fields.length > 0 ? { fields } : {},
429
- hasApiParamsTag
430
- };
431
- }
399
+ result = readInterfaceTypeDocs(interfaceDecl);
432
400
  } else {
433
401
  const typeAlias = sourceFile.getTypeAlias(typeName);
434
- if (typeAlias) {
435
- const typeDescription = readJsDocSummary(typeAlias);
436
- if (typeDescription) {
437
- result = { typeDescription };
438
- }
439
- }
402
+ result = typeAlias ? readTypeAliasDocs(typeAlias) : void 0;
403
+ }
404
+ return result;
405
+ }
406
+ function readInterfaceTypeDocs(interfaceDecl) {
407
+ const typeDescription = readJsDocSummary(interfaceDecl);
408
+ const hasApiParamsTag = hasJsDocFlag(interfaceDecl, "dbxModelApiParams");
409
+ const fields = interfaceDecl.getProperties().map((property) => readInterfaceField(property));
410
+ let result;
411
+ if (typeDescription || fields.length > 0 || hasApiParamsTag) {
412
+ result = {
413
+ ...typeDescription ? { typeDescription } : {},
414
+ ...fields.length > 0 ? { fields } : {},
415
+ hasApiParamsTag
416
+ };
440
417
  }
441
418
  return result;
442
419
  }
420
+ function readInterfaceField(property) {
421
+ const description = readJsDocSummary(property);
422
+ const adminOnly = hasJsDocFlag(property, "dbxModelApiAdminOnly");
423
+ return {
424
+ name: property.getName(),
425
+ typeText: property.getTypeNode()?.getText().trim() ?? "",
426
+ ...description ? { description } : {},
427
+ ...adminOnly ? { accessLevel: "adminOnly" } : {}
428
+ };
429
+ }
430
+ function readTypeAliasDocs(typeAlias) {
431
+ const typeDescription = readJsDocSummary(typeAlias);
432
+ return typeDescription ? { typeDescription } : void 0;
433
+ }
443
434
  function hasJsDocFlag(node, tagName) {
444
435
  let result = false;
445
436
  for (const doc of node.getJsDocs()) {
@@ -468,8 +459,8 @@ function readJsDocTagValue(node, tagName) {
468
459
  function readJsDocSummary(node) {
469
460
  let result;
470
461
  const docs = node.getJsDocs();
471
- if (docs.length > 0) {
472
- const last = docs[docs.length - 1];
462
+ const last = docs.at(-1);
463
+ if (last) {
473
464
  const description = last.getDescription().trim();
474
465
  if (description.length > 0) {
475
466
  result = description;
@@ -478,14 +469,92 @@ function readJsDocSummary(node) {
478
469
  return result;
479
470
  }
480
471
 
472
+ // packages/util/src/lib/string/sort.ts
473
+ var compareStrings = (a, b) => a.localeCompare(b);
474
+
475
+ // packages/dbx-cli/src/lib/scan-helpers/firestore-model-extract-utils.ts
476
+ import { Node as Node3 } from "ts-morph";
477
+ var PASSTHROUGH_TYPE_WRAPPERS = /* @__PURE__ */ new Set(["Partial", "Required", "Readonly", "NonNullable", "MaybeMap", "Pick", "Omit"]);
478
+ function parseFirestoreModelIdentityArgs(args) {
479
+ let result;
480
+ if (args.length === 1) {
481
+ const modelType = stringLiteralValue(args[0]);
482
+ if (modelType !== void 0) {
483
+ result = { modelType, collectionPrefix: void 0, parentIdentityConst: void 0 };
484
+ }
485
+ } else if (args.length === 2) {
486
+ const first = stringLiteralValue(args[0]);
487
+ if (first === void 0) {
488
+ const modelType = stringLiteralValue(args[1]);
489
+ if (modelType !== void 0) {
490
+ result = { modelType, collectionPrefix: void 0, parentIdentityConst: identifierName(args[0]) };
491
+ }
492
+ } else {
493
+ result = { modelType: first, collectionPrefix: stringLiteralValue(args[1]), parentIdentityConst: void 0 };
494
+ }
495
+ } else if (args.length >= 3) {
496
+ const modelType = stringLiteralValue(args[1]);
497
+ if (modelType !== void 0) {
498
+ result = { modelType, collectionPrefix: stringLiteralValue(args[2]), parentIdentityConst: identifierName(args[0]) };
499
+ }
500
+ }
501
+ return result;
502
+ }
503
+ function resolveExtendsName(expr) {
504
+ const head = expr.getExpression().getText();
505
+ let result = head;
506
+ if (PASSTHROUGH_TYPE_WRAPPERS.has(head)) {
507
+ const typeArgs = expr.getTypeArguments();
508
+ if (typeArgs.length > 0) {
509
+ const peeled = peelTypeNode(typeArgs[0]);
510
+ if (peeled !== void 0) {
511
+ result = peeled;
512
+ }
513
+ }
514
+ }
515
+ return result;
516
+ }
517
+ function peelTypeNode(node) {
518
+ let current = node;
519
+ while (Node3.isParenthesizedTypeNode(current)) {
520
+ current = current.getTypeNode();
521
+ }
522
+ let result;
523
+ if (Node3.isTypeReference(current)) {
524
+ const name = current.getTypeName().getText();
525
+ if (PASSTHROUGH_TYPE_WRAPPERS.has(name)) {
526
+ const inner = current.getTypeArguments();
527
+ if (inner.length > 0) {
528
+ result = peelTypeNode(inner[0]);
529
+ }
530
+ } else {
531
+ result = name;
532
+ }
533
+ }
534
+ return result;
535
+ }
536
+ function stringLiteralValue(node) {
537
+ let result;
538
+ if (Node3.isStringLiteral(node) || Node3.isNoSubstitutionTemplateLiteral(node)) {
539
+ result = node.getLiteralText();
540
+ }
541
+ return result;
542
+ }
543
+ function identifierName(node) {
544
+ let result;
545
+ if (Node3.isIdentifier(node)) {
546
+ result = node.getText();
547
+ }
548
+ return result;
549
+ }
550
+
481
551
  // packages/dbx-cli/manifest-extract/src/lib/extract-models.ts
482
- import { Node as Node3, Project as Project3 } from "ts-morph";
552
+ import { Node as Node4, Project as Project3 } from "ts-morph";
483
553
  var READ_LEVEL_VALUES = /* @__PURE__ */ new Set(["system", "owner", "admin-only", "permissions"]);
484
554
  var SERVICE_FACTORY_TAG = "dbxModelServiceFactory";
485
555
  var MCP_TOOL_NAME_SEGMENT_TAG = "dbxModelMcpToolNameSegment";
486
556
  var MODEL_TYPE_VALUE_PATTERN = /^[a-z][A-Za-z0-9_$]*$/;
487
557
  var TOOL_NAME_SEGMENT_PATTERN = /^[A-Za-z][A-Za-z0-9_$]*$/;
488
- var PASSTHROUGH_TYPE_WRAPPERS = /* @__PURE__ */ new Set(["Partial", "Required", "Readonly", "NonNullable", "MaybeMap", "Pick", "Omit"]);
489
558
  var IDENTITY_FN = "firestoreModelIdentity";
490
559
  var CONVERTER_FN_NAMES = ["snapshotConverterFunctions", "firestoreSubObject", "firestoreObjectArray"];
491
560
  var SUB_OBJECT_FN = "firestoreSubObject";
@@ -493,6 +562,7 @@ var OBJECT_ARRAY_FN = "firestoreObjectArray";
493
562
  var SNAPSHOT_FN = "snapshotConverterFunctions";
494
563
  var FIELDS_LITERAL_KEY = "fields";
495
564
  var OBJECT_FIELD_KEY = "objectField";
565
+ var FIRESTORE_FIELD_KEY = "firestoreField";
496
566
  function extractModelsFromSource(input) {
497
567
  const project = new Project3({ useInMemoryFileSystem: true, skipAddingFilesFromTsConfig: true });
498
568
  const sourceFile = project.createSourceFile(input.name, input.text, { overwrite: true });
@@ -510,9 +580,9 @@ function readIdentities(sourceFile) {
510
580
  if (!statement.isExported()) continue;
511
581
  for (const decl of statement.getDeclarations()) {
512
582
  const initializer = decl.getInitializer();
513
- if (!initializer || !Node3.isCallExpression(initializer)) continue;
583
+ if (!initializer || !Node4.isCallExpression(initializer)) continue;
514
584
  if (initializer.getExpression().getText() !== IDENTITY_FN) continue;
515
- const parsed = parseIdentityArgs(initializer);
585
+ const parsed = parseFirestoreModelIdentityArgs(initializer.getArguments());
516
586
  if (parsed) {
517
587
  out.push({ identityConst: decl.getName(), ...parsed });
518
588
  }
@@ -520,32 +590,6 @@ function readIdentities(sourceFile) {
520
590
  }
521
591
  return out;
522
592
  }
523
- function parseIdentityArgs(call) {
524
- const args = call.getArguments();
525
- let result;
526
- if (args.length === 1) {
527
- const modelType = stringLiteralValue(args[0]);
528
- if (modelType !== void 0) {
529
- result = { modelType, collectionPrefix: void 0, parentIdentityConst: void 0 };
530
- }
531
- } else if (args.length === 2) {
532
- const first = stringLiteralValue(args[0]);
533
- if (first === void 0) {
534
- const modelType = stringLiteralValue(args[1]);
535
- if (modelType !== void 0) {
536
- result = { modelType, collectionPrefix: void 0, parentIdentityConst: identifierName(args[0]) };
537
- }
538
- } else {
539
- result = { modelType: first, collectionPrefix: stringLiteralValue(args[1]), parentIdentityConst: void 0 };
540
- }
541
- } else if (args.length >= 3) {
542
- const modelType = stringLiteralValue(args[1]);
543
- if (modelType !== void 0) {
544
- result = { modelType, collectionPrefix: stringLiteralValue(args[2]), parentIdentityConst: identifierName(args[0]) };
545
- }
546
- }
547
- return result;
548
- }
549
593
  function readInterfaces(sourceFile) {
550
594
  const out = [];
551
595
  for (const decl of sourceFile.getInterfaces()) {
@@ -646,46 +690,13 @@ function readServiceFactoryModelType(jsDocs) {
646
690
  }
647
691
  return result;
648
692
  }
649
- function resolveExtendsName(expr) {
650
- const head = expr.getExpression().getText();
651
- let result = head;
652
- if (PASSTHROUGH_TYPE_WRAPPERS.has(head)) {
653
- const typeArgs = expr.getTypeArguments();
654
- if (typeArgs.length > 0) {
655
- const peeled = peelTypeNode(typeArgs[0]);
656
- if (peeled !== void 0) {
657
- result = peeled;
658
- }
659
- }
660
- }
661
- return result;
662
- }
663
- function peelTypeNode(node) {
664
- let current = node;
665
- while (Node3.isParenthesizedTypeNode(current)) {
666
- current = current.getTypeNode();
667
- }
668
- let result;
669
- if (Node3.isTypeReference(current)) {
670
- const name = current.getTypeName().getText();
671
- if (PASSTHROUGH_TYPE_WRAPPERS.has(name)) {
672
- const inner = current.getTypeArguments();
673
- if (inner.length > 0) {
674
- result = peelTypeNode(inner[0]);
675
- }
676
- } else {
677
- result = name;
678
- }
679
- }
680
- return result;
681
- }
682
693
  function readConverters(sourceFile) {
683
694
  const out = [];
684
695
  for (const statement of sourceFile.getVariableStatements()) {
685
696
  if (!statement.isExported()) continue;
686
697
  for (const decl of statement.getDeclarations()) {
687
698
  const initializer = decl.getInitializer();
688
- if (!initializer || !Node3.isCallExpression(initializer)) continue;
699
+ if (!initializer || !Node4.isCallExpression(initializer)) continue;
689
700
  const factory = initializer.getExpression().getText();
690
701
  if (!isConverterFactoryName(factory)) continue;
691
702
  const interfaceName = readGenericInterfaceName(initializer);
@@ -719,13 +730,13 @@ function readConverterFields(call) {
719
730
  let result;
720
731
  if (args.length > 0) {
721
732
  const config = args[0];
722
- if (Node3.isObjectLiteralExpression(config)) {
733
+ if (Node4.isObjectLiteralExpression(config)) {
723
734
  let fieldsLiteral;
724
735
  if (fnName === SNAPSHOT_FN) {
725
736
  fieldsLiteral = readObjectProperty(config, FIELDS_LITERAL_KEY);
726
737
  } else {
727
738
  const objectField = readPropertyValue(config, OBJECT_FIELD_KEY);
728
- if (objectField && Node3.isObjectLiteralExpression(objectField)) {
739
+ if (objectField && Node4.isObjectLiteralExpression(objectField)) {
729
740
  fieldsLiteral = readObjectProperty(objectField, FIELDS_LITERAL_KEY);
730
741
  }
731
742
  }
@@ -739,7 +750,7 @@ function readConverterFields(call) {
739
750
  function readFieldEntries(fields) {
740
751
  const out = [];
741
752
  for (const property of fields.getProperties()) {
742
- if (Node3.isPropertyAssignment(property)) {
753
+ if (Node4.isPropertyAssignment(property)) {
743
754
  const initializer = property.getInitializer();
744
755
  const converterText = initializer ? initializer.getText().replaceAll(/\s+/g, " ").trim() : "";
745
756
  const nested = initializer ? readNestedFromExpression(initializer) : void 0;
@@ -750,7 +761,7 @@ function readFieldEntries(fields) {
750
761
  nestedConverterInline: nested?.inline,
751
762
  nestedIsArray: nested?.isArray
752
763
  });
753
- } else if (Node3.isShorthandPropertyAssignment(property)) {
764
+ } else if (Node4.isShorthandPropertyAssignment(property)) {
754
765
  const name = property.getName();
755
766
  out.push({ key: name, converter: name });
756
767
  }
@@ -759,54 +770,69 @@ function readFieldEntries(fields) {
759
770
  }
760
771
  function readNestedFromExpression(expr) {
761
772
  let result;
762
- if (Node3.isCallExpression(expr)) {
773
+ if (Node4.isCallExpression(expr)) {
763
774
  const fnName = expr.getExpression().getText();
764
775
  if (fnName === SUB_OBJECT_FN || fnName === OBJECT_ARRAY_FN) {
765
- const args = expr.getArguments();
766
- if (args.length > 0) {
767
- const config = args[0];
768
- if (Node3.isObjectLiteralExpression(config)) {
769
- const objectField = readPropertyValue(config, OBJECT_FIELD_KEY);
770
- if (objectField) {
771
- const isArray = fnName === OBJECT_ARRAY_FN;
772
- if (Node3.isIdentifier(objectField)) {
773
- result = { ref: objectField.getText(), isArray };
774
- } else if (Node3.isObjectLiteralExpression(objectField)) {
775
- const fieldsLiteral = readObjectProperty(objectField, FIELDS_LITERAL_KEY);
776
- if (fieldsLiteral) {
777
- const inlineFields = readFieldEntries(fieldsLiteral);
778
- result = {
779
- inline: {
780
- converterConst: void 0,
781
- factory: fnName,
782
- interfaceName: readGenericInterfaceName(expr),
783
- fields: inlineFields,
784
- line: expr.getStartLineNumber()
785
- },
786
- isArray
787
- };
788
- }
789
- }
790
- }
791
- }
776
+ result = readNestedConverterCall(expr, fnName);
777
+ }
778
+ }
779
+ return result;
780
+ }
781
+ function readNestedConverterCall(call, fnName) {
782
+ let result;
783
+ const args = call.getArguments();
784
+ if (args.length > 0) {
785
+ const config = args[0];
786
+ if (Node4.isObjectLiteralExpression(config)) {
787
+ const valueNode = readPropertyValue(config, OBJECT_FIELD_KEY) ?? readPropertyValue(config, FIRESTORE_FIELD_KEY);
788
+ if (valueNode) {
789
+ result = buildNestedConverterMatch({ objectField: valueNode, call, fnName });
792
790
  }
793
791
  }
794
792
  }
795
793
  return result;
796
794
  }
795
+ function buildNestedConverterMatch(input) {
796
+ const { objectField, call, fnName } = input;
797
+ const isArray = fnName === OBJECT_ARRAY_FN;
798
+ let result;
799
+ if (Node4.isIdentifier(objectField)) {
800
+ result = { ref: objectField.getText(), isArray };
801
+ } else if (Node4.isObjectLiteralExpression(objectField)) {
802
+ const fieldsLiteral = readObjectProperty(objectField, FIELDS_LITERAL_KEY);
803
+ if (fieldsLiteral) {
804
+ result = {
805
+ inline: {
806
+ converterConst: void 0,
807
+ factory: fnName,
808
+ interfaceName: readGenericInterfaceName(call),
809
+ fields: readFieldEntries(fieldsLiteral),
810
+ line: call.getStartLineNumber()
811
+ },
812
+ isArray
813
+ };
814
+ }
815
+ } else if (Node4.isCallExpression(objectField)) {
816
+ const nested = readNestedFromExpression(objectField);
817
+ if (nested) {
818
+ result = { ...nested, isArray };
819
+ }
820
+ }
821
+ return result;
822
+ }
797
823
  function readPropertyValue(literal, key) {
798
824
  const property = literal.getProperty(key);
799
825
  let result;
800
- if (property && Node3.isPropertyAssignment(property)) {
826
+ if (property && Node4.isPropertyAssignment(property)) {
801
827
  result = property.getInitializer();
802
- } else if (property && Node3.isShorthandPropertyAssignment(property)) {
828
+ } else if (property && Node4.isShorthandPropertyAssignment(property)) {
803
829
  result = property.getNameNode();
804
830
  }
805
831
  return result;
806
832
  }
807
833
  function readObjectProperty(literal, key) {
808
834
  const value = readPropertyValue(literal, key);
809
- return value && Node3.isObjectLiteralExpression(value) ? value : void 0;
835
+ return value && Node4.isObjectLiteralExpression(value) ? value : void 0;
810
836
  }
811
837
  function readEnums(sourceFile) {
812
838
  const out = [];
@@ -905,20 +931,6 @@ function firstParagraph(text) {
905
931
  }
906
932
  return collected.join(" ").trim();
907
933
  }
908
- function stringLiteralValue(node) {
909
- let result;
910
- if (Node3.isStringLiteral(node) || Node3.isNoSubstitutionTemplateLiteral(node)) {
911
- result = node.getLiteralText();
912
- }
913
- return result;
914
- }
915
- function identifierName(node) {
916
- let result;
917
- if (Node3.isIdentifier(node)) {
918
- result = node.getText();
919
- }
920
- return result;
921
- }
922
934
 
923
935
  // packages/dbx-cli/firebase-api-manifest/src/generate-api-manifest/find-api-files.ts
924
936
  function findApiFiles(packageRoot) {
@@ -1021,6 +1033,47 @@ function assembleModels(input) {
1021
1033
  accumulator.entries.sort((a, b) => a.modelType.localeCompare(b.modelType));
1022
1034
  return accumulator.entries;
1023
1035
  }
1036
+ function collectModelEnums(input) {
1037
+ const registry = buildEnumRegistry(input.extractions);
1038
+ const referenced = collectReferencedEnumNames(input.models);
1039
+ const out = {};
1040
+ for (const name of [...referenced].sort((a, b) => a.localeCompare(b))) {
1041
+ const extracted = registry.get(name);
1042
+ if (extracted) {
1043
+ out[name] = buildModelEnumEntry(extracted);
1044
+ }
1045
+ }
1046
+ return out;
1047
+ }
1048
+ function buildEnumRegistry(extractions) {
1049
+ const registry = /* @__PURE__ */ new Map();
1050
+ for (const { extraction } of extractions) {
1051
+ for (const e of extraction.enums) {
1052
+ if (!registry.has(e.name)) registry.set(e.name, e);
1053
+ }
1054
+ }
1055
+ return registry;
1056
+ }
1057
+ function collectReferencedEnumNames(models) {
1058
+ const referenced = /* @__PURE__ */ new Set();
1059
+ for (const model of models) {
1060
+ addReferencedEnumNames(model.fields, referenced);
1061
+ }
1062
+ return referenced;
1063
+ }
1064
+ function addReferencedEnumNames(fields, referenced) {
1065
+ for (const field of fields) {
1066
+ if (field.enumRef) referenced.add(field.enumRef);
1067
+ if (field.nestedFields) addReferencedEnumNames(field.nestedFields, referenced);
1068
+ }
1069
+ }
1070
+ function buildModelEnumEntry(e) {
1071
+ return {
1072
+ name: e.name,
1073
+ values: e.values.map((v) => v.description ? { name: v.name, value: v.value, description: v.description } : { name: v.name, value: v.value }),
1074
+ ...e.description ? { description: e.description } : {}
1075
+ };
1076
+ }
1024
1077
  function buildGlobalRegistries(extractions) {
1025
1078
  const converterRegistry = /* @__PURE__ */ new Map();
1026
1079
  const interfaceRegistry = /* @__PURE__ */ new Map();
@@ -1086,28 +1139,32 @@ function buildEntryForIdentity(input) {
1086
1139
  depth: 0,
1087
1140
  visitedConverters: /* @__PURE__ */ new Set()
1088
1141
  });
1089
- const modelGroup = registries.groupByModelName.get(modelName);
1090
- const serviceFactory = registries.serviceFactoryByModelType.get(identity.modelType);
1091
- result = {
1092
- modelType: identity.modelType,
1093
- modelName,
1094
- ...modelGroup ? { modelGroup } : {},
1095
- identityConst: identity.identityConst,
1096
- collectionPrefix: identity.collectionPrefix,
1097
- ...identity.parentIdentityConst ? { parentIdentityConst: identity.parentIdentityConst } : {},
1098
- ...iface.description ? { description: iface.description } : {},
1099
- sourcePackage: source.sourcePackage,
1100
- sourceFile: source.sourceFile,
1101
- fields,
1102
- ...iface.mcpToolNameSegment ? { mcpToolNameSegment: iface.mcpToolNameSegment } : {},
1103
- ...iface.dbxModelRead ? { read: iface.dbxModelRead } : {},
1104
- ...serviceFactory ? { serviceFactory } : {}
1105
- };
1142
+ result = buildManifestEntry({ identity, modelName, collectionPrefix: identity.collectionPrefix, iface, fields, source, registries });
1106
1143
  }
1107
1144
  }
1108
1145
  }
1109
1146
  return result;
1110
1147
  }
1148
+ function buildManifestEntry(input) {
1149
+ const { identity, modelName, collectionPrefix, iface, fields, source, registries } = input;
1150
+ const modelGroup = registries.groupByModelName.get(modelName);
1151
+ const serviceFactory = registries.serviceFactoryByModelType.get(identity.modelType);
1152
+ return {
1153
+ modelType: identity.modelType,
1154
+ modelName,
1155
+ ...modelGroup ? { modelGroup } : {},
1156
+ identityConst: identity.identityConst,
1157
+ collectionPrefix,
1158
+ ...identity.parentIdentityConst ? { parentIdentityConst: identity.parentIdentityConst } : {},
1159
+ ...iface.description ? { description: iface.description } : {},
1160
+ sourcePackage: source.sourcePackage,
1161
+ sourceFile: source.sourceFile,
1162
+ fields,
1163
+ ...iface.mcpToolNameSegment ? { mcpToolNameSegment: iface.mcpToolNameSegment } : {},
1164
+ ...iface.dbxModelRead ? { read: iface.dbxModelRead } : {},
1165
+ ...serviceFactory ? { serviceFactory } : {}
1166
+ };
1167
+ }
1111
1168
  function findConverterForInterface(extraction, interfaceName) {
1112
1169
  return extraction.converters.find((c) => c.interfaceName === interfaceName);
1113
1170
  }
@@ -1158,21 +1215,10 @@ function buildField(input) {
1158
1215
  return out;
1159
1216
  }
1160
1217
  function resolveNestedFields(input) {
1161
- const { field } = input;
1162
1218
  let result;
1163
1219
  if (input.depth < MAX_NESTED_DEPTH) {
1164
- let nestedConverter;
1165
- let aborted = false;
1166
- if (field.nestedConverterInline) {
1167
- nestedConverter = field.nestedConverterInline;
1168
- } else if (field.nestedConverterRef) {
1169
- if (input.visitedConverters.has(field.nestedConverterRef)) {
1170
- aborted = true;
1171
- } else {
1172
- nestedConverter = input.converterRegistry.get(field.nestedConverterRef);
1173
- }
1174
- }
1175
- if (!aborted && nestedConverter) {
1220
+ const nestedConverter = selectNestedConverter(input);
1221
+ if (nestedConverter) {
1176
1222
  const nextVisited = new Set(input.visitedConverters);
1177
1223
  if (nestedConverter.converterConst) nextVisited.add(nestedConverter.converterConst);
1178
1224
  const nestedIface = nestedConverter.interfaceName ? input.interfaceRegistry.get(nestedConverter.interfaceName) : void 0;
@@ -1185,11 +1231,21 @@ function resolveNestedFields(input) {
1185
1231
  depth: input.depth + 1,
1186
1232
  visitedConverters: nextVisited
1187
1233
  });
1188
- result = { fields, isArray: field.nestedIsArray ?? false };
1234
+ result = { fields, isArray: input.field.nestedIsArray ?? false };
1189
1235
  }
1190
1236
  }
1191
1237
  return result;
1192
1238
  }
1239
+ function selectNestedConverter(input) {
1240
+ const { field } = input;
1241
+ let result;
1242
+ if (field.nestedConverterInline) {
1243
+ result = field.nestedConverterInline;
1244
+ } else if (field.nestedConverterRef && !input.visitedConverters.has(field.nestedConverterRef)) {
1245
+ result = input.converterRegistry.get(field.nestedConverterRef);
1246
+ }
1247
+ return result;
1248
+ }
1193
1249
  function collectAncestors(iface, registry) {
1194
1250
  const out = [];
1195
1251
  const visited = /* @__PURE__ */ new Set([iface.name]);
@@ -1261,31 +1317,42 @@ function findIdentifierInBarrelChain(filePath, identifier, visited) {
1261
1317
  let result = false;
1262
1318
  if (!visited.has(filePath)) {
1263
1319
  visited.add(filePath);
1264
- let text;
1265
- try {
1266
- text = readFileSync5(filePath, "utf8");
1267
- } catch {
1268
- text = void 0;
1269
- }
1320
+ const text = readFileTextSafe(filePath);
1270
1321
  if (text !== void 0) {
1271
- for (const pattern of EXPORT_DECL_PATTERNS) {
1272
- const re = new RegExp(pattern.source.replace("IDENT", escapeRegExp(identifier)));
1273
- if (re.test(text)) {
1274
- result = true;
1275
- break;
1276
- }
1277
- }
1278
- if (!result) {
1279
- const dir = dirname2(filePath);
1280
- for (const reExportTarget of collectReExportTargets(text)) {
1281
- const resolved = resolveReExport(dir, reExportTarget);
1282
- if (!resolved) continue;
1283
- if (findIdentifierInBarrelChain(resolved, identifier, visited)) {
1284
- result = true;
1285
- break;
1286
- }
1287
- }
1288
- }
1322
+ result = barrelTextDeclaresIdentifier(text, identifier) || reExportChainHasIdentifier({ fromFile: filePath, text, identifier, visited });
1323
+ }
1324
+ }
1325
+ return result;
1326
+ }
1327
+ function readFileTextSafe(filePath) {
1328
+ let text;
1329
+ try {
1330
+ text = readFileSync5(filePath, "utf8");
1331
+ } catch {
1332
+ text = void 0;
1333
+ }
1334
+ return text;
1335
+ }
1336
+ function barrelTextDeclaresIdentifier(text, identifier) {
1337
+ let result = false;
1338
+ for (const pattern of EXPORT_DECL_PATTERNS) {
1339
+ const re = new RegExp(pattern.source.replace("IDENT", escapeRegExp(identifier)));
1340
+ if (re.test(text)) {
1341
+ result = true;
1342
+ break;
1343
+ }
1344
+ }
1345
+ return result;
1346
+ }
1347
+ function reExportChainHasIdentifier(input) {
1348
+ const { fromFile, text, identifier, visited } = input;
1349
+ let result = false;
1350
+ const dir = dirname2(fromFile);
1351
+ for (const reExportTarget of collectReExportTargets(text)) {
1352
+ const resolved = resolveReExport(dir, reExportTarget);
1353
+ if (resolved && findIdentifierInBarrelChain(resolved, identifier, visited)) {
1354
+ result = true;
1355
+ break;
1289
1356
  }
1290
1357
  }
1291
1358
  return result;
@@ -1343,13 +1410,8 @@ function escapeRegExp(value) {
1343
1410
 
1344
1411
  // packages/dbx-cli/firebase-api-manifest/src/generate-api-manifest/emit.ts
1345
1412
  import { format, resolveConfig } from "prettier";
1346
-
1347
- // packages/util/src/lib/string/sort.ts
1348
- var compareStrings = (a, b) => a.localeCompare(b);
1349
-
1350
- // packages/dbx-cli/firebase-api-manifest/src/generate-api-manifest/emit.ts
1351
1413
  async function renderManifest(input) {
1352
- const { outputFile, entries, projectName, namespace, modelEntries, modelNamespace, emitConverters = false } = input;
1414
+ const { outputFile, entries, projectName, namespace, modelEntries, modelNamespace, enumEntries, enumNamespace, emitConverters = false } = input;
1353
1415
  const importsByPackage = /* @__PURE__ */ new Map();
1354
1416
  for (const entry of entries) {
1355
1417
  if (!entry.packageName || !entry.validatorName) continue;
@@ -1363,12 +1425,18 @@ async function renderManifest(input) {
1363
1425
  });
1364
1426
  const entryLines = entries.map((e) => renderEntry(e));
1365
1427
  const emitModels = Boolean(modelEntries && modelEntries.length > 0 && modelNamespace);
1366
- const dbxCliTypeImports = emitModels ? `import { type CliApiManifest, type CliModelManifest } from '@dereekb/dbx-cli';` : `import { type CliApiManifest } from '@dereekb/dbx-cli';`;
1428
+ const emitEnums = Boolean(enumEntries && Object.keys(enumEntries).length > 0 && enumNamespace);
1429
+ const dbxCliTypeNames = ["type CliApiManifest", ...emitModels ? ["type CliModelManifest"] : [], ...emitEnums ? ["type CliEnumManifest"] : []];
1430
+ const dbxCliTypeImports = `import { ${dbxCliTypeNames.join(", ")} } from '@dereekb/dbx-cli';`;
1367
1431
  const modelSection = emitModels ? `
1368
1432
 
1369
1433
  export const ${modelNamespace}: CliModelManifest = [
1370
1434
  ${(modelEntries ?? []).map((m) => renderModelEntry(m, emitConverters)).join(",\n")}
1371
1435
  ];
1436
+ ` : "";
1437
+ const enumSection = emitEnums ? `
1438
+
1439
+ export const ${enumNamespace}: CliEnumManifest = ${renderEnumManifest(enumEntries ?? {})};
1372
1440
  ` : "";
1373
1441
  const source = `/* eslint-disable @nx/enforce-module-boundaries */
1374
1442
  // AUTO-GENERATED \u2014 DO NOT EDIT.
@@ -1379,7 +1447,7 @@ ${dbxCliTypeImports}
1379
1447
 
1380
1448
  export const ${namespace}: CliApiManifest = [
1381
1449
  ${entryLines.join(",\n")}
1382
- ];${modelSection}
1450
+ ];${modelSection}${enumSection}
1383
1451
  `;
1384
1452
  return formatWithPrettier(source, outputFile);
1385
1453
  }
@@ -1402,7 +1470,7 @@ function renderEntry({ entry, validatorName }) {
1402
1470
  entry.mcpResultTypeDescription ? `mcpResultTypeDescription: ${JSON.stringify(entry.mcpResultTypeDescription)}` : void 0,
1403
1471
  entry.mcpResultFields && entry.mcpResultFields.length > 0 ? `mcpResultFields: ${renderDocFields(entry.mcpResultFields)}` : void 0
1404
1472
  ];
1405
- return ` { ${fields.filter((v) => Boolean(v)).join(", ")} }`;
1473
+ return ` { ${fields.filter(Boolean).join(", ")} }`;
1406
1474
  }
1407
1475
  function renderDocFields(fields) {
1408
1476
  const items = fields.map((field) => {
@@ -1432,7 +1500,7 @@ function renderModelEntry(entry, emitConverters) {
1432
1500
  entry.read ? `read: ${JSON.stringify(entry.read)}` : void 0,
1433
1501
  entry.serviceFactory ? `serviceFactory: { exportName: ${JSON.stringify(entry.serviceFactory.exportName)}, sourceFile: ${JSON.stringify(entry.serviceFactory.sourceFile)} }` : void 0
1434
1502
  ];
1435
- return ` { ${fields.filter((v) => Boolean(v)).join(", ")} }`;
1503
+ return ` { ${fields.filter(Boolean).join(", ")} }`;
1436
1504
  }
1437
1505
  function renderModelFields(fields, emitConverters) {
1438
1506
  let result;
@@ -1444,6 +1512,23 @@ function renderModelFields(fields, emitConverters) {
1444
1512
  }
1445
1513
  return result;
1446
1514
  }
1515
+ function renderEnumManifest(enums) {
1516
+ const names = Object.keys(enums).sort((a, b) => a.localeCompare(b));
1517
+ const items = names.map((name) => `${JSON.stringify(name)}: ${renderEnumEntry(enums[name])}`);
1518
+ return names.length === 0 ? "{}" : `{ ${items.join(", ")} }`;
1519
+ }
1520
+ function renderEnumEntry(entry) {
1521
+ const parts = [`name: ${JSON.stringify(entry.name)}`, `values: ${renderEnumValues(entry.values)}`, entry.description ? `description: ${JSON.stringify(entry.description)}` : void 0];
1522
+ return `{ ${parts.filter(Boolean).join(", ")} }`;
1523
+ }
1524
+ function renderEnumValues(values) {
1525
+ const items = values.map((value) => renderEnumValue(value));
1526
+ return `[${items.join(", ")}]`;
1527
+ }
1528
+ function renderEnumValue(value) {
1529
+ const parts = [`name: ${JSON.stringify(value.name)}`, `value: ${JSON.stringify(value.value)}`, value.description ? `description: ${JSON.stringify(value.description)}` : void 0];
1530
+ return `{ ${parts.filter(Boolean).join(", ")} }`;
1531
+ }
1447
1532
  function renderModelField(field, emitConverters) {
1448
1533
  const nestedIsArrayLiteral = field.nestedIsArray ? "true" : "false";
1449
1534
  const parts = [
@@ -1458,7 +1543,7 @@ function renderModelField(field, emitConverters) {
1458
1543
  field.nestedFields ? `nestedFields: ${renderModelFields(field.nestedFields, emitConverters)}` : void 0,
1459
1544
  field.nestedFields ? `nestedIsArray: ${nestedIsArrayLiteral}` : void 0
1460
1545
  ];
1461
- return `{ ${parts.filter((v) => Boolean(v)).join(", ")} }`;
1546
+ return `{ ${parts.filter(Boolean).join(", ")} }`;
1462
1547
  }
1463
1548
 
1464
1549
  // packages/dbx-cli/firebase-api-manifest/src/generate-api-manifest/main.ts
@@ -1551,8 +1636,9 @@ async function main() {
1551
1636
  collected.sort(compareEntries);
1552
1637
  const modelEntries = flags.emitModels ? assembleModels({ extractions: modelSources }) : [];
1553
1638
  const filteredModelEntries = flags.only ? modelEntries.filter((m) => flags.only?.has(m.modelType)) : modelEntries;
1639
+ const enumEntries = flags.emitModels ? collectModelEnums({ extractions: modelSources, models: filteredModelEntries }) : {};
1554
1640
  ensureOutputDir(outputDir);
1555
- const formatted = await renderManifest({ outputFile, entries: collected, projectName, namespace, modelEntries: filteredModelEntries, modelNamespace: deriveModelNamespace(flags.project), emitConverters: flags.emitModelConverters });
1641
+ const formatted = await renderManifest({ outputFile, entries: collected, projectName, namespace, modelEntries: filteredModelEntries, modelNamespace: deriveModelNamespace(flags.project), enumEntries, enumNamespace: deriveEnumNamespace(flags.project), emitConverters: flags.emitModelConverters });
1556
1642
  if (existsSync3(outputFile) && readFileSync6(outputFile, "utf8") === formatted) {
1557
1643
  console.log(`[unchanged] ${relative2(WORKSPACE_ROOT, outputFile)}`);
1558
1644
  } else {
@@ -1560,7 +1646,7 @@ async function main() {
1560
1646
  console.log(`[wrote] ${relative2(WORKSPACE_ROOT, outputFile)}`);
1561
1647
  }
1562
1648
  const groupCount = packageCache.size === 0 ? 0 : new Set(collected.map((c) => c.entry.groupName)).size;
1563
- const modelSummary = flags.emitModels ? ` \xB7 ${filteredModelEntries.length} models` : "";
1649
+ const modelSummary = flags.emitModels ? ` \xB7 ${filteredModelEntries.length} models \xB7 ${Object.keys(enumEntries).length} enums` : "";
1564
1650
  console.log(`Summary: ${groupCount} groups \xB7 ${collected.length} entries \xB7 ${collected.length - missingValidators} validators bound \xB7 ${missingValidators} missing \xB7 ${skippedGroups} skipped${modelSummary}`);
1565
1651
  if (flags.strict && missingValidators > 0) {
1566
1652
  console.error(`[strict] ${missingValidators} validator(s) missing \u2014 failing build.`);
@@ -1592,6 +1678,10 @@ function deriveModelNamespace(projectName) {
1592
1678
  const base = (projectName ?? "cli").replaceAll(/[^a-zA-Z0-9]+/g, "_");
1593
1679
  return `${base.toUpperCase()}_MODEL_MANIFEST`;
1594
1680
  }
1681
+ function deriveEnumNamespace(projectName) {
1682
+ const base = (projectName ?? "cli").replaceAll(/[^a-zA-Z0-9]+/g, "_");
1683
+ return `${base.toUpperCase()}_ENUM_MANIFEST`;
1684
+ }
1595
1685
  function parseFlags(argv) {
1596
1686
  let only;
1597
1687
  let strict = false;