Skip to content

feat(cli): rename --pack to --package and add --package-only - #17307

Merged
iamnamananand996 merged 2 commits into
mainfrom
devin/1785453404-pack-only
Jul 31, 2026
Merged

feat(cli): rename --pack to --package and add --package-only#17307
iamnamananand996 merged 2 commits into
mainfrom
devin/1785453404-pack-only

Conversation

@iamnamananand996

@iamnamananand996 iamnamananand996 commented Jul 30, 2026

Copy link
Copy Markdown
Member

Description

Renames the packaging flags on fern generate and adds an artifact-only mode:

  • --pack--package, --pack-mode--package-mode (no aliases kept; --pack shipped very recently and known users are migrating their scripts).
  • New --package-only: after generating and packaging, everything in the local output directory except fern-dist/ is deleted, so only the shareable artifact remains — no SDK source is left behind.
fern generate --group node-sdk-local --version 0.0.1 --package-only --package-mode docker
# → SDKs/twilio-core-ts-sdk/ contains only fern-dist/twilio-core-0.0.1.tgz

Changes Made

  • cli.ts: package, package-mode, package-only options; --package-only implies --package (shouldPackage = argv.package || argv.packageOnly); validation messages updated.
  • packLocalOutputForGroup takes packOnly; after a generator's packaging succeeds, removeEverythingExceptDist removes every entry in that output dir except fern-dist/ (per-generator, so a failed sibling keeps its source for debugging).
  • Plumbed packOnly through generateAPIWorkspaces / generateWorkspace.
  • Changelog entry under packages/cli/cli/changes/unreleased/.

Testing

  • Unit tests added/updated — 2 new tests (packOnly leaves only fern-dist/; default keeps source); 10/10 pass.
  • Manual testing completed — built the prod CLI and ran generate --group go-sdk-local --package-only against fern-demo/twilio-core-fern-config: output dir ends up containing only fern-dist/twilio-core-go-sdk-source.zip, with log "Removed generated SDK source from …; only fern-dist/ remains."
  • Updated README.md generator (not applicable)

Link to Devin session: https://app.devin.ai/sessions/0076a65e5b6743f78ba15496f2573f41
Requested by: @iamnamananand996


Open in Devin Review

@iamnamananand996 iamnamananand996 self-assigned this Jul 30, 2026
@devin-ai-integration

Copy link
Copy Markdown
Contributor

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@nitpickybot nitpickybot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AI Review Summary

Renames --pack/--pack-mode to --package/--package-mode and adds --package-only, which wipes everything except fern-dist/ in the output directory. Plumbing and tests look fine, but the new destructive cleanup deserves guardrails: it recursively deletes arbitrary user-configured output directories, and a cleanup failure is currently reported as a packaging failure. The changelog also classifies a breaking flag rename as feat.

  • 🔴 1 critical issue(s)
  • 🟡 2 warning(s)
  • 🔵 1 suggestion(s)

Comment on lines +282 to +287
for (const entry of await readdir(outputPath)) {
if (entry === PACK_OUTPUT_DIRECTORY) {
continue;
}
await rm(join(outputPath, RelativeFilePath.of(entry)), { recursive: true, force: true });
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 critical

This recursively force-deletes every entry in a user-configured output-path. If someone points a local generator at a directory that isn't exclusively generator output (a checked-out SDK repo with .git, a folder with hand-written files, ./), --package-only silently destroys it — no dry run, no confirmation, no undo.

At minimum, refuse to run when the directory looks like it contains non-generated content (e.g. a .git directory) or preserve dotfiles, and log which entries are being removed at debug level. A safety check against outputPath resolving to a repo root / home dir would also be cheap insurance.

Comment on lines +68 to +70
if (packOnly) {
await removeEverythingExceptDist({ outputPath, context });
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 warning

The cleanup is inside the packaging try, so a failed rm (very plausible in --package-mode docker, where artifacts can be root-owned — force: true doesn't help with EACCES) is reported as "Failed to package …" even though the artifact built fine, and the whole group fails. Separate the concerns:

Suggested change
if (packOnly) {
await removeEverythingExceptDist({ outputPath, context });
}
}
if (packOnly) {
try {
await removeEverythingExceptDist({ outputPath, context });
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
context.logger.warn(
`Packaged ${generator.name} but failed to remove generated source at ${outputPath}: ${message}`
);
}
}

(Note: this suggestion assumes the try/catch block is restructured so the catch precedes it — adjust placement accordingly.)

`--package-mode`, and add `--package-only`, which builds the distributable artifact into
`fern-dist/` and removes the generated SDK source, leaving only the package in the output
directory.
type: feat

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 warning

Removing --pack/--pack-mode with no alias is a breaking CLI change; type: feat will bury it in a minor release note. Use the breaking type (break) so users who scripted against --pack actually see it — or keep hidden aliases for one release.

return cliContext.failWithoutThrowing("The --pack flag cannot be used with --preview.", undefined, {
const shouldPackage = argv.package || argv.packageOnly;
if (shouldPackage && argv.preview) {
return cliContext.failWithoutThrowing("The --package flag cannot be used with --preview.", undefined, {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 suggestion

When the user passed --package-only, these messages say --package, which they never typed. Consider deriving the flag name from which option was set, e.g. const packageFlag = argv.packageOnly ? "--package-only" : "--package"; and interpolating it into the three validation messages.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 2 potential issues.

Open in Devin Review

Comment on lines +68 to +70
if (packOnly) {
await removeEverythingExceptDist({ outputPath, context });
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Swift SDK output is completely erased when only the package is requested

The whole generated Swift output folder is wiped (removeEverythingExceptDist at packages/cli/cli/src/commands/generate/packLocalOutput.ts:68-70) even though no shareable package was ever produced for Swift, so the user is left with an empty directory and no artifact.
Impact: Anyone running Swift generation with the package-only option loses their entire generated SDK and gets nothing back.

Swift packaging is a no-op warning, so nothing is created in fern-dist before deletion

In packOutputForLanguage, the swift branch (packages/cli/cli/src/commands/generate/packLocalOutput.ts:174-179) only logs a warning and returns — it never creates fern-dist/ nor any artifact. Because it doesn't throw, the packOnly path immediately runs removeEverythingExceptDist, which deletes every entry in the output dir (no fern-dist entry exists to skip), leaving the directory empty while logging "only fern-dist/ remains".

A fix would be to make packaging report whether an artifact was actually produced (or to skip the cleanup for languages with no package format), and only remove the source when fern-dist/ contains at least one artifact.

Prompt for agents
In packages/cli/cli/src/commands/generate/packLocalOutput.ts, when packOnly is set, removeEverythingExceptDist is called unconditionally after packOutputForLanguage resolves. For the `swift` language, packOutputForLanguage just logs a warning and returns without creating fern-dist/ or any artifact, so the cleanup deletes the entire generated output directory and leaves nothing behind (while logging that only fern-dist/ remains). Consider having packOutputForLanguage signal whether an artifact was produced (e.g. return a boolean or throw/skip for languages with no package format), or verify that fern-dist/ exists and is non-empty before removing anything, so a no-op packaging step can never wipe the generated source.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in a1bfc44: packOutputForLanguage now returns whether an artifact was produced (false for Swift), and --package-only only removes source when one was — otherwise it warns and keeps the generated output. Covered by a new test.

Comment on lines +282 to +288
for (const entry of await readdir(outputPath)) {
if (entry === PACK_OUTPUT_DIRECTORY) {
continue;
}
await rm(join(outputPath, RelativeFilePath.of(entry)), { recursive: true, force: true });
}
context.logger.info(`Removed generated SDK source from ${outputPath}; only ${PACK_OUTPUT_DIRECTORY}/ remains.`);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Hand-written and version-control files in the output directory are deleted along with generated code

Every entry in the output directory is deleted (rm(..., { recursive: true, force: true }) at packages/cli/cli/src/commands/generate/packLocalOutput.ts:286), including files the user authored and asked to preserve as well as version-control data, so work unrelated to the generated SDK is lost.
Impact: Users can permanently lose custom code and repository history that lived next to the generated SDK.

Mechanism: cleanup does not exclude preserved or VCS entries

removeEverythingExceptDist (packages/cli/cli/src/commands/generate/packLocalOutput.ts:282-288) iterates all entries of the output dir and skips only PACK_OUTPUT_DIRECTORY. Local-file-system generation intentionally preserves files listed in the output directory's .fernignore (custom, hand-written code), and output directories are commonly checked-in repos containing .git. Note that zipDirectory deliberately excludes .git from artifacts (packages/cli/cli/src/commands/generate/packLocalOutput.ts:245-247), showing that .git is not considered generated content, yet the cleanup removes it. Excluding .git (and, ideally, .fernignore-protected paths) would keep the flag's intent while avoiding destroying non-generated data.

Prompt for agents
removeEverythingExceptDist in packages/cli/cli/src/commands/generate/packLocalOutput.ts deletes every entry of the generator's output directory except fern-dist/. This also removes .git (output directories are often git checkouts) and files preserved via .fernignore (hand-written custom code that local generation intentionally keeps). Consider excluding .git — consistent with zipDirectory, which already skips .git when building the source zip — and consider honoring the output directory's .fernignore entries so only generated SDK source is removed.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in a1bfc44: the cleanup now always preserves .git, .fernignore, and every top-level path covered by a .fernignore entry (nested/glob entries are handled conservatively by keeping their whole top-level directory). Covered by a new test.

@iamnamananand996
iamnamananand996 merged commit a6255b2 into main Jul 31, 2026
61 checks passed
@iamnamananand996
iamnamananand996 deleted the devin/1785453404-pack-only branch July 31, 2026 00:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants