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.
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.
- 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-syncreads the target project'spackage.json, resolves its dependency tree up to a configurable depth (default2), and intersects it with the registered packages. Every match is mirrored from its source into the project'snode_modules. - Only relevant files are copied: by default the
dist,lib,buildandsrcdirectories, 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.
npm install -g @piotar/pkg-syncRequires Node.js >= 22.
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...
- Register each dependency — from
~/external/Uiand~/external/Storerunpkg-sync add .(.resolves the name from the closestpackage.json). - From
~/projects/Apprunpkg-sync sync .— this copies the dependency files intoApp/node_modulesand starts watching for changes.
That's it. Step 1 is only needed the first time; afterwards a single pkg-sync sync . is enough.
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 duringsync/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_COLORenvironment variable and is disabled automatically when output is not a TTY.
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, sopkg-sync list --jsonis always parseable. - Exit codes:
0on success,1on 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.
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-sessionTo 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-syncEither way Claude gains a pkg-sync skill that knows when and how to mirror your local package into another project.
Run pkg-sync <command> --help for the full, up-to-date options of any command.
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 inpackage.json)-f, --force— overwrite an existing registration-d, --dir <dirs>— comma-separated directories to watch, overriding the defaults
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
Show the data file path, the default watch directories, and every registered package.
Preview which registered packages would be synced for a project, without copying anything.
-d, --depth <n>— dependency search depth (default2)
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 (default2)
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).
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.
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 givenconfig restore— reset the config to defaults
Check npm for a newer version of pkg-sync.