@forwardimpact/libcodegen 0.1.43 → 0.1.45

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,13 @@
1
+ # libcodegen
2
+
3
+ Protocol Buffer code generation utilities.
4
+
5
+ ## Getting Started
6
+
7
+ ```sh
8
+ npx fit-codegen --all
9
+ ```
10
+
11
+ ```js
12
+ import { CodegenTypes, CodegenServices, CodegenDefinitions } from '@forwardimpact/libcodegen';
13
+ ```
@@ -16,6 +16,7 @@ import {
16
16
  CodegenTypes,
17
17
  CodegenServices,
18
18
  CodegenDefinitions,
19
+ CodegenMetadata,
19
20
  } from "@forwardimpact/libcodegen";
20
21
  import { createStorage } from "@forwardimpact/libstorage";
21
22
 
@@ -39,6 +40,10 @@ const definition = {
39
40
  type: "boolean",
40
41
  description: "Generate service definitions only",
41
42
  },
43
+ metadata: {
44
+ type: "boolean",
45
+ description: "Generate field metadata only",
46
+ },
42
47
  help: { type: "boolean", short: "h", description: "Show this help" },
43
48
  version: { type: "boolean", description: "Show version" },
44
49
  json: { type: "boolean", description: "Output help as JSON" },
@@ -102,9 +107,14 @@ function parseFlags() {
102
107
  doServices: doAll || values.service,
103
108
  doClients: doAll || values.client,
104
109
  doDefinitions: doAll || values.definition,
110
+ doMetadata: doAll || values.metadata,
105
111
  hasGenerationFlags() {
106
112
  return (
107
- this.doTypes || this.doServices || this.doClients || this.doDefinitions
113
+ this.doTypes ||
114
+ this.doServices ||
115
+ this.doClients ||
116
+ this.doDefinitions ||
117
+ this.doMetadata
108
118
  );
109
119
  },
110
120
  };
@@ -174,6 +184,7 @@ function createCodegen(
174
184
  types: new CodegenTypes(base),
175
185
  services: new CodegenServices(base),
176
186
  definitions: new CodegenDefinitions(base),
187
+ metadata: new CodegenMetadata(base),
177
188
  };
178
189
  }
179
190
 
@@ -237,6 +248,7 @@ function printSummary(sourcePath, flags) {
237
248
  flags.doServices && "services",
238
249
  flags.doClients && "clients",
239
250
  flags.doDefinitions && "definitions",
251
+ flags.doMetadata && "metadata",
240
252
  ].filter(Boolean);
241
253
  process.stdout.write(
242
254
  `\nCode generation complete (${generated.join(", ")}).\n`,
@@ -265,6 +277,9 @@ async function executeGeneration(codegens, sourcePath, flags) {
265
277
  if (flags.doDefinitions) {
266
278
  tasks.push(codegens.definitions.run(sourcePath));
267
279
  }
280
+ if (flags.doMetadata) {
281
+ tasks.push(codegens.metadata.run(sourcePath));
282
+ }
268
283
 
269
284
  await Promise.all(tasks);
270
285
 
@@ -294,7 +309,7 @@ async function runCodegen(protoDirs, projectRoot, finder) {
294
309
 
295
310
  if (!parsedFlags.hasGenerationFlags()) {
296
311
  cli.usageError(
297
- "no generation flags specified (use --all, --type, --service, --client, or --definition)",
312
+ "no generation flags specified (use --all, --type, --service, --client, --definition, or --metadata)",
298
313
  );
299
314
  process.exit(2);
300
315
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forwardimpact/libcodegen",
3
- "version": "0.1.43",
3
+ "version": "0.1.45",
4
4
  "description": "Protocol Buffer code generation utilities for Guide",
5
5
  "license": "Apache-2.0",
6
6
  "author": "D. Olsson <hi@senzilla.io>",
package/src/base.js CHANGED
@@ -1,5 +1,31 @@
1
1
  import { execFile } from "node:child_process";
2
2
  import { fileURLToPath } from "node:url";
3
+ import protobuf from "protobufjs";
4
+
5
+ /** Convert camelCase to snake_case (protobufjs normalizes field names) */
6
+ function camelToSnake(str) {
7
+ return str.replace(/[A-Z]/g, (ch) => `_${ch.toLowerCase()}`);
8
+ }
9
+
10
+ // Protobufjs scalar type names are already human-readable (string, int32, bool, etc.)
11
+ // so most pass through as-is. This map handles any aliases needed.
12
+ const PBJS_TYPE_MAP = {
13
+ string: "string",
14
+ int32: "int32",
15
+ int64: "int64",
16
+ uint32: "uint32",
17
+ uint64: "uint64",
18
+ sint32: "sint32",
19
+ sint64: "sint64",
20
+ fixed32: "fixed32",
21
+ fixed64: "fixed64",
22
+ sfixed32: "sfixed32",
23
+ sfixed64: "sfixed64",
24
+ float: "float",
25
+ double: "double",
26
+ bool: "bool",
27
+ bytes: "bytes",
28
+ };
3
29
 
4
30
  /**
5
31
  * Base class for code generation utilities providing shared functionality
@@ -242,6 +268,75 @@ export class CodegenBase {
242
268
  };
243
269
  }
244
270
 
271
+ /**
272
+ * Parse metadata for a proto file: service methods with request type fields.
273
+ * Uses protobufjs for field types, optionality, and comments.
274
+ * @param {string} protoPath - Absolute path to .proto file
275
+ * @returns {{packageName:string, serviceName:string, methods:object}|null}
276
+ */
277
+ parseMetadata(protoPath) {
278
+ const parsed = this.parseProtoFile(protoPath);
279
+ if (!parsed) return null;
280
+
281
+ const { packageName, serviceName, methods: parsedMethods } = parsed;
282
+
283
+ // Load with protobufjs for field types, optionality, and comments
284
+ const root = new protobuf.Root();
285
+ root.resolvePath = (origin, target) => {
286
+ for (const dir of this.includeDirs) {
287
+ const candidate = this.#path.join(dir, target);
288
+ if (this.#fs.existsSync(candidate)) return candidate;
289
+ }
290
+ return target;
291
+ };
292
+ root.loadSync(protoPath, { alternateCommentMode: true });
293
+ root.resolveAll();
294
+
295
+ const methods = {};
296
+ for (const method of parsedMethods) {
297
+ const reqTypeName = `${method.requestTypeNamespace}.${method.requestType}`;
298
+ let pbjsType = null;
299
+ try {
300
+ pbjsType = root.lookupType(reqTypeName);
301
+ } catch {
302
+ try {
303
+ pbjsType = root.lookupType(method.requestType);
304
+ } catch {
305
+ // no type found
306
+ }
307
+ }
308
+
309
+ const fields = {};
310
+ if (pbjsType) {
311
+ for (const [camelName, field] of Object.entries(pbjsType.fields)) {
312
+ const name = camelToSnake(camelName);
313
+ // Map protobufjs type to simple type name
314
+ const typeName = field.resolvedType
315
+ ? "message"
316
+ : PBJS_TYPE_MAP[field.type] || field.type;
317
+
318
+ // proto3 optional uses synthetic oneofs named _fieldname
319
+ const hasOptionalKeyword =
320
+ pbjsType.oneofs?.[`_${camelName}`] !== undefined;
321
+
322
+ fields[name] = {
323
+ type: typeName,
324
+ optional: hasOptionalKeyword,
325
+ repeated: field.repeated || false,
326
+ description: field.comment || null,
327
+ };
328
+ }
329
+ }
330
+
331
+ methods[method.name] = {
332
+ requestType: `${method.requestTypeNamespace}.${method.requestType}`,
333
+ fields,
334
+ };
335
+ }
336
+
337
+ return { packageName, serviceName, methods };
338
+ }
339
+
245
340
  /**
246
341
  * Find the namespace for a given type by comparing structure
247
342
  * @param {object} typeToFind - The type definition to find namespace for
package/src/index.js CHANGED
@@ -3,3 +3,4 @@ export { CodegenBase } from "./base.js";
3
3
  export { CodegenTypes } from "./types.js";
4
4
  export { CodegenServices } from "./services.js";
5
5
  export { CodegenDefinitions } from "./definitions.js";
6
+ export { CodegenMetadata } from "./metadata.js";
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Generates field metadata for all service methods from proto files.
3
+ * Output is a static ESM module consumed by libmcp for schema generation.
4
+ */
5
+ export class CodegenMetadata {
6
+ #base;
7
+
8
+ /**
9
+ * @param {object} base - CodegenBase instance providing shared utilities
10
+ */
11
+ constructor(base) {
12
+ if (!base) throw new Error("CodegenBase instance is required");
13
+ this.#base = base;
14
+ }
15
+
16
+ /**
17
+ * Generate metadata.js containing field descriptors for all service methods.
18
+ * @param {string} generatedPath - Path to generated code directory
19
+ * @returns {Promise<void>}
20
+ */
21
+ async run(generatedPath) {
22
+ if (!generatedPath) throw new Error("generatedPath is required");
23
+
24
+ const protoFiles = this.#base
25
+ .collectProtoFiles({ includeTools: false })
26
+ .filter((file) => !file.endsWith(this.#base.path.sep + "common.proto"));
27
+
28
+ const metadata = {};
29
+
30
+ for (const protoFile of protoFiles) {
31
+ const parsed = this.#base.parseMetadata(protoFile);
32
+ if (!parsed) continue;
33
+
34
+ const serviceKey = `${parsed.packageName}.${parsed.serviceName}`;
35
+ metadata[serviceKey] = parsed.methods;
36
+ }
37
+
38
+ const typesDir = this.#base.path.join(generatedPath, "types");
39
+ this.#base.fs.mkdirSync(typesDir, { recursive: true });
40
+
41
+ const content =
42
+ `/** @generated by fit-codegen — do not edit */\n` +
43
+ `export const metadata = ${JSON.stringify(metadata, null, 2)};\n`;
44
+
45
+ this.#base.fs.writeFileSync(
46
+ this.#base.path.join(typesDir, "metadata.js"),
47
+ content,
48
+ );
49
+ }
50
+ }