From d7ba92a85dfe34fabdf970b68d72c7fa1c939095 Mon Sep 17 00:00:00 2001 From: askalf <263217947+askalf@users.noreply.github.com> Date: Mon, 15 Jun 2026 15:07:37 -0400 Subject: [PATCH] fix(cli): run main() under symlink invocation + release 0.25.3 (#109) The installed `deepdive` bin (an npm symlink to dist/cli.js) was a silent no-op: the entry-point guard compared process.argv[1] (the symlink path) to import.meta.url (the resolved module path), which never matched, so main() never ran. Resolve both with realpathSync before comparing. Regression test spawns the CLI through a symlink. Affected every npm-global install; missed because the bench/tests invoke node dist/cli.js directly. --- CHANGELOG.md | 6 ++++++ README.md | 4 ++-- package.json | 2 +- src/cli.ts | 14 ++++++++++++-- test/cli-entrypoint.test.mjs | 36 ++++++++++++++++++++++++++++++++++++ 5 files changed, 57 insertions(+), 5 deletions(-) create mode 100644 test/cli-entrypoint.test.mjs diff --git a/CHANGELOG.md b/CHANGELOG.md index 1040451..8e79a7e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), ## [Unreleased] +## [0.25.3] - 2026-06-15 + +### Fixed — the installed `deepdive` command was a silent no-op + +- **`npm i -g @askalf/deepdive` then `deepdive …` ran nothing** (exit 0, no output); only `node dist/cli.js …` worked. npm installs the bin as a symlink, so `process.argv[1]` was the symlink path while `import.meta.url` was the resolved module path — the entry-point guard compared them raw, never matched, and skipped `main()`. The guard now compares `realpathSync(argv[1])` to `realpathSync(import.meta.url)`, so `deepdive`, `npx deepdive`, and `node dist/cli.js` all run while imports (tests) still don't. Pinned by `test/cli-entrypoint.test.mjs` (spawns the CLI through a symlink). Went unnoticed because the bench and tests invoke `node dist/cli.js` directly, never the installed bin. + ## [0.25.2] - 2026-06-15 ### Fixed — synthesis reliability on long / stalling generations diff --git a/README.md b/README.md index 2559e2c..d777db7 100644 --- a/README.md +++ b/README.md @@ -469,12 +469,12 @@ One command, aggregated health report. Paste the output when filing issues. ```bash $ deepdive doctor -deepdive doctor — v0.25.2 +deepdive doctor — v0.25.3 # environment OK Node v22.21.1 --- Platform win32 x64 - --- deepdive v0.25.2 + --- deepdive v0.25.3 # cache --- dir ~/.deepdive/cache diff --git a/package.json b/package.json index bf4e3f8..8c7e8d6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@askalf/deepdive", - "version": "0.25.2", + "version": "0.25.3", "description": "own your research — local agent, cited answers. Part of Own Your Stack.", "type": "module", "bin": { diff --git a/src/cli.ts b/src/cli.ts index 224bf6f..66ba1b2 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -9,7 +9,7 @@ // Prints the cited markdown report to stdout (or JSON with --json). Progress // events go to stderr when --verbose is set or DEEPDIVE_VERBOSE=1. -import { writeFileSync } from "node:fs"; +import { realpathSync, writeFileSync } from "node:fs"; import { resolve, join } from "node:path"; import { tmpdir } from "node:os"; import { spawn } from "node:child_process"; @@ -1785,7 +1785,17 @@ const isEntryPoint = process.argv[1] !== undefined && (() => { try { - return process.argv[1] === fileURLToPath(import.meta.url); + // Resolve symlinks before comparing: npm installs the bin as a symlink, + // so `process.argv[1]` is the symlink path (…/bin/deepdive) while + // `import.meta.url` is the real module path (…/dist/cli.js). Comparing + // raw paths made the installed `deepdive` command a silent no-op — it + // never matched, so main() never ran (#109). realpathSync collapses both + // to the canonical file, so `deepdive`, `npx deepdive`, and + // `node dist/cli.js` all run main while imports (tests) still don't. + return ( + realpathSync(process.argv[1]) === + realpathSync(fileURLToPath(import.meta.url)) + ); } catch { return false; } diff --git a/test/cli-entrypoint.test.mjs b/test/cli-entrypoint.test.mjs new file mode 100644 index 0000000..cfe97c8 --- /dev/null +++ b/test/cli-entrypoint.test.mjs @@ -0,0 +1,36 @@ +// Regression for #109: npm installs the `deepdive` bin as a symlink to +// dist/cli.js. The entry-point guard must still run main() under symlink +// invocation — comparing raw paths (argv[1] vs import.meta.url) made the +// installed command a silent no-op. We assert the symlinked invocation both +// exits 0 AND prints output (the no-op exited 0 with empty stdout). + +import test from "node:test"; +import assert from "node:assert/strict"; +import { spawnSync } from "node:child_process"; +import { mkdtempSync, symlinkSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +const CLI = resolve(fileURLToPath(import.meta.url), "..", "..", "dist", "cli.js"); + +test( + "cli entry point: runs main() when invoked through a symlink (#109)", + { skip: process.platform === "win32" ? "symlinks need privileges on Windows" : false }, + () => { + const dir = mkdtempSync(join(tmpdir(), "dd-bin-")); + const link = join(dir, "deepdive"); // mimic the npm bin symlink + try { + symlinkSync(CLI, link); + const r = spawnSync(process.execPath, [link, "--version"], { encoding: "utf8" }); + assert.equal(r.status, 0, `exit 0 expected; stderr: ${r.stderr}`); + assert.match( + r.stdout.trim(), + /^\d+\.\d+\.\d+/, + `symlinked invocation must print a version (the #109 bug left this empty), got: ${JSON.stringify(r.stdout)}`, + ); + } finally { + rmSync(dir, { recursive: true, force: true }); + } + }, +);