Reference
Architecture
Each supported config format (JSON/JSONC, YAML, JS/TS) has its own adapter, picked by file extension. The JSON adapter is a thin layer over jsonc-parser's point-edit API. The YAML adapter is built on the yaml package's mutable document model. The JS/TS adapter is built on ts-morph and only ever reads or writes literal values (strings, numbers, booleans, null, and arrays/objects built entirely from those) — a dynamic expression (a spread, a function call, an imported variable) is left alone, and LintSync reports that it can't safely touch it rather than guessing. This is also why the JS/TS adapter can read a real ESLint flat config array, provided its config object is the array's trailing element (the shape a real flat config with spread base configs takes).
sync's three-way comparison, per managed key, works out to:
- The file's value equals the manifest's last-applied value (you haven't touched it) → if it also differs from the preset, that's a change to apply; otherwise the manifest bookkeeping is just refreshed.
- The file's value already equals the preset's value (you made the same edit independently, or nothing's changed) → nothing to write.
- Anything else → a conflict.
A conflict on any key aborts that tool's whole batch of pending changes — nothing is written half-way for that file. managedKeys wildcards (rules.*) only match exactly one path segment at that position — not a recursive or multi-segment glob — and the set of candidate keys checked is the union of what's actually present in the file and what the preset itself defines, so a rule a newer preset version adds shows up in status/sync even before it exists in your project's file at all.
Known limitation: the YAML adapter doesn't resolve merge keys (<<: *anchor). A managed key defined only through a merged anchor reads as not-found rather than its actual merged value, which can make sync think a key needs to be added when it's already effectively set via an anchor.
Comparison
Unlike a shareable ESLint config published as its own npm package (eslint-config-*, @org/eslint-config), LintSync doesn't add an extends dependency at all — it writes the preset's rules directly into your own config file, so what you get is a normal, fully-visible config you can read and hand-edit, not an opaque extends: ['@org/eslint-config'] pointing at someone else's package. The tradeoff is the one LintSync is built around: a normal shareable config has no way to tell "the base config changed this rule" apart from "I changed this rule myself" once both are merged into the same resolved config — that distinction is exactly what the manifest and the conflict system exist to preserve.
It's also not a monorepo-wide config manager for a single repository (nothing here assumes a workspace or hoists a config from a root) — its batch mode is aimed at a folder of otherwise-independent projects, each with its own config files, not one workspace with one shared config location.
Development
git clone https://github.com/macrulezru/lintsync.git
cd lintsync
npm install
npm run build # tsc -> dist/
npm test # vitest
npm run lint # eslint .
npm run format # prettier --check .
npm run typecheck # tsc --noEmitLicense
MIT — see LICENSE.