Agent skill
package-npm-nix
Package npm/TypeScript/Bun CLI tools for Nix. Use when creating Nix derivations for JavaScript/TypeScript tools from npm registry or GitHub sources, handling pre-built packages or source builds with dependency management.
Install this agent skill to your Project
npx add-skill https://github.com/YPares/agent-skills/tree/main/package-npm-nix
SKILL.md
<quick_start> <pre_built_from_npm> For tools already built and published to npm (fastest approach):
{
lib,
stdenv,
fetchzip,
nodejs,
}:
stdenv.mkDerivation rec {
pname = "tool-name";
version = "1.0.0";
src = fetchzip {
url = "https://registry.npmjs.org/${pname}/-/${pname}-${version}.tgz";
hash = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";
};
nativeBuildInputs = [ nodejs ];
installPhase = ''
runHook preInstall
mkdir -p $out/bin
cp $src/dist/cli.js $out/bin/tool-name
chmod +x $out/bin/tool-name
# Fix shebang
substituteInPlace $out/bin/tool-name \
--replace-quiet "#!/usr/bin/env node" "#!${nodejs}/bin/node"
runHook postInstall
'';
meta = with lib; {
description = "Tool description";
homepage = "https://github.com/org/repo";
license = licenses.mit;
sourceProvenance = with lib.sourceTypes; [ binaryBytecode ];
maintainers = with maintainers; [ ];
mainProgram = "tool-name";
platforms = platforms.all;
};
}
Get the hash:
nix-prefetch-url --unpack https://registry.npmjs.org/tool-name/-/tool-name-1.0.0.tgz
# Convert to SRI format:
nix hash convert --to sri --hash-algo sha256 <hash-output>
</pre_built_from_npm>
<source_build_with_bun> For tools that need to be built from source using Bun:
{
lib,
stdenv,
stdenvNoCC,
fetchFromGitHub,
bun,
makeBinaryWrapper,
nodejs,
autoPatchelfHook,
}:
let
fetchBunDeps =
{ src, hash, ... }@args:
stdenvNoCC.mkDerivation {
pname = args.pname or "${src.name or "source"}-bun-deps";
version = args.version or src.version or "unknown";
inherit src;
nativeBuildInputs = [ bun ];
buildPhase = ''
export HOME=$TMPDIR
export npm_config_ignore_scripts=true
bun install --no-progress --frozen-lockfile --ignore-scripts
'';
installPhase = ''
mkdir -p $out
cp -R ./node_modules $out
cp ./bun.lock $out/
'';
dontFixup = true;
outputHash = hash;
outputHashAlgo = "sha256";
outputHashMode = "recursive";
};
version = "1.0.0";
src = fetchFromGitHub {
owner = "org";
repo = "repo";
rev = "v${version}";
hash = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";
};
node_modules = fetchBunDeps {
pname = "tool-name-bun-deps";
inherit version src;
hash = "sha256-BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=";
};
in
stdenv.mkDerivation rec {
pname = "tool-name";
inherit version src;
nativeBuildInputs = [
bun
nodejs
makeBinaryWrapper
autoPatchelfHook
];
buildInputs = [
stdenv.cc.cc.lib
];
buildPhase = ''
# Verify lockfile match
diff -q ./bun.lock ${node_modules}/bun.lock || exit 1
# Copy and patch node_modules
cp -R ${node_modules}/node_modules .
chmod -R u+w node_modules
patchShebangs node_modules
autoPatchelf node_modules
export HOME=$TMPDIR
export npm_config_ignore_scripts=true
bun run build
'';
installPhase = ''
mkdir -p $out/bin
cp dist/tool-name $out/bin/tool-name
chmod +x $out/bin/tool-name
'';
dontStrip = true;
meta = with lib; {
description = "Tool description";
homepage = "https://github.com/org/repo";
license = licenses.mit;
sourceProvenance = with lib.sourceTypes; [ fromSource ];
maintainers = with maintainers; [ ];
mainProgram = "tool-name";
platforms = [ "x86_64-linux" ];
};
}
</source_build_with_bun> </quick_start>
Check the npm package:
# Download and inspect
nix-prefetch-url --unpack https://registry.npmjs.org/package/-/package-1.0.0.tgz
ls -la /nix/store/<hash>-package-1.0.0.tgz/
If dist/ directory exists with built files:
→ Use pre-built approach (simpler, faster)
If only src/ exists or package.json has build scripts:
→ Use source build approach
Check package.json for:
"bin"field: Shows what executables are provided"type": "module": ES modules (common in modern packages)"scripts": Build commands (indicates source build needed)- Runtime: Look for bun, node, or specific version requirements </step_1_identify_package_type>
<step_2_fetch_hashes> Get source and dependency hashes:
For pre-built packages:
# Fetch npm tarball
nix-prefetch-url --unpack https://registry.npmjs.org/pkg/-/pkg-1.0.0.tgz
# Output: 1abc... (base32 format)
# Convert to SRI format
nix hash convert --to sri --hash-algo sha256 1abc...
# Output: sha256-xyz...
For source builds:
# Get GitHub source hash
nix-prefetch-url --unpack https://github.com/org/repo/archive/v1.0.0.tar.gz
# Get dependencies hash (requires iteration):
# 1. Use lib.fakeHash in fetchBunDeps
# 2. Try to build
# 3. Nix will show expected hash in error
# 4. Update hash and rebuild
</step_2_fetch_hashes>
<step_3_create_package_files> Create package structure:
mkdir -p packages/tool-name
Create packages/tool-name/package.nix with full derivation (see quick_start).
Create packages/tool-name/default.nix:
{ pkgs }: pkgs.callPackage ./package.nix { }
This two-file pattern allows the package to be used standalone or integrated into a flake. </step_3_create_package_files>
<step_4_handle_special_cases> Common additional requirements:
WASM files or other assets:
installPhase = ''
mkdir -p $out/bin
cp $src/dist/cli.js $out/bin/tool
cp $src/dist/*.wasm $out/bin/ # Copy WASM alongside
chmod +x $out/bin/tool
substituteInPlace $out/bin/tool \
--replace-quiet "#!/usr/bin/env node" "#!${nodejs}/bin/node"
'';
Multiple executables:
# package.json might have:
# "bin": {
# "tool": "dist/cli.js",
# "tool-admin": "dist/admin.js"
# }
installPhase = ''
mkdir -p $out/bin
for exe in tool tool-admin; do
cp $src/dist/$exe.js $out/bin/$exe
chmod +x $out/bin/$exe
substituteInPlace $out/bin/$exe \
--replace-quiet "#!/usr/bin/env node" "#!${nodejs}/bin/node"
done
'';
meta.mainProgram = "tool"; # Primary command
Platform-specific binaries:
meta = {
platforms = [ "x86_64-linux" ]; # Bun-compiled binaries often Linux-only
# or
platforms = platforms.all; # Pure JS works everywhere
};
</step_4_handle_special_cases>
<step_5_test_build> Build and test:
# Build
nix build .#tool-name
# Test the binary
./result/bin/tool-name --version
./result/bin/tool-name --help
# Check dependencies (Linux)
ldd ./result/bin/tool-name # Should show all deps resolved
# Format
nix fmt
# Run flake checks
nix flake check
</step_5_test_build>
<metadata_requirements> <essential_fields> Every package must have complete metadata:
meta = with lib; {
description = "Clear, concise description";
homepage = "https://project-homepage.com";
changelog = "https://github.com/org/repo/releases"; # Optional but nice
license = licenses.mit; # or licenses.unfree for proprietary
sourceProvenance = with lib.sourceTypes; [
fromSource # Built from source
# or
binaryBytecode # Pre-built JS/TS (npm dist/)
# or
binaryNativeCode # Compiled binaries
];
maintainers = with maintainers; [ ]; # Empty OK for community packages
mainProgram = "binary-name";
platforms = platforms.all; # or specific: [ "x86_64-linux" ]
};
</essential_fields>
<source_provenance_guide> Choose based on what you're packaging:
fromSource: Built from TypeScript/source during derivationbinaryBytecode: Pre-compiled JS from npm registrybinaryNativeCode: Native binaries (Rust, Go, Bun-compiled)
This affects security auditing and reproducibility expectations. </source_provenance_guide> </metadata_requirements>
<common_patterns> <shebang_replacement> Always replace shebangs for reproducibility:
# Single file
substituteInPlace $out/bin/tool \
--replace-quiet "#!/usr/bin/env node" "#!${nodejs}/bin/node"
# Multiple files
find $out/bin -type f -exec substituteInPlace {} \
--replace-quiet "#!/usr/bin/env node" "#!${nodejs}/bin/node" \;
The --replace-quiet flag suppresses warnings if pattern not found.
</shebang_replacement>
<native_dependencies> Handle native modules (like sqlite, sharp):
nativeBuildInputs = [
bun
nodejs
makeBinaryWrapper
autoPatchelfHook # Linux: patches ELF binaries
];
buildInputs = [
stdenv.cc.cc.lib # Provides libgcc_s.so.1, libstdc++.so.6
];
autoPatchelfIgnoreMissingDeps = [
"libc.musl-x86_64.so.1" # Ignore musl if not available
];
autoPatchelf runs automatically on Linux, fixing RPATH for .so files.
</native_dependencies>
<bun_compiled_binaries> Don't strip Bun-compiled executables:
# Bun embeds JavaScript in the binary
dontStrip = true;
Stripping would remove the embedded JS, breaking the program. </bun_compiled_binaries>
<checking_tarball_contents> Inspect npm package structure:
# After nix-prefetch-url
ls -la /nix/store/*-pkg-1.0.0.tgz/
# Common layouts:
# dist/cli.js → Pre-built, use directly
# dist/index.js → Main entry, check package.json "bin"
# src/index.ts → Source only, need to build
# lib/ → Built CommonJS
# esm/ → Built ES modules
Check package.json to find the correct entry point. </checking_tarball_contents> </common_patterns>
<anti_patterns> <avoid_these> Don't do this:
❌ Hardcode node paths:
# Bad
"#!/usr/bin/node" # Won't work on NixOS
✅ Use substituteInPlace:
# Good
substituteInPlace $out/bin/tool \
--replace-quiet "#!/usr/bin/env node" "#!${nodejs}/bin/node"
❌ Skip hash verification:
# Bad - insecure
hash = lib.fakeHash;
✅ Get real hash:
# Good - reproducible and secure
hash = "sha256-actual-hash-here";
❌ Forget to make executable:
# Bad - won't run
cp $src/dist/cli.js $out/bin/tool
✅ Set executable bit:
# Good
cp $src/dist/cli.js $out/bin/tool
chmod +x $out/bin/tool
❌ Strip Bun binaries:
# Bad - breaks Bun-compiled executables
# (default behavior strips binaries)
✅ Disable stripping:
# Good
dontStrip = true;
</avoid_these> </anti_patterns>
The hash you provided doesn't match what Nix fetched.
Solution:
- Nix error shows "got: sha256-XYZ..."
- Copy that hash into your derivation
- Rebuild
For fetchBunDeps, this is expected the first time—use the error output to get the correct hash.
</hash_mismatch>
<missing_executable> Error: Binary not found after build
Check:
# List what was actually built
ls -R result/
# Check package.json "bin" field
cat /nix/store/*-source/package.json | jq .bin
# Check build output location
cat /nix/store/*-source/package.json | jq .scripts.build
The build might output to a different directory than expected. </missing_executable>
<elf_interpreter_error> Error: "No such file or directory" when running binary (Linux)
The binary needs ELF patching for native dependencies.
Solution:
nativeBuildInputs = [
autoPatchelfHook
];
buildInputs = [
stdenv.cc.cc.lib
];
For node_modules with native addons:
buildPhase = ''
cp -R ${node_modules}/node_modules .
chmod -R u+w node_modules
autoPatchelf node_modules # Patch .node files
'';
</elf_interpreter_error>
<bun_lock_mismatch> Error: "bun.lock mismatch"
The lockfile in your source doesn't match the cached dependencies.
This happens when:
- Source version updated but dependency hash not updated
- Source repo has uncommitted lockfile changes
Solution:
- Update source hash to match new version
- Set dependency hash to
lib.fakeHash - Build to get correct dependency hash
- Update dependency hash
- Rebuild </bun_lock_mismatch>
-
nix build .#package-namesucceeds -
./result/bin/tool --versionworks -
./result/bin/tool --helpworks -
nix flake checkpasses -
meta.descriptionis clear and concise -
meta.homepagepoints to project site -
meta.licenseis correct -
meta.sourceProvenancematches what you packaged -
meta.mainProgramis set -
meta.platformsis appropriate for the tool - All hashes are real (no
lib.fakeHash) - Shebangs use Nix store paths, not /usr/bin
- File is formatted with
nix fmt</build_checklist>
<testing_on_other_platforms>
If you only have Linux but package claims platforms.all:
Consider asking maintainers with macOS/ARM to test, or:
- Mark platforms conservatively based on what you can test
- Note in package that other platforms are untested
- Let CI or other contributors expand platform support </testing_on_other_platforms>
<success_criteria> A well-packaged npm tool has:
- Clean build with no warnings or errors
- Working executable in
result/bin/ - Complete and accurate metadata
- Proper source provenance classification
- All dependencies resolved (no missing libraries)
- Reproducible builds (real hashes, no network access during build)
- Follows Nix packaging conventions (shebang patching, proper phases) </success_criteria>
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
nix-profile-manager
Expert guidance for agents to manage local Nix profiles for installing tools and dependencies. Covers flakes, profile management, package searching, and registry configuration.
github-pr-workflow
Working with GitHub Pull Requests using the gh CLI. Use for fetching PR details, review comments, CI status, and understanding the difference between PR-level comments vs inline code review comments.
working-with-jj
Expert guidance for using JJ (Jujutsu) version control system. Use when working with JJ, whatever the subject. Operations, revsets, templates, debugging change evolution, etc. Covers JJ commands, template system, evolog, operations log, and interoperability with git remotes.
typst-writer
Write correct and idiomatic Typst code for document typesetting. Use when creating or editing Typst (.typ) files, working with Typst markup, or answering questions about Typst syntax and features. Focuses on avoiding common syntax confusion (arrays vs content blocks, proper function definitions, state management).
nushell-plugin-builder
Guide for creating Nushell plugins in Rust using nu_plugin and nu_protocol crates. Use when users want to build custom Nushell commands, extend Nushell with new functionality, create data transformations, or integrate external tools/APIs into Nushell. Covers project setup, command implementation, streaming data, custom values, and testing.
textual-builder
Build Text User Interface (TUI) applications using the Textual Python framework (v0.86.0+). Use when creating terminal-based applications, prototyping card games or interactive CLIs, or when the user mentions Textual, TUI, or terminal UI. Includes comprehensive reference documentation, card game starter template, and styling guides.
Didn't find tool you were looking for?