npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

refactor-tracker

v0.6.0

Published

Run configurable shell detections to track and report technical-refactor progress.

Downloads

1,194

Readme

refactor-tracker

npm version npm downloads CI License: Apache 2.0

[!NOTE] Beta — pre-1.0. The CLI flags, config schema, and reporter API may change between minor releases. Pin an exact version in CI until 1.0 ships.

A language-agnostic CLI that runs configurable shell detection commands, counts progress for each tracked refactor, and reports deltas to pluggable outputs. Designed to run in CI (on merge) and locally on demand.

The tool is a number collector and reporter. Detection is fully delegated to the shell: a command must print a non-negative integer to stdout — what produces that number (grep, ast-grep, ts-morph, a custom script, …) is up to you.

Install

pnpm add -D refactor-tracker
# or run ad hoc
pnpm dlx refactor-tracker

Configuration

Create .refactor-tracker.yml at your repo root (override with --config <path>):

reporters:
  - type: stdout
  - type: markdown
    output: docs/refactor-progress.md
  - type: json
    output: .refactor-report.json

refactors:
  - id: lazy-routes
    name: Importer dynamiquement les routes
    description: Lazy-load top-level route views to cut the initial JS bundle
    detect:
      done:
        command: "grep -rl 'React.lazy' frontend/src/views | wc -l | tr -d ' '"
      total:
        command: "ls frontend/src/views | wc -l | tr -d ' '"

  - id: react-hook-form
    name: Replace custom useForm with react-hook-form
    detect:
      done:
        command: "grep -rl 'react-hook-form' frontend/src | wc -l | tr -d ' '"
      remaining:
        command: "grep -rl 'hooks/useForm' frontend/src | wc -l | tr -d ' '"

  - id: upgrade-somelib
    name: Upgrade somelib to v3
    detect:
      command: 'node -e "process.exit(require(''./package.json'').dependencies.somelib.startsWith(''3'') ? 0 : 1)"'
      binary: true

Detection shapes

Provide any two of done / remaining / total and the third is computed:

| Fields provided | Computed | | --------------------- | ------------------------------------------------------------ | | done + total | remaining = total − done | | done + remaining | total = done + remaining | | remaining + total | done = total − remaining | | binary: true | total = 1, done = 1 if the command exits 0, else 0 |

Each command must print a non-negative integer to stdout (binary commands signal via exit code instead).

An optional description field gives a one-line context blurb. It flows through to the JSON output, renders as a subtitle in the HTML reporter, and adds a Description column to the markdown reporter (the column is omitted entirely when no refactor has one). The stdout reporter ignores it.

Listing remaining items

Optionally attach a list command alongside the counts to surface the actual items left to migrate (file paths, symbols, …) — one per line on stdout:

refactors:
  - id: lazy-routes
    name: Lazy-load top-level routes
    detect:
      done: { command: "grep -rl 'React.lazy' frontend/src/views | wc -l" }
      total: { command: 'ls frontend/src/views | wc -l' }
      list:
        {
          command: "comm -23 <(ls frontend/src/views | sort) <(grep -rl 'React.lazy' frontend/src/views | xargs -n1 basename | sort)",
        }

The list command only runs when remaining > 0. Markdown and HTML reporters render the items in a collapsible <details> block per refactor; the JSON reporter serializes them as items: string[]; the stdout reporter ignores them. The list is not allowed with binary: true.

Reporter output paths are resolved against the config file's directory, so relative paths Just Work regardless of where the CLI is invoked from. Absolute paths are used as-is.

Reporter config values that are exactly $VAR (e.g. token: $MY_TOKEN) are expanded from the environment at runtime and never stored. A missing variable is a hard error.

CLI

refactor-tracker [options]

  -c, --config <path>          Path to config file (default: .refactor-tracker.yml)
  --dry-run                    Run detections and print the report as JSON; do not invoke reporters
  --fail-on-regression         Exit 1 if any task's done count decreased vs the cache
  --report-output <path>       Write the full Report as JSON to this path, independent of reporters
  --reporter <type[:path]>     Override configured reporters; repeatable (e.g. --reporter stdout
                               --reporter markdown:out.md). `custom` reporters are config-only.
  --id <id>                    Filter refactors by id; repeatable, OR semantics. Combines with
                               --tag via AND.
  --no-cache                   Skip reading and writing the cache file; delta becomes null
  --cache-path <path>          Override cache location (default: next to the config)

--report-output is useful when a downstream tool (CI script, GitHub Action, dashboard) needs the typed Report alongside whatever reporters fire. Reporters in the config still run; the file is written after detections succeed and works with or without --dry-run. The path is resolved against the current working directory.

--reporter replaces any reporters declared in the config for that run. Use stdout on its own, or <type>:<path> for the file-based reporters — handy for a one-off Markdown dump without editing the YAML:

refactor-tracker --reporter markdown:./reports/now.md
refactor-tracker --reporter stdout --reporter json:./reports/now.json

--no-cache is convenient in ephemeral environments where caching has no meaning (one-shot CI containers, ad-hoc runs); since delta is always null, --fail-on-regression is effectively a no-op when combined with it. --cache-path is useful when you want the cache somewhere other than next to the config (e.g. under a CI artifact directory). The path is resolved against the current working directory.

GitHub Action

- name: Sync refactor progress
  run: pnpm dlx refactor-tracker

# Regression guard on PRs
- name: Check for regressions
  run: pnpm dlx refactor-tracker --fail-on-regression --dry-run

Tagging

Attach tags to any refactor to group reports and filter from the CLI:

refactors:
  - id: lazy-routes
    name: Lazy-load top-level routes
    tags: [frontend, performance]
    detect: { ... }

Tags are optional and a refactor may carry any number of them. When any refactor has a tag, stdout, markdown, and html reporters render one section per tag (a refactor with N tags appears in N sections). Untagged refactors fall into a trailing Untagged group; the group is omitted when every refactor has at least one tag.

Filter with --tag (repeatable, OR semantics):

refactor-tracker --tag frontend
refactor-tracker --tag frontend --tag performance   # any refactor with frontend OR performance
refactor-tracker --tag=frontend                     # = form also accepted

Skipped refactors keep their cache entries from previous runs, so partial runs don't break --fail-on-regression on the next full run.

Milestones

Each refactor carries two timestamps:

  • registeredAt — when the tracker first saw the refactor.
  • completedAt — the first run that observed 100% progress. Sticky: it is never cleared, even if the count regresses afterwards.

Milestones live in .refactor-tracker-state.json next to your config. Unlike the cache, this file should be committed so milestones travel with the codebase and are available in CI.

Backfilling registeredAt

For refactors that predate adopting the tracker, set registeredAt in the YAML and it will win over whatever is in the state file:

refactors:
  - id: migrate-to-typescript
    name: Migrate to TypeScript
    registeredAt: 2026-03-12 # ISO date or full timestamp
    detect: ...

Flags

  • --show-completed — include refactors that have already reached 100% (hidden by default).
  • --sort-by registered|completed|progress — sort tasks by milestone or progress. Default is config order. registered is ascending (oldest first), completed is descending (most recent first), progress is ascending (least done first). Refactors with no value for the chosen key sort last.

These are presentation concerns: they apply to stdout, markdown, html, and custom reporters. The json reporter always emits the full unfiltered, unsorted dataset.

Reporters

| Reporter | Output | | ---------- | ------------------------------------------------------------------------------------------- | | stdout | Progress table to the terminal (default when no reporters are configured) | | json | The full report object to a file (output: <path> required) | | markdown | A progress table to a .md file (output: <path> required) | | html | A self-contained HTML page with progress bars to a .html file (output: <path> required) | | custom | Your own module — file path or npm package; extension point for Slack, Linear, Notion, etc. |

Custom reporters

A custom reporter loads an external module and uses it as a Reporter implementation. Reference it by either a relative file path or an npm module specifier:

reporters:
  # Local file
  - type: custom
    path: ./reporters/slack.mjs

  # npm package
  - type: custom
    module: refactor-tracker-notion-reporter
    token: $NOTION_TOKEN
    databaseId: 1a2b3c4d-...
    dataSourceId: 0a1b2c3d-...

Exactly one of path or module is required. If the module's default export is a function, it's called as a factory with the reporter config (minus type, module, and path) and must return a Reporter. Otherwise the default export is used directly as the Reporter:

import type { Reporter } from 'refactor-tracker';

// Shape A — default-export an instance:
const reporter: Reporter = {
  async report(report) {
    // report.tasks: { id, name, done, total, percentage, delta, ... }[]
    // report.hasChanges: skip expensive work when nothing changed
  },
};
export default reporter;

// Shape B — default-export a factory that receives the config block:
export default function createReporter(config: { token: string }): Reporter {
  return {
    async report(report) {
      /* … */
    },
  };
}

$VAR references in any reporter field are expanded against process.env (missing variables are a hard error).

Notion

To sync each snapshot to a Notion database (donut, per-task progress bars, table on a private team page), install refactor-tracker-notion-reporter and wire it as a custom reporter. See the package README on npm for the one-time Notion setup (integration, database schema, page layout, where to find both IDs).

How it works

Each run compares current counts against .refactor-tracker-cache.json (gitignored) to compute per-task delta and a global hasChanges flag, so reporters can skip noisy or expensive work when nothing moved. The cache is not updated in --dry-run mode.