FNSPM runs your existing package manager and optionally keeps node_modules in a
separate local directory connected by a symlink. It supports npm, Yarn, pnpm,
Bun, and Deno command forwarding. Node.js 22 or newer is required.
This is a wrapper: native command names and flags retain their meaning. It does not make installs inherently faster or make cloud providers share one exclusion mechanism.
Install and run
npm install -g fnspm@0.3.0
fnspm --help
fnspm --version
fnspm install # auto-detect the project's manager
fnspm --pm npm install lodash --save-dev
fnspm --pm yarn add lodash
fnspm --pm pnpm add lodash
fnspm --pm bun add lodash
fnspm --pm deno install npm:lodash
fnspm --debug run build
fnspm run build -- --debug # pass --debug to the underlying command
The selected package manager must already be installed and on PATH. FNSPM
preserves argument boundaries, terminal input/output, child exit codes, and
interrupts. It does not install or pin the manager version recorded in your
manifest; doctor reports the executable version actually available.
--pm <manager> and --debug are FNSPM options before --. Arguments after
--, including wrapper-looking flags, pass through unchanged. help,
initialize, init-config, doctor, migrate, and restore are reserved
FNSPM commands. fnspm init still invokes the native manager's init command.
Use the manager directly if it has a command that collides with a reserved name.
Configuration
Initialization is optional. Without a config file, FNSPM uses npm as its fallback,
auto-detection, and project-local node_modules.nosync storage.
fnspm initialize # interactive terminal wizard
fnspm initialize --default # noninteractive defaults
fnspm initialize --pm pnpm --detection auto --no-symlink
Initialization writes into the current directory, including a workspace section.
It writes fnspm.config.cjs, or fnspm.config.mjs for projects with
"type": "module". Existing configuration is never overwritten. .js files are
also supported according to the project's module type. Keep exactly one config
file. Invalid configuration stops execution rather than enabling default settings
unexpectedly.
Partial configuration works; omitted settings receive these defaults:
/** @type {import('fnspm').UserConfig} */
const config = {
packageManager: { default: 'npm', detection: 'auto' },
symlink: {
enabled: true,
addToGitIgnore: true,
nosyncName: 'node_modules.nosync',
// storagePath: '/absolute/path/outside/synced-folders/my-project-deps',
},
debug: { verbose: false },
};
module.exports = config; // use `export default config` in .mjs / ESM .js
Config files execute as JavaScript, like a package manager's project scripts. Only load configurations from projects you trust.
| Initialization option | Effect |
|---|---|
--default, -d |
Use defaults without prompting; other options still override them |
--pm <manager> |
Set fallback manager |
--detection <mode> |
auto, default, or a manager name |
--symlink, --no-symlink |
Enable or disable automatic migration |
--sync-folder <name> |
Set a single project-local directory name |
--storage-path <absolute-path> |
Set a dedicated storage directory outside the project |
--no-sync-folder |
Alias for --no-symlink |
--add-to-gitignore, --no-add-to-gitignore |
Enable or disable generated ignore rules |
--verbose, --no-verbose |
Enable or disable command diagnostics |
Legacy boolean forms such as --symlink true and --verbose false also work.
Detection and workspaces
Selection precedence:
--pmfor the current invocation.- Explicit config detection (
defaultor a manager name). package.json#packageManager, such as"pnpm@10.0.0".- A single lockfile manager.
- Deno configuration, then the configured fallback.
Recognized locks: package-lock.json, npm-shrinkwrap.json, yarn.lock,
pnpm-lock.yaml, bun.lock, bun.lockb, and deno.lock. Bun's two formats count
as one manager. Conflicting managers without a manifest declaration require an
explicit choice instead of silently selecting npm.
Configuration and manager detection search upward for the nearest project or
workspace root, stopping at a Git boundary. Root markers include a config file,
a lockfile, pnpm-workspace.yaml, deno.json/deno.jsonc, or a manifest with
workspaces. A nested project with its own root marker is treated independently.
Configuration is resolved independently: the closest config applies, including
one inherited from a parent project, until another config or Git boundary. A
section config overrides the parent config as a whole; its omitted fields receive
the built-in defaults. Local migration state or a generated section lockfile does
not discard an inherited configuration. The underlying command still runs in the
directory from which you invoked it, so workspace/package context is preserved.
For example, initialize inside packages/web to give it its own manager and
web.nosync directory; packages/api can use different settings or inherit the
parent config. Each section's local dependency directory is converted separately.
Inspect, migrate, and restore
fnspm doctor # read-only diagnostics; nonzero for errors
fnspm doctor --pm bun
fnspm migrate --dry-run # inspect without writing files
fnspm migrate # migrate existing node_modules explicitly
fnspm restore --dry-run
fnspm restore # return tracked dependencies to node_modules
Migration renames the directory and creates a relative symlink on macOS/Linux or
an absolute junction on Windows. It refuses existing destinations, foreign
symlinks, unsafe paths, and cross-filesystem moves. If link creation fails, it
attempts to return the original directory to its previous location. FNSPM records
the directory identity in .fnspm-state.json; restoration refuses storage that
was replaced by another directory. Do not copy this state file between projects.
Automatic conversion follows successful native commands, preserving the previous
behavior for install, add, run, list, and other commands. It only converts
node_modules in the current invocation directory, including workspace sections;
it does not start managing ancestor or sibling directories. Global,
redirected, dry-run, and help/version invocations are excluded. doctor itself is
read-only.
Tracked local storage is restored before dependency-changing commands (even if
automatic migration was disabled), so clean installs can replace node_modules.
Matching legacy project-local nosync links are tracked before a clean install;
no dependency files are moved during adoption. After success, local dependencies
are converted again if enabled. Failed installs keep their native exit status
and leave the native layout available for repair. An optional conversion failure
prints a diagnostic without claiming the package manager itself failed.
An install from a workspace section can also replace the shared root dependencies. If that workspace root already has tracked storage, FNSPM restores it before the command and converts it again after success using the root's own configuration. An untracked ancestor directory is left in its native layout.
With addToGitIgnore, migration adds root rules for node_modules, project-local
storage, migration state, and the operation lock. Both link and target must be
ignored; node_modules/ alone does not necessarily ignore the symlink itself.
restore retains ignore rules and configuration. Use --no-symlink / edit the
configuration if future installations should remain in the native layout.
Storage outside synced folders
Set storagePath to an absolute, dedicated and currently nonexistent directory
outside the project. Its parent must already exist on the same filesystem. Choose
a different destination for every project. Dependencies containing relative links
to external workspace or linked-package directories cannot be relocated when that
would break those links; use project-local storage in that case.
.nosync is an iCloud-oriented convention/workaround, not a cross-provider API.
FNSPM does not configure or verify iCloud, Dropbox, or Google Drive exclusion.
For Dropbox, use its documented ignored-files mechanism.
For other providers, verify their exclusion behavior yourself or place dependency
storage outside the synced folder. Automatic migration happens after installation,
so files may be synced while an install is running. A symlink by itself is not a
guarantee about a provider's behavior.
Layouts and recovery
- Modern Yarn defaults to Plug'n'Play, which has
no
node_modules. FNSPM forwards commands and skips directory migration. - Deno can use a global cache or local
node_modules, depending on its configuration. Only an existing local dependency directory can be migrated. - A valid project-local symlink from FNSPM 0.2 that matches
nosyncNameis adopted without moving or deleting dependency files. Runmigrate --dry-runto inspect adoption, ormigrateto record ownership beforerestore. Automatic conversion also adopts these links. Foreign, broken, and untracked external links are never adopted or moved automatically. - If a migration was interrupted,
doctoridentifies broken links/state. A tracked target with a missing source can be recovered usingmigrateorrestore. A stale.fnspm-operation.lockrequires checking that the recorded process is no longer running before removing the lock. Never delete dependency storage to resolve a lock or destination conflict. - Avoid simultaneous installs/migrations. The FNSPM operation lock coordinates migration/restoration, not other package managers or cloud clients.
- Restore dependencies before moving/renaming a project or changing storage to another device. Absolute links pointing into the storage must be changed to relative links before restoration. Recorded ownership and absolute external paths are local to the original project location.
Development and validation
npm ci
npm run check
npm test
npm run test:package
npm run test:managers -- npm bun pnpm # optional; each manager must be on PATH
npm run format:check
npm audit
The repository uses one npm lockfile. Tests cover CLI forwarding, CJS/ESM configs,
workspace detection, migration collisions, rollback, interrupted operations,
restoration, and publish-artifact installation/type checking. prepack builds the
CLI and declarations automatically. Importing fnspm does not execute the CLI.
CI runs on Node 22/24 across macOS, Linux, and Windows. Cloud-provider behavior is
not part of automated tests, and command forwarding is not a promise that every
historical package manager version shares the same dependency layout.
License and contributing
GPL-3.0-only. Commercial use is permitted under the GPL's terms; see GNU's licensing FAQ. The previous README's non-commercial restriction was inconsistent with the GPL license file and has been removed. Contributions are welcome via pull requests.
Author: Marin-Eusebiu Șerban.