scenv 0.2.1 → 0.3.2

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 ADDED
@@ -0,0 +1,65 @@
1
+ # scenv
2
+
3
+ Environment and context variables with runtime-configurable resolution.
4
+
5
+ Define variables once with `scenv()`, then resolve values from **set overrides** (e.g. CLI `--set`), **environment**, **context files**, or **defaults**. Control behavior via config (file, env, or `configure()`)—same code, different config per run.
6
+
7
+ **Requires Node 18+.**
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ pnpm add scenv
13
+ # or
14
+ npm install scenv
15
+ ```
16
+
17
+ ## Quick start
18
+
19
+ ```ts
20
+ import { configure, parseScenvArgs, scenv } from "scenv";
21
+
22
+ // Optional: apply CLI flags (--set, --context, --prompt, etc.)
23
+ configure(parseScenvArgs(process.argv.slice(2)));
24
+
25
+ const apiUrl = scenv("API URL", {
26
+ key: "api_url",
27
+ env: "API_URL",
28
+ default: "http://localhost:4000",
29
+ });
30
+
31
+ const url = await apiUrl.get(); // throws if missing or invalid
32
+ const result = await apiUrl.safeGet(); // { success, value? } | { success: false, error? }
33
+ await apiUrl.save(); // write current value to a context file
34
+ ```
35
+
36
+ ## Resolution order
37
+
38
+ 1. **Set overrides** – e.g. `--set key=value`
39
+ 2. **Environment** – `process.env[envKey]`
40
+ 3. **Context** – merged JSON context files
41
+ 4. **Default** – variable’s `default` option
42
+
43
+ Prompting (when to ask the user) is controlled by config `prompt`: `always` | `never` | `fallback` | `no-env`.
44
+
45
+ ## Optional integrations
46
+
47
+ | Package | Purpose |
48
+ |--------|--------|
49
+ | [scenv-zod](https://www.npmjs.com/package/scenv-zod) | `validator(zodSchema)` for type-safe validation and coercion. |
50
+ | [scenv-inquirer](https://www.npmjs.com/package/scenv-inquirer) | `prompt()` and callbacks for interactive prompts. |
51
+
52
+ ## Documentation
53
+
54
+ Full docs (config, contexts, resolution, saving, API) live in the [monorepo](https://github.com/PKWadsy/senv):
55
+
56
+ - [Configuration](https://github.com/PKWadsy/senv/blob/main/docs/CONFIGURATION.md)
57
+ - [Contexts](https://github.com/PKWadsy/senv/blob/main/docs/CONTEXTS.md)
58
+ - [Resolution](https://github.com/PKWadsy/senv/blob/main/docs/RESOLUTION.md)
59
+ - [Saving](https://github.com/PKWadsy/senv/blob/main/docs/SAVING.md)
60
+ - [API reference](https://github.com/PKWadsy/senv/blob/main/docs/API.md)
61
+ - [Integration (scenv-zod, scenv-inquirer)](https://github.com/PKWadsy/senv/blob/main/docs/INTEGRATION.md)
62
+
63
+ ## License
64
+
65
+ MIT
package/dist/index.cjs CHANGED
@@ -235,26 +235,6 @@ function writeToContext(contextName, key, value) {
235
235
  (0, import_node_fs2.writeFileSync)(path, JSON.stringify(data, null, 2) + "\n", "utf-8");
236
236
  }
237
237
 
238
- // src/prompt-default.ts
239
- var import_node_readline = require("readline");
240
- function defaultPrompt(name, defaultValue) {
241
- const defaultStr = defaultValue !== void 0 && defaultValue !== null ? String(defaultValue) : "";
242
- const message = defaultStr ? `Enter ${name} [${defaultStr}]: ` : `Enter ${name}: `;
243
- const rl = (0, import_node_readline.createInterface)({ input: process.stdin, output: process.stdout });
244
- return new Promise((resolve, reject) => {
245
- rl.question(message, (answer) => {
246
- rl.close();
247
- const trimmed = answer.trim();
248
- const value = trimmed !== "" ? trimmed : defaultStr;
249
- resolve(value);
250
- });
251
- rl.on("error", (err) => {
252
- rl.close();
253
- reject(err);
254
- });
255
- });
256
- }
257
-
258
238
  // src/variable.ts
259
239
  function defaultKeyFromName(name) {
260
240
  return name.toLowerCase().replace(/\s+/g, "_").replace(/[^a-z0-9_]/gi, "");
@@ -296,23 +276,29 @@ function scenv(name, options = {}) {
296
276
  if (mode === "no-env") return !hadEnv;
297
277
  return false;
298
278
  }
299
- async function getResolvedValue() {
279
+ async function getResolvedValue(overrides) {
300
280
  const config = loadConfig();
301
281
  const raw = await resolveRaw();
302
282
  const hadEnv = !config.ignoreEnv && process.env[envKey] !== void 0 && process.env[envKey] !== "";
303
283
  const hadValue = raw !== void 0;
284
+ const effectiveDefault = overrides?.default !== void 0 ? overrides.default : defaultValue;
304
285
  let wasPrompted = false;
305
286
  let value;
306
287
  if (shouldPrompt(config, hadValue, hadEnv)) {
307
- const defaultForPrompt = raw !== void 0 ? raw : defaultValue;
308
288
  const callbacks = getCallbacks();
309
- const fn = promptFn ?? callbacks.defaultPrompt ?? defaultPrompt;
289
+ const fn = overrides?.prompt ?? promptFn ?? callbacks.defaultPrompt;
290
+ if (typeof fn !== "function") {
291
+ throw new Error(
292
+ `Prompt required for variable "${name}" (key: ${key}) but no prompt was supplied and no defaultPrompt callback is configured. Set a prompt on the variable or configure({ callbacks: { defaultPrompt: ... } }).`
293
+ );
294
+ }
295
+ const defaultForPrompt = raw !== void 0 ? raw : effectiveDefault;
310
296
  value = await Promise.resolve(fn(name, defaultForPrompt));
311
297
  wasPrompted = true;
312
298
  } else if (raw !== void 0) {
313
299
  value = raw;
314
- } else if (defaultValue !== void 0) {
315
- value = defaultValue;
300
+ } else if (effectiveDefault !== void 0) {
301
+ value = effectiveDefault;
316
302
  } else {
317
303
  throw new Error(`Missing value for variable "${name}" (key: ${key})`);
318
304
  }
@@ -331,8 +317,8 @@ function scenv(name, options = {}) {
331
317
  error: "error" in normalized ? normalized.error : void 0
332
318
  };
333
319
  }
334
- async function get() {
335
- const { value, wasPrompted } = await getResolvedValue();
320
+ async function get(options2) {
321
+ const { value, wasPrompted } = await getResolvedValue(options2);
336
322
  const validated = validate(value);
337
323
  if (!validated.success) {
338
324
  throw new Error(
@@ -346,7 +332,12 @@ function scenv(name, options = {}) {
346
332
  const shouldAskSave = savePrompt === "always" || savePrompt === "ask" && wasPrompted;
347
333
  if (shouldAskSave) {
348
334
  const callbacks = getCallbacks();
349
- const ctxToSave = callbacks.onAskSaveAfterPrompt && await callbacks.onAskSaveAfterPrompt(
335
+ if (typeof callbacks.onAskSaveAfterPrompt !== "function") {
336
+ throw new Error(
337
+ `savePrompt is "${savePrompt}" but onAskSaveAfterPrompt callback is not set. Configure callbacks via configure({ callbacks: { onAskSaveAfterPrompt: ... } }).`
338
+ );
339
+ }
340
+ const ctxToSave = await callbacks.onAskSaveAfterPrompt(
350
341
  name,
351
342
  final,
352
343
  config.contexts ?? []
@@ -356,9 +347,9 @@ function scenv(name, options = {}) {
356
347
  }
357
348
  return final;
358
349
  }
359
- async function safeGet() {
350
+ async function safeGet(options2) {
360
351
  try {
361
- const v = await get();
352
+ const v = await get(options2);
362
353
  return { success: true, value: v };
363
354
  } catch (err) {
364
355
  return { success: false, error: err };
@@ -376,14 +367,15 @@ function scenv(name, options = {}) {
376
367
  let contextName = config.saveContextTo;
377
368
  if (contextName === "ask") {
378
369
  const callbacks = getCallbacks();
379
- if (typeof callbacks.onAskContext === "function") {
380
- contextName = await callbacks.onAskContext(
381
- name,
382
- config.contexts ?? []
370
+ if (typeof callbacks.onAskContext !== "function") {
371
+ throw new Error(
372
+ `saveContextTo is "ask" but onAskContext callback is not set. Configure callbacks via configure({ callbacks: { onAskContext: ... } }).`
383
373
  );
384
- } else {
385
- contextName = config.contexts?.[0] ?? "default";
386
374
  }
375
+ contextName = await callbacks.onAskContext(
376
+ name,
377
+ config.contexts ?? []
378
+ );
387
379
  }
388
380
  if (!contextName) contextName = config.contexts?.[0] ?? "default";
389
381
  writeToContext(contextName, key, String(validated.data));
package/dist/index.d.cts CHANGED
@@ -70,9 +70,16 @@ interface ScenvVariableOptions<T> {
70
70
  validator?: (val: T) => ValidatorResult<T>;
71
71
  prompt?: PromptFn<T>;
72
72
  }
73
+ /** Overrides for a single .get() or .safeGet() call. */
74
+ interface GetOptions<T> {
75
+ /** Use this prompt for this call instead of the variable's prompt or callbacks.defaultPrompt. */
76
+ prompt?: PromptFn<T>;
77
+ /** Use this as the default for this call if no value from set/env/context. */
78
+ default?: T;
79
+ }
73
80
  interface ScenvVariable<T> {
74
- get(): Promise<T>;
75
- safeGet(): Promise<{
81
+ get(options?: GetOptions<T>): Promise<T>;
82
+ safeGet(options?: GetOptions<T>): Promise<{
76
83
  success: true;
77
84
  value: T;
78
85
  } | {
@@ -90,4 +97,4 @@ declare function scenv<T>(name: string, options?: ScenvVariableOptions<T>): Scen
90
97
  */
91
98
  declare function parseScenvArgs(argv: string[]): Partial<ScenvConfig>;
92
99
 
93
- export { type DefaultPromptFn, type PromptMode, type SavePromptMode, type ScenvCallbacks, type ScenvConfig, type ScenvVariable, configure, discoverContextPaths, getCallbacks, getContextValues, loadConfig, parseScenvArgs, resetConfig, scenv };
100
+ export { type DefaultPromptFn, type GetOptions, type PromptMode, type SavePromptMode, type ScenvCallbacks, type ScenvConfig, type ScenvVariable, configure, discoverContextPaths, getCallbacks, getContextValues, loadConfig, parseScenvArgs, resetConfig, scenv };
package/dist/index.d.ts CHANGED
@@ -70,9 +70,16 @@ interface ScenvVariableOptions<T> {
70
70
  validator?: (val: T) => ValidatorResult<T>;
71
71
  prompt?: PromptFn<T>;
72
72
  }
73
+ /** Overrides for a single .get() or .safeGet() call. */
74
+ interface GetOptions<T> {
75
+ /** Use this prompt for this call instead of the variable's prompt or callbacks.defaultPrompt. */
76
+ prompt?: PromptFn<T>;
77
+ /** Use this as the default for this call if no value from set/env/context. */
78
+ default?: T;
79
+ }
73
80
  interface ScenvVariable<T> {
74
- get(): Promise<T>;
75
- safeGet(): Promise<{
81
+ get(options?: GetOptions<T>): Promise<T>;
82
+ safeGet(options?: GetOptions<T>): Promise<{
76
83
  success: true;
77
84
  value: T;
78
85
  } | {
@@ -90,4 +97,4 @@ declare function scenv<T>(name: string, options?: ScenvVariableOptions<T>): Scen
90
97
  */
91
98
  declare function parseScenvArgs(argv: string[]): Partial<ScenvConfig>;
92
99
 
93
- export { type DefaultPromptFn, type PromptMode, type SavePromptMode, type ScenvCallbacks, type ScenvConfig, type ScenvVariable, configure, discoverContextPaths, getCallbacks, getContextValues, loadConfig, parseScenvArgs, resetConfig, scenv };
100
+ export { type DefaultPromptFn, type GetOptions, type PromptMode, type SavePromptMode, type ScenvCallbacks, type ScenvConfig, type ScenvVariable, configure, discoverContextPaths, getCallbacks, getContextValues, loadConfig, parseScenvArgs, resetConfig, scenv };
package/dist/index.js CHANGED
@@ -208,26 +208,6 @@ function writeToContext(contextName, key, value) {
208
208
  writeFileSync(path, JSON.stringify(data, null, 2) + "\n", "utf-8");
209
209
  }
210
210
 
211
- // src/prompt-default.ts
212
- import { createInterface } from "readline";
213
- function defaultPrompt(name, defaultValue) {
214
- const defaultStr = defaultValue !== void 0 && defaultValue !== null ? String(defaultValue) : "";
215
- const message = defaultStr ? `Enter ${name} [${defaultStr}]: ` : `Enter ${name}: `;
216
- const rl = createInterface({ input: process.stdin, output: process.stdout });
217
- return new Promise((resolve, reject) => {
218
- rl.question(message, (answer) => {
219
- rl.close();
220
- const trimmed = answer.trim();
221
- const value = trimmed !== "" ? trimmed : defaultStr;
222
- resolve(value);
223
- });
224
- rl.on("error", (err) => {
225
- rl.close();
226
- reject(err);
227
- });
228
- });
229
- }
230
-
231
211
  // src/variable.ts
232
212
  function defaultKeyFromName(name) {
233
213
  return name.toLowerCase().replace(/\s+/g, "_").replace(/[^a-z0-9_]/gi, "");
@@ -269,23 +249,29 @@ function scenv(name, options = {}) {
269
249
  if (mode === "no-env") return !hadEnv;
270
250
  return false;
271
251
  }
272
- async function getResolvedValue() {
252
+ async function getResolvedValue(overrides) {
273
253
  const config = loadConfig();
274
254
  const raw = await resolveRaw();
275
255
  const hadEnv = !config.ignoreEnv && process.env[envKey] !== void 0 && process.env[envKey] !== "";
276
256
  const hadValue = raw !== void 0;
257
+ const effectiveDefault = overrides?.default !== void 0 ? overrides.default : defaultValue;
277
258
  let wasPrompted = false;
278
259
  let value;
279
260
  if (shouldPrompt(config, hadValue, hadEnv)) {
280
- const defaultForPrompt = raw !== void 0 ? raw : defaultValue;
281
261
  const callbacks = getCallbacks();
282
- const fn = promptFn ?? callbacks.defaultPrompt ?? defaultPrompt;
262
+ const fn = overrides?.prompt ?? promptFn ?? callbacks.defaultPrompt;
263
+ if (typeof fn !== "function") {
264
+ throw new Error(
265
+ `Prompt required for variable "${name}" (key: ${key}) but no prompt was supplied and no defaultPrompt callback is configured. Set a prompt on the variable or configure({ callbacks: { defaultPrompt: ... } }).`
266
+ );
267
+ }
268
+ const defaultForPrompt = raw !== void 0 ? raw : effectiveDefault;
283
269
  value = await Promise.resolve(fn(name, defaultForPrompt));
284
270
  wasPrompted = true;
285
271
  } else if (raw !== void 0) {
286
272
  value = raw;
287
- } else if (defaultValue !== void 0) {
288
- value = defaultValue;
273
+ } else if (effectiveDefault !== void 0) {
274
+ value = effectiveDefault;
289
275
  } else {
290
276
  throw new Error(`Missing value for variable "${name}" (key: ${key})`);
291
277
  }
@@ -304,8 +290,8 @@ function scenv(name, options = {}) {
304
290
  error: "error" in normalized ? normalized.error : void 0
305
291
  };
306
292
  }
307
- async function get() {
308
- const { value, wasPrompted } = await getResolvedValue();
293
+ async function get(options2) {
294
+ const { value, wasPrompted } = await getResolvedValue(options2);
309
295
  const validated = validate(value);
310
296
  if (!validated.success) {
311
297
  throw new Error(
@@ -319,7 +305,12 @@ function scenv(name, options = {}) {
319
305
  const shouldAskSave = savePrompt === "always" || savePrompt === "ask" && wasPrompted;
320
306
  if (shouldAskSave) {
321
307
  const callbacks = getCallbacks();
322
- const ctxToSave = callbacks.onAskSaveAfterPrompt && await callbacks.onAskSaveAfterPrompt(
308
+ if (typeof callbacks.onAskSaveAfterPrompt !== "function") {
309
+ throw new Error(
310
+ `savePrompt is "${savePrompt}" but onAskSaveAfterPrompt callback is not set. Configure callbacks via configure({ callbacks: { onAskSaveAfterPrompt: ... } }).`
311
+ );
312
+ }
313
+ const ctxToSave = await callbacks.onAskSaveAfterPrompt(
323
314
  name,
324
315
  final,
325
316
  config.contexts ?? []
@@ -329,9 +320,9 @@ function scenv(name, options = {}) {
329
320
  }
330
321
  return final;
331
322
  }
332
- async function safeGet() {
323
+ async function safeGet(options2) {
333
324
  try {
334
- const v = await get();
325
+ const v = await get(options2);
335
326
  return { success: true, value: v };
336
327
  } catch (err) {
337
328
  return { success: false, error: err };
@@ -349,14 +340,15 @@ function scenv(name, options = {}) {
349
340
  let contextName = config.saveContextTo;
350
341
  if (contextName === "ask") {
351
342
  const callbacks = getCallbacks();
352
- if (typeof callbacks.onAskContext === "function") {
353
- contextName = await callbacks.onAskContext(
354
- name,
355
- config.contexts ?? []
343
+ if (typeof callbacks.onAskContext !== "function") {
344
+ throw new Error(
345
+ `saveContextTo is "ask" but onAskContext callback is not set. Configure callbacks via configure({ callbacks: { onAskContext: ... } }).`
356
346
  );
357
- } else {
358
- contextName = config.contexts?.[0] ?? "default";
359
347
  }
348
+ contextName = await callbacks.onAskContext(
349
+ name,
350
+ config.contexts ?? []
351
+ );
360
352
  }
361
353
  if (!contextName) contextName = config.contexts?.[0] ?? "default";
362
354
  writeToContext(contextName, key, String(validated.data));
package/package.json CHANGED
@@ -1,17 +1,10 @@
1
1
  {
2
2
  "name": "scenv",
3
- "version": "0.2.1",
3
+ "version": "0.3.2",
4
4
  "description": "Environment and context variables with runtime-configurable resolution",
5
- "repository": {
6
- "type": "git",
7
- "url": "git+https://github.com/PKWadsy/senv.git"
8
- },
9
- "keywords": [
10
- "env",
11
- "config",
12
- "context",
13
- "variables"
14
- ],
5
+ "repository": { "type": "git", "url": "https://github.com/PKWadsy/scenv" },
6
+ "publishConfig": { "access": "public", "provenance": true },
7
+ "keywords": ["env", "config", "context", "variables"],
15
8
  "type": "module",
16
9
  "main": "dist/index.cjs",
17
10
  "module": "dist/index.js",
@@ -26,6 +19,11 @@
26
19
  "files": [
27
20
  "dist"
28
21
  ],
22
+ "scripts": {
23
+ "build": "tsup src/index.ts --format cjs,esm --dts --clean",
24
+ "test": "vitest run",
25
+ "test:watch": "vitest"
26
+ },
29
27
  "devDependencies": {
30
28
  "@types/node": "^20.0.0",
31
29
  "@vitest/coverage-v8": "^2.1.9",
@@ -35,10 +33,5 @@
35
33
  },
36
34
  "engines": {
37
35
  "node": ">=18"
38
- },
39
- "scripts": {
40
- "build": "tsup src/index.ts --format cjs,esm --dts --clean",
41
- "test": "vitest run",
42
- "test:watch": "vitest"
43
36
  }
44
- }
37
+ }