Skip to content

fix(cli): init refuses a name too long for a folder in one plain sentence - #5071

Merged
miguel-heygen merged 2 commits into
mainfrom
fix/cli-init-long-name
Oct 6, 2026
Merged

miguel-heygen merged 2 commits into
mainfrom
fix/cli-init-long-name

Conversation

@miguel-heygen

@miguel-heygen miguel-heygen commented Oct 5, 2026 •

Copy link
Copy Markdown
Collaborator

What

hyperframes init refuses a project name that is too long for a folder, with one plain sentence that names the limit and the name's length, before it creates anything:

That name is 256 bytes long; a folder name can be at most 255 bytes.

Why

A name longer than the disk's folder-name limit reached mkdirSync, and the command exited with Node's raw error and the full path:

ENAMETOOLONG: name too long, mkdir '<cwd>/aaaa…'

Repro on 0.8.126 and on current main: npx hyperframes init $(printf 'a%.0s' $(seq 300)) --example blank --non-interactive.

Related work

None found.

How

  • nameTooLongMessage(name, platform) in packages/cli/src/commands/init.ts checks each folder in the resolved destination (a nested name like parent/<name> is held to the limit per folder, not as a whole path) and measures it the way the disk does: UTF-8 bytes on Linux, UTF-16 units on macOS and Windows. The limit is 255 for all three.
  • Both name paths validate the resolved destination before any disk work, so a long component removed by .. cannot reject a valid destination: non-interactive mode prints the sentence with the usual error style; interactive mode prints it through the prompt UI and cancels. Both exit 1 and leave no folder behind.
  • The sentence never echoes the name or a path.

Test plan

  • Unit tests added/updated
  • Manual testing performed
  • Documentation updated (if applicable)
  • Comments follow CONTRIBUTING.md "Comments": they say why, not what, and a bug fix says what the code must do and how to reproduce the bug

Ran on Linux:

  • New tests in init.test.ts, written first and red before the fix (the over-limit run printed the raw ENAMETOOLONG with the path):
    • a 256-character name exits 1 with the sentence, no ENAMETOOLONG, no path, and no folder created;
    • a 255-character name scaffolds;
    • the per-platform rule: 128 é (256 bytes, 128 characters) is refused on Linux and accepted on macOS, 127 é is accepted on Linux, a nested name is held to the limit per folder.
  • Final focused checks: init.test.ts passed 34 tests and init.interactive.test.ts passed 2 tests, each run alone three times with one worker. Every run exited 0 with no skipped tests.
  • Measuring by characters on Linux makes the per-platform test fail.
  • Interactive mode, driven under a pseudo-terminal with a 256-character name: the prompt shows the sentence, says "Setup cancelled.", and no folder is created.
  • tsc --noEmit in packages/cli, the pre-commit hooks (oxlint, oxfmt, fallow, typecheck), check-comment-citations and comment-ratchet pass.
  • The interactive path has its own test (init.interactive.test.ts): a long name given on the command line and one typed into the prompt each show the sentence and leave no folder. Removing the interactive check fails it.
  • A path whose oversized component is removed by .. scaffolds its resolved destination. Checking the raw name fails this regression and reports the wrong length in both interactive cases.
  • A Windows name like parent\<256 characters> is measured per folder. Splitting on / only fails that assertion.

Not exercised: a macOS or Windows run; the macOS and Windows rule is covered by the unit test only. Filesystems with a shorter limit than 255 (some encrypted or network mounts) still reach mkdirSync and its own error.

Known limits

  • Older macOS disks (HFS+) store accented letters decomposed, so a name of 128 é passes the check there and still fails with the raw error. Counting decomposed would wrongly refuse valid names on APFS, the current macOS format.
  • Linux writing to a Windows-format drive (WSL's /mnt/c, an NTFS or exFAT USB stick) is measured in bytes, so a name that would fit there can be refused. It fails with the clear sentence, never the raw error.

Why this PR is small

One user-facing fix with its tests; nothing else in flight shares its code path, so there is nothing to bundle it with.

Independent review

Audited Result
Argument parsing and both init branches Both validate the resolved destination before filesystem writes.
Interactive failures Full diagnostic asserted for positional and prompted names; the parent directory remains empty.
Failure sensitivity Removing interactive validation or Windows separator handling fails the corresponding tests; raw-path validation fails the normalization cases.

@github-actions

github-actions Bot commented Oct 5, 2026 •

Copy link
Copy Markdown

Edit accuracy: accurate 2055 (base branch 2055), smooth 1627 of those

The gate passes.
Smoothness is reported in the artifact, not gated. A case fails only if it fails 2 of 3 runs.

Quarantined, measured but not gated (0)

@miguel-heygen
miguel-heygen marked this pull request as ready for review October 5, 2026 20:56

@jrusso1020 jrusso1020 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Approving at d9ba903a.

The check measures what the disk measures. nameTooLongMessage checks each folder of the resolved destination against 255:

  • UTF-8 bytes on Linux, UTF-16 units elsewhere.
  • On Windows it splits on both \ and /.

Both init paths run it on resolve(name) before existsSync or any mkdir. So a long folder that .. removes doesn't block a valid destination, and nothing is created on refusal. The whole-path limit is not checked, which is right: Node handles long full paths on Windows, and on Linux/macOS the per-folder limit is the one people hit.

Tests:

  • init.test.ts + init.interactive.test.ts: 36/36 on Linux.
  • Run by hand against src/cli.ts:
    • 300 a prints "That name is 300 bytes long; a folder name can be at most 255 bytes."
    • 128 é prints the 256-bytes sentence.
    • In both cases nothing is created and no ENAMETOOLONG or path is printed.

Mutations: 6 of 6 turn tests red:

  • limit 256
  • >=
  • characters on Linux
  • splitting on / only
  • checking the raw name instead of the resolved one
  • dropping the interactive check

Reuse: I didn't find an existing name validator in the CLI to reuse. toPackageName only normalizes for package.json.

One small fit with existing code, not blocking: for a name typed at the prompt, clack.text takes validate, as promptForKey in auth/login.ts already uses. validate: (v) => nameTooLongMessage(resolve(v || "my-video")) ?? undefined would re-ask for the name instead of cancelling the whole setup. The positional path still needs the check as written.

Simplify: the code is 19 lines; the other 135 are tests that each pin a separate rule, so I don't see anything to cut. The other design would be catching ENAMETOOLONG from mkdir. That measures the real filesystem (HFS+, NTFS mounts, shorter limits), but a nested name can leave parent folders behind. Checking first, as here, is the better trade for "nothing is created".

Not verified by me: the macOS unit. I didn't run on macOS, and whether APFS counts UTF-16 units or UTF-8 bytes decides whether a long accented name gets this sentence or the raw error there. Either way it's no worse than today.

CI: 92 pass, 0 fail.

— Rames

@miguel-heygen
miguel-heygen added this pull request to the merge queue Oct 6, 2026
Merged via the queue into main with commit d45cb34 Oct 6, 2026
94 checks passed
@miguel-heygen
miguel-heygen deleted the fix/cli-init-long-name branch October 6, 2026 02:05
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