| name | npmjs |
| description | 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.
|
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
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
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
for ext in extensions/pi-*/; do
name=$(basename "$ext")
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
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
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
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
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')
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
for ext in extensions/pi-*/; do
name=$(basename "$ext")
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.
EXTENSION="pi-example"
cd "extensions/$EXTENSION" && npx tsc --noEmit
for f in README.md CHANGELOG.md package.json .npmignore; do
[ -f "extensions/$EXTENSION/$f" ] || echo "MISSING: $f"
done
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
cd "extensions/$EXTENSION" && npm pack --dry-run
grep -q "AGENTS.md" "extensions/$EXTENSION/.npmignore" || echo "WARNING: AGENTS.md not in .npmignore"
Batch pre-publish check for all packages:
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... "
if ! (cd "$ext" && npx tsc --noEmit 2>&1) > /dev/null; then
echo "❌ typecheck failed"
failed=$((failed + 1))
continue
fi
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
EXTENSION="pi-example"
BUMP="patch"
cd "extensions/$EXTENSION"
npm version $BUMP --no-git-tag-version
new_ver=$(cat package.json | jq -r '.version')
echo "Bumped to $new_ver"
Bulk Version Bump
BUMP="patch"
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
EXTENSION="pi-example"
NEW_VERSION="0.2.0"
DATE=$(date +%Y-%m-%d)
last_ver=$(grep -m1 '## \[' "extensions/$EXTENSION/CHANGELOG.md" | sed 's/## \[\(.*\)\].*/\1/')
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:
## [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
EXTENSION="pi-example"
cd "extensions/$EXTENSION" && npm publish --access public
Bulk Publish
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
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
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
npm view @e9n/pi-example --json | jq '{version, dist, files: .dist.fileCount}'
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:
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}"
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.
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
}
'
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:
- Audit — Run health checks, verify deps, identify changed packages
- Bump — Version bump changed packages (patch/minor/major as appropriate)
- Changelog — Update CHANGELOG.md for each bumped package
- Check — Run batch pre-publish checks (typecheck, required fields, tarball)
- Commit —
git add -A && git commit -m "release: bump <packages>"
- Publish — Bulk publish changed packages
- Verify — Post-publish version check
- Tag — Git tag each published version
- 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