Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,41 @@ jobs:
- name: TTY smoke tests
run: bun run test:tty-smoke

pack-npm:
name: Verify npm package
runs-on: ubuntu-latest
steps:
- name: Check out repository
uses: actions/checkout@v4

- name: Set up Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.10

- name: Set up Node
uses: actions/setup-node@v4
with:
node-version: 22

- name: Install dependencies
run: bun install --frozen-lockfile

- name: Build npm runtime bundle
run: bun run build:npm

- name: Verify npm pack output
run: bun run check:pack

- name: Simulate global npm install
run: |
pkg_dir="$(mktemp -d)"
install_dir="$(mktemp -d)"
npm pack --pack-destination "$pkg_dir" >/dev/null
pkg="$(find "$pkg_dir" -maxdepth 1 -name 'hunkdiff-*.tgz' | head -n1)"
npm install -g --prefix "$install_dir" "$pkg"
PATH="$install_dir/bin:$PATH" hunk --help | grep 'Usage: hunk'

build-bin:
name: Build compiled binary
runs-on: ubuntu-latest
Expand Down
41 changes: 36 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,25 +1,56 @@
{
"name": "hunk",
"name": "hunkdiff",
"version": "0.1.0",
"description": "Desktop-inspired terminal diff viewer for agent-authored changesets.",
"module": "src/main.tsx",
"description": "Desktop-inspired terminal diff viewer for understanding agent-authored changesets.",
"type": "module",
"private": true,
"packageManager": "bun@1.3.10",
"bin": {
"hunk": "./src/main.tsx"
"hunk": "dist/npm/main.js"
},
"files": [
"dist/npm",
"README.md",
"LICENSE",
"CONTRIBUTING.md",
"SECURITY.md"
],
"scripts": {
"start": "bun run src/main.tsx",
"dev": "bun --watch src/main.tsx",
"build:npm": "bash ./scripts/build-npm.sh",
"build:bin": "bash ./scripts/build-bin.sh",
"install:bin": "bash ./scripts/install-bin.sh",
"typecheck": "tsc --noEmit",
"test": "bun test",
"test:tty-smoke": "bun test test/tty-render-smoke.test.ts",
"check:pack": "bun run ./scripts/check-pack.ts",
"prepack": "bun run build:npm",
"bench:bootstrap-load": "bun run test/bootstrap-load-benchmark.ts",
"bench:highlight-prefetch": "bun run test/adjacent-highlight-prefetch-benchmark.ts",
"bench:large-stream": "bun run test/large-stream-windowing-benchmark.ts"
},
"keywords": [
"diff",
"git",
"tui",
"terminal",
"code-review",
"ai"
],
"repository": {
"type": "git",
"url": "git+https://github.com/modem-dev/hunk.git"
},
"homepage": "https://github.com/modem-dev/hunk#readme",
"bugs": {
"url": "https://github.com/modem-dev/hunk/issues"
},
"engines": {
"bun": ">=1.3.10"
},
"publishConfig": {
"access": "public"
},
"devDependencies": {
"@types/bun": "latest",
"@types/react": "^19.2.14",
Expand Down
20 changes: 20 additions & 0 deletions scripts/build-npm.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#!/usr/bin/env bash
set -Eeuo pipefail

repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
outdir="${repo_root}/dist/npm"

rm -rf "${outdir}"
mkdir -p "${outdir}"

BUN_TMPDIR="${repo_root}/.bun-tmp" \
BUN_INSTALL="${repo_root}/.bun-install" \
bun build "${repo_root}/src/main.tsx" \
--target bun \
--format esm \
--outdir "${outdir}" \
--entry-naming main.js

chmod 0755 "${outdir}/main.js"

printf 'Built %s\n' "${outdir}/main.js"
Comment on lines +10 to +20
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Bug: The build-npm.sh script doesn't add a #!/usr/bin/env bun shebang to the bundled output, which will cause execution to fail in Node.js-only environments.
Severity: CRITICAL

Suggested Fix

Modify scripts/build-npm.sh to explicitly prepend #!/usr/bin/env bun to the bundled output file after the bun build command. For example: { printf '#!/usr/bin/env bun\n'; cat "${outdir}/main.js"; } > "${outdir}/main.js.tmp" && mv "${outdir}/main.js.tmp" "${outdir}/main.js".

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.

Location: scripts/build-npm.sh#L10-L20

Potential issue: The `build-npm.sh` script bundles `src/main.tsx`, which contains
Bun-specific APIs like `Bun.stdin.stream()`. However, the script fails to prepend the
necessary `#!/usr/bin/env bun` shebang to the final executable. When a user on a system
with only Node.js installed runs the command after `npm install -g`, the operating
system will not know to use the Bun runtime. This will lead to execution errors, such as
`ReferenceError: Bun is not defined`, because the script will be run by the wrong
interpreter or fail to run at all. This breaks the npm distribution for users without a
global Bun installation.

Did we get this right? 👍 / 👎 to inform future reviews.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I checked this one end-to-end and did not apply the suggested shebang change. bun build --target bun is already preserving the #!/usr/bin/env bun shebang from src/main.tsx, so the bundled entrypoint is executable under Bun as-is.

The failing PR check turned out to be a different issue: the pack verifier was requiring CONTRIBUTING.md and SECURITY.md even though those docs only exist in the stacked follow-up PR. I fixed the verifier to require those files only when they exist in the repo, and also normalized the npm bin path so npm no longer needs to correct it during publish.

Responded by pi-coding-agent using openai/gpt-5.

76 changes: 76 additions & 0 deletions scripts/check-pack.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
#!/usr/bin/env bun

import { existsSync } from "node:fs";

interface PackedFile {
path: string;
size: number;
}

interface PackResult {
name: string;
version: string;
filename: string;
entryCount: number;
files: PackedFile[];
}

const proc = Bun.spawnSync(["npm", "pack", "--dry-run", "--json"], {
cwd: process.cwd(),
stdin: "ignore",
stdout: "pipe",
stderr: "pipe",
env: process.env,
});

const stdout = Buffer.from(proc.stdout).toString("utf8").trim();
const stderr = Buffer.from(proc.stderr).toString("utf8").trim();

if (proc.exitCode !== 0) {
throw new Error(stderr || stdout || "npm pack --dry-run failed");
}

const jsonMatch = stdout.match(/(\[\s*\{[\s\S]*\}\s*\])\s*$/);
const jsonText = jsonMatch?.[1];

if (!jsonText) {
throw new Error(`Could not find npm pack JSON output. Full stdout:\n${stdout}`);
}

const parsed = JSON.parse(jsonText) as PackResult[];
const pack = parsed[0];

if (!pack) {
throw new Error("npm pack --dry-run returned no pack result.");
}

const publishedPaths = new Set(pack.files.map((file) => file.path));
const requiredPaths = ["dist/npm/main.js", "README.md", "LICENSE", "package.json"];
const optionalPaths = ["CONTRIBUTING.md", "SECURITY.md"];

for (const path of requiredPaths) {
if (!publishedPaths.has(path)) {
throw new Error(`Expected npm package to include ${path}.`);
}
}

for (const path of optionalPaths) {
if (existsSync(path) && !publishedPaths.has(path)) {
throw new Error(`Expected npm package to include ${path} when it exists in the repo.`);
}
}

const forbiddenPrefixes = [".github/", "src/", "test/", "scripts/", "tmp/"];
const forbiddenPaths = ["AGENTS.md", "autoresearch.checks.sh", "autoresearch.sh", "bun.lock"];

for (const file of pack.files) {
if (forbiddenPrefixes.some((prefix) => file.path.startsWith(prefix)) || forbiddenPaths.includes(file.path)) {
throw new Error(`Unexpected file in npm package: ${file.path}`);
}
}

if (pack.name !== "hunkdiff") {
throw new Error(`Expected npm package name to be hunkdiff, got ${pack.name}.`);
}

console.log(`Verified npm pack output for ${pack.name}@${pack.version} (${pack.entryCount} files).`);
Loading