Skip to content

fix(cli): 修复符号链接下主模块判断失效 / Fix main-module detection under symlinked paths - #25

Merged
Qiuner merged 1 commit into
Qiuner:mainfrom
JillVernus:fix/symlink-main-guard
Sep 22, 2026
Merged

Qiuner merged 1 commit into
Qiuner:mainfrom
JillVernus:fix/symlink-main-guard

Conversation

@JillVernus

@JillVernus JillVernus commented Sep 21, 2026

Copy link
Copy Markdown
Contributor

Pull Request

Change

Six CLI entry points decide whether they are being run directly with:

if (process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) {

import.meta.url reports the resolved real path, while path.resolve(process.argv[1]) keeps the path
as it was invoked. When the installation directory is reached through a symlink the two differ, the
guard is false, and the CLI block never runs: the process exits 0 with no output and no output file.

Affected entry points: validate.mjs, render.mjs, render-constraints.mjs,
discover-constraints.mjs, compile-constraint-rules.mjs, constraint-freshness.mjs.

Reproduction on d9e9da0:

ln -sfn "$PWD" /tmp/bv-link

node "$PWD/scripts/discover-constraints.mjs" . /tmp/a.json T
#   {"sources":14,...}   exit 0   /tmp/a.json created

node /tmp/bv-link/scripts/discover-constraints.mjs . /tmp/b.json T
#   (no output)          exit 0   /tmp/b.json missing

scripts/birdview.mjs does not use this guard — it locates files with new URL('...', import.meta.url)
so it is unaffected, and doctor reports a healthy installation through the same path that silently
disables the other six:

node /tmp/bv-link/scripts/birdview.mjs doctor
#   OK: example validation, renderer dependencies and template assets.   exit 0

This PR changes only the guard. Whether doctor should detect this is a design question and is
raised separately in #26.

Fix

A shared isMainModule helper resolves both sides with the same call:

return fs.realpathSync(fileURLToPath(moduleUrl)) === fs.realpathSync(entry);

Resolving only process.argv[1] was tried first and rejected: fs.realpathSync throws where
path.resolve did not, so an entry path that cannot be resolved turns the guard into a crash, and
under --preserve-symlinks-main — where import.meta.url keeps the symlinked path — it moves the
silent failure instead of removing it. Normalizing both sides covers symlinks,
--preserve-symlinks-main and Windows path casing, and an unresolvable entry simply leaves the guard
false. The helper is placed in src/main-module.mts rather than repeated in six files so the edge
cases can be covered once.

src/validate.mts no longer uses path or pathToFileURL and drops both imports.

Scope note

docs/installation.md documents installing into a real directory (~/.claude/skills/birdview); the
symlinked layout that surfaced this is not the documented one. It arose from sharing a single
installation between agents, which the docs encourage ("An existing ~/.agents/skills/birdview
installation is also discoverable; avoid duplicates"). Independently of layout, exiting 0 with no
output and no file is a failure mode worth removing.

Verification

All commands from CONTRIBUTING.md, run on Linux with Node.js 24:

Check Result
npm ci pass
npm run typecheck pass
npm run check:build pass — output and exchange schemas match distributed artifacts
npm test pass — 86 tests, 0 failed, 0 skipped
npm run validate:examples pass
node scripts/check-docs.mjs pass — 27 documentation pairs and local links
npm run build:demo pass — tracked demo unchanged
npm run check:install pass — committed archive installs, runs and reproduces artifacts

test/main-module.test.mts was checked against three builds to confirm it fails for the right
reasons:

  • against d9e9da0: fails with empty stdout and exit 0, the reported failure mode;
  • against a fix that resolves only process.argv[1]: fails with
    ENOENT: lstat 'no-such-entry.mjs', and fails the --preserve-symlinks-main case;
  • against this branch: passes.

The test creates the link with fs.symlinkSync(target, link, 'junction') so it does not require
symlink privileges on Windows, and skips with a message if the link cannot be created. Not run
locally: Windows, Node.js 18, and the Chromium suites — CI covers these. No UI change, so no
screenshots.

Checklist

  • Changes are scoped to the stated problem.
  • Relevant checks passed; remaining limitations are described.
  • Changed documentation is synchronized in English and Chinese, with the documentation check passing (if applicable).
  • No secrets, private project data or unrelated generated files are included.

@Qiuner
Qiuner merged commit 86adc61 into Qiuner:main Sep 22, 2026
5 checks passed
@JillVernus
JillVernus deleted the fix/symlink-main-guard branch September 22, 2026 01:43
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