Skip to content

piotar/pkg-sync

Repository files navigation

pkg-sync

GitHub package.json version npm (scoped) node-current (scoped) NPM

Develop a local package and see it live in another project — without symlinks and without touching any package.json.

pkg-sync watches a package's build output and copies it straight into the consuming project's node_modules, so the project sees it exactly as if it were installed from the registry.

Why not npm link?

npm link creates a symlink, and symlinks break in many real-world setups: bundlers resolving a dependency twice (duplicate React, hooks errors), peerDependencies resolved from the wrong tree, tools that don't follow symlinks, and Windows quirks. Publishing to a registry on every change is too slow for tight feedback loops.

pkg-sync avoids all of that by copying real files into node_modules/<package>. Nothing in the consuming project changes — no symlink, no edited package.json, no lockfile churn — so the dependency behaves like a normal install.

How it works

  • You register a package once by its path. Its name and location are stored in a global registry at ~/.pkg-sync/data.json.
  • On sync, pkg-sync reads the target project's package.json, resolves its dependency tree up to a configurable depth (default 2), and intersects it with the registered packages. Every match is mirrored from its source into the project's node_modules.
  • Only relevant files are copied: by default the dist, lib, build and src directories, minus common noise (VCS folders, lockfiles, editor files). A package can override which directories are watched.
  • With watching enabled (the default), changes in a source package are debounced and re-copied automatically, giving a live local feedback loop.

Installation

npm install -g @piotar/pkg-sync

Requires Node.js >= 22.

Usage

We have a main project (App) and 2 dependencies (Ui and Store) in different locations (this is not a monorepo):

~
├── projects
│   └── App
│       ├── node_modules
│       ├── package.json
│       └── etc...
└── external
    ├── Ui
    │   ├── node_modules
    │   ├── package.json
    │   └── etc...
    └── Store
        ├── node_modules
        ├── package.json
        └── etc...
  1. Register each dependency — from ~/external/Ui and ~/external/Store run pkg-sync add . (. resolves the name from the closest package.json).
  2. From ~/projects/App run pkg-sync sync . — this copies the dependency files into App/node_modules and starts watching for changes.

That's it. Step 1 is only needed the first time; afterwards a single pkg-sync sync . is enough.

Configuration

Settings live in the same global registry and are managed with pkg-sync config:

  • pkg-sync config get — print all settings.
  • pkg-sync config set depth 3 — how deep the dependency tree is searched during sync/validate (values are parsed as JSON).
  • pkg-sync config restore — reset to defaults.

To watch non-default directories for a specific package, pass them when registering: pkg-sync add . -d dist,types.

Colored output honors the NO_COLOR environment variable and is disabled automatically when output is not a TTY.

Using from automation / AI agents

Every command accepts --json and prints a single JSON object on stdout:

pkg-sync list --json
pkg-sync validate /path/to/app --json      # { "packages": ["my-lib"] }
pkg-sync sync /path/to/app --no-watch --json   # { "synced": ["my-lib"], "watch": false }
pkg-sync status --json                          # { "targets": [{ "path": "/app", "packages": ["my-lib"], "syncedAt": 0, "stale": false }] }
pkg-sync unsync /path/to/app --json             # { "unsynced": ["my-lib"], "reinstalled": true }

The output contract:

  • stdout carries data only — human text, or one JSON line under --json. Diagnostics, progress and the update notice go to stderr, so pkg-sync list --json is always parseable.
  • Exit codes: 0 on success, 1 on error. Under --json, errors are emitted as {"error": "<message>"} on stderr.
  • Stay non-interactive: always pass --no-watch (the watcher never returns) and avoid -i/--interactive (it needs a TTY) — pass package names/paths explicitly instead.

A SKILL.md (agentskills.io format) ships with the package so the tool can be taught to AI agents and installed via a skill manager.

Claude Code plugin

The package doubles as a Claude Code plugin — a .claude-plugin/plugin.json manifest exposes the bundled SKILL.md as a pkg-sync skill. Load it straight from node_modules (no separate install):

claude --plugin-dir node_modules/@piotar/pkg-sync     # per-session

To make the skill available globally instead, symlink the installed package into your skills directory — it then auto-loads in every session:

ln -s "$(npm root -g)/@piotar/pkg-sync" ~/.claude/skills/pkg-sync

Either way Claude gains a pkg-sync skill that knows when and how to mirror your local package into another project.

Commands

Run pkg-sync <command> --help for the full, up-to-date options of any command.

add [path]

Register a package so it can be synced. path defaults to the closest package.json.

  • -n, --name <name> — override the package name (instead of the one in package.json)
  • -f, --force — overwrite an existing registration
  • -d, --dir <dirs> — comma-separated directories to watch, overriding the defaults

remove [packages...] (alias rm)

Unregister packages. Use . for the name in the closest package.json.

  • -i, --interactive — pick packages to remove from a list
  • -a, --all — remove every registered package

list (alias ls)

Show the data file path, the default watch directories, and every registered package.

validate [path]

Preview which registered packages would be synced for a project, without copying anything.

  • -d, --depth <n> — dependency search depth (default 2)

sync [path] [packages...]

Copy registered dependencies into a project's node_modules and watch for changes. Pass package names to limit the sync to a subset.

  • --no-watch — copy once and exit instead of watching
  • -i, --interactive — pick packages to sync from a list
  • -d, --depth <n> — dependency search depth (default 2)

unsync [path] (alias restore)

Remove previously synced files from a project and restore the published versions. Operates on the syncs recorded for the project (see status); path defaults to the closest package.json.

  • --no-reinstall — only delete the synced files, skip the package-manager reinstall
  • -i, --interactive — pick which synced packages to unsync from a list

The package manager is detected from the project's lockfile (bun, pnpm, yarn, else npm).

status [path]

Show which packages are currently synced into which projects. Without path, every recorded target is listed; a target is flagged stale when its node_modules no longer exists.

config <set|get|restore>

Manage stored settings.

  • config set <key> <value> — set a value, parsed as JSON (e.g. config set depth 3)
  • config get [key] — print one value, or all settings when no key is given
  • config restore — reset the config to defaults

update-check

Check npm for a newer version of pkg-sync.

About

Develop a local package and see it live in another project — without symlinks and without touching any package.json.

Resources

License

Stars

Watchers

Forks

Contributors