Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
d432045
feat: route fern sdk preview through Fiddle for remote generation by …
devin-ai-integration[bot] Apr 9, 2026
3a55ff6
feat(cli): route fern sdk preview through Fiddle for remote generatio…
devin-ai-integration[bot] Apr 9, 2026
8e6a69d
feat(cli): add --push-diff flag and use publishV2 by default for remo…
devin-ai-integration[bot] Apr 9, 2026
7d15e13
merge: resolve versions.yml conflict with main
devin-ai-integration[bot] Apr 9, 2026
bae186e
chore: trigger CI re-run with correct PR title
devin-ai-integration[bot] Apr 9, 2026
a0f612d
fix(cli): set isPreview=false for remote preview to avoid dry-run pub…
devin-ai-integration[bot] Apr 9, 2026
3744bab
chore(cli): remove internal Fiddle implementation details from comments
devin-ai-integration[bot] Apr 9, 2026
94266cb
fix(cli): address PR review — _other returns undefined, isPreview con…
devin-ai-integration[bot] Apr 9, 2026
a324692
fix(cli): decouple isPreview from Fiddle preview field, remove --push…
devin-ai-integration[bot] Apr 9, 2026
9b99487
fix(cli): replace fiddlePreview with publishV2 output mode check
devin-ai-integration[bot] Apr 9, 2026
4af2e13
fix(cli): simplify preview field — just send isPreview, let Fiddle ha…
devin-ai-integration[bot] Apr 9, 2026
f63ad6e
feat(cli): decouple fiddlePreview from isPreview, add --push-diff sup…
Apr 10, 2026
befa375
chore(cli): consolidate overrideGroupOutputForPreview into single fun…
Apr 10, 2026
e037457
fix(cli): gate --push-diff, add validation, clarify dryRun comment
Apr 10, 2026
dfaace3
docs(cli): improve --output and --push-diff option descriptions
Apr 10, 2026
ac0d410
feat(cli): add --local flag to fern sdk preview, make --output use re…
devin-ai-integration[bot] Apr 10, 2026
f30b0ac
docs(cli): update --push-diff help text to reference --local instead …
devin-ai-integration[bot] Apr 10, 2026
c8418f9
merge: resolve versions.yml conflict with main
devin-ai-integration[bot] Apr 10, 2026
6527d9e
fix(cli): override output mode to downloadFiles for remote disk-only …
Apr 10, 2026
e026fdf
feat(cli): add unit tests for sdk preview output mode overrides
Apr 10, 2026
9704bc1
fix(cli): remove unnecessary isPreview param from createAndStartJob
Apr 10, 2026
bb481fa
merge: resolve conflicts with main (automation flags, versions.yml)
Apr 10, 2026
89ac52f
chore(cli): remove test config files accidentally committed
Apr 10, 2026
850c744
fix(cli): remove extra destructured params from createJob inner function
Apr 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 24 additions & 2 deletions packages/cli/cli/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2447,7 +2447,27 @@ function addSdkPreviewCommand(cli: Argv<GlobalCliOptions>, cliContext: CliContex
string: true,
description:
"Output targets: filesystem paths and/or registry URLs. " +
"Omit to publish to the default preview registry and write to a temp directory."
"When omitted, publishes to the default preview registry via remote generation. " +
"Examples: --output ./out (disk only), --output https://registry.example.com (registry only), " +
"--output ./out --output https://registry.example.com (both)."
})
.option("local", {
boolean: true,
default: false,
description:
"Run generation locally via Docker instead of remotely through Fiddle. " +
"Requires Docker to be installed. " +
"Can be combined with --output for local disk output."
})
.option("push-diff", {
boolean: true,
default: false,
description:
"Push a preview diff branch (fern-preview-{version}) to the SDK repo " +
"in addition to publishing to the preview registry. " +
"Requires the generator to have github output configuration " +
"and the Fern GitHub App installed on the target repo. " +
"Cannot be combined with --local."
}),
async (argv) => {
await cliContext.instrumentPostHogEvent({
Expand All @@ -2461,7 +2481,9 @@ function addSdkPreviewCommand(cli: Argv<GlobalCliOptions>, cliContext: CliContex
generatorFilter,
apiName: argv.api,
json: argv.json,
output: argv.output
output: argv.output,
local: argv.local,
pushDiff: argv.pushDiff
});
}
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,52 @@
import type { generatorsYml } from "@fern-api/configuration-loader";
import { AbsoluteFilePath } from "@fern-api/fs-utils";
import { FernFiddle } from "@fern-fern/fiddle-sdk";
import { describe, expect, it } from "vitest";
import {
getGithubOwnerRepo,
isNpmGenerator,
overrideGroupOutputForDownload,
overrideGroupOutputForPreview
} from "../overrideOutputForPreview.js";

import { isNpmGenerator } from "../overrideOutputForPreview.js";
/**
* Creates a minimal GeneratorInvocation for testing.
* Only the fields relevant to output mode override logic are set;
* everything else uses safe defaults.
*/
function makeGenerator(
outputMode: FernFiddle.remoteGen.OutputMode,
overrides?: Partial<generatorsYml.GeneratorInvocation>
): generatorsYml.GeneratorInvocation {
return {
name: "fernapi/fern-typescript-node-sdk",
version: "0.57.10",
config: {},
outputMode,
automation: { generate: false, upgrade: false, preview: false, verify: false },
containerImage: undefined,
irVersionOverride: undefined,
absolutePathToLocalOutput: AbsoluteFilePath.of("/tmp/test-output"),
absolutePathToLocalSnippets: undefined,
keywords: undefined,
smartCasing: false,
disableExamples: false,
language: undefined,
publishMetadata: undefined,
readme: undefined,
settings: undefined,
...overrides
};
}

function makeGroup(generators: generatorsYml.GeneratorInvocation[]): generatorsYml.GeneratorGroup {
return {
groupName: "test-group",
audiences: { type: "all" },
generators,
reviewers: undefined
};
}

describe("isNpmGenerator", () => {
it("recognizes known TypeScript SDK generators", () => {
Expand Down Expand Up @@ -33,3 +79,250 @@ describe("isNpmGenerator", () => {
expect(isNpmGenerator("fernapi/fern-csharp-sdk")).toBe(false);
});
});

describe("getGithubOwnerRepo", () => {
it("returns undefined for downloadFiles", () => {
const mode = FernFiddle.remoteGen.OutputMode.downloadFiles({});
expect(getGithubOwnerRepo(mode)).toBeUndefined();
});

it("extracts owner/repo from github (v1)", () => {
const mode = FernFiddle.OutputMode.github({
owner: "fern-api",
repo: "fern-typescript-sdk"
});
expect(getGithubOwnerRepo(mode)).toEqual({ owner: "fern-api", repo: "fern-typescript-sdk" });
});

it("extracts owner/repo from githubV2 push", () => {
const mode = FernFiddle.OutputMode.githubV2(
FernFiddle.GithubOutputModeV2.push({
owner: "fern-demo",
repo: "sdk-preview-test-sdk",
branch: undefined,
license: undefined,
downloadSnippets: false
})
);
expect(getGithubOwnerRepo(mode)).toEqual({ owner: "fern-demo", repo: "sdk-preview-test-sdk" });
});

it("extracts owner/repo from githubV2 commitAndRelease", () => {
const mode = FernFiddle.OutputMode.githubV2(
FernFiddle.GithubOutputModeV2.commitAndRelease({
owner: "my-org",
repo: "my-sdk"
})
);
expect(getGithubOwnerRepo(mode)).toEqual({ owner: "my-org", repo: "my-sdk" });
});

it("extracts owner/repo from githubV2 pullRequest", () => {
const mode = FernFiddle.OutputMode.githubV2(
FernFiddle.GithubOutputModeV2.pullRequest({
owner: "acme",
repo: "acme-sdk"
})
);
expect(getGithubOwnerRepo(mode)).toEqual({ owner: "acme", repo: "acme-sdk" });
});

it("returns undefined for publish (v1)", () => {
const mode = FernFiddle.OutputMode.publish({
registryOverrides: {
npm: {
registryUrl: "https://registry.npmjs.org",
packageName: "@acme/sdk",
token: "token"
}
}
});
expect(getGithubOwnerRepo(mode)).toBeUndefined();
});

it("returns undefined for publishV2 — registry-only, no owner/repo", () => {
const mode = FernFiddle.OutputMode.publishV2(
FernFiddle.remoteGen.PublishOutputModeV2.npmOverride({
registryUrl: "https://npm.buildwithfern.com",
packageName: "@acme/sdk",
token: "token",
downloadSnippets: false
})
);
expect(getGithubOwnerRepo(mode)).toBeUndefined();
});
});

describe("overrideGroupOutputForDownload", () => {
it("overrides output mode to downloadFiles", () => {
const generator = makeGenerator(
FernFiddle.OutputMode.publishV2(
FernFiddle.remoteGen.PublishOutputModeV2.npmOverride({
registryUrl: "https://registry.npmjs.org",
packageName: "@acme/sdk",
token: "token",
downloadSnippets: false
})
)
);
const group = makeGroup([generator]);

const result = overrideGroupOutputForDownload({ group });

expect(result.generators).toHaveLength(1);
expect(result.generators[0]?.outputMode.type).toBe("downloadFiles");
expect(result.generators[0]?.absolutePathToLocalOutput).toBeUndefined();
});

it("preserves other generator fields", () => {
const generator = makeGenerator(FernFiddle.OutputMode.github({ owner: "o", repo: "r" }), {
name: "fernapi/fern-typescript-sdk",
version: "1.0.0"
});
const group = makeGroup([generator]);

const result = overrideGroupOutputForDownload({ group });

expect(result.generators[0]?.name).toBe("fernapi/fern-typescript-sdk");
expect(result.generators[0]?.version).toBe("1.0.0");
});

it("overrides all generators in the group", () => {
const gen1 = makeGenerator(FernFiddle.OutputMode.github({ owner: "o", repo: "r" }));
const gen2 = makeGenerator(
FernFiddle.OutputMode.publishV2(
FernFiddle.remoteGen.PublishOutputModeV2.npmOverride({
registryUrl: "https://npm.buildwithfern.com",
packageName: "@acme/sdk",
token: "t",
downloadSnippets: false
})
)
);
const group = makeGroup([gen1, gen2]);

const result = overrideGroupOutputForDownload({ group });

expect(result.generators).toHaveLength(2);
for (const gen of result.generators) {
expect(gen.outputMode.type).toBe("downloadFiles");
}
});
});

describe("overrideGroupOutputForPreview", () => {
const previewParams = {
packageName: "@fern-preview/acme-sdk",
token: "fern-token-123",
registryUrl: "https://npm.buildwithfern.com"
};

it("uses publishV2(npmOverride) by default (no pushDiff)", () => {
const generator = makeGenerator(FernFiddle.OutputMode.github({ owner: "o", repo: "r" }));
const group = makeGroup([generator]);

const result = overrideGroupOutputForPreview({ group, ...previewParams });

expect(result.generators).toHaveLength(1);
const outputMode = result.generators[0]?.outputMode;
expect(outputMode?.type).toBe("publishV2");
expect(result.generators[0]?.absolutePathToLocalOutput).toBeUndefined();
});

it("uses publishV2(npmOverride) when pushDiff is false", () => {
const generator = makeGenerator(
FernFiddle.OutputMode.githubV2(
FernFiddle.GithubOutputModeV2.push({
owner: "fern-demo",
repo: "sdk-repo",
branch: undefined,
license: undefined,
downloadSnippets: false
})
)
);
const group = makeGroup([generator]);

const result = overrideGroupOutputForPreview({ group, ...previewParams, pushDiff: false });

expect(result.generators[0]?.outputMode.type).toBe("publishV2");
});

it("uses githubV2(push) with publishInfo when pushDiff is true and generator has github config", () => {
const generator = makeGenerator(
FernFiddle.OutputMode.githubV2(
FernFiddle.GithubOutputModeV2.push({
owner: "fern-demo",
repo: "sdk-preview-test-sdk",
branch: undefined,
license: undefined,
downloadSnippets: false
})
)
);
const group = makeGroup([generator]);

const result = overrideGroupOutputForPreview({ group, ...previewParams, pushDiff: true });

expect(result.generators).toHaveLength(1);
const outputMode = result.generators[0]?.outputMode;
expect(outputMode?.type).toBe("githubV2");
});

it("falls back to publishV2(npmOverride) when pushDiff is true but generator has no github config", () => {
const generator = makeGenerator(
FernFiddle.OutputMode.publishV2(
FernFiddle.remoteGen.PublishOutputModeV2.npmOverride({
registryUrl: "https://registry.npmjs.org",
packageName: "@acme/sdk",
token: "original-token",
downloadSnippets: false
})
)
);
const group = makeGroup([generator]);

const result = overrideGroupOutputForPreview({ group, ...previewParams, pushDiff: true });

expect(result.generators[0]?.outputMode.type).toBe("publishV2");
});

it("falls back to publishV2(npmOverride) when pushDiff is true but generator uses downloadFiles", () => {
const generator = makeGenerator(FernFiddle.remoteGen.OutputMode.downloadFiles({}));
const group = makeGroup([generator]);

const result = overrideGroupOutputForPreview({ group, ...previewParams, pushDiff: true });

expect(result.generators[0]?.outputMode.type).toBe("publishV2");
});

it("extracts owner/repo from github v1 when pushDiff is true", () => {
const generator = makeGenerator(FernFiddle.OutputMode.github({ owner: "legacy-org", repo: "legacy-sdk" }));
const group = makeGroup([generator]);

const result = overrideGroupOutputForPreview({ group, ...previewParams, pushDiff: true });

expect(result.generators[0]?.outputMode.type).toBe("githubV2");
});

it("clears absolutePathToLocalOutput", () => {
const generator = makeGenerator(FernFiddle.OutputMode.github({ owner: "o", repo: "r" }), {
absolutePathToLocalOutput: AbsoluteFilePath.of("/tmp/original-output")
});
const group = makeGroup([generator]);

const result = overrideGroupOutputForPreview({ group, ...previewParams });

expect(result.generators[0]?.absolutePathToLocalOutput).toBeUndefined();
});

it("preserves group-level fields", () => {
const generator = makeGenerator(FernFiddle.remoteGen.OutputMode.downloadFiles({}));
const group = makeGroup([generator]);
group.groupName = "my-preview-group";

const result = overrideGroupOutputForPreview({ group, ...previewParams });

expect(result.groupName).toBe("my-preview-group");
});
});
Loading
Loading