@piotar/pkg-sync
v2.4.0
Published
A utility for synchronization multiple packages, without any symlinks and any changes in projects(package.json, etc.).
Maintainers
Readme
pkg-sync
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-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.
Installation
npm install -g @piotar/pkg-syncRequires 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...- 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.
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 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.
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, 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.
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-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.
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 inpackage.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 (default2)
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 (default2)
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 givenconfig restore— reset the config to defaults
update-check
Check npm for a newer version of pkg-sync.
