Claude Code Plugins

Community-maintained marketplace

Feedback

package-npm-nix

@YPares/agent-skills
2
0

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 Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name package-npm-nix
description 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.
Create Nix packages for npm-based CLI tools, covering both pre-built packages from npm registry and source builds with proper dependency management. This skill provides patterns for fetching, building, and packaging JavaScript/TypeScript/Bun tools in Nix environments. 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>

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" ];
  };
}
**Determine build approach**:

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
**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
**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.

**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
};
**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
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" ]
};
**Choose based on what you're packaging**:
  • fromSource: Built from TypeScript/source during derivation
  • binaryBytecode: Pre-compiled JS from npm registry
  • binaryNativeCode: Native binaries (Rust, Go, Bun-compiled)

This affects security auditing and reproducibility expectations.

**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.

**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.

**Don't strip Bun-compiled executables**:
# Bun embeds JavaScript in the binary
dontStrip = true;

Stripping would remove the embedded JS, breaking the program.

**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.

**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;
**Error: "hash mismatch in fixed-output derivation"**

The hash you provided doesn't match what Nix fetched.

Solution:

  1. Nix error shows "got: sha256-XYZ..."
  2. Copy that hash into your derivation
  3. Rebuild

For fetchBunDeps, this is expected the first time—use the error output to get the correct hash.

**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.

**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
'';
**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:

  1. Update source hash to match new version
  2. Set dependency hash to lib.fakeHash
  3. Build to get correct dependency hash
  4. Update dependency hash
  5. Rebuild
Before considering the package done:
  • nix build .#package-name succeeds
  • ./result/bin/tool --version works
  • ./result/bin/tool --help works
  • nix flake check passes
  • meta.description is clear and concise
  • meta.homepage points to project site
  • meta.license is correct
  • meta.sourceProvenance matches what you packaged
  • meta.mainProgram is set
  • meta.platforms is 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
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
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)