Imported from JustinBeckwith/linkinator (
AGENTS.md). Install upstream withnpx skills add JustinBeckwith/linkinator. Copyright stays with the author.
Agent Guide for Linkinator
This document provides guidance for AI coding assistants (Claude, GitHub Copilot, etc.) and developers working on linkinator. It documents architectural decisions, common pitfalls, and important considerations to prevent regressions.
Table of Contents
- Build & Package Structure
- Dual Runtime Targets
- Critical Files & Dependencies
- Common Pitfalls
- Testing Requirements
- Release Process
Build & Package Structure
Directory Layout
linkinator/
├── src/ # TypeScript source files
│ ├── cli.ts # CLI entry point
│ ├── index.ts # Library entry point
│ └── ...
├── build/ # Compiled JavaScript output (gitignored)
│ ├── package.json # AUTO-GENERATED copy from root
│ ├── src/
│ │ ├── cli.js # Compiled CLI
│ │ └── ...
│ └── test/
├── package.json # Root package metadata
└── test/ # Test files
TypeScript Compilation
The project uses TypeScript with these key settings (tsconfig.json):
{
"compilerOptions": {
"rootDir": ".",
"outDir": "build",
"resolveJsonModule": true // ⚠️ IMPORTANT: Copies JSON imports to build/
}
}
Important: When TypeScript sees import foo from './bar.json', it:
- Copies
bar.jsonto the build directory (maintaining relative structure) - Generates a
.d.tsdeclaration file for type safety
NPM Package Contents
The files array in package.json controls what gets published to npm:
{
"files": [
"build/src", // All compiled source
"build/package.json" // REQUIRED for CLI version info
]
}
⚠️ CRITICAL: The npm package does NOT include:
- Root
package.json build/test/directory- Any files outside
build/src/andbuild/package.json
Dual Runtime Targets
Linkinator supports two distinct runtime environments:
1. NPM Package (Node.js)
npm install linkinator
linkinator ./docs
How it works:
- User installs via npm
- Gets contents specified in
filesarray - Runs
node_modules/linkinator/build/src/cli.js - Imports resolve to files in
node_modules/linkinator/
2. Compiled Binaries (Standalone Executables)
./linkinator-linux ./docs
How it works:
- Created via
bun build --compile - Bundles all JavaScript and dependencies into a single executable
- No access to filesystem for imports (everything is embedded)
- Uses embedded JSON imports from build time
Critical Files & Dependencies
CLI Version Information
File: src/cli.ts:6
import packageJson from '../package.json' with { type: 'json' };
Why this exists:
- Originally added in #760 to fix
--versionflag in compiled binaries - Before this, meow tried to read package.json at runtime, which failed in binaries
How it works:
- During build: TypeScript copies
package.json→build/package.json - Compiled to:
import packageJson from '../package.json'(relative path unchanged) - At runtime (npm):
build/src/cli.jsimportsbuild/package.json✅ - At compile time (binaries): Bundler embeds the JSON content ✅
⚠️ CRITICAL REQUIREMENT:
build/package.jsonMUST be included in the npm package- Without it, npm users get:
Cannot find module '/node_modules/linkinator/build/package.json' - This caused issue #763
Package File Checklist
When modifying packaging, verify:
-
"build/package.json"is in thefilesarray - The import path in
cli.tsis../package.json(not../../) -
tsconfig.jsonhasresolveJsonModule: true -
npm pack --dry-runincludesbuild/package.json
Common Pitfalls
🚨 Pitfall 1: Removing build/package.json from files Array
What happens: NPM users can't run the CLI
Error: Cannot find module '/node_modules/linkinator/build/package.json'
Why: The CLI imports from ../package.json relative to build/src/cli.js
How to avoid: Never remove build/package.json from the files array without changing how the CLI gets version info
🚨 Pitfall 2: Changing the Import Path to ../../package.json
What happens: Works for npm, breaks for compiled binaries Why:
- From
src/cli.ts,../../package.jsonwould look outside the project - TypeScript won't copy a file from outside the project
- Compiled binaries won't have the package.json embedded
How to avoid: Always import from
../package.json(one level up from src/)
🚨 Pitfall 3: Version Drift Between Root and Build
What happens: --version shows wrong version
Why:
- Someone edited
package.jsonwithout rebuilding build/package.jsonhas stale version How to avoid:- Always run
npm run buildafter version changes - The release workflow rebuilds automatically
- Test
test.cli.tsverifies version consistency
🚨 Pitfall 4: Adding JSON Imports Without Updating files Array
Scenario: You add import config from '../config.json' in the CLI
What happens:
- TypeScript copies it to
build/config.json✅ - Works locally ✅
- NPM package doesn't include
build/config.json❌ - NPM users get import error ❌
How to avoid: When adding new JSON imports used by the CLI:
- Add the file to the
filesarray:"build/config.json" - Test with
npm pack --dry-run - Verify the file is listed in the tarball contents
🚨 Pitfall 5: Forgetting About Compiled Binaries
Scenario: You use runtime file system access:
const version = JSON.parse(fs.readFileSync('package.json', 'utf8')).version;
What happens:
- Works for npm ✅
- Breaks in compiled binaries ❌ (no filesystem)
How to avoid:
- Use static imports for data needed in both environments
- Test changes with
npm run build-binaries - Run the binary and verify it works
Testing Requirements
Minimum Tests for Packaging Changes
If you modify:
package.jsonfilesarray- Imports in
cli.ts - Build configuration (
tsconfig.json) - Package metadata
You must verify:
- All tests pass:
npm test - Version consistency test passes: Ensures
build/package.jsonmatches root - NPM package contents:
npm pack --dry-run | grep build/ - Compiled binary works:
npm run build-binaries ./build/binaries/linkinator-macos --version - Local CLI works:
node build/src/cli.js --version
Test Structure
test/test.cli.ts- CLI behavior, version flag, packagingtest/test.index.ts- Core library functionalitytest/test.*.ts- Feature-specific tests
Key Test: Version Consistency
File: test/test.cli.ts:65-80
it('should have build/package.json with matching version', () => {
const rootPkg = JSON.parse(
fs.readFileSync(new URL('../package.json', import.meta.url), 'utf8'),
);
const buildPkg = JSON.parse(
fs.readFileSync(new URL('../build/package.json', import.meta.url), 'utf8'),
);
assert.strictEqual(
buildPkg.version,
rootPkg.version,
'build/package.json version should match root package.json version',
);
});
This test catches stale builds after version bumps.
Release Process
Automated Release Workflow
File: .github/workflows/release.yaml
The workflow:
- release-please: Creates release PR with version bumps
- publish job (when PR merged):
- run: npm ci # Installs deps, runs prepare hook - run: npm run build # Explicit rebuild (ensure fresh build) - run: npm publish # Publishes to npm - run: npm run build-binaries # Creates platform binaries - run: gh release upload ... # Uploads binaries to GitHub release
Version Bump Process
- Release-please creates PR with version change
- Merging PR triggers publish workflow
- Workflow rebuilds from scratch
- Publishes with fresh
build/package.jsonmatching root version
⚠️ Important:
- Don't manually publish without rebuilding
- Don't edit version in
build/package.jsondirectly (auto-generated) - Trust the CI/CD process - it rebuilds correctly
Manual Publishing (Emergency Only)
If you must publish manually:
# 1. Ensure clean state
git status # Should be clean
# 2. Bump version
npm version patch|minor|major
# 3. MUST rebuild
npm run build
# 4. Verify package contents
npm pack --dry-run | grep build/package.json
# 5. Publish
npm publish
# 6. Build and upload binaries
npm run build-binaries
gh release upload <tag> build/binaries/*
Architecture Decision Records
ADR 1: Why build/package.json is in the npm package
Context: CLI needs to display version via --version flag in both npm and binary distributions.
Decision: Include build/package.json in npm packages and import it in CLI.
Alternatives considered:
- ❌ Use
import.meta.urlto read at runtime - doesn't work in binaries - ❌ Generate
version.tsfile - extra build complexity - ❌ Use bundler for constant injection - requires switching from
tsc - ✅ Import JSON and include in package - simple, works everywhere
Consequences:
- ✅ Works for npm and compiled binaries
- ✅ Minimal code changes
- ⚠️ Adds 2.5kB to package (duplicate package.json)
- ⚠️ Must keep
filesarray in sync
References: #759, #760, #763
ADR 2: Why We Support Both NPM and Compiled Binaries
Context: Users want both:
- Easy npm installation for Node.js projects
- Standalone binaries for CI/CD and non-Node environments
Decision: Maintain both distribution methods.
Consequences:
- Must test both environments
- Can't use npm-only features (like reading node_modules at runtime)
- Import statements must work when bundled
- Adds complexity to CI/CD
Quick Reference
Common Commands
# Build TypeScript
npm run build
# Run tests
npm test
# Test CLI locally
node build/src/cli.js <args>
# Check package contents
npm pack --dry-run
# Build platform binaries
npm run build-binaries
# Test binary
./build/binaries/linkinator-macos --version
Files to Check When Modifying Packaging
-
package.json-filesarray,binentry -
tsconfig.json-rootDir,outDir,resolveJsonModule -
src/cli.ts- Import paths for JSON -
test/test.cli.ts- Add tests for new imports -
.github/workflows/release.yaml- CI/CD build steps
Red Flags 🚩
Watch out for these in code reviews:
- ❌ Removing items from
filesarray without testing npm pack - ❌ Adding JSON imports without updating
filesarray - ❌ Using
fs.readFileSync()in code that runs in binaries - ❌ Changing import paths without testing both npm and binaries
- ❌ Modifying
build/directory manually (it's auto-generated) - ❌ Committing
build/directory to git (it's gitignored for a reason)
Historical Context
Issue Timeline
- #759 (Dec 2024):
--versionflag didn't work in compiled binaries - #760 (Dec 2024): Added
import packageJsonto fix binaries- ⚠️ Regression: Broke npm installs (missing build/package.json)
- #763 (Dec 2024): Users reported "Cannot find module" error
- #764 (Dec 2024): Added
build/package.jsontofilesarray
Lessons Learned
- Test both distribution methods: npm install AND compiled binaries
- Verify npm package contents: Use
npm pack --dry-run - Understand TypeScript JSON handling:
resolveJsonModulecopies files - Document architectural decisions: Hence this file!
Getting Help
If you're unsure about a change:
- Read this file - especially Common Pitfalls
- Test locally:
npm run build && npm test - Check package:
npm pack --dry-run - Test binary:
npm run build-binaries && ./build/binaries/linkinator-macos --version - Ask in PR: Tag maintainers if uncertain
Remember: It's better to ask than to introduce a regression! 🙂
Last Updated: 2025-12-27 Maintainer: @JustinBeckwith Contributors: Claude (learned the hard way 😅)