Skip to content

fix: run the CLI when invoked through a symlink (npx / installed bin) - #54

Merged
mpicciolli merged 4 commits into
mainfrom
fix/cli-entrypoint-symlink
Aug 14, 2026
Merged

fix: run the CLI when invoked through a symlink (npx / installed bin)#54
mpicciolli merged 4 commits into
mainfrom
fix/cli-entrypoint-symlink

Conversation

@mpicciolli

@mpicciolli mpicciolli commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator

Problem

The CLI did nothing when launched via npx or the installed binary: exit code 0, no output, no file written — not even --help. Only node dist/cli.mjs in out worked.

$ npx -y cdb-converter base.cdb base.sqlite
$ echo $?
0
$ ls base.sqlite
ls: base.sqlite: No such file or directory

Cause

The entry-point guard at the end of src/cli.ts:

const isDirectRun = typeof process.argv[1] === "string"
  && import.meta.url === pathToFileURL(resolve(process.argv[1])).href;

npm installs the bin as a symlink (node_modules/.bin/cdb-converter../cdb-converter/dist/cli.mjs), and npx goes through it. Node puts the symlink path in process.argv[1] but the real path in import.meta.url. resolve() normalises without dereferencing, so the two URLs never matched and the guard was always false — the module was imported, run() was never called, exit 0.

Verified independently with a probe script:

--- direct ---       argv1: .../real/probe.mjs   meta: file:///.../real/probe.mjs   equal: true
--- via symlink ---  argv1: .../bin/probe        meta: file:///.../real/probe.mjs   equal: false

Fix

Dereference both sides with realpathSync before comparing. The guard is repaired, not removed — importing the module as a library (import { run } from "cdb-converter") still must not run the converter. Two edge cases handled:

  • realpathSync throws on paths that do not exist → fall back to the resolved path.
  • Windows drive-letter casing can differ for the same file → compare case-insensitively on win32.

isDirectRun is now a pure exported function taking argv1 and moduleUrl.

Regression test

test/cliEntrypoint.test.ts spawns the built CLI through a symlink (bin/cdb-converter -> dist/cli.mjs, what npm creates), in a subprocess, on a real .cdb fixture, asserting the output file exists and the exit code is 0. A test calling run() in-process cannot observe this bug — which is exactly why it shipped.

Proof it catches it: with the previous code, 4 of its 5 tests fail. The fifth — the library import that must run nothing — passes in both, so the guard it protects stays covered.

It is a separate file because it is the only test in the repo that builds, writes to a temp dir and spawns subprocesses; putting its build beforeAll in test/cli.test.ts would have made the pure parseArgs unit tests depend on dist/ and pay the build on every run, including watch mode. Happy to merge the files if preferred.

Verification

Full suite: 83 tests / 11 files, all passing.

End-to-end from a temp dir — npm pack, install the tarball, run the installed binary (symlink confirmed):

Command Result
--help usage printed, exit 0
--version 0.3.0, exit 0
base.cdb base.sqlite summary + Tables : 136, 1,871,872 B written, exit 0
base.sqlite roundtrip.cdb 324,495 B written, exit 0
base.cdb (default output) base.sqlite written

Out of scope

Noted while verifying, not changed here: the tarball ships whatever dist/ is in the working tree (no prepublishOnly), so a release depends on a manual npm run build first.

npm installs the bin as a symlink (node_modules/.bin/cdb-converter ->
../cdb-converter/dist/cli.mjs) and npx goes through that link. Node puts
the symlink path in process.argv[1] but the real path in import.meta.url,
and resolve() normalises without dereferencing, so the entry-point guard
was always false: the module was imported, run() never called, exit 0 with
no output and no file written.

Dereference both sides with realpathSync before comparing. The guard is
repaired, not removed: importing the module as a library still must not
run the converter. Two edge cases are handled: realpathSync throws on
paths that do not exist (fall back to the resolved path), and Windows
drive-letter casing can differ (compare case-insensitively on win32).

Add an end-to-end regression that spawns the built CLI through a symlink,
as npm/npx do. Calling run() in-process cannot observe this bug, which is
why it shipped. Against the previous code, 4 of its 5 tests fail; the
library-import test passes in both, covering the guard it protects.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings August 14, 2026 02:38

Copilot AI 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.

Pull request overview

This PR fixes the CLI entrypoint guard so the converter actually runs when invoked via npx or an installed bin symlink, while still preventing accidental execution when the module is imported programmatically. It also adds an end-to-end regression test that exercises the “symlinked entrypoint” scenario by spawning the built CLI through a symlink.

Changes:

  • Update src/cli.ts entrypoint detection to dereference symlinks (via realpathSync) and to compare Windows paths case-insensitively.
  • Add an end-to-end Vitest suite that builds the project, creates a bin/ symlink to dist/cli.mjs, and validates conversions + --help/--version.
  • Add a “library import” check to ensure importing the CLI module doesn’t run conversion.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
src/cli.ts Fixes the direct-run guard to work when executed via a symlinked entrypoint (npx/installed bin).
test/cliEntrypoint.test.ts Adds an end-to-end regression test that spawns the built CLI through a symlink and validates expected behavior.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread test/cliEntrypoint.test.ts Outdated
Comment thread test/cliEntrypoint.test.ts Outdated
Comment thread src/cli.ts Outdated
mpicciolli and others added 2 commits August 14, 2026 15:57
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

Copilot AI 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.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

Comment thread test/cliEntrypoint.test.ts
@mpicciolli
mpicciolli merged commit c505545 into main Aug 14, 2026
6 checks passed
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