cpconfig is a tiny toolkit that keeps project-specific configuration files in sync with disk. Give it the
list of files you care about, and it will write them, ensure parent directories exist, and maintain a clearly
marked block inside .gitignore.
The goal is to provide the ergonomic bootstrap experience of tools like coconfig
without any custom module loading or complicated configuration layering—just code.
- Deterministic sync – Declare files and their contents,
cpconfighandles creation and updates. - Managed
.gitignoreblock – Automatically keeps tracked configs out of git with a dedicated annotated section. - Safe by default – No implicit imports or magic; works with plain TypeScript/JavaScript data.
- Dry runs – Compute what would change without touching the file system.
yarn add @sesamecare-oss/cpconfig
# or
npm install @sesamecare-oss/cpconfigimport { syncConfigs } from '@sesamecare-oss/cpconfig';
await syncConfigs(
{
'config/.env.local': {
contents: 'API_TOKEN=abc123',
},
'.secrets.json': {
contents: () => JSON.stringify({ key: 'value' }, null, 2),
},
},
{
rootDir: process.cwd(), // optional, defaults to process.cwd()
},
);The call above writes the files if necessary and maintains this .gitignore block:
# Managed by cpconfig
config/.env.local
.secrets.json
Run the function as many times as you like—it is idempotent and only touches files when their content changes.
Most projects keep their file definitions in a dedicated module and call the bundled CLI as part of
postinstall:
{
"name": "my-project",
"scripts": {
"postinstall": "cpconfig"
},
"config": {
"cpconfig": "./cpconfig.config.mjs"
}
}The CLI walks up from the current working directory, finds the nearest package.json, and reads the
config.cpconfig entry. The value must be a string pointing at a module—top-level cpconfig keys or inline
objects are rejected. cpconfig resolves the module using standard Node.js rules and expects it to export one of:
- a default or named
configobject with{ files, options }; - a plain object map of files;
- a factory function (sync or async) returning either of the above.
Factory functions receive two arguments: the entire package.json config object (so you can read sibling settings)
and the raw CLI argument array (e.g. ['--json']). Use them to branch on runtime options or shared configurations.
For example, cpconfig.config.mjs might look like:
export default {
files: {
'config/.env.local': { contents: 'API_TOKEN=abc123' },
'.secrets.json': { contents: '{\n "key": "value"\n}' },
},
};- Run
npx cpconfigmanually whenever you need to refresh files. - Add
cpconfigtopostinstallto keep developer machines in sync automatically.
| Option | Type | Default | Description |
|---|---|---|---|
rootDir |
string |
process.cwd() |
Base directory used to resolve all config paths. |
dryRun |
boolean |
false |
When true, compute the diff without writing to disk. |
encoding |
BufferEncoding |
'utf8' |
Encoding used when reading and writing files. |
gitignorePath |
string |
<rootDir>/.gitignore |
Custom location for the managed gitignore file. |
Each config file can optionally set gitignore: false to opt out of the managed block, or provide a mode
(integer) that is applied when the file is first created. Add a sentinel string to embed your own marker
inside the file (for example // @cpconfig or <!-- cpconfig -->). cpconfig only rewrites files that include
their sentinel, so you never clobber hand-crafted files. Make sure the string appears in the requested contents.
When an existing file is missing its sentinel, cpconfig leaves it untouched and prints a warning explaining why.
syncConfigs resolves with a structured result describing what happened:
const result = await syncConfigs(files);
result.files; // [{ path: 'config/.env.local', managed: true, action: 'created', gitignored: true }, ...]
result.gitignore; // { updated: true, added: ['config/.env.local'], removed: [] }Use this information to log progress, emit metrics, or drive prompts.
cpconfig --help
Options:
--dry-run Compute changes without writing files
--json Print the sync result as JSON
--root <path> Override the root directory used for file writes
--gitignore <path> Override the gitignore file path
--config <path> Load configuration from an explicit JSON file
--help, -h Show this message
Combine dryRun: true with the result payload to surface pending configuration changes without touching disk:
const preview = await syncConfigs(files, { dryRun: true });
if (preview.gitignore.updated || preview.files.some((file) => file.action !== 'unchanged')) {
console.log('Configuration out of date. Run cpconfig to apply changes.');
}UNLICENSED – tailor to your organisation’s distribution policy before publishing.