scenv 0.7.0 → 0.9.0
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 +1 -1
- package/dist/index.cjs +62 -69
- package/dist/index.d.cts +59 -30
- package/dist/index.d.ts +59 -30
- package/dist/index.js +62 -69
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -48,7 +48,7 @@ Prompting (when to ask the user) is controlled by config `prompt`: `always` | `n
|
|
|
48
48
|
|
|
49
49
|
| Package | Purpose |
|
|
50
50
|
| -------------------------------------------------------------- | ------------------------------------------------------------- |
|
|
51
|
-
| [scenv-zod](https://www.npmjs.com/package/scenv-zod) | `
|
|
51
|
+
| [scenv-zod](https://www.npmjs.com/package/scenv-zod) | `parser(zodSchema)` for type-safe parsing and coercion. |
|
|
52
52
|
| [scenv-inquirer](https://www.npmjs.com/package/scenv-inquirer) | `prompt()` and callbacks for interactive prompts. |
|
|
53
53
|
|
|
54
54
|
## Documentation
|
package/dist/index.cjs
CHANGED
|
@@ -48,12 +48,12 @@ function defaultPrompt(name, defaultValue) {
|
|
|
48
48
|
const defaultStr = defaultValue !== void 0 && defaultValue !== null ? String(defaultValue) : "";
|
|
49
49
|
const message = defaultStr ? `Enter ${name} [${defaultStr}]: ` : `Enter ${name}: `;
|
|
50
50
|
const rl = (0, import_node_readline.createInterface)({ input: process.stdin, output: process.stdout });
|
|
51
|
-
return new Promise((
|
|
51
|
+
return new Promise((resolve2, reject) => {
|
|
52
52
|
rl.question(message, (answer) => {
|
|
53
53
|
rl.close();
|
|
54
54
|
const trimmed = answer.trim();
|
|
55
55
|
const value = trimmed !== "" ? trimmed : defaultStr;
|
|
56
|
-
|
|
56
|
+
resolve2(value);
|
|
57
57
|
});
|
|
58
58
|
rl.on("error", (err) => {
|
|
59
59
|
rl.close();
|
|
@@ -72,7 +72,7 @@ var envKeyMap = {
|
|
|
72
72
|
SCENV_IGNORE_ENV: "ignoreEnv",
|
|
73
73
|
SCENV_IGNORE_CONTEXT: "ignoreContext",
|
|
74
74
|
SCENV_SAVE_CONTEXT_TO: "saveContextTo",
|
|
75
|
-
|
|
75
|
+
SCENV_SAVE_MODE: "saveMode",
|
|
76
76
|
SCENV_LOG_LEVEL: "logLevel"
|
|
77
77
|
};
|
|
78
78
|
var programmaticConfig = {};
|
|
@@ -108,8 +108,10 @@ function configFromEnv() {
|
|
|
108
108
|
out[configKey] = v;
|
|
109
109
|
} else if (configKey === "saveContextTo") {
|
|
110
110
|
out.saveContextTo = val;
|
|
111
|
-
} else if (configKey === "
|
|
112
|
-
|
|
111
|
+
} else if (configKey === "saveMode") {
|
|
112
|
+
const v = val.toLowerCase();
|
|
113
|
+
if (v === "all" || v === "prompts-only")
|
|
114
|
+
out[configKey] = v;
|
|
113
115
|
} else if (configKey === "logLevel") {
|
|
114
116
|
const v = val.toLowerCase();
|
|
115
117
|
if (LOG_LEVELS.includes(v)) out.logLevel = v;
|
|
@@ -149,8 +151,8 @@ function loadConfigFile(configDir) {
|
|
|
149
151
|
out.set = parsed.set;
|
|
150
152
|
if (typeof parsed.saveContextTo === "string")
|
|
151
153
|
out.saveContextTo = parsed.saveContextTo;
|
|
152
|
-
if (typeof parsed.
|
|
153
|
-
|
|
154
|
+
if (typeof parsed.saveMode === "string" && ["all", "prompts-only"].includes(parsed.saveMode))
|
|
155
|
+
out.saveMode = parsed.saveMode;
|
|
154
156
|
if (typeof parsed.root === "string") out.root = parsed.root;
|
|
155
157
|
if (typeof parsed.logLevel === "string" && LOG_LEVELS.includes(parsed.logLevel))
|
|
156
158
|
out.logLevel = parsed.logLevel;
|
|
@@ -238,7 +240,7 @@ var CONFIG_LOG_KEYS = [
|
|
|
238
240
|
"ignoreContext",
|
|
239
241
|
"set",
|
|
240
242
|
"saveContextTo",
|
|
241
|
-
"
|
|
243
|
+
"saveMode",
|
|
242
244
|
"logLevel"
|
|
243
245
|
];
|
|
244
246
|
function configForLog(config) {
|
|
@@ -259,7 +261,7 @@ function logConfigLoaded(config) {
|
|
|
259
261
|
if (config.ignoreEnv === true) parts.push("ignoreEnv=true");
|
|
260
262
|
if (config.ignoreContext === true) parts.push("ignoreContext=true");
|
|
261
263
|
if (config.saveContextTo !== void 0) parts.push("saveContextTo=" + config.saveContextTo);
|
|
262
|
-
if (config.
|
|
264
|
+
if (config.saveMode !== void 0) parts.push("saveMode=" + config.saveMode);
|
|
263
265
|
log("info", "config loaded", parts.join(" "));
|
|
264
266
|
}
|
|
265
267
|
if (levelNum >= LEVEL_NUM.debug) {
|
|
@@ -328,10 +330,23 @@ function discoverContextPaths(dir, found = /* @__PURE__ */ new Map()) {
|
|
|
328
330
|
return found;
|
|
329
331
|
}
|
|
330
332
|
function getContext(contextName, root) {
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
333
|
+
let filePath;
|
|
334
|
+
if (root !== void 0) {
|
|
335
|
+
const paths = discoverContextPaths(root);
|
|
336
|
+
filePath = paths.get(contextName);
|
|
337
|
+
} else {
|
|
338
|
+
const cwd = process.cwd();
|
|
339
|
+
const paths = discoverContextPaths(cwd);
|
|
340
|
+
filePath = paths.get(contextName);
|
|
341
|
+
if (!filePath) {
|
|
342
|
+
const config = loadConfig();
|
|
343
|
+
const projectRoot = config.root ?? cwd;
|
|
344
|
+
if (projectRoot !== cwd) {
|
|
345
|
+
const rootPaths = discoverContextPaths(projectRoot);
|
|
346
|
+
filePath = rootPaths.get(contextName);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
335
350
|
if (!filePath) {
|
|
336
351
|
log("trace", `getContext: context "${contextName}" not found`);
|
|
337
352
|
return {};
|
|
@@ -356,17 +371,9 @@ function getMergedContextValues() {
|
|
|
356
371
|
const config = loadConfig();
|
|
357
372
|
logConfigLoaded(config);
|
|
358
373
|
if (config.ignoreContext) return {};
|
|
359
|
-
const
|
|
360
|
-
const paths = discoverContextPaths(
|
|
374
|
+
const searchRoot = process.cwd();
|
|
375
|
+
const paths = discoverContextPaths(searchRoot);
|
|
361
376
|
const out = {};
|
|
362
|
-
if (config.saveContextTo) {
|
|
363
|
-
const savePath = resolveSaveContextPath(config.saveContextTo);
|
|
364
|
-
const saveCtx = getContextAtPath(savePath);
|
|
365
|
-
for (const [k, v] of Object.entries(saveCtx)) out[k] = v;
|
|
366
|
-
if (Object.keys(saveCtx).length > 0) {
|
|
367
|
-
log("debug", `saveContextTo "${config.saveContextTo}" loaded keys=${JSON.stringify(Object.keys(saveCtx))}`);
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
377
|
for (const contextName of config.context ?? []) {
|
|
371
378
|
const filePath = paths.get(contextName);
|
|
372
379
|
if (!filePath) {
|
|
@@ -398,32 +405,11 @@ function getContextWritePath(contextName) {
|
|
|
398
405
|
return contextName.endsWith(CONTEXT_SUFFIX) ? contextName : contextName + CONTEXT_SUFFIX;
|
|
399
406
|
}
|
|
400
407
|
const config = loadConfig();
|
|
401
|
-
const
|
|
402
|
-
const paths = discoverContextPaths(root);
|
|
408
|
+
const paths = discoverContextPaths(process.cwd());
|
|
403
409
|
const existing = paths.get(contextName);
|
|
404
410
|
if (existing) return existing;
|
|
405
|
-
const
|
|
406
|
-
return (0, import_node_path2.join)(
|
|
407
|
-
}
|
|
408
|
-
function resolveSaveContextPath(nameOrPath) {
|
|
409
|
-
if ((0, import_node_path2.isAbsolute)(nameOrPath) || nameOrPath.includes(import_node_path2.sep)) {
|
|
410
|
-
return nameOrPath.endsWith(CONTEXT_SUFFIX) ? nameOrPath : nameOrPath + CONTEXT_SUFFIX;
|
|
411
|
-
}
|
|
412
|
-
return getContextWritePath(nameOrPath);
|
|
413
|
-
}
|
|
414
|
-
function getContextAtPath(filePath) {
|
|
415
|
-
if (!(0, import_node_fs2.existsSync)(filePath)) return {};
|
|
416
|
-
try {
|
|
417
|
-
const raw = (0, import_node_fs2.readFileSync)(filePath, "utf-8");
|
|
418
|
-
const data = JSON.parse(raw);
|
|
419
|
-
const out = {};
|
|
420
|
-
for (const [k, v] of Object.entries(data)) {
|
|
421
|
-
if (typeof v === "string") out[k] = v;
|
|
422
|
-
}
|
|
423
|
-
return out;
|
|
424
|
-
} catch {
|
|
425
|
-
return {};
|
|
426
|
-
}
|
|
411
|
+
const projectRoot = config.root ?? process.cwd();
|
|
412
|
+
return (0, import_node_path2.join)(projectRoot, `${contextName}${CONTEXT_SUFFIX}`);
|
|
427
413
|
}
|
|
428
414
|
function writeToContext(contextName, key, value) {
|
|
429
415
|
const path = getContextWritePath(contextName);
|
|
@@ -491,7 +477,7 @@ function defaultKeyFromName(name) {
|
|
|
491
477
|
function defaultEnvFromKey(key) {
|
|
492
478
|
return key.toUpperCase().replace(/-/g, "_");
|
|
493
479
|
}
|
|
494
|
-
function
|
|
480
|
+
function normalizeParserResult(result) {
|
|
495
481
|
if (typeof result === "boolean") {
|
|
496
482
|
return result ? { success: true } : { success: false };
|
|
497
483
|
}
|
|
@@ -501,7 +487,7 @@ function normalizeValidatorResult(result) {
|
|
|
501
487
|
function scenv(name, options = {}) {
|
|
502
488
|
const key = options.key ?? defaultKeyFromName(name);
|
|
503
489
|
const envKey = options.env ?? defaultEnvFromKey(key);
|
|
504
|
-
const
|
|
490
|
+
const parserFn = options.parser;
|
|
505
491
|
const promptFn = options.prompt;
|
|
506
492
|
const defaultValue = options.default;
|
|
507
493
|
async function resolveRaw() {
|
|
@@ -588,10 +574,10 @@ function scenv(name, options = {}) {
|
|
|
588
574
|
log("info", `variable "${name}" (key=${key}) resolved from ${resolvedFrom}`);
|
|
589
575
|
return { value, raw, hadEnv, wasPrompted };
|
|
590
576
|
}
|
|
591
|
-
function
|
|
592
|
-
if (!
|
|
593
|
-
const result =
|
|
594
|
-
const normalized =
|
|
577
|
+
function parse(value) {
|
|
578
|
+
if (!parserFn) return { success: true, data: value };
|
|
579
|
+
const result = parserFn(value);
|
|
580
|
+
const normalized = normalizeParserResult(result);
|
|
595
581
|
if (normalized.success) {
|
|
596
582
|
const data = "data" in normalized && normalized.data !== void 0 ? normalized.data : value;
|
|
597
583
|
return { success: true, data };
|
|
@@ -603,17 +589,18 @@ function scenv(name, options = {}) {
|
|
|
603
589
|
}
|
|
604
590
|
async function get(options2) {
|
|
605
591
|
const { value, wasPrompted } = await getResolvedValue(options2);
|
|
606
|
-
const
|
|
607
|
-
if (!
|
|
608
|
-
const errMsg = `
|
|
592
|
+
const parsed = parse(value);
|
|
593
|
+
if (!parsed.success) {
|
|
594
|
+
const errMsg = `Parsing failed for "${name}": ${parsed.error ?? "unknown"}`;
|
|
609
595
|
log("error", errMsg);
|
|
610
596
|
throw new Error(errMsg);
|
|
611
597
|
}
|
|
612
|
-
const final =
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
598
|
+
const final = parsed.data;
|
|
599
|
+
const config = loadConfig();
|
|
600
|
+
if (wasPrompted) setInMemoryContext(key, String(final));
|
|
601
|
+
if (config.saveContextTo) {
|
|
602
|
+
const saveMode = config.saveMode ?? "all";
|
|
603
|
+
if (saveMode === "all" || wasPrompted) {
|
|
617
604
|
writeToContext(config.saveContextTo, key, String(final));
|
|
618
605
|
log("info", `Saved key=${key} to saveContextTo ${config.saveContextTo}`);
|
|
619
606
|
}
|
|
@@ -630,16 +617,16 @@ function scenv(name, options = {}) {
|
|
|
630
617
|
}
|
|
631
618
|
async function save(value) {
|
|
632
619
|
const toSave = value ?? (await getResolvedValue()).value;
|
|
633
|
-
const
|
|
634
|
-
if (!
|
|
635
|
-
const errMsg = `
|
|
620
|
+
const parsed = parse(toSave);
|
|
621
|
+
if (!parsed.success) {
|
|
622
|
+
const errMsg = `Parsing failed for "${name}": ${parsed.error ?? "unknown"}`;
|
|
636
623
|
log("error", errMsg);
|
|
637
624
|
throw new Error(errMsg);
|
|
638
625
|
}
|
|
639
626
|
const config = loadConfig();
|
|
640
|
-
setInMemoryContext(key, String(
|
|
627
|
+
setInMemoryContext(key, String(parsed.data));
|
|
641
628
|
if (config.saveContextTo) {
|
|
642
|
-
writeToContext(config.saveContextTo, key, String(
|
|
629
|
+
writeToContext(config.saveContextTo, key, String(parsed.data));
|
|
643
630
|
log("info", `Saved key=${key} to saveContextTo ${config.saveContextTo}`);
|
|
644
631
|
}
|
|
645
632
|
}
|
|
@@ -647,12 +634,15 @@ function scenv(name, options = {}) {
|
|
|
647
634
|
}
|
|
648
635
|
|
|
649
636
|
// src/cli-args.ts
|
|
637
|
+
var import_node_path3 = require("path");
|
|
650
638
|
function parseScenvArgs(argv) {
|
|
651
639
|
const config = {};
|
|
652
640
|
let i = 0;
|
|
653
641
|
while (i < argv.length) {
|
|
654
642
|
const arg = argv[i];
|
|
655
|
-
if (arg === "--
|
|
643
|
+
if (arg === "--root" && argv[i + 1] !== void 0) {
|
|
644
|
+
config.root = (0, import_node_path3.resolve)(argv[++i]);
|
|
645
|
+
} else if (arg === "--context" && argv[i + 1] !== void 0) {
|
|
656
646
|
config.context = argv[++i].split(",").map((s) => s.trim()).filter(Boolean);
|
|
657
647
|
} else if (arg === "--add-context" && argv[i + 1] !== void 0) {
|
|
658
648
|
config.addContext = argv[++i].split(",").map((s) => s.trim()).filter(Boolean);
|
|
@@ -674,8 +664,11 @@ function parseScenvArgs(argv) {
|
|
|
674
664
|
}
|
|
675
665
|
} else if (arg === "--save-context-to" && argv[i + 1] !== void 0) {
|
|
676
666
|
config.saveContextTo = argv[++i];
|
|
677
|
-
} else if (arg === "--
|
|
678
|
-
|
|
667
|
+
} else if (arg === "--save-mode" && argv[i + 1] !== void 0) {
|
|
668
|
+
const v = argv[++i].toLowerCase();
|
|
669
|
+
if (v === "all" || v === "prompts-only") {
|
|
670
|
+
config.saveMode = v;
|
|
671
|
+
}
|
|
679
672
|
} else if ((arg === "--log-level" || arg === "--log") && argv[i + 1] !== void 0) {
|
|
680
673
|
const v = argv[++i].toLowerCase();
|
|
681
674
|
if (LOG_LEVELS.includes(v)) {
|
package/dist/index.d.cts
CHANGED
|
@@ -13,6 +13,12 @@ type PromptMode = "always" | "never" | "fallback" | "no-env";
|
|
|
13
13
|
declare const LOG_LEVELS: readonly ["none", "trace", "debug", "info", "warn", "error"];
|
|
14
14
|
/** Log level type. `"none"` disables logging; higher values are more verbose. */
|
|
15
15
|
type LogLevel = (typeof LOG_LEVELS)[number];
|
|
16
|
+
/**
|
|
17
|
+
* When to write resolved values to saveContextTo during get().
|
|
18
|
+
* - `"all"` – Save every resolved variable (from set, env, context, or prompt). Useful for re-running with the same values.
|
|
19
|
+
* - `"prompts-only"` – Save only when the user was prompted. Default is `"all"`.
|
|
20
|
+
*/
|
|
21
|
+
type SaveMode = "all" | "prompts-only";
|
|
16
22
|
/**
|
|
17
23
|
* Full scenv configuration. Built from file (scenv.config.json), environment (SCENV_*),
|
|
18
24
|
* and programmatic config (configure()), with programmatic > env > file precedence.
|
|
@@ -33,9 +39,9 @@ interface ScenvConfig {
|
|
|
33
39
|
set?: Record<string, string>;
|
|
34
40
|
/** Optional path or context name (without .context.json) where to save resolved values. If set, all saves go here and this context is used before prompting. If unset, values are saved to an in-memory context only (same process). */
|
|
35
41
|
saveContextTo?: string;
|
|
36
|
-
/**
|
|
37
|
-
|
|
38
|
-
/**
|
|
42
|
+
/** When to write to saveContextTo during get(): "all" (default) saves every resolved variable; "prompts-only" saves only when the user was prompted. */
|
|
43
|
+
saveMode?: SaveMode;
|
|
44
|
+
/** Project root: where to search for scenv.config.json and where new context files are saved. Defaults to the directory containing scenv.config.json (when found) or cwd. Context files are discovered from cwd. */
|
|
39
45
|
root?: string;
|
|
40
46
|
/** Logging level. Default is `"none"`. Messages go to stderr. */
|
|
41
47
|
logLevel?: LogLevel;
|
|
@@ -120,17 +126,18 @@ declare function discoverContextPaths(dir: string, found?: Map<string, string>):
|
|
|
120
126
|
/**
|
|
121
127
|
* Loads key-value pairs from a single context file. Used when resolving @context:key references.
|
|
122
128
|
* Does not depend on config.context or ignoreContext; the context file is read if it exists
|
|
123
|
-
* under the
|
|
129
|
+
* under the search directory.
|
|
124
130
|
*
|
|
125
131
|
* @param contextName - Name of the context (e.g. "prod", "dev") — file is contextName.context.json.
|
|
126
|
-
* @param root - Optional
|
|
132
|
+
* @param root - Optional directory to search. If omitted, searches from process.cwd() then from project root (config.root) if the context is not found under cwd.
|
|
127
133
|
* @returns A flat record of key → string value from that context file. Empty if file not found or invalid.
|
|
128
134
|
*/
|
|
129
135
|
declare function getContext(contextName: string, root?: string): Record<string, string>;
|
|
130
136
|
/**
|
|
131
|
-
* Loads and merges context values from the current config
|
|
132
|
-
* is
|
|
133
|
-
*
|
|
137
|
+
* Loads and merges context values from the current config from {@link ScenvConfig.context} only.
|
|
138
|
+
* saveContextTo is not used for resolution; it is only a write target. To use the same context
|
|
139
|
+
* for reading, add it explicitly via context or addContext.
|
|
140
|
+
* Respects {@link ScenvConfig.ignoreContext}. Later contexts overwrite earlier for the same key.
|
|
134
141
|
* Used during variable resolution (set > env > in-memory > merged context > default).
|
|
135
142
|
*
|
|
136
143
|
* @returns A flat record of key → string value. Empty if ignoreContext is true or no context loaded.
|
|
@@ -139,7 +146,7 @@ declare function getMergedContextValues(): Record<string, string>;
|
|
|
139
146
|
/**
|
|
140
147
|
* Returns the file path used for a context name or path when saving.
|
|
141
148
|
* - If contextName is path-like (absolute or contains path separator), returns that path with .context.json appended if not already present.
|
|
142
|
-
* - Otherwise, if that context was already discovered under
|
|
149
|
+
* - Otherwise, if that context was already discovered under cwd, returns its path; else saves under project root (config.root or cwd).
|
|
143
150
|
*
|
|
144
151
|
* @param contextName - Context name (e.g. "dev", "prod") or file path without suffix (e.g. "/path/to/myfile" → myfile.context.json).
|
|
145
152
|
* @returns Absolute path to the context JSON file.
|
|
@@ -147,18 +154,35 @@ declare function getMergedContextValues(): Record<string, string>;
|
|
|
147
154
|
declare function getContextWritePath(contextName: string): string;
|
|
148
155
|
|
|
149
156
|
/**
|
|
150
|
-
* Return type for a variable's optional `
|
|
151
|
-
*
|
|
152
|
-
*
|
|
153
|
-
* - `
|
|
157
|
+
* Return type for a variable's optional `parser` function. Input is always the raw string
|
|
158
|
+
* (from set/env/context) or the default. Use a boolean for simple pass/fail, or an object
|
|
159
|
+
* to pass a parsed/transformed value or a custom error.
|
|
160
|
+
* - `true` or `{ success: true, data?: T }` – parsing passed; optional `data` is the output value (type T).
|
|
161
|
+
* - `false` or `{ success: false, error?: unknown }` – parsing failed; `.get()` throws with the error.
|
|
154
162
|
*/
|
|
155
|
-
type
|
|
163
|
+
type ParserResult<T> = boolean | {
|
|
156
164
|
success: true;
|
|
157
165
|
data?: T;
|
|
158
166
|
} | {
|
|
159
167
|
success: false;
|
|
160
168
|
error?: unknown;
|
|
161
169
|
};
|
|
170
|
+
/**
|
|
171
|
+
* Infers the variable's value type (output type) from options: when a parser returns
|
|
172
|
+
* `{ success: true, data: D }`, that D is used; otherwise the type comes from `default` or defaults to string.
|
|
173
|
+
*/
|
|
174
|
+
type InferVariableType<O> = O extends {
|
|
175
|
+
parser: (v: unknown) => infer R;
|
|
176
|
+
} ? R extends {
|
|
177
|
+
success: true;
|
|
178
|
+
data: infer D;
|
|
179
|
+
} ? D extends undefined ? O extends {
|
|
180
|
+
default: infer Def;
|
|
181
|
+
} ? Def : string : D : O extends {
|
|
182
|
+
default: infer Def;
|
|
183
|
+
} ? Def : string : O extends {
|
|
184
|
+
default: infer Def;
|
|
185
|
+
} ? Def : string;
|
|
162
186
|
/**
|
|
163
187
|
* Prompt function signature. Called when config requests prompting for this variable.
|
|
164
188
|
* Receives the variable's display name and the current default (from set/env/context or option default).
|
|
@@ -168,16 +192,17 @@ type PromptFn<T> = (name: string, defaultValue: T) => T | Promise<T>;
|
|
|
168
192
|
/**
|
|
169
193
|
* Options when creating a variable with {@link scenv}. All properties are optional.
|
|
170
194
|
* If you omit `key` and `env`, they are derived from `name`: e.g. "API URL" → key `api_url`, env `API_URL`.
|
|
195
|
+
* Input from set/env/context is always string; `default` and the variable's value type are the output type T.
|
|
171
196
|
*/
|
|
172
|
-
interface ScenvVariableOptions<T> {
|
|
197
|
+
interface ScenvVariableOptions<T = string> {
|
|
173
198
|
/** Internal key for --set, context files, and env. Default: name lowercased, spaces → underscores, non-alphanumeric stripped (e.g. "API URL" → "api_url"). */
|
|
174
199
|
key?: string;
|
|
175
200
|
/** Environment variable name (e.g. API_URL). Default: key uppercased, hyphens → underscores. */
|
|
176
201
|
env?: string;
|
|
177
|
-
/** Fallback when nothing is provided via --set, env, or context (and we're not prompting). */
|
|
202
|
+
/** Fallback when nothing is provided via --set, env, or context (and we're not prompting). Same type T as the variable's output (not parsed). */
|
|
178
203
|
default?: T;
|
|
179
|
-
/** Optional.
|
|
180
|
-
|
|
204
|
+
/** Optional. Parses the raw string (or default) into output type T. Receives raw value (typically string). Return { success: true, data } with the parsed value; variable type is inferred from data. */
|
|
205
|
+
parser?: (val: unknown) => ParserResult<T>;
|
|
181
206
|
/** Optional. Called when config says to prompt (e.g. prompt: "fallback" and no value found). Overrides callbacks.defaultPrompt for this variable. */
|
|
182
207
|
prompt?: PromptFn<T>;
|
|
183
208
|
}
|
|
@@ -197,8 +222,8 @@ interface GetOptions<T> {
|
|
|
197
222
|
*/
|
|
198
223
|
interface ScenvVariable<T> {
|
|
199
224
|
/**
|
|
200
|
-
* Resolve and return the value. Uses resolution order (set > env > context > default) and any prompt/
|
|
201
|
-
* @throws If no value is found and no default/prompt, or if
|
|
225
|
+
* Resolve and return the value. Uses resolution order (set > env > context > default) and any prompt/parser.
|
|
226
|
+
* @throws If no value is found and no default/prompt, or if parsing fails.
|
|
202
227
|
*/
|
|
203
228
|
get(options?: GetOptions<T>): Promise<T>;
|
|
204
229
|
/**
|
|
@@ -234,11 +259,10 @@ interface ScenvVariable<T> {
|
|
|
234
259
|
* the default option). The callback's return value is used as the value. When we don't prompt, we use the
|
|
235
260
|
* raw value if present, otherwise the default option, otherwise get() throws (no value).
|
|
236
261
|
*
|
|
237
|
-
* ##
|
|
262
|
+
* ## Parser
|
|
238
263
|
*
|
|
239
|
-
* If you pass a
|
|
240
|
-
* { success: true }
|
|
241
|
-
* Use it to coerce types (e.g. string to number) or enforce rules.
|
|
264
|
+
* If you pass a parser option, it is called with the raw value (typically string from set/env/context) or the default.
|
|
265
|
+
* Return { success: true, data } with the parsed output; the variable's type is inferred from data. On { success: false }, get() throws.
|
|
242
266
|
*
|
|
243
267
|
* ## save()
|
|
244
268
|
*
|
|
@@ -247,22 +271,27 @@ interface ScenvVariable<T> {
|
|
|
247
271
|
* When the user is prompted during get(), the value is always saved (to saveContextTo file if set, and always to in-memory)
|
|
248
272
|
* so a second get() on the same variable does not prompt again.
|
|
249
273
|
*
|
|
250
|
-
* @typeParam T -
|
|
274
|
+
* @typeParam T - Output (value) type of the variable, e.g. string, number, URL. Defaults to string.
|
|
251
275
|
* @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").
|
|
252
|
-
* @param options - Optional. key, env, default,
|
|
253
|
-
* @returns A {@link ScenvVariable} with get(), safeGet(), and save().
|
|
276
|
+
* @param options - Optional. key, env, default, parser, prompt. See {@link ScenvVariableOptions}.
|
|
277
|
+
* @returns A {@link ScenvVariable<T>} with get(), safeGet(), and save().
|
|
254
278
|
*
|
|
255
279
|
* @example
|
|
256
280
|
* const apiUrl = scenv("API URL", { default: "http://localhost:4000" });
|
|
257
281
|
* const url = await apiUrl.get();
|
|
282
|
+
*
|
|
283
|
+
* @example
|
|
284
|
+
* const port = scenv<number>("Port", { parser: parser(z.coerce.number()), default: 3000 });
|
|
285
|
+
* const n = await port.get(); // number
|
|
258
286
|
*/
|
|
259
|
-
declare function scenv<T>(name: string, options?: ScenvVariableOptions<T>): ScenvVariable<T>;
|
|
287
|
+
declare function scenv<T = string>(name: string, options?: ScenvVariableOptions<T>): ScenvVariable<T>;
|
|
260
288
|
|
|
261
289
|
/**
|
|
262
290
|
* Parses command-line arguments into a partial {@link ScenvConfig} suitable for {@link configure}.
|
|
263
291
|
* Typical use: `configure(parseScenvArgs(process.argv.slice(2)))`. Unrecognized flags are ignored.
|
|
264
292
|
*
|
|
265
293
|
* Supported flags:
|
|
294
|
+
* - `--root path` – Project root (where to find scenv.config.json and where new context files are saved). Relative paths are resolved from cwd.
|
|
266
295
|
* - `--context a,b,c` – Set context list (replace).
|
|
267
296
|
* - `--add-context x,y` – Add context names.
|
|
268
297
|
* - `--prompt always|never|fallback|no-env` – Prompt mode.
|
|
@@ -270,7 +299,7 @@ declare function scenv<T>(name: string, options?: ScenvVariableOptions<T>): Scen
|
|
|
270
299
|
* - `--ignore-context` – Set ignoreContext to true.
|
|
271
300
|
* - `--set key=value` or `--set=key=value` – Add to set overrides (multiple allowed).
|
|
272
301
|
* - `--save-context-to pathOrName` – saveContextTo (path or context name without .context.json).
|
|
273
|
-
* - `--
|
|
302
|
+
* - `--save-mode all|prompts-only` – When to write to saveContextTo during get(); default is all.
|
|
274
303
|
* - `--log-level level`, `--log level`, `--log=level` – logLevel.
|
|
275
304
|
*
|
|
276
305
|
* @param argv - Array of CLI arguments (e.g. process.argv.slice(2)).
|
|
@@ -278,4 +307,4 @@ declare function scenv<T>(name: string, options?: ScenvVariableOptions<T>): Scen
|
|
|
278
307
|
*/
|
|
279
308
|
declare function parseScenvArgs(argv: string[]): Partial<ScenvConfig>;
|
|
280
309
|
|
|
281
|
-
export { type DefaultPromptFn, type GetOptions, LOG_LEVELS, type LogLevel, type PromptMode, type ScenvCallbacks, type ScenvConfig, type ScenvVariable, configure, discoverContextPaths, getCallbacks, getContext, getContextWritePath, getInMemoryContext, getMergedContextValues, loadConfig, parseScenvArgs, resetConfig, resetInMemoryContext, resetLogState, scenv, setInMemoryContext };
|
|
310
|
+
export { type DefaultPromptFn, type GetOptions, type InferVariableType, LOG_LEVELS, type LogLevel, type ParserResult, type PromptMode, type SaveMode, type ScenvCallbacks, type ScenvConfig, type ScenvVariable, type ScenvVariableOptions, configure, discoverContextPaths, getCallbacks, getContext, getContextWritePath, getInMemoryContext, getMergedContextValues, loadConfig, parseScenvArgs, resetConfig, resetInMemoryContext, resetLogState, scenv, setInMemoryContext };
|
package/dist/index.d.ts
CHANGED
|
@@ -13,6 +13,12 @@ type PromptMode = "always" | "never" | "fallback" | "no-env";
|
|
|
13
13
|
declare const LOG_LEVELS: readonly ["none", "trace", "debug", "info", "warn", "error"];
|
|
14
14
|
/** Log level type. `"none"` disables logging; higher values are more verbose. */
|
|
15
15
|
type LogLevel = (typeof LOG_LEVELS)[number];
|
|
16
|
+
/**
|
|
17
|
+
* When to write resolved values to saveContextTo during get().
|
|
18
|
+
* - `"all"` – Save every resolved variable (from set, env, context, or prompt). Useful for re-running with the same values.
|
|
19
|
+
* - `"prompts-only"` – Save only when the user was prompted. Default is `"all"`.
|
|
20
|
+
*/
|
|
21
|
+
type SaveMode = "all" | "prompts-only";
|
|
16
22
|
/**
|
|
17
23
|
* Full scenv configuration. Built from file (scenv.config.json), environment (SCENV_*),
|
|
18
24
|
* and programmatic config (configure()), with programmatic > env > file precedence.
|
|
@@ -33,9 +39,9 @@ interface ScenvConfig {
|
|
|
33
39
|
set?: Record<string, string>;
|
|
34
40
|
/** Optional path or context name (without .context.json) where to save resolved values. If set, all saves go here and this context is used before prompting. If unset, values are saved to an in-memory context only (same process). */
|
|
35
41
|
saveContextTo?: string;
|
|
36
|
-
/**
|
|
37
|
-
|
|
38
|
-
/**
|
|
42
|
+
/** When to write to saveContextTo during get(): "all" (default) saves every resolved variable; "prompts-only" saves only when the user was prompted. */
|
|
43
|
+
saveMode?: SaveMode;
|
|
44
|
+
/** Project root: where to search for scenv.config.json and where new context files are saved. Defaults to the directory containing scenv.config.json (when found) or cwd. Context files are discovered from cwd. */
|
|
39
45
|
root?: string;
|
|
40
46
|
/** Logging level. Default is `"none"`. Messages go to stderr. */
|
|
41
47
|
logLevel?: LogLevel;
|
|
@@ -120,17 +126,18 @@ declare function discoverContextPaths(dir: string, found?: Map<string, string>):
|
|
|
120
126
|
/**
|
|
121
127
|
* Loads key-value pairs from a single context file. Used when resolving @context:key references.
|
|
122
128
|
* Does not depend on config.context or ignoreContext; the context file is read if it exists
|
|
123
|
-
* under the
|
|
129
|
+
* under the search directory.
|
|
124
130
|
*
|
|
125
131
|
* @param contextName - Name of the context (e.g. "prod", "dev") — file is contextName.context.json.
|
|
126
|
-
* @param root - Optional
|
|
132
|
+
* @param root - Optional directory to search. If omitted, searches from process.cwd() then from project root (config.root) if the context is not found under cwd.
|
|
127
133
|
* @returns A flat record of key → string value from that context file. Empty if file not found or invalid.
|
|
128
134
|
*/
|
|
129
135
|
declare function getContext(contextName: string, root?: string): Record<string, string>;
|
|
130
136
|
/**
|
|
131
|
-
* Loads and merges context values from the current config
|
|
132
|
-
* is
|
|
133
|
-
*
|
|
137
|
+
* Loads and merges context values from the current config from {@link ScenvConfig.context} only.
|
|
138
|
+
* saveContextTo is not used for resolution; it is only a write target. To use the same context
|
|
139
|
+
* for reading, add it explicitly via context or addContext.
|
|
140
|
+
* Respects {@link ScenvConfig.ignoreContext}. Later contexts overwrite earlier for the same key.
|
|
134
141
|
* Used during variable resolution (set > env > in-memory > merged context > default).
|
|
135
142
|
*
|
|
136
143
|
* @returns A flat record of key → string value. Empty if ignoreContext is true or no context loaded.
|
|
@@ -139,7 +146,7 @@ declare function getMergedContextValues(): Record<string, string>;
|
|
|
139
146
|
/**
|
|
140
147
|
* Returns the file path used for a context name or path when saving.
|
|
141
148
|
* - If contextName is path-like (absolute or contains path separator), returns that path with .context.json appended if not already present.
|
|
142
|
-
* - Otherwise, if that context was already discovered under
|
|
149
|
+
* - Otherwise, if that context was already discovered under cwd, returns its path; else saves under project root (config.root or cwd).
|
|
143
150
|
*
|
|
144
151
|
* @param contextName - Context name (e.g. "dev", "prod") or file path without suffix (e.g. "/path/to/myfile" → myfile.context.json).
|
|
145
152
|
* @returns Absolute path to the context JSON file.
|
|
@@ -147,18 +154,35 @@ declare function getMergedContextValues(): Record<string, string>;
|
|
|
147
154
|
declare function getContextWritePath(contextName: string): string;
|
|
148
155
|
|
|
149
156
|
/**
|
|
150
|
-
* Return type for a variable's optional `
|
|
151
|
-
*
|
|
152
|
-
*
|
|
153
|
-
* - `
|
|
157
|
+
* Return type for a variable's optional `parser` function. Input is always the raw string
|
|
158
|
+
* (from set/env/context) or the default. Use a boolean for simple pass/fail, or an object
|
|
159
|
+
* to pass a parsed/transformed value or a custom error.
|
|
160
|
+
* - `true` or `{ success: true, data?: T }` – parsing passed; optional `data` is the output value (type T).
|
|
161
|
+
* - `false` or `{ success: false, error?: unknown }` – parsing failed; `.get()` throws with the error.
|
|
154
162
|
*/
|
|
155
|
-
type
|
|
163
|
+
type ParserResult<T> = boolean | {
|
|
156
164
|
success: true;
|
|
157
165
|
data?: T;
|
|
158
166
|
} | {
|
|
159
167
|
success: false;
|
|
160
168
|
error?: unknown;
|
|
161
169
|
};
|
|
170
|
+
/**
|
|
171
|
+
* Infers the variable's value type (output type) from options: when a parser returns
|
|
172
|
+
* `{ success: true, data: D }`, that D is used; otherwise the type comes from `default` or defaults to string.
|
|
173
|
+
*/
|
|
174
|
+
type InferVariableType<O> = O extends {
|
|
175
|
+
parser: (v: unknown) => infer R;
|
|
176
|
+
} ? R extends {
|
|
177
|
+
success: true;
|
|
178
|
+
data: infer D;
|
|
179
|
+
} ? D extends undefined ? O extends {
|
|
180
|
+
default: infer Def;
|
|
181
|
+
} ? Def : string : D : O extends {
|
|
182
|
+
default: infer Def;
|
|
183
|
+
} ? Def : string : O extends {
|
|
184
|
+
default: infer Def;
|
|
185
|
+
} ? Def : string;
|
|
162
186
|
/**
|
|
163
187
|
* Prompt function signature. Called when config requests prompting for this variable.
|
|
164
188
|
* Receives the variable's display name and the current default (from set/env/context or option default).
|
|
@@ -168,16 +192,17 @@ type PromptFn<T> = (name: string, defaultValue: T) => T | Promise<T>;
|
|
|
168
192
|
/**
|
|
169
193
|
* Options when creating a variable with {@link scenv}. All properties are optional.
|
|
170
194
|
* If you omit `key` and `env`, they are derived from `name`: e.g. "API URL" → key `api_url`, env `API_URL`.
|
|
195
|
+
* Input from set/env/context is always string; `default` and the variable's value type are the output type T.
|
|
171
196
|
*/
|
|
172
|
-
interface ScenvVariableOptions<T> {
|
|
197
|
+
interface ScenvVariableOptions<T = string> {
|
|
173
198
|
/** Internal key for --set, context files, and env. Default: name lowercased, spaces → underscores, non-alphanumeric stripped (e.g. "API URL" → "api_url"). */
|
|
174
199
|
key?: string;
|
|
175
200
|
/** Environment variable name (e.g. API_URL). Default: key uppercased, hyphens → underscores. */
|
|
176
201
|
env?: string;
|
|
177
|
-
/** Fallback when nothing is provided via --set, env, or context (and we're not prompting). */
|
|
202
|
+
/** Fallback when nothing is provided via --set, env, or context (and we're not prompting). Same type T as the variable's output (not parsed). */
|
|
178
203
|
default?: T;
|
|
179
|
-
/** Optional.
|
|
180
|
-
|
|
204
|
+
/** Optional. Parses the raw string (or default) into output type T. Receives raw value (typically string). Return { success: true, data } with the parsed value; variable type is inferred from data. */
|
|
205
|
+
parser?: (val: unknown) => ParserResult<T>;
|
|
181
206
|
/** Optional. Called when config says to prompt (e.g. prompt: "fallback" and no value found). Overrides callbacks.defaultPrompt for this variable. */
|
|
182
207
|
prompt?: PromptFn<T>;
|
|
183
208
|
}
|
|
@@ -197,8 +222,8 @@ interface GetOptions<T> {
|
|
|
197
222
|
*/
|
|
198
223
|
interface ScenvVariable<T> {
|
|
199
224
|
/**
|
|
200
|
-
* Resolve and return the value. Uses resolution order (set > env > context > default) and any prompt/
|
|
201
|
-
* @throws If no value is found and no default/prompt, or if
|
|
225
|
+
* Resolve and return the value. Uses resolution order (set > env > context > default) and any prompt/parser.
|
|
226
|
+
* @throws If no value is found and no default/prompt, or if parsing fails.
|
|
202
227
|
*/
|
|
203
228
|
get(options?: GetOptions<T>): Promise<T>;
|
|
204
229
|
/**
|
|
@@ -234,11 +259,10 @@ interface ScenvVariable<T> {
|
|
|
234
259
|
* the default option). The callback's return value is used as the value. When we don't prompt, we use the
|
|
235
260
|
* raw value if present, otherwise the default option, otherwise get() throws (no value).
|
|
236
261
|
*
|
|
237
|
-
* ##
|
|
262
|
+
* ## Parser
|
|
238
263
|
*
|
|
239
|
-
* If you pass a
|
|
240
|
-
* { success: true }
|
|
241
|
-
* Use it to coerce types (e.g. string to number) or enforce rules.
|
|
264
|
+
* If you pass a parser option, it is called with the raw value (typically string from set/env/context) or the default.
|
|
265
|
+
* Return { success: true, data } with the parsed output; the variable's type is inferred from data. On { success: false }, get() throws.
|
|
242
266
|
*
|
|
243
267
|
* ## save()
|
|
244
268
|
*
|
|
@@ -247,22 +271,27 @@ interface ScenvVariable<T> {
|
|
|
247
271
|
* When the user is prompted during get(), the value is always saved (to saveContextTo file if set, and always to in-memory)
|
|
248
272
|
* so a second get() on the same variable does not prompt again.
|
|
249
273
|
*
|
|
250
|
-
* @typeParam T -
|
|
274
|
+
* @typeParam T - Output (value) type of the variable, e.g. string, number, URL. Defaults to string.
|
|
251
275
|
* @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").
|
|
252
|
-
* @param options - Optional. key, env, default,
|
|
253
|
-
* @returns A {@link ScenvVariable} with get(), safeGet(), and save().
|
|
276
|
+
* @param options - Optional. key, env, default, parser, prompt. See {@link ScenvVariableOptions}.
|
|
277
|
+
* @returns A {@link ScenvVariable<T>} with get(), safeGet(), and save().
|
|
254
278
|
*
|
|
255
279
|
* @example
|
|
256
280
|
* const apiUrl = scenv("API URL", { default: "http://localhost:4000" });
|
|
257
281
|
* const url = await apiUrl.get();
|
|
282
|
+
*
|
|
283
|
+
* @example
|
|
284
|
+
* const port = scenv<number>("Port", { parser: parser(z.coerce.number()), default: 3000 });
|
|
285
|
+
* const n = await port.get(); // number
|
|
258
286
|
*/
|
|
259
|
-
declare function scenv<T>(name: string, options?: ScenvVariableOptions<T>): ScenvVariable<T>;
|
|
287
|
+
declare function scenv<T = string>(name: string, options?: ScenvVariableOptions<T>): ScenvVariable<T>;
|
|
260
288
|
|
|
261
289
|
/**
|
|
262
290
|
* Parses command-line arguments into a partial {@link ScenvConfig} suitable for {@link configure}.
|
|
263
291
|
* Typical use: `configure(parseScenvArgs(process.argv.slice(2)))`. Unrecognized flags are ignored.
|
|
264
292
|
*
|
|
265
293
|
* Supported flags:
|
|
294
|
+
* - `--root path` – Project root (where to find scenv.config.json and where new context files are saved). Relative paths are resolved from cwd.
|
|
266
295
|
* - `--context a,b,c` – Set context list (replace).
|
|
267
296
|
* - `--add-context x,y` – Add context names.
|
|
268
297
|
* - `--prompt always|never|fallback|no-env` – Prompt mode.
|
|
@@ -270,7 +299,7 @@ declare function scenv<T>(name: string, options?: ScenvVariableOptions<T>): Scen
|
|
|
270
299
|
* - `--ignore-context` – Set ignoreContext to true.
|
|
271
300
|
* - `--set key=value` or `--set=key=value` – Add to set overrides (multiple allowed).
|
|
272
301
|
* - `--save-context-to pathOrName` – saveContextTo (path or context name without .context.json).
|
|
273
|
-
* - `--
|
|
302
|
+
* - `--save-mode all|prompts-only` – When to write to saveContextTo during get(); default is all.
|
|
274
303
|
* - `--log-level level`, `--log level`, `--log=level` – logLevel.
|
|
275
304
|
*
|
|
276
305
|
* @param argv - Array of CLI arguments (e.g. process.argv.slice(2)).
|
|
@@ -278,4 +307,4 @@ declare function scenv<T>(name: string, options?: ScenvVariableOptions<T>): Scen
|
|
|
278
307
|
*/
|
|
279
308
|
declare function parseScenvArgs(argv: string[]): Partial<ScenvConfig>;
|
|
280
309
|
|
|
281
|
-
export { type DefaultPromptFn, type GetOptions, LOG_LEVELS, type LogLevel, type PromptMode, type ScenvCallbacks, type ScenvConfig, type ScenvVariable, configure, discoverContextPaths, getCallbacks, getContext, getContextWritePath, getInMemoryContext, getMergedContextValues, loadConfig, parseScenvArgs, resetConfig, resetInMemoryContext, resetLogState, scenv, setInMemoryContext };
|
|
310
|
+
export { type DefaultPromptFn, type GetOptions, type InferVariableType, LOG_LEVELS, type LogLevel, type ParserResult, type PromptMode, type SaveMode, type ScenvCallbacks, type ScenvConfig, type ScenvVariable, type ScenvVariableOptions, configure, discoverContextPaths, getCallbacks, getContext, getContextWritePath, getInMemoryContext, getMergedContextValues, loadConfig, parseScenvArgs, resetConfig, resetInMemoryContext, resetLogState, scenv, setInMemoryContext };
|
package/dist/index.js
CHANGED
|
@@ -8,12 +8,12 @@ function defaultPrompt(name, defaultValue) {
|
|
|
8
8
|
const defaultStr = defaultValue !== void 0 && defaultValue !== null ? String(defaultValue) : "";
|
|
9
9
|
const message = defaultStr ? `Enter ${name} [${defaultStr}]: ` : `Enter ${name}: `;
|
|
10
10
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
11
|
-
return new Promise((
|
|
11
|
+
return new Promise((resolve2, reject) => {
|
|
12
12
|
rl.question(message, (answer) => {
|
|
13
13
|
rl.close();
|
|
14
14
|
const trimmed = answer.trim();
|
|
15
15
|
const value = trimmed !== "" ? trimmed : defaultStr;
|
|
16
|
-
|
|
16
|
+
resolve2(value);
|
|
17
17
|
});
|
|
18
18
|
rl.on("error", (err) => {
|
|
19
19
|
rl.close();
|
|
@@ -32,7 +32,7 @@ var envKeyMap = {
|
|
|
32
32
|
SCENV_IGNORE_ENV: "ignoreEnv",
|
|
33
33
|
SCENV_IGNORE_CONTEXT: "ignoreContext",
|
|
34
34
|
SCENV_SAVE_CONTEXT_TO: "saveContextTo",
|
|
35
|
-
|
|
35
|
+
SCENV_SAVE_MODE: "saveMode",
|
|
36
36
|
SCENV_LOG_LEVEL: "logLevel"
|
|
37
37
|
};
|
|
38
38
|
var programmaticConfig = {};
|
|
@@ -68,8 +68,10 @@ function configFromEnv() {
|
|
|
68
68
|
out[configKey] = v;
|
|
69
69
|
} else if (configKey === "saveContextTo") {
|
|
70
70
|
out.saveContextTo = val;
|
|
71
|
-
} else if (configKey === "
|
|
72
|
-
|
|
71
|
+
} else if (configKey === "saveMode") {
|
|
72
|
+
const v = val.toLowerCase();
|
|
73
|
+
if (v === "all" || v === "prompts-only")
|
|
74
|
+
out[configKey] = v;
|
|
73
75
|
} else if (configKey === "logLevel") {
|
|
74
76
|
const v = val.toLowerCase();
|
|
75
77
|
if (LOG_LEVELS.includes(v)) out.logLevel = v;
|
|
@@ -109,8 +111,8 @@ function loadConfigFile(configDir) {
|
|
|
109
111
|
out.set = parsed.set;
|
|
110
112
|
if (typeof parsed.saveContextTo === "string")
|
|
111
113
|
out.saveContextTo = parsed.saveContextTo;
|
|
112
|
-
if (typeof parsed.
|
|
113
|
-
|
|
114
|
+
if (typeof parsed.saveMode === "string" && ["all", "prompts-only"].includes(parsed.saveMode))
|
|
115
|
+
out.saveMode = parsed.saveMode;
|
|
114
116
|
if (typeof parsed.root === "string") out.root = parsed.root;
|
|
115
117
|
if (typeof parsed.logLevel === "string" && LOG_LEVELS.includes(parsed.logLevel))
|
|
116
118
|
out.logLevel = parsed.logLevel;
|
|
@@ -198,7 +200,7 @@ var CONFIG_LOG_KEYS = [
|
|
|
198
200
|
"ignoreContext",
|
|
199
201
|
"set",
|
|
200
202
|
"saveContextTo",
|
|
201
|
-
"
|
|
203
|
+
"saveMode",
|
|
202
204
|
"logLevel"
|
|
203
205
|
];
|
|
204
206
|
function configForLog(config) {
|
|
@@ -219,7 +221,7 @@ function logConfigLoaded(config) {
|
|
|
219
221
|
if (config.ignoreEnv === true) parts.push("ignoreEnv=true");
|
|
220
222
|
if (config.ignoreContext === true) parts.push("ignoreContext=true");
|
|
221
223
|
if (config.saveContextTo !== void 0) parts.push("saveContextTo=" + config.saveContextTo);
|
|
222
|
-
if (config.
|
|
224
|
+
if (config.saveMode !== void 0) parts.push("saveMode=" + config.saveMode);
|
|
223
225
|
log("info", "config loaded", parts.join(" "));
|
|
224
226
|
}
|
|
225
227
|
if (levelNum >= LEVEL_NUM.debug) {
|
|
@@ -295,10 +297,23 @@ function discoverContextPaths(dir, found = /* @__PURE__ */ new Map()) {
|
|
|
295
297
|
return found;
|
|
296
298
|
}
|
|
297
299
|
function getContext(contextName, root) {
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
300
|
+
let filePath;
|
|
301
|
+
if (root !== void 0) {
|
|
302
|
+
const paths = discoverContextPaths(root);
|
|
303
|
+
filePath = paths.get(contextName);
|
|
304
|
+
} else {
|
|
305
|
+
const cwd = process.cwd();
|
|
306
|
+
const paths = discoverContextPaths(cwd);
|
|
307
|
+
filePath = paths.get(contextName);
|
|
308
|
+
if (!filePath) {
|
|
309
|
+
const config = loadConfig();
|
|
310
|
+
const projectRoot = config.root ?? cwd;
|
|
311
|
+
if (projectRoot !== cwd) {
|
|
312
|
+
const rootPaths = discoverContextPaths(projectRoot);
|
|
313
|
+
filePath = rootPaths.get(contextName);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
302
317
|
if (!filePath) {
|
|
303
318
|
log("trace", `getContext: context "${contextName}" not found`);
|
|
304
319
|
return {};
|
|
@@ -323,17 +338,9 @@ function getMergedContextValues() {
|
|
|
323
338
|
const config = loadConfig();
|
|
324
339
|
logConfigLoaded(config);
|
|
325
340
|
if (config.ignoreContext) return {};
|
|
326
|
-
const
|
|
327
|
-
const paths = discoverContextPaths(
|
|
341
|
+
const searchRoot = process.cwd();
|
|
342
|
+
const paths = discoverContextPaths(searchRoot);
|
|
328
343
|
const out = {};
|
|
329
|
-
if (config.saveContextTo) {
|
|
330
|
-
const savePath = resolveSaveContextPath(config.saveContextTo);
|
|
331
|
-
const saveCtx = getContextAtPath(savePath);
|
|
332
|
-
for (const [k, v] of Object.entries(saveCtx)) out[k] = v;
|
|
333
|
-
if (Object.keys(saveCtx).length > 0) {
|
|
334
|
-
log("debug", `saveContextTo "${config.saveContextTo}" loaded keys=${JSON.stringify(Object.keys(saveCtx))}`);
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
344
|
for (const contextName of config.context ?? []) {
|
|
338
345
|
const filePath = paths.get(contextName);
|
|
339
346
|
if (!filePath) {
|
|
@@ -365,32 +372,11 @@ function getContextWritePath(contextName) {
|
|
|
365
372
|
return contextName.endsWith(CONTEXT_SUFFIX) ? contextName : contextName + CONTEXT_SUFFIX;
|
|
366
373
|
}
|
|
367
374
|
const config = loadConfig();
|
|
368
|
-
const
|
|
369
|
-
const paths = discoverContextPaths(root);
|
|
375
|
+
const paths = discoverContextPaths(process.cwd());
|
|
370
376
|
const existing = paths.get(contextName);
|
|
371
377
|
if (existing) return existing;
|
|
372
|
-
const
|
|
373
|
-
return join2(
|
|
374
|
-
}
|
|
375
|
-
function resolveSaveContextPath(nameOrPath) {
|
|
376
|
-
if (isAbsolute(nameOrPath) || nameOrPath.includes(sep)) {
|
|
377
|
-
return nameOrPath.endsWith(CONTEXT_SUFFIX) ? nameOrPath : nameOrPath + CONTEXT_SUFFIX;
|
|
378
|
-
}
|
|
379
|
-
return getContextWritePath(nameOrPath);
|
|
380
|
-
}
|
|
381
|
-
function getContextAtPath(filePath) {
|
|
382
|
-
if (!existsSync2(filePath)) return {};
|
|
383
|
-
try {
|
|
384
|
-
const raw = readFileSync2(filePath, "utf-8");
|
|
385
|
-
const data = JSON.parse(raw);
|
|
386
|
-
const out = {};
|
|
387
|
-
for (const [k, v] of Object.entries(data)) {
|
|
388
|
-
if (typeof v === "string") out[k] = v;
|
|
389
|
-
}
|
|
390
|
-
return out;
|
|
391
|
-
} catch {
|
|
392
|
-
return {};
|
|
393
|
-
}
|
|
378
|
+
const projectRoot = config.root ?? process.cwd();
|
|
379
|
+
return join2(projectRoot, `${contextName}${CONTEXT_SUFFIX}`);
|
|
394
380
|
}
|
|
395
381
|
function writeToContext(contextName, key, value) {
|
|
396
382
|
const path = getContextWritePath(contextName);
|
|
@@ -458,7 +444,7 @@ function defaultKeyFromName(name) {
|
|
|
458
444
|
function defaultEnvFromKey(key) {
|
|
459
445
|
return key.toUpperCase().replace(/-/g, "_");
|
|
460
446
|
}
|
|
461
|
-
function
|
|
447
|
+
function normalizeParserResult(result) {
|
|
462
448
|
if (typeof result === "boolean") {
|
|
463
449
|
return result ? { success: true } : { success: false };
|
|
464
450
|
}
|
|
@@ -468,7 +454,7 @@ function normalizeValidatorResult(result) {
|
|
|
468
454
|
function scenv(name, options = {}) {
|
|
469
455
|
const key = options.key ?? defaultKeyFromName(name);
|
|
470
456
|
const envKey = options.env ?? defaultEnvFromKey(key);
|
|
471
|
-
const
|
|
457
|
+
const parserFn = options.parser;
|
|
472
458
|
const promptFn = options.prompt;
|
|
473
459
|
const defaultValue = options.default;
|
|
474
460
|
async function resolveRaw() {
|
|
@@ -555,10 +541,10 @@ function scenv(name, options = {}) {
|
|
|
555
541
|
log("info", `variable "${name}" (key=${key}) resolved from ${resolvedFrom}`);
|
|
556
542
|
return { value, raw, hadEnv, wasPrompted };
|
|
557
543
|
}
|
|
558
|
-
function
|
|
559
|
-
if (!
|
|
560
|
-
const result =
|
|
561
|
-
const normalized =
|
|
544
|
+
function parse(value) {
|
|
545
|
+
if (!parserFn) return { success: true, data: value };
|
|
546
|
+
const result = parserFn(value);
|
|
547
|
+
const normalized = normalizeParserResult(result);
|
|
562
548
|
if (normalized.success) {
|
|
563
549
|
const data = "data" in normalized && normalized.data !== void 0 ? normalized.data : value;
|
|
564
550
|
return { success: true, data };
|
|
@@ -570,17 +556,18 @@ function scenv(name, options = {}) {
|
|
|
570
556
|
}
|
|
571
557
|
async function get(options2) {
|
|
572
558
|
const { value, wasPrompted } = await getResolvedValue(options2);
|
|
573
|
-
const
|
|
574
|
-
if (!
|
|
575
|
-
const errMsg = `
|
|
559
|
+
const parsed = parse(value);
|
|
560
|
+
if (!parsed.success) {
|
|
561
|
+
const errMsg = `Parsing failed for "${name}": ${parsed.error ?? "unknown"}`;
|
|
576
562
|
log("error", errMsg);
|
|
577
563
|
throw new Error(errMsg);
|
|
578
564
|
}
|
|
579
|
-
const final =
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
565
|
+
const final = parsed.data;
|
|
566
|
+
const config = loadConfig();
|
|
567
|
+
if (wasPrompted) setInMemoryContext(key, String(final));
|
|
568
|
+
if (config.saveContextTo) {
|
|
569
|
+
const saveMode = config.saveMode ?? "all";
|
|
570
|
+
if (saveMode === "all" || wasPrompted) {
|
|
584
571
|
writeToContext(config.saveContextTo, key, String(final));
|
|
585
572
|
log("info", `Saved key=${key} to saveContextTo ${config.saveContextTo}`);
|
|
586
573
|
}
|
|
@@ -597,16 +584,16 @@ function scenv(name, options = {}) {
|
|
|
597
584
|
}
|
|
598
585
|
async function save(value) {
|
|
599
586
|
const toSave = value ?? (await getResolvedValue()).value;
|
|
600
|
-
const
|
|
601
|
-
if (!
|
|
602
|
-
const errMsg = `
|
|
587
|
+
const parsed = parse(toSave);
|
|
588
|
+
if (!parsed.success) {
|
|
589
|
+
const errMsg = `Parsing failed for "${name}": ${parsed.error ?? "unknown"}`;
|
|
603
590
|
log("error", errMsg);
|
|
604
591
|
throw new Error(errMsg);
|
|
605
592
|
}
|
|
606
593
|
const config = loadConfig();
|
|
607
|
-
setInMemoryContext(key, String(
|
|
594
|
+
setInMemoryContext(key, String(parsed.data));
|
|
608
595
|
if (config.saveContextTo) {
|
|
609
|
-
writeToContext(config.saveContextTo, key, String(
|
|
596
|
+
writeToContext(config.saveContextTo, key, String(parsed.data));
|
|
610
597
|
log("info", `Saved key=${key} to saveContextTo ${config.saveContextTo}`);
|
|
611
598
|
}
|
|
612
599
|
}
|
|
@@ -614,12 +601,15 @@ function scenv(name, options = {}) {
|
|
|
614
601
|
}
|
|
615
602
|
|
|
616
603
|
// src/cli-args.ts
|
|
604
|
+
import { resolve } from "path";
|
|
617
605
|
function parseScenvArgs(argv) {
|
|
618
606
|
const config = {};
|
|
619
607
|
let i = 0;
|
|
620
608
|
while (i < argv.length) {
|
|
621
609
|
const arg = argv[i];
|
|
622
|
-
if (arg === "--
|
|
610
|
+
if (arg === "--root" && argv[i + 1] !== void 0) {
|
|
611
|
+
config.root = resolve(argv[++i]);
|
|
612
|
+
} else if (arg === "--context" && argv[i + 1] !== void 0) {
|
|
623
613
|
config.context = argv[++i].split(",").map((s) => s.trim()).filter(Boolean);
|
|
624
614
|
} else if (arg === "--add-context" && argv[i + 1] !== void 0) {
|
|
625
615
|
config.addContext = argv[++i].split(",").map((s) => s.trim()).filter(Boolean);
|
|
@@ -641,8 +631,11 @@ function parseScenvArgs(argv) {
|
|
|
641
631
|
}
|
|
642
632
|
} else if (arg === "--save-context-to" && argv[i + 1] !== void 0) {
|
|
643
633
|
config.saveContextTo = argv[++i];
|
|
644
|
-
} else if (arg === "--
|
|
645
|
-
|
|
634
|
+
} else if (arg === "--save-mode" && argv[i + 1] !== void 0) {
|
|
635
|
+
const v = argv[++i].toLowerCase();
|
|
636
|
+
if (v === "all" || v === "prompts-only") {
|
|
637
|
+
config.saveMode = v;
|
|
638
|
+
}
|
|
646
639
|
} else if ((arg === "--log-level" || arg === "--log") && argv[i + 1] !== void 0) {
|
|
647
640
|
const v = argv[++i].toLowerCase();
|
|
648
641
|
if (LOG_LEVELS.includes(v)) {
|