From 8897ea07095e8c2d3018a01c87cdf2725e0fd715 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 18 Mar 2026 19:53:21 +0000 Subject: [PATCH 01/12] fix(cli): parallelize protobuf buf generate passes Co-Authored-By: Niels Swimberghe <3382717+Swimburger@users.noreply.github.com> --- packages/cli/cli/versions.yml | 12 +++ .../lazy-fern-workspace/src/OSSWorkspace.ts | 99 ++++++++++--------- .../src/utils/getAllOpenAPISpecs.ts | 56 ++++++++--- 3 files changed, 105 insertions(+), 62 deletions(-) diff --git a/packages/cli/cli/versions.yml b/packages/cli/cli/versions.yml index c0ea9f68c3c6..86470f9b9dcd 100644 --- a/packages/cli/cli/versions.yml +++ b/packages/cli/cli/versions.yml @@ -1,4 +1,16 @@ # yaml-language-server: $schema=../../../fern-versions-yml.schema.json +- version: 4.34.2 + changelogEntry: + - summary: | + Parallelize protobuf generation passes. The two `buf generate` + invocations (protoc-gen-openapi and protoc-gen-fern) now run + concurrently instead of serially, and per-file OpenAPI generation + within a single protobuf spec is also parallelized after the first + file resolves dependencies. + type: fix + createdAt: "2026-03-18" + irVersion: 65 + - version: 4.34.1 changelogEntry: - summary: | diff --git a/packages/cli/workspace/lazy-fern-workspace/src/OSSWorkspace.ts b/packages/cli/workspace/lazy-fern-workspace/src/OSSWorkspace.ts index b5fe2d51d3e0..f9c6da8b2357 100644 --- a/packages/cli/workspace/lazy-fern-workspace/src/OSSWorkspace.ts +++ b/packages/cli/workspace/lazy-fern-workspace/src/OSSWorkspace.ts @@ -30,7 +30,6 @@ import { v4 as uuidv4 } from "uuid"; import { loadOpenRpc } from "./loaders/index.js"; import { OpenAPILoader } from "./loaders/OpenAPILoader.js"; import { ProtobufIRGenerator } from "./protobuf/ProtobufIRGenerator.js"; -import { MaybeValid } from "./protobuf/utils.js"; import { getAllOpenAPISpecs } from "./utils/getAllOpenAPISpecs.js"; export declare namespace OSSWorkspace { @@ -259,6 +258,9 @@ export class OSSWorkspace extends BaseOpenAPIWorkspace { generateV1Examples: boolean; logWarnings: boolean; }): Promise { + // Start protobuf IR generation in parallel with OpenAPI processing + const protobufIRResultsPromise = this.generateAllProtobufIRs({ context }); + const specs = await getAllOpenAPISpecs({ context, specs: this.specs }); const documents = await this.loader.loadDocuments({ context, specs }); @@ -409,54 +411,21 @@ export class OSSWorkspace extends BaseOpenAPIWorkspace { ? result : mergeIntermediateRepresentation(mergedIr, result, casingsGenerator); } - } else if (spec.type === "protobuf") { - // Handle protobuf specs by calling buf generate with protoc-gen-fern - try { - const protobufIRGenerator = new ProtobufIRGenerator({ context }); - const protobufIRFilepath = await protobufIRGenerator.generate({ - absoluteFilepathToProtobufRoot: spec.absoluteFilepathToProtobufRoot, - absoluteFilepathToProtobufTarget: spec.absoluteFilepathToProtobufTarget, - local: true, - deps: spec.dependencies - }); - - const result = await readFile(protobufIRFilepath, "utf-8"); - - const casingsGenerator = constructCasingsGenerator({ - generationLanguage: "typescript", - keywords: undefined, - smartCasing: false - }); - - if (result != null) { - let serializedIr: MaybeValid; - try { - serializedIr = serialization.IntermediateRepresentation.parse(JSON.parse(result), { - allowUnrecognizedEnumValues: true, - skipValidation: true - }); - if (serializedIr.ok) { - mergedIr = - mergedIr === undefined - ? serializedIr.value - : mergeIntermediateRepresentation( - mergedIr, - serializedIr.value, - casingsGenerator - ); - } else { - throw new Error(); - } - } catch (error) { - context.logger.log("error", "Failed to parse protobuf IR: "); - } - } - } catch (error) { - context.logger.log("warn", "Failed to parse protobuf IR: " + error); - } } } + // Await and merge protobuf IR results (generated in parallel with OpenAPI processing) + const protobufIRResults = await protobufIRResultsPromise; + const protobufCasingsGenerator = constructCasingsGenerator({ + generationLanguage: "typescript", + keywords: undefined, + smartCasing: false + }); + for (const ir of protobufIRResults) { + mergedIr = + mergedIr === undefined ? ir : mergeIntermediateRepresentation(mergedIr, ir, protobufCasingsGenerator); + } + for (const errorCollector of errorCollectors) { if (errorCollector.hasErrors()) { const errorStats = errorCollector.getErrorStats(); @@ -486,6 +455,44 @@ export class OSSWorkspace extends BaseOpenAPIWorkspace { return mergedIr; } + private async generateAllProtobufIRs({ context }: { context: TaskContext }): Promise { + const protobufSpecs = this.allSpecs.filter((spec): spec is ProtobufSpec => spec.type === "protobuf"); + if (protobufSpecs.length === 0) { + return []; + } + + const results = await Promise.all( + protobufSpecs.map(async (spec) => { + try { + const protobufIRGenerator = new ProtobufIRGenerator({ context }); + const protobufIRFilepath = await protobufIRGenerator.generate({ + absoluteFilepathToProtobufRoot: spec.absoluteFilepathToProtobufRoot, + absoluteFilepathToProtobufTarget: spec.absoluteFilepathToProtobufTarget, + local: true, + deps: spec.dependencies + }); + + const result = await readFile(protobufIRFilepath, "utf-8"); + if (result != null) { + const serializedIr = serialization.IntermediateRepresentation.parse(JSON.parse(result), { + allowUnrecognizedEnumValues: true, + skipValidation: true + }); + if (serializedIr.ok) { + return serializedIr.value; + } + context.logger.log("error", "Failed to parse protobuf IR"); + } + } catch (error) { + context.logger.log("warn", "Failed to parse protobuf IR: " + error); + } + return undefined; + }) + ); + + return results.filter((ir): ir is IntermediateRepresentation => ir != null); + } + public async toFernWorkspace( { context }: { context: TaskContext }, settings?: OSSWorkspace.Settings, diff --git a/packages/cli/workspace/lazy-fern-workspace/src/utils/getAllOpenAPISpecs.ts b/packages/cli/workspace/lazy-fern-workspace/src/utils/getAllOpenAPISpecs.ts index 9da8d58388de..ed21049eb295 100644 --- a/packages/cli/workspace/lazy-fern-workspace/src/utils/getAllOpenAPISpecs.ts +++ b/packages/cli/workspace/lazy-fern-workspace/src/utils/getAllOpenAPISpecs.ts @@ -1,6 +1,6 @@ import { OpenAPISpec, ProtobufSpec, Spec } from "@fern-api/api-workspace-commons"; import { isNonNullish } from "@fern-api/core-utils"; -import { listFiles, RelativeFilePath } from "@fern-api/fs-utils"; +import { AbsoluteFilePath, listFiles, RelativeFilePath } from "@fern-api/fs-utils"; import { TaskContext } from "@fern-api/task-context"; import { ProtobufOpenAPIGenerator } from "../protobuf/ProtobufOpenAPIGenerator.js"; @@ -28,26 +28,50 @@ export async function getAllOpenAPISpecs({ return result ? [result.openApiSpec] : []; } const allProtobufTargetFilepaths = await listFiles(protobufSpec.absoluteFilepathToProtobufRoot, "proto"); - let accumulatedBufLockContents: string | undefined; + if (allProtobufTargetFilepaths.length === 0) { + return []; + } + + // Process first file to get buf.lock contents for caching + // Safe to index — length > 0 is checked above + const firstFile = allProtobufTargetFilepaths[0] as AbsoluteFilePath; + const firstResult = await convertProtobufToOpenAPI({ + generator, + protobufSpec: { + ...protobufSpec, + absoluteFilepathToProtobufTarget: firstFile + }, + relativePathToDependency + }); + + const bufLockContents = firstResult?.bufLockContents; const openApiSpecs: OpenAPISpec[] = []; + if (firstResult != null) { + openApiSpecs.push(firstResult.openApiSpec); + } - for (const file of allProtobufTargetFilepaths) { - const result = await convertProtobufToOpenAPI({ - generator, - protobufSpec: { - ...protobufSpec, - absoluteFilepathToProtobufTarget: file - }, - relativePathToDependency, - existingBufLockContents: accumulatedBufLockContents - }); - if (result != null) { - openApiSpecs.push(result.openApiSpec); - if (result.bufLockContents != null) { - accumulatedBufLockContents = result.bufLockContents; + // Parallelize remaining files with cached buf.lock contents + if (allProtobufTargetFilepaths.length > 1) { + const remainingResults = await Promise.all( + allProtobufTargetFilepaths.slice(1).map((file) => + convertProtobufToOpenAPI({ + generator, + protobufSpec: { + ...protobufSpec, + absoluteFilepathToProtobufTarget: file + }, + relativePathToDependency, + existingBufLockContents: bufLockContents + }) + ) + ); + for (const result of remainingResults) { + if (result != null) { + openApiSpecs.push(result.openApiSpec); } } } + return openApiSpecs; }) ); From d4346e00cfb0bf80e77738701072d00618450719 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 18 Mar 2026 20:42:08 +0000 Subject: [PATCH 02/12] fix(cli): optimize buf generate to reuse single working dir across proto files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Instead of creating a new temp dir, copying the proto root, running which checks, writing configs, and resolving deps for EVERY proto file, we now: 1. prepare() - does all setup once: temp dir, copy, which checks, buf.yaml, buf dep update 2. generateFromPrepared() - only runs 'buf generate ' per file For N proto files this eliminates (N-1) × expensive setup operations: - temp dir creation + recursive copy of proto root - 2 'which' subprocess calls (buf, protoc-gen-openapi) - buf.yaml + buf.gen.yaml file writes - buf dep update (network call to resolve dependencies) Co-Authored-By: Niels Swimberghe <3382717+Swimburger@users.noreply.github.com> --- .../src/protobuf/ProtobufOpenAPIGenerator.ts | 144 +++++++++++++++++- .../src/utils/getAllOpenAPISpecs.ts | 108 +++++++------ 2 files changed, 196 insertions(+), 56 deletions(-) diff --git a/packages/cli/workspace/lazy-fern-workspace/src/protobuf/ProtobufOpenAPIGenerator.ts b/packages/cli/workspace/lazy-fern-workspace/src/protobuf/ProtobufOpenAPIGenerator.ts index dec1d2079fa5..dd87718885e3 100644 --- a/packages/cli/workspace/lazy-fern-workspace/src/protobuf/ProtobufOpenAPIGenerator.ts +++ b/packages/cli/workspace/lazy-fern-workspace/src/protobuf/ProtobufOpenAPIGenerator.ts @@ -1,7 +1,7 @@ import { AbsoluteFilePath, join, RelativeFilePath, relative } from "@fern-api/fs-utils"; import { createLoggingExecutable } from "@fern-api/logging-execa"; import { TaskContext } from "@fern-api/task-context"; -import { access, cp, readFile, unlink, writeFile } from "fs/promises"; +import { access, cp, readFile, rename, unlink, writeFile } from "fs/promises"; import tmp from "tmp-promise"; import { detectAirGappedModeForProtobuf, getProtobufYamlV1 } from "./utils.js"; @@ -9,6 +9,15 @@ const PROTOBUF_GENERATOR_CONFIG_FILENAME = "buf.gen.yaml"; const PROTOBUF_GENERATOR_OUTPUT_PATH = "output"; const PROTOBUF_GENERATOR_OUTPUT_FILEPATH = `${PROTOBUF_GENERATOR_OUTPUT_PATH}/openapi.yaml`; +/** + * Prepared working directory for running buf generate multiple times + * without repeating setup (copy, which checks, dep resolution). + */ +interface PreparedWorkingDir { + cwd: AbsoluteFilePath; + bufLockContents: string | undefined; +} + export class ProtobufOpenAPIGenerator { private context: TaskContext; private isAirGapped: boolean | undefined; @@ -44,6 +53,139 @@ export class ProtobufOpenAPIGenerator { return this.generateRemote(); } + /** + * Prepares a reusable working directory: copies the proto root once, + * writes buf config files, checks for required binaries, and resolves + * dependencies. Returns a handle that can be passed to generateFromPrepared() + * for each target file — no repeated setup. + */ + public async prepare({ + absoluteFilepathToProtobufRoot, + relativeFilepathToProtobufRoot, + local, + deps + }: { + absoluteFilepathToProtobufRoot: AbsoluteFilePath; + relativeFilepathToProtobufRoot: RelativeFilePath; + local: boolean; + deps: string[]; + }): Promise { + if (!local) { + this.context.failAndThrow("Remote Protobuf generation is unimplemented."); + } + + if (deps.length > 0 && this.isAirGapped === undefined) { + this.isAirGapped = await detectAirGappedModeForProtobuf( + absoluteFilepathToProtobufRoot, + this.context.logger + ); + } + + const cwd = AbsoluteFilePath.of((await tmp.dir()).path); + await cp(absoluteFilepathToProtobufRoot, cwd, { recursive: true }); + await writeFile( + join(cwd, RelativeFilePath.of(PROTOBUF_GENERATOR_CONFIG_FILENAME)), + getProtobufGeneratorConfig({ relativeFilepathToProtobufRoot }) + ); + + // Check required binaries once + const which = createLoggingExecutable("which", { + cwd, + logger: undefined, + doNotPipeOutput: true + }); + try { + await which(["buf"]); + } catch (err) { + this.context.failAndThrow( + "Missing required dependency; please install 'buf' to continue (e.g. 'brew install buf')." + ); + } + try { + await which(["protoc-gen-openapi"]); + } catch (err) { + this.context.failAndThrow( + "Missing required dependency; please install 'protoc-gen-openapi' to continue (e.g. 'brew install go && go install github.com/fern-api/protoc-gen-openapi/cmd/protoc-gen-openapi@latest')." + ); + } + + // Write buf.yaml and resolve dependencies once + const bufYamlPath = join(cwd, RelativeFilePath.of("buf.yaml")); + const bufLockPath = join(cwd, RelativeFilePath.of("buf.lock")); + await writeFile(bufYamlPath, getProtobufYamlV1(deps)); + + let bufLockContents: string | undefined; + if (deps.length > 0) { + if (this.isAirGapped) { + this.context.logger.debug("Air-gapped mode: skipping buf dep update"); + try { + await access(bufLockPath); + } catch { + this.context.failAndThrow( + "Air-gapped mode requires a pre-cached buf.lock file. Please run 'buf dep update' at build time to cache dependencies." + ); + } + } else { + const buf = createLoggingExecutable("buf", { + cwd, + logger: this.context.logger, + stdout: "ignore", + stderr: "pipe" + }); + await buf(["dep", "update"]); + } + try { + bufLockContents = await readFile(bufLockPath, "utf-8"); + } catch { + bufLockContents = undefined; + } + } + + return { cwd, bufLockContents }; + } + + /** + * Generates OpenAPI output for a single proto target using a previously + * prepared working directory. Each call only runs `buf generate ` + * — all heavy setup was done once in prepare(). + * + * Because protoc-gen-openapi writes to a fixed output path, each call + * renames the output to a unique temp file to avoid conflicts when called + * sequentially for multiple targets. + */ + public async generateFromPrepared({ + preparedDir, + absoluteFilepathToProtobufRoot, + absoluteFilepathToProtobufTarget + }: { + preparedDir: PreparedWorkingDir; + absoluteFilepathToProtobufRoot: AbsoluteFilePath; + absoluteFilepathToProtobufTarget: AbsoluteFilePath; + }): Promise<{ absoluteFilepath: AbsoluteFilePath; bufLockContents: string | undefined }> { + const target = relative(absoluteFilepathToProtobufRoot, absoluteFilepathToProtobufTarget); + const buf = createLoggingExecutable("buf", { + cwd: preparedDir.cwd, + logger: this.context.logger, + stdout: "ignore", + stderr: "pipe" + }); + + const bufGenerateResult = await buf(["generate", target.toString()]); + if (bufGenerateResult.exitCode !== 0) { + this.context.failAndThrow(bufGenerateResult.stderr); + } + + // Move output to a unique temp file so the next call doesn't overwrite it + const outputPath = join(preparedDir.cwd, RelativeFilePath.of(PROTOBUF_GENERATOR_OUTPUT_FILEPATH)); + const uniqueOutput = AbsoluteFilePath.of((await tmp.file({ postfix: ".yaml" })).path); + await rename(outputPath, uniqueOutput); + + return { + absoluteFilepath: uniqueOutput, + bufLockContents: preparedDir.bufLockContents + }; + } + private async generateLocal({ absoluteFilepathToProtobufRoot, absoluteFilepathToProtobufTarget, diff --git a/packages/cli/workspace/lazy-fern-workspace/src/utils/getAllOpenAPISpecs.ts b/packages/cli/workspace/lazy-fern-workspace/src/utils/getAllOpenAPISpecs.ts index ed21049eb295..f20342f2c45e 100644 --- a/packages/cli/workspace/lazy-fern-workspace/src/utils/getAllOpenAPISpecs.ts +++ b/packages/cli/workspace/lazy-fern-workspace/src/utils/getAllOpenAPISpecs.ts @@ -32,44 +32,27 @@ export async function getAllOpenAPISpecs({ return []; } - // Process first file to get buf.lock contents for caching - // Safe to index — length > 0 is checked above - const firstFile = allProtobufTargetFilepaths[0] as AbsoluteFilePath; - const firstResult = await convertProtobufToOpenAPI({ - generator, - protobufSpec: { - ...protobufSpec, - absoluteFilepathToProtobufTarget: firstFile - }, - relativePathToDependency + // Prepare a single working directory: copies proto root, resolves + // dependencies, and checks binaries once for all files. + const preparedDir = await generator.prepare({ + absoluteFilepathToProtobufRoot: protobufSpec.absoluteFilepathToProtobufRoot, + relativeFilepathToProtobufRoot: protobufSpec.relativeFilepathToProtobufRoot, + local: protobufSpec.generateLocally, + deps: protobufSpec.dependencies }); - const bufLockContents = firstResult?.bufLockContents; + // Generate each file sequentially using the shared working dir. + // Sequential because protoc-gen-openapi writes to a fixed output + // path and buf generate is not safe to run concurrently in the + // same directory. const openApiSpecs: OpenAPISpec[] = []; - if (firstResult != null) { - openApiSpecs.push(firstResult.openApiSpec); - } - - // Parallelize remaining files with cached buf.lock contents - if (allProtobufTargetFilepaths.length > 1) { - const remainingResults = await Promise.all( - allProtobufTargetFilepaths.slice(1).map((file) => - convertProtobufToOpenAPI({ - generator, - protobufSpec: { - ...protobufSpec, - absoluteFilepathToProtobufTarget: file - }, - relativePathToDependency, - existingBufLockContents: bufLockContents - }) - ) - ); - for (const result of remainingResults) { - if (result != null) { - openApiSpecs.push(result.openApiSpec); - } - } + for (const file of allProtobufTargetFilepaths) { + const result = await generator.generateFromPrepared({ + preparedDir, + absoluteFilepathToProtobufRoot: protobufSpec.absoluteFilepathToProtobufRoot, + absoluteFilepathToProtobufTarget: file + }); + openApiSpecs.push(makeOpenApiSpec({ result, protobufSpec, relativePathToDependency, target: file })); } return openApiSpecs; @@ -83,39 +66,54 @@ export async function getAllOpenAPISpecs({ export async function convertProtobufToOpenAPI({ generator, protobufSpec, - relativePathToDependency, - existingBufLockContents + relativePathToDependency }: { generator: ProtobufOpenAPIGenerator; protobufSpec: ProtobufSpec; relativePathToDependency?: RelativeFilePath; - existingBufLockContents?: string; -}): Promise<{ bufLockContents: string | undefined; openApiSpec: OpenAPISpec } | undefined> { +}): Promise<{ openApiSpec: OpenAPISpec } | undefined> { if (protobufSpec.absoluteFilepathToProtobufTarget == null) { return undefined; } - const openAPIAbsoluteFilePath = await generator.generate({ + const result = await generator.generate({ absoluteFilepathToProtobufRoot: protobufSpec.absoluteFilepathToProtobufRoot, absoluteFilepathToProtobufTarget: protobufSpec.absoluteFilepathToProtobufTarget, relativeFilepathToProtobufRoot: protobufSpec.relativeFilepathToProtobufRoot, local: protobufSpec.generateLocally, - deps: protobufSpec.dependencies, - existingBufLockContents + deps: protobufSpec.dependencies }); return { - bufLockContents: openAPIAbsoluteFilePath.bufLockContents, - openApiSpec: { - type: "openapi", - absoluteFilepath: openAPIAbsoluteFilePath.absoluteFilepath, - absoluteFilepathToOverrides: protobufSpec.absoluteFilepathToOverrides, - absoluteFilepathToOverlays: undefined, - settings: protobufSpec.settings, - source: { - type: "protobuf", - relativePathToDependency, - root: protobufSpec.absoluteFilepathToProtobufRoot, - file: protobufSpec.absoluteFilepathToProtobufTarget - } + openApiSpec: makeOpenApiSpec({ + result, + protobufSpec, + relativePathToDependency, + target: protobufSpec.absoluteFilepathToProtobufTarget + }) + }; +} + +function makeOpenApiSpec({ + result, + protobufSpec, + relativePathToDependency, + target +}: { + result: { absoluteFilepath: AbsoluteFilePath; bufLockContents: string | undefined }; + protobufSpec: ProtobufSpec; + relativePathToDependency?: RelativeFilePath; + target: AbsoluteFilePath; +}): OpenAPISpec { + return { + type: "openapi", + absoluteFilepath: result.absoluteFilepath, + absoluteFilepathToOverrides: protobufSpec.absoluteFilepathToOverrides, + absoluteFilepathToOverlays: undefined, + settings: protobufSpec.settings, + source: { + type: "protobuf", + relativePathToDependency, + root: protobufSpec.absoluteFilepathToProtobufRoot, + file: target } }; } From 1f98a9d06eecb4faccce76b90c4ad526f8f6a126 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 18 Mar 2026 20:59:44 +0000 Subject: [PATCH 03/12] fix: log error in catch block per REVIEW.md conventions Co-Authored-By: Niels Swimberghe <3382717+Swimburger@users.noreply.github.com> --- .../src/protobuf/ProtobufOpenAPIGenerator.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/cli/workspace/lazy-fern-workspace/src/protobuf/ProtobufOpenAPIGenerator.ts b/packages/cli/workspace/lazy-fern-workspace/src/protobuf/ProtobufOpenAPIGenerator.ts index dd87718885e3..5fb7a2772950 100644 --- a/packages/cli/workspace/lazy-fern-workspace/src/protobuf/ProtobufOpenAPIGenerator.ts +++ b/packages/cli/workspace/lazy-fern-workspace/src/protobuf/ProtobufOpenAPIGenerator.ts @@ -136,7 +136,8 @@ export class ProtobufOpenAPIGenerator { } try { bufLockContents = await readFile(bufLockPath, "utf-8"); - } catch { + } catch (err) { + this.context.logger.debug(`Failed to read buf.lock: ${err}`); bufLockContents = undefined; } } From 661c26a04b3f31c981dd7ebeb9c3564f1e844a26 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 18 Mar 2026 22:21:22 +0000 Subject: [PATCH 04/12] fix: group protobuf specs by root to share prepared working dir across explicit targets Co-Authored-By: Niels Swimberghe <3382717+Swimburger@users.noreply.github.com> --- .../src/utils/getAllOpenAPISpecs.ts | 86 +++++++++++-------- 1 file changed, 51 insertions(+), 35 deletions(-) diff --git a/packages/cli/workspace/lazy-fern-workspace/src/utils/getAllOpenAPISpecs.ts b/packages/cli/workspace/lazy-fern-workspace/src/utils/getAllOpenAPISpecs.ts index f20342f2c45e..09609df17ebb 100644 --- a/packages/cli/workspace/lazy-fern-workspace/src/utils/getAllOpenAPISpecs.ts +++ b/packages/cli/workspace/lazy-fern-workspace/src/utils/getAllOpenAPISpecs.ts @@ -21,46 +21,62 @@ export async function getAllOpenAPISpecs({ return { ...spec, relativePathToDependency }; }); const protobufSpecs: ProtobufSpec[] = specs.filter((spec): spec is ProtobufSpec => spec.type === "protobuf"); - const openApiSpecsFromProto = await Promise.all( - protobufSpecs.map(async (protobufSpec) => { - if (protobufSpec.absoluteFilepathToProtobufTarget != null) { - const result = await convertProtobufToOpenAPI({ generator, protobufSpec, relativePathToDependency }); - return result ? [result.openApiSpec] : []; - } - const allProtobufTargetFilepaths = await listFiles(protobufSpec.absoluteFilepathToProtobufRoot, "proto"); - if (allProtobufTargetFilepaths.length === 0) { - return []; - } - // Prepare a single working directory: copies proto root, resolves - // dependencies, and checks binaries once for all files. - const preparedDir = await generator.prepare({ - absoluteFilepathToProtobufRoot: protobufSpec.absoluteFilepathToProtobufRoot, - relativeFilepathToProtobufRoot: protobufSpec.relativeFilepathToProtobufRoot, - local: protobufSpec.generateLocally, - deps: protobufSpec.dependencies - }); + // Group protobuf specs by their proto root so we can prepare a single + // working directory per root, then generate each target from it. + const specsByRoot = new Map(); + for (const spec of protobufSpecs) { + const key = spec.absoluteFilepathToProtobufRoot; + const group = specsByRoot.get(key) ?? []; + group.push(spec); + specsByRoot.set(key, group); + } + + const openApiSpecsFromProto: OpenAPISpec[] = []; + + for (const group of specsByRoot.values()) { + // Use the first spec in the group for the shared prepare() call. + const representative = group[0]; + if (representative == null) { + continue; + } - // Generate each file sequentially using the shared working dir. - // Sequential because protoc-gen-openapi writes to a fixed output - // path and buf generate is not safe to run concurrently in the - // same directory. - const openApiSpecs: OpenAPISpec[] = []; - for (const file of allProtobufTargetFilepaths) { - const result = await generator.generateFromPrepared({ - preparedDir, - absoluteFilepathToProtobufRoot: protobufSpec.absoluteFilepathToProtobufRoot, - absoluteFilepathToProtobufTarget: file - }); - openApiSpecs.push(makeOpenApiSpec({ result, protobufSpec, relativePathToDependency, target: file })); + const preparedDir = await generator.prepare({ + absoluteFilepathToProtobufRoot: representative.absoluteFilepathToProtobufRoot, + relativeFilepathToProtobufRoot: representative.relativeFilepathToProtobufRoot, + local: representative.generateLocally, + deps: representative.dependencies + }); + + // Collect all targets: explicit targets from specs + discovered files + // for specs without a target. + const targetEntries: Array<{ target: AbsoluteFilePath; spec: ProtobufSpec }> = []; + for (const spec of group) { + if (spec.absoluteFilepathToProtobufTarget != null) { + targetEntries.push({ target: spec.absoluteFilepathToProtobufTarget, spec }); + } else { + const files = await listFiles(spec.absoluteFilepathToProtobufRoot, "proto"); + for (const file of files) { + targetEntries.push({ target: file, spec }); + } } + } - return openApiSpecs; - }) - ); + // Generate each target sequentially using the shared working dir. + // Sequential because protoc-gen-openapi writes to a fixed output + // path and buf generate is not safe to run concurrently in the + // same directory. + for (const { target, spec } of targetEntries) { + const result = await generator.generateFromPrepared({ + preparedDir, + absoluteFilepathToProtobufRoot: spec.absoluteFilepathToProtobufRoot, + absoluteFilepathToProtobufTarget: target + }); + openApiSpecsFromProto.push(makeOpenApiSpec({ result, protobufSpec: spec, relativePathToDependency, target })); + } + } - const flattenedSpecs = openApiSpecsFromProto.flat().filter((spec) => isNonNullish(spec)); - return [...openApiSpecs, ...flattenedSpecs]; + return [...openApiSpecs, ...openApiSpecsFromProto]; } export async function convertProtobufToOpenAPI({ From 7807ffa7f3f5614c684838165dc697ef1552d9e2 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 18 Mar 2026 22:23:19 +0000 Subject: [PATCH 05/12] fix: remove unused isNonNullish import and fix biome formatting Co-Authored-By: Niels Swimberghe <3382717+Swimburger@users.noreply.github.com> --- .../lazy-fern-workspace/src/utils/getAllOpenAPISpecs.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/cli/workspace/lazy-fern-workspace/src/utils/getAllOpenAPISpecs.ts b/packages/cli/workspace/lazy-fern-workspace/src/utils/getAllOpenAPISpecs.ts index 09609df17ebb..2b895677197b 100644 --- a/packages/cli/workspace/lazy-fern-workspace/src/utils/getAllOpenAPISpecs.ts +++ b/packages/cli/workspace/lazy-fern-workspace/src/utils/getAllOpenAPISpecs.ts @@ -1,5 +1,4 @@ import { OpenAPISpec, ProtobufSpec, Spec } from "@fern-api/api-workspace-commons"; -import { isNonNullish } from "@fern-api/core-utils"; import { AbsoluteFilePath, listFiles, RelativeFilePath } from "@fern-api/fs-utils"; import { TaskContext } from "@fern-api/task-context"; @@ -72,7 +71,9 @@ export async function getAllOpenAPISpecs({ absoluteFilepathToProtobufRoot: spec.absoluteFilepathToProtobufRoot, absoluteFilepathToProtobufTarget: target }); - openApiSpecsFromProto.push(makeOpenApiSpec({ result, protobufSpec: spec, relativePathToDependency, target })); + openApiSpecsFromProto.push( + makeOpenApiSpec({ result, protobufSpec: spec, relativePathToDependency, target }) + ); } } From 997f3ee8e56084938774638177924138274088bd Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 18 Mar 2026 22:45:14 +0000 Subject: [PATCH 06/12] fix: make protobuf IR generation sequential to avoid npm install -g race condition Co-Authored-By: Niels Swimberghe <3382717+Swimburger@users.noreply.github.com> --- .../lazy-fern-workspace/src/OSSWorkspace.ts | 53 ++++++++++--------- 1 file changed, 27 insertions(+), 26 deletions(-) diff --git a/packages/cli/workspace/lazy-fern-workspace/src/OSSWorkspace.ts b/packages/cli/workspace/lazy-fern-workspace/src/OSSWorkspace.ts index f9c6da8b2357..a6af829638c7 100644 --- a/packages/cli/workspace/lazy-fern-workspace/src/OSSWorkspace.ts +++ b/packages/cli/workspace/lazy-fern-workspace/src/OSSWorkspace.ts @@ -461,36 +461,37 @@ export class OSSWorkspace extends BaseOpenAPIWorkspace { return []; } - const results = await Promise.all( - protobufSpecs.map(async (spec) => { - try { - const protobufIRGenerator = new ProtobufIRGenerator({ context }); - const protobufIRFilepath = await protobufIRGenerator.generate({ - absoluteFilepathToProtobufRoot: spec.absoluteFilepathToProtobufRoot, - absoluteFilepathToProtobufTarget: spec.absoluteFilepathToProtobufTarget, - local: true, - deps: spec.dependencies - }); + // Process specs sequentially because ProtobufIRGenerator.generate() + // runs `npm install -g` which is not safe to invoke concurrently. + const results: IntermediateRepresentation[] = []; + for (const spec of protobufSpecs) { + try { + const protobufIRGenerator = new ProtobufIRGenerator({ context }); + const protobufIRFilepath = await protobufIRGenerator.generate({ + absoluteFilepathToProtobufRoot: spec.absoluteFilepathToProtobufRoot, + absoluteFilepathToProtobufTarget: spec.absoluteFilepathToProtobufTarget, + local: true, + deps: spec.dependencies + }); - const result = await readFile(protobufIRFilepath, "utf-8"); - if (result != null) { - const serializedIr = serialization.IntermediateRepresentation.parse(JSON.parse(result), { - allowUnrecognizedEnumValues: true, - skipValidation: true - }); - if (serializedIr.ok) { - return serializedIr.value; - } - context.logger.log("error", "Failed to parse protobuf IR"); + const result = await readFile(protobufIRFilepath, "utf-8"); + if (result != null) { + const serializedIr = serialization.IntermediateRepresentation.parse(JSON.parse(result), { + allowUnrecognizedEnumValues: true, + skipValidation: true + }); + if (serializedIr.ok) { + results.push(serializedIr.value); + continue; } - } catch (error) { - context.logger.log("warn", "Failed to parse protobuf IR: " + error); + context.logger.log("error", "Failed to parse protobuf IR"); } - return undefined; - }) - ); + } catch (error) { + context.logger.log("warn", "Failed to parse protobuf IR: " + error); + } + } - return results.filter((ir): ir is IntermediateRepresentation => ir != null); + return results; } public async toFernWorkspace( From c3de78fbe588abf0fa62b045fec158b92b49fb0d Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 18 Mar 2026 22:51:32 +0000 Subject: [PATCH 07/12] chore: regenerate url-form-encoded IR test snapshots after main merge Co-Authored-By: Niels Swimberghe <3382717+Swimburger@users.noreply.github.com> --- .../test-definitions/url-form-encoded.json | 240 +++ .../test-definitions/url-form-encoded.json | 1647 ++++++++++++++++- 2 files changed, 1832 insertions(+), 55 deletions(-) diff --git a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/url-form-encoded.json b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/url-form-encoded.json index 2aa6317520b9..d3862cf66729 100644 --- a/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/url-form-encoded.json +++ b/packages/cli/generation/ir-generator-tests/src/dynamic-snippets/__test__/test-definitions/url-form-encoded.json @@ -99,6 +99,198 @@ ], "extends": null, "additionalProperties": false + }, + "type_:TokenRequest": { + "type": "object", + "declaration": { + "name": { + "originalName": "TokenRequest", + "camelCase": { + "unsafeName": "tokenRequest", + "safeName": "tokenRequest" + }, + "snakeCase": { + "unsafeName": "token_request", + "safeName": "token_request" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN_REQUEST", + "safeName": "TOKEN_REQUEST" + }, + "pascalCase": { + "unsafeName": "TokenRequest", + "safeName": "TokenRequest" + } + }, + "fernFilepath": { + "allParts": [], + "packagePath": [], + "file": null + } + }, + "properties": [ + { + "name": { + "name": { + "originalName": "client_id", + "camelCase": { + "unsafeName": "clientID", + "safeName": "clientID" + }, + "snakeCase": { + "unsafeName": "client_id", + "safeName": "client_id" + }, + "screamingSnakeCase": { + "unsafeName": "CLIENT_ID", + "safeName": "CLIENT_ID" + }, + "pascalCase": { + "unsafeName": "ClientID", + "safeName": "ClientID" + } + }, + "wireValue": "client_id" + }, + "typeReference": { + "type": "primitive", + "value": "STRING" + }, + "propertyAccess": null, + "variable": null + }, + { + "name": { + "name": { + "originalName": "client_secret", + "camelCase": { + "unsafeName": "clientSecret", + "safeName": "clientSecret" + }, + "snakeCase": { + "unsafeName": "client_secret", + "safeName": "client_secret" + }, + "screamingSnakeCase": { + "unsafeName": "CLIENT_SECRET", + "safeName": "CLIENT_SECRET" + }, + "pascalCase": { + "unsafeName": "ClientSecret", + "safeName": "ClientSecret" + } + }, + "wireValue": "client_secret" + }, + "typeReference": { + "type": "primitive", + "value": "STRING" + }, + "propertyAccess": null, + "variable": null + } + ], + "extends": null, + "additionalProperties": false + }, + "type_:TokenResponse": { + "type": "object", + "declaration": { + "name": { + "originalName": "TokenResponse", + "camelCase": { + "unsafeName": "tokenResponse", + "safeName": "tokenResponse" + }, + "snakeCase": { + "unsafeName": "token_response", + "safeName": "token_response" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN_RESPONSE", + "safeName": "TOKEN_RESPONSE" + }, + "pascalCase": { + "unsafeName": "TokenResponse", + "safeName": "TokenResponse" + } + }, + "fernFilepath": { + "allParts": [], + "packagePath": [], + "file": null + } + }, + "properties": [ + { + "name": { + "name": { + "originalName": "access_token", + "camelCase": { + "unsafeName": "accessToken", + "safeName": "accessToken" + }, + "snakeCase": { + "unsafeName": "access_token", + "safeName": "access_token" + }, + "screamingSnakeCase": { + "unsafeName": "ACCESS_TOKEN", + "safeName": "ACCESS_TOKEN" + }, + "pascalCase": { + "unsafeName": "AccessToken", + "safeName": "AccessToken" + } + }, + "wireValue": "access_token" + }, + "typeReference": { + "type": "optional", + "value": { + "type": "primitive", + "value": "STRING" + } + }, + "propertyAccess": null, + "variable": null + }, + { + "name": { + "name": { + "originalName": "expires_in", + "camelCase": { + "unsafeName": "expiresIn", + "safeName": "expiresIn" + }, + "snakeCase": { + "unsafeName": "expires_in", + "safeName": "expires_in" + }, + "screamingSnakeCase": { + "unsafeName": "EXPIRES_IN", + "safeName": "EXPIRES_IN" + }, + "pascalCase": { + "unsafeName": "ExpiresIn", + "safeName": "ExpiresIn" + } + }, + "wireValue": "expires_in" + }, + "typeReference": { + "type": "optional", + "value": { + "type": "primitive", + "value": "INTEGER" + } + }, + "propertyAccess": null, + "variable": null + } + ], + "extends": null, + "additionalProperties": false } }, "headers": [], @@ -240,6 +432,54 @@ "type": "json" }, "examples": null + }, + "endpoint_.get_token": { + "auth": null, + "declaration": { + "name": { + "originalName": "get_token", + "camelCase": { + "unsafeName": "getToken", + "safeName": "getToken" + }, + "snakeCase": { + "unsafeName": "get_token", + "safeName": "get_token" + }, + "screamingSnakeCase": { + "unsafeName": "GET_TOKEN", + "safeName": "GET_TOKEN" + }, + "pascalCase": { + "unsafeName": "GetToken", + "safeName": "GetToken" + } + }, + "fernFilepath": { + "allParts": [], + "packagePath": [], + "file": null + } + }, + "location": { + "method": "POST", + "path": "/token" + }, + "request": { + "type": "body", + "pathParameters": [], + "body": { + "type": "typeReference", + "value": { + "type": "named", + "value": "type_:TokenRequest" + } + } + }, + "response": { + "type": "json" + }, + "examples": null } }, "pathParameters": [], diff --git a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/url-form-encoded.json b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/url-form-encoded.json index 1dd89280c453..eb70c4c8cdae 100644 --- a/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/url-form-encoded.json +++ b/packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/url-form-encoded.json @@ -176,6 +176,286 @@ "v2Examples": null, "availability": null, "docs": null + }, + "type_:TokenRequest": { + "inline": null, + "name": { + "name": { + "originalName": "TokenRequest", + "camelCase": { + "unsafeName": "tokenRequest", + "safeName": "tokenRequest" + }, + "snakeCase": { + "unsafeName": "token_request", + "safeName": "token_request" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN_REQUEST", + "safeName": "TOKEN_REQUEST" + }, + "pascalCase": { + "unsafeName": "TokenRequest", + "safeName": "TokenRequest" + } + }, + "fernFilepath": { + "allParts": [], + "packagePath": [], + "file": null + }, + "displayName": null, + "typeId": "type_:TokenRequest" + }, + "shape": { + "_type": "object", + "extends": [], + "properties": [ + { + "name": { + "name": { + "originalName": "client_id", + "camelCase": { + "unsafeName": "clientID", + "safeName": "clientID" + }, + "snakeCase": { + "unsafeName": "client_id", + "safeName": "client_id" + }, + "screamingSnakeCase": { + "unsafeName": "CLIENT_ID", + "safeName": "CLIENT_ID" + }, + "pascalCase": { + "unsafeName": "ClientID", + "safeName": "ClientID" + } + }, + "wireValue": "client_id" + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + }, + "propertyAccess": null, + "v2Examples": { + "userSpecifiedExamples": {}, + "autogeneratedExamples": {} + }, + "availability": null, + "docs": "Client identifier" + }, + { + "name": { + "name": { + "originalName": "client_secret", + "camelCase": { + "unsafeName": "clientSecret", + "safeName": "clientSecret" + }, + "snakeCase": { + "unsafeName": "client_secret", + "safeName": "client_secret" + }, + "screamingSnakeCase": { + "unsafeName": "CLIENT_SECRET", + "safeName": "CLIENT_SECRET" + }, + "pascalCase": { + "unsafeName": "ClientSecret", + "safeName": "ClientSecret" + } + }, + "wireValue": "client_secret" + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + }, + "propertyAccess": null, + "v2Examples": { + "userSpecifiedExamples": {}, + "autogeneratedExamples": {} + }, + "availability": null, + "docs": "Client secret" + } + ], + "extra-properties": false, + "extendedProperties": [] + }, + "referencedTypes": [], + "encoding": { + "json": {}, + "proto": null + }, + "source": null, + "userProvidedExamples": [], + "autogeneratedExamples": [], + "v2Examples": null, + "availability": null, + "docs": null + }, + "type_:TokenResponse": { + "inline": null, + "name": { + "name": { + "originalName": "TokenResponse", + "camelCase": { + "unsafeName": "tokenResponse", + "safeName": "tokenResponse" + }, + "snakeCase": { + "unsafeName": "token_response", + "safeName": "token_response" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN_RESPONSE", + "safeName": "TOKEN_RESPONSE" + }, + "pascalCase": { + "unsafeName": "TokenResponse", + "safeName": "TokenResponse" + } + }, + "fernFilepath": { + "allParts": [], + "packagePath": [], + "file": null + }, + "displayName": null, + "typeId": "type_:TokenResponse" + }, + "shape": { + "_type": "object", + "extends": [], + "properties": [ + { + "name": { + "name": { + "originalName": "access_token", + "camelCase": { + "unsafeName": "accessToken", + "safeName": "accessToken" + }, + "snakeCase": { + "unsafeName": "access_token", + "safeName": "access_token" + }, + "screamingSnakeCase": { + "unsafeName": "ACCESS_TOKEN", + "safeName": "ACCESS_TOKEN" + }, + "pascalCase": { + "unsafeName": "AccessToken", + "safeName": "AccessToken" + } + }, + "wireValue": "access_token" + }, + "valueType": { + "_type": "container", + "container": { + "_type": "optional", + "optional": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + }, + "propertyAccess": null, + "v2Examples": { + "userSpecifiedExamples": {}, + "autogeneratedExamples": {} + }, + "availability": null, + "docs": null + }, + { + "name": { + "name": { + "originalName": "expires_in", + "camelCase": { + "unsafeName": "expiresIn", + "safeName": "expiresIn" + }, + "snakeCase": { + "unsafeName": "expires_in", + "safeName": "expires_in" + }, + "screamingSnakeCase": { + "unsafeName": "EXPIRES_IN", + "safeName": "EXPIRES_IN" + }, + "pascalCase": { + "unsafeName": "ExpiresIn", + "safeName": "ExpiresIn" + } + }, + "wireValue": "expires_in" + }, + "valueType": { + "_type": "container", + "container": { + "_type": "optional", + "optional": { + "_type": "primitive", + "primitive": { + "v1": "INTEGER", + "v2": { + "type": "integer", + "default": null, + "validation": null + } + } + } + } + }, + "propertyAccess": null, + "v2Examples": { + "userSpecifiedExamples": {}, + "autogeneratedExamples": {} + }, + "availability": null, + "docs": null + } + ], + "extra-properties": false, + "extendedProperties": [] + }, + "referencedTypes": [], + "encoding": { + "json": {}, + "proto": null + }, + "source": null, + "userProvidedExamples": [], + "autogeneratedExamples": [], + "v2Examples": null, + "availability": null, + "docs": null } }, "errors": {}, @@ -1125,61 +1405,1076 @@ "responseHeaders": [], "availability": null, "docs": null - } - ], - "audiences": null - } - }, - "constants": { - "errorInstanceIdKey": { - "name": { - "originalName": "errorInstanceId", - "camelCase": { - "unsafeName": "errorInstanceID", - "safeName": "errorInstanceID" - }, - "snakeCase": { - "unsafeName": "error_instance_id", - "safeName": "error_instance_id" - }, - "screamingSnakeCase": { - "unsafeName": "ERROR_INSTANCE_ID", - "safeName": "ERROR_INSTANCE_ID" }, - "pascalCase": { - "unsafeName": "ErrorInstanceID", - "safeName": "ErrorInstanceID" - } - }, - "wireValue": "errorInstanceId" - } - }, - "environments": null, - "errorDiscriminationStrategy": { - "type": "statusCode" - }, - "basePath": null, - "pathParameters": [], - "variables": [], - "serviceTypeReferenceInfo": { - "typesReferencedOnlyByService": { - "service_": [ - "type_:PostSubmitResponse" - ] - }, - "sharedTypes": [] - }, - "webhookGroups": {}, - "websocketChannels": {}, - "readmeConfig": null, - "sourceConfig": null, - "publishConfig": null, - "dynamic": { - "version": "1.0.0", - "types": { - "type_:PostSubmitResponse": { - "type": "object", - "declaration": { + { + "id": "endpoint_.get_token", + "name": { + "originalName": "get_token", + "camelCase": { + "unsafeName": "getToken", + "safeName": "getToken" + }, + "snakeCase": { + "unsafeName": "get_token", + "safeName": "get_token" + }, + "screamingSnakeCase": { + "unsafeName": "GET_TOKEN", + "safeName": "GET_TOKEN" + }, + "pascalCase": { + "unsafeName": "GetToken", + "safeName": "GetToken" + } + }, + "displayName": "Get OAuth token", + "auth": false, + "security": null, + "idempotent": false, + "baseUrl": null, + "v2BaseUrls": null, + "method": "POST", + "basePath": null, + "path": { + "head": "/token", + "parts": [] + }, + "fullPath": { + "head": "token", + "parts": [] + }, + "pathParameters": [], + "allPathParameters": [], + "queryParameters": [], + "headers": [], + "requestBody": { + "type": "reference", + "requestBodyType": { + "_type": "named", + "name": { + "originalName": "TokenRequest", + "camelCase": { + "unsafeName": "tokenRequest", + "safeName": "tokenRequest" + }, + "snakeCase": { + "unsafeName": "token_request", + "safeName": "token_request" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN_REQUEST", + "safeName": "TOKEN_REQUEST" + }, + "pascalCase": { + "unsafeName": "TokenRequest", + "safeName": "TokenRequest" + } + }, + "fernFilepath": { + "allParts": [], + "packagePath": [], + "file": null + }, + "displayName": null, + "typeId": "type_:TokenRequest", + "default": null, + "inline": null + }, + "docs": null, + "contentType": "application/x-www-form-urlencoded", + "v2Examples": null + }, + "v2RequestBodies": null, + "sdkRequest": { + "shape": { + "type": "justRequestBody", + "value": { + "type": "typeReference", + "requestBodyType": { + "_type": "named", + "name": { + "originalName": "TokenRequest", + "camelCase": { + "unsafeName": "tokenRequest", + "safeName": "tokenRequest" + }, + "snakeCase": { + "unsafeName": "token_request", + "safeName": "token_request" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN_REQUEST", + "safeName": "TOKEN_REQUEST" + }, + "pascalCase": { + "unsafeName": "TokenRequest", + "safeName": "TokenRequest" + } + }, + "fernFilepath": { + "allParts": [], + "packagePath": [], + "file": null + }, + "displayName": null, + "typeId": "type_:TokenRequest", + "default": null, + "inline": null + }, + "docs": null, + "contentType": null, + "v2Examples": null + } + }, + "requestParameterName": { + "originalName": "request", + "camelCase": { + "unsafeName": "request", + "safeName": "request" + }, + "snakeCase": { + "unsafeName": "request", + "safeName": "request" + }, + "screamingSnakeCase": { + "unsafeName": "REQUEST", + "safeName": "REQUEST" + }, + "pascalCase": { + "unsafeName": "Request", + "safeName": "Request" + } + }, + "streamParameter": null + }, + "response": { + "body": { + "type": "json", + "value": { + "type": "response", + "responseBodyType": { + "_type": "named", + "name": { + "originalName": "TokenResponse", + "camelCase": { + "unsafeName": "tokenResponse", + "safeName": "tokenResponse" + }, + "snakeCase": { + "unsafeName": "token_response", + "safeName": "token_response" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN_RESPONSE", + "safeName": "TOKEN_RESPONSE" + }, + "pascalCase": { + "unsafeName": "TokenResponse", + "safeName": "TokenResponse" + } + }, + "fernFilepath": { + "allParts": [], + "packagePath": [], + "file": null + }, + "displayName": null, + "typeId": "type_:TokenResponse", + "default": null, + "inline": null + }, + "docs": "Token issued successfully", + "v2Examples": null + } + }, + "status-code": 200, + "isWildcardStatusCode": null, + "docs": "Token issued successfully" + }, + "v2Responses": null, + "errors": [], + "userSpecifiedExamples": [ + { + "example": { + "id": "df67dd0c", + "name": null, + "url": "/token", + "rootPathParameters": [], + "endpointPathParameters": [], + "servicePathParameters": [], + "endpointHeaders": [], + "serviceHeaders": [], + "queryParameters": [], + "request": { + "type": "reference", + "shape": { + "type": "named", + "typeName": { + "typeId": "type_:TokenRequest", + "fernFilepath": { + "allParts": [], + "packagePath": [], + "file": null + }, + "name": { + "originalName": "TokenRequest", + "camelCase": { + "unsafeName": "tokenRequest", + "safeName": "tokenRequest" + }, + "snakeCase": { + "unsafeName": "token_request", + "safeName": "token_request" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN_REQUEST", + "safeName": "TOKEN_REQUEST" + }, + "pascalCase": { + "unsafeName": "TokenRequest", + "safeName": "TokenRequest" + } + }, + "displayName": null + }, + "shape": { + "type": "object", + "properties": [ + { + "name": { + "name": { + "originalName": "client_id", + "camelCase": { + "unsafeName": "clientID", + "safeName": "clientID" + }, + "snakeCase": { + "unsafeName": "client_id", + "safeName": "client_id" + }, + "screamingSnakeCase": { + "unsafeName": "CLIENT_ID", + "safeName": "CLIENT_ID" + }, + "pascalCase": { + "unsafeName": "ClientID", + "safeName": "ClientID" + } + }, + "wireValue": "client_id" + }, + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "client_id" + } + } + }, + "jsonExample": "client_id" + }, + "originalTypeDeclaration": { + "typeId": "type_:TokenRequest", + "fernFilepath": { + "allParts": [], + "packagePath": [], + "file": null + }, + "name": { + "originalName": "TokenRequest", + "camelCase": { + "unsafeName": "tokenRequest", + "safeName": "tokenRequest" + }, + "snakeCase": { + "unsafeName": "token_request", + "safeName": "token_request" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN_REQUEST", + "safeName": "TOKEN_REQUEST" + }, + "pascalCase": { + "unsafeName": "TokenRequest", + "safeName": "TokenRequest" + } + }, + "displayName": null + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "client_secret", + "camelCase": { + "unsafeName": "clientSecret", + "safeName": "clientSecret" + }, + "snakeCase": { + "unsafeName": "client_secret", + "safeName": "client_secret" + }, + "screamingSnakeCase": { + "unsafeName": "CLIENT_SECRET", + "safeName": "CLIENT_SECRET" + }, + "pascalCase": { + "unsafeName": "ClientSecret", + "safeName": "ClientSecret" + } + }, + "wireValue": "client_secret" + }, + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "client_secret" + } + } + }, + "jsonExample": "client_secret" + }, + "originalTypeDeclaration": { + "typeId": "type_:TokenRequest", + "fernFilepath": { + "allParts": [], + "packagePath": [], + "file": null + }, + "name": { + "originalName": "TokenRequest", + "camelCase": { + "unsafeName": "tokenRequest", + "safeName": "tokenRequest" + }, + "snakeCase": { + "unsafeName": "token_request", + "safeName": "token_request" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN_REQUEST", + "safeName": "TOKEN_REQUEST" + }, + "pascalCase": { + "unsafeName": "TokenRequest", + "safeName": "TokenRequest" + } + }, + "displayName": null + }, + "propertyAccess": null + } + ], + "extraProperties": null + } + }, + "jsonExample": { + "client_id": "client_id", + "client_secret": "client_secret" + } + }, + "response": { + "type": "ok", + "value": { + "type": "body", + "value": { + "shape": { + "type": "named", + "typeName": { + "typeId": "type_:TokenResponse", + "fernFilepath": { + "allParts": [], + "packagePath": [], + "file": null + }, + "name": { + "originalName": "TokenResponse", + "camelCase": { + "unsafeName": "tokenResponse", + "safeName": "tokenResponse" + }, + "snakeCase": { + "unsafeName": "token_response", + "safeName": "token_response" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN_RESPONSE", + "safeName": "TOKEN_RESPONSE" + }, + "pascalCase": { + "unsafeName": "TokenResponse", + "safeName": "TokenResponse" + } + }, + "displayName": null + }, + "shape": { + "type": "object", + "properties": [ + { + "name": { + "name": { + "originalName": "access_token", + "camelCase": { + "unsafeName": "accessToken", + "safeName": "accessToken" + }, + "snakeCase": { + "unsafeName": "access_token", + "safeName": "access_token" + }, + "screamingSnakeCase": { + "unsafeName": "ACCESS_TOKEN", + "safeName": "ACCESS_TOKEN" + }, + "pascalCase": { + "unsafeName": "AccessToken", + "safeName": "AccessToken" + } + }, + "wireValue": "access_token" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "access_token" + } + } + }, + "jsonExample": "access_token" + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + }, + "jsonExample": "access_token" + }, + "originalTypeDeclaration": { + "typeId": "type_:TokenResponse", + "fernFilepath": { + "allParts": [], + "packagePath": [], + "file": null + }, + "name": { + "originalName": "TokenResponse", + "camelCase": { + "unsafeName": "tokenResponse", + "safeName": "tokenResponse" + }, + "snakeCase": { + "unsafeName": "token_response", + "safeName": "token_response" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN_RESPONSE", + "safeName": "TOKEN_RESPONSE" + }, + "pascalCase": { + "unsafeName": "TokenResponse", + "safeName": "TokenResponse" + } + }, + "displayName": null + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "expires_in", + "camelCase": { + "unsafeName": "expiresIn", + "safeName": "expiresIn" + }, + "snakeCase": { + "unsafeName": "expires_in", + "safeName": "expires_in" + }, + "screamingSnakeCase": { + "unsafeName": "EXPIRES_IN", + "safeName": "EXPIRES_IN" + }, + "pascalCase": { + "unsafeName": "ExpiresIn", + "safeName": "ExpiresIn" + } + }, + "wireValue": "expires_in" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "integer", + "integer": 1 + } + }, + "jsonExample": 1 + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "INTEGER", + "v2": { + "type": "integer", + "default": null, + "validation": null + } + } + } + } + }, + "jsonExample": 1 + }, + "originalTypeDeclaration": { + "typeId": "type_:TokenResponse", + "fernFilepath": { + "allParts": [], + "packagePath": [], + "file": null + }, + "name": { + "originalName": "TokenResponse", + "camelCase": { + "unsafeName": "tokenResponse", + "safeName": "tokenResponse" + }, + "snakeCase": { + "unsafeName": "token_response", + "safeName": "token_response" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN_RESPONSE", + "safeName": "TOKEN_RESPONSE" + }, + "pascalCase": { + "unsafeName": "TokenResponse", + "safeName": "TokenResponse" + } + }, + "displayName": null + }, + "propertyAccess": null + } + ], + "extraProperties": null + } + }, + "jsonExample": { + "access_token": "access_token", + "expires_in": 1 + } + } + } + }, + "docs": null + }, + "codeSamples": null + } + ], + "autogeneratedExamples": [ + { + "example": { + "id": "801af41a", + "url": "/token", + "name": null, + "endpointHeaders": [], + "endpointPathParameters": [], + "queryParameters": [], + "servicePathParameters": [], + "serviceHeaders": [], + "rootPathParameters": [], + "request": { + "type": "reference", + "shape": { + "type": "named", + "shape": { + "type": "object", + "properties": [ + { + "name": { + "name": { + "originalName": "client_id", + "camelCase": { + "unsafeName": "clientID", + "safeName": "clientID" + }, + "snakeCase": { + "unsafeName": "client_id", + "safeName": "client_id" + }, + "screamingSnakeCase": { + "unsafeName": "CLIENT_ID", + "safeName": "CLIENT_ID" + }, + "pascalCase": { + "unsafeName": "ClientID", + "safeName": "ClientID" + } + }, + "wireValue": "client_id" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "TokenRequest", + "camelCase": { + "unsafeName": "tokenRequest", + "safeName": "tokenRequest" + }, + "snakeCase": { + "unsafeName": "token_request", + "safeName": "token_request" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN_REQUEST", + "safeName": "TOKEN_REQUEST" + }, + "pascalCase": { + "unsafeName": "TokenRequest", + "safeName": "TokenRequest" + } + }, + "fernFilepath": { + "allParts": [], + "packagePath": [], + "file": null + }, + "displayName": null, + "typeId": "type_:TokenRequest" + }, + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "client_id" + } + } + }, + "jsonExample": "client_id" + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "client_secret", + "camelCase": { + "unsafeName": "clientSecret", + "safeName": "clientSecret" + }, + "snakeCase": { + "unsafeName": "client_secret", + "safeName": "client_secret" + }, + "screamingSnakeCase": { + "unsafeName": "CLIENT_SECRET", + "safeName": "CLIENT_SECRET" + }, + "pascalCase": { + "unsafeName": "ClientSecret", + "safeName": "ClientSecret" + } + }, + "wireValue": "client_secret" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "TokenRequest", + "camelCase": { + "unsafeName": "tokenRequest", + "safeName": "tokenRequest" + }, + "snakeCase": { + "unsafeName": "token_request", + "safeName": "token_request" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN_REQUEST", + "safeName": "TOKEN_REQUEST" + }, + "pascalCase": { + "unsafeName": "TokenRequest", + "safeName": "TokenRequest" + } + }, + "fernFilepath": { + "allParts": [], + "packagePath": [], + "file": null + }, + "displayName": null, + "typeId": "type_:TokenRequest" + }, + "value": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "client_secret" + } + } + }, + "jsonExample": "client_secret" + }, + "propertyAccess": null + } + ], + "extraProperties": null + }, + "typeName": { + "name": { + "originalName": "TokenRequest", + "camelCase": { + "unsafeName": "tokenRequest", + "safeName": "tokenRequest" + }, + "snakeCase": { + "unsafeName": "token_request", + "safeName": "token_request" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN_REQUEST", + "safeName": "TOKEN_REQUEST" + }, + "pascalCase": { + "unsafeName": "TokenRequest", + "safeName": "TokenRequest" + } + }, + "fernFilepath": { + "allParts": [], + "packagePath": [], + "file": null + }, + "displayName": null, + "typeId": "type_:TokenRequest" + } + }, + "jsonExample": { + "client_id": "client_id", + "client_secret": "client_secret" + } + }, + "response": { + "type": "ok", + "value": { + "type": "body", + "value": { + "shape": { + "type": "named", + "shape": { + "type": "object", + "properties": [ + { + "name": { + "name": { + "originalName": "access_token", + "camelCase": { + "unsafeName": "accessToken", + "safeName": "accessToken" + }, + "snakeCase": { + "unsafeName": "access_token", + "safeName": "access_token" + }, + "screamingSnakeCase": { + "unsafeName": "ACCESS_TOKEN", + "safeName": "ACCESS_TOKEN" + }, + "pascalCase": { + "unsafeName": "AccessToken", + "safeName": "AccessToken" + } + }, + "wireValue": "access_token" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "TokenResponse", + "camelCase": { + "unsafeName": "tokenResponse", + "safeName": "tokenResponse" + }, + "snakeCase": { + "unsafeName": "token_response", + "safeName": "token_response" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN_RESPONSE", + "safeName": "TOKEN_RESPONSE" + }, + "pascalCase": { + "unsafeName": "TokenResponse", + "safeName": "TokenResponse" + } + }, + "fernFilepath": { + "allParts": [], + "packagePath": [], + "file": null + }, + "displayName": null, + "typeId": "type_:TokenResponse" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "string", + "string": { + "original": "access_token" + } + } + }, + "jsonExample": "access_token" + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "STRING", + "v2": { + "type": "string", + "default": null, + "validation": null + } + } + } + } + }, + "jsonExample": "access_token" + }, + "propertyAccess": null + }, + { + "name": { + "name": { + "originalName": "expires_in", + "camelCase": { + "unsafeName": "expiresIn", + "safeName": "expiresIn" + }, + "snakeCase": { + "unsafeName": "expires_in", + "safeName": "expires_in" + }, + "screamingSnakeCase": { + "unsafeName": "EXPIRES_IN", + "safeName": "EXPIRES_IN" + }, + "pascalCase": { + "unsafeName": "ExpiresIn", + "safeName": "ExpiresIn" + } + }, + "wireValue": "expires_in" + }, + "originalTypeDeclaration": { + "name": { + "originalName": "TokenResponse", + "camelCase": { + "unsafeName": "tokenResponse", + "safeName": "tokenResponse" + }, + "snakeCase": { + "unsafeName": "token_response", + "safeName": "token_response" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN_RESPONSE", + "safeName": "TOKEN_RESPONSE" + }, + "pascalCase": { + "unsafeName": "TokenResponse", + "safeName": "TokenResponse" + } + }, + "fernFilepath": { + "allParts": [], + "packagePath": [], + "file": null + }, + "displayName": null, + "typeId": "type_:TokenResponse" + }, + "value": { + "shape": { + "type": "container", + "container": { + "type": "optional", + "optional": { + "shape": { + "type": "primitive", + "primitive": { + "type": "integer", + "integer": 1 + } + }, + "jsonExample": 1 + }, + "valueType": { + "_type": "primitive", + "primitive": { + "v1": "INTEGER", + "v2": { + "type": "integer", + "default": null, + "validation": null + } + } + } + } + }, + "jsonExample": 1 + }, + "propertyAccess": null + } + ], + "extraProperties": null + }, + "typeName": { + "name": { + "originalName": "TokenResponse", + "camelCase": { + "unsafeName": "tokenResponse", + "safeName": "tokenResponse" + }, + "snakeCase": { + "unsafeName": "token_response", + "safeName": "token_response" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN_RESPONSE", + "safeName": "TOKEN_RESPONSE" + }, + "pascalCase": { + "unsafeName": "TokenResponse", + "safeName": "TokenResponse" + } + }, + "fernFilepath": { + "allParts": [], + "packagePath": [], + "file": null + }, + "displayName": null, + "typeId": "type_:TokenResponse" + } + }, + "jsonExample": { + "access_token": "access_token", + "expires_in": 1 + } + } + } + }, + "docs": null + } + } + ], + "pagination": null, + "transport": null, + "v2Examples": null, + "source": null, + "audiences": null, + "retries": null, + "apiPlayground": null, + "responseHeaders": [], + "availability": null, + "docs": null + } + ], + "audiences": null + } + }, + "constants": { + "errorInstanceIdKey": { + "name": { + "originalName": "errorInstanceId", + "camelCase": { + "unsafeName": "errorInstanceID", + "safeName": "errorInstanceID" + }, + "snakeCase": { + "unsafeName": "error_instance_id", + "safeName": "error_instance_id" + }, + "screamingSnakeCase": { + "unsafeName": "ERROR_INSTANCE_ID", + "safeName": "ERROR_INSTANCE_ID" + }, + "pascalCase": { + "unsafeName": "ErrorInstanceID", + "safeName": "ErrorInstanceID" + } + }, + "wireValue": "errorInstanceId" + } + }, + "environments": null, + "errorDiscriminationStrategy": { + "type": "statusCode" + }, + "basePath": null, + "pathParameters": [], + "variables": [], + "serviceTypeReferenceInfo": { + "typesReferencedOnlyByService": { + "service_": [ + "type_:PostSubmitResponse", + "type_:TokenRequest", + "type_:TokenResponse" + ] + }, + "sharedTypes": [] + }, + "webhookGroups": {}, + "websocketChannels": {}, + "readmeConfig": null, + "sourceConfig": null, + "publishConfig": null, + "dynamic": { + "version": "1.0.0", + "types": { + "type_:PostSubmitResponse": { + "type": "object", + "declaration": { "name": { "originalName": "PostSubmitResponse", "camelCase": { @@ -1275,6 +2570,198 @@ ], "extends": null, "additionalProperties": false + }, + "type_:TokenRequest": { + "type": "object", + "declaration": { + "name": { + "originalName": "TokenRequest", + "camelCase": { + "unsafeName": "tokenRequest", + "safeName": "tokenRequest" + }, + "snakeCase": { + "unsafeName": "token_request", + "safeName": "token_request" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN_REQUEST", + "safeName": "TOKEN_REQUEST" + }, + "pascalCase": { + "unsafeName": "TokenRequest", + "safeName": "TokenRequest" + } + }, + "fernFilepath": { + "allParts": [], + "packagePath": [], + "file": null + } + }, + "properties": [ + { + "name": { + "name": { + "originalName": "client_id", + "camelCase": { + "unsafeName": "clientID", + "safeName": "clientID" + }, + "snakeCase": { + "unsafeName": "client_id", + "safeName": "client_id" + }, + "screamingSnakeCase": { + "unsafeName": "CLIENT_ID", + "safeName": "CLIENT_ID" + }, + "pascalCase": { + "unsafeName": "ClientID", + "safeName": "ClientID" + } + }, + "wireValue": "client_id" + }, + "typeReference": { + "type": "primitive", + "value": "STRING" + }, + "propertyAccess": null, + "variable": null + }, + { + "name": { + "name": { + "originalName": "client_secret", + "camelCase": { + "unsafeName": "clientSecret", + "safeName": "clientSecret" + }, + "snakeCase": { + "unsafeName": "client_secret", + "safeName": "client_secret" + }, + "screamingSnakeCase": { + "unsafeName": "CLIENT_SECRET", + "safeName": "CLIENT_SECRET" + }, + "pascalCase": { + "unsafeName": "ClientSecret", + "safeName": "ClientSecret" + } + }, + "wireValue": "client_secret" + }, + "typeReference": { + "type": "primitive", + "value": "STRING" + }, + "propertyAccess": null, + "variable": null + } + ], + "extends": null, + "additionalProperties": false + }, + "type_:TokenResponse": { + "type": "object", + "declaration": { + "name": { + "originalName": "TokenResponse", + "camelCase": { + "unsafeName": "tokenResponse", + "safeName": "tokenResponse" + }, + "snakeCase": { + "unsafeName": "token_response", + "safeName": "token_response" + }, + "screamingSnakeCase": { + "unsafeName": "TOKEN_RESPONSE", + "safeName": "TOKEN_RESPONSE" + }, + "pascalCase": { + "unsafeName": "TokenResponse", + "safeName": "TokenResponse" + } + }, + "fernFilepath": { + "allParts": [], + "packagePath": [], + "file": null + } + }, + "properties": [ + { + "name": { + "name": { + "originalName": "access_token", + "camelCase": { + "unsafeName": "accessToken", + "safeName": "accessToken" + }, + "snakeCase": { + "unsafeName": "access_token", + "safeName": "access_token" + }, + "screamingSnakeCase": { + "unsafeName": "ACCESS_TOKEN", + "safeName": "ACCESS_TOKEN" + }, + "pascalCase": { + "unsafeName": "AccessToken", + "safeName": "AccessToken" + } + }, + "wireValue": "access_token" + }, + "typeReference": { + "type": "optional", + "value": { + "type": "primitive", + "value": "STRING" + } + }, + "propertyAccess": null, + "variable": null + }, + { + "name": { + "name": { + "originalName": "expires_in", + "camelCase": { + "unsafeName": "expiresIn", + "safeName": "expiresIn" + }, + "snakeCase": { + "unsafeName": "expires_in", + "safeName": "expires_in" + }, + "screamingSnakeCase": { + "unsafeName": "EXPIRES_IN", + "safeName": "EXPIRES_IN" + }, + "pascalCase": { + "unsafeName": "ExpiresIn", + "safeName": "ExpiresIn" + } + }, + "wireValue": "expires_in" + }, + "typeReference": { + "type": "optional", + "value": { + "type": "primitive", + "value": "INTEGER" + } + }, + "propertyAccess": null, + "variable": null + } + ], + "extends": null, + "additionalProperties": false } }, "headers": [], @@ -1416,6 +2903,54 @@ "type": "json" }, "examples": null + }, + "endpoint_.get_token": { + "auth": null, + "declaration": { + "name": { + "originalName": "get_token", + "camelCase": { + "unsafeName": "getToken", + "safeName": "getToken" + }, + "snakeCase": { + "unsafeName": "get_token", + "safeName": "get_token" + }, + "screamingSnakeCase": { + "unsafeName": "GET_TOKEN", + "safeName": "GET_TOKEN" + }, + "pascalCase": { + "unsafeName": "GetToken", + "safeName": "GetToken" + } + }, + "fernFilepath": { + "allParts": [], + "packagePath": [], + "file": null + } + }, + "location": { + "method": "POST", + "path": "/token" + }, + "request": { + "type": "body", + "pathParameters": [], + "body": { + "type": "typeReference", + "value": { + "type": "named", + "value": "type_:TokenRequest" + } + } + }, + "response": { + "type": "json" + }, + "examples": null } }, "pathParameters": [], @@ -1436,7 +2971,9 @@ "websocket": null, "service": "service_", "types": [ - "type_:PostSubmitResponse" + "type_:PostSubmitResponse", + "type_:TokenRequest", + "type_:TokenResponse" ], "errors": [], "subpackages": [], From c8c4634fa68280d08032b1eb11d82d67100472e1 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 18 Mar 2026 23:19:03 +0000 Subject: [PATCH 08/12] refactor: remove dead convertProtobufToOpenAPI and unused bufLockContents from generateFromPrepared Co-Authored-By: Niels Swimberghe <3382717+Swimburger@users.noreply.github.com> --- .../src/protobuf/ProtobufOpenAPIGenerator.ts | 7 ++--- .../src/utils/getAllOpenAPISpecs.ts | 31 +------------------ 2 files changed, 3 insertions(+), 35 deletions(-) diff --git a/packages/cli/workspace/lazy-fern-workspace/src/protobuf/ProtobufOpenAPIGenerator.ts b/packages/cli/workspace/lazy-fern-workspace/src/protobuf/ProtobufOpenAPIGenerator.ts index be7b5dff24cd..f03537877450 100644 --- a/packages/cli/workspace/lazy-fern-workspace/src/protobuf/ProtobufOpenAPIGenerator.ts +++ b/packages/cli/workspace/lazy-fern-workspace/src/protobuf/ProtobufOpenAPIGenerator.ts @@ -187,7 +187,7 @@ export class ProtobufOpenAPIGenerator { preparedDir: PreparedWorkingDir; absoluteFilepathToProtobufRoot: AbsoluteFilePath; absoluteFilepathToProtobufTarget: AbsoluteFilePath; - }): Promise<{ absoluteFilepath: AbsoluteFilePath; bufLockContents: string | undefined }> { + }): Promise<{ absoluteFilepath: AbsoluteFilePath }> { const target = relative(absoluteFilepathToProtobufRoot, absoluteFilepathToProtobufTarget); const buf = createLoggingExecutable("buf", { cwd: preparedDir.cwd, @@ -207,10 +207,7 @@ export class ProtobufOpenAPIGenerator { const uniqueOutput = AbsoluteFilePath.of((await tmp.file({ postfix: ".yaml" })).path); await rename(outputPath, uniqueOutput); - return { - absoluteFilepath: uniqueOutput, - bufLockContents: preparedDir.bufLockContents - }; + return { absoluteFilepath: uniqueOutput }; } private async generateLocal({ diff --git a/packages/cli/workspace/lazy-fern-workspace/src/utils/getAllOpenAPISpecs.ts b/packages/cli/workspace/lazy-fern-workspace/src/utils/getAllOpenAPISpecs.ts index 2b895677197b..4f49b236ffea 100644 --- a/packages/cli/workspace/lazy-fern-workspace/src/utils/getAllOpenAPISpecs.ts +++ b/packages/cli/workspace/lazy-fern-workspace/src/utils/getAllOpenAPISpecs.ts @@ -80,42 +80,13 @@ export async function getAllOpenAPISpecs({ return [...openApiSpecs, ...openApiSpecsFromProto]; } -export async function convertProtobufToOpenAPI({ - generator, - protobufSpec, - relativePathToDependency -}: { - generator: ProtobufOpenAPIGenerator; - protobufSpec: ProtobufSpec; - relativePathToDependency?: RelativeFilePath; -}): Promise<{ openApiSpec: OpenAPISpec } | undefined> { - if (protobufSpec.absoluteFilepathToProtobufTarget == null) { - return undefined; - } - const result = await generator.generate({ - absoluteFilepathToProtobufRoot: protobufSpec.absoluteFilepathToProtobufRoot, - absoluteFilepathToProtobufTarget: protobufSpec.absoluteFilepathToProtobufTarget, - relativeFilepathToProtobufRoot: protobufSpec.relativeFilepathToProtobufRoot, - local: protobufSpec.generateLocally, - deps: protobufSpec.dependencies - }); - return { - openApiSpec: makeOpenApiSpec({ - result, - protobufSpec, - relativePathToDependency, - target: protobufSpec.absoluteFilepathToProtobufTarget - }) - }; -} - function makeOpenApiSpec({ result, protobufSpec, relativePathToDependency, target }: { - result: { absoluteFilepath: AbsoluteFilePath; bufLockContents: string | undefined }; + result: { absoluteFilepath: AbsoluteFilePath }; protobufSpec: ProtobufSpec; relativePathToDependency?: RelativeFilePath; target: AbsoluteFilePath; From b40a0a98e7153b8672cacda9da7c02d8db190320 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 18 Mar 2026 23:46:17 +0000 Subject: [PATCH 09/12] fix: cache protobuf OpenAPI specs to avoid duplicate buf generate passes Co-Authored-By: Niels Swimberghe <3382717+Swimburger@users.noreply.github.com> --- .../lazy-fern-workspace/src/OSSWorkspace.ts | 31 +++++++++++++++++-- .../oss-validator/src/validateOSSWorkspace.ts | 4 +-- 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/packages/cli/workspace/lazy-fern-workspace/src/OSSWorkspace.ts b/packages/cli/workspace/lazy-fern-workspace/src/OSSWorkspace.ts index 29bee9c29c13..8fd6513f6ee6 100644 --- a/packages/cli/workspace/lazy-fern-workspace/src/OSSWorkspace.ts +++ b/packages/cli/workspace/lazy-fern-workspace/src/OSSWorkspace.ts @@ -94,6 +94,11 @@ export class OSSWorkspace extends BaseOpenAPIWorkspace { private graphqlOperations: Record = {}; private graphqlTypes: Record = {}; + // Cache for protobuf → OpenAPI generation results, keyed by relativePathToDependency. + // This avoids running buf generate twice when both toFernWorkspace() and + // validateOSSWorkspace() need the same OpenAPI specs. + private openApiSpecsCache: Map> = new Map(); + constructor({ allSpecs, specs, ...superArgs }: OSSWorkspace.Args) { const openapiSpecs = specs.filter((spec) => spec.type === "openapi" && spec.source.type === "openapi"); super({ @@ -214,6 +219,28 @@ export class OSSWorkspace extends BaseOpenAPIWorkspace { } } + /** + * Returns cached OpenAPI specs (including protobuf → OpenAPI conversions). + * The expensive buf generate work is performed once per unique + * relativePathToDependency and reused across getOpenAPIIr(), + * getIntermediateRepresentation(), and workspace validation. + */ + public getOpenAPISpecsCached({ + context, + relativePathToDependency + }: { + context: TaskContext; + relativePathToDependency?: RelativeFilePath; + }): Promise { + const key = relativePathToDependency ?? ""; + let cached = this.openApiSpecsCache.get(key); + if (cached == null) { + cached = getAllOpenAPISpecs({ context, specs: this.specs, relativePathToDependency }); + this.openApiSpecsCache.set(key, cached); + } + return cached; + } + public async getOpenAPIIr( { context, @@ -226,7 +253,7 @@ export class OSSWorkspace extends BaseOpenAPIWorkspace { }, settings?: OSSWorkspace.Settings ): Promise { - const openApiSpecs = await getAllOpenAPISpecs({ context, specs: this.specs, relativePathToDependency }); + const openApiSpecs = await this.getOpenAPISpecsCached({ context, relativePathToDependency }); return parse({ context, documents: await this.loader.loadDocuments({ @@ -261,7 +288,7 @@ export class OSSWorkspace extends BaseOpenAPIWorkspace { // Start protobuf IR generation in parallel with OpenAPI processing const protobufIRResultsPromise = this.generateAllProtobufIRs({ context }); - const specs = await getAllOpenAPISpecs({ context, specs: this.specs }); + const specs = await this.getOpenAPISpecsCached({ context }); const documents = await this.loader.loadDocuments({ context, specs }); const authOverrides = diff --git a/packages/cli/workspace/oss-validator/src/validateOSSWorkspace.ts b/packages/cli/workspace/oss-validator/src/validateOSSWorkspace.ts index 7e644864d173..f061bbdb0367 100644 --- a/packages/cli/workspace/oss-validator/src/validateOSSWorkspace.ts +++ b/packages/cli/workspace/oss-validator/src/validateOSSWorkspace.ts @@ -1,4 +1,4 @@ -import { getAllOpenAPISpecs, OSSWorkspace } from "@fern-api/lazy-fern-workspace"; +import { OSSWorkspace } from "@fern-api/lazy-fern-workspace"; import { TaskContext } from "@fern-api/task-context"; import { getAllRules } from "./getAllRules.js"; import { Rule } from "./Rule.js"; @@ -20,7 +20,7 @@ export async function runRulesOnOSSWorkspace({ context: TaskContext; rules: Rule[]; }): Promise { - const openApiSpecs = await getAllOpenAPISpecs({ context, specs: workspace.specs }); + const openApiSpecs = await workspace.getOpenAPISpecsCached({ context }); const ruleResults = await Promise.all( rules.map(async (rule) => { const violations = await rule.run({ workspace, specs: openApiSpecs, context }); From be7b150cc4fb8b31e94a6c977e66f01fa9acf1b2 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 19 Mar 2026 01:14:31 +0000 Subject: [PATCH 10/12] fix: integrate buf auto-download into prepare() path and remove unused bufLockContents from PreparedWorkingDir Co-Authored-By: Niels Swimberghe <3382717+Swimburger@users.noreply.github.com> --- .../src/protobuf/ProtobufOpenAPIGenerator.ts | 58 ++++--------------- 1 file changed, 11 insertions(+), 47 deletions(-) diff --git a/packages/cli/workspace/lazy-fern-workspace/src/protobuf/ProtobufOpenAPIGenerator.ts b/packages/cli/workspace/lazy-fern-workspace/src/protobuf/ProtobufOpenAPIGenerator.ts index 3be6bd77e9c6..91646a27a496 100644 --- a/packages/cli/workspace/lazy-fern-workspace/src/protobuf/ProtobufOpenAPIGenerator.ts +++ b/packages/cli/workspace/lazy-fern-workspace/src/protobuf/ProtobufOpenAPIGenerator.ts @@ -17,7 +17,6 @@ const PROTOBUF_GENERATOR_OUTPUT_FILEPATH = `${PROTOBUF_GENERATOR_OUTPUT_PATH}/op */ interface PreparedWorkingDir { cwd: AbsoluteFilePath; - bufLockContents: string | undefined; envOverride: Record | undefined; } @@ -80,10 +79,15 @@ export class ProtobufOpenAPIGenerator { this.context.failAndThrow("Remote Protobuf generation is unimplemented."); } + // Resolve buf and protoc-gen-openapi binaries once + await this.ensureBufResolved(); + await this.ensureProtocGenOpenAPIResolved(); + if (deps.length > 0 && this.isAirGapped === undefined) { this.isAirGapped = await detectAirGappedModeForProtobuf( absoluteFilepathToProtobufRoot, - this.context.logger + this.context.logger, + this.resolvedBufCommand ); } @@ -94,41 +98,6 @@ export class ProtobufOpenAPIGenerator { getProtobufGeneratorConfig({ relativeFilepathToProtobufRoot }) ); - // Check required binaries once - const which = createLoggingExecutable("which", { - cwd, - logger: undefined, - doNotPipeOutput: true - }); - try { - await which(["buf"]); - } catch (err) { - this.context.failAndThrow( - "Missing required dependency; please install 'buf' to continue (e.g. 'brew install buf')." - ); - } - - let protocGenOpenAPIOnPath = false; - try { - await which(["protoc-gen-openapi"]); - protocGenOpenAPIOnPath = true; - } catch (err) { - this.context.logger.debug( - `protoc-gen-openapi not found on PATH: ${err instanceof Error ? err.message : String(err)}` - ); - } - - if (!protocGenOpenAPIOnPath) { - if (this.protocGenOpenAPIBinDir == null) { - this.protocGenOpenAPIBinDir = await resolveProtocGenOpenAPI(this.context.logger); - } - if (this.protocGenOpenAPIBinDir == null) { - this.context.failAndThrow( - "Missing required dependency; please install 'protoc-gen-openapi' to continue (e.g. 'brew install go && go install github.com/fern-api/protoc-gen-openapi/cmd/protoc-gen-openapi@latest')." - ); - } - } - // If we downloaded protoc-gen-openapi, prepend its directory to PATH so buf can find it const envOverride = this.protocGenOpenAPIBinDir != null @@ -140,7 +109,6 @@ export class ProtobufOpenAPIGenerator { const bufLockPath = join(cwd, RelativeFilePath.of("buf.lock")); await writeFile(bufYamlPath, getProtobufYamlV1(deps)); - let bufLockContents: string | undefined; if (deps.length > 0) { if (this.isAirGapped) { this.context.logger.debug("Air-gapped mode: skipping buf dep update"); @@ -152,7 +120,8 @@ export class ProtobufOpenAPIGenerator { ); } } else { - const buf = createLoggingExecutable("buf", { + const bufCommand = this.resolvedBufCommand ?? "buf"; + const buf = createLoggingExecutable(bufCommand, { cwd, logger: this.context.logger, stdout: "ignore", @@ -161,15 +130,9 @@ export class ProtobufOpenAPIGenerator { }); await buf(["dep", "update"]); } - try { - bufLockContents = await readFile(bufLockPath, "utf-8"); - } catch (err) { - this.context.logger.debug(`Failed to read buf.lock: ${err}`); - bufLockContents = undefined; - } } - return { cwd, bufLockContents, envOverride }; + return { cwd, envOverride }; } /** @@ -191,7 +154,8 @@ export class ProtobufOpenAPIGenerator { absoluteFilepathToProtobufTarget: AbsoluteFilePath; }): Promise<{ absoluteFilepath: AbsoluteFilePath }> { const target = relative(absoluteFilepathToProtobufRoot, absoluteFilepathToProtobufTarget); - const buf = createLoggingExecutable("buf", { + const bufCommand = this.resolvedBufCommand ?? "buf"; + const buf = createLoggingExecutable(bufCommand, { cwd: preparedDir.cwd, logger: this.context.logger, stdout: "ignore", From b511f649d903185d9ffa4adc3f6e0cf7ec0878a4 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 19 Mar 2026 01:18:34 +0000 Subject: [PATCH 11/12] chore: remove orphaned seed folders (pnpm seed clean) Co-Authored-By: Niels Swimberghe <3382717+Swimburger@users.noreply.github.com> --- .../basic-auth/.github/workflows/ci.yml | 52 ------------------- .../.github/workflows/ci.yml | 52 ------------------- .../.github/workflows/ci.yml | 52 ------------------- 3 files changed, 156 deletions(-) delete mode 100644 seed/csharp-sdk/basic-auth/.github/workflows/ci.yml delete mode 100644 seed/csharp-sdk/bearer-token-environment-variable/.github/workflows/ci.yml delete mode 100644 seed/csharp-sdk/oauth-client-credentials-mandatory-auth/.github/workflows/ci.yml diff --git a/seed/csharp-sdk/basic-auth/.github/workflows/ci.yml b/seed/csharp-sdk/basic-auth/.github/workflows/ci.yml deleted file mode 100644 index 7788a0a054a7..000000000000 --- a/seed/csharp-sdk/basic-auth/.github/workflows/ci.yml +++ /dev/null @@ -1,52 +0,0 @@ -name: ci - -on: [push] - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: false - -env: - DOTNET_NOLOGO: true - -jobs: - ci: - runs-on: ubuntu-latest - - steps: - - name: Checkout repo - uses: actions/checkout@v6 - - - name: Setup .NET - uses: actions/setup-dotnet@v5 - with: - dotnet-version: 10.x - - - name: Install tools - run: dotnet tool restore - - - name: Restore dependencies - run: dotnet restore src/SeedBasicAuth/SeedBasicAuth.csproj - - - name: Build - run: dotnet build src/SeedBasicAuth/SeedBasicAuth.csproj --no-restore -c Release - - - name: Restore test dependencies - run: dotnet restore src/SeedBasicAuth.Test/SeedBasicAuth.Test.csproj - - - name: Build tests - run: dotnet build src/SeedBasicAuth.Test/SeedBasicAuth.Test.csproj --no-restore -c Release - - - name: Test - run: dotnet test src/SeedBasicAuth.Test/SeedBasicAuth.Test.csproj --no-restore --no-build -c Release - - - name: Pack - if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') - run: dotnet pack src/SeedBasicAuth/SeedBasicAuth.csproj --no-build --no-restore -c Release - - - name: Publish to NuGet.org - if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') - env: - NUGET_API_KEY: ${{ secrets.NUGET_API_TOKEN }} - run: dotnet nuget push src/SeedBasicAuth/bin/Release/*.nupkg --api-key $NUGET_API_KEY --source "nuget.org" - diff --git a/seed/csharp-sdk/bearer-token-environment-variable/.github/workflows/ci.yml b/seed/csharp-sdk/bearer-token-environment-variable/.github/workflows/ci.yml deleted file mode 100644 index 40730a91452f..000000000000 --- a/seed/csharp-sdk/bearer-token-environment-variable/.github/workflows/ci.yml +++ /dev/null @@ -1,52 +0,0 @@ -name: ci - -on: [push] - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: false - -env: - DOTNET_NOLOGO: true - -jobs: - ci: - runs-on: ubuntu-latest - - steps: - - name: Checkout repo - uses: actions/checkout@v6 - - - name: Setup .NET - uses: actions/setup-dotnet@v5 - with: - dotnet-version: 10.x - - - name: Install tools - run: dotnet tool restore - - - name: Restore dependencies - run: dotnet restore src/SeedBearerTokenEnvironmentVariable/SeedBearerTokenEnvironmentVariable.csproj - - - name: Build - run: dotnet build src/SeedBearerTokenEnvironmentVariable/SeedBearerTokenEnvironmentVariable.csproj --no-restore -c Release - - - name: Restore test dependencies - run: dotnet restore src/SeedBearerTokenEnvironmentVariable.Test/SeedBearerTokenEnvironmentVariable.Test.csproj - - - name: Build tests - run: dotnet build src/SeedBearerTokenEnvironmentVariable.Test/SeedBearerTokenEnvironmentVariable.Test.csproj --no-restore -c Release - - - name: Test - run: dotnet test src/SeedBearerTokenEnvironmentVariable.Test/SeedBearerTokenEnvironmentVariable.Test.csproj --no-restore --no-build -c Release - - - name: Pack - if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') - run: dotnet pack src/SeedBearerTokenEnvironmentVariable/SeedBearerTokenEnvironmentVariable.csproj --no-build --no-restore -c Release - - - name: Publish to NuGet.org - if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') - env: - NUGET_API_KEY: ${{ secrets.NUGET_API_TOKEN }} - run: dotnet nuget push src/SeedBearerTokenEnvironmentVariable/bin/Release/*.nupkg --api-key $NUGET_API_KEY --source "nuget.org" - diff --git a/seed/csharp-sdk/oauth-client-credentials-mandatory-auth/.github/workflows/ci.yml b/seed/csharp-sdk/oauth-client-credentials-mandatory-auth/.github/workflows/ci.yml deleted file mode 100644 index 0eafcd059722..000000000000 --- a/seed/csharp-sdk/oauth-client-credentials-mandatory-auth/.github/workflows/ci.yml +++ /dev/null @@ -1,52 +0,0 @@ -name: ci - -on: [push] - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: false - -env: - DOTNET_NOLOGO: true - -jobs: - ci: - runs-on: ubuntu-latest - - steps: - - name: Checkout repo - uses: actions/checkout@v6 - - - name: Setup .NET - uses: actions/setup-dotnet@v5 - with: - dotnet-version: 10.x - - - name: Install tools - run: dotnet tool restore - - - name: Restore dependencies - run: dotnet restore src/SeedOauthClientCredentialsMandatoryAuth/SeedOauthClientCredentialsMandatoryAuth.csproj - - - name: Build - run: dotnet build src/SeedOauthClientCredentialsMandatoryAuth/SeedOauthClientCredentialsMandatoryAuth.csproj --no-restore -c Release - - - name: Restore test dependencies - run: dotnet restore src/SeedOauthClientCredentialsMandatoryAuth.Test/SeedOauthClientCredentialsMandatoryAuth.Test.csproj - - - name: Build tests - run: dotnet build src/SeedOauthClientCredentialsMandatoryAuth.Test/SeedOauthClientCredentialsMandatoryAuth.Test.csproj --no-restore -c Release - - - name: Test - run: dotnet test src/SeedOauthClientCredentialsMandatoryAuth.Test/SeedOauthClientCredentialsMandatoryAuth.Test.csproj --no-restore --no-build -c Release - - - name: Pack - if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') - run: dotnet pack src/SeedOauthClientCredentialsMandatoryAuth/SeedOauthClientCredentialsMandatoryAuth.csproj --no-build --no-restore -c Release - - - name: Publish to NuGet.org - if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') - env: - NUGET_API_KEY: ${{ secrets.NUGET_API_TOKEN }} - run: dotnet nuget push src/SeedOauthClientCredentialsMandatoryAuth/bin/Release/*.nupkg --api-key $NUGET_API_KEY --source "nuget.org" - From 3b85b3fd3c2d10ccf694e12fd3f4b3d15451a326 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 19 Mar 2026 01:21:57 +0000 Subject: [PATCH 12/12] feat: add debug logging when buf and protoc-gen-openapi are found on PATH Co-Authored-By: Niels Swimberghe <3382717+Swimburger@users.noreply.github.com> --- .../src/protobuf/ProtobufOpenAPIGenerator.ts | 8 +++++++- .../workspace/lazy-fern-workspace/src/protobuf/utils.ts | 9 ++++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/packages/cli/workspace/lazy-fern-workspace/src/protobuf/ProtobufOpenAPIGenerator.ts b/packages/cli/workspace/lazy-fern-workspace/src/protobuf/ProtobufOpenAPIGenerator.ts index 91646a27a496..915ff15d4cb5 100644 --- a/packages/cli/workspace/lazy-fern-workspace/src/protobuf/ProtobufOpenAPIGenerator.ts +++ b/packages/cli/workspace/lazy-fern-workspace/src/protobuf/ProtobufOpenAPIGenerator.ts @@ -328,7 +328,13 @@ export class ProtobufOpenAPIGenerator { }); try { - await which(["protoc-gen-openapi"]); + const result = await which(["protoc-gen-openapi"]); + const resolvedPath = result.stdout?.trim(); + if (resolvedPath) { + this.context.logger.debug(`Found protoc-gen-openapi on PATH: ${resolvedPath}`); + } else { + this.context.logger.debug("Found protoc-gen-openapi on PATH"); + } this.protocGenOpenAPIResolved = true; } catch { this.context.logger.debug("protoc-gen-openapi not found on PATH, attempting auto-download"); diff --git a/packages/cli/workspace/lazy-fern-workspace/src/protobuf/utils.ts b/packages/cli/workspace/lazy-fern-workspace/src/protobuf/utils.ts index 5ff2dd30077c..575c8ca2c605 100644 --- a/packages/cli/workspace/lazy-fern-workspace/src/protobuf/utils.ts +++ b/packages/cli/workspace/lazy-fern-workspace/src/protobuf/utils.ts @@ -153,12 +153,19 @@ export async function ensureBufCommand(logger: Logger): Promise { }); try { - await which(["buf"]); + const result = await which(["buf"]); + const bufPath = result.stdout?.trim(); + if (bufPath) { + logger.debug(`Found buf on PATH: ${bufPath}`); + } else { + logger.debug("Found buf on PATH"); + } return "buf"; } catch { logger.debug("buf not found on PATH, attempting auto-download"); const downloadedBufPath = await resolveBuf(logger); if (downloadedBufPath != null) { + logger.debug(`Using auto-downloaded buf: ${downloadedBufPath}`); return downloadedBufPath; } throw new Error("Missing required dependency; please install 'buf' to continue (e.g. 'brew install buf').");