Claude Skill

node-deps-bumper

Use for dependency updates: bump npm/pnpm/yarn/bun packages, check outdated, or run taze.

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download paulrberg-agent-skills-skills_node-deps-bumper-913232a.zip · 13 KB
Part of paulrberg/agent-skills — 42 skills

Install

skills CLI npx skills add https://github.com/PaulRBerg/agent-skills/tree/main/skills/node-deps-bumper
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install paulrberg-agent-skills@llmmart
Git git clone https://github.com/PaulRBerg/agent-skills.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole paulrberg/agent-skills collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

Node Dependency Bumper

Use Taze to build one structured update plan, apply compatible ranged updates, and make major-version decisions as a batch.

Adding Dependencies

For a new package or CLI, use the repository's package manager and existing range convention instead of Taze. Installing package@latest and retaining the package manager's resulting ^x.y.z range is allowed. Do not replace a caret range with an exact version merely because it can admit future releases.

When an effective package-manager minimum-release-age policy exists, it provides the freshness boundary for candidate versions; read references/conditional-workflows.md. The committed lockfile and frozen deployment install provide reproducibility. Exact-pin only when the user or repository requires it, a known compatibility constraint justifies it, or the package will run without a committed lockfile and frozen install.

Multiple Repositories

When the user names two or more repositories or asks to sync or align dependencies across repositories, read references/multi-repo-alignment.md. It runs the Workflow below per repository and adds shared-target selection, one cross-repository major batch, and per-repository commits.

Workflow

  1. Resolve the skill directory and save the helper plan from the target repository:

    bash <skill-dir>/scripts/run-taze.sh --plan [--include package-a,package-b] > <taze-plan.json>
    

    The JSON plan classifies every discovered update as apply, review-major, review, or skip-fixed. The helper detects monorepos, includes locked versions during scans, and mirrors Bun minimum-release-age settings. If the repository uses package-manager age gates or Bun catalogs, read references/conditional-workflows.md for that active branch only.

  2. If --dry-run was requested, present the plan and counts, then stop without changing manifests or lockfiles.

  3. Select every ranged minor/patch update marked apply. Reuse explicit approval of package/version transitions; never auto-approve a major merely from its package name. Present unresolved review-major and unknown updates in one decision batch with current version, target version, package role, and relevant migration/release notes. Apply only authorized majors.

  4. If nothing is selected, report the no-op and stop. If the root manifest uses Bun catalogs, preview the exact selected catalog transitions from the accepted plan. Pass only selected packages present in a catalog; skip the helper when that subset is empty:

    uv run <skill-dir>/scripts/update-bun-catalogs.py \
      --root <repo> --plan <taze-plan.json> --include package-a,package-b
    

    The preview is read-only. Missing catalog entries, conflicting plan rows, unsupported versions, or a catalog value that no longer matches the plan fail before writes. The helper does not select upgrades.

  5. Before the first manifest or lockfile write, select and run a baseline that can detect regressions from the chosen updates. Honor repository-required checks; otherwise select dependency-resolution, build, test, typecheck, lint, formatting, codegen, or invariant checks from the updated packages' actual consumers and risk. Use an aggregate suite for shared toolchain or runtime changes, not merely because it exists. Use frozen or non-writing modes where available, and record the exact commands and coverage for the post-bump rerun. Attribute every failure before deciding whether it blocks. Proceed when an unrelated pre-existing failure is reproducible, can be compared after the bump, and does not prevent dependency resolution or the checks needed to detect regressions; do not fix it as part of the bump. Dependency or peer-resolution conflicts, actionable unsafe behavior, or a baseline that cannot provide trustworthy before/after signal block the bump. In that case, stop with ### ⛔ Dependency bump blocked — baseline unusable and report the exact prerequisite and diagnostics without asking for redundant authorization. Informational notices such as unavoidable deprecations do not block.

  6. For Bun catalogs, apply the previewed transitions first: rerun update-bun-catalogs.py with the same plan and catalog include subset plus --write. It atomically updates every matching default/named catalog occurrence and preserves each existing ^, ~, or empty prefix. Apply before Taze, whose native catalog writes would make this plan stale.

    Then write all selected Taze updates in one command:

    bash <skill-dir>/scripts/run-taze.sh --write --include package-a,package-b
    
  7. Run ni so the repository's package manager updates its lockfile.

  8. Inspect the manifest and lockfile diff. Rerun the exact baseline commands, plus the narrowest checks that exercise the updated dependencies and any required migrations. Treat every newly introduced error, type issue, check failure (including tests, builds, lint, formatting, codegen, and repository invariants), dependency or peer-resolution conflict, and actionable compatibility or safety warning as caused by the bump unless evidence shows otherwise.

  9. Fix every issue caused by the bump, including required source or configuration migrations, while preserving intended behavior. Do not suppress diagnostics, weaken validation, or change expected behavior merely to make checks pass. After each fix, rerun checks whose inputs or behavior changed. At completion, require valid results for the entire recorded suite against the final state, with no new failures against the accepted baseline; reuse passing results whose inputs remain unchanged. Only unrelated pre-existing failures meeting step 5's comparison requirements may remain. If no clear safe fix exists within the task's authority, stop with ### ⚠️ Dependency regression decision required. Present all such issues in one table with the evidence, affected locations, fix and revert options, and likely effects. Do not report completion until the user chooses, the fix is applied or the offending update is reverted, the lockfile is regenerated, and the complete suite meets that same baseline-comparison requirement.

User-Facing Output

Present plans as ### 📦 Dependency plan with counts and a compact table:

ID Plan value Decision Package Current → target Type Notes

Use the plan's exact apply, review-major, review, and skip-fixed values alongside plain-language decisions. Assign stable IDs to rows needing a choice so the user can answer once. Use ### 🔎 Dry run — no files written for a preview and ### ✅ No selected updates for a no-op.

Finish applied work with ### 🏁 Dependencies updated, a tree of changed manifests/lockfiles, and ### 🧪 Verification. Use ### ⚠️ Remaining review only for non-blocking informational matters, never for an unresolved issue caused by the bump. Keep helper JSON, package/version strings, commands, and diagnostics exact and undecorated.

Invariants

  • Fixed versions and non-semver protocols remain unchanged unless the user explicitly asks otherwise.
  • Caret ranges are valid for new dependencies and CLIs; do not describe them as unreproducible when a committed lockfile and frozen install control resolution.
  • Package arguments constrain both scan and write phases.
  • The same maturity-period policy applies to scan and write.
  • Bun catalog preview and write use the same accepted Taze plan and selected package set; stale plans never write.
  • Do not infer compatibility from SemVer alone when repository evidence, peer ranges, or release notes indicate otherwise.

Completion requires an attributed, comparison-safe pre-write baseline, a reviewed plan, the retained selected updates, a regenerated lockfile, no new failure in the recorded suite or dependency-specific checks, and no unresolved issue caused by the bump. Dry-run completion requires the structured plan and zero writes.

Files (agent-skills)
  • agents
    • openai.yaml 42 B
      policy:
        allow_implicit_invocation: true
      
  • references
    • conditional-workflows.md 3.5 KB
      # Conditional Workflows
      
      Both sections below are gated: read only when the triggering condition in `SKILL.md` is met.
      
      ## Minimum Release Age Mode
      
      Use this mode for projects that configure a package-manager minimum-age policy.
      
      Discover the effective project and user-level package-manager configuration. A configured age gate makes `@latest` a
      valid initial selector and `^x.y.z` a valid retained manifest range: the installer filters out releases inside the
      cooldown. Do not exact-pin solely to impose the same freshness boundary; Bun still age-gates exact requests but they
      bypass its rapid-release stability check. The age gate is not a reproducibility mechanism; commit the lockfile and use
      frozen installs in deployment. If the deployment resolves dependencies without that lockfile, require the same effective
      age policy there or exact-pin for that workflow.
      
      Taze calls this `maturityPeriod`:
      
      - `--maturity-period [days]` filters out package versions newer than the given number of days
      - `--maturity-period-exclude <packages>` excludes packages from that filter, when supported by the installed Taze
        version
      
      ```bash
      # 7-day cooldown
      taze major -r --maturity-period 7
      ```
      
      For Bun `minimumReleaseAge`, convert seconds to whole days using a ceiling division. Example: `604800` seconds becomes
      `--maturity-period 7`. If the configured seconds are not a whole number of days, round up so Taze is not weaker than the
      package manager policy.
      
      Taze v19.13.0+ auto-infers maturity periods from pnpm and Yarn workspace config, but not from Bun `bunfig.toml`. For Bun
      projects, pass `--maturity-period` explicitly.
      
      For Bun lockfile projects, `run-taze.sh` uses Python 3.11+ through `uv` to parse the global `.bunfig.toml` under
      `$XDG_CONFIG_HOME` (or `$HOME` when unset), then overlay project `[install]` keys. Multiline exclusion arrays are
      supported; an explicit local `0` or empty array overrides the inherited value. See
      [Bun configuration](https://bun.sh/docs/runtime/bunfig#global-vs-local). Verify the installer actually enforces
      inherited age settings; if it ignores them, preserve the policy in project configuration before installing.
      
      When the package manager config has an exclude list, pass matching Taze excludes if available:
      
      ```bash
      taze major -r --maturity-period 7 --maturity-period-exclude react,webpack
      ```
      
      `run-taze.sh` adds these flags itself for Bun lockfile projects and rejects extra options; Taze infers them for pnpm and
      Yarn. Append the same maturity flags only to direct Taze scan and write commands. After Taze writes manifests, run the
      project package manager install as usual; the package manager remains the final enforcement layer for direct and
      transitive resolution.
      
      ## Update Bun Catalogs
      
      When the root `package.json` contains `catalog` / `catalogs` at the top level or under `workspaces`, use
      `scripts/update-bun-catalogs.py` with the saved Taze plan and the selected packages present in catalogs. Skip this
      helper if that subset is empty. Preview before manifest writes; after the baseline passes, rerun the same command with
      `--write`, then perform the selected Taze write and regenerate the lockfile. Taze can update Bun catalogs natively, so
      running it first would invalidate the helper's saved plan.
      
      The helper owns default/named catalog discovery, multiple occurrences, prefix preservation, stale-plan validation, and
      atomic replacement. The agent owns which upgrades are accepted and whether a major migration is compatible. Do not
      manually reproduce the catalog transition or weaken a helper failure.
      
    • multi-repo-alignment.md 3.6 KB
      # Multi-Repository Alignment
      
      Use when the user names two or more repositories or asks to sync or align dependency versions across repositories. Each
      repository keeps its own package manager, range style, age policy, and validation suite; alignment only chooses shared
      targets. Treat an argument that resolves to a directory containing `package.json` as a repository root.
      
      ## 1. Plan Every Repository
      
      Save one plan per repository:
      
      ```sh
      bash <skill-dir>/scripts/run-taze.sh --plan <repo> > <repo-plan.json>
      ```
      
      The helper applies that repository's own minimum-release-age policy, so each row's `available` is the newest version
      that repository admits. Plans list only packages with updates; also record every direct dependency and catalog entry
      from each manifest so packages that are already current still count as shared.
      
      ## 2. Respect Holds
      
      Search each repository for documented holds: `overrides`, `resolutions`, or `pnpm.overrides` entries; exact pins; and
      guidance, changelog, or code comments explaining a pin or version cap. A hold caps that repository's ceiling or excludes
      the package; never override it silently. Report every hold with its source and effect on the shared target.
      
      ## 3. Choose One Shared Target
      
      For every package declared directly in two or more repositories:
      
      - A repository's ceiling is its plan row's `available`, or its current version when its plan has no row.
      - The shared target is the highest version every repository's plan allows: the lowest ceiling.
      - Never move a repository below its current version. When one repository is already ahead of another's ceiling, leave
        the package unaligned and report why.
      - Fixed versions and non-semver protocols stay unchanged unless the user asks otherwise.
      
      Packages found in only one repository follow that repository's plan exactly as in the single-repository workflow.
      
      ## 4. Review Majors Once
      
      Present every shared target that crosses a major version in any repository, plus every `review` or unknown row, in one
      cross-repository decision batch: package, per-repository current → target, package role, and migration notes. Reuse
      explicit approval of those transitions and ask only about unresolved rows; never infer major approval from a package
      name. A declined major leaves that package unchanged everywhere.
      
      ## 5. Apply Per Repository
      
      Run the single-repository baseline (Workflow step 5) in each repository before its first write. Then, from each root:
      
      - Bun catalogs: change catalog definitions, never `catalog:` references in workspace manifests. When the shared target
        differs from the plan's `available`, set that row's `available` to the target in a copy of the saved plan, then run
        `update-bun-catalogs.py` preview and `--write` with that copy.
      - Other direct dependencies: when the target equals the plan's `available`, write with
        `run-taze.sh --write --include <packages>`. Otherwise install the target through the repository's package manager in
        the same dependency section with the existing range prefix (for example `bun add -d pkg@^x.y.z`).
      - Regenerate the lockfile with the repository's package manager.
      
      ## 6. Validate and Commit Per Repository
      
      Rerun each repository's recorded baseline plus the narrowest checks that exercise the updated packages, fixing or
      escalating regressions exactly as in Workflow steps 8-9. Commit each repository separately with only its manifest,
      catalog, lockfile, and required migration changes, following that repository's commit conventions.
      
      ## Report
      
      Use one table: package, shared target, per-repository current → new, and notes. List unaligned packages, declined
      majors, and holds with their sources, then per-repository verification and commit receipts.
      
  • scripts
    • bun-maturity.py 2 KB
      #!/usr/bin/env python3
      # /// script
      # requires-python = ">=3.11"
      # ///
      """Print Taze arguments for the inherited Bun release-age policy."""
      
      import argparse
      import os
      import sys
      import tomllib
      from pathlib import Path
      
      
      def main() -> None:
          parser = argparse.ArgumentParser(description=__doc__)
          parser.add_argument("--exclude-supported", action="store_true")
          args = parser.parse_args()
      
          global_root = os.environ.get("XDG_CONFIG_HOME") or os.environ.get("HOME")
          configs = [Path(global_root) / ".bunfig.toml"] if global_root else []
          configs.append(Path("bunfig.toml"))
          policy = {}
          for config in configs:
              try:
                  with config.open("rb") as source:
                      install = tomllib.load(source).get("install", {})
              except FileNotFoundError:
                  continue
              except (OSError, ValueError):
                  # Configs can contain registry credentials; do not echo parser input.
                  raise ValueError(f"cannot read Bun configuration: {config}") from None
              if not isinstance(install, dict):
                  raise ValueError(f"expected an install table in {config}")
              policy.update(install)
      
          age = policy.get("minimumReleaseAge", 0)
          excludes = policy.get("minimumReleaseAgeExcludes", [])
          if type(age) is not int or age < 0:
              raise ValueError("minimumReleaseAge must be a nonnegative integer")
          if not isinstance(excludes, list) or any(
              not isinstance(name, str) or not name or any(c in name for c in ",\r\n")
              for name in excludes
          ):
              raise ValueError("minimumReleaseAgeExcludes must be an array of package names")
          if age:
              print("--maturity-period")
              print((age + 86399) // 86400)
              if excludes and args.exclude_supported:
                  print("--maturity-period-exclude")
                  print(",".join(excludes))
      
      
      if __name__ == "__main__":
          try:
              main()
          except ValueError as error:
              print(f"ERROR: {error}", file=sys.stderr)
              sys.exit(1)
      
    • parse-taze-plan.py 2.8 KB
      #!/usr/bin/env -S uv run --script
      """Convert stable, no-color Taze output into a machine-readable update plan."""
      
      from __future__ import annotations
      
      import argparse
      import json
      import re
      from pathlib import Path
      
      
      ANSI_RE = re.compile(r"\x1b\[[0-?]*[ -/]*[@-~]")
      VERSION_RE = re.compile(r"^(?:workspace:)?[~^<>=v]*([0-9]+)\.([0-9]+)\.([0-9]+)(?:[-+][0-9A-Za-z.-]+)?$")
      
      
      def version_parts(value: str) -> tuple[int, int, int] | None:
          match = VERSION_RE.match(value)
          return tuple(map(int, match.groups())) if match else None
      
      
      def classify(current: str, available: str) -> str:
          old = version_parts(current)
          new = version_parts(available)
          if old is None or new is None:
              return "unknown"
          if old[0] != new[0]:
              return "major"
          if old[1] != new[1]:
              return "minor"
          return "patch"
      
      
      def parse(text: str) -> list[dict[str, str]]:
          entries: list[dict[str, str]] = []
          for raw_line in ANSI_RE.sub("", text).splitlines():
              if "→" not in raw_line:
                  continue
              left, right = raw_line.split("→", 1)
              left_tokens = left.split()
              right_tokens = right.split()
              if not left_tokens or not right_tokens:
                  continue
              available = right_tokens[0]
              current_index = next(
                  (index for index in range(len(left_tokens) - 1, -1, -1) if version_parts(left_tokens[index]) is not None),
                  None,
              )
              if current_index is None or version_parts(available) is None:
                  continue
              current = left_tokens[current_index]
              package = left_tokens[0]
              update_type = classify(current, available)
              fixed = not current.startswith(("^", "~", ">", "<", "="))
              if fixed:
                  action = "skip-fixed"
              elif update_type == "major":
                  action = "review-major"
              elif update_type in {"minor", "patch"}:
                  action = "apply"
              else:
                  action = "review"
              entries.append(
                  {
                      "package": package,
                      "current": current,
                      "available": available,
                      "type": update_type,
                      "action": action,
                  }
              )
          priority = {"review-major": 0, "review": 1, "apply": 2, "skip-fixed": 3}
          return sorted(entries, key=lambda item: (priority[item["action"]], item["package"]))
      
      
      def main() -> int:
          parser = argparse.ArgumentParser()
          parser.add_argument("--input", required=True, type=Path)
          args = parser.parse_args()
          entries = parse(args.input.read_text(encoding="utf-8", errors="replace"))
          counts = {key: sum(item["type"] == key for item in entries) for key in ("major", "minor", "patch", "unknown")}
          print(json.dumps({"updates": entries, "counts": counts, "total": len(entries)}, indent=2, sort_keys=True))
          return 0
      
      
      if __name__ == "__main__":
          raise SystemExit(main())
      
    • run-taze.sh 3.5 KB
      #!/usr/bin/env bash
      # run-taze.sh - Run taze in non-interactive mode
      #
      # Usage: run-taze.sh [--include pkg1,pkg2] [--concurrency n] [--plan|--write] [path]
      #
      # Automatically detects monorepo projects (workspaces in package.json
      # or pnpm-workspace.yaml) and enables recursive mode.
      #
      # Bun projects with local or global minimumReleaseAge get matching Taze
      # maturity-period flags. Taze does not auto-infer Bun's age gate.
      #
      # Exit codes:
      #   0 - Success (updates displayed)
      #   1 - taze not installed
      #   2 - No package.json found
      #   64 - Usage error
      
      set -euo pipefail
      
      include=""
      concurrency=""
      write=false
      plan=false
      target_dir="."
      script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
      
      while [[ $# -gt 0 ]]; do
        case "$1" in
        --include)
          include="${2:?ERROR: --include requires a value}"
          shift 2
          ;;
        --include=*)
          include="${1#*=}"
          shift
          ;;
        --concurrency)
          concurrency="${2:?ERROR: --concurrency requires a value}"
          shift 2
          ;;
        --concurrency=*)
          concurrency="${1#*=}"
          shift
          ;;
        --write)
          write=true
          shift
          ;;
        --plan)
          plan=true
          shift
          ;;
        -*)
          echo "ERROR: Unknown option: $1" >&2
          exit 64
          ;;
        *)
          if [[ "$target_dir" != "." ]]; then
            echo "ERROR: Only one target path is supported" >&2
            exit 64
          fi
          target_dir="$1"
          shift
          ;;
        esac
      done
      
      # Check for package.json
      if [[ ! -f "$target_dir/package.json" ]]; then
        echo "ERROR: No package.json found in $target_dir" >&2
        exit 2
      fi
      
      cd "$target_dir"
      
      # Refresh registry metadata so cached scans cannot hide newly published releases.
      taze_args=(major --force)
      
      # Auto-detect monorepo
      if grep -q '"workspaces"' package.json 2>/dev/null ||
        [[ -f pnpm-workspace.yaml ]]; then
        taze_args+=("-r")
      fi
      
      # Check taze availability
      if ! command -v taze &>/dev/null; then
        cat >&2 <<'EOF'
      ERROR: taze CLI is not installed.
      
      Install taze globally:
        npm install -g taze
      
      Or run via bunx:
        bunx taze
      
      Documentation: https://github.com/antfu-collective/taze
      EOF
        exit 1
      fi
      
      if [[ -n "$include" ]]; then
        taze_args+=("--include" "$include")
      fi
      
      if [[ -n "$concurrency" ]]; then
        taze_args+=("--concurrency" "$concurrency")
      fi
      
      if [[ "$write" == true && -z "$include" ]]; then
        echo "ERROR: --write requires --include with the selected package list" >&2
        exit 64
      fi
      
      if [[ "$write" == true && "$plan" == true ]]; then
        echo "ERROR: --plan and --write are mutually exclusive" >&2
        exit 64
      fi
      
      # Mirror Bun's delayed-resolution policy for direct dependency candidates.
      # bunfig.toml stores seconds; Taze expects whole days.
      if [[ -f bun.lock || -f bun.lockb ]]; then
        policy_args=()
        if taze --help 2>/dev/null | grep -q -- '--maturity-period-exclude'; then
          policy_args+=("--exclude-supported")
        fi
        # Parse TOML rather than lines: lists may span lines and local keys override global keys.
        maturity_args="$(uv run "$script_dir/bun-maturity.py" ${policy_args[@]+"${policy_args[@]}"})"
        if [[ -n "$maturity_args" ]]; then
          while IFS= read -r argument; do
            taze_args+=("$argument")
          done <<<"$maturity_args"
        fi
      fi
      
      if [[ "$write" == true ]]; then
        taze_args+=("--write")
      else
        # Scan all available updates, including fixed versions (no ^ or ~).
        taze_args+=("--include-locked")
      fi
      
      if [[ "$plan" == true ]]; then
        plan_output="$(mktemp "${TMPDIR:-/tmp}/taze-plan.XXXXXX")"
        trap 'rm -f "$plan_output"' EXIT
        NO_COLOR=1 taze "${taze_args[@]}" --no-group --no-timediff --no-nodecompat --sort name-asc >"$plan_output" 2>&1
        uv run "$script_dir/parse-taze-plan.py" --input "$plan_output"
      else
        taze "${taze_args[@]}" 2>&1
      fi
      
    • update-bun-catalogs.py 6.2 KB
      #!/usr/bin/env python3
      """Preview or atomically apply selected Taze updates to Bun catalogs."""
      
      from __future__ import annotations
      
      import argparse
      import json
      import os
      import re
      import sys
      import tempfile
      from pathlib import Path
      from typing import Any
      
      
      VERSION_RE = re.compile(r"^(?P<prefix>\^|~)?(?P<version>\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)$")
      
      
      class CatalogError(ValueError):
          pass
      
      
      def parse_version(value: Any, context: str) -> tuple[str, str]:
          if not isinstance(value, str) or not (match := VERSION_RE.fullmatch(value)):
              raise CatalogError(f"unsupported version in {context}: {value!r}")
          return match.group("prefix") or "", match.group("version")
      
      
      def selected_names(values: list[str]) -> list[str]:
          names: list[str] = []
          for value in values:
              names.extend(item.strip() for item in value.split(",") if item.strip())
          if not names:
              raise CatalogError("--include must select at least one package")
          return list(dict.fromkeys(names))
      
      
      def plan_updates(plan: dict[str, Any], names: list[str]) -> dict[str, dict[str, Any]]:
          updates = plan.get("updates")
          if not isinstance(updates, list):
              raise CatalogError("plan updates must be an array")
          selected: dict[str, dict[str, Any]] = {}
          for name in names:
              matches = [entry for entry in updates if isinstance(entry, dict) and entry.get("package") == name]
              if not matches:
                  raise CatalogError(f"selected package is missing from plan: {name}")
              normalized = {(entry.get("current"), entry.get("available")) for entry in matches}
              if len(normalized) != 1:
                  raise CatalogError(f"ambiguous plan entries for package: {name}")
              entry = matches[0]
              parse_version(entry.get("current"), f"plan current for {name}")
              parse_version(entry.get("available"), f"plan available for {name}")
              selected[name] = entry
          return selected
      
      
      def catalog_maps(document: dict[str, Any]) -> list[tuple[str, dict[str, Any]]]:
          workspaces = document.get("workspaces")
          if isinstance(workspaces, dict) and ("catalog" in workspaces or "catalogs" in workspaces):
              location = workspaces
              label = "workspaces."
          elif "catalog" in document or "catalogs" in document:
              location = document
              label = ""
          else:
              raise CatalogError("package.json has no Bun catalog configuration")
          maps: list[tuple[str, dict[str, Any]]] = []
          default = location.get("catalog")
          if default is not None:
              if not isinstance(default, dict):
                  raise CatalogError(f"{label}catalog must be an object")
              maps.append(("default", default))
          named = location.get("catalogs")
          if named is not None:
              if not isinstance(named, dict):
                  raise CatalogError(f"{label}catalogs must be an object")
              for catalog_name, catalog in sorted(named.items()):
                  if not isinstance(catalog, dict):
                      raise CatalogError(f"{label}catalogs.{catalog_name} must be an object")
                  maps.append((catalog_name, catalog))
          return maps
      
      
      def update_document(document: dict[str, Any], selected: dict[str, dict[str, Any]]) -> list[dict[str, str]]:
          maps = catalog_maps(document)
          changes: list[dict[str, str]] = []
          for package, entry in selected.items():
              plan_prefix, plan_current = parse_version(entry["current"], f"plan current for {package}")
              _available_prefix, available = parse_version(entry["available"], f"plan available for {package}")
              occurrences = [(catalog_name, catalog) for catalog_name, catalog in maps if package in catalog]
              if not occurrences:
                  raise CatalogError(f"selected package is missing from Bun catalogs: {package}")
              for catalog_name, catalog in occurrences:
                  prefix, current = parse_version(catalog[package], f"catalog {catalog_name} entry for {package}")
                  if current != plan_current:
                      raise CatalogError(
                          f"stale plan for {package} in catalog {catalog_name}: catalog has {current}, plan has {plan_current}"
                      )
                  if plan_prefix and prefix != plan_prefix:
                      raise CatalogError(
                          f"stale plan prefix for {package} in catalog {catalog_name}: catalog has {prefix!r}, plan has {plan_prefix!r}"
                      )
                  replacement = f"{prefix}{available}"
                  changes.append({"package": package, "catalog": catalog_name, "from": catalog[package], "to": replacement})
                  catalog[package] = replacement
          return changes
      
      
      def atomic_write(path: Path, document: dict[str, Any]) -> None:
          encoded = (json.dumps(document, indent=2, ensure_ascii=False) + "\n").encode()
          descriptor, temporary_name = tempfile.mkstemp(prefix=f".{path.name}.", dir=path.parent)
          temporary = Path(temporary_name)
          try:
              with os.fdopen(descriptor, "wb") as handle:
                  handle.write(encoded)
                  handle.flush()
                  os.fsync(handle.fileno())
              os.chmod(temporary, path.stat().st_mode)
              os.replace(temporary, path)
          finally:
              if temporary.exists():
                  temporary.unlink()
      
      
      def main() -> int:
          parser = argparse.ArgumentParser(description=__doc__)
          parser.add_argument("--root", required=True, type=Path)
          parser.add_argument("--plan", required=True, type=Path)
          parser.add_argument("--include", required=True, action="append")
          parser.add_argument("--write", action="store_true")
          args = parser.parse_args()
          package_path = args.root if args.root.name == "package.json" else args.root / "package.json"
          try:
              names = selected_names(args.include)
              document = json.loads(package_path.read_text(encoding="utf-8"))
              plan = json.loads(args.plan.read_text(encoding="utf-8"))
              changes = update_document(document, plan_updates(plan, names))
              if args.write:
                  atomic_write(package_path, document)
          except (OSError, json.JSONDecodeError, CatalogError) as exc:
              print(f"ERROR: {exc}", file=sys.stderr)
              return 64
          print(json.dumps({"schemaVersion": 1, "wrote": args.write, "file": str(package_path), "changes": changes}, indent=2))
          return 0
      
      
      if __name__ == "__main__":
          raise SystemExit(main())
      
  • SKILL.md 8.3 KB
    ---
    argument-hint: "[--dry-run] [package ...] [repo-path ...]"
    effort: medium
    model: sonnet
    name: node-deps-bumper
    description: "Use for dependency updates: bump npm/pnpm/yarn/bun packages, check outdated, or run taze."
    ---
    
    # Node Dependency Bumper
    
    Use Taze to build one structured update plan, apply compatible ranged updates, and make major-version decisions as a
    batch.
    
    ## Adding Dependencies
    
    For a new package or CLI, use the repository's package manager and existing range convention instead of Taze. Installing
    `package@latest` and retaining the package manager's resulting `^x.y.z` range is allowed. Do not replace a caret range
    with an exact version merely because it can admit future releases.
    
    When an effective package-manager minimum-release-age policy exists, it provides the freshness boundary for candidate
    versions; read [references/conditional-workflows.md](references/conditional-workflows.md). The committed lockfile and
    frozen deployment install provide reproducibility. Exact-pin only when the user or repository requires it, a known
    compatibility constraint justifies it, or the package will run without a committed lockfile and frozen install.
    
    ## Multiple Repositories
    
    When the user names two or more repositories or asks to sync or align dependencies across repositories, read
    [references/multi-repo-alignment.md](references/multi-repo-alignment.md). It runs the Workflow below per repository and
    adds shared-target selection, one cross-repository major batch, and per-repository commits.
    
    ## Workflow
    
    1. Resolve the skill directory and save the helper plan from the target repository:
    
       ```sh
       bash <skill-dir>/scripts/run-taze.sh --plan [--include package-a,package-b] > <taze-plan.json>
       ```
    
       The JSON plan classifies every discovered update as `apply`, `review-major`, `review`, or `skip-fixed`. The helper
       detects monorepos, includes locked versions during scans, and mirrors Bun minimum-release-age settings. If the
       repository uses package-manager age gates or Bun catalogs, read
       [references/conditional-workflows.md](references/conditional-workflows.md) for that active branch only.
    
    2. If `--dry-run` was requested, present the plan and counts, then stop without changing manifests or lockfiles.
    
    3. Select every ranged minor/patch update marked `apply`. Reuse explicit approval of package/version transitions; never
       auto-approve a major merely from its package name. Present unresolved `review-major` and unknown updates in one
       decision batch with current version, target version, package role, and relevant migration/release notes. Apply only
       authorized majors.
    
    4. If nothing is selected, report the no-op and stop. If the root manifest uses Bun catalogs, preview the exact selected
       catalog transitions from the accepted plan. Pass only selected packages present in a catalog; skip the helper when
       that subset is empty:
    
       ```sh
       uv run <skill-dir>/scripts/update-bun-catalogs.py \
         --root <repo> --plan <taze-plan.json> --include package-a,package-b
       ```
    
       The preview is read-only. Missing catalog entries, conflicting plan rows, unsupported versions, or a catalog value
       that no longer matches the plan fail before writes. The helper does not select upgrades.
    
    5. Before the first manifest or lockfile write, select and run a baseline that can detect regressions from the chosen
       updates. Honor repository-required checks; otherwise select dependency-resolution, build, test, typecheck, lint,
       formatting, codegen, or invariant checks from the updated packages' actual consumers and risk. Use an aggregate suite
       for shared toolchain or runtime changes, not merely because it exists. Use frozen or non-writing modes where
       available, and record the exact commands and coverage for the post-bump rerun. Attribute every failure before
       deciding whether it blocks. Proceed when an unrelated pre-existing failure is reproducible, can be compared after the
       bump, and does not prevent dependency resolution or the checks needed to detect regressions; do not fix it as part of
       the bump. Dependency or peer-resolution conflicts, actionable unsafe behavior, or a baseline that cannot provide
       trustworthy before/after signal block the bump. In that case, stop with
       `### ⛔ Dependency bump blocked — baseline unusable` and report the exact prerequisite and diagnostics without asking
       for redundant authorization. Informational notices such as unavoidable deprecations do not block.
    
    6. For Bun catalogs, apply the previewed transitions first: rerun `update-bun-catalogs.py` with the same plan and
       catalog include subset plus `--write`. It atomically updates every matching default/named catalog occurrence and
       preserves each existing `^`, `~`, or empty prefix. Apply before Taze, whose native catalog writes would make this
       plan stale.
    
       Then write all selected Taze updates in one command:
    
       ```sh
       bash <skill-dir>/scripts/run-taze.sh --write --include package-a,package-b
       ```
    
    7. Run `ni` so the repository's package manager updates its lockfile.
    
    8. Inspect the manifest and lockfile diff. Rerun the exact baseline commands, plus the narrowest checks that exercise
       the updated dependencies and any required migrations. Treat every newly introduced error, type issue, check failure
       (including tests, builds, lint, formatting, codegen, and repository invariants), dependency or peer-resolution
       conflict, and actionable compatibility or safety warning as caused by the bump unless evidence shows otherwise.
    
    9. Fix every issue caused by the bump, including required source or configuration migrations, while preserving intended
       behavior. Do not suppress diagnostics, weaken validation, or change expected behavior merely to make checks pass.
       After each fix, rerun checks whose inputs or behavior changed. At completion, require valid results for the entire
       recorded suite against the final state, with no new failures against the accepted baseline; reuse passing results
       whose inputs remain unchanged. Only unrelated pre-existing failures meeting step 5's comparison requirements may
       remain. If no clear safe fix exists within the task's authority, stop with
       `### ⚠️ Dependency regression decision required`. Present all such issues in one table with the evidence, affected
       locations, fix and revert options, and likely effects. Do not report completion until the user chooses, the fix is
       applied or the offending update is reverted, the lockfile is regenerated, and the complete suite meets that same
       baseline-comparison requirement.
    
    ## User-Facing Output
    
    Present plans as `### 📦 Dependency plan` with counts and a compact table:
    
    | ID  | Plan value | Decision | Package | Current → target | Type | Notes |
    | --- | ---------- | -------- | ------- | ---------------- | ---- | ----- |
    
    Use the plan's exact `apply`, `review-major`, `review`, and `skip-fixed` values alongside plain-language decisions.
    Assign stable IDs to rows needing a choice so the user can answer once. Use `### 🔎 Dry run — no files written` for a
    preview and `### ✅ No selected updates` for a no-op.
    
    Finish applied work with `### 🏁 Dependencies updated`, a tree of changed manifests/lockfiles, and
    `### 🧪 Verification`. Use `### ⚠️ Remaining review` only for non-blocking informational matters, never for an
    unresolved issue caused by the bump. Keep helper JSON, package/version strings, commands, and diagnostics exact and
    undecorated.
    
    ## Invariants
    
    - Fixed versions and non-semver protocols remain unchanged unless the user explicitly asks otherwise.
    - Caret ranges are valid for new dependencies and CLIs; do not describe them as unreproducible when a committed lockfile
      and frozen install control resolution.
    - Package arguments constrain both scan and write phases.
    - The same maturity-period policy applies to scan and write.
    - Bun catalog preview and write use the same accepted Taze plan and selected package set; stale plans never write.
    - Do not infer compatibility from SemVer alone when repository evidence, peer ranges, or release notes indicate
      otherwise.
    
    Completion requires an attributed, comparison-safe pre-write baseline, a reviewed plan, the retained selected updates, a
    regenerated lockfile, no new failure in the recorded suite or dependency-specific checks, and no unresolved issue caused
    by the bump. Dry-run completion requires the structured plan and zero writes.
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related