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

vasari-sync

v0.6.3

Published

Sync non-VCS project files (.env, secrets, local config) to storage you already own

Downloads

1,468

Readme

vasari-sync (vsync)

A cross-platform CLI for syncing the project files that must never touch version control — .env files, secrets, internal docs, per-machine config — to storage you already own: your own S3 bucket, your own SFTP server, your own WebDAV share, or a private GitHub repo.

Why this exists

Every project has a handful of files that are too sensitive for a shared git repo but still need to follow you between machines: your laptop and your desktop, or you and a teammate. The usual answers are bad ones:

  • Commit them — leaks secrets into history forever.
  • Recreate by hand on every machine — drifts, gets forgotten, breaks at the worst moment.
  • A shared cloud drive — works until you want S3, or SFTP, or a repo you already run.

vsync closes that gap. git clone carries your committed files; one vsync pull carries the rest.

⚠️ Security model — read this first

vsync does not encrypt anything. Files are uploaded to your backend exactly as they are on disk. The security model is simple:

Trust the backend, not the tool.

  • Use a private backend (a private bucket, your own server, a private repo). vsync is not a "safe to push to a public repo" tool.
  • Credentials (S3 keys, SFTP passwords, GitHub tokens) are stored locally in ~/.vsync/config.json with 0600 permissions (owner-only) — readable by your user account and anything running as it. There is no OS-keychain integration in v1; the file permission is the boundary.
  • vsync status and vsync diff print paths only by default. Content diffs are opt-in (vsync diff --show-values) and print actual values to your terminal.

Requirements

  • Node.js ≥ 22.13
  • git on your PATH (file discovery uses real .gitignore semantics)

Install

npm install -g vasari-sync

Or run it without installing:

npx vasari-sync --help

Quickstart

Machine A (first time):

# 1. Point vsync at storage you own (interactive; tests the connection
#    before saving). Secrets go to ~/.vsync/config.json, never the project.
vsync config

# 2. Inside your project (a git repo): pick a project ID and which
#    git-ignored files to track — a folder tree; suggested files (.env*,
#    *.pem, id_rsa*, *secret*, ...) are pre-checked, folders select whole
#    subtrees. Nested git repos ignored by the parent are scanned too.
vsync init

# 3. Upload the tracked files.
vsync push

# Later — see what changed and sync it:
vsync status        # paths only, cheap (one backend listing)
vsync diff          # differing paths + untracked candidates
vsync push

init writes .vsync/manifest.json (backend name, tracked paths + hashes) and keeps .vsync/ out of git — the manifest lists your secret paths, so it must never reach the repo. Nothing machine-specific enters the project: each machine resolves settings and credentials from its own ~/.vsync/config.json profile.

Machine B (or a fresh clone):

git clone <your-repo> my-project && cd my-project

vsync config        # once per machine: save backend settings + credentials
vsync link my-project   # rebuild the manifest from the backend, then pull
                        # (project ID = what `vsync list` shows on machine A)

From then on, the loop on any machine is: work → vsync push; sit down elsewhere → vsync pull. Comparison is live two-way: the current local files against the backend's current state (tracked in a small .vsync-index.json sidecar vsync keeps on the backend). A differing file is simply overwritten in the direction you choose — push makes the remote match local (files deleted locally are deleted remotely), pull makes local match the remote. Both show the full plan and ask for confirmation first (--yes to skip).

Full command reference: docs/commands.md.

Supported backends

| Backend | For | Fields prompted by vsync config | | ------------- | ---------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | s3 | AWS S3, MinIO, Cloudflare R2, Backblaze B2, DigitalOcean Spaces, any S3-compatible store | region, bucket, endpoint (blank = AWS), accessKeyId *, secretAccessKey *, forcePathStyle | | sftp | Your own SSH/SFTP server | host, port (default 22), username, password *, privateKeyPath (instead of password), remoteBasePath | | webdav | Nextcloud, Apache/nginx DAV, any WebDAV share | url, username, password *, remoteBasePath | | github-repo | A private GitHub repo — the only backend with native versioning (git history) | owner (defaults to gh login), repo (name or URL), branch (blank = repo default), token * (reused from the gh CLI when signed in), remoteBasePath (optional) | | local-fs | A plain directory — test/eval backend; also handy for sync via a mounted/synced folder (Dropbox-style folders, mounted NAS shares) | basePath |

* = secret; stored only in ~/.vsync/config.json (0600), never in the

github-repo note: files of any size up to GitHub's 100 MB blob limit are supported — reads over 1 MB transparently go through the git blobs API (the contents API caps reads at 1 MB). project config that gets committed.

Remote layout: every project's files live under a <projectId>/ prefix at the backend (<remoteBasePath>/<projectId>/<project-relative-path>), so several projects can share one bucket/server without colliding.

What lives where

| Path | Contents | Committed to git? | | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | | .vsync/manifest.json (project) | backend name, tracked paths (a plain list — no hashes) | no — git-ignored, rebuilt via vsync link | | <projectId>/.vsync-index.json (backend) | vsync's remote index: hash/size/push-time of every file on the backend | n/a — lives on the backend, never local | | ~/.vsync/config.json (machine) | backend profiles, credentials (0600), local project registry (checkout paths for vsync list) — or any file passed via --config/VSYNC_CONFIG (see below) | no |

Which files does init suggest?

vsync init scans what git ignores (project, nested, and global excludesfile semantics — via git status --ignored) and classifies:

  • Suggested (pre-checked): .env*, *secret*, *credential*, *.pem, *.key, id_rsa*, config.local.* — under 50 KB.
  • Shown (unchecked): every other ignored file under 10 MB.
  • Never shown: anything inside node_modules/, dist/, build/, .next/, target/, vendor/, __pycache__/, cache/log dirs, or over 10 MB.

Selection happens in a folder-tree checklist: folders show [ ]/[~]/[x] (none/some/all selected) and toggling one selects or clears its whole subtree; / expand/collapse, a/n select all/none.

Nested git repos

Projects sometimes contain other git repos ignored by the parent (the umbrella-repo pattern: parent .gitignore has /some-subrepo/). init scans those too — the sub-repo's own .gitignore decides what counts as a candidate, so a .env ignored inside it is offered under its project-relative path (some-subrepo/.env) while the sub-repo's tracked files never are. In the tree picker those folders are tagged nested repo. This is exactly how you sync .env/CLAUDE.md files from checked-out sub-repos under one umbrella project.

Scripting & agents

Every command runs without a TTY. Each interactive prompt has a flag twin: a flag wins, a TTY prompts as before, and with no TTY the missing flag either takes a safe default or fails fast naming the flag — nothing hangs. Every command also takes --json for machine-readable output (exactly one object on stdout; warnings, progress, and errors go to stderr; exit codes: 0 success, 1 failure). On a partial push/pull the result object is still printed before the error, so agents get per-file detail plus exit 1.

Flag matrix

| Command | Non-interactive flags | | --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | config | --backend <name>, --set key=value (repeatable), --secret key=value (repeatable) — or the VSYNC_SECRET_* env vars; a failed connection test aborts (nothing saved) | | init | --project-id <id> (default: folder name), --backend <name> (default: global default), --files a,b (repeatable; omitted = track nothing), --yes (re-init), --list (print candidates and exit) | | link | --pull (pull right after linking; without a TTY the pull is simply skipped — exit 0) | | update | -y/--yes (without it and no TTY: error) | | add, rm, status, diff, push, pull | already non-interactive (--yes on push/pull, --show-values on diff) |

Large file lists (add/rm paths, init --files) can go over stdin instead of argv — vsync add - < paths.txt reads newline-separated project-relative paths. The OS caps command-line length (cmd.exe ~8k characters), so lists beyond ~100 paths belong on stdin.

Multiple profiles on one machine (--config)

Every command accepts a global --config <file> flag (or the VSYNC_CONFIG env var) that points at a complete config file — profiles, credentials, project registry — instead of ~/.vsync/config.json. Two people sharing a device (or one person keeping work/personal profiles separate) each pass their own file and never collide:

# one-time setup per user (secrets via env vars preferred over --secret)
VSYNC_SECRET_ACCESS_KEY_ID=AKIA... VSYNC_SECRET_SECRET_ACCESS_KEY=... \
  vsync --config ~/.vsync/alice.json config --backend s3 \
  --set region=eu-west-1 --set bucket=alices-vault --json

# then every command through that file — an alias hides the flag
alias va='vsync --config ~/.vsync/alice.json'
va init --project-id myapp --files .env
va push
va list            # only alice's projects — registries are per-file

--config may appear before or after the subcommand, beats the VSYNC_HOME env var, and never falls back to the shared ~/.vsync/config.json while set. Without it, behavior is exactly as before. (The per-project manifest stays per-checkout — each user keeps their own clone, as with any shared working tree.)

Secrets for config

Each backend's secret fields can arrive via flag or env var (constant-case field name, VSYNC_SECRET_ prefix). --secret beats the env var; both beat a previously saved secret. A secret supplied through --set is auto-routed to secret storage, never the plaintext profile. A gh CLI token is reused automatically for github-repo when no other token is supplied.

VSYNC_SECRET_ACCESS_KEY_ID=AKIA... \
VSYNC_SECRET_SECRET_ACCESS_KEY=... \
  vsync config --backend s3 --set region=us-east-1 --set bucket=my-bucket --json

| Backend | Secret fields | | ------------- | -------------------------------------------------------------- | | s3 | VSYNC_SECRET_ACCESS_KEY_ID, VSYNC_SECRET_SECRET_ACCESS_KEY | | sftp | VSYNC_SECRET_PASSWORD | | webdav | VSYNC_SECRET_PASSWORD | | github-repo | VSYNC_SECRET_TOKEN |

(--secret argv values are visible in process listings — prefer env vars.)

JSON shapes (stable; additive changes only)

  • status{projectId, backend, files: [{path, status, note?}]}statusdiffers | missing-locally | remote-missing | unchanged
  • diff{projectId, backend, files: [{path, status}], candidates: [{path, size, classification, rule?}], patches?: [{path, patch}]} (patches only with --show-values)
  • list{projects: [{projectId, backend, fileCount|null, linked, path?, lastSyncedAt?, missingOnDisk?}], unreachable: [string]}
  • init{projectId, backend, files: [string]}; init --list{candidates: [...]}
  • link{projectId, backend, files: [string], pull?: <pull result>} (pull present only with --pull)
  • push/pull{projectId, backend, files: [{path, outcome, note?}], summary: {<outcome>: count}}
  • add{added: [string]}; rm{removed: [string]}
  • config{backend, saved: true, secretsStored: [string]}; config --show{defaultBackend, profiles: {name: {backend, settings, secrets: [fieldNames]}}}
  • update{current, latest, updated}

Typical agent session

vsync config --backend local-fs --set basePath=/srv/vsync --json </dev/null
cd myproject
vsync init --list --json </dev/null            # discovery: what's trackable
vsync init --project-id myproject --files .env --json </dev/null
vsync status --json                            # paths + statuses only
vsync push --json
vsync pull --json                              # on the next machine after `link`

Updating

vsync update   # checks npm, confirms, installs — or: npm install -g vasari-sync@latest

Development

npm install
npm run build      # tsc → dist/
npm test           # vitest (unit + integration, all against fakes/local-fs)
npm run lint
npm run typecheck

See local-docs/PLAN.md for the task breakdown and docs/commands.md for the full command reference.

License

MIT