configre 2.1.4 → 2.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,151 +1,128 @@
1
- # 🔧 Configre
1
+ # Configre
2
2
 
3
- Welcome to Configre, the coolest way to manage your project's configuration with a twist of personality based on your environment! Whether you're in development, testing, or production, Configre seamlessly adjusts to your project's needs by loading specific configurations tailored to each environment. Say goodbye to manual config tweaks and hello to automatic, hassle-free setup! 🚀
3
+ Define your defaults once. Let each environment describe what changes. Keep secrets alongside the settings they belong to.
4
4
 
5
- ## ✨ Features
5
+ Configre loads a base configuration, merges in a host or profile, and returns a plain JavaScript object. Configuration lives in `.cjs` files, so you get nested objects, comments, and ordinary JavaScript. Optional secret files follow the same structure and can be shared with your servers through encrypted files in Git.
6
6
 
7
- - **Environment-Specific Configurations**: Automatically loads configurations based on the hostname or a custom profile forced via `--config=<profile>` CLI argument.
8
- - **Fallback to Defaults**: Uses a default configuration as a baseline, ensuring your application always has the necessary settings.
9
- - **Easy Integration**: A simple setup process that integrates effortlessly into any project.
10
- - **Support for `.cjs` Config Files Only**: Config files must use the `.cjs` extension. This allows dynamic configuration values and comments, and works the same in both CommonJS and ESM projects.
7
+ ## Quick start
11
8
 
12
- ## 🌟 Getting Started
9
+ Requires **Node.js 22.13 or later**.
13
10
 
14
- To get started with Configre, follow these steps:
15
-
16
- 1. **Install the Library**
17
-
18
- Configre requires **Node.js 22.13 or later**. Add it to your project:
19
-
20
- ```bash
21
- npm install configre --save
22
- ```
23
-
24
- > You can also add Configre as a skill for AI agentic development:
25
- > ```bash
26
- > npx skills add https://github.com/clasen/Configre --skill configre
27
- > ```
28
-
29
- 2. **Setup Your Configuration Files**
30
-
31
- Organize your configuration files within a directory (e.g., `config`). Create a default configuration file and environment-specific files as needed.
32
-
33
- - `index.cjs`: Your default configuration.
34
- - `[hostname].cjs`: Override configurations for specific hosts.
35
- - `[hostname].dev.cjs`: Development-specific configurations.
36
-
37
- > **Note:** Config files must always use the `.cjs` extension; `.js` config files are not accepted. That way they work in both CommonJS and ESM projects and you can use `module.exports` in them.
38
-
39
- 3. **Use Configre in Your Project**
40
-
41
- Import and use Configre to load your configurations. The configuration path is required:
11
+ ```bash
12
+ npm install configre
13
+ ```
42
14
 
43
- ```javascript
44
- import Configre from "configre";
45
- import { join } from "node:path";
15
+ Create `config/index.cjs` with your defaults:
46
16
 
47
- const cfg = Configre(join(import.meta.dirname, "config"));
48
- console.log(cfg.db); // Access your db configuration
49
- ```
17
+ ```javascript
18
+ module.exports = {
19
+ db: {
20
+ host: "localhost",
21
+ port: 5432,
22
+ password: ""
23
+ },
24
+ api: {
25
+ key: ""
26
+ }
27
+ };
28
+ ```
50
29
 
51
- Prefer an absolute path anchored to the module, as above. Avoid deriving it from `process.cwd()` (for example, `path.join(process.cwd(), "config")`), because that can point to a different location depending on where the process is started.
30
+ Load it from `app.mjs`, next to the `config` directory:
52
31
 
53
- Configre is a native ES module. CommonJS consumers keep the same synchronous API, without accessing `.default`:
32
+ ```javascript
33
+ import Configre from "configre";
34
+ import { join } from "node:path";
54
35
 
55
- ```javascript
56
- const Configre = require("configre");
57
- const { join } = require("node:path");
36
+ const cfg = Configre(join(import.meta.dirname, "config"));
37
+ console.log(cfg.db.port); // 5432
38
+ ```
58
39
 
59
- const cfg = Configre(join(__dirname, "config"));
60
- ```
40
+ ```bash
41
+ node app.mjs
42
+ ```
61
43
 
62
- Both module systems share the same implementation. Existing configuration files stay in `.cjs` format; Configre uses Node's `createRequire` only to load these files synchronously.
44
+ The configuration path is required. Anchor it to your module so starting the process from another directory does not change which configuration it loads. Avoid building this path from `process.cwd()`.
63
45
 
64
- ## 📚 Example
46
+ Configre is a native ES module. CommonJS consumers can also use the synchronous API: `require("configre")` returns the function directly. Configuration files use `.cjs` and `module.exports` in either kind of project.
65
47
 
66
- Imagine you have the following structure in your `config` directory:
48
+ ## Configuration that builds on defaults
67
49
 
68
- - `index.cjs`: Contains default settings.
69
- - `myhostname.cjs`: Contains overrides for the host named `myhostname`.
50
+ An environment should describe what changes. Everything else comes from the base.
70
51
 
71
- Your `index.cjs` might look like this:
52
+ Add `config/production.cjs`:
72
53
 
73
54
  ```javascript
74
55
  module.exports = {
75
56
  db: {
76
- host: 'localhost',
77
- user: 'myuser',
78
- password: 'mypassword'
79
- },
80
- api: {
81
- key: 'development-api-key'
57
+ host: "db.internal",
58
+ ssl: true
82
59
  }
83
- }
60
+ };
61
+ ```
62
+
63
+ Select that profile:
64
+
65
+ ```bash
66
+ node app.mjs --config=production
84
67
  ```
85
68
 
86
- And your `myhostname.cjs`:
69
+ The resulting configuration contains:
87
70
 
88
71
  ```javascript
89
- module.exports = {
72
+ {
90
73
  db: {
91
- user: 'john',
92
- password: 'johns-password'
74
+ host: "db.internal",
75
+ port: 5432,
76
+ password: "",
77
+ ssl: true
78
+ },
79
+ api: {
80
+ key: ""
93
81
  }
94
82
  }
95
83
  ```
96
84
 
97
- Configre merges these configurations based on your environment, making your app adaptable and easier to manage.
85
+ `host` is overwritten, `port` is inherited, and `ssl` is added. The `api` object stays intact. A profile can extend nested objects without repeating their other fields.
98
86
 
99
- ## 🎯 Forcing a hostname / config
87
+ ### Selecting a profile
100
88
 
101
- By default Configre uses the machine’s hostname to choose the config file (e.g. `config/<hostname>.cjs`). You can force which hostname or profile to use with the `--config=<hostname>` CLI argument:
89
+ By default, Configre uses the machine's hostname. A machine named `web-01` loads `config/web-01.cjs`. Pass `--config=<profile>` to choose a name explicitly; the flag can appear alongside other application arguments.
102
90
 
103
- ```bash
104
- node demo.js --config=staging
105
- node demo.js --config=production
106
- node demo.js --port=3000 --config=myhost --debug
107
- ```
91
+ | File | Role |
92
+ | --- | --- |
93
+ | `index.cjs` | Base configuration |
94
+ | `<profile>.cjs` | Additions and overrides for the selected profile |
95
+ | `<profile>.dev.cjs` | Preferred over `<profile>.cjs` when present |
108
96
 
109
- This loads `config/staging.cjs` (or `config/staging.dev.cjs`) instead of the one for the actual hostname. The `--config=` flag can appear anywhere in the command and works alongside other arguments.
97
+ If no profile file exists, Configre uses the base configuration. When `production.dev.cjs` exists, it is selected **instead of** `production.cjs`; those two files do not merge. This selection depends on the files present, not on `NODE_ENV`.
110
98
 
111
- ## 🔐 Shared secrets: follow the demo
99
+ Objects merge recursively. Later values override matching fields. Arrays merge **by index**, rather than being replaced as a whole: `["a", "b"]` followed by `["c"]` becomes `["c", "b"]`.
112
100
 
113
- The [secrets demo](demo/secrets/) shows how to keep API keys out of your public configuration and share them with a server through Git. Use one checkout as the **administrator**, where you edit secrets, and another as the **server**, which reads the encrypted values.
101
+ ## Secrets, organized like your configuration
114
102
 
115
- Run the commands below from the repository root. In [demo/secrets/demo.js](demo/secrets/demo.js), the configuration is loaded with:
103
+ Database credentials belong under `db`. API keys belong under `api`. Secret files keep that organization while separating sensitive values from public settings.
116
104
 
117
- ```javascript
118
- import Configre from "../../index.js";
119
- import { join } from "node:path";
120
-
121
- const configPath = join(import.meta.dirname, "config");
122
- const cfg = Configre(configPath);
105
+ ```text
106
+ config/
107
+ index.cjs # Shared public settings
108
+ production.cjs # Production additions and overrides
109
+ index.secret.cjs # Shared secrets, edited locally
110
+ production.secret.cjs # Production secrets, edited locally
123
111
  ```
124
112
 
125
- Secrets activate automatically when the base configuration or selected profile has a corresponding `.secret.cjs` file, or when `secrets.enc.json` already exists. No options are needed. Loading remains synchronous, and your application reads the result through ordinary properties such as `cfg.api.key`.
113
+ A secret file exports only the fields it needs to supply. Your application still reads `cfg.db.password` and `cfg.api.key`; there is no separate secrets API.
126
114
 
127
- > The demo reports `API key configured:` without printing the key. Avoid logging `cfg` in your application: it contains the decrypted secrets. Use `cfg.print()` to log configuration with secret fields omitted.
115
+ For `--config=production`, the merge order is:
128
116
 
129
- ```javascript
130
- const cfg = Configre(configPath);
131
- cfg.print();
117
+ ```text
118
+ index.cjs → production.cjs → index.secret.cjs → production.secret.cjs
132
119
  ```
133
120
 
134
- `print()` calls Configre's `log.debug`; enable its output with `DEBUG=Configre:*`.
135
- It omits fields supplied by the base and selected profile's `.secret.cjs` files,
136
- including when loaded from the encrypted file. Public siblings in nested objects
137
- remain visible; arrays supplied by secrets are omitted entirely. It does not
138
- modify the configuration or log automatically on load. `cfg.print()` logs its
139
- current values, including changes made after loading. The method is non-enumerable
140
- and does not appear in `Object.keys(cfg)`, object spreads or JSON output.
141
- `new Configre(configPath).print()` is also supported. If your configuration already
142
- has a field named `print`, that value is preserved; use the constructor API to print it.
143
- Sensitivity is determined by secret-file fields, not by names such as `password`
144
- or `token`: keep sensitive values in `.secret.cjs` files, not in public settings.
121
+ Later layers win. Shared secrets can be extended or overwritten by profile secrets, just like public settings. Secret selection independently prefers `production.dev.secret.cjs` over `production.secret.cjs`; it does not merge the two variants.
145
122
 
146
- ### 1. Start the demo on the administrator machine
123
+ ### Add your secrets
147
124
 
148
- For this walkthrough, use an empty public placeholder in [demo/secrets/config/index.cjs](demo/secrets/config/index.cjs):
125
+ Create `config/index.secret.cjs` for shared values:
149
126
 
150
127
  ```javascript
151
128
  module.exports = {
@@ -155,175 +132,144 @@ module.exports = {
155
132
  };
156
133
  ```
157
134
 
158
- Create `demo/secrets/config/index.secret.cjs` yourself to enable secrets, initially with:
135
+ For a production database credential, create `config/production.secret.cjs`:
159
136
 
160
137
  ```javascript
161
- module.exports = {};
162
- ```
163
-
164
- Then run:
165
-
166
- ```bash
167
- node demo/secrets/demo.js
138
+ module.exports = {
139
+ db: {
140
+ password: ""
141
+ }
142
+ };
168
143
  ```
169
144
 
170
- For a new setup, Configre generates the encrypted file, recipients directory and Git exclusions alongside your existing configuration files:
145
+ Fill in the values in these local files. Configre generates `secrets.enc.json` and adds Git exclusions for the editable secret files when your application runs.
171
146
 
172
- ```text
173
- demo/secrets/config/
174
- index.cjs # Public configuration
175
- index.secret.cjs # Editable secrets you create yourself
176
- recipients/ # Public keys of servers that register
177
- secrets.enc.json # Generated encrypted values
178
- .gitignore # Keeps .secret.cjs files out of Git
179
- ```
147
+ ### Share with a server
180
148
 
181
- It also creates this OS user's identity outside the repository, at `~/.config/configre/identity.pem` and `identity.pub`.
149
+ 1. **Local:** run your application and push the generated encrypted file.
150
+ 2. **Server:** pull and run. On the first run, Configre automatically commits and pushes the server's public key as `recipients/<profile>.pub` (or `<hostname>.pub` when no profile is specified).
151
+ 3. **Local:** pull, run again to include that public key in the encrypted file, and push.
152
+ 4. **Server:** pull and restart. The secrets are now available through `cfg`.
182
153
 
183
- With the public placeholder empty and no secrets added, look for:
154
+ The server needs Git push access for registration. Until the updated encrypted file arrives, Configre loads public settings only.
184
155
 
185
- ```text
186
- API key configured: false
187
- ```
156
+ To update secrets later, edit them locally, run, and push. Pull and restart on the server. Registration happens only once per identity.
188
157
 
189
- **Configre never creates `.secret.cjs` files.** Without a secret counterpart for the base configuration or selected profile, and without an encrypted file, it loads only public settings and creates no identity or secret artifacts. A secret module contains only the fields you choose to override; public settings continue to come from `index.cjs`.
158
+ **Profiles organize secrets; they do not isolate access.** Authorized identities can decrypt all profiles in the encrypted file. Pulling a new registration and publishing the updated encrypted file grants that identity access.
190
159
 
191
- These initialization steps describe a new setup. If `secrets.enc.json` already exists, Configre uses that encrypted file instead of resetting it. A checkout containing the encrypted file but no `.secret.cjs` files is treated as a server checkout.
160
+ ## Utilities
192
161
 
193
- ### 2. Add an API key and publish the encrypted file
162
+ ### Inspect configuration with `cfg.print()`
194
163
 
195
- On the administrator machine, edit `demo/secrets/config/index.secret.cjs`:
164
+ The returned configuration contains decrypted secrets. Use `cfg.print()` when inspecting it:
196
165
 
197
166
  ```javascript
198
- module.exports = {
199
- api: {
200
- key: ""
201
- }
202
- };
167
+ const cfg = Configre(join(import.meta.dirname, "config"));
168
+ cfg.print();
203
169
  ```
204
170
 
205
- Replace the empty string with your actual key **in this local file**, then run the demo again:
171
+ Enable its debug output with:
206
172
 
207
173
  ```bash
208
- node demo/secrets/demo.js
174
+ DEBUG=Configre:* node app.mjs --config=production
209
175
  ```
210
176
 
211
- Configre updates `demo/secrets/config/secrets.enc.json`, merges the secret into the configuration, and the demo reports:
177
+ `print()` omits fields supplied by the base and selected profile's secret files, including when those values come from the encrypted file. Public siblings remain visible; arrays supplied by secrets are omitted entirely. Omission is based on secret-file fields, not names such as `password` or `token`. Keep sensitive values in secret files.
212
178
 
213
- ```text
214
- API key configured: true
215
- ```
179
+ It logs current values, including edits made after loading, without changing the configuration. It does not log automatically on load. The method is non-enumerable, so it stays out of `Object.keys(cfg)`, spreads, and JSON output. Spreads and JSON output still include the configuration's secret values.
216
180
 
217
- Commit and push the demo's public files, including `config/index.cjs`, the generated `config/.gitignore`, and `config/secrets.enc.json`. Keep `config/index.secret.cjs` on the administrator machine.
181
+ The constructor API is also available:
218
182
 
219
- | File in `demo/secrets/config/` | Purpose | Share through Git? |
220
- | --- | --- | --- |
221
- | `index.cjs` | Public settings and empty placeholders | Yes |
222
- | `index.secret.cjs` | Values you edit on the administrator | No |
223
- | `secrets.enc.json` | Encrypted values generated by Configre | Yes |
224
- | `recipients/truco.pub` | Public identity of the server named `truco` | Yes; the server publishes it |
225
- | `.gitignore` | Excludes editable secret files | Yes |
183
+ ```javascript
184
+ const config = new Configre(join(import.meta.dirname, "config"));
185
+ const cfg = config.get();
186
+ config.print();
187
+ ```
226
188
 
227
- ### 3. Register the server as `truco`
189
+ If your configuration already has a field named `print`, Configre preserves it. Use the constructor API to print in that case.
228
190
 
229
- On the server, clone or pull the repository **including `demo/secrets/config/secrets.enc.json`**. Use a branch with a configured upstream, Git author name/email, and credentials that allow a push without an interactive prompt. Before a new registration, the local branch must match its upstream.
191
+ ### Apply environment variables explicitly
230
192
 
231
- Run:
193
+ Use `applyConfigEnv(cfg.env)` when a library reads its settings from `process.env`:
232
194
 
233
- ```bash
234
- node demo/secrets/demo.js --config=truco
195
+ ```javascript
196
+ import Configre, { applyConfigEnv } from "configre";
197
+ import { join } from "node:path";
198
+
199
+ const cfg = Configre(join(import.meta.dirname, "config"));
200
+ applyConfigEnv(cfg.env);
235
201
  ```
236
202
 
237
- `--config=truco` selects the profile and names the public-key file. Without this flag, Configre uses the hostname.
203
+ For CommonJS, use `const { applyConfigEnv } = require("configre")`.
238
204
 
239
- If this server is not authorized yet, Configre automatically:
205
+ The helper converts values with `String(value)` and sets only variables that are undefined in `process.env`. Existing values, including empty strings, are preserved. An undefined `cfg.env` does nothing; null or undefined entries are skipped.
240
206
 
241
- 1. Creates its local identity, if needed.
242
- 2. Writes `demo/secrets/config/recipients/truco.pub`.
243
- 3. Creates and pushes a commit containing only that public-key file.
244
- 4. Logs a warning and continues with public configuration only until the administrator publishes an updated encrypted file.
207
+ It rejects null, arrays and non-objects as `cfg.env`, empty keys, and object-valued entries. Entries are processed in order; an invalid entry throws without undoing previous assignments. Loading Configre does not call this helper automatically, and empty placeholders are never filled from environment variables automatically.
245
208
 
246
- Publishing a public key does not let the server decrypt the existing ciphertext. While authorization is pending, `cfg` preserves the public defaults and selected public profile; fields that exist only in the encrypted file are absent. Repeating the command does not publish another registration commit for the same key. If Git registration fails, Configre logs a warning and still returns public settings; the next Configre load retries registration.
209
+ ## Advanced reference
247
210
 
248
- Configre uses `log.info` when it creates identity files, writes Git exclusions, creates the recipients directory or a public-key file, updates the encrypted file, and successfully pushes the public key. Messages contain operation names and file paths, never secret values or key contents. These messages use the existing `Configre` LemonLog namespace; use `DEBUG=Configre:*` to display them. Unchanged files and previously published public keys do not generate another creation or publication log.
211
+ <details>
212
+ <summary>Secret formats, identities, Git behavior, and recovery</summary>
249
213
 
250
- ### 4. Include the server in the encrypted file
214
+ ### Secret files and loading
251
215
 
252
- Back on the administrator machine:
216
+ Secret `.cjs` modules execute locally on the machine where you edit them and are reloaded on each Configre call. They must export plain objects containing JSON-compatible objects, arrays, strings, finite numbers, booleans, and `null`. Functions, `undefined`, accessors, symbols, custom objects, circular references, and properties named `__proto__`, `constructor`, or `prototype` are rejected. Servers decrypt data without executing these modules.
253
217
 
254
- ```bash
255
- git pull --ff-only
256
- node demo/secrets/demo.js
257
- ```
218
+ Secrets activate when the base or selected profile has a secret counterpart, or when the encrypted file already exists. Without either, loading public configuration creates no secret artifacts. Once active, all local profile secret files are encrypted together, including inactive profiles.
258
219
 
259
- The pull brings in `demo/secrets/config/recipients/truco.pub`. When the demo runs, Configre automatically includes that public key and regenerates `demo/secrets/config/secrets.enc.json`. There is no separate approval step.
220
+ A selected profile's secret file can activate secrets without `index.secret.cjs`. A public profile alone never creates a secret counterpart. A missing profile uses the available base settings and base secrets. A checkout with an encrypted file and no editable secret files acts as a consumer. Existing ciphertext is read and validated before updates; it is not reset on startup. There is no `secrets` option, and function, constructor, ESM, and CommonJS usage share the same behavior.
260
221
 
261
- Commit and push the updated encrypted file. Then, on the server:
222
+ For an individual configuration file, sidecars live beside it: `settings.secret.cjs`, `settings.cjs.recipients/`, and `settings.cjs.secrets.enc.json`. An extensionless path resolving to `settings.cjs` uses the same sidecars.
262
223
 
263
- ```bash
264
- git pull --ff-only
265
- node demo/secrets/demo.js --config=truco
266
- ```
224
+ ### Identities and registration
267
225
 
268
- The server can now decrypt the key, and the demo reports `API key configured: true`. It does not need `index.secret.cjs`; the value reaches `cfg.api.key` through the encrypted file.
226
+ Automatic registration requires a Git checkout with a configured upstream, Git author name/email, and credentials that allow a non-interactive push. The branch must match its upstream before a new registration. Failed registrations are retried on the next load.
269
227
 
270
- **Repository write access allows a machine to register for all project secrets.** Configre publishes the server's public key automatically. Pulling changes and publishing the administrator's updated encrypted file remain part of your Git/deployment workflow.
228
+ Each OS user has one identity, stored at `~/.config/configre/identity.pem` and `identity.pub`, reused across projects. Authorization is per project. A service running under another OS user needs its own registration.
271
229
 
272
- ### 5. Update keys or use profile-specific secrets
230
+ Recipient files must contain a single RSA-3072 public key in PEM format, with exponent 65537, as generated by Configre. Other extensions are ignored, and duplicate keys do not add recipients. The identity encrypting the file is always included.
273
231
 
274
- To change the shared API key, edit `demo/secrets/config/index.secret.cjs` on the administrator and run:
232
+ Registration names must start with a letter or digit and contain only letters, digits, dots, underscores, or hyphens. If another key already occupies `production.pub`, registration stops without overwriting it, and configuration loading continues with a warning and public settings only. Use a distinct profile or resolve the key replacement explicitly.
275
233
 
276
- ```bash
277
- node demo/secrets/demo.js
278
- ```
234
+ Registration uses an isolated Git index and publishes only the public-key file. It preserves unrelated staged and unstaged changes, runs no commit or pre-push hooks, and does not push local tags, merge, rebase, or force-push. Repeated loads do not publish another registration commit for the same key. Each Git command has a 30-second timeout. An authorized server loads secrets without Git commands or project writes.
279
235
 
280
- Commit and push the updated `secrets.enc.json`, then pull and restart the demo on the server. **You do not need to copy or register `truco.pub` again.** Its existing authorization also covers new keys you add later.
236
+ Configre appends Git exclusions without removing existing rules, including when no repository exists yet. It refuses to proceed on the machine holding editable secrets if those modules are already tracked. It does not untrack files or rewrite history.
281
237
 
282
- For settings specific to `truco`, add `demo/secrets/config/truco.cjs` and create `truco.secret.cjs` yourself with that profile's secrets. The secret file activates secrets when `truco` is selected, even without `index.secret.cjs`. A public profile alone never creates a secret counterpart. Once secrets are active, all profile secret files, including inactive ones, are encrypted together when the administrator runs the demo.
238
+ ### Updates, revocation, and recovery
283
239
 
284
- For `--config=truco`, the merge order is:
240
+ The editable `.secret.cjs` files are the source of truth for values; `recipients/` is the source of truth for authorization. To revoke a server, remove its public-key file, run Configre locally, and commit and push both the removal and the updated encrypted file. Remove the server's repository write access too if it must not register again. Revocation protects new encrypted versions. Rotate credentials at their providers to invalidate values a server already received.
285
241
 
286
- ```text
287
- index.cjs → truco.cjs → index.secret.cjs → truco.secret.cjs
288
- ```
242
+ Removing all recipient files revokes them on the next local load; old ciphertext does not restore them. Recover accidentally deleted public-key files from Git before reloading. Missing Git exclusions and ciphertext are regenerated on the machine holding editable secrets, and existing secret modules are never overwritten.
289
243
 
290
- Later files override matching fields while preserving other fields. If `truco.dev.cjs` exists, it is selected instead of `truco.cjs`. Secret selection independently prefers `truco.dev.secret.cjs` over `truco.secret.cjs`; those two secret variants do not merge with each other. A missing profile uses the available base configuration and base secrets.
244
+ Back up editable secrets and the private identity securely. Never share or commit `identity.pem`. On POSIX, the identity directory must have owner-only permissions (`0700`), and the private file must have owner-only permissions (`0600`). On Windows, protection depends on the user profile's filesystem permissions. Private identities and secret input files cannot be symlinks.
291
245
 
292
- To revoke this server, remove `demo/secrets/config/recipients/truco.pub`, run the demo on the administrator, and commit and push both the removal and the updated encrypted file. Remove the server's repository write access too if it must not register itself again. Revocation protects new encrypted versions; rotate the key at its provider to invalidate a credential the server already received.
246
+ A corrupt or missing private identity is never silently replaced. Restore it from backup, or register a new identity on a consumer. A missing public file can be regenerated from the valid private key. Losing every authorized private key makes existing ciphertext unrecoverable; surviving editable secrets can be used to create a fresh encrypted file in a new setup.
293
247
 
294
- <details>
295
- <summary>File formats, Git behavior and recovery</summary>
296
-
297
- - Secret `.cjs` modules execute on the administrator and are reloaded on each Configre call. They must export plain objects containing JSON-compatible objects, arrays, strings, finite numbers, booleans and `null`. Functions, `undefined`, accessors, symbols, custom objects, circular references, and properties named `__proto__`, `constructor` or `prototype` are rejected. Consumers decrypt data without executing these modules.
298
- - Empty strings stay empty strings. Configre does not fill them from environment variables or populate `process.env`. The existing deep-merge behavior, including array merging, also applies to secrets.
299
- - Activation depends only on the existing files; there is no `secrets` option. The same behavior applies to `new Configre(configPath).get()` and CommonJS consumers.
300
- - Each OS user has one identity reused across projects; authorization is per project. A service running under another OS user needs its own registration. All authorized identities can decrypt all profiles in the project's encrypted file.
301
- - Public recipient files must contain a single RSA-3072 public key in PEM format, with exponent 65537, as generated by Configre. Other file extensions are ignored, and duplicate keys do not add recipients. The administrator is always included.
302
- - Registration uses an isolated Git index and publishes only the public-key file. It preserves unrelated staged and unstaged changes, runs no commit or pre-push hooks, and does not push local tags, merge, rebase or force-push. Failed registrations can be retried at the next startup; each Git command has a 30-second timeout. An authorized server loads secrets without Git commands or project writes.
303
- - Registration names must start with a letter or digit and contain only letters, digits, dots, underscores or hyphens. If another key already occupies `truco.pub`, registration stops without overwriting it, and configuration loading continues with a warning and public settings only; use a distinct profile or resolve the key replacement explicitly.
304
- - Configre appends Git exclusions without removing existing rules and refuses to proceed on the administrator if editable secret modules are already tracked. It does not untrack files or rewrite history. It also prepares `.gitignore` when no repository exists yet.
305
- - The administrator's `.secret.cjs` files are the source of truth for values, and `recipients/` is the source of truth for authorization. Removing all recipient files revokes them on the next administrator reload; old ciphertext does not restore them. Recover accidentally deleted public-key files from Git before reloading. Missing Git exclusions and ciphertext are regenerated on the administrator, and existing secret modules are never overwritten.
306
- - Back up the administrator's editable secrets and private identity securely. Never share or commit `identity.pem`. On POSIX, its directory must have owner-only permissions (`0700`) and the private file must have owner-only permissions (`0600`); on Windows, protection depends on the user profile's filesystem permissions. Private identities and secret input files cannot be symlinks.
307
- - A corrupt or missing private identity is never silently replaced. Restore it from backup, or register a new identity on a consumer. A missing public file can be regenerated from the valid private key. Losing every authorized private key makes existing ciphertext unrecoverable; an administrator with surviving editable secret files can create fresh ciphertext.
308
- - The encrypted JSON envelope uses AES-256-GCM with a fresh content key and nonce for each update, wrapping that key for each recipient with RSA-3072 / OAEP-SHA-256. Version and recipient metadata are authenticated. Invalid modules, malformed ciphertext and authentication failures stop loading without replacing the existing encrypted file.
309
- - Ciphertext is replaced atomically under an exclusive writer lock. Temporary vault files contain only encrypted data and public metadata. After a crash, remove a reported stale `.lock` only after confirming its writer has stopped. Unchanged values and recipient keys do not cause a rewrite.
310
- - For an individual configuration file, the same workflow uses sidecars beside that file: `settings.secret.cjs`, `settings.cjs.recipients/` and `settings.cjs.secrets.enc.json`. An extensionless path resolving to `settings.cjs` uses the same sidecars.
248
+ ### Encryption and writes
249
+
250
+ The encrypted JSON envelope uses AES-256-GCM with a fresh content key and nonce for each update. That key is wrapped for each recipient with RSA-3072 / OAEP-SHA-256. Version and recipient metadata are authenticated. Invalid modules, malformed ciphertext, and authentication failures stop loading without replacing the existing encrypted file.
251
+
252
+ Ciphertext is replaced atomically under an exclusive writer lock. Temporary files contain only encrypted data and public metadata. After a crash, remove a reported stale `.lock` only after confirming its writer has stopped. Unchanged values and recipient keys do not cause a rewrite.
253
+
254
+ ### Operational logs
255
+
256
+ With `DEBUG=Configre:*`, Configre reports identity creation, Git exclusions, recipient registration, encrypted-file updates, and successful public-key publication. These operation messages contain names and paths, never secret values or key contents. Unchanged files and previously published keys do not generate another creation or publication log.
311
257
 
312
258
  </details>
313
259
 
314
- ## 🤔 Why Configre?
260
+ ## Resources and contributions
315
261
 
316
- - **No More Manual Switching**: Automatically adjusts your configuration based on the environment.
317
- - **Clarity and Convenience**: Keep your configuration organized and easy to understand.
318
- - **Flexibility**: Supports dynamic configuration values for complex setups.
262
+ Explore the [basic demo](https://github.com/clasen/Configre/blob/main/demo/demo.js), [ESM project demo](https://github.com/clasen/Configre/blob/main/demo/module/demo.js), or [secrets demo](https://github.com/clasen/Configre/blob/main/demo/secrets/demo.js).
319
263
 
320
- ## 🤝 Contribute
264
+ To add the Configre skill to your coding agent:
321
265
 
322
- Found a bug or have a feature request? Contributions are welcome! Feel free to open an issue or submit a pull request.
266
+ ```bash
267
+ npx skills add https://github.com/clasen/Configre --skill configre
268
+ ```
323
269
 
324
- Let's make Configre even better, together! 🎉
270
+ Found a bug or a useful improvement? [Open an issue](https://github.com/clasen/Configre/issues) or submit a pull request. A small, reproducible example helps.
325
271
 
326
- ## 📄 License
272
+ ## License
327
273
 
328
274
  The MIT License (MIT)
329
275
 
File without changes
File without changes
@@ -1,5 +1,5 @@
1
1
  module.exports = {
2
- api: {
3
- key: ""
2
+ "api": {
3
+ "url": "https://api.example.com"
4
4
  }
5
- };
5
+ }
package/index.js CHANGED
@@ -9,6 +9,30 @@ import loadSecrets from "./secrets/index.js";
9
9
  const requireConfig = createRequire(import.meta.url);
10
10
  const log = lemonlog("Configre");
11
11
 
12
+ function applyConfigEnv(env) {
13
+ if (env === undefined) {
14
+ return;
15
+ }
16
+ if (env === null || typeof env !== "object" || Array.isArray(env)) {
17
+ throw new TypeError("config.env must be a plain object");
18
+ }
19
+
20
+ for (const [key, value] of Object.entries(env)) {
21
+ if (key.length === 0) {
22
+ throw new TypeError("config.env keys must be non-empty strings");
23
+ }
24
+ if (value == null) {
25
+ continue;
26
+ }
27
+ if (typeof value === "object") {
28
+ throw new TypeError(`config.env.${key} must be a scalar`);
29
+ }
30
+ if (process.env[key] === undefined) {
31
+ process.env[key] = String(value);
32
+ }
33
+ }
34
+ }
35
+
12
36
  function omitSecrets(settings, secrets) {
13
37
  for (const [key, value] of Object.entries(secrets)) {
14
38
  if (!Object.hasOwn(settings, key)) continue;
@@ -127,4 +151,6 @@ function Configre(path) {
127
151
  }
128
152
  }
129
153
 
130
- export { Configre as default, Configre as "module.exports" };
154
+ Configre.applyConfigEnv = applyConfigEnv;
155
+
156
+ export { Configre as default, Configre as "module.exports", applyConfigEnv };
package/package.json CHANGED
@@ -1,14 +1,11 @@
1
1
  {
2
2
  "name": "configre",
3
- "version": "2.1.4",
3
+ "version": "2.1.5",
4
4
  "description": "🔧 Effortlessly Tailor Your Settings",
5
5
  "type": "module",
6
6
  "engines": {
7
7
  "node": ">=22.13.0"
8
8
  },
9
- "scripts": {
10
- "test": "node --test"
11
- },
12
9
  "dependencies": {
13
10
  "lemonlog": "^1.2.2"
14
11
  },
@@ -50,7 +47,6 @@
50
47
  "url": "https://github.com/clasen/Configre/issues"
51
48
  },
52
49
  "homepage": "https://github.com/clasen/Configre#readme",
53
- "packageManager": "pnpm@11.9.0",
54
50
  "files": [
55
51
  "index.js",
56
52
  "merge.js",
@@ -66,5 +62,8 @@
66
62
  "demo/module/config/hostname.cjs",
67
63
  "demo/secrets/demo.js",
68
64
  "demo/secrets/config/index.cjs"
69
- ]
70
- }
65
+ ],
66
+ "scripts": {
67
+ "test": "node --test"
68
+ }
69
+ }
@@ -1,14 +1,14 @@
1
1
  ---
2
2
  name: configre
3
- description: Set up and manage environment-specific configuration in Node.js projects using the Configre library. Use when the user wants to add configuration management, create environment configs, set up hostname-based settings, or mentions "configre", "config files", "environment config", or "per-host settings".
3
+ description: Set up and manage Configre configuration in Node.js projects, including hostname/profile overrides, encrypted shared secrets, recipient registration, and configuration printing with secret fields omitted. Use when adopting Configre or working on its configuration files, secret sharing, or print() API.
4
4
  metadata:
5
5
  category: configuration
6
- tags: [nodejs, config, environment, settings, env]
6
+ tags: [nodejs, config, environment, settings, secrets]
7
7
  ---
8
8
 
9
9
  # Configre
10
10
 
11
- Environment-specific configuration manager for Node.js. Merges a default config with hostname or profile-based overrides using deep merge (lodash).
11
+ Environment-specific configuration manager for Node.js. Merges public defaults, hostname or profile overrides, and optional secrets synchronously using its own deep-merge implementation.
12
12
 
13
13
  ## Instructions
14
14
 
@@ -40,15 +40,17 @@ module.exports = {
40
40
  host: 'localhost',
41
41
  port: 5432,
42
42
  user: 'dev',
43
- password: 'dev-pass'
43
+ password: ""
44
44
  },
45
45
  api: {
46
- key: 'default-key',
46
+ key: "",
47
47
  url: 'http://localhost:3000'
48
48
  }
49
49
  };
50
50
  ```
51
51
 
52
+ Keep credentials in `.secret.cjs` files. Public settings should contain explicit empty placeholders, not real credentials or automatic environment-variable fallbacks.
53
+
52
54
  ### Step 4: Write host/profile overrides
53
55
 
54
56
  Create `config/<hostname>.cjs` with only the keys that differ — they are deep-merged over defaults:
@@ -56,8 +58,7 @@ Create `config/<hostname>.cjs` with only the keys that differ — they are deep-
56
58
  ```javascript
57
59
  module.exports = {
58
60
  db: {
59
- user: 'prod-user',
60
- password: 'prod-secret'
61
+ user: 'prod-user'
61
62
  }
62
63
  };
63
64
  ```
@@ -90,7 +91,7 @@ Configre determines the active profile by looking for a `--config=<profile>` arg
90
91
 
91
92
  1. `config/<profile>.dev.cjs` — dev override
92
93
  2. `config/<profile>.cjs` — production override
93
- 3. No match — uses defaults only
94
+ 3. No match — uses public defaults; available secrets still apply
94
95
 
95
96
  To force a specific profile at runtime:
96
97
 
@@ -101,6 +102,85 @@ node app.js --port=3000 --config=production --debug
101
102
 
102
103
  Using `--config=` (instead of a positional argument) avoids conflicts with other CLI flags.
103
104
 
105
+ ## Shared secrets
106
+
107
+ ### Activation and file layout
108
+
109
+ Secret support activates when the base config or selected profile has a corresponding `.secret.cjs` file, or when an encrypted file already exists. There is no `secrets` option. Without either condition, Configre loads public settings without accessing an identity or creating secret artifacts. Configre never creates editable `.secret.cjs` modules itself; create them only when secret support is wanted.
110
+
111
+ For a config directory:
112
+
113
+ ```text
114
+ config/
115
+ ├── index.cjs
116
+ ├── production.cjs
117
+ ├── index.secret.cjs # Administrator-only base secrets
118
+ ├── production.secret.cjs # Optional administrator-only profile secrets
119
+ ├── production.dev.secret.cjs # Optional preferred secret profile variant
120
+ ├── secrets.enc.json # Generated ciphertext; share through Git
121
+ ├── recipients/*.pub # Public recipient keys; share through Git
122
+ └── .gitignore # Generated exclusions for editable secrets
123
+ ```
124
+
125
+ For an individual `settings.cjs` config file, sidecars are `settings.secret.cjs`, `settings.cjs.secrets.enc.json`, and `settings.cjs.recipients/`, beside the config file. An extensionless path resolving to that file uses the same sidecars. File mode selects the base secret module, not profile secret modules.
126
+
127
+ Secret modules export a plain object containing only JSON-compatible values. For example, create `config/index.secret.cjs` with empty values for the administrator to fill privately:
128
+
129
+ ```javascript
130
+ module.exports = {
131
+ db: { password: "" },
132
+ api: { key: "" }
133
+ };
134
+ ```
135
+
136
+ Empty strings remain empty. Configre does not fill values from the environment or populate `process.env`. Functions, `undefined`, accessors, symbols, custom objects, circular references, non-finite numbers, sparse arrays, and keys named `__proto__`, `constructor`, or `prototype` are rejected in secret modules.
137
+
138
+ ### Merge order
139
+
140
+ For `--config=production`, later layers override earlier ones:
141
+
142
+ ```text
143
+ index.cjs → selected public profile → index.secret.cjs → selected secret profile
144
+ ```
145
+
146
+ Public selection prefers `production.dev.cjs` over `production.cjs`. Independently, secret selection prefers `production.dev.secret.cjs` over `production.secret.cjs`; dev and regular variants do not merge together. A selected profile secret can activate secrets without `index.secret.cjs`. Once activated, directory mode encrypts all local `.secret.cjs` files, including inactive profiles, while only base and selected profile values enter the returned config.
147
+
148
+ ### Administrator and server workflow
149
+
150
+ 1. On the administrator, create the needed `.secret.cjs` files and load Configre. It creates or reuses the machine identity, prepares Git exclusions and `recipients/`, and writes `secrets.enc.json`. Editable secrets must not already be tracked by Git; Configre refuses to proceed if they are. It does not untrack files or rewrite history.
151
+ 2. Share the public configuration, generated `.gitignore`, recipient public keys, and ciphertext through Git. Keep editable secret modules and the private identity local. A server with ciphertext and no editable secret modules decrypts the bundle without executing secret modules.
152
+ 3. On an unauthorized server, loading Configre attempts to publish `recipients/<profile>.pub` through Git. New registration requires a branch matching its upstream, Git author identity, and credentials that work without an interactive prompt. Profile names must start with a letter or digit and contain only letters, digits, dots, underscores, or hyphens.
153
+ 4. On the administrator, pull the public-key registration, reload Configre, and publish the updated ciphertext. The server then pulls it and reloads Configre. Publishing a public key alone does not authorize decryption. Any authorized recipient can decrypt the whole bundle, including other profiles; profiles are not access-control boundaries.
154
+
155
+ Loading an unauthorized server config can fetch, create a public-key commit, and push it. Account for these side effects before using application startup as a verification command; use isolated fixtures for local checks unless the requested work authorizes live registration. Configre preserves unrelated staged and unstaged changes through an isolated index, runs no commit or pre-push hooks, and does not merge, rebase, force-push, or publish local tags. Already published matching keys do not create another registration commit. Conflicting keys for the same profile are not overwritten. Failed registration is retried on a later load; each registration Git command has a 30-second timeout.
156
+
157
+ While authorization is pending, Configre warns and returns public settings only: public placeholders remain and encrypted-only fields are absent. Registration failure also permits public-only startup. Do not assume successful application startup proves secrets were loaded; verify required credentials without printing their values. Invalid secret modules, damaged identities, malformed ciphertext, and authentication failures stop loading instead of silently resetting data.
158
+
159
+ ### Updates, revocation, and identity
160
+
161
+ - The administrator's local `.secret.cjs` files are authoritative for values; `recipients/` is authoritative for additional recipients. Reloading updates ciphertext when values or recipients change. Unchanged inputs do not rewrite it; existing editable modules are never overwritten. Deleting every local secret module while retaining ciphertext switches to consumer behavior rather than clearing the bundle.
162
+ - To revoke a recipient, remove its public-key file on the administrator, reload, and distribute the new ciphertext. The current administrator is always included. Remove repository write access too if the machine must not register again. Old ciphertext and previously obtained credentials remain usable; rotate affected credentials when revocation requires it.
163
+ - Identity files live at `~/.config/configre/identity.pem` (private) and `identity.pub` (public). Share only the public key. Back up editable secrets and the private identity securely. On POSIX, the identity directory must have owner-only permissions (`0700`) and the private key owner-only permissions (`0600`). Private identities and secret input files cannot be symlinks.
164
+ - A missing public identity can be regenerated from a valid private key. A corrupt private identity, or a missing private identity with an existing public key, is not silently replaced. Restore the identity or deliberately register a new one. Losing all authorized private keys makes existing ciphertext unrecoverable.
165
+ - Configre uses AES-256-GCM and RSA-3072/OAEP-SHA-256 for encryption and recipient key wrapping. Ciphertext is replaced atomically under a writer lock. If a stale lock is reported after a crash, confirm its writer has stopped before removing it; do not reset ciphertext or identities as a retry strategy.
166
+
167
+ ## Print configuration without secret fields
168
+
169
+ Use `print()` instead of logging the merged config object, which contains decrypted secrets:
170
+
171
+ ```javascript
172
+ const cfg = Configre(join(import.meta.dirname, "config"));
173
+ cfg.print();
174
+ ```
175
+
176
+ `print()` calls Configre's LemonLog `log.debug`. Enable output with `DEBUG=Configre:*`; the `--debug` argument alone does not enable these logs. File creation, ciphertext updates, and public-key publication use `log.info`; authorization failures use `log.warn`, in the same namespace.
177
+
178
+ - `cfg.print()` prints current values, including edits made after loading, without modifying `cfg` or logging automatically on load.
179
+ - Fields present in loaded base or selected profile secret modules are omitted, including when read from ciphertext. Public sibling fields remain visible; arrays supplied by secrets are omitted entirely and emptied secret objects are removed.
180
+ - Classification is by secret-file fields, not names such as `password` or `token`. Sensitive values placed only in public settings or copied to another field are not automatically detected. Keep them in secret modules and do not treat `print()` as a general-purpose redactor.
181
+ - The method is non-enumerable: it does not appear in `Object.keys(cfg)`, object spreads, or JSON output. Those operations still include secret data, so do not use them as redaction. A spread or JSON round trip does not preserve the method.
182
+ - `new Configre(configPath).print()` is also supported and prints the instance's merged settings. `.get()` still returns plain configuration data. If a configuration already has its own `print` field, that value is preserved; use the constructor's `print()` method instead.
183
+
104
184
  ## Examples
105
185
 
106
186
  **Example 1: Basic setup**
@@ -120,7 +200,7 @@ Result: staging overrides are merged over defaults, without conflicting with oth
120
200
 
121
201
  ## Key behaviors
122
202
 
123
- - **Deep merge**: nested objects merge recursively via lodash `_.merge`
203
+ - **Deep merge**: nested plain objects merge recursively using Configre's own implementation; arrays merge by index and retain existing trailing entries rather than being replaced wholesale
124
204
  - **Config files must use the `.cjs` extension** (`.js` is not accepted; works in both CommonJS and ESM projects)
125
205
  - **Required path**: always pass the config directory or file path; prefer an absolute module-relative path over a process-relative path
126
- - **Function vs constructor**: `Configre(path)` returns the merged config directly; `new Configre(path)` returns the instance (use `.get()` to retrieve config)
206
+ - **Function vs constructor**: `Configre(path)` returns the merged config with non-enumerable `print()` when that field is available; `new Configre(path)` returns the instance with `.get()` and `.print()`
@@ -4,7 +4,65 @@ import os from "node:os";
4
4
  import path from "node:path";
5
5
  import { spawnSync } from "node:child_process";
6
6
  import test from "node:test";
7
- import Configre from "../index.js";
7
+ import Configre, { applyConfigEnv } from "../index.js";
8
+
9
+ test("applyConfigEnv fills missing variables without overwriting existing values", t => {
10
+ const env = {
11
+ CONFIGRE_TEST_ENV_TEXT: "value",
12
+ CONFIGRE_TEST_ENV_NUMBER: 0,
13
+ CONFIGRE_TEST_ENV_BOOLEAN: false,
14
+ CONFIGRE_TEST_ENV_EMPTY: "",
15
+ CONFIGRE_TEST_ENV_NULL: null,
16
+ CONFIGRE_TEST_ENV_UNDEFINED: undefined,
17
+ CONFIGRE_TEST_ENV_EXISTING: "replacement",
18
+ CONFIGRE_TEST_ENV_EXISTING_EMPTY: "replacement"
19
+ };
20
+ const original = Object.fromEntries(Object.keys(env).map(key => [key, process.env[key]]));
21
+ t.after(() => {
22
+ for (const [key, value] of Object.entries(original)) {
23
+ if (value === undefined) delete process.env[key];
24
+ else process.env[key] = value;
25
+ }
26
+ });
27
+ for (const key of Object.keys(env)) delete process.env[key];
28
+ process.env.CONFIGRE_TEST_ENV_EXISTING = "original";
29
+ process.env.CONFIGRE_TEST_ENV_EXISTING_EMPTY = "";
30
+
31
+ assert.equal(applyConfigEnv(env), undefined);
32
+ assert.equal(process.env.CONFIGRE_TEST_ENV_TEXT, "value");
33
+ assert.equal(process.env.CONFIGRE_TEST_ENV_NUMBER, "0");
34
+ assert.equal(process.env.CONFIGRE_TEST_ENV_BOOLEAN, "false");
35
+ assert.equal(process.env.CONFIGRE_TEST_ENV_EMPTY, "");
36
+ assert.equal(process.env.CONFIGRE_TEST_ENV_NULL, undefined);
37
+ assert.equal(process.env.CONFIGRE_TEST_ENV_UNDEFINED, undefined);
38
+ assert.equal(process.env.CONFIGRE_TEST_ENV_EXISTING, "original");
39
+ assert.equal(process.env.CONFIGRE_TEST_ENV_EXISTING_EMPTY, "");
40
+ applyConfigEnv({ CONFIGRE_TEST_ENV_TEXT: "replacement" });
41
+ assert.equal(process.env.CONFIGRE_TEST_ENV_TEXT, "value");
42
+ assert.equal(env.CONFIGRE_TEST_ENV_NUMBER, 0);
43
+ assert.equal(env.CONFIGRE_TEST_ENV_BOOLEAN, false);
44
+ });
45
+
46
+ test("applyConfigEnv accepts omitted configuration and rejects invalid inputs", () => {
47
+ assert.equal(applyConfigEnv(), undefined);
48
+ assert.equal(applyConfigEnv({}), undefined);
49
+ for (const env of [null, [], "text", 1, true, () => {}]) {
50
+ assert.throws(() => applyConfigEnv(env), {
51
+ name: "TypeError",
52
+ message: "config.env must be a plain object"
53
+ });
54
+ }
55
+ assert.throws(() => applyConfigEnv({ "": null }), {
56
+ name: "TypeError",
57
+ message: "config.env keys must be non-empty strings"
58
+ });
59
+ for (const value of [{ nested: true }, [], new Date()]) {
60
+ assert.throws(() => applyConfigEnv({ CONFIGRE_TEST_ENV_INVALID: value }), {
61
+ name: "TypeError",
62
+ message: "config.env.CONFIGRE_TEST_ENV_INVALID must be a scalar"
63
+ });
64
+ }
65
+ });
8
66
 
9
67
  test("requires a config path", () => {
10
68
  assert.throws(
@@ -44,17 +102,25 @@ for (const format of ["commonjs", "module"]) {
44
102
  const entry = format === "commonjs" ? `
45
103
  const assert = require('node:assert/strict');
46
104
  const Configre = require('configre');
47
- import('configre').then(module => assert.equal(module.default, Configre));
105
+ const { applyConfigEnv } = Configre;
106
+ import('configre').then(module => {
107
+ assert.equal(module.default, Configre);
108
+ assert.equal(module.applyConfigEnv, applyConfigEnv);
109
+ });
48
110
  ` : `
49
111
  import assert from 'node:assert/strict';
50
112
  import { createRequire } from 'node:module';
51
- import Configre from 'configre';
113
+ import Configre, { applyConfigEnv } from 'configre';
52
114
  const require = createRequire(import.meta.url);
53
115
  assert.equal(require('configre'), Configre);
116
+ assert.equal(require('configre').applyConfigEnv, applyConfigEnv);
54
117
  `;
55
118
  const filename = path.join(directory, format === "commonjs" ? "consumer.cjs" : "consumer.mjs");
56
119
  fs.writeFileSync(filename, entry + `
57
120
  assert.equal(typeof Configre, 'function');
121
+ delete process.env.CONFIGRE_TEST_ENV_INTEROP;
122
+ applyConfigEnv({ CONFIGRE_TEST_ENV_INTEROP: 123 });
123
+ assert.equal(process.env.CONFIGRE_TEST_ENV_INTEROP, '123');
58
124
  const configPath = ${JSON.stringify(config)};
59
125
  const expected = { db: { host: 'localhost', port: 456 } };
60
126
  assert.deepEqual(Configre(configPath), expected);