scenv 0.4.2 → 0.5.1
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 +16 -14
- package/dist/index.cjs +146 -32
- package/dist/index.d.cts +195 -30
- package/dist/index.d.ts +195 -30
- package/dist/index.js +144 -31
- package/package.json +15 -4
package/README.md
CHANGED
|
@@ -28,9 +28,9 @@ const apiUrl = scenv("API URL", {
|
|
|
28
28
|
default: "http://localhost:4000",
|
|
29
29
|
});
|
|
30
30
|
|
|
31
|
-
const url = await apiUrl.get();
|
|
32
|
-
const result = await apiUrl.safeGet();
|
|
33
|
-
await apiUrl.save();
|
|
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
34
|
```
|
|
35
35
|
|
|
36
36
|
## Resolution order
|
|
@@ -40,25 +40,27 @@ await apiUrl.save(); // write current value to a context file
|
|
|
40
40
|
3. **Context** – merged JSON context files
|
|
41
41
|
4. **Default** – variable’s `default` option
|
|
42
42
|
|
|
43
|
+
Any string value matching **`@<context>:<key>`** (e.g. `@prod:core_server_url`) is resolved from that context file first—in set, env, context, default, and prompts.
|
|
44
|
+
|
|
43
45
|
Prompting (when to ask the user) is controlled by config `prompt`: `always` | `never` | `fallback` | `no-env`.
|
|
44
46
|
|
|
45
47
|
## Optional integrations
|
|
46
48
|
|
|
47
|
-
| Package
|
|
48
|
-
|
|
49
|
-
| [scenv-zod](https://www.npmjs.com/package/scenv-zod)
|
|
50
|
-
| [scenv-inquirer](https://www.npmjs.com/package/scenv-inquirer) | `prompt()` and callbacks for interactive prompts.
|
|
49
|
+
| Package | Purpose |
|
|
50
|
+
| -------------------------------------------------------------- | ------------------------------------------------------------- |
|
|
51
|
+
| [scenv-zod](https://www.npmjs.com/package/scenv-zod) | `validator(zodSchema)` for type-safe validation and coercion. |
|
|
52
|
+
| [scenv-inquirer](https://www.npmjs.com/package/scenv-inquirer) | `prompt()` and callbacks for interactive prompts. |
|
|
51
53
|
|
|
52
54
|
## Documentation
|
|
53
55
|
|
|
54
|
-
Full docs (config, contexts, resolution, saving, API) live in the [monorepo](https://github.com/PKWadsy/
|
|
56
|
+
Full docs (config, contexts, resolution, saving, API) live in the [monorepo](https://github.com/PKWadsy/scenv):
|
|
55
57
|
|
|
56
|
-
- [Configuration](https://github.com/PKWadsy/
|
|
57
|
-
- [Contexts](https://github.com/PKWadsy/
|
|
58
|
-
- [Resolution](https://github.com/PKWadsy/
|
|
59
|
-
- [Saving](https://github.com/PKWadsy/
|
|
60
|
-
- [API reference](https://github.com/PKWadsy/
|
|
61
|
-
- [Integration (scenv-zod, scenv-inquirer)](https://github.com/PKWadsy/
|
|
58
|
+
- [Configuration](https://github.com/PKWadsy/scenv/blob/main/docs/CONFIGURATION.md)
|
|
59
|
+
- [Contexts](https://github.com/PKWadsy/scenv/blob/main/docs/CONTEXTS.md)
|
|
60
|
+
- [Resolution](https://github.com/PKWadsy/scenv/blob/main/docs/RESOLUTION.md)
|
|
61
|
+
- [Saving](https://github.com/PKWadsy/scenv/blob/main/docs/SAVING.md)
|
|
62
|
+
- [API reference](https://github.com/PKWadsy/scenv/blob/main/docs/API.md)
|
|
63
|
+
- [Integration (scenv-zod, scenv-inquirer)](https://github.com/PKWadsy/scenv/blob/main/docs/INTEGRATION.md)
|
|
62
64
|
|
|
63
65
|
## License
|
|
64
66
|
|
package/dist/index.cjs
CHANGED
|
@@ -24,7 +24,8 @@ __export(index_exports, {
|
|
|
24
24
|
configure: () => configure,
|
|
25
25
|
discoverContextPaths: () => discoverContextPaths,
|
|
26
26
|
getCallbacks: () => getCallbacks,
|
|
27
|
-
|
|
27
|
+
getContext: () => getContext,
|
|
28
|
+
getMergedContextValues: () => getMergedContextValues,
|
|
28
29
|
loadConfig: () => loadConfig,
|
|
29
30
|
parseScenvArgs: () => parseScenvArgs,
|
|
30
31
|
resetConfig: () => resetConfig,
|
|
@@ -36,6 +37,52 @@ module.exports = __toCommonJS(index_exports);
|
|
|
36
37
|
// src/config.ts
|
|
37
38
|
var import_node_fs = require("fs");
|
|
38
39
|
var import_node_path = require("path");
|
|
40
|
+
|
|
41
|
+
// src/prompt-default.ts
|
|
42
|
+
var import_node_readline = require("readline");
|
|
43
|
+
function ask(message) {
|
|
44
|
+
const rl = (0, import_node_readline.createInterface)({ input: process.stdin, output: process.stdout });
|
|
45
|
+
return new Promise((resolve, reject) => {
|
|
46
|
+
rl.question(message, (answer) => {
|
|
47
|
+
rl.close();
|
|
48
|
+
resolve(answer.trim());
|
|
49
|
+
});
|
|
50
|
+
rl.on("error", (err) => {
|
|
51
|
+
rl.close();
|
|
52
|
+
reject(err);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
function defaultPrompt(name, defaultValue) {
|
|
57
|
+
const defaultStr = defaultValue !== void 0 && defaultValue !== null ? String(defaultValue) : "";
|
|
58
|
+
const message = defaultStr ? `Enter ${name} [${defaultStr}]: ` : `Enter ${name}: `;
|
|
59
|
+
const rl = (0, import_node_readline.createInterface)({ input: process.stdin, output: process.stdout });
|
|
60
|
+
return new Promise((resolve, reject) => {
|
|
61
|
+
rl.question(message, (answer) => {
|
|
62
|
+
rl.close();
|
|
63
|
+
const trimmed = answer.trim();
|
|
64
|
+
const value = trimmed !== "" ? trimmed : defaultStr;
|
|
65
|
+
resolve(value);
|
|
66
|
+
});
|
|
67
|
+
rl.on("error", (err) => {
|
|
68
|
+
rl.close();
|
|
69
|
+
reject(err);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
async function defaultAskWhetherToSave(name, _value) {
|
|
74
|
+
const answer = await ask(`Save "${name}" for next time? (y/n): `);
|
|
75
|
+
const v = answer.toLowerCase();
|
|
76
|
+
return v === "y" || v === "yes" || v === "1" || v === "true";
|
|
77
|
+
}
|
|
78
|
+
async function defaultAskContext(name, contextNames) {
|
|
79
|
+
const hint = contextNames.length > 0 ? ` (${contextNames.join(", ")})` : "";
|
|
80
|
+
const answer = await ask(`Save "${name}" to which context?${hint}: `);
|
|
81
|
+
if (answer) return answer;
|
|
82
|
+
return contextNames[0] ?? "default";
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// src/config.ts
|
|
39
86
|
var LOG_LEVELS = ["none", "trace", "debug", "info", "warn", "error"];
|
|
40
87
|
var CONFIG_FILENAME = "scenv.config.json";
|
|
41
88
|
var envKeyMap = {
|
|
@@ -44,14 +91,18 @@ var envKeyMap = {
|
|
|
44
91
|
SCENV_PROMPT: "prompt",
|
|
45
92
|
SCENV_IGNORE_ENV: "ignoreEnv",
|
|
46
93
|
SCENV_IGNORE_CONTEXT: "ignoreContext",
|
|
47
|
-
SCENV_SAVE_PROMPT: "
|
|
94
|
+
SCENV_SAVE_PROMPT: "shouldSavePrompt",
|
|
48
95
|
SCENV_SAVE_CONTEXT_TO: "saveContextTo",
|
|
49
96
|
SCENV_LOG_LEVEL: "logLevel"
|
|
50
97
|
};
|
|
51
98
|
var programmaticConfig = {};
|
|
52
99
|
var programmaticCallbacks = {};
|
|
53
100
|
function getCallbacks() {
|
|
54
|
-
return {
|
|
101
|
+
return {
|
|
102
|
+
defaultPrompt: programmaticCallbacks.defaultPrompt ?? defaultPrompt,
|
|
103
|
+
onAskWhetherToSave: programmaticCallbacks.onAskWhetherToSave ?? defaultAskWhetherToSave,
|
|
104
|
+
onAskContext: programmaticCallbacks.onAskContext ?? defaultAskContext
|
|
105
|
+
};
|
|
55
106
|
}
|
|
56
107
|
function findConfigDir(startDir) {
|
|
57
108
|
let dir = startDir;
|
|
@@ -73,11 +124,11 @@ function configFromEnv() {
|
|
|
73
124
|
out[configKey] = val.split(",").map((s) => s.trim()).filter(Boolean);
|
|
74
125
|
} else if (configKey === "ignoreEnv" || configKey === "ignoreContext") {
|
|
75
126
|
out[configKey] = val === "1" || val === "true" || val.toLowerCase() === "yes";
|
|
76
|
-
} else if (configKey === "prompt" || configKey === "
|
|
127
|
+
} else if (configKey === "prompt" || configKey === "shouldSavePrompt") {
|
|
77
128
|
const v = val.toLowerCase();
|
|
78
129
|
if (configKey === "prompt" && (v === "always" || v === "never" || v === "fallback" || v === "no-env"))
|
|
79
130
|
out[configKey] = v;
|
|
80
|
-
if (configKey === "
|
|
131
|
+
if (configKey === "shouldSavePrompt" && (v === "always" || v === "never" || v === "ask"))
|
|
81
132
|
out[configKey] = v;
|
|
82
133
|
} else if (configKey === "saveContextTo") {
|
|
83
134
|
out.saveContextTo = val;
|
|
@@ -110,8 +161,8 @@ function loadConfigFile(configDir) {
|
|
|
110
161
|
out.ignoreContext = parsed.ignoreContext;
|
|
111
162
|
if (parsed.set && typeof parsed.set === "object" && !Array.isArray(parsed.set))
|
|
112
163
|
out.set = parsed.set;
|
|
113
|
-
if (typeof parsed.savePrompt === "string" && ["always", "never", "ask"].includes(parsed.savePrompt))
|
|
114
|
-
out.
|
|
164
|
+
if (typeof (parsed.shouldSavePrompt ?? parsed.savePrompt) === "string" && ["always", "never", "ask"].includes(parsed.shouldSavePrompt ?? parsed.savePrompt))
|
|
165
|
+
out.shouldSavePrompt = parsed.shouldSavePrompt ?? parsed.savePrompt;
|
|
115
166
|
if (typeof parsed.saveContextTo === "string")
|
|
116
167
|
out.saveContextTo = parsed.saveContextTo;
|
|
117
168
|
if (typeof parsed.root === "string") out.root = parsed.root;
|
|
@@ -255,7 +306,32 @@ function discoverContextPaths(dir, found = /* @__PURE__ */ new Map()) {
|
|
|
255
306
|
);
|
|
256
307
|
return found;
|
|
257
308
|
}
|
|
258
|
-
function
|
|
309
|
+
function getContext(contextName, root) {
|
|
310
|
+
const config = loadConfig();
|
|
311
|
+
const searchRoot = root ?? config.root ?? process.cwd();
|
|
312
|
+
const paths = discoverContextPaths(searchRoot);
|
|
313
|
+
const filePath = paths.get(contextName);
|
|
314
|
+
if (!filePath) {
|
|
315
|
+
log("trace", `getContext: context "${contextName}" not found`);
|
|
316
|
+
return {};
|
|
317
|
+
}
|
|
318
|
+
try {
|
|
319
|
+
const raw = (0, import_node_fs2.readFileSync)(filePath, "utf-8");
|
|
320
|
+
const data = JSON.parse(raw);
|
|
321
|
+
const out = {};
|
|
322
|
+
for (const [k, v] of Object.entries(data)) {
|
|
323
|
+
if (typeof v === "string") out[k] = v;
|
|
324
|
+
}
|
|
325
|
+
return out;
|
|
326
|
+
} catch (err) {
|
|
327
|
+
log(
|
|
328
|
+
"trace",
|
|
329
|
+
`getContext: "${contextName}" unreadable: ${err instanceof Error ? err.message : String(err)}`
|
|
330
|
+
);
|
|
331
|
+
return {};
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
function getMergedContextValues() {
|
|
259
335
|
const config = loadConfig();
|
|
260
336
|
logConfigLoaded(config);
|
|
261
337
|
if (config.ignoreContext) return {};
|
|
@@ -312,6 +388,27 @@ function writeToContext(contextName, key, value) {
|
|
|
312
388
|
}
|
|
313
389
|
|
|
314
390
|
// src/variable.ts
|
|
391
|
+
var CONTEXT_REF_REGEX = /^@([^:]+):(.+)$/;
|
|
392
|
+
var MAX_CONTEXT_REF_DEPTH = 10;
|
|
393
|
+
function resolveContextReference(raw, depth = 0) {
|
|
394
|
+
if (depth >= MAX_CONTEXT_REF_DEPTH) {
|
|
395
|
+
throw new Error(
|
|
396
|
+
`Context reference resolution exceeded max depth (${MAX_CONTEXT_REF_DEPTH}): possible circular reference`
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
const match = raw.match(CONTEXT_REF_REGEX);
|
|
400
|
+
if (!match) return raw;
|
|
401
|
+
const [, contextName, refKey] = match;
|
|
402
|
+
const ctx = getContext(contextName);
|
|
403
|
+
const resolved = ctx[refKey];
|
|
404
|
+
if (resolved === void 0) {
|
|
405
|
+
const hasContext = Object.keys(ctx).length > 0;
|
|
406
|
+
const msg = hasContext ? `Context reference @${contextName}:${refKey} could not be resolved: key "${refKey}" is not defined in context "${contextName}".` : `Context reference @${contextName}:${refKey} could not be resolved: context "${contextName}" not found (no ${contextName}.context.json).`;
|
|
407
|
+
log("error", msg);
|
|
408
|
+
throw new Error(msg);
|
|
409
|
+
}
|
|
410
|
+
return resolveContextReference(resolved, depth + 1);
|
|
411
|
+
}
|
|
315
412
|
function defaultKeyFromName(name) {
|
|
316
413
|
return name.toLowerCase().replace(/\s+/g, "_").replace(/[^a-z0-9_]/gi, "");
|
|
317
414
|
}
|
|
@@ -336,22 +433,25 @@ function scenv(name, options = {}) {
|
|
|
336
433
|
log("trace", `resolveRaw: checking set for key=${key}`);
|
|
337
434
|
if (config.set?.[key] !== void 0) {
|
|
338
435
|
log("trace", `resolveRaw: set hit key=${key}`);
|
|
339
|
-
|
|
436
|
+
const raw = resolveContextReference(config.set[key]);
|
|
437
|
+
return { raw, source: "set" };
|
|
340
438
|
}
|
|
341
439
|
if (!config.ignoreEnv) {
|
|
342
440
|
log("trace", `resolveRaw: checking env ${envKey}`);
|
|
343
441
|
const envVal = process.env[envKey];
|
|
344
442
|
if (envVal !== void 0 && envVal !== "") {
|
|
345
443
|
log("trace", "resolveRaw: env hit");
|
|
346
|
-
|
|
444
|
+
const raw = resolveContextReference(envVal);
|
|
445
|
+
return { raw, source: "env" };
|
|
347
446
|
}
|
|
348
447
|
}
|
|
349
448
|
if (!config.ignoreContext) {
|
|
350
449
|
log("trace", "resolveRaw: checking context");
|
|
351
|
-
const ctx =
|
|
450
|
+
const ctx = getMergedContextValues();
|
|
352
451
|
if (ctx[key] !== void 0) {
|
|
353
452
|
log("trace", `resolveRaw: context hit key=${key}`);
|
|
354
|
-
|
|
453
|
+
const raw = resolveContextReference(ctx[key]);
|
|
454
|
+
return { raw, source: "context" };
|
|
355
455
|
}
|
|
356
456
|
}
|
|
357
457
|
log("trace", "resolveRaw: no value");
|
|
@@ -377,6 +477,7 @@ function scenv(name, options = {}) {
|
|
|
377
477
|
`prompt decision key=${key} prompt=${config.prompt ?? "fallback"} hadValue=${hadValue} hadEnv=${hadEnv} -> ${doPrompt ? "prompt" : "no prompt"}`
|
|
378
478
|
);
|
|
379
479
|
const effectiveDefault = overrides?.default !== void 0 ? overrides.default : defaultValue;
|
|
480
|
+
const resolvedDefault = effectiveDefault === void 0 ? void 0 : typeof effectiveDefault === "string" ? resolveContextReference(effectiveDefault) : effectiveDefault;
|
|
380
481
|
let wasPrompted = false;
|
|
381
482
|
let value;
|
|
382
483
|
let resolvedFrom;
|
|
@@ -388,15 +489,16 @@ function scenv(name, options = {}) {
|
|
|
388
489
|
`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: ... } }).`
|
|
389
490
|
);
|
|
390
491
|
}
|
|
391
|
-
const defaultForPrompt = raw !== void 0 ? raw :
|
|
392
|
-
|
|
492
|
+
const defaultForPrompt = raw !== void 0 ? raw : resolvedDefault;
|
|
493
|
+
let promptedValue = await Promise.resolve(fn(name, defaultForPrompt));
|
|
494
|
+
value = typeof promptedValue === "string" ? resolveContextReference(promptedValue) : promptedValue;
|
|
393
495
|
wasPrompted = true;
|
|
394
496
|
resolvedFrom = "prompt";
|
|
395
497
|
} else if (raw !== void 0) {
|
|
396
498
|
value = raw;
|
|
397
499
|
resolvedFrom = source;
|
|
398
|
-
} else if (
|
|
399
|
-
value =
|
|
500
|
+
} else if (resolvedDefault !== void 0) {
|
|
501
|
+
value = resolvedDefault;
|
|
400
502
|
resolvedFrom = "default";
|
|
401
503
|
} else {
|
|
402
504
|
throw new Error(`Missing value for variable "${name}" (key: ${key})`);
|
|
@@ -428,25 +530,36 @@ function scenv(name, options = {}) {
|
|
|
428
530
|
const final = validated.data;
|
|
429
531
|
if (wasPrompted) {
|
|
430
532
|
const config = loadConfig();
|
|
431
|
-
const
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
533
|
+
const mode = config.shouldSavePrompt ?? (config.prompt === "never" ? "never" : "ask");
|
|
534
|
+
if (mode === "never") return final;
|
|
535
|
+
let doSave;
|
|
536
|
+
if (mode === "ask") {
|
|
537
|
+
const callbacks2 = getCallbacks();
|
|
538
|
+
if (typeof callbacks2.onAskWhetherToSave !== "function") {
|
|
436
539
|
throw new Error(
|
|
437
|
-
`
|
|
540
|
+
`shouldSavePrompt is "ask" but onAskWhetherToSave callback is not set. Configure callbacks via configure({ callbacks: { onAskWhetherToSave: ... } }).`
|
|
438
541
|
);
|
|
439
542
|
}
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
543
|
+
doSave = await callbacks2.onAskWhetherToSave(name, final);
|
|
544
|
+
} else {
|
|
545
|
+
doSave = true;
|
|
546
|
+
}
|
|
547
|
+
if (!doSave) return final;
|
|
548
|
+
const callbacks = getCallbacks();
|
|
549
|
+
const contextNames = config.contexts ?? [];
|
|
550
|
+
let ctxToSave;
|
|
551
|
+
if (config.saveContextTo === "ask") {
|
|
552
|
+
if (typeof callbacks.onAskContext !== "function") {
|
|
553
|
+
throw new Error(
|
|
554
|
+
`saveContextTo is "ask" but onAskContext callback is not set. Configure callbacks via configure({ callbacks: { onAskContext: ... } }).`
|
|
555
|
+
);
|
|
448
556
|
}
|
|
557
|
+
ctxToSave = await callbacks.onAskContext(name, contextNames);
|
|
558
|
+
} else {
|
|
559
|
+
ctxToSave = config.saveContextTo ?? contextNames[0] ?? "default";
|
|
449
560
|
}
|
|
561
|
+
writeToContext(ctxToSave, key, String(final));
|
|
562
|
+
log("info", `Saved key=${key} to context ${ctxToSave}`);
|
|
450
563
|
}
|
|
451
564
|
return final;
|
|
452
565
|
}
|
|
@@ -516,7 +629,7 @@ function parseScenvArgs(argv) {
|
|
|
516
629
|
} else if (arg === "--save-prompt" && argv[i + 1] !== void 0) {
|
|
517
630
|
const v = argv[++i].toLowerCase();
|
|
518
631
|
if (["always", "never", "ask"].includes(v)) {
|
|
519
|
-
config.
|
|
632
|
+
config.shouldSavePrompt = v;
|
|
520
633
|
}
|
|
521
634
|
} else if (arg === "--save-context-to" && argv[i + 1] !== void 0) {
|
|
522
635
|
config.saveContextTo = argv[++i];
|
|
@@ -548,7 +661,8 @@ function parseScenvArgs(argv) {
|
|
|
548
661
|
configure,
|
|
549
662
|
discoverContextPaths,
|
|
550
663
|
getCallbacks,
|
|
551
|
-
|
|
664
|
+
getContext,
|
|
665
|
+
getMergedContextValues,
|
|
552
666
|
loadConfig,
|
|
553
667
|
parseScenvArgs,
|
|
554
668
|
resetConfig,
|
package/dist/index.d.cts
CHANGED
|
@@ -1,67 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* When to prompt the user for a variable value during resolution.
|
|
3
|
+
* - `"always"` – Always call the variable's prompt (or defaultPrompt) when resolving.
|
|
4
|
+
* - `"never"` – Never prompt; use set/env/context/default only.
|
|
5
|
+
* - `"fallback"` – Prompt only when no value was found from set, env, or context.
|
|
6
|
+
* - `"no-env"` – Prompt when the env var is not set (even if context has a value).
|
|
7
|
+
*/
|
|
1
8
|
type PromptMode = "always" | "never" | "fallback" | "no-env";
|
|
9
|
+
/**
|
|
10
|
+
* What to do with the value after the user was just prompted for it.
|
|
11
|
+
* - `"never"` – Do not save; discard for next time.
|
|
12
|
+
* - `"always"` – Save it (no prompt). Use saveContextTo or onAskContext only to pick where.
|
|
13
|
+
* - `"ask"` – Call {@link ScenvCallbacks.onAskWhetherToSave}; if true save, if false don't save.
|
|
14
|
+
*/
|
|
2
15
|
type SavePromptMode = "always" | "never" | "ask";
|
|
16
|
+
/**
|
|
17
|
+
* Valid log levels. Use with {@link ScenvConfig.logLevel}.
|
|
18
|
+
* Messages at or above the configured level are written to stderr.
|
|
19
|
+
*/
|
|
3
20
|
declare const LOG_LEVELS: readonly ["none", "trace", "debug", "info", "warn", "error"];
|
|
21
|
+
/** Log level type. `"none"` disables logging; higher values are more verbose. */
|
|
4
22
|
type LogLevel = (typeof LOG_LEVELS)[number];
|
|
23
|
+
/**
|
|
24
|
+
* Full scenv configuration. Built from file (scenv.config.json), environment (SCENV_*),
|
|
25
|
+
* and programmatic config (configure()), with programmatic > env > file precedence.
|
|
26
|
+
* All properties are optional; defaults apply when omitted.
|
|
27
|
+
*/
|
|
5
28
|
interface ScenvConfig {
|
|
6
|
-
/** Replace
|
|
29
|
+
/** Replace the context list entirely (CLI: `--context a,b,c`). Loaded in order; later overwrites earlier for same key. */
|
|
7
30
|
contexts?: string[];
|
|
8
|
-
/** Merge these
|
|
31
|
+
/** Merge these context names with existing (CLI: `--add-context a,b,c`). Ignored if `contexts` is set in the same layer. */
|
|
9
32
|
addContexts?: string[];
|
|
10
|
-
/** When to prompt for variable value */
|
|
33
|
+
/** When to prompt for a variable value. See {@link PromptMode}. Default is `"fallback"`. */
|
|
11
34
|
prompt?: PromptMode;
|
|
12
|
-
/**
|
|
35
|
+
/** If true, environment variables are not used during resolution. */
|
|
13
36
|
ignoreEnv?: boolean;
|
|
14
|
-
/**
|
|
37
|
+
/** If true, context files are not loaded during resolution. */
|
|
15
38
|
ignoreContext?: boolean;
|
|
16
|
-
/** Override values
|
|
39
|
+
/** Override values by key (CLI: `--set key=value`). Takes precedence over env and context. */
|
|
17
40
|
set?: Record<string, string>;
|
|
18
|
-
/**
|
|
19
|
-
|
|
20
|
-
/**
|
|
41
|
+
/** After a prompt: "never" = don't save, "always" = save without asking, "ask" = call onAskWhetherToSave (then save if true). */
|
|
42
|
+
shouldSavePrompt?: SavePromptMode;
|
|
43
|
+
/** Target context for saving: a context name, or `"ask"` to use {@link ScenvCallbacks.onAskContext}. */
|
|
21
44
|
saveContextTo?: "ask" | string;
|
|
22
|
-
/** Root directory for config
|
|
45
|
+
/** Root directory for config file search and context discovery. Default is cwd or the directory containing scenv.config.json. */
|
|
23
46
|
root?: string;
|
|
24
|
-
/**
|
|
47
|
+
/** Logging level. Default is `"none"`. Messages go to stderr. */
|
|
25
48
|
logLevel?: LogLevel;
|
|
26
49
|
}
|
|
27
|
-
/**
|
|
50
|
+
/**
|
|
51
|
+
* Default prompt function signature. Called when a variable has no `prompt` option and
|
|
52
|
+
* config requests prompting. Receives the variable's display name and default value;
|
|
53
|
+
* returns the value to use (sync or async). Overridable per variable via the variable's
|
|
54
|
+
* `prompt` option or per call via get({ prompt: fn }).
|
|
55
|
+
*/
|
|
28
56
|
type DefaultPromptFn = (name: string, defaultValue: unknown) => unknown | Promise<unknown>;
|
|
57
|
+
/**
|
|
58
|
+
* Callbacks for interactive behaviour. All have built-in readline defaults when not set.
|
|
59
|
+
* Pass to {@link configure} via `configure({ callbacks: { ... } })`.
|
|
60
|
+
*/
|
|
29
61
|
interface ScenvCallbacks {
|
|
30
|
-
/**
|
|
62
|
+
/** Used when a variable does not define its own `prompt`. Variable-level `prompt` overrides this. */
|
|
31
63
|
defaultPrompt?: DefaultPromptFn;
|
|
32
|
-
/**
|
|
33
|
-
|
|
34
|
-
/**
|
|
64
|
+
/** Called only when {@link ScenvConfig.shouldSavePrompt} is "ask" and the user was just prompted. Return true to save, false to skip. (With "always", we save without calling this.) */
|
|
65
|
+
onAskWhetherToSave?: (name: string, value: unknown) => Promise<boolean>;
|
|
66
|
+
/** Called when the save destination is ambiguous: saveContextTo is "ask", or after a prompt when the user said yes and saveContextTo is "ask". Return the context name to write to. */
|
|
35
67
|
onAskContext?: (name: string, contextNames: string[]) => Promise<string>;
|
|
36
68
|
}
|
|
69
|
+
/**
|
|
70
|
+
* Returns the current callbacks (with built-in defaults merged in for any unset callback).
|
|
71
|
+
* Useful for inspection or to pass a subset to another layer. Each call returns a new object.
|
|
72
|
+
*
|
|
73
|
+
* @returns Copy of the effective callbacks: defaultPrompt, onAskWhetherToSave, onAskContext (always defined).
|
|
74
|
+
*/
|
|
37
75
|
declare function getCallbacks(): ScenvCallbacks;
|
|
38
76
|
/**
|
|
39
|
-
*
|
|
77
|
+
* Loads the full merged configuration. Searches for scenv.config.json upward from the given
|
|
78
|
+
* root (or cwd / programmatic root), then overlays SCENV_* env vars, then programmatic config.
|
|
79
|
+
* Use this to read the effective config (e.g. for logging or conditional logic).
|
|
80
|
+
*
|
|
81
|
+
* @param root - Optional directory to start searching for scenv.config.json. If omitted, uses programmatic config.root or process.cwd().
|
|
82
|
+
* @returns The merged {@link ScenvConfig} with at least `root` and `contexts` defined.
|
|
40
83
|
*/
|
|
41
84
|
declare function loadConfig(root?: string): ScenvConfig;
|
|
42
85
|
/**
|
|
43
|
-
*
|
|
86
|
+
* Merges config and/or callbacks into the programmatic layer. Programmatic config has
|
|
87
|
+
* highest precedence in {@link loadConfig}. Call multiple times to merge; later values
|
|
88
|
+
* overwrite earlier for the same key. Typical use: pass the result of {@link parseScenvArgs}
|
|
89
|
+
* or your own partial config.
|
|
90
|
+
*
|
|
91
|
+
* @param partial - Partial config and/or `callbacks`. Omitted keys are left unchanged. Nested objects (e.g. callbacks, set) are merged shallowly with existing.
|
|
44
92
|
*/
|
|
45
93
|
declare function configure(partial: Partial<ScenvConfig> & {
|
|
46
94
|
callbacks?: ScenvCallbacks;
|
|
47
95
|
}): void;
|
|
48
96
|
/**
|
|
49
|
-
*
|
|
97
|
+
* Clears all programmatic config and callbacks. File and env config are unaffected.
|
|
98
|
+
* Mainly for tests. After calling, the next loadConfig() will not include any programmatic overrides.
|
|
50
99
|
*/
|
|
51
100
|
declare function resetConfig(): void;
|
|
52
101
|
|
|
53
|
-
/**
|
|
102
|
+
/**
|
|
103
|
+
* Resets internal log state (e.g. the "config loaded" one-time guard). Call after
|
|
104
|
+
* {@link resetConfig} in tests if you need to see config-loaded messages again or
|
|
105
|
+
* assert on log output.
|
|
106
|
+
*/
|
|
54
107
|
declare function resetLogState(): void;
|
|
55
108
|
|
|
56
109
|
/**
|
|
57
|
-
* Recursively
|
|
110
|
+
* Recursively discovers all `*.context.json` files under a directory. Context name is the
|
|
111
|
+
* filename without the suffix (e.g. `dev.context.json` → "dev"). First file found for a
|
|
112
|
+
* given name wins. Used internally for loading and saving; you can call it to inspect
|
|
113
|
+
* available contexts.
|
|
114
|
+
*
|
|
115
|
+
* @param dir - Root directory to search (e.g. config.root or process.cwd()).
|
|
116
|
+
* @param found - Optional existing map to merge results into. If omitted, a new Map is used.
|
|
117
|
+
* @returns Map from context name to absolute file path.
|
|
58
118
|
*/
|
|
59
119
|
declare function discoverContextPaths(dir: string, found?: Map<string, string>): Map<string, string>;
|
|
60
120
|
/**
|
|
61
|
-
*
|
|
121
|
+
* Loads key-value pairs from a single context file. Used when resolving @context:key references.
|
|
122
|
+
* Does not depend on config.contexts or ignoreContext; the context file is read if it exists
|
|
123
|
+
* under the config root.
|
|
124
|
+
*
|
|
125
|
+
* @param contextName - Name of the context (e.g. "prod", "dev") — file is contextName.context.json.
|
|
126
|
+
* @param root - Optional root directory to search. If omitted, uses loadConfig().root.
|
|
127
|
+
* @returns A flat record of key → string value from that context file. Empty if file not found or invalid.
|
|
128
|
+
*/
|
|
129
|
+
declare function getContext(contextName: string, root?: string): Record<string, string>;
|
|
130
|
+
/**
|
|
131
|
+
* Loads and merges context values from the current config. Respects {@link ScenvConfig.contexts}
|
|
132
|
+
* order and {@link ScenvConfig.ignoreContext}. Each context file is a JSON object of string
|
|
133
|
+
* key-value pairs; later contexts overwrite earlier for the same key. Used during variable
|
|
134
|
+
* resolution (set > env > context > default).
|
|
135
|
+
*
|
|
136
|
+
* @returns A flat record of key → string value. Empty if ignoreContext is true or no contexts loaded.
|
|
62
137
|
*/
|
|
63
|
-
declare function
|
|
138
|
+
declare function getMergedContextValues(): Record<string, string>;
|
|
64
139
|
|
|
140
|
+
/**
|
|
141
|
+
* Return type for a variable's optional `validator` function. Use a boolean for simple
|
|
142
|
+
* pass/fail, or an object to pass a transformed value or a custom error.
|
|
143
|
+
* - `true` or `{ success: true, data?: T }` – validation passed; optional `data` replaces the value.
|
|
144
|
+
* - `false` or `{ success: false, error?: unknown }` – validation failed; `.get()` throws with the error.
|
|
145
|
+
*/
|
|
65
146
|
type ValidatorResult<T> = boolean | {
|
|
66
147
|
success: true;
|
|
67
148
|
data?: T;
|
|
@@ -69,23 +150,51 @@ type ValidatorResult<T> = boolean | {
|
|
|
69
150
|
success: false;
|
|
70
151
|
error?: unknown;
|
|
71
152
|
};
|
|
153
|
+
/**
|
|
154
|
+
* Prompt function signature. Called when config requests prompting for this variable.
|
|
155
|
+
* Receives the variable's display name and the current default (from set/env/context or option default).
|
|
156
|
+
* Return the value to use (sync or async). Used as the variable's `prompt` option or in get({ prompt: fn }).
|
|
157
|
+
*/
|
|
72
158
|
type PromptFn<T> = (name: string, defaultValue: T) => T | Promise<T>;
|
|
159
|
+
/**
|
|
160
|
+
* Options when creating a variable with {@link scenv}. All properties are optional.
|
|
161
|
+
* If you omit `key` and `env`, they are derived from `name`: e.g. "API URL" → key `api_url`, env `API_URL`.
|
|
162
|
+
*/
|
|
73
163
|
interface ScenvVariableOptions<T> {
|
|
164
|
+
/** Internal key for --set, context files, and env. Default: name lowercased, spaces → underscores, non-alphanumeric stripped (e.g. "API URL" → "api_url"). */
|
|
74
165
|
key?: string;
|
|
166
|
+
/** Environment variable name (e.g. API_URL). Default: key uppercased, hyphens → underscores. */
|
|
75
167
|
env?: string;
|
|
168
|
+
/** Fallback when nothing is provided via --set, env, or context (and we're not prompting). */
|
|
76
169
|
default?: T;
|
|
170
|
+
/** Optional. Run after value is resolved or prompted. Return true / { success: true } to accept, false / { success: false, error } to reject (get() throws). Use to coerce types or enforce rules. */
|
|
77
171
|
validator?: (val: T) => ValidatorResult<T>;
|
|
172
|
+
/** Optional. Called when config says to prompt (e.g. prompt: "fallback" and no value found). Overrides callbacks.defaultPrompt for this variable. */
|
|
78
173
|
prompt?: PromptFn<T>;
|
|
79
174
|
}
|
|
80
|
-
/**
|
|
175
|
+
/**
|
|
176
|
+
* Overrides for a single get() or safeGet() call. Only that call is affected.
|
|
177
|
+
*/
|
|
81
178
|
interface GetOptions<T> {
|
|
82
|
-
/** Use this prompt for this call
|
|
179
|
+
/** Use this prompt for this call only (e.g. a one-off inquirer prompt). */
|
|
83
180
|
prompt?: PromptFn<T>;
|
|
84
|
-
/** Use this as the default for this call
|
|
181
|
+
/** Use this as the default for this call when no value from set/env/context. */
|
|
85
182
|
default?: T;
|
|
86
183
|
}
|
|
184
|
+
/**
|
|
185
|
+
* A scenv variable: a named setting (e.g. "API URL") whose value is resolved from CLI overrides (--set),
|
|
186
|
+
* then environment variables, then context files, then default (or an optional prompt). You call get() or
|
|
187
|
+
* safeGet() to read the value; optionally save() to persist it to a context file for next time.
|
|
188
|
+
*/
|
|
87
189
|
interface ScenvVariable<T> {
|
|
190
|
+
/**
|
|
191
|
+
* Resolve and return the value. Uses resolution order (set > env > context > default) and any prompt/validator.
|
|
192
|
+
* @throws If no value is found and no default/prompt, or if validation fails.
|
|
193
|
+
*/
|
|
88
194
|
get(options?: GetOptions<T>): Promise<T>;
|
|
195
|
+
/**
|
|
196
|
+
* Like get(), but never throws. Returns { success: true, value } or { success: false, error }.
|
|
197
|
+
*/
|
|
89
198
|
safeGet(options?: GetOptions<T>): Promise<{
|
|
90
199
|
success: true;
|
|
91
200
|
value: T;
|
|
@@ -93,15 +202,71 @@ interface ScenvVariable<T> {
|
|
|
93
202
|
success: false;
|
|
94
203
|
error?: unknown;
|
|
95
204
|
}>;
|
|
205
|
+
/**
|
|
206
|
+
* Write the value to a context file (e.g. for next run). Target context comes from config.saveContextTo or onAskContext.
|
|
207
|
+
* If you don't pass a value, the last resolved value is used. Does not prompt "save?"; it saves.
|
|
208
|
+
*/
|
|
96
209
|
save(value?: T): Promise<void>;
|
|
97
210
|
}
|
|
211
|
+
/**
|
|
212
|
+
* Creates a scenv variable: a named config value (e.g. API URL, port) that you read with get() or safeGet()
|
|
213
|
+
* and optionally write with save(). You pass a display name and optional options; key and env are derived from
|
|
214
|
+
* the name if you omit them.
|
|
215
|
+
*
|
|
216
|
+
* ## Resolution (how get() gets a value)
|
|
217
|
+
*
|
|
218
|
+
* get() first looks for a raw value in this order, stopping at the first found:
|
|
219
|
+
* - Set overrides from config (e.g. from --set key=value or configure({ set: { key: "value" } }))
|
|
220
|
+
* - Environment variable (e.g. API_URL for key "api_url")
|
|
221
|
+
* - Context files (merged key-value from the contexts in config, e.g. dev.context.json)
|
|
222
|
+
*
|
|
223
|
+
* If config says to prompt (see prompt mode in config: "always", "fallback", "no-env"), the prompt callback
|
|
224
|
+
* may run. When it runs, it receives the variable name and a suggested value (the raw value if any, otherwise
|
|
225
|
+
* the default option). The callback's return value is used as the value. When we don't prompt, we use the
|
|
226
|
+
* raw value if present, otherwise the default option, otherwise get() throws (no value).
|
|
227
|
+
*
|
|
228
|
+
* ## Validator
|
|
229
|
+
*
|
|
230
|
+
* If you pass a validator option, it is called with the resolved or prompted value. It can return true or
|
|
231
|
+
* { success: true } to accept, or false or { success: false, error } to reject; on reject, get() throws.
|
|
232
|
+
* Use it to coerce types (e.g. string to number) or enforce rules.
|
|
233
|
+
*
|
|
234
|
+
* ## save()
|
|
235
|
+
*
|
|
236
|
+
* The variable has a save(value?) method. It writes the value (or the last resolved value if you omit it)
|
|
237
|
+
* to a context file. The target context comes from config.saveContextTo or from the onAskContext callback
|
|
238
|
+
* when saveContextTo is "ask". save() does not ask "save?"; it saves. Optional "save after prompt" behavior
|
|
239
|
+
* is controlled by config.shouldSavePrompt and callbacks.onAskWhetherToSave.
|
|
240
|
+
*
|
|
241
|
+
* @typeParam T - Value type (default string). Use a validator to coerce to number, boolean, etc.
|
|
242
|
+
* @param name - Display name used in prompts and errors. If you omit key/env, key is derived from name (e.g. "API URL" → "api_url") and env from key (e.g. "API_URL").
|
|
243
|
+
* @param options - Optional. key, env, default, validator, prompt. See {@link ScenvVariableOptions}.
|
|
244
|
+
* @returns A {@link ScenvVariable} with get(), safeGet(), and save().
|
|
245
|
+
*
|
|
246
|
+
* @example
|
|
247
|
+
* const apiUrl = scenv("API URL", { default: "http://localhost:4000" });
|
|
248
|
+
* const url = await apiUrl.get();
|
|
249
|
+
*/
|
|
98
250
|
declare function scenv<T>(name: string, options?: ScenvVariableOptions<T>): ScenvVariable<T>;
|
|
99
251
|
|
|
100
252
|
/**
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
253
|
+
* Parses command-line arguments into a partial {@link ScenvConfig} suitable for {@link configure}.
|
|
254
|
+
* Typical use: `configure(parseScenvArgs(process.argv.slice(2)))`. Unrecognized flags are ignored.
|
|
255
|
+
*
|
|
256
|
+
* Supported flags:
|
|
257
|
+
* - `--context a,b,c` – Set contexts (replace).
|
|
258
|
+
* - `--add-context x,y` – Add contexts.
|
|
259
|
+
* - `--prompt always|never|fallback|no-env` – Prompt mode.
|
|
260
|
+
* - `--ignore-env` – Set ignoreEnv to true.
|
|
261
|
+
* - `--ignore-context` – Set ignoreContext to true.
|
|
262
|
+
* - `--set key=value` or `--set=key=value` – Add to set overrides (multiple allowed).
|
|
263
|
+
* - `--save-prompt always|never|ask` – shouldSavePrompt.
|
|
264
|
+
* - `--save-context-to name` – saveContextTo.
|
|
265
|
+
* - `--log-level level`, `--log level`, `--log=level` – logLevel.
|
|
266
|
+
*
|
|
267
|
+
* @param argv - Array of CLI arguments (e.g. process.argv.slice(2)).
|
|
268
|
+
* @returns Partial ScenvConfig with only the keys that were present in argv.
|
|
104
269
|
*/
|
|
105
270
|
declare function parseScenvArgs(argv: string[]): Partial<ScenvConfig>;
|
|
106
271
|
|
|
107
|
-
export { type DefaultPromptFn, type GetOptions, LOG_LEVELS, type LogLevel, type PromptMode, type SavePromptMode, type ScenvCallbacks, type ScenvConfig, type ScenvVariable, configure, discoverContextPaths, getCallbacks,
|
|
272
|
+
export { type DefaultPromptFn, type GetOptions, LOG_LEVELS, type LogLevel, type PromptMode, type SavePromptMode, type ScenvCallbacks, type ScenvConfig, type ScenvVariable, configure, discoverContextPaths, getCallbacks, getContext, getMergedContextValues, loadConfig, parseScenvArgs, resetConfig, resetLogState, scenv };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,67 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* When to prompt the user for a variable value during resolution.
|
|
3
|
+
* - `"always"` – Always call the variable's prompt (or defaultPrompt) when resolving.
|
|
4
|
+
* - `"never"` – Never prompt; use set/env/context/default only.
|
|
5
|
+
* - `"fallback"` – Prompt only when no value was found from set, env, or context.
|
|
6
|
+
* - `"no-env"` – Prompt when the env var is not set (even if context has a value).
|
|
7
|
+
*/
|
|
1
8
|
type PromptMode = "always" | "never" | "fallback" | "no-env";
|
|
9
|
+
/**
|
|
10
|
+
* What to do with the value after the user was just prompted for it.
|
|
11
|
+
* - `"never"` – Do not save; discard for next time.
|
|
12
|
+
* - `"always"` – Save it (no prompt). Use saveContextTo or onAskContext only to pick where.
|
|
13
|
+
* - `"ask"` – Call {@link ScenvCallbacks.onAskWhetherToSave}; if true save, if false don't save.
|
|
14
|
+
*/
|
|
2
15
|
type SavePromptMode = "always" | "never" | "ask";
|
|
16
|
+
/**
|
|
17
|
+
* Valid log levels. Use with {@link ScenvConfig.logLevel}.
|
|
18
|
+
* Messages at or above the configured level are written to stderr.
|
|
19
|
+
*/
|
|
3
20
|
declare const LOG_LEVELS: readonly ["none", "trace", "debug", "info", "warn", "error"];
|
|
21
|
+
/** Log level type. `"none"` disables logging; higher values are more verbose. */
|
|
4
22
|
type LogLevel = (typeof LOG_LEVELS)[number];
|
|
23
|
+
/**
|
|
24
|
+
* Full scenv configuration. Built from file (scenv.config.json), environment (SCENV_*),
|
|
25
|
+
* and programmatic config (configure()), with programmatic > env > file precedence.
|
|
26
|
+
* All properties are optional; defaults apply when omitted.
|
|
27
|
+
*/
|
|
5
28
|
interface ScenvConfig {
|
|
6
|
-
/** Replace
|
|
29
|
+
/** Replace the context list entirely (CLI: `--context a,b,c`). Loaded in order; later overwrites earlier for same key. */
|
|
7
30
|
contexts?: string[];
|
|
8
|
-
/** Merge these
|
|
31
|
+
/** Merge these context names with existing (CLI: `--add-context a,b,c`). Ignored if `contexts` is set in the same layer. */
|
|
9
32
|
addContexts?: string[];
|
|
10
|
-
/** When to prompt for variable value */
|
|
33
|
+
/** When to prompt for a variable value. See {@link PromptMode}. Default is `"fallback"`. */
|
|
11
34
|
prompt?: PromptMode;
|
|
12
|
-
/**
|
|
35
|
+
/** If true, environment variables are not used during resolution. */
|
|
13
36
|
ignoreEnv?: boolean;
|
|
14
|
-
/**
|
|
37
|
+
/** If true, context files are not loaded during resolution. */
|
|
15
38
|
ignoreContext?: boolean;
|
|
16
|
-
/** Override values
|
|
39
|
+
/** Override values by key (CLI: `--set key=value`). Takes precedence over env and context. */
|
|
17
40
|
set?: Record<string, string>;
|
|
18
|
-
/**
|
|
19
|
-
|
|
20
|
-
/**
|
|
41
|
+
/** After a prompt: "never" = don't save, "always" = save without asking, "ask" = call onAskWhetherToSave (then save if true). */
|
|
42
|
+
shouldSavePrompt?: SavePromptMode;
|
|
43
|
+
/** Target context for saving: a context name, or `"ask"` to use {@link ScenvCallbacks.onAskContext}. */
|
|
21
44
|
saveContextTo?: "ask" | string;
|
|
22
|
-
/** Root directory for config
|
|
45
|
+
/** Root directory for config file search and context discovery. Default is cwd or the directory containing scenv.config.json. */
|
|
23
46
|
root?: string;
|
|
24
|
-
/**
|
|
47
|
+
/** Logging level. Default is `"none"`. Messages go to stderr. */
|
|
25
48
|
logLevel?: LogLevel;
|
|
26
49
|
}
|
|
27
|
-
/**
|
|
50
|
+
/**
|
|
51
|
+
* Default prompt function signature. Called when a variable has no `prompt` option and
|
|
52
|
+
* config requests prompting. Receives the variable's display name and default value;
|
|
53
|
+
* returns the value to use (sync or async). Overridable per variable via the variable's
|
|
54
|
+
* `prompt` option or per call via get({ prompt: fn }).
|
|
55
|
+
*/
|
|
28
56
|
type DefaultPromptFn = (name: string, defaultValue: unknown) => unknown | Promise<unknown>;
|
|
57
|
+
/**
|
|
58
|
+
* Callbacks for interactive behaviour. All have built-in readline defaults when not set.
|
|
59
|
+
* Pass to {@link configure} via `configure({ callbacks: { ... } })`.
|
|
60
|
+
*/
|
|
29
61
|
interface ScenvCallbacks {
|
|
30
|
-
/**
|
|
62
|
+
/** Used when a variable does not define its own `prompt`. Variable-level `prompt` overrides this. */
|
|
31
63
|
defaultPrompt?: DefaultPromptFn;
|
|
32
|
-
/**
|
|
33
|
-
|
|
34
|
-
/**
|
|
64
|
+
/** Called only when {@link ScenvConfig.shouldSavePrompt} is "ask" and the user was just prompted. Return true to save, false to skip. (With "always", we save without calling this.) */
|
|
65
|
+
onAskWhetherToSave?: (name: string, value: unknown) => Promise<boolean>;
|
|
66
|
+
/** Called when the save destination is ambiguous: saveContextTo is "ask", or after a prompt when the user said yes and saveContextTo is "ask". Return the context name to write to. */
|
|
35
67
|
onAskContext?: (name: string, contextNames: string[]) => Promise<string>;
|
|
36
68
|
}
|
|
69
|
+
/**
|
|
70
|
+
* Returns the current callbacks (with built-in defaults merged in for any unset callback).
|
|
71
|
+
* Useful for inspection or to pass a subset to another layer. Each call returns a new object.
|
|
72
|
+
*
|
|
73
|
+
* @returns Copy of the effective callbacks: defaultPrompt, onAskWhetherToSave, onAskContext (always defined).
|
|
74
|
+
*/
|
|
37
75
|
declare function getCallbacks(): ScenvCallbacks;
|
|
38
76
|
/**
|
|
39
|
-
*
|
|
77
|
+
* Loads the full merged configuration. Searches for scenv.config.json upward from the given
|
|
78
|
+
* root (or cwd / programmatic root), then overlays SCENV_* env vars, then programmatic config.
|
|
79
|
+
* Use this to read the effective config (e.g. for logging or conditional logic).
|
|
80
|
+
*
|
|
81
|
+
* @param root - Optional directory to start searching for scenv.config.json. If omitted, uses programmatic config.root or process.cwd().
|
|
82
|
+
* @returns The merged {@link ScenvConfig} with at least `root` and `contexts` defined.
|
|
40
83
|
*/
|
|
41
84
|
declare function loadConfig(root?: string): ScenvConfig;
|
|
42
85
|
/**
|
|
43
|
-
*
|
|
86
|
+
* Merges config and/or callbacks into the programmatic layer. Programmatic config has
|
|
87
|
+
* highest precedence in {@link loadConfig}. Call multiple times to merge; later values
|
|
88
|
+
* overwrite earlier for the same key. Typical use: pass the result of {@link parseScenvArgs}
|
|
89
|
+
* or your own partial config.
|
|
90
|
+
*
|
|
91
|
+
* @param partial - Partial config and/or `callbacks`. Omitted keys are left unchanged. Nested objects (e.g. callbacks, set) are merged shallowly with existing.
|
|
44
92
|
*/
|
|
45
93
|
declare function configure(partial: Partial<ScenvConfig> & {
|
|
46
94
|
callbacks?: ScenvCallbacks;
|
|
47
95
|
}): void;
|
|
48
96
|
/**
|
|
49
|
-
*
|
|
97
|
+
* Clears all programmatic config and callbacks. File and env config are unaffected.
|
|
98
|
+
* Mainly for tests. After calling, the next loadConfig() will not include any programmatic overrides.
|
|
50
99
|
*/
|
|
51
100
|
declare function resetConfig(): void;
|
|
52
101
|
|
|
53
|
-
/**
|
|
102
|
+
/**
|
|
103
|
+
* Resets internal log state (e.g. the "config loaded" one-time guard). Call after
|
|
104
|
+
* {@link resetConfig} in tests if you need to see config-loaded messages again or
|
|
105
|
+
* assert on log output.
|
|
106
|
+
*/
|
|
54
107
|
declare function resetLogState(): void;
|
|
55
108
|
|
|
56
109
|
/**
|
|
57
|
-
* Recursively
|
|
110
|
+
* Recursively discovers all `*.context.json` files under a directory. Context name is the
|
|
111
|
+
* filename without the suffix (e.g. `dev.context.json` → "dev"). First file found for a
|
|
112
|
+
* given name wins. Used internally for loading and saving; you can call it to inspect
|
|
113
|
+
* available contexts.
|
|
114
|
+
*
|
|
115
|
+
* @param dir - Root directory to search (e.g. config.root or process.cwd()).
|
|
116
|
+
* @param found - Optional existing map to merge results into. If omitted, a new Map is used.
|
|
117
|
+
* @returns Map from context name to absolute file path.
|
|
58
118
|
*/
|
|
59
119
|
declare function discoverContextPaths(dir: string, found?: Map<string, string>): Map<string, string>;
|
|
60
120
|
/**
|
|
61
|
-
*
|
|
121
|
+
* Loads key-value pairs from a single context file. Used when resolving @context:key references.
|
|
122
|
+
* Does not depend on config.contexts or ignoreContext; the context file is read if it exists
|
|
123
|
+
* under the config root.
|
|
124
|
+
*
|
|
125
|
+
* @param contextName - Name of the context (e.g. "prod", "dev") — file is contextName.context.json.
|
|
126
|
+
* @param root - Optional root directory to search. If omitted, uses loadConfig().root.
|
|
127
|
+
* @returns A flat record of key → string value from that context file. Empty if file not found or invalid.
|
|
128
|
+
*/
|
|
129
|
+
declare function getContext(contextName: string, root?: string): Record<string, string>;
|
|
130
|
+
/**
|
|
131
|
+
* Loads and merges context values from the current config. Respects {@link ScenvConfig.contexts}
|
|
132
|
+
* order and {@link ScenvConfig.ignoreContext}. Each context file is a JSON object of string
|
|
133
|
+
* key-value pairs; later contexts overwrite earlier for the same key. Used during variable
|
|
134
|
+
* resolution (set > env > context > default).
|
|
135
|
+
*
|
|
136
|
+
* @returns A flat record of key → string value. Empty if ignoreContext is true or no contexts loaded.
|
|
62
137
|
*/
|
|
63
|
-
declare function
|
|
138
|
+
declare function getMergedContextValues(): Record<string, string>;
|
|
64
139
|
|
|
140
|
+
/**
|
|
141
|
+
* Return type for a variable's optional `validator` function. Use a boolean for simple
|
|
142
|
+
* pass/fail, or an object to pass a transformed value or a custom error.
|
|
143
|
+
* - `true` or `{ success: true, data?: T }` – validation passed; optional `data` replaces the value.
|
|
144
|
+
* - `false` or `{ success: false, error?: unknown }` – validation failed; `.get()` throws with the error.
|
|
145
|
+
*/
|
|
65
146
|
type ValidatorResult<T> = boolean | {
|
|
66
147
|
success: true;
|
|
67
148
|
data?: T;
|
|
@@ -69,23 +150,51 @@ type ValidatorResult<T> = boolean | {
|
|
|
69
150
|
success: false;
|
|
70
151
|
error?: unknown;
|
|
71
152
|
};
|
|
153
|
+
/**
|
|
154
|
+
* Prompt function signature. Called when config requests prompting for this variable.
|
|
155
|
+
* Receives the variable's display name and the current default (from set/env/context or option default).
|
|
156
|
+
* Return the value to use (sync or async). Used as the variable's `prompt` option or in get({ prompt: fn }).
|
|
157
|
+
*/
|
|
72
158
|
type PromptFn<T> = (name: string, defaultValue: T) => T | Promise<T>;
|
|
159
|
+
/**
|
|
160
|
+
* Options when creating a variable with {@link scenv}. All properties are optional.
|
|
161
|
+
* If you omit `key` and `env`, they are derived from `name`: e.g. "API URL" → key `api_url`, env `API_URL`.
|
|
162
|
+
*/
|
|
73
163
|
interface ScenvVariableOptions<T> {
|
|
164
|
+
/** Internal key for --set, context files, and env. Default: name lowercased, spaces → underscores, non-alphanumeric stripped (e.g. "API URL" → "api_url"). */
|
|
74
165
|
key?: string;
|
|
166
|
+
/** Environment variable name (e.g. API_URL). Default: key uppercased, hyphens → underscores. */
|
|
75
167
|
env?: string;
|
|
168
|
+
/** Fallback when nothing is provided via --set, env, or context (and we're not prompting). */
|
|
76
169
|
default?: T;
|
|
170
|
+
/** Optional. Run after value is resolved or prompted. Return true / { success: true } to accept, false / { success: false, error } to reject (get() throws). Use to coerce types or enforce rules. */
|
|
77
171
|
validator?: (val: T) => ValidatorResult<T>;
|
|
172
|
+
/** Optional. Called when config says to prompt (e.g. prompt: "fallback" and no value found). Overrides callbacks.defaultPrompt for this variable. */
|
|
78
173
|
prompt?: PromptFn<T>;
|
|
79
174
|
}
|
|
80
|
-
/**
|
|
175
|
+
/**
|
|
176
|
+
* Overrides for a single get() or safeGet() call. Only that call is affected.
|
|
177
|
+
*/
|
|
81
178
|
interface GetOptions<T> {
|
|
82
|
-
/** Use this prompt for this call
|
|
179
|
+
/** Use this prompt for this call only (e.g. a one-off inquirer prompt). */
|
|
83
180
|
prompt?: PromptFn<T>;
|
|
84
|
-
/** Use this as the default for this call
|
|
181
|
+
/** Use this as the default for this call when no value from set/env/context. */
|
|
85
182
|
default?: T;
|
|
86
183
|
}
|
|
184
|
+
/**
|
|
185
|
+
* A scenv variable: a named setting (e.g. "API URL") whose value is resolved from CLI overrides (--set),
|
|
186
|
+
* then environment variables, then context files, then default (or an optional prompt). You call get() or
|
|
187
|
+
* safeGet() to read the value; optionally save() to persist it to a context file for next time.
|
|
188
|
+
*/
|
|
87
189
|
interface ScenvVariable<T> {
|
|
190
|
+
/**
|
|
191
|
+
* Resolve and return the value. Uses resolution order (set > env > context > default) and any prompt/validator.
|
|
192
|
+
* @throws If no value is found and no default/prompt, or if validation fails.
|
|
193
|
+
*/
|
|
88
194
|
get(options?: GetOptions<T>): Promise<T>;
|
|
195
|
+
/**
|
|
196
|
+
* Like get(), but never throws. Returns { success: true, value } or { success: false, error }.
|
|
197
|
+
*/
|
|
89
198
|
safeGet(options?: GetOptions<T>): Promise<{
|
|
90
199
|
success: true;
|
|
91
200
|
value: T;
|
|
@@ -93,15 +202,71 @@ interface ScenvVariable<T> {
|
|
|
93
202
|
success: false;
|
|
94
203
|
error?: unknown;
|
|
95
204
|
}>;
|
|
205
|
+
/**
|
|
206
|
+
* Write the value to a context file (e.g. for next run). Target context comes from config.saveContextTo or onAskContext.
|
|
207
|
+
* If you don't pass a value, the last resolved value is used. Does not prompt "save?"; it saves.
|
|
208
|
+
*/
|
|
96
209
|
save(value?: T): Promise<void>;
|
|
97
210
|
}
|
|
211
|
+
/**
|
|
212
|
+
* Creates a scenv variable: a named config value (e.g. API URL, port) that you read with get() or safeGet()
|
|
213
|
+
* and optionally write with save(). You pass a display name and optional options; key and env are derived from
|
|
214
|
+
* the name if you omit them.
|
|
215
|
+
*
|
|
216
|
+
* ## Resolution (how get() gets a value)
|
|
217
|
+
*
|
|
218
|
+
* get() first looks for a raw value in this order, stopping at the first found:
|
|
219
|
+
* - Set overrides from config (e.g. from --set key=value or configure({ set: { key: "value" } }))
|
|
220
|
+
* - Environment variable (e.g. API_URL for key "api_url")
|
|
221
|
+
* - Context files (merged key-value from the contexts in config, e.g. dev.context.json)
|
|
222
|
+
*
|
|
223
|
+
* If config says to prompt (see prompt mode in config: "always", "fallback", "no-env"), the prompt callback
|
|
224
|
+
* may run. When it runs, it receives the variable name and a suggested value (the raw value if any, otherwise
|
|
225
|
+
* the default option). The callback's return value is used as the value. When we don't prompt, we use the
|
|
226
|
+
* raw value if present, otherwise the default option, otherwise get() throws (no value).
|
|
227
|
+
*
|
|
228
|
+
* ## Validator
|
|
229
|
+
*
|
|
230
|
+
* If you pass a validator option, it is called with the resolved or prompted value. It can return true or
|
|
231
|
+
* { success: true } to accept, or false or { success: false, error } to reject; on reject, get() throws.
|
|
232
|
+
* Use it to coerce types (e.g. string to number) or enforce rules.
|
|
233
|
+
*
|
|
234
|
+
* ## save()
|
|
235
|
+
*
|
|
236
|
+
* The variable has a save(value?) method. It writes the value (or the last resolved value if you omit it)
|
|
237
|
+
* to a context file. The target context comes from config.saveContextTo or from the onAskContext callback
|
|
238
|
+
* when saveContextTo is "ask". save() does not ask "save?"; it saves. Optional "save after prompt" behavior
|
|
239
|
+
* is controlled by config.shouldSavePrompt and callbacks.onAskWhetherToSave.
|
|
240
|
+
*
|
|
241
|
+
* @typeParam T - Value type (default string). Use a validator to coerce to number, boolean, etc.
|
|
242
|
+
* @param name - Display name used in prompts and errors. If you omit key/env, key is derived from name (e.g. "API URL" → "api_url") and env from key (e.g. "API_URL").
|
|
243
|
+
* @param options - Optional. key, env, default, validator, prompt. See {@link ScenvVariableOptions}.
|
|
244
|
+
* @returns A {@link ScenvVariable} with get(), safeGet(), and save().
|
|
245
|
+
*
|
|
246
|
+
* @example
|
|
247
|
+
* const apiUrl = scenv("API URL", { default: "http://localhost:4000" });
|
|
248
|
+
* const url = await apiUrl.get();
|
|
249
|
+
*/
|
|
98
250
|
declare function scenv<T>(name: string, options?: ScenvVariableOptions<T>): ScenvVariable<T>;
|
|
99
251
|
|
|
100
252
|
/**
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
253
|
+
* Parses command-line arguments into a partial {@link ScenvConfig} suitable for {@link configure}.
|
|
254
|
+
* Typical use: `configure(parseScenvArgs(process.argv.slice(2)))`. Unrecognized flags are ignored.
|
|
255
|
+
*
|
|
256
|
+
* Supported flags:
|
|
257
|
+
* - `--context a,b,c` – Set contexts (replace).
|
|
258
|
+
* - `--add-context x,y` – Add contexts.
|
|
259
|
+
* - `--prompt always|never|fallback|no-env` – Prompt mode.
|
|
260
|
+
* - `--ignore-env` – Set ignoreEnv to true.
|
|
261
|
+
* - `--ignore-context` – Set ignoreContext to true.
|
|
262
|
+
* - `--set key=value` or `--set=key=value` – Add to set overrides (multiple allowed).
|
|
263
|
+
* - `--save-prompt always|never|ask` – shouldSavePrompt.
|
|
264
|
+
* - `--save-context-to name` – saveContextTo.
|
|
265
|
+
* - `--log-level level`, `--log level`, `--log=level` – logLevel.
|
|
266
|
+
*
|
|
267
|
+
* @param argv - Array of CLI arguments (e.g. process.argv.slice(2)).
|
|
268
|
+
* @returns Partial ScenvConfig with only the keys that were present in argv.
|
|
104
269
|
*/
|
|
105
270
|
declare function parseScenvArgs(argv: string[]): Partial<ScenvConfig>;
|
|
106
271
|
|
|
107
|
-
export { type DefaultPromptFn, type GetOptions, LOG_LEVELS, type LogLevel, type PromptMode, type SavePromptMode, type ScenvCallbacks, type ScenvConfig, type ScenvVariable, configure, discoverContextPaths, getCallbacks,
|
|
272
|
+
export { type DefaultPromptFn, type GetOptions, LOG_LEVELS, type LogLevel, type PromptMode, type SavePromptMode, type ScenvCallbacks, type ScenvConfig, type ScenvVariable, configure, discoverContextPaths, getCallbacks, getContext, getMergedContextValues, loadConfig, parseScenvArgs, resetConfig, resetLogState, scenv };
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,52 @@
|
|
|
1
1
|
// src/config.ts
|
|
2
2
|
import { readFileSync, existsSync } from "fs";
|
|
3
3
|
import { dirname, join } from "path";
|
|
4
|
+
|
|
5
|
+
// src/prompt-default.ts
|
|
6
|
+
import { createInterface } from "readline";
|
|
7
|
+
function ask(message) {
|
|
8
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
9
|
+
return new Promise((resolve, reject) => {
|
|
10
|
+
rl.question(message, (answer) => {
|
|
11
|
+
rl.close();
|
|
12
|
+
resolve(answer.trim());
|
|
13
|
+
});
|
|
14
|
+
rl.on("error", (err) => {
|
|
15
|
+
rl.close();
|
|
16
|
+
reject(err);
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
function defaultPrompt(name, defaultValue) {
|
|
21
|
+
const defaultStr = defaultValue !== void 0 && defaultValue !== null ? String(defaultValue) : "";
|
|
22
|
+
const message = defaultStr ? `Enter ${name} [${defaultStr}]: ` : `Enter ${name}: `;
|
|
23
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
24
|
+
return new Promise((resolve, reject) => {
|
|
25
|
+
rl.question(message, (answer) => {
|
|
26
|
+
rl.close();
|
|
27
|
+
const trimmed = answer.trim();
|
|
28
|
+
const value = trimmed !== "" ? trimmed : defaultStr;
|
|
29
|
+
resolve(value);
|
|
30
|
+
});
|
|
31
|
+
rl.on("error", (err) => {
|
|
32
|
+
rl.close();
|
|
33
|
+
reject(err);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
async function defaultAskWhetherToSave(name, _value) {
|
|
38
|
+
const answer = await ask(`Save "${name}" for next time? (y/n): `);
|
|
39
|
+
const v = answer.toLowerCase();
|
|
40
|
+
return v === "y" || v === "yes" || v === "1" || v === "true";
|
|
41
|
+
}
|
|
42
|
+
async function defaultAskContext(name, contextNames) {
|
|
43
|
+
const hint = contextNames.length > 0 ? ` (${contextNames.join(", ")})` : "";
|
|
44
|
+
const answer = await ask(`Save "${name}" to which context?${hint}: `);
|
|
45
|
+
if (answer) return answer;
|
|
46
|
+
return contextNames[0] ?? "default";
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// src/config.ts
|
|
4
50
|
var LOG_LEVELS = ["none", "trace", "debug", "info", "warn", "error"];
|
|
5
51
|
var CONFIG_FILENAME = "scenv.config.json";
|
|
6
52
|
var envKeyMap = {
|
|
@@ -9,14 +55,18 @@ var envKeyMap = {
|
|
|
9
55
|
SCENV_PROMPT: "prompt",
|
|
10
56
|
SCENV_IGNORE_ENV: "ignoreEnv",
|
|
11
57
|
SCENV_IGNORE_CONTEXT: "ignoreContext",
|
|
12
|
-
SCENV_SAVE_PROMPT: "
|
|
58
|
+
SCENV_SAVE_PROMPT: "shouldSavePrompt",
|
|
13
59
|
SCENV_SAVE_CONTEXT_TO: "saveContextTo",
|
|
14
60
|
SCENV_LOG_LEVEL: "logLevel"
|
|
15
61
|
};
|
|
16
62
|
var programmaticConfig = {};
|
|
17
63
|
var programmaticCallbacks = {};
|
|
18
64
|
function getCallbacks() {
|
|
19
|
-
return {
|
|
65
|
+
return {
|
|
66
|
+
defaultPrompt: programmaticCallbacks.defaultPrompt ?? defaultPrompt,
|
|
67
|
+
onAskWhetherToSave: programmaticCallbacks.onAskWhetherToSave ?? defaultAskWhetherToSave,
|
|
68
|
+
onAskContext: programmaticCallbacks.onAskContext ?? defaultAskContext
|
|
69
|
+
};
|
|
20
70
|
}
|
|
21
71
|
function findConfigDir(startDir) {
|
|
22
72
|
let dir = startDir;
|
|
@@ -38,11 +88,11 @@ function configFromEnv() {
|
|
|
38
88
|
out[configKey] = val.split(",").map((s) => s.trim()).filter(Boolean);
|
|
39
89
|
} else if (configKey === "ignoreEnv" || configKey === "ignoreContext") {
|
|
40
90
|
out[configKey] = val === "1" || val === "true" || val.toLowerCase() === "yes";
|
|
41
|
-
} else if (configKey === "prompt" || configKey === "
|
|
91
|
+
} else if (configKey === "prompt" || configKey === "shouldSavePrompt") {
|
|
42
92
|
const v = val.toLowerCase();
|
|
43
93
|
if (configKey === "prompt" && (v === "always" || v === "never" || v === "fallback" || v === "no-env"))
|
|
44
94
|
out[configKey] = v;
|
|
45
|
-
if (configKey === "
|
|
95
|
+
if (configKey === "shouldSavePrompt" && (v === "always" || v === "never" || v === "ask"))
|
|
46
96
|
out[configKey] = v;
|
|
47
97
|
} else if (configKey === "saveContextTo") {
|
|
48
98
|
out.saveContextTo = val;
|
|
@@ -75,8 +125,8 @@ function loadConfigFile(configDir) {
|
|
|
75
125
|
out.ignoreContext = parsed.ignoreContext;
|
|
76
126
|
if (parsed.set && typeof parsed.set === "object" && !Array.isArray(parsed.set))
|
|
77
127
|
out.set = parsed.set;
|
|
78
|
-
if (typeof parsed.savePrompt === "string" && ["always", "never", "ask"].includes(parsed.savePrompt))
|
|
79
|
-
out.
|
|
128
|
+
if (typeof (parsed.shouldSavePrompt ?? parsed.savePrompt) === "string" && ["always", "never", "ask"].includes(parsed.shouldSavePrompt ?? parsed.savePrompt))
|
|
129
|
+
out.shouldSavePrompt = parsed.shouldSavePrompt ?? parsed.savePrompt;
|
|
80
130
|
if (typeof parsed.saveContextTo === "string")
|
|
81
131
|
out.saveContextTo = parsed.saveContextTo;
|
|
82
132
|
if (typeof parsed.root === "string") out.root = parsed.root;
|
|
@@ -226,7 +276,32 @@ function discoverContextPaths(dir, found = /* @__PURE__ */ new Map()) {
|
|
|
226
276
|
);
|
|
227
277
|
return found;
|
|
228
278
|
}
|
|
229
|
-
function
|
|
279
|
+
function getContext(contextName, root) {
|
|
280
|
+
const config = loadConfig();
|
|
281
|
+
const searchRoot = root ?? config.root ?? process.cwd();
|
|
282
|
+
const paths = discoverContextPaths(searchRoot);
|
|
283
|
+
const filePath = paths.get(contextName);
|
|
284
|
+
if (!filePath) {
|
|
285
|
+
log("trace", `getContext: context "${contextName}" not found`);
|
|
286
|
+
return {};
|
|
287
|
+
}
|
|
288
|
+
try {
|
|
289
|
+
const raw = readFileSync2(filePath, "utf-8");
|
|
290
|
+
const data = JSON.parse(raw);
|
|
291
|
+
const out = {};
|
|
292
|
+
for (const [k, v] of Object.entries(data)) {
|
|
293
|
+
if (typeof v === "string") out[k] = v;
|
|
294
|
+
}
|
|
295
|
+
return out;
|
|
296
|
+
} catch (err) {
|
|
297
|
+
log(
|
|
298
|
+
"trace",
|
|
299
|
+
`getContext: "${contextName}" unreadable: ${err instanceof Error ? err.message : String(err)}`
|
|
300
|
+
);
|
|
301
|
+
return {};
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
function getMergedContextValues() {
|
|
230
305
|
const config = loadConfig();
|
|
231
306
|
logConfigLoaded(config);
|
|
232
307
|
if (config.ignoreContext) return {};
|
|
@@ -283,6 +358,27 @@ function writeToContext(contextName, key, value) {
|
|
|
283
358
|
}
|
|
284
359
|
|
|
285
360
|
// src/variable.ts
|
|
361
|
+
var CONTEXT_REF_REGEX = /^@([^:]+):(.+)$/;
|
|
362
|
+
var MAX_CONTEXT_REF_DEPTH = 10;
|
|
363
|
+
function resolveContextReference(raw, depth = 0) {
|
|
364
|
+
if (depth >= MAX_CONTEXT_REF_DEPTH) {
|
|
365
|
+
throw new Error(
|
|
366
|
+
`Context reference resolution exceeded max depth (${MAX_CONTEXT_REF_DEPTH}): possible circular reference`
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
const match = raw.match(CONTEXT_REF_REGEX);
|
|
370
|
+
if (!match) return raw;
|
|
371
|
+
const [, contextName, refKey] = match;
|
|
372
|
+
const ctx = getContext(contextName);
|
|
373
|
+
const resolved = ctx[refKey];
|
|
374
|
+
if (resolved === void 0) {
|
|
375
|
+
const hasContext = Object.keys(ctx).length > 0;
|
|
376
|
+
const msg = hasContext ? `Context reference @${contextName}:${refKey} could not be resolved: key "${refKey}" is not defined in context "${contextName}".` : `Context reference @${contextName}:${refKey} could not be resolved: context "${contextName}" not found (no ${contextName}.context.json).`;
|
|
377
|
+
log("error", msg);
|
|
378
|
+
throw new Error(msg);
|
|
379
|
+
}
|
|
380
|
+
return resolveContextReference(resolved, depth + 1);
|
|
381
|
+
}
|
|
286
382
|
function defaultKeyFromName(name) {
|
|
287
383
|
return name.toLowerCase().replace(/\s+/g, "_").replace(/[^a-z0-9_]/gi, "");
|
|
288
384
|
}
|
|
@@ -307,22 +403,25 @@ function scenv(name, options = {}) {
|
|
|
307
403
|
log("trace", `resolveRaw: checking set for key=${key}`);
|
|
308
404
|
if (config.set?.[key] !== void 0) {
|
|
309
405
|
log("trace", `resolveRaw: set hit key=${key}`);
|
|
310
|
-
|
|
406
|
+
const raw = resolveContextReference(config.set[key]);
|
|
407
|
+
return { raw, source: "set" };
|
|
311
408
|
}
|
|
312
409
|
if (!config.ignoreEnv) {
|
|
313
410
|
log("trace", `resolveRaw: checking env ${envKey}`);
|
|
314
411
|
const envVal = process.env[envKey];
|
|
315
412
|
if (envVal !== void 0 && envVal !== "") {
|
|
316
413
|
log("trace", "resolveRaw: env hit");
|
|
317
|
-
|
|
414
|
+
const raw = resolveContextReference(envVal);
|
|
415
|
+
return { raw, source: "env" };
|
|
318
416
|
}
|
|
319
417
|
}
|
|
320
418
|
if (!config.ignoreContext) {
|
|
321
419
|
log("trace", "resolveRaw: checking context");
|
|
322
|
-
const ctx =
|
|
420
|
+
const ctx = getMergedContextValues();
|
|
323
421
|
if (ctx[key] !== void 0) {
|
|
324
422
|
log("trace", `resolveRaw: context hit key=${key}`);
|
|
325
|
-
|
|
423
|
+
const raw = resolveContextReference(ctx[key]);
|
|
424
|
+
return { raw, source: "context" };
|
|
326
425
|
}
|
|
327
426
|
}
|
|
328
427
|
log("trace", "resolveRaw: no value");
|
|
@@ -348,6 +447,7 @@ function scenv(name, options = {}) {
|
|
|
348
447
|
`prompt decision key=${key} prompt=${config.prompt ?? "fallback"} hadValue=${hadValue} hadEnv=${hadEnv} -> ${doPrompt ? "prompt" : "no prompt"}`
|
|
349
448
|
);
|
|
350
449
|
const effectiveDefault = overrides?.default !== void 0 ? overrides.default : defaultValue;
|
|
450
|
+
const resolvedDefault = effectiveDefault === void 0 ? void 0 : typeof effectiveDefault === "string" ? resolveContextReference(effectiveDefault) : effectiveDefault;
|
|
351
451
|
let wasPrompted = false;
|
|
352
452
|
let value;
|
|
353
453
|
let resolvedFrom;
|
|
@@ -359,15 +459,16 @@ function scenv(name, options = {}) {
|
|
|
359
459
|
`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: ... } }).`
|
|
360
460
|
);
|
|
361
461
|
}
|
|
362
|
-
const defaultForPrompt = raw !== void 0 ? raw :
|
|
363
|
-
|
|
462
|
+
const defaultForPrompt = raw !== void 0 ? raw : resolvedDefault;
|
|
463
|
+
let promptedValue = await Promise.resolve(fn(name, defaultForPrompt));
|
|
464
|
+
value = typeof promptedValue === "string" ? resolveContextReference(promptedValue) : promptedValue;
|
|
364
465
|
wasPrompted = true;
|
|
365
466
|
resolvedFrom = "prompt";
|
|
366
467
|
} else if (raw !== void 0) {
|
|
367
468
|
value = raw;
|
|
368
469
|
resolvedFrom = source;
|
|
369
|
-
} else if (
|
|
370
|
-
value =
|
|
470
|
+
} else if (resolvedDefault !== void 0) {
|
|
471
|
+
value = resolvedDefault;
|
|
371
472
|
resolvedFrom = "default";
|
|
372
473
|
} else {
|
|
373
474
|
throw new Error(`Missing value for variable "${name}" (key: ${key})`);
|
|
@@ -399,25 +500,36 @@ function scenv(name, options = {}) {
|
|
|
399
500
|
const final = validated.data;
|
|
400
501
|
if (wasPrompted) {
|
|
401
502
|
const config = loadConfig();
|
|
402
|
-
const
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
503
|
+
const mode = config.shouldSavePrompt ?? (config.prompt === "never" ? "never" : "ask");
|
|
504
|
+
if (mode === "never") return final;
|
|
505
|
+
let doSave;
|
|
506
|
+
if (mode === "ask") {
|
|
507
|
+
const callbacks2 = getCallbacks();
|
|
508
|
+
if (typeof callbacks2.onAskWhetherToSave !== "function") {
|
|
407
509
|
throw new Error(
|
|
408
|
-
`
|
|
510
|
+
`shouldSavePrompt is "ask" but onAskWhetherToSave callback is not set. Configure callbacks via configure({ callbacks: { onAskWhetherToSave: ... } }).`
|
|
409
511
|
);
|
|
410
512
|
}
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
513
|
+
doSave = await callbacks2.onAskWhetherToSave(name, final);
|
|
514
|
+
} else {
|
|
515
|
+
doSave = true;
|
|
516
|
+
}
|
|
517
|
+
if (!doSave) return final;
|
|
518
|
+
const callbacks = getCallbacks();
|
|
519
|
+
const contextNames = config.contexts ?? [];
|
|
520
|
+
let ctxToSave;
|
|
521
|
+
if (config.saveContextTo === "ask") {
|
|
522
|
+
if (typeof callbacks.onAskContext !== "function") {
|
|
523
|
+
throw new Error(
|
|
524
|
+
`saveContextTo is "ask" but onAskContext callback is not set. Configure callbacks via configure({ callbacks: { onAskContext: ... } }).`
|
|
525
|
+
);
|
|
419
526
|
}
|
|
527
|
+
ctxToSave = await callbacks.onAskContext(name, contextNames);
|
|
528
|
+
} else {
|
|
529
|
+
ctxToSave = config.saveContextTo ?? contextNames[0] ?? "default";
|
|
420
530
|
}
|
|
531
|
+
writeToContext(ctxToSave, key, String(final));
|
|
532
|
+
log("info", `Saved key=${key} to context ${ctxToSave}`);
|
|
421
533
|
}
|
|
422
534
|
return final;
|
|
423
535
|
}
|
|
@@ -487,7 +599,7 @@ function parseScenvArgs(argv) {
|
|
|
487
599
|
} else if (arg === "--save-prompt" && argv[i + 1] !== void 0) {
|
|
488
600
|
const v = argv[++i].toLowerCase();
|
|
489
601
|
if (["always", "never", "ask"].includes(v)) {
|
|
490
|
-
config.
|
|
602
|
+
config.shouldSavePrompt = v;
|
|
491
603
|
}
|
|
492
604
|
} else if (arg === "--save-context-to" && argv[i + 1] !== void 0) {
|
|
493
605
|
config.saveContextTo = argv[++i];
|
|
@@ -518,7 +630,8 @@ export {
|
|
|
518
630
|
configure,
|
|
519
631
|
discoverContextPaths,
|
|
520
632
|
getCallbacks,
|
|
521
|
-
|
|
633
|
+
getContext,
|
|
634
|
+
getMergedContextValues,
|
|
522
635
|
loadConfig,
|
|
523
636
|
parseScenvArgs,
|
|
524
637
|
resetConfig,
|
package/package.json
CHANGED
|
@@ -1,10 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "scenv",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "Environment and context variables with runtime-configurable resolution",
|
|
5
|
-
"repository": {
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "https://github.com/PKWadsy/scenv"
|
|
8
|
+
},
|
|
9
|
+
"publishConfig": {
|
|
10
|
+
"access": "public",
|
|
11
|
+
"provenance": true
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"env",
|
|
15
|
+
"config",
|
|
16
|
+
"context",
|
|
17
|
+
"variables"
|
|
18
|
+
],
|
|
8
19
|
"type": "module",
|
|
9
20
|
"main": "dist/index.cjs",
|
|
10
21
|
"module": "dist/index.js",
|