@dereekb/dbx-cli 13.11.0 → 13.11.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.
@@ -0,0 +1,594 @@
1
+ #!/usr/bin/env node
2
+ import { createRequire as __createRequire } from 'node:module';
3
+ const require = __createRequire(import.meta.url);
4
+
5
+ // packages/dbx-cli/firebase-api-manifest/src/generate-api-manifest/main.ts
6
+ import { existsSync as existsSync3, mkdirSync, readFileSync as readFileSync5, writeFileSync } from "node:fs";
7
+ import { dirname as dirname3, isAbsolute as isAbsolute3, relative as relative2, resolve as resolve3 } from "node:path";
8
+
9
+ // packages/dbx-cli/firebase-api-manifest/src/generate-api-manifest/parse-functions.ts
10
+ import { readFileSync } from "node:fs";
11
+ import { Node, Project } from "ts-morph";
12
+ function parseFunctionsConfig(functionsTsPath) {
13
+ const project = new Project({ useInMemoryFileSystem: true, skipAddingFilesFromTsConfig: true });
14
+ const text = readFileSync(functionsTsPath, "utf8");
15
+ const sourceFile = project.createSourceFile("functions.ts", text, { overwrite: true });
16
+ const importIdentToModule = collectImportSpecifierMap(sourceFile);
17
+ const result = [];
18
+ for (const variable of sourceFile.getVariableDeclarations()) {
19
+ const initializer = readFunctionsConfigInitializer(variable);
20
+ if (initializer) collectGroupsFromConfig(initializer, importIdentToModule, result);
21
+ }
22
+ return result;
23
+ }
24
+ function readFunctionsConfigInitializer(variable) {
25
+ let result;
26
+ if (isFunctionsConfigVariable(variable)) {
27
+ const initializer = variable.getInitializer();
28
+ if (initializer && Node.isObjectLiteralExpression(initializer)) result = initializer;
29
+ }
30
+ return result;
31
+ }
32
+ function collectGroupsFromConfig(initializer, importIdentToModule, sink) {
33
+ for (const property of initializer.getProperties()) {
34
+ const group = readGroupFromProperty(property, importIdentToModule);
35
+ if (group) sink.push(group);
36
+ }
37
+ }
38
+ function readGroupFromProperty(property, importIdentToModule) {
39
+ if (!Node.isPropertyAssignment(property)) return void 0;
40
+ const valueExpr = property.getInitializer();
41
+ if (!valueExpr || !Node.isArrayLiteralExpression(valueExpr)) return void 0;
42
+ const first = valueExpr.getElements()[0];
43
+ if (!first || !Node.isIdentifier(first)) return void 0;
44
+ const className = first.getText();
45
+ const importedFromModule = importIdentToModule.get(className);
46
+ if (!importedFromModule) return void 0;
47
+ return { groupKey: property.getName(), className, importedFromModule };
48
+ }
49
+ function isFunctionsConfigVariable(variable) {
50
+ const name = variable.getName();
51
+ return name.endsWith("FIREBASE_FUNCTIONS_CONFIG");
52
+ }
53
+ function collectImportSpecifierMap(sourceFile) {
54
+ const map = /* @__PURE__ */ new Map();
55
+ for (const decl of sourceFile.getImportDeclarations()) {
56
+ const moduleSpecifier = decl.getModuleSpecifierValue();
57
+ for (const named of decl.getNamedImports()) {
58
+ const localName = named.getAliasNode()?.getText() ?? named.getName();
59
+ map.set(localName, moduleSpecifier);
60
+ }
61
+ }
62
+ return map;
63
+ }
64
+
65
+ // packages/dbx-cli/firebase-api-manifest/src/generate-api-manifest/resolve-package.ts
66
+ import { existsSync, readFileSync as readFileSync2 } from "node:fs";
67
+ import { dirname, isAbsolute, join, relative, resolve } from "node:path";
68
+ var _pathsCache;
69
+ function loadTsconfigPaths(workspaceRoot) {
70
+ if (_pathsCache) return _pathsCache;
71
+ const tsconfigPath = join(workspaceRoot, "tsconfig.base.json");
72
+ const raw = readFileSync2(tsconfigPath, "utf8");
73
+ const cleaned = stripJsonComments(raw);
74
+ const tsconfig = JSON.parse(cleaned);
75
+ const paths = tsconfig.compilerOptions?.paths ?? {};
76
+ const map = /* @__PURE__ */ new Map();
77
+ for (const [key, value] of Object.entries(paths)) {
78
+ if (Array.isArray(value) && value[0]) {
79
+ map.set(key, resolve(workspaceRoot, value[0]));
80
+ }
81
+ }
82
+ _pathsCache = map;
83
+ return map;
84
+ }
85
+ function resolveModuleToPackage(input) {
86
+ const { workspaceRoot, importingFile, moduleSpecifier } = input;
87
+ if (moduleSpecifier.startsWith(".")) {
88
+ const importingDir = dirname(importingFile);
89
+ const candidatePath = resolve(importingDir, moduleSpecifier);
90
+ return locatePackageForPath(workspaceRoot, candidatePath);
91
+ }
92
+ const paths = loadTsconfigPaths(workspaceRoot);
93
+ const indexFile = paths.get(moduleSpecifier);
94
+ if (indexFile) return locatePackageForPath(workspaceRoot, indexFile);
95
+ return locatePackageInNodeModules({ workspaceRoot, importingFile, moduleSpecifier });
96
+ }
97
+ function locatePackageInNodeModules(input) {
98
+ const { importingFile, moduleSpecifier } = input;
99
+ let current = dirname(importingFile);
100
+ while (current && current !== dirname(current)) {
101
+ const candidateRoot = join(current, "node_modules", moduleSpecifier);
102
+ const pkgJson = join(candidateRoot, "package.json");
103
+ if (existsSync(pkgJson)) {
104
+ try {
105
+ const pkg = JSON.parse(readFileSync2(pkgJson, "utf8"));
106
+ if (pkg.name) return { packageName: pkg.name, packageRoot: candidateRoot };
107
+ } catch {
108
+ }
109
+ }
110
+ current = dirname(current);
111
+ }
112
+ return void 0;
113
+ }
114
+ function locatePackageForPath(workspaceRoot, startPath) {
115
+ let current = startPath;
116
+ while (current && current !== workspaceRoot && current !== dirname(current)) {
117
+ if (existsSync(join(current, "package.json"))) {
118
+ try {
119
+ const pkg = JSON.parse(readFileSync2(join(current, "package.json"), "utf8"));
120
+ if (pkg.name) return { packageName: pkg.name, packageRoot: current };
121
+ } catch {
122
+ }
123
+ }
124
+ current = dirname(current);
125
+ }
126
+ return void 0;
127
+ }
128
+ function relPath(workspaceRoot, absolutePath) {
129
+ return relative(workspaceRoot, absolutePath).split("\\").join("/");
130
+ }
131
+ function stripJsonComments(text) {
132
+ return text.replaceAll(/\/\*[\s\S]*?\*\//g, "").replaceAll(/^\s*\/\/.*$/gm, "");
133
+ }
134
+
135
+ // packages/dbx-cli/firebase-api-manifest/src/generate-api-manifest/find-api-files.ts
136
+ import { readdirSync, readFileSync as readFileSync3, statSync } from "node:fs";
137
+ import { join as join2 } from "node:path";
138
+
139
+ // packages/dbx-cli/firebase-api-manifest/src/generate-api-manifest/extract-crud.ts
140
+ import { Node as Node2, Project as Project2 } from "ts-morph";
141
+ var SUPPORTED_VERBS = /* @__PURE__ */ new Set(["create", "read", "update", "delete", "query"]);
142
+ function extractCrudEntries(source) {
143
+ const project = new Project2({ useInMemoryFileSystem: true, skipAddingFilesFromTsConfig: true });
144
+ const sourceFile = project.createSourceFile(source.name, source.text, { overwrite: true });
145
+ const entries = [];
146
+ const modelKeys = [];
147
+ const crudConfigType = findTypeAliasByEnding(sourceFile, "ModelCrudFunctionsConfig");
148
+ const groupName = inferGroupName(sourceFile);
149
+ const functionsClassName = findFunctionsClassName(sourceFile);
150
+ if (crudConfigType) {
151
+ const literal = crudConfigType.getTypeNode();
152
+ if (literal && Node2.isTypeLiteral(literal)) {
153
+ for (const member of literal.getMembers()) {
154
+ if (!Node2.isPropertySignature(member)) continue;
155
+ const modelName = member.getName();
156
+ modelKeys.push(modelName);
157
+ const valueNode = member.getTypeNode();
158
+ if (!valueNode) continue;
159
+ if (isNullLiteralType(valueNode)) continue;
160
+ if (Node2.isTypeLiteral(valueNode)) {
161
+ for (const verbMember of valueNode.getMembers()) {
162
+ if (!Node2.isPropertySignature(verbMember)) continue;
163
+ const verb = verbMember.getName();
164
+ if (!SUPPORTED_VERBS.has(verb)) continue;
165
+ const verbValueNode = verbMember.getTypeNode();
166
+ if (!verbValueNode) continue;
167
+ collectVerbEntries({ modelName, verb, valueNode: verbValueNode, entries });
168
+ }
169
+ }
170
+ }
171
+ }
172
+ }
173
+ const functionTypeMap = findTypeAliasByEnding(sourceFile, "FunctionTypeMap");
174
+ if (functionTypeMap) {
175
+ const literal = functionTypeMap.getTypeNode();
176
+ if (literal && Node2.isTypeLiteral(literal)) {
177
+ for (const member of literal.getMembers()) {
178
+ if (!Node2.isPropertySignature(member)) continue;
179
+ const key = member.getName();
180
+ const valueNode = member.getTypeNode();
181
+ const tuple = valueNode ? readTupleParamsResult(valueNode) : void 0;
182
+ entries.push({
183
+ model: key,
184
+ verb: "standalone",
185
+ specifier: void 0,
186
+ paramsTypeName: tuple?.params,
187
+ resultTypeName: tuple?.result,
188
+ line: member.getStartLineNumber()
189
+ });
190
+ }
191
+ }
192
+ }
193
+ return { groupName, modelKeys, entries, functionsClassName };
194
+ }
195
+ function findTypeAliasByEnding(sourceFile, ending) {
196
+ for (const alias of sourceFile.getTypeAliases()) {
197
+ if (alias.getName().endsWith(ending) && alias.getTypeNode()) return alias;
198
+ }
199
+ return void 0;
200
+ }
201
+ function findFunctionsClassName(sourceFile) {
202
+ for (const cls of sourceFile.getClasses()) {
203
+ if (!cls.isAbstract()) continue;
204
+ const name = cls.getName();
205
+ if (name?.endsWith("Functions")) return name;
206
+ }
207
+ return void 0;
208
+ }
209
+ function inferGroupName(sourceFile) {
210
+ for (const alias of sourceFile.getTypeAliases()) {
211
+ const name = alias.getName();
212
+ if (name.endsWith("ModelCrudFunctionsConfig")) {
213
+ const stem = name.slice(0, -"ModelCrudFunctionsConfig".length);
214
+ if (stem.length > 0) return stem;
215
+ }
216
+ }
217
+ for (const alias of sourceFile.getTypeAliases()) {
218
+ const name = alias.getName();
219
+ if (name.endsWith("FunctionTypeMap")) {
220
+ const stem = name.slice(0, -"FunctionTypeMap".length);
221
+ if (stem.length > 0) return stem;
222
+ }
223
+ }
224
+ return void 0;
225
+ }
226
+ function isNullLiteralType(node) {
227
+ if (Node2.isLiteralTypeNode(node)) {
228
+ const literal = node.getLiteral();
229
+ if (Node2.isNullLiteral(literal)) return true;
230
+ }
231
+ return false;
232
+ }
233
+ function collectVerbEntries(input) {
234
+ const { modelName, verb, valueNode, entries } = input;
235
+ if (Node2.isTypeLiteral(valueNode)) {
236
+ for (const specMember of valueNode.getMembers()) {
237
+ if (!Node2.isPropertySignature(specMember)) continue;
238
+ const specifier = specMember.getName();
239
+ const leafNode = specMember.getTypeNode();
240
+ const leaf2 = leafNode ? readTupleParamsResult(leafNode) ?? readBareParams(leafNode) : void 0;
241
+ entries.push({
242
+ model: modelName,
243
+ verb,
244
+ specifier,
245
+ paramsTypeName: leaf2?.params,
246
+ resultTypeName: leaf2?.result,
247
+ line: specMember.getStartLineNumber()
248
+ });
249
+ }
250
+ return;
251
+ }
252
+ const leaf = readTupleParamsResult(valueNode) ?? readBareParams(valueNode);
253
+ entries.push({
254
+ model: modelName,
255
+ verb,
256
+ specifier: void 0,
257
+ paramsTypeName: leaf?.params,
258
+ resultTypeName: leaf?.result,
259
+ line: valueNode.getStartLineNumber()
260
+ });
261
+ }
262
+ function readTupleParamsResult(node) {
263
+ if (!Node2.isTupleTypeNode(node)) return void 0;
264
+ const elements = node.getElements();
265
+ if (elements.length === 0) return void 0;
266
+ const params = elements[0] ? typeNodeName(elements[0]) : void 0;
267
+ const result = elements[1] ? typeNodeName(elements[1]) : void 0;
268
+ return { params, result };
269
+ }
270
+ function readBareParams(node) {
271
+ const params = typeNodeName(node);
272
+ if (!params) return void 0;
273
+ return { params, result: void 0 };
274
+ }
275
+ function typeNodeName(node) {
276
+ if (Node2.isTypeReference(node)) {
277
+ return node.getTypeName().getText();
278
+ }
279
+ const text = node.getText().trim();
280
+ return text.length > 0 ? text : void 0;
281
+ }
282
+
283
+ // packages/dbx-cli/firebase-api-manifest/src/generate-api-manifest/find-api-files.ts
284
+ function findApiFiles(packageRoot) {
285
+ const libRoot = join2(packageRoot, "src", "lib");
286
+ if (!safeIsDirectory(libRoot)) return [];
287
+ const out = [];
288
+ const seenClassNames = /* @__PURE__ */ new Set();
289
+ for (const file of walkApiFiles(libRoot)) {
290
+ const text = readFileSync3(file, "utf8");
291
+ const extraction = extractCrudEntries({ name: file, text });
292
+ if (!extraction.functionsClassName) continue;
293
+ if (seenClassNames.has(extraction.functionsClassName)) continue;
294
+ seenClassNames.add(extraction.functionsClassName);
295
+ out.push({ filePath: file, className: extraction.functionsClassName, extraction });
296
+ }
297
+ return out;
298
+ }
299
+ function* walkApiFiles(dir) {
300
+ for (const entry of readdirSync(dir).sort()) {
301
+ if (entry === "node_modules" || entry === "dist") continue;
302
+ const p = join2(dir, entry);
303
+ const stat = statSync(p);
304
+ if (stat.isDirectory()) {
305
+ yield* walkApiFiles(p);
306
+ } else if (isApiFile(entry)) {
307
+ yield p;
308
+ }
309
+ }
310
+ }
311
+ function isApiFile(name) {
312
+ if (name.endsWith(".spec.ts") || name.endsWith(".test.ts")) return false;
313
+ return name.endsWith(".api.ts") || name.endsWith(".api.d.ts");
314
+ }
315
+ function safeIsDirectory(p) {
316
+ try {
317
+ return statSync(p).isDirectory();
318
+ } catch {
319
+ return false;
320
+ }
321
+ }
322
+
323
+ // packages/dbx-cli/firebase-api-manifest/src/generate-api-manifest/bind-validators.ts
324
+ import { existsSync as existsSync2, readdirSync as readdirSync2, readFileSync as readFileSync4, statSync as statSync2 } from "node:fs";
325
+ import { dirname as dirname2, isAbsolute as isAbsolute2, join as join3, resolve as resolve2 } from "node:path";
326
+ function deriveValidatorName(paramsTypeName) {
327
+ if (!paramsTypeName) return "";
328
+ return paramsTypeName.charAt(0).toLowerCase() + paramsTypeName.slice(1) + "Type";
329
+ }
330
+ function isExportedFromPackage(input) {
331
+ const { packageRoot, identifier } = input;
332
+ const indexPath = locateBarrelEntry(packageRoot);
333
+ if (!indexPath) return false;
334
+ return findIdentifierInBarrelChain(indexPath, identifier, /* @__PURE__ */ new Set());
335
+ }
336
+ function locateBarrelEntry(packageRoot) {
337
+ const candidates = [join3(packageRoot, "src", "index.ts"), join3(packageRoot, "src", "index.d.ts"), join3(packageRoot, "index.d.ts"), join3(packageRoot, "index.ts")];
338
+ return candidates.find((candidate) => existsSync2(candidate));
339
+ }
340
+ var EXPORT_DECL_PATTERNS = [/export\s+(?:declare\s+)?const\s+IDENT\b/, /export\s+(?:declare\s+)?function\s+IDENT\b/, /export\s*\{[^}]*\bIDENT\b[^}]*\}/];
341
+ function findIdentifierInBarrelChain(filePath, identifier, visited) {
342
+ if (visited.has(filePath)) return false;
343
+ visited.add(filePath);
344
+ let text;
345
+ try {
346
+ text = readFileSync4(filePath, "utf8");
347
+ } catch {
348
+ return false;
349
+ }
350
+ for (const pattern of EXPORT_DECL_PATTERNS) {
351
+ const re = new RegExp(pattern.source.replace("IDENT", escapeRegExp(identifier)));
352
+ if (re.test(text)) return true;
353
+ }
354
+ const dir = dirname2(filePath);
355
+ for (const reExportTarget of collectReExportTargets(text)) {
356
+ const resolved = resolveReExport(dir, reExportTarget);
357
+ if (!resolved) continue;
358
+ if (findIdentifierInBarrelChain(resolved, identifier, visited)) return true;
359
+ }
360
+ return false;
361
+ }
362
+ function collectReExportTargets(text) {
363
+ const out = [];
364
+ const re = /export\s*(?:\*|\{[^}]*\})\s*from\s*['"]([^'"]+)['"]/g;
365
+ let match;
366
+ while ((match = re.exec(text)) !== null) {
367
+ out.push(match[1]);
368
+ }
369
+ return out;
370
+ }
371
+ function resolveReExport(fromDir, target) {
372
+ if (!target.startsWith(".")) return void 0;
373
+ const candidate = isAbsolute2(target) ? target : resolve2(fromDir, target);
374
+ let result;
375
+ for (const ext of [".ts", ".mts", ".d.ts", "/index.ts", "/index.mts", "/index.d.ts"]) {
376
+ const probe = hasTsModuleExtension(candidate) ? candidate : candidate + ext;
377
+ const resolved = resolveExistingTsPath(probe);
378
+ if (resolved) {
379
+ result = resolved;
380
+ break;
381
+ }
382
+ }
383
+ return result;
384
+ }
385
+ function hasTsModuleExtension(value) {
386
+ return value.endsWith(".ts") || value.endsWith(".mts");
387
+ }
388
+ function resolveExistingTsPath(probe) {
389
+ if (!existsSync2(probe)) return void 0;
390
+ const stat = statSync2(probe);
391
+ if (stat.isFile()) return probe;
392
+ if (!stat.isDirectory()) return void 0;
393
+ const sourceIndex = join3(probe, "index.ts");
394
+ if (existsSync2(sourceIndex)) return sourceIndex;
395
+ const declarationIndex = join3(probe, "index.d.ts");
396
+ return existsSync2(declarationIndex) ? declarationIndex : void 0;
397
+ }
398
+ function escapeRegExp(value) {
399
+ return value.replaceAll(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`);
400
+ }
401
+
402
+ // packages/dbx-cli/firebase-api-manifest/src/generate-api-manifest/emit.ts
403
+ import { format, resolveConfig } from "prettier";
404
+ async function renderManifest(input) {
405
+ const { outputFile, entries, projectName, namespace } = input;
406
+ const importsByPackage = /* @__PURE__ */ new Map();
407
+ for (const entry of entries) {
408
+ if (!entry.packageName || !entry.validatorName) continue;
409
+ const set = importsByPackage.get(entry.packageName) ?? /* @__PURE__ */ new Set();
410
+ set.add(entry.validatorName);
411
+ importsByPackage.set(entry.packageName, set);
412
+ }
413
+ const importLines = [...importsByPackage.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([pkg, names]) => {
414
+ const sortedNames = [...names].sort((a, b) => a.localeCompare(b)).join(", ");
415
+ return `import { ${sortedNames} } from '${pkg}';`;
416
+ });
417
+ const entryLines = entries.map((e) => renderEntry(e));
418
+ const source = `/* eslint-disable @nx/enforce-module-boundaries */
419
+ // AUTO-GENERATED \u2014 DO NOT EDIT.
420
+ // Run \`pnpm nx run ${projectName}:generate-api-manifest\` to refresh.
421
+
422
+ ${importLines.join("\n")}
423
+ import { type CliApiManifest } from '@dereekb/dbx-cli';
424
+
425
+ export const ${namespace}: CliApiManifest = [
426
+ ${entryLines.join(",\n")}
427
+ ];
428
+ `;
429
+ return formatWithPrettier(source, outputFile);
430
+ }
431
+ function renderEntry({ entry, validatorName }) {
432
+ const fields = [
433
+ `model: ${JSON.stringify(entry.model)}`,
434
+ `verb: ${JSON.stringify(entry.verb)}`,
435
+ entry.specifier ? `specifier: ${JSON.stringify(entry.specifier)}` : void 0,
436
+ entry.paramsTypeName ? `paramsTypeName: ${JSON.stringify(entry.paramsTypeName)}` : void 0,
437
+ validatorName ? `paramsValidator: ${validatorName}` : void 0,
438
+ entry.resultTypeName ? `resultTypeName: ${JSON.stringify(entry.resultTypeName)}` : void 0,
439
+ `groupName: ${JSON.stringify(entry.groupName)}`,
440
+ `sourceFile: ${JSON.stringify(entry.sourceFile)}`
441
+ ];
442
+ return ` { ${fields.filter((v) => Boolean(v)).join(", ")} }`;
443
+ }
444
+ async function formatWithPrettier(source, outputFile) {
445
+ const config = await resolveConfig(outputFile);
446
+ return format(source, { ...config, filepath: outputFile });
447
+ }
448
+
449
+ // packages/dbx-cli/firebase-api-manifest/src/generate-api-manifest/main.ts
450
+ var WORKSPACE_ROOT = process.cwd();
451
+ async function main() {
452
+ const flags = parseFlags(process.argv.slice(2));
453
+ if (!flags.functionsConfig || !flags.output) {
454
+ printUsageAndExit();
455
+ return;
456
+ }
457
+ const functionsConfigPath = resolveWorkspacePath(flags.functionsConfig);
458
+ const outputFile = resolveWorkspacePath(flags.output);
459
+ const outputDir = dirname3(outputFile);
460
+ const projectName = flags.project ?? "<cli>";
461
+ const namespace = deriveNamespace(flags.project);
462
+ if (!existsSync3(functionsConfigPath)) {
463
+ throw new Error(`functions-config file not found: ${functionsConfigPath}`);
464
+ }
465
+ const groups = parseFunctionsConfig(functionsConfigPath);
466
+ if (groups.length === 0) {
467
+ throw new Error(`No function groups discovered in ${relPath(WORKSPACE_ROOT, functionsConfigPath)}.`);
468
+ }
469
+ const packageCache = /* @__PURE__ */ new Map();
470
+ const apiFilesCache = /* @__PURE__ */ new Map();
471
+ const collected = [];
472
+ let missingValidators = 0;
473
+ let skippedGroups = 0;
474
+ for (const group of groups) {
475
+ const pkg = resolveModuleToPackage({ workspaceRoot: WORKSPACE_ROOT, importingFile: functionsConfigPath, moduleSpecifier: group.importedFromModule });
476
+ if (!pkg) {
477
+ console.warn(`[skip] ${group.groupKey}: cannot resolve module '${group.importedFromModule}' for class ${group.className}`);
478
+ skippedGroups++;
479
+ continue;
480
+ }
481
+ if (!packageCache.has(pkg.packageRoot)) packageCache.set(pkg.packageRoot, pkg);
482
+ if (!apiFilesCache.has(pkg.packageRoot)) apiFilesCache.set(pkg.packageRoot, findApiFiles(pkg.packageRoot));
483
+ const apiFiles = apiFilesCache.get(pkg.packageRoot) ?? [];
484
+ const match = apiFiles.find((f) => f.className === group.className);
485
+ if (!match) {
486
+ console.warn(`[skip] ${group.groupKey}: no .api.{ts,d.ts} under ${pkg.packageName} declares 'export abstract class ${group.className}'`);
487
+ skippedGroups++;
488
+ continue;
489
+ }
490
+ if (match.extraction.entries.length === 0) {
491
+ console.warn(`[skip] ${group.groupKey}: ${group.className} is not a CRUD model group (no *ModelCrudFunctionsConfig in ${relPath(WORKSPACE_ROOT, match.filePath)})`);
492
+ skippedGroups++;
493
+ continue;
494
+ }
495
+ const groupName = match.extraction.groupName ?? group.className.replace(/Functions$/, "");
496
+ const sourceFileRel = relPath(WORKSPACE_ROOT, match.filePath);
497
+ for (const entry of match.extraction.entries) {
498
+ if (flags.only && !flags.only.has(entry.model)) continue;
499
+ if (entry.verb === "standalone") continue;
500
+ const enriched = { entry: { ...entry, groupName, sourceFile: sourceFileRel } };
501
+ if (entry.paramsTypeName) {
502
+ const validatorName = deriveValidatorName(entry.paramsTypeName);
503
+ const found = isExportedFromPackage({ packageRoot: pkg.packageRoot, identifier: validatorName });
504
+ if (found) {
505
+ enriched.packageName = pkg.packageName;
506
+ enriched.validatorName = validatorName;
507
+ } else {
508
+ missingValidators++;
509
+ const specPart = entry.specifier ? "/" + entry.specifier : "";
510
+ console.warn(`[no-validator] ${pkg.packageName} \xB7 ${entry.model}/${entry.verb}${specPart} \u2192 ${validatorName} not exported`);
511
+ }
512
+ }
513
+ collected.push(enriched);
514
+ }
515
+ }
516
+ collected.sort(compareEntries);
517
+ ensureOutputDir(outputDir);
518
+ const formatted = await renderManifest({ outputFile, entries: collected, projectName, namespace });
519
+ if (existsSync3(outputFile) && readFileSync5(outputFile, "utf8") === formatted) {
520
+ console.log(`[unchanged] ${relative2(WORKSPACE_ROOT, outputFile)}`);
521
+ } else {
522
+ writeFileSync(outputFile, formatted);
523
+ console.log(`[wrote] ${relative2(WORKSPACE_ROOT, outputFile)}`);
524
+ }
525
+ const groupCount = packageCache.size === 0 ? 0 : new Set(collected.map((c) => c.entry.groupName)).size;
526
+ console.log(`Summary: ${groupCount} groups \xB7 ${collected.length} entries \xB7 ${collected.length - missingValidators} validators bound \xB7 ${missingValidators} missing \xB7 ${skippedGroups} skipped`);
527
+ if (flags.strict && missingValidators > 0) {
528
+ console.error(`[strict] ${missingValidators} validator(s) missing \u2014 failing build.`);
529
+ process.exit(1);
530
+ }
531
+ }
532
+ function compareEntries(a, b) {
533
+ if (a.entry.model !== b.entry.model) return a.entry.model.localeCompare(b.entry.model);
534
+ if (a.entry.verb !== b.entry.verb) return a.entry.verb.localeCompare(b.entry.verb);
535
+ return (a.entry.specifier ?? "").localeCompare(b.entry.specifier ?? "");
536
+ }
537
+ function ensureOutputDir(outputDir) {
538
+ if (!existsSync3(outputDir)) mkdirSync(outputDir, { recursive: true });
539
+ }
540
+ function resolveWorkspacePath(value) {
541
+ return isAbsolute3(value) ? value : resolve3(WORKSPACE_ROOT, value);
542
+ }
543
+ function deriveNamespace(projectName) {
544
+ const base = (projectName ?? "cli").replaceAll(/[^a-zA-Z0-9]+/g, "_");
545
+ return `${base.toUpperCase()}_API_MANIFEST`;
546
+ }
547
+ function parseFlags(argv) {
548
+ let only;
549
+ let strict = false;
550
+ let functionsConfig;
551
+ let output;
552
+ let project;
553
+ for (const arg of argv) {
554
+ if (arg === "--strict") {
555
+ strict = true;
556
+ } else if (arg.startsWith("--only=")) {
557
+ const list = arg.slice("--only=".length).split(",").map((s) => s.trim()).filter(Boolean);
558
+ if (list.length > 0) only = new Set(list);
559
+ } else if (arg.startsWith("--functions-config=")) {
560
+ functionsConfig = arg.slice("--functions-config=".length);
561
+ } else if (arg.startsWith("--output=")) {
562
+ output = arg.slice("--output=".length);
563
+ } else if (arg.startsWith("--project=")) {
564
+ project = arg.slice("--project=".length);
565
+ }
566
+ }
567
+ return { only, strict, functionsConfig, output, project };
568
+ }
569
+ function printUsageAndExit() {
570
+ console.error(String.raw`generate-api-manifest
571
+
572
+ Usage:
573
+ node dist/packages/dbx-cli/firebase-api-manifest/main.js \
574
+ --project=<name> \
575
+ --functions-config=<path-to-functions.ts> \
576
+ --output=<path-to-manifest.generated.ts> \
577
+ [--only=model[,model]] [--strict]
578
+
579
+ Required flags:
580
+ --functions-config=<path> Path to the app's functions.ts (workspace-relative ok).
581
+ --output=<path> Path to the manifest TS file to write (workspace-relative ok).
582
+
583
+ Optional:
584
+ --project=<name> Project name to show in the regenerate banner.
585
+ --only=<csv> Filter to listed model names.
586
+ --strict Fail when any validator binding is missing.`);
587
+ process.exit(1);
588
+ }
589
+ try {
590
+ await main();
591
+ } catch (e) {
592
+ console.error(e);
593
+ process.exit(1);
594
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "name": "@dereekb/dbx-cli-firebase-api-manifest",
3
+ "version": "13.11.1",
4
+ "private": true,
5
+ "type": "module",
6
+ "devDependencies": {
7
+ "prettier": "3.8.3",
8
+ "ts-morph": "^21.0.0"
9
+ }
10
+ }
package/index.cjs.js CHANGED
@@ -7102,20 +7102,30 @@ var MANIFEST_HELP_DATA_FORMATS = new Set([
7102
7102
  'both'
7103
7103
  ]);
7104
7104
  var DATA_HELP_FLAG = '--data-help';
7105
+ /**
7106
+ * Default name of the parent command that groups all per-model manifest commands.
7107
+ * Surfaces as `<cli> model <model> <action>` so the top-level `--help` stays focused
7108
+ * on first-class commands instead of dumping every model.
7109
+ */ var DEFAULT_MANIFEST_MODEL_COMMAND_NAME = 'model';
7105
7110
  /**
7106
7111
  * Builds yargs `CommandModule[]` from a generated {@link CliApiManifest}.
7107
7112
  *
7108
- * Groups entries by `model`, emitting one parent command per model and one child action per entry
7109
- * (named `<verb>` or `<verb>-<specifier>`). Each child accepts `--data <json>`, validates the
7113
+ * Returns a single parent command (default name `model`) whose subcommands are the per-model
7114
+ * dispatch commands. Each per-model subcommand has one child action per entry
7115
+ * (named `<verb>` or `<verb>-<specifier>`). Each leaf accepts `--data <json>`, validates the
7110
7116
  * payload against the entry's bound arktype validator (when present), and dispatches via the
7111
7117
  * authenticated CLI context's `callModel` helper. Standalone entries are skipped because they
7112
7118
  * are not dispatched through the `/model/call` endpoint.
7113
7119
  *
7120
+ * Wrapping under `model` keeps the top-level `--help` short — users invoke
7121
+ * `<cli> model --help` to see the full list of available models.
7122
+ *
7114
7123
  * @param manifest - The generated manifest array.
7115
7124
  * @param options - Optional overrides; see {@link BuildManifestCommandsOptions}.
7116
- * @returns The yargs `CommandModule[]` ready to be passed to `runCli({ apiCommands })`.
7125
+ * @returns The yargs `CommandModule[]` ready to be passed to `runCli({ apiCommands })`. Empty
7126
+ * when the manifest has no callable entries.
7117
7127
  */ function buildManifestCommands(manifest, options) {
7118
- var _ref, _ref1, _ref2, _ref3;
7128
+ var _ref, _ref1, _ref2, _ref3, _ref4;
7119
7129
  var callable = manifest.filter(function(e) {
7120
7130
  return !SKIPPED_VERBS.has(e.verb);
7121
7131
  });
@@ -7147,38 +7157,55 @@ var DATA_HELP_FLAG = '--data-help';
7147
7157
  }
7148
7158
  }
7149
7159
  }
7160
+ if (byModel.size === 0) {
7161
+ return [];
7162
+ }
7150
7163
  var argv = (_ref = options === null || options === void 0 ? void 0 : options.argv) !== null && _ref !== void 0 ? _ref : process.argv;
7151
7164
  var dataHelpFormat = (_ref1 = options === null || options === void 0 ? void 0 : options.dataHelpFormat) !== null && _ref1 !== void 0 ? _ref1 : detectDataHelpFormat(argv);
7152
7165
  var focusHelp = ((_ref2 = options === null || options === void 0 ? void 0 : options.focusHelpOnDataHelp) !== null && _ref2 !== void 0 ? _ref2 : true) && hasDataHelpFlag(argv) && !hasAllHelpFlag(argv);
7153
7166
  var hideOnFocus = focusHelp ? (_ref3 = options === null || options === void 0 ? void 0 : options.hiddenWhenFocused) !== null && _ref3 !== void 0 ? _ref3 : STANDARD_GLOBAL_OPTION_NAMES : [];
7154
- var commands = [];
7155
- var _iteratorNormalCompletion1 = true, _didIteratorError1 = false, _iteratorError1 = undefined;
7156
- try {
7157
- for(var _iterator1 = _to_consumable_array(byModel.entries()).sort(function(param, param1) {
7158
- var _param = _sliced_to_array(param, 1), a = _param[0], _param1 = _sliced_to_array(param1, 1), b = _param1[0];
7159
- return a.localeCompare(b);
7160
- })[Symbol.iterator](), _step1; !(_iteratorNormalCompletion1 = (_step1 = _iterator1.next()).done); _iteratorNormalCompletion1 = true){
7161
- var _step_value = _sliced_to_array(_step1.value, 2), model = _step_value[0], entries = _step_value[1];
7162
- commands.push(buildModelCommand(model, entries, {
7163
- dataHelpFormat: dataHelpFormat,
7164
- hideOnFocus: hideOnFocus
7165
- }));
7166
- }
7167
- } catch (err) {
7168
- _didIteratorError1 = true;
7169
- _iteratorError1 = err;
7170
- } finally{
7171
- try {
7172
- if (!_iteratorNormalCompletion1 && _iterator1.return != null) {
7173
- _iterator1.return();
7174
- }
7175
- } finally{
7176
- if (_didIteratorError1) {
7177
- throw _iteratorError1;
7167
+ var modelCommandName = (_ref4 = options === null || options === void 0 ? void 0 : options.modelCommandName) !== null && _ref4 !== void 0 ? _ref4 : DEFAULT_MANIFEST_MODEL_COMMAND_NAME;
7168
+ var sortedModels = _to_consumable_array(byModel.entries()).sort(function(param, param1) {
7169
+ var _param = _sliced_to_array(param, 1), a = _param[0], _param1 = _sliced_to_array(param1, 1), b = _param1[0];
7170
+ return a.localeCompare(b);
7171
+ });
7172
+ var context = {
7173
+ dataHelpFormat: dataHelpFormat,
7174
+ hideOnFocus: hideOnFocus
7175
+ };
7176
+ return [
7177
+ {
7178
+ command: "".concat(modelCommandName, " <model>"),
7179
+ describe: "Call typed model APIs (".concat(byModel.size, " model").concat(byModel.size === 1 ? '' : 's', "). Use `").concat(modelCommandName, " --help` to list them."),
7180
+ builder: function builder(yargs) {
7181
+ var _iteratorNormalCompletion = true, _didIteratorError = false, _iteratorError = undefined;
7182
+ try {
7183
+ for(var _iterator = sortedModels[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true){
7184
+ var _step_value = _sliced_to_array(_step.value, 2), model = _step_value[0], entries = _step_value[1];
7185
+ yargs.command(buildModelCommand(model, entries, context));
7186
+ }
7187
+ } catch (err) {
7188
+ _didIteratorError = true;
7189
+ _iteratorError = err;
7190
+ } finally{
7191
+ try {
7192
+ if (!_iteratorNormalCompletion && _iterator.return != null) {
7193
+ _iterator.return();
7194
+ }
7195
+ } finally{
7196
+ if (_didIteratorError) {
7197
+ throw _iteratorError;
7198
+ }
7199
+ }
7200
+ }
7201
+ hideGlobalOptions(yargs, hideOnFocus);
7202
+ return yargs.demandCommand(1, 'Please specify a model.');
7203
+ },
7204
+ handler: function handler() {
7205
+ return undefined;
7178
7206
  }
7179
7207
  }
7180
- }
7181
- return commands;
7208
+ ];
7182
7209
  }
7183
7210
  /**
7184
7211
  * Returns true if `--data-help` (with or without a value) appears anywhere in
@@ -8092,6 +8119,7 @@ exports.CliError = CliError;
8092
8119
  exports.DEFAULT_CLI_OIDC_SCOPES = DEFAULT_CLI_OIDC_SCOPES;
8093
8120
  exports.DEFAULT_CLI_SECRET_PATTERNS = DEFAULT_CLI_SECRET_PATTERNS;
8094
8121
  exports.DEFAULT_MANIFEST_HELP_DATA_FORMAT = DEFAULT_MANIFEST_HELP_DATA_FORMAT;
8122
+ exports.DEFAULT_MANIFEST_MODEL_COMMAND_NAME = DEFAULT_MANIFEST_MODEL_COMMAND_NAME;
8095
8123
  exports.DUMP_MERGE_MODES = DUMP_MERGE_MODES;
8096
8124
  exports.DUMP_OUTPUT_MODES = DUMP_OUTPUT_MODES;
8097
8125
  exports.MODEL_WRITE_OIDC_SCOPES = MODEL_WRITE_OIDC_SCOPES;
package/index.esm.js CHANGED
@@ -7100,20 +7100,30 @@ var MANIFEST_HELP_DATA_FORMATS = new Set([
7100
7100
  'both'
7101
7101
  ]);
7102
7102
  var DATA_HELP_FLAG = '--data-help';
7103
+ /**
7104
+ * Default name of the parent command that groups all per-model manifest commands.
7105
+ * Surfaces as `<cli> model <model> <action>` so the top-level `--help` stays focused
7106
+ * on first-class commands instead of dumping every model.
7107
+ */ var DEFAULT_MANIFEST_MODEL_COMMAND_NAME = 'model';
7103
7108
  /**
7104
7109
  * Builds yargs `CommandModule[]` from a generated {@link CliApiManifest}.
7105
7110
  *
7106
- * Groups entries by `model`, emitting one parent command per model and one child action per entry
7107
- * (named `<verb>` or `<verb>-<specifier>`). Each child accepts `--data <json>`, validates the
7111
+ * Returns a single parent command (default name `model`) whose subcommands are the per-model
7112
+ * dispatch commands. Each per-model subcommand has one child action per entry
7113
+ * (named `<verb>` or `<verb>-<specifier>`). Each leaf accepts `--data <json>`, validates the
7108
7114
  * payload against the entry's bound arktype validator (when present), and dispatches via the
7109
7115
  * authenticated CLI context's `callModel` helper. Standalone entries are skipped because they
7110
7116
  * are not dispatched through the `/model/call` endpoint.
7111
7117
  *
7118
+ * Wrapping under `model` keeps the top-level `--help` short — users invoke
7119
+ * `<cli> model --help` to see the full list of available models.
7120
+ *
7112
7121
  * @param manifest - The generated manifest array.
7113
7122
  * @param options - Optional overrides; see {@link BuildManifestCommandsOptions}.
7114
- * @returns The yargs `CommandModule[]` ready to be passed to `runCli({ apiCommands })`.
7123
+ * @returns The yargs `CommandModule[]` ready to be passed to `runCli({ apiCommands })`. Empty
7124
+ * when the manifest has no callable entries.
7115
7125
  */ function buildManifestCommands(manifest, options) {
7116
- var _ref, _ref1, _ref2, _ref3;
7126
+ var _ref, _ref1, _ref2, _ref3, _ref4;
7117
7127
  var callable = manifest.filter(function(e) {
7118
7128
  return !SKIPPED_VERBS.has(e.verb);
7119
7129
  });
@@ -7145,38 +7155,55 @@ var DATA_HELP_FLAG = '--data-help';
7145
7155
  }
7146
7156
  }
7147
7157
  }
7158
+ if (byModel.size === 0) {
7159
+ return [];
7160
+ }
7148
7161
  var argv = (_ref = options === null || options === void 0 ? void 0 : options.argv) !== null && _ref !== void 0 ? _ref : process.argv;
7149
7162
  var dataHelpFormat = (_ref1 = options === null || options === void 0 ? void 0 : options.dataHelpFormat) !== null && _ref1 !== void 0 ? _ref1 : detectDataHelpFormat(argv);
7150
7163
  var focusHelp = ((_ref2 = options === null || options === void 0 ? void 0 : options.focusHelpOnDataHelp) !== null && _ref2 !== void 0 ? _ref2 : true) && hasDataHelpFlag(argv) && !hasAllHelpFlag(argv);
7151
7164
  var hideOnFocus = focusHelp ? (_ref3 = options === null || options === void 0 ? void 0 : options.hiddenWhenFocused) !== null && _ref3 !== void 0 ? _ref3 : STANDARD_GLOBAL_OPTION_NAMES : [];
7152
- var commands = [];
7153
- var _iteratorNormalCompletion1 = true, _didIteratorError1 = false, _iteratorError1 = undefined;
7154
- try {
7155
- for(var _iterator1 = _to_consumable_array(byModel.entries()).sort(function(param, param1) {
7156
- var _param = _sliced_to_array(param, 1), a = _param[0], _param1 = _sliced_to_array(param1, 1), b = _param1[0];
7157
- return a.localeCompare(b);
7158
- })[Symbol.iterator](), _step1; !(_iteratorNormalCompletion1 = (_step1 = _iterator1.next()).done); _iteratorNormalCompletion1 = true){
7159
- var _step_value = _sliced_to_array(_step1.value, 2), model = _step_value[0], entries = _step_value[1];
7160
- commands.push(buildModelCommand(model, entries, {
7161
- dataHelpFormat: dataHelpFormat,
7162
- hideOnFocus: hideOnFocus
7163
- }));
7164
- }
7165
- } catch (err) {
7166
- _didIteratorError1 = true;
7167
- _iteratorError1 = err;
7168
- } finally{
7169
- try {
7170
- if (!_iteratorNormalCompletion1 && _iterator1.return != null) {
7171
- _iterator1.return();
7172
- }
7173
- } finally{
7174
- if (_didIteratorError1) {
7175
- throw _iteratorError1;
7165
+ var modelCommandName = (_ref4 = options === null || options === void 0 ? void 0 : options.modelCommandName) !== null && _ref4 !== void 0 ? _ref4 : DEFAULT_MANIFEST_MODEL_COMMAND_NAME;
7166
+ var sortedModels = _to_consumable_array(byModel.entries()).sort(function(param, param1) {
7167
+ var _param = _sliced_to_array(param, 1), a = _param[0], _param1 = _sliced_to_array(param1, 1), b = _param1[0];
7168
+ return a.localeCompare(b);
7169
+ });
7170
+ var context = {
7171
+ dataHelpFormat: dataHelpFormat,
7172
+ hideOnFocus: hideOnFocus
7173
+ };
7174
+ return [
7175
+ {
7176
+ command: "".concat(modelCommandName, " <model>"),
7177
+ describe: "Call typed model APIs (".concat(byModel.size, " model").concat(byModel.size === 1 ? '' : 's', "). Use `").concat(modelCommandName, " --help` to list them."),
7178
+ builder: function builder(yargs) {
7179
+ var _iteratorNormalCompletion = true, _didIteratorError = false, _iteratorError = undefined;
7180
+ try {
7181
+ for(var _iterator = sortedModels[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true){
7182
+ var _step_value = _sliced_to_array(_step.value, 2), model = _step_value[0], entries = _step_value[1];
7183
+ yargs.command(buildModelCommand(model, entries, context));
7184
+ }
7185
+ } catch (err) {
7186
+ _didIteratorError = true;
7187
+ _iteratorError = err;
7188
+ } finally{
7189
+ try {
7190
+ if (!_iteratorNormalCompletion && _iterator.return != null) {
7191
+ _iterator.return();
7192
+ }
7193
+ } finally{
7194
+ if (_didIteratorError) {
7195
+ throw _iteratorError;
7196
+ }
7197
+ }
7198
+ }
7199
+ hideGlobalOptions(yargs, hideOnFocus);
7200
+ return yargs.demandCommand(1, 'Please specify a model.');
7201
+ },
7202
+ handler: function handler() {
7203
+ return undefined;
7176
7204
  }
7177
7205
  }
7178
- }
7179
- return commands;
7206
+ ];
7180
7207
  }
7181
7208
  /**
7182
7209
  * Returns true if `--data-help` (with or without a value) appears anywhere in
@@ -8081,4 +8108,4 @@ function printPaginatedOutput(input) {
8081
8108
  })();
8082
8109
  }
8083
8110
 
8084
- export { CALL_MODEL_API_PATH, CliError, DEFAULT_CLI_OIDC_SCOPES, DEFAULT_CLI_SECRET_PATTERNS, DEFAULT_MANIFEST_HELP_DATA_FORMAT, DUMP_MERGE_MODES, DUMP_OUTPUT_MODES, MODEL_WRITE_OIDC_SCOPES, MULTIPLE_PAGES_OUTPUT_MODES, PROMPT_CANCELLED_ERROR_CODE, STANDARD_GLOBAL_OPTION_NAMES, applyEnvVarOverrides, buildAuthorizationUrl, buildCliPaths, buildDumpFilePath, buildErrorOutput, buildManifestCommands, callModelOverHttp, callPassthroughCommand, configureCliErrorMapper, configureCliSecretPatterns, configureOutputOptions, createAuthCommand, createAuthMiddleware, createCallModelCommand, createCli, createCliContext, createCliTokenCacheStore, createContextSlot, createDoctorCommand, createEnvCommand, createOutputCommand, createOutputMiddleware, defaultDoctorChecks, detectDataHelpFormat, discoverOidcMetadata, dumpTimestamp, exchangeAuthorizationCode, fetchUserInfo, filterReadOnlyModelScopes, findCliEnvDefault, generateOAuthState, generatePkceMaterial, getCliContext, getOutputOptions, isCliEnvConfigComplete, isTokenExpired, loadCliConfig, maskSecret, mergeCliConfig, mergeCliEnvWithDefault, mergeOutputConfig, openStreamingDump, outputError, outputResult, parsePastedRedirect, pickFields, promptLine, refreshAccessToken, requireCliContext, resolveActiveEnvName, resolveOutputConfig, revokeToken, runCli, runPaginatedList, sanitizeString, saveCliConfig, setCliContext, withCallModelArgs, withEnv, withMultiplePages, withOutput, wrapCommandHandler };
8111
+ export { CALL_MODEL_API_PATH, CliError, DEFAULT_CLI_OIDC_SCOPES, DEFAULT_CLI_SECRET_PATTERNS, DEFAULT_MANIFEST_HELP_DATA_FORMAT, DEFAULT_MANIFEST_MODEL_COMMAND_NAME, DUMP_MERGE_MODES, DUMP_OUTPUT_MODES, MODEL_WRITE_OIDC_SCOPES, MULTIPLE_PAGES_OUTPUT_MODES, PROMPT_CANCELLED_ERROR_CODE, STANDARD_GLOBAL_OPTION_NAMES, applyEnvVarOverrides, buildAuthorizationUrl, buildCliPaths, buildDumpFilePath, buildErrorOutput, buildManifestCommands, callModelOverHttp, callPassthroughCommand, configureCliErrorMapper, configureCliSecretPatterns, configureOutputOptions, createAuthCommand, createAuthMiddleware, createCallModelCommand, createCli, createCliContext, createCliTokenCacheStore, createContextSlot, createDoctorCommand, createEnvCommand, createOutputCommand, createOutputMiddleware, defaultDoctorChecks, detectDataHelpFormat, discoverOidcMetadata, dumpTimestamp, exchangeAuthorizationCode, fetchUserInfo, filterReadOnlyModelScopes, findCliEnvDefault, generateOAuthState, generatePkceMaterial, getCliContext, getOutputOptions, isCliEnvConfigComplete, isTokenExpired, loadCliConfig, maskSecret, mergeCliConfig, mergeCliEnvWithDefault, mergeOutputConfig, openStreamingDump, outputError, outputResult, parsePastedRedirect, pickFields, promptLine, refreshAccessToken, requireCliContext, resolveActiveEnvName, resolveOutputConfig, revokeToken, runCli, runPaginatedList, sanitizeString, saveCliConfig, setCliContext, withCallModelArgs, withEnv, withMultiplePages, withOutput, wrapCommandHandler };
package/package.json CHANGED
@@ -1,18 +1,14 @@
1
1
  {
2
2
  "name": "@dereekb/dbx-cli",
3
- "version": "13.11.0",
3
+ "version": "13.11.1",
4
4
  "sideEffects": false,
5
- "peerDependencies": {
6
- "@dereekb/firebase": "13.11.0",
7
- "@dereekb/nestjs": "13.11.0",
8
- "@dereekb/util": "13.11.0",
9
- "arktype": "^2.2.0",
10
- "yargs": "^18.0.0"
11
- },
12
- "devDependencies": {
13
- "@types/yargs": "^17.0.35"
5
+ "bin": {
6
+ "dbx-cli-generate-firebase-api-manifest": "firebase-api-manifest/main.js"
14
7
  },
15
8
  "exports": {
9
+ "./firebase-api-manifest": {
10
+ "default": "./firebase-api-manifest/main.js"
11
+ },
16
12
  "./package.json": "./package.json",
17
13
  ".": {
18
14
  "module": "./index.esm.js",
@@ -21,6 +17,16 @@
21
17
  "default": "./index.cjs.js"
22
18
  }
23
19
  },
20
+ "peerDependencies": {
21
+ "@dereekb/firebase": "13.11.1",
22
+ "@dereekb/nestjs": "13.11.1",
23
+ "@dereekb/util": "13.11.1",
24
+ "arktype": "^2.2.0",
25
+ "yargs": "^18.0.0"
26
+ },
27
+ "devDependencies": {
28
+ "@types/yargs": "^17.0.35"
29
+ },
24
30
  "module": "./index.esm.js",
25
31
  "main": "./index.cjs.js",
26
32
  "types": "./index.d.ts"
@@ -48,19 +48,36 @@ export interface BuildManifestCommandsOptions {
48
48
  * {@link createCli}).
49
49
  */
50
50
  readonly hiddenWhenFocused?: readonly string[];
51
+ /**
52
+ * Name of the parent command that groups all per-model dispatch subcommands.
53
+ * Defaults to {@link DEFAULT_MANIFEST_MODEL_COMMAND_NAME} (`model`), so the
54
+ * full invocation reads `<cli> model <model> <action>`.
55
+ */
56
+ readonly modelCommandName?: string;
51
57
  }
58
+ /**
59
+ * Default name of the parent command that groups all per-model manifest commands.
60
+ * Surfaces as `<cli> model <model> <action>` so the top-level `--help` stays focused
61
+ * on first-class commands instead of dumping every model.
62
+ */
63
+ export declare const DEFAULT_MANIFEST_MODEL_COMMAND_NAME = "model";
52
64
  /**
53
65
  * Builds yargs `CommandModule[]` from a generated {@link CliApiManifest}.
54
66
  *
55
- * Groups entries by `model`, emitting one parent command per model and one child action per entry
56
- * (named `<verb>` or `<verb>-<specifier>`). Each child accepts `--data <json>`, validates the
67
+ * Returns a single parent command (default name `model`) whose subcommands are the per-model
68
+ * dispatch commands. Each per-model subcommand has one child action per entry
69
+ * (named `<verb>` or `<verb>-<specifier>`). Each leaf accepts `--data <json>`, validates the
57
70
  * payload against the entry's bound arktype validator (when present), and dispatches via the
58
71
  * authenticated CLI context's `callModel` helper. Standalone entries are skipped because they
59
72
  * are not dispatched through the `/model/call` endpoint.
60
73
  *
74
+ * Wrapping under `model` keeps the top-level `--help` short — users invoke
75
+ * `<cli> model --help` to see the full list of available models.
76
+ *
61
77
  * @param manifest - The generated manifest array.
62
78
  * @param options - Optional overrides; see {@link BuildManifestCommandsOptions}.
63
- * @returns The yargs `CommandModule[]` ready to be passed to `runCli({ apiCommands })`.
79
+ * @returns The yargs `CommandModule[]` ready to be passed to `runCli({ apiCommands })`. Empty
80
+ * when the manifest has no callable entries.
64
81
  */
65
82
  export declare function buildManifestCommands(manifest: CliApiManifest, options?: BuildManifestCommandsOptions): CommandModule[];
66
83
  /**
@@ -20,6 +20,9 @@ export interface CreateCliInput {
20
20
  * App-specific API commands appended after the built-in `call` passthrough.
21
21
  *
22
22
  * Commands listed here run after the auth middleware, so they have access to the {@link CliContext}.
23
+ *
24
+ * For manifest-driven typed model commands, pass the result of `buildManifestCommands(manifest)` —
25
+ * it returns a single parent `model <model>` command so the top-level `--help` stays focused.
23
26
  */
24
27
  readonly apiCommands?: CommandModule[];
25
28
  /**
@@ -1,44 +0,0 @@
1
- /**
2
- * For a given Params type name (e.g. `SetProfileUsernameParams`), derives the
3
- * canonical arktype validator identifier (`setProfileUsernameParamsType`) by
4
- * naming convention and confirms it is exported from the resolved package.
5
- *
6
- * Verification is best-effort: we string-search the package's `src/index.ts`
7
- * and follow `export * from './...';` re-exports recursively until we find
8
- * the declaration of the validator. The convention everywhere in the codebase
9
- * is `export const <name>ParamsType = ... as Type<<Name>Params>`.
10
- */
11
- /**
12
- * Derives the canonical arktype validator identifier from a Params type name.
13
- *
14
- * `SetProfileUsernameParams` → `setProfileUsernameParamsType`.
15
- *
16
- * @param paramsTypeName - PascalCase Params type identifier.
17
- * @returns The lowerCamelCase validator identifier (or empty string for empty input).
18
- */
19
- export declare function deriveValidatorName(paramsTypeName: string): string;
20
- /**
21
- * Inputs for {@link isExportedFromPackage}.
22
- */
23
- export interface IsExportedInput {
24
- readonly packageRoot: string;
25
- readonly identifier: string;
26
- }
27
- /**
28
- * Confirms an identifier is exported from `packageRoot/src/index.ts` —
29
- * directly or via re-export chains.
30
- *
31
- * @param input - Package root + identifier to look up.
32
- * @returns `true` when the identifier is reachable from the barrel.
33
- */
34
- export declare function isExportedFromPackage(input: IsExportedInput): boolean;
35
- /**
36
- * Walks the `src/lib` tree under a package and returns the absolute file
37
- * paths of every `.ts` file (used as a fallback when index-chain lookup
38
- * misses the identifier — some packages rely on flat barrels that don't
39
- * `export *`).
40
- *
41
- * @param packageRoot - Absolute path to the source package's root directory.
42
- * @returns Absolute paths of every non-spec `.ts` file under `src`.
43
- */
44
- export declare function listPackageTsFiles(packageRoot: string): string[];
@@ -1,29 +0,0 @@
1
- /**
2
- * Renders the final manifest TS module — banner + grouped imports + the
3
- * `<NAMESPACE>` array literal. Formatted with the workspace prettier config
4
- * so the output matches what `prettier --write` would produce on the
5
- * committed file.
6
- *
7
- * The reusable `CliApiManifest` type is imported from `@dereekb/dbx-cli` so
8
- * any consuming app gets it from the shared dbx-cli barrel.
9
- */
10
- import type { CollectedEntry } from './types';
11
- /**
12
- * Inputs for {@link renderManifest}.
13
- */
14
- export interface RenderManifestInput {
15
- readonly outputFile: string;
16
- readonly entries: readonly CollectedEntry[];
17
- readonly projectName: string;
18
- readonly namespace: string;
19
- }
20
- /**
21
- * Renders the manifest TS source for a CLI app and formats it with the
22
- * workspace prettier config so the output matches a `prettier --write` of the
23
- * committed file.
24
- *
25
- * @param input - Output file path, collected entries, project name (banner),
26
- * and the manifest namespace identifier.
27
- * @returns Prettier-formatted TypeScript source.
28
- */
29
- export declare function renderManifest(input: RenderManifestInput): Promise<string>;
@@ -1,25 +0,0 @@
1
- /**
2
- * Walks `<Group>ModelCrudFunctionsConfig` and `<Group>FunctionTypeMap` type
3
- * aliases in a `<model>.api.ts` source. Returns one entry per callable leaf,
4
- * keyed by (model, verb, specifier).
5
- *
6
- * Mirrors `extractCrudEntries` from
7
- * packages/dbx-components-mcp/src/tools/model-api-shared/extract-crud.ts —
8
- * keep the two in lockstep when the .api.ts convention changes.
9
- */
10
- import type { CrudExtraction } from './types';
11
- /**
12
- * Inputs for {@link extractCrudEntries}.
13
- */
14
- export interface ExtractCrudInput {
15
- readonly name: string;
16
- readonly text: string;
17
- }
18
- /**
19
- * Extracts CRUD + standalone entries from a `.api.ts` source by walking its
20
- * `<Group>ModelCrudFunctionsConfig` and `<Group>FunctionTypeMap` aliases.
21
- *
22
- * @param source - The source file's name + text.
23
- * @returns The extracted entries, group name, model keys, and `*Functions` class name.
24
- */
25
- export declare function extractCrudEntries(source: ExtractCrudInput): CrudExtraction;
@@ -1,16 +0,0 @@
1
- /**
2
- * Walks a package's `src/lib/**\/*.api.ts` and returns the files that declare
3
- * an abstract `*Functions` class along with the class name. Used to map a
4
- * class identifier from `<APP>_FIREBASE_FUNCTIONS_CONFIG` back to the source
5
- * `*.api.ts`.
6
- */
7
- import type { ApiFileMatch } from './types';
8
- /**
9
- * Walks `packageRoot/src/lib/**\/*.api.ts` and returns the files that declare
10
- * an abstract `*Functions` class along with the class name and the
11
- * extracted CRUD entries.
12
- *
13
- * @param packageRoot - Absolute path to the source package's root directory.
14
- * @returns One {@link ApiFileMatch} per qualifying `.api.ts`.
15
- */
16
- export declare function findApiFiles(packageRoot: string): ApiFileMatch[];
@@ -1,46 +0,0 @@
1
- /**
2
- * Generates an API manifest TS file for any dbx-components CLI app.
3
- *
4
- * Pipeline (build-time, run via `nx run <cli>:generate-api-manifest`):
5
- *
6
- * 1. Parse the app's `<APP>_FIREBASE_FUNCTIONS_CONFIG` from the file passed
7
- * via --functions-config to enumerate the `*Functions` abstract classes
8
- * used by the app, including their source-module specifier
9
- * ("@dereekb/firebase", "demo-firebase", "./model", "./development",
10
- * ...). The leading-app prefix is matched generically by
11
- * `/FIREBASE_FUNCTIONS_CONFIG$/` so any app variable name works.
12
- *
13
- * 2. Resolve each module to a source-package root via the workspace's
14
- * tsconfig.base.json `paths`.
15
- *
16
- * 3. Walk that package's `src/lib/**\/*.api.ts`, run the CRUD-entry walker
17
- * on each, and pick the file whose abstract `*Functions` class name
18
- * matches the entry's class identifier.
19
- *
20
- * 4. For each CRUD entry with a `paramsTypeName`, derive the canonical
21
- * arktype validator name (lowercaseFirst + `Type`) and confirm it is
22
- * exported from the package's barrel chain. Warn on miss; the entry is
23
- * still emitted with `paramsValidator: undefined`. `--strict` makes a
24
- * miss fatal.
25
- *
26
- * 5. Emit the manifest to the path passed via --output with grouped
27
- * per-package imports + the `<NAMESPACE>_API_MANIFEST` array literal.
28
- * Skip the write if the file content is byte-identical (preserves mtime
29
- * for incremental builds).
30
- *
31
- * Flags:
32
- * --functions-config=<path> (required) path to the app's functions.ts.
33
- * Absolute or workspace-relative.
34
- * --output=<path> (required) path to the manifest TS file to write.
35
- * Absolute or workspace-relative.
36
- * --project=<name> Project name shown in the regenerate banner
37
- * (defaults to "<cli>"). Also used to derive
38
- * the manifest namespace (e.g. "demo-cli" ->
39
- * DEMO_CLI_API_MANIFEST).
40
- * --only=<model[,model]> Filter the emitted entries to those models.
41
- * --strict Exit 1 if any validator is missing.
42
- *
43
- * Run from any cwd; workspace-relative paths resolve against `process.cwd()`
44
- * (Nx invokes with cwd: "{workspaceRoot}").
45
- */
46
- export {};
@@ -1,18 +0,0 @@
1
- /**
2
- * Parses the `<APP>_FIREBASE_FUNCTIONS_CONFIG` variable from a CLI app's
3
- * `functions.ts` and returns the (groupKey, className, importedFromModule)
4
- * tuple for every entry.
5
- *
6
- * We follow the import declaration of the abstract-class identifier to know
7
- * which package the `*.api.ts` lives in. The variable name is matched
8
- * generically by `/FIREBASE_FUNCTIONS_CONFIG$/` so any app-prefix works.
9
- */
10
- import type { FunctionsGroup } from './types';
11
- /**
12
- * Parses the `<APP>_FIREBASE_FUNCTIONS_CONFIG` literal at `functionsTsPath`
13
- * into one entry per group declared in the config object.
14
- *
15
- * @param functionsTsPath - Absolute path to the app's `functions.ts`.
16
- * @returns One {@link FunctionsGroup} per `<groupKey>: [<Class>, ...]` pair.
17
- */
18
- export declare function parseFunctionsConfig(functionsTsPath: string): FunctionsGroup[];
@@ -1,59 +0,0 @@
1
- /**
2
- * Resolves a workspace module specifier (e.g. "@dereekb/firebase",
3
- * "demo-firebase", "./model", "./development") to a source-package root and
4
- * canonical import name.
5
- *
6
- * Reads the workspace tsconfig.base.json `compilerOptions.paths` to map bare
7
- * specifiers to their `src/index.ts` entry. Relative specifiers are resolved
8
- * against the importing file's directory and then walked back up to the
9
- * nearest `package.json` that owns them.
10
- */
11
- import type { PackageRef } from './types';
12
- /**
13
- * Inputs accepted by {@link resolveModuleToPackage}.
14
- */
15
- export interface ResolveModuleInput {
16
- readonly workspaceRoot: string;
17
- readonly importingFile: string;
18
- readonly moduleSpecifier: string;
19
- }
20
- /**
21
- * Reads `tsconfig.base.json` and returns the `compilerOptions.paths` map
22
- * (canonical bare specifier → absolute path to its `src/index.ts`).
23
- *
24
- * @param workspaceRoot - Workspace root directory.
25
- * @returns Cached map of canonical specifiers to absolute index paths.
26
- */
27
- export declare function loadTsconfigPaths(workspaceRoot: string): Map<string, string>;
28
- /**
29
- * Resolves a module specifier to the source-package that owns it.
30
- *
31
- * @param input - Workspace root + importing-file location + the specifier.
32
- * @returns The {@link PackageRef} of the owning package, or `undefined` when unresolved.
33
- */
34
- export declare function resolveModuleToPackage(input: ResolveModuleInput): PackageRef | undefined;
35
- /**
36
- * Walks up from `startPath` until it finds a directory containing a `package.json`
37
- * with a `name` field, then returns its package name + root.
38
- *
39
- * @param workspaceRoot - Stop walking when this directory is reached.
40
- * @param startPath - Path to walk up from.
41
- * @returns The package name + root, or `undefined` if no package.json is found.
42
- */
43
- export declare function locatePackageForPath(workspaceRoot: string, startPath: string): PackageRef | undefined;
44
- /**
45
- * Returns the workspace-root-relative path with forward slashes for display.
46
- *
47
- * @param workspaceRoot - Workspace root directory.
48
- * @param absolutePath - Absolute path to make relative.
49
- * @returns The workspace-relative path with forward slashes.
50
- */
51
- export declare function relPath(workspaceRoot: string, absolutePath: string): string;
52
- /**
53
- * Type-narrowed `isAbsolute` re-export so the generator scripts only depend on
54
- * `node:path` indirectly through this module.
55
- *
56
- * @param value - Path to test.
57
- * @returns `true` when the path is absolute.
58
- */
59
- export declare function isAbsolutePathLike(value: string): boolean;
@@ -1,44 +0,0 @@
1
- /**
2
- * Internal types shared between the generator's pipeline stages.
3
- *
4
- * The runtime manifest types (`CliApiManifest`, `CliApiManifestEntry`,
5
- * `CliApiVerb`) live in `packages/dbx-cli/src/lib/manifest/types.ts` and are
6
- * re-exported from `@dereekb/dbx-cli`. The generator emits TypeScript that
7
- * imports those runtime types — it does not reference them itself.
8
- */
9
- export interface FunctionsGroup {
10
- readonly groupKey: string;
11
- readonly className: string;
12
- readonly importedFromModule: string;
13
- }
14
- export interface PackageRef {
15
- readonly packageName: string;
16
- readonly packageRoot: string;
17
- }
18
- export interface CrudEntry {
19
- readonly model: string;
20
- readonly verb: string;
21
- readonly specifier?: string;
22
- readonly paramsTypeName?: string;
23
- readonly resultTypeName?: string;
24
- readonly line: number;
25
- }
26
- export interface CrudExtraction {
27
- readonly groupName: string | undefined;
28
- readonly modelKeys: readonly string[];
29
- readonly entries: readonly CrudEntry[];
30
- readonly functionsClassName?: string;
31
- }
32
- export interface ApiFileMatch {
33
- readonly filePath: string;
34
- readonly className: string;
35
- readonly extraction: CrudExtraction;
36
- }
37
- export interface CollectedEntry {
38
- readonly entry: CrudEntry & {
39
- readonly groupName: string;
40
- readonly sourceFile: string;
41
- };
42
- readonly packageName?: string;
43
- readonly validatorName?: string;
44
- }