# Npmjs

> npm package maintenance and lifecycle management for monorepo extensions. Covers health audits, dependency sync, version bumping, changelog updates, pre-publish checks, coordinated publishing, and post-publish verification. **Triggers — use this skill when:** - User asks to "publish", "release", or "version bump" extensions/packages - User says "audit packages", "check package health", "dependency sync" - User asks to "update changelogs", "prepare release", "pre-publish check" - User mentions "npm publish", "version management", "package lifecycle" - User wants to "sync dependencies" or "align versions" across packages - User asks "what needs publishing" or "which packages changed" **Covers:** Monorepo with multiple npm packages under `extensions/`. Each package has its own package.json, CHANGELOG.md, README.md, and version. Packages are scoped (e.g. `@e9n/pi-*`) and published individually.

- Skill: `espennilsen/npmjs` (Agent Skill)
- Install (CLI): `npx skillmds@latest add espennilsen/npmjs`
- Raw SKILL.md: https://api.skillmd.com/api/skills/espennilsen/npmjs/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Security
- Author: espennilsen (https://skillmd.com/u/espennilsen)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/espennilsen/npmjs

---


# npmjs — Package Maintenance & Lifecycle Management

Manage npm packages in a monorepo: audit health, sync dependencies, bump versions,
update changelogs, run pre-publish checks, and coordinate publishing.

---

## Commands Reference

### Health & Audit

```bash
# List all extension packages with versions and private status
for ext in extensions/pi-*/; do
  name=$(basename "$ext")
  pkg=$(cat "$ext/package.json")
  version=$(echo "$pkg" | jq -r '.version')
  private=$(echo "$pkg" | jq -r '.private // false')
  echo "$name@$version (private: $private)"
done

# Check what's published vs local
for ext in extensions/pi-*/; do
  local_name=$(cat "$ext/package.json" | jq -r '.name')
  local_ver=$(cat "$ext/package.json" | jq -r '.version')
  private=$(cat "$ext/package.json" | jq -r '.private // false')
  [ "$private" = "true" ] && continue
  remote_ver=$(npm view "$local_name" version 2>/dev/null || echo "not published")
  status="✅"
  [ "$local_ver" != "$remote_ver" ] && status="⚠️  local=$local_ver npm=$remote_ver"
  echo "$local_name $status"
done

# Full dependency audit
for ext in extensions/pi-*/; do
  name=$(basename "$ext")
  # Check for missing imports (imported but not declared)
  imports=$(grep -rh "from ['\"]@" "$ext/src/" 2>/dev/null | sed "s/.*from ['\"]//;s/['\"].*//" | grep "^@" | sort -u)
  for imp in $imports; do
    in_deps=$(cat "$ext/package.json" | jq -r --arg p "$imp" '(.dependencies // {})[$p] // (.peerDependencies // {})[$p] // empty')
    [ -z "$in_deps" ] && echo "MISSING: $name imports $imp"
  done
  # Check for unused deps (declared but not imported)
  deps=$(cat "$ext/package.json" | jq -r '.dependencies // {} | keys[]' 2>/dev/null)
  for dep in $deps; do
    grep -rq "from ['\"]$dep\|require(['\"]$dep\|import(['\"]$dep" "$ext/src/" 2>/dev/null || \
      echo "UNUSED: $name has $dep in dependencies"
  done
done

# Check for phantom peer deps
for ext in extensions/pi-*/; do
  name=$(basename "$ext")
  peers=$(cat "$ext/package.json" | jq -r '.peerDependencies // {} | keys[]' 2>/dev/null)
  for peer in $peers; do
    grep -rq "from ['\"]$peer" "$ext/src/" 2>/dev/null || \
      echo "PHANTOM PEER: $name declares $peer but never imports it"
  done
done

# Shared dependency version consistency
echo "=== Dependency Version Report ==="
for ext in extensions/pi-*/; do
  cat "$ext/package.json" | jq -r --arg ext "$(basename "$ext")" \
    '.dependencies // {} | to_entries[] | "\(.key) \(.value) \($ext)"'
done | sort | awk '{
  key=$1; ver=$2; ext=$3
  if (key != prev) { if (prev && count > 1 && versions > 1) print ""; prev=key; count=0; versions=0; delete seen }
  if (!seen[ver]) { seen[ver]=1; versions++ }
  count++
  print "  " ext ": " key "@" ver
}'
```

### Detecting Changed Packages

```bash
# Find extensions with changes since last publish (compare to npm registry)
for ext in extensions/pi-*/; do
  name=$(cat "$ext/package.json" | jq -r '.name')
  private=$(cat "$ext/package.json" | jq -r '.private // false')
  [ "$private" = "true" ] && continue
  version=$(cat "$ext/package.json" | jq -r '.version')
  # Check git diff since last tag or version bump
  last_tag=$(git tag -l "${name}@*" --sort=-creatordate | head -1)
  if [ -z "$last_tag" ]; then
    echo "NEW: $name@$version (never tagged)"
  else
    changes=$(git diff --name-only "$last_tag" -- "$ext" 2>/dev/null | grep -v 'CHANGELOG\|README\|AGENTS' | head -5)
    [ -n "$changes" ] && echo "CHANGED: $name ($last_tag → now)" && echo "$changes" | sed 's/^/  /'
  fi
done

# Simpler: find extensions with uncommitted or recent source changes
for ext in extensions/pi-*/; do
  name=$(basename "$ext")
  # Check for source changes in last N commits
  changes=$(git log --oneline -10 -- "$ext/src/" | head -5)
  [ -n "$changes" ] && echo "$name:" && echo "$changes" | sed 's/^/  /'
done
```

---

## Lifecycle Operations

### 1. Pre-publish Checks

Run these before every publish. All must pass.

```bash
EXTENSION="pi-example"  # Set target

# 1. TypeScript check
cd "extensions/$EXTENSION" && npx tsc --noEmit

# 2. Required files exist
for f in README.md CHANGELOG.md package.json .npmignore; do
  [ -f "extensions/$EXTENSION/$f" ] || echo "MISSING: $f"
done

# 3. Package.json has required fields
cat "extensions/$EXTENSION/package.json" | jq '{
  name: .name,
  version: .version,
  description: .description,
  license: .license,
  author: .author,
  keywords: .keywords,
  files: .files,
  repository: .repository,
  pi: .pi
}' | jq 'to_entries[] | select(.value == null or .value == "") | "MISSING: \(.key)"' -r

# 4. Verify tarball contents (dry run)
cd "extensions/$EXTENSION" && npm pack --dry-run

# 5. No AGENTS.md in tarball (should be in .npmignore)
grep -q "AGENTS.md" "extensions/$EXTENSION/.npmignore" || echo "WARNING: AGENTS.md not in .npmignore"
```

**Batch pre-publish check for all packages:**

```bash
failed=0
for ext in extensions/pi-*/; do
  name=$(basename "$ext")
  private=$(cat "$ext/package.json" | jq -r '.private // false')
  [ "$private" = "true" ] && continue
  echo -n "$name... "
  # Typecheck
  if ! (cd "$ext" && npx tsc --noEmit 2>&1) > /dev/null; then
    echo "❌ typecheck failed"
    failed=$((failed + 1))
    continue
  fi
  # Required fields
  missing=$(cat "$ext/package.json" | jq -r '[
    (if .name == null then "name" else empty end),
    (if .version == null then "version" else empty end),
    (if .description == null then "description" else empty end),
    (if .license == null then "license" else empty end),
    (if .author == null then "author" else empty end),
    (if .files == null then "files" else empty end)
  ] | join(", ")')
  if [ -n "$missing" ]; then
    echo "❌ missing: $missing"
    failed=$((failed + 1))
    continue
  fi
  echo "✅"
done
echo "---"
echo "$failed package(s) with issues"
```

### 2. Version Bumping

#### Single Package

```bash
EXTENSION="pi-example"
BUMP="patch"  # patch | minor | major

cd "extensions/$EXTENSION"
# npm version updates package.json and creates a git tag
npm version $BUMP --no-git-tag-version
# Read new version
new_ver=$(cat package.json | jq -r '.version')
echo "Bumped to $new_ver"
```

#### Bulk Version Bump

```bash
BUMP="patch"  # Same bump for all

for ext in extensions/pi-*/; do
  name=$(basename "$ext")
  private=$(cat "$ext/package.json" | jq -r '.private // false')
  [ "$private" = "true" ] && continue
  old_ver=$(cat "$ext/package.json" | jq -r '.version')
  cd "$ext" && npm version $BUMP --no-git-tag-version && cd - > /dev/null
  new_ver=$(cat "$ext/package.json" | jq -r '.version')
  echo "$name: $old_ver → $new_ver"
done
```

#### Selective Bump (only changed packages)

Identify which packages have source changes since their last publish, then bump only those.
Use the "Detecting Changed Packages" commands above first.

### 3. Changelog Updates

Before publishing, update CHANGELOG.md for each package being released.

#### Per-Package Changelog

```bash
EXTENSION="pi-example"
NEW_VERSION="0.2.0"
DATE=$(date +%Y-%m-%d)

# Get commits since last version entry in CHANGELOG
last_ver=$(grep -m1 '## \[' "extensions/$EXTENSION/CHANGELOG.md" | sed 's/## \[\(.*\)\].*/\1/')
# Get relevant commits
git log --oneline -- "extensions/$EXTENSION/src/" | head -20
```

Then manually write a changelog entry following Keep a Changelog format.
Insert after the `## [Unreleased]` or first `## [` line:

```markdown
## [0.2.0] - 2026-02-17

### Added
- Description of new features

### Changed
- Description of changes

### Fixed
- Description of bug fixes
```

**Rules:**
- Imperative mood: "Add", "Fix", "Remove" — not past tense
- User-facing changes only — skip CI, refactors, typos
- Be specific: "Fix crash when saving empty file" not "Fix save bug"
- Link PRs/issues where applicable: `(#42)`
- Mark breaking changes: `**BREAKING:** Remove deprecated endpoint`

### 4. Publishing

#### Single Package

```bash
EXTENSION="pi-example"
cd "extensions/$EXTENSION" && npm publish --access public
```

#### Bulk Publish

```bash
# Publish all non-private packages
for ext in extensions/pi-*/; do
  name=$(basename "$ext")
  private=$(cat "$ext/package.json" | jq -r '.private // false')
  [ "$private" = "true" ] && echo "⏭️  $name (private)" && continue
  echo -n "📦 $name... "
  result=$(cd "$ext" && npm publish --access public 2>&1)
  if echo "$result" | grep -q "^\+"; then
    echo "✅"
  else
    echo "❌"
    echo "$result" | grep "npm error" | head -3
  fi
done
```

#### Publish Only Changed Packages

```bash
for ext in extensions/pi-*/; do
  name=$(cat "$ext/package.json" | jq -r '.name')
  local_ver=$(cat "$ext/package.json" | jq -r '.version')
  private=$(cat "$ext/package.json" | jq -r '.private // false')
  [ "$private" = "true" ] && continue
  remote_ver=$(npm view "$name" version 2>/dev/null || echo "none")
  if [ "$local_ver" != "$remote_ver" ]; then
    echo -n "📦 $name@$local_ver (was $remote_ver)... "
    result=$(cd "$ext" && npm publish --access public 2>&1)
    echo "$result" | grep -q "^\+" && echo "✅" || echo "❌"
  fi
done
```

#### OTP Handling

If npm requires OTP, add `--otp=<code>` to publish commands.
For bulk publish with OTP, the token is time-based (30s window) — publish quickly or use automation tokens.

### 5. Post-publish Verification

```bash
# Verify all packages are published with correct versions
for ext in extensions/pi-*/; do
  name=$(cat "$ext/package.json" | jq -r '.name')
  local_ver=$(cat "$ext/package.json" | jq -r '.version')
  private=$(cat "$ext/package.json" | jq -r '.private // false')
  [ "$private" = "true" ] && continue
  remote_ver=$(npm view "$name" version 2>/dev/null || echo "NOT FOUND")
  if [ "$local_ver" = "$remote_ver" ]; then
    echo "✅ $name@$remote_ver"
  else
    echo "❌ $name local=$local_ver npm=$remote_ver"
  fi
done

# Verify tarball contents for a specific package
npm view @e9n/pi-example --json | jq '{version, dist, files: .dist.fileCount}'

# Test install in a temp dir
tmpdir=$(mktemp -d)
cd "$tmpdir" && npm init -y && npm install @e9n/pi-example && ls node_modules/@e9n/pi-example/
rm -rf "$tmpdir"
```

### 6. Git Tagging

After publishing, tag the release in git:

```bash
# Tag individual package releases
EXTENSION="pi-example"
VERSION=$(cat "extensions/$EXTENSION/package.json" | jq -r '.version')
NAME=$(cat "extensions/$EXTENSION/package.json" | jq -r '.name')
git tag "${NAME}@${VERSION}"
git push origin "${NAME}@${VERSION}"

# Bulk tag all packages at current versions
for ext in extensions/pi-*/; do
  name=$(cat "$ext/package.json" | jq -r '.name')
  version=$(cat "$ext/package.json" | jq -r '.version')
  private=$(cat "$ext/package.json" | jq -r '.private // false')
  [ "$private" = "true" ] && continue
  tag="${name}@${version}"
  if ! git tag -l "$tag" | grep -q .; then
    git tag "$tag"
    echo "Tagged $tag"
  fi
done
git push origin --tags
```

### 7. Dependency Sync

Align shared dependency versions across all packages.

```bash
# Find all shared dependencies and their version ranges
echo "=== Shared Dependency Versions ==="
for ext in extensions/pi-*/; do
  cat "$ext/package.json" | jq -r --arg ext "$(basename "$ext")" \
    '.dependencies // {} | to_entries[] | "\(.key)|\(.value)|\($ext)"'
done | sort -t'|' -k1,1 | awk -F'|' '
{
  if ($1 != prev) {
    if (NR > 1 && count > 1) print ""
    prev = $1; count = 0; delete vers
  }
  count++
  vers[$2] = vers[$2] ? vers[$2] ", " $3 : $3
}
END {
  # Use the commands output to identify mismatches
}
'

# To update a specific dependency across all extensions:
DEP="better-sqlite3"
NEW_VERSION="^12.6.2"
for ext in extensions/pi-*/; do
  has_dep=$(cat "$ext/package.json" | jq -r --arg d "$DEP" '.dependencies[$d] // empty')
  if [ -n "$has_dep" ]; then
    cat "$ext/package.json" | jq --arg d "$DEP" --arg v "$NEW_VERSION" \
      '.dependencies[$d] = $v' > "$ext/package.json.tmp" && mv "$ext/package.json.tmp" "$ext/package.json"
    echo "Updated $(basename $ext): $DEP → $NEW_VERSION"
  fi
done
```

---

## Full Release Workflow

When doing a release, follow these steps in order:

1. **Audit** — Run health checks, verify deps, identify changed packages
2. **Bump** — Version bump changed packages (patch/minor/major as appropriate)
3. **Changelog** — Update CHANGELOG.md for each bumped package
4. **Check** — Run batch pre-publish checks (typecheck, required fields, tarball)
5. **Commit** — `git add -A && git commit -m "release: bump <packages>"`
6. **Publish** — Bulk publish changed packages
7. **Verify** — Post-publish version check
8. **Tag** — Git tag each published version
9. **Push** — `git push origin <branch> --tags`

---

## Conventions

- **Scope:** All packages under `@e9n/` npm scope
- **Versioning:** SemVer — patch for fixes, minor for features, major for breaking
- **Private packages:** Have `"private": true` in package.json, skipped by all publish commands
- **Changelogs:** Keep a Changelog format, ISO 8601 dates
- **Tags:** `@e9n/pi-example@1.2.3` format
- **Tarballs:** Should contain `src/`, `package.json`, `README.md`, `CHANGELOG.md` plus any extra content (skills/, prompts/, HTML files). Never `AGENTS.md`, `node_modules/`, `tsconfig.json`.
- **Peer deps:** Pi core packages (`@mariozechner/pi-coding-agent`, `@mariozechner/pi-ai`, `@sinclair/typebox`, `@mariozechner/pi-agent-core`, `@mariozechner/pi-tui`) always in `peerDependencies` with `"*"` range

