scenv 0.5.1 → 0.6.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/dist/index.cjs +106 -40
- package/dist/index.d.cts +13 -10
- package/dist/index.d.ts +13 -10
- package/dist/index.js +107 -41
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -86,13 +86,14 @@ async function defaultAskContext(name, contextNames) {
|
|
|
86
86
|
var LOG_LEVELS = ["none", "trace", "debug", "info", "warn", "error"];
|
|
87
87
|
var CONFIG_FILENAME = "scenv.config.json";
|
|
88
88
|
var envKeyMap = {
|
|
89
|
-
SCENV_CONTEXT: "
|
|
90
|
-
|
|
89
|
+
SCENV_CONTEXT: "context",
|
|
90
|
+
SCENV_ADD_CONTEXT: "addContext",
|
|
91
91
|
SCENV_PROMPT: "prompt",
|
|
92
92
|
SCENV_IGNORE_ENV: "ignoreEnv",
|
|
93
93
|
SCENV_IGNORE_CONTEXT: "ignoreContext",
|
|
94
94
|
SCENV_SAVE_PROMPT: "shouldSavePrompt",
|
|
95
95
|
SCENV_SAVE_CONTEXT_TO: "saveContextTo",
|
|
96
|
+
SCENV_CONTEXT_DIR: "contextDir",
|
|
96
97
|
SCENV_LOG_LEVEL: "logLevel"
|
|
97
98
|
};
|
|
98
99
|
var programmaticConfig = {};
|
|
@@ -120,7 +121,7 @@ function configFromEnv() {
|
|
|
120
121
|
for (const [envKey, configKey] of Object.entries(envKeyMap)) {
|
|
121
122
|
const val = process.env[envKey];
|
|
122
123
|
if (val === void 0 || val === "") continue;
|
|
123
|
-
if (configKey === "
|
|
124
|
+
if (configKey === "context" || configKey === "addContext") {
|
|
124
125
|
out[configKey] = val.split(",").map((s) => s.trim()).filter(Boolean);
|
|
125
126
|
} else if (configKey === "ignoreEnv" || configKey === "ignoreContext") {
|
|
126
127
|
out[configKey] = val === "1" || val === "true" || val.toLowerCase() === "yes";
|
|
@@ -132,6 +133,8 @@ function configFromEnv() {
|
|
|
132
133
|
out[configKey] = v;
|
|
133
134
|
} else if (configKey === "saveContextTo") {
|
|
134
135
|
out.saveContextTo = val;
|
|
136
|
+
} else if (configKey === "contextDir") {
|
|
137
|
+
out.contextDir = val;
|
|
135
138
|
} else if (configKey === "logLevel") {
|
|
136
139
|
const v = val.toLowerCase();
|
|
137
140
|
if (LOG_LEVELS.includes(v)) out.logLevel = v;
|
|
@@ -146,12 +149,20 @@ function loadConfigFile(configDir) {
|
|
|
146
149
|
const raw = (0, import_node_fs.readFileSync)(path, "utf-8");
|
|
147
150
|
const parsed = JSON.parse(raw);
|
|
148
151
|
const out = {};
|
|
149
|
-
if (Array.isArray(parsed.
|
|
150
|
-
out.
|
|
152
|
+
if (Array.isArray(parsed.context))
|
|
153
|
+
out.context = parsed.context.filter(
|
|
151
154
|
(x) => typeof x === "string"
|
|
152
155
|
);
|
|
153
|
-
if (Array.isArray(parsed.
|
|
154
|
-
out.
|
|
156
|
+
else if (Array.isArray(parsed.contexts))
|
|
157
|
+
out.context = parsed.contexts.filter(
|
|
158
|
+
(x) => typeof x === "string"
|
|
159
|
+
);
|
|
160
|
+
if (Array.isArray(parsed.addContext))
|
|
161
|
+
out.addContext = parsed.addContext.filter(
|
|
162
|
+
(x) => typeof x === "string"
|
|
163
|
+
);
|
|
164
|
+
else if (Array.isArray(parsed.addContexts))
|
|
165
|
+
out.addContext = parsed.addContexts.filter(
|
|
155
166
|
(x) => typeof x === "string"
|
|
156
167
|
);
|
|
157
168
|
if (typeof parsed.prompt === "string" && ["always", "never", "fallback", "no-env"].includes(parsed.prompt))
|
|
@@ -165,6 +176,8 @@ function loadConfigFile(configDir) {
|
|
|
165
176
|
out.shouldSavePrompt = parsed.shouldSavePrompt ?? parsed.savePrompt;
|
|
166
177
|
if (typeof parsed.saveContextTo === "string")
|
|
167
178
|
out.saveContextTo = parsed.saveContextTo;
|
|
179
|
+
if (typeof parsed.contextDir === "string") out.contextDir = parsed.contextDir;
|
|
180
|
+
else if (typeof parsed.contextsDir === "string") out.contextDir = parsed.contextsDir;
|
|
168
181
|
if (typeof parsed.root === "string") out.root = parsed.root;
|
|
169
182
|
if (typeof parsed.logLevel === "string" && LOG_LEVELS.includes(parsed.logLevel))
|
|
170
183
|
out.logLevel = parsed.logLevel;
|
|
@@ -180,11 +193,11 @@ function loadConfigFile(configDir) {
|
|
|
180
193
|
}
|
|
181
194
|
}
|
|
182
195
|
function mergeContexts(fileConfig, envConfig, progConfig) {
|
|
183
|
-
const fromFile = fileConfig.
|
|
184
|
-
const fromEnvAdd = envConfig.
|
|
185
|
-
const fromEnvReplace = envConfig.
|
|
186
|
-
const fromProgAdd = progConfig.
|
|
187
|
-
const fromProgReplace = progConfig.
|
|
196
|
+
const fromFile = fileConfig.context ?? fileConfig.addContext ?? [];
|
|
197
|
+
const fromEnvAdd = envConfig.addContext ?? [];
|
|
198
|
+
const fromEnvReplace = envConfig.context;
|
|
199
|
+
const fromProgAdd = progConfig.addContext ?? [];
|
|
200
|
+
const fromProgReplace = progConfig.context;
|
|
188
201
|
const replace = fromProgReplace ?? fromEnvReplace;
|
|
189
202
|
if (replace !== void 0) return replace;
|
|
190
203
|
const base = [...fromFile];
|
|
@@ -208,8 +221,8 @@ function loadConfig(root) {
|
|
|
208
221
|
...envConfig,
|
|
209
222
|
...programmaticConfig
|
|
210
223
|
};
|
|
211
|
-
merged.
|
|
212
|
-
delete merged.
|
|
224
|
+
merged.context = mergeContexts(fileConfig, envConfig, programmaticConfig);
|
|
225
|
+
delete merged.addContext;
|
|
213
226
|
if (configDir && !merged.root) merged.root = configDir;
|
|
214
227
|
else if (!merged.root) merged.root = startDir;
|
|
215
228
|
return merged;
|
|
@@ -244,16 +257,43 @@ var configLoadedLogged = false;
|
|
|
244
257
|
function resetLogState() {
|
|
245
258
|
configLoadedLogged = false;
|
|
246
259
|
}
|
|
260
|
+
var CONFIG_LOG_KEYS = [
|
|
261
|
+
"root",
|
|
262
|
+
"context",
|
|
263
|
+
"prompt",
|
|
264
|
+
"ignoreEnv",
|
|
265
|
+
"ignoreContext",
|
|
266
|
+
"set",
|
|
267
|
+
"shouldSavePrompt",
|
|
268
|
+
"saveContextTo",
|
|
269
|
+
"contextDir",
|
|
270
|
+
"logLevel"
|
|
271
|
+
];
|
|
272
|
+
function configForLog(config) {
|
|
273
|
+
return Object.fromEntries(
|
|
274
|
+
CONFIG_LOG_KEYS.filter((k) => config[k] !== void 0).map((k) => [k, config[k]])
|
|
275
|
+
);
|
|
276
|
+
}
|
|
247
277
|
function logConfigLoaded(config) {
|
|
248
278
|
if (configLoadedLogged) return;
|
|
249
|
-
if (getLevelNum() < LEVEL_NUM.info) return;
|
|
250
279
|
configLoadedLogged = true;
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
280
|
+
const levelNum = LEVEL_NUM[config.logLevel ?? "none"];
|
|
281
|
+
if (levelNum >= LEVEL_NUM.info) {
|
|
282
|
+
const parts = [
|
|
283
|
+
"root=" + (config.root ?? "(cwd)"),
|
|
284
|
+
"context=" + JSON.stringify(config.context ?? [])
|
|
285
|
+
];
|
|
286
|
+
if (config.prompt !== void 0) parts.push("prompt=" + config.prompt);
|
|
287
|
+
if (config.ignoreEnv === true) parts.push("ignoreEnv=true");
|
|
288
|
+
if (config.ignoreContext === true) parts.push("ignoreContext=true");
|
|
289
|
+
if (config.saveContextTo !== void 0) parts.push("saveContextTo=" + config.saveContextTo);
|
|
290
|
+
if (config.contextDir !== void 0) parts.push("contextDir=" + config.contextDir);
|
|
291
|
+
if (config.shouldSavePrompt !== void 0) parts.push("shouldSavePrompt=" + config.shouldSavePrompt);
|
|
292
|
+
log("info", "config loaded", parts.join(" "));
|
|
293
|
+
}
|
|
294
|
+
if (levelNum >= LEVEL_NUM.debug) {
|
|
295
|
+
log("debug", "config (full)", JSON.stringify(configForLog(config)));
|
|
296
|
+
}
|
|
257
297
|
}
|
|
258
298
|
function log(level, msg, ...args) {
|
|
259
299
|
const configured = getLevelNum();
|
|
@@ -338,7 +378,7 @@ function getMergedContextValues() {
|
|
|
338
378
|
const root = config.root ?? process.cwd();
|
|
339
379
|
const paths = discoverContextPaths(root);
|
|
340
380
|
const out = {};
|
|
341
|
-
for (const contextName of config.
|
|
381
|
+
for (const contextName of config.context ?? []) {
|
|
342
382
|
const filePath = paths.get(contextName);
|
|
343
383
|
if (!filePath) {
|
|
344
384
|
log("warn", `context "${contextName}" not found (no *.context.json)`);
|
|
@@ -370,7 +410,8 @@ function getContextWritePath(contextName) {
|
|
|
370
410
|
const paths = discoverContextPaths(root);
|
|
371
411
|
const existing = paths.get(contextName);
|
|
372
412
|
if (existing) return existing;
|
|
373
|
-
|
|
413
|
+
const saveDir = config.contextDir ? (0, import_node_path2.isAbsolute)(config.contextDir) ? config.contextDir : (0, import_node_path2.join)(root, config.contextDir) : root;
|
|
414
|
+
return (0, import_node_path2.join)(saveDir, `${contextName}${CONTEXT_SUFFIX}`);
|
|
374
415
|
}
|
|
375
416
|
function writeToContext(contextName, key, value) {
|
|
376
417
|
const path = getContextWritePath(contextName);
|
|
@@ -388,26 +429,49 @@ function writeToContext(contextName, key, value) {
|
|
|
388
429
|
}
|
|
389
430
|
|
|
390
431
|
// src/variable.ts
|
|
391
|
-
var
|
|
432
|
+
var CONTEXT_REF_FULL_REGEX = /^@([^:]+):(.+)$/;
|
|
433
|
+
var CONTEXT_REF_SHORT_REGEX = /^@([^:]+)$/;
|
|
434
|
+
var ENV_REF_REGEX = /\$([A-Za-z_][A-Za-z0-9_]*|\{[A-Za-z_][A-Za-z0-9_]*\})/g;
|
|
392
435
|
var MAX_CONTEXT_REF_DEPTH = 10;
|
|
393
|
-
function
|
|
436
|
+
function expandEnvReferences(str) {
|
|
437
|
+
return str.replace(ENV_REF_REGEX, (_, name) => {
|
|
438
|
+
const key = name.startsWith("{") ? name.slice(1, -1) : name;
|
|
439
|
+
return process.env[key] ?? "";
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
function resolveContextReference(raw, currentKey, depth = 0) {
|
|
394
443
|
if (depth >= MAX_CONTEXT_REF_DEPTH) {
|
|
395
444
|
throw new Error(
|
|
396
445
|
`Context reference resolution exceeded max depth (${MAX_CONTEXT_REF_DEPTH}): possible circular reference`
|
|
397
446
|
);
|
|
398
447
|
}
|
|
399
|
-
const
|
|
400
|
-
|
|
401
|
-
|
|
448
|
+
const afterEnv = expandEnvReferences(raw);
|
|
449
|
+
let contextName;
|
|
450
|
+
let refKey;
|
|
451
|
+
const fullMatch = afterEnv.match(CONTEXT_REF_FULL_REGEX);
|
|
452
|
+
if (fullMatch) {
|
|
453
|
+
[, contextName, refKey] = fullMatch;
|
|
454
|
+
} else {
|
|
455
|
+
const shortMatch = afterEnv.match(CONTEXT_REF_SHORT_REGEX);
|
|
456
|
+
if (!shortMatch) return afterEnv;
|
|
457
|
+
contextName = shortMatch[1];
|
|
458
|
+
if (currentKey === void 0) {
|
|
459
|
+
throw new Error(
|
|
460
|
+
`Context reference @${contextName} (short form) cannot be resolved without a variable key. Use @${contextName}:<key> to specify a key.`
|
|
461
|
+
);
|
|
462
|
+
}
|
|
463
|
+
refKey = currentKey;
|
|
464
|
+
}
|
|
402
465
|
const ctx = getContext(contextName);
|
|
403
466
|
const resolved = ctx[refKey];
|
|
404
467
|
if (resolved === void 0) {
|
|
405
468
|
const hasContext = Object.keys(ctx).length > 0;
|
|
406
|
-
const
|
|
469
|
+
const displayRef = refKey === currentKey ? `@${contextName}` : `@${contextName}:${refKey}`;
|
|
470
|
+
const msg = hasContext ? `Context reference ${displayRef} could not be resolved: key "${refKey}" is not defined in context "${contextName}".` : `Context reference ${displayRef} could not be resolved: context "${contextName}" not found (no ${contextName}.context.json).`;
|
|
407
471
|
log("error", msg);
|
|
408
472
|
throw new Error(msg);
|
|
409
473
|
}
|
|
410
|
-
return resolveContextReference(resolved, depth + 1);
|
|
474
|
+
return resolveContextReference(resolved, currentKey, depth + 1);
|
|
411
475
|
}
|
|
412
476
|
function defaultKeyFromName(name) {
|
|
413
477
|
return name.toLowerCase().replace(/\s+/g, "_").replace(/[^a-z0-9_]/gi, "");
|
|
@@ -433,7 +497,7 @@ function scenv(name, options = {}) {
|
|
|
433
497
|
log("trace", `resolveRaw: checking set for key=${key}`);
|
|
434
498
|
if (config.set?.[key] !== void 0) {
|
|
435
499
|
log("trace", `resolveRaw: set hit key=${key}`);
|
|
436
|
-
const raw = resolveContextReference(config.set[key]);
|
|
500
|
+
const raw = resolveContextReference(config.set[key], key);
|
|
437
501
|
return { raw, source: "set" };
|
|
438
502
|
}
|
|
439
503
|
if (!config.ignoreEnv) {
|
|
@@ -441,7 +505,7 @@ function scenv(name, options = {}) {
|
|
|
441
505
|
const envVal = process.env[envKey];
|
|
442
506
|
if (envVal !== void 0 && envVal !== "") {
|
|
443
507
|
log("trace", "resolveRaw: env hit");
|
|
444
|
-
const raw = resolveContextReference(envVal);
|
|
508
|
+
const raw = resolveContextReference(envVal, key);
|
|
445
509
|
return { raw, source: "env" };
|
|
446
510
|
}
|
|
447
511
|
}
|
|
@@ -450,7 +514,7 @@ function scenv(name, options = {}) {
|
|
|
450
514
|
const ctx = getMergedContextValues();
|
|
451
515
|
if (ctx[key] !== void 0) {
|
|
452
516
|
log("trace", `resolveRaw: context hit key=${key}`);
|
|
453
|
-
const raw = resolveContextReference(ctx[key]);
|
|
517
|
+
const raw = resolveContextReference(ctx[key], key);
|
|
454
518
|
return { raw, source: "context" };
|
|
455
519
|
}
|
|
456
520
|
}
|
|
@@ -477,7 +541,7 @@ function scenv(name, options = {}) {
|
|
|
477
541
|
`prompt decision key=${key} prompt=${config.prompt ?? "fallback"} hadValue=${hadValue} hadEnv=${hadEnv} -> ${doPrompt ? "prompt" : "no prompt"}`
|
|
478
542
|
);
|
|
479
543
|
const effectiveDefault = overrides?.default !== void 0 ? overrides.default : defaultValue;
|
|
480
|
-
const resolvedDefault = effectiveDefault === void 0 ? void 0 : typeof effectiveDefault === "string" ? resolveContextReference(effectiveDefault) : effectiveDefault;
|
|
544
|
+
const resolvedDefault = effectiveDefault === void 0 ? void 0 : typeof effectiveDefault === "string" ? resolveContextReference(effectiveDefault, key) : effectiveDefault;
|
|
481
545
|
let wasPrompted = false;
|
|
482
546
|
let value;
|
|
483
547
|
let resolvedFrom;
|
|
@@ -491,7 +555,7 @@ function scenv(name, options = {}) {
|
|
|
491
555
|
}
|
|
492
556
|
const defaultForPrompt = raw !== void 0 ? raw : resolvedDefault;
|
|
493
557
|
let promptedValue = await Promise.resolve(fn(name, defaultForPrompt));
|
|
494
|
-
value = typeof promptedValue === "string" ? resolveContextReference(promptedValue) : promptedValue;
|
|
558
|
+
value = typeof promptedValue === "string" ? resolveContextReference(promptedValue, key) : promptedValue;
|
|
495
559
|
wasPrompted = true;
|
|
496
560
|
resolvedFrom = "prompt";
|
|
497
561
|
} else if (raw !== void 0) {
|
|
@@ -546,7 +610,7 @@ function scenv(name, options = {}) {
|
|
|
546
610
|
}
|
|
547
611
|
if (!doSave) return final;
|
|
548
612
|
const callbacks = getCallbacks();
|
|
549
|
-
const contextNames = config.
|
|
613
|
+
const contextNames = config.context ?? [];
|
|
550
614
|
let ctxToSave;
|
|
551
615
|
if (config.saveContextTo === "ask") {
|
|
552
616
|
if (typeof callbacks.onAskContext !== "function") {
|
|
@@ -590,10 +654,10 @@ function scenv(name, options = {}) {
|
|
|
590
654
|
}
|
|
591
655
|
contextName = await callbacks.onAskContext(
|
|
592
656
|
name,
|
|
593
|
-
config.
|
|
657
|
+
config.context ?? []
|
|
594
658
|
);
|
|
595
659
|
}
|
|
596
|
-
if (!contextName) contextName = config.
|
|
660
|
+
if (!contextName) contextName = config.context?.[0] ?? "default";
|
|
597
661
|
writeToContext(contextName, key, String(validated.data));
|
|
598
662
|
log("info", `Saved key=${key} to context ${contextName}`);
|
|
599
663
|
}
|
|
@@ -607,9 +671,9 @@ function parseScenvArgs(argv) {
|
|
|
607
671
|
while (i < argv.length) {
|
|
608
672
|
const arg = argv[i];
|
|
609
673
|
if (arg === "--context" && argv[i + 1] !== void 0) {
|
|
610
|
-
config.
|
|
674
|
+
config.context = argv[++i].split(",").map((s) => s.trim()).filter(Boolean);
|
|
611
675
|
} else if (arg === "--add-context" && argv[i + 1] !== void 0) {
|
|
612
|
-
config.
|
|
676
|
+
config.addContext = argv[++i].split(",").map((s) => s.trim()).filter(Boolean);
|
|
613
677
|
} else if (arg === "--prompt" && argv[i + 1] !== void 0) {
|
|
614
678
|
const v = argv[++i].toLowerCase();
|
|
615
679
|
if (["always", "never", "fallback", "no-env"].includes(v)) {
|
|
@@ -633,6 +697,8 @@ function parseScenvArgs(argv) {
|
|
|
633
697
|
}
|
|
634
698
|
} else if (arg === "--save-context-to" && argv[i + 1] !== void 0) {
|
|
635
699
|
config.saveContextTo = argv[++i];
|
|
700
|
+
} else if (arg === "--context-dir" && argv[i + 1] !== void 0) {
|
|
701
|
+
config.contextDir = argv[++i];
|
|
636
702
|
} else if ((arg === "--log-level" || arg === "--log") && argv[i + 1] !== void 0) {
|
|
637
703
|
const v = argv[++i].toLowerCase();
|
|
638
704
|
if (LOG_LEVELS.includes(v)) {
|
package/dist/index.d.cts
CHANGED
|
@@ -27,9 +27,9 @@ type LogLevel = (typeof LOG_LEVELS)[number];
|
|
|
27
27
|
*/
|
|
28
28
|
interface ScenvConfig {
|
|
29
29
|
/** Replace the context list entirely (CLI: `--context a,b,c`). Loaded in order; later overwrites earlier for same key. */
|
|
30
|
-
|
|
31
|
-
/** Merge these context names with existing (CLI: `--add-context a,b,c`). Ignored if `
|
|
32
|
-
|
|
30
|
+
context?: string[];
|
|
31
|
+
/** Merge these context names with existing (CLI: `--add-context a,b,c`). Ignored if `context` is set in the same layer. */
|
|
32
|
+
addContext?: string[];
|
|
33
33
|
/** When to prompt for a variable value. See {@link PromptMode}. Default is `"fallback"`. */
|
|
34
34
|
prompt?: PromptMode;
|
|
35
35
|
/** If true, environment variables are not used during resolution. */
|
|
@@ -42,6 +42,8 @@ interface ScenvConfig {
|
|
|
42
42
|
shouldSavePrompt?: SavePromptMode;
|
|
43
43
|
/** Target context for saving: a context name, or `"ask"` to use {@link ScenvCallbacks.onAskContext}. */
|
|
44
44
|
saveContextTo?: "ask" | string;
|
|
45
|
+
/** Directory to save context files to when the context is not already discovered. Relative to root unless absolute. If unset, new context files are saved under root. */
|
|
46
|
+
contextDir?: string;
|
|
45
47
|
/** Root directory for config file search and context discovery. Default is cwd or the directory containing scenv.config.json. */
|
|
46
48
|
root?: string;
|
|
47
49
|
/** Logging level. Default is `"none"`. Messages go to stderr. */
|
|
@@ -79,7 +81,7 @@ declare function getCallbacks(): ScenvCallbacks;
|
|
|
79
81
|
* Use this to read the effective config (e.g. for logging or conditional logic).
|
|
80
82
|
*
|
|
81
83
|
* @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 `
|
|
84
|
+
* @returns The merged {@link ScenvConfig} with at least `root` and `context` defined.
|
|
83
85
|
*/
|
|
84
86
|
declare function loadConfig(root?: string): ScenvConfig;
|
|
85
87
|
/**
|
|
@@ -119,7 +121,7 @@ declare function resetLogState(): void;
|
|
|
119
121
|
declare function discoverContextPaths(dir: string, found?: Map<string, string>): Map<string, string>;
|
|
120
122
|
/**
|
|
121
123
|
* Loads key-value pairs from a single context file. Used when resolving @context:key references.
|
|
122
|
-
* Does not depend on config.
|
|
124
|
+
* Does not depend on config.context or ignoreContext; the context file is read if it exists
|
|
123
125
|
* under the config root.
|
|
124
126
|
*
|
|
125
127
|
* @param contextName - Name of the context (e.g. "prod", "dev") — file is contextName.context.json.
|
|
@@ -128,12 +130,12 @@ declare function discoverContextPaths(dir: string, found?: Map<string, string>):
|
|
|
128
130
|
*/
|
|
129
131
|
declare function getContext(contextName: string, root?: string): Record<string, string>;
|
|
130
132
|
/**
|
|
131
|
-
* Loads and merges context values from the current config. Respects {@link ScenvConfig.
|
|
133
|
+
* Loads and merges context values from the current config. Respects {@link ScenvConfig.context}
|
|
132
134
|
* order and {@link ScenvConfig.ignoreContext}. Each context file is a JSON object of string
|
|
133
135
|
* key-value pairs; later contexts overwrite earlier for the same key. Used during variable
|
|
134
136
|
* resolution (set > env > context > default).
|
|
135
137
|
*
|
|
136
|
-
* @returns A flat record of key → string value. Empty if ignoreContext is true or no
|
|
138
|
+
* @returns A flat record of key → string value. Empty if ignoreContext is true or no context loaded.
|
|
137
139
|
*/
|
|
138
140
|
declare function getMergedContextValues(): Record<string, string>;
|
|
139
141
|
|
|
@@ -218,7 +220,7 @@ interface ScenvVariable<T> {
|
|
|
218
220
|
* get() first looks for a raw value in this order, stopping at the first found:
|
|
219
221
|
* - Set overrides from config (e.g. from --set key=value or configure({ set: { key: "value" } }))
|
|
220
222
|
* - Environment variable (e.g. API_URL for key "api_url")
|
|
221
|
-
* - Context files (merged key-value from the
|
|
223
|
+
* - Context files (merged key-value from the context list in config, e.g. dev.context.json)
|
|
222
224
|
*
|
|
223
225
|
* If config says to prompt (see prompt mode in config: "always", "fallback", "no-env"), the prompt callback
|
|
224
226
|
* may run. When it runs, it receives the variable name and a suggested value (the raw value if any, otherwise
|
|
@@ -254,14 +256,15 @@ declare function scenv<T>(name: string, options?: ScenvVariableOptions<T>): Scen
|
|
|
254
256
|
* Typical use: `configure(parseScenvArgs(process.argv.slice(2)))`. Unrecognized flags are ignored.
|
|
255
257
|
*
|
|
256
258
|
* Supported flags:
|
|
257
|
-
* - `--context a,b,c` – Set
|
|
258
|
-
* - `--add-context x,y` – Add
|
|
259
|
+
* - `--context a,b,c` – Set context list (replace).
|
|
260
|
+
* - `--add-context x,y` – Add context names.
|
|
259
261
|
* - `--prompt always|never|fallback|no-env` – Prompt mode.
|
|
260
262
|
* - `--ignore-env` – Set ignoreEnv to true.
|
|
261
263
|
* - `--ignore-context` – Set ignoreContext to true.
|
|
262
264
|
* - `--set key=value` or `--set=key=value` – Add to set overrides (multiple allowed).
|
|
263
265
|
* - `--save-prompt always|never|ask` – shouldSavePrompt.
|
|
264
266
|
* - `--save-context-to name` – saveContextTo.
|
|
267
|
+
* - `--context-dir path` – contextDir (directory to save context files to by default).
|
|
265
268
|
* - `--log-level level`, `--log level`, `--log=level` – logLevel.
|
|
266
269
|
*
|
|
267
270
|
* @param argv - Array of CLI arguments (e.g. process.argv.slice(2)).
|
package/dist/index.d.ts
CHANGED
|
@@ -27,9 +27,9 @@ type LogLevel = (typeof LOG_LEVELS)[number];
|
|
|
27
27
|
*/
|
|
28
28
|
interface ScenvConfig {
|
|
29
29
|
/** Replace the context list entirely (CLI: `--context a,b,c`). Loaded in order; later overwrites earlier for same key. */
|
|
30
|
-
|
|
31
|
-
/** Merge these context names with existing (CLI: `--add-context a,b,c`). Ignored if `
|
|
32
|
-
|
|
30
|
+
context?: string[];
|
|
31
|
+
/** Merge these context names with existing (CLI: `--add-context a,b,c`). Ignored if `context` is set in the same layer. */
|
|
32
|
+
addContext?: string[];
|
|
33
33
|
/** When to prompt for a variable value. See {@link PromptMode}. Default is `"fallback"`. */
|
|
34
34
|
prompt?: PromptMode;
|
|
35
35
|
/** If true, environment variables are not used during resolution. */
|
|
@@ -42,6 +42,8 @@ interface ScenvConfig {
|
|
|
42
42
|
shouldSavePrompt?: SavePromptMode;
|
|
43
43
|
/** Target context for saving: a context name, or `"ask"` to use {@link ScenvCallbacks.onAskContext}. */
|
|
44
44
|
saveContextTo?: "ask" | string;
|
|
45
|
+
/** Directory to save context files to when the context is not already discovered. Relative to root unless absolute. If unset, new context files are saved under root. */
|
|
46
|
+
contextDir?: string;
|
|
45
47
|
/** Root directory for config file search and context discovery. Default is cwd or the directory containing scenv.config.json. */
|
|
46
48
|
root?: string;
|
|
47
49
|
/** Logging level. Default is `"none"`. Messages go to stderr. */
|
|
@@ -79,7 +81,7 @@ declare function getCallbacks(): ScenvCallbacks;
|
|
|
79
81
|
* Use this to read the effective config (e.g. for logging or conditional logic).
|
|
80
82
|
*
|
|
81
83
|
* @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 `
|
|
84
|
+
* @returns The merged {@link ScenvConfig} with at least `root` and `context` defined.
|
|
83
85
|
*/
|
|
84
86
|
declare function loadConfig(root?: string): ScenvConfig;
|
|
85
87
|
/**
|
|
@@ -119,7 +121,7 @@ declare function resetLogState(): void;
|
|
|
119
121
|
declare function discoverContextPaths(dir: string, found?: Map<string, string>): Map<string, string>;
|
|
120
122
|
/**
|
|
121
123
|
* Loads key-value pairs from a single context file. Used when resolving @context:key references.
|
|
122
|
-
* Does not depend on config.
|
|
124
|
+
* Does not depend on config.context or ignoreContext; the context file is read if it exists
|
|
123
125
|
* under the config root.
|
|
124
126
|
*
|
|
125
127
|
* @param contextName - Name of the context (e.g. "prod", "dev") — file is contextName.context.json.
|
|
@@ -128,12 +130,12 @@ declare function discoverContextPaths(dir: string, found?: Map<string, string>):
|
|
|
128
130
|
*/
|
|
129
131
|
declare function getContext(contextName: string, root?: string): Record<string, string>;
|
|
130
132
|
/**
|
|
131
|
-
* Loads and merges context values from the current config. Respects {@link ScenvConfig.
|
|
133
|
+
* Loads and merges context values from the current config. Respects {@link ScenvConfig.context}
|
|
132
134
|
* order and {@link ScenvConfig.ignoreContext}. Each context file is a JSON object of string
|
|
133
135
|
* key-value pairs; later contexts overwrite earlier for the same key. Used during variable
|
|
134
136
|
* resolution (set > env > context > default).
|
|
135
137
|
*
|
|
136
|
-
* @returns A flat record of key → string value. Empty if ignoreContext is true or no
|
|
138
|
+
* @returns A flat record of key → string value. Empty if ignoreContext is true or no context loaded.
|
|
137
139
|
*/
|
|
138
140
|
declare function getMergedContextValues(): Record<string, string>;
|
|
139
141
|
|
|
@@ -218,7 +220,7 @@ interface ScenvVariable<T> {
|
|
|
218
220
|
* get() first looks for a raw value in this order, stopping at the first found:
|
|
219
221
|
* - Set overrides from config (e.g. from --set key=value or configure({ set: { key: "value" } }))
|
|
220
222
|
* - Environment variable (e.g. API_URL for key "api_url")
|
|
221
|
-
* - Context files (merged key-value from the
|
|
223
|
+
* - Context files (merged key-value from the context list in config, e.g. dev.context.json)
|
|
222
224
|
*
|
|
223
225
|
* If config says to prompt (see prompt mode in config: "always", "fallback", "no-env"), the prompt callback
|
|
224
226
|
* may run. When it runs, it receives the variable name and a suggested value (the raw value if any, otherwise
|
|
@@ -254,14 +256,15 @@ declare function scenv<T>(name: string, options?: ScenvVariableOptions<T>): Scen
|
|
|
254
256
|
* Typical use: `configure(parseScenvArgs(process.argv.slice(2)))`. Unrecognized flags are ignored.
|
|
255
257
|
*
|
|
256
258
|
* Supported flags:
|
|
257
|
-
* - `--context a,b,c` – Set
|
|
258
|
-
* - `--add-context x,y` – Add
|
|
259
|
+
* - `--context a,b,c` – Set context list (replace).
|
|
260
|
+
* - `--add-context x,y` – Add context names.
|
|
259
261
|
* - `--prompt always|never|fallback|no-env` – Prompt mode.
|
|
260
262
|
* - `--ignore-env` – Set ignoreEnv to true.
|
|
261
263
|
* - `--ignore-context` – Set ignoreContext to true.
|
|
262
264
|
* - `--set key=value` or `--set=key=value` – Add to set overrides (multiple allowed).
|
|
263
265
|
* - `--save-prompt always|never|ask` – shouldSavePrompt.
|
|
264
266
|
* - `--save-context-to name` – saveContextTo.
|
|
267
|
+
* - `--context-dir path` – contextDir (directory to save context files to by default).
|
|
265
268
|
* - `--log-level level`, `--log level`, `--log=level` – logLevel.
|
|
266
269
|
*
|
|
267
270
|
* @param argv - Array of CLI arguments (e.g. process.argv.slice(2)).
|
package/dist/index.js
CHANGED
|
@@ -50,13 +50,14 @@ async function defaultAskContext(name, contextNames) {
|
|
|
50
50
|
var LOG_LEVELS = ["none", "trace", "debug", "info", "warn", "error"];
|
|
51
51
|
var CONFIG_FILENAME = "scenv.config.json";
|
|
52
52
|
var envKeyMap = {
|
|
53
|
-
SCENV_CONTEXT: "
|
|
54
|
-
|
|
53
|
+
SCENV_CONTEXT: "context",
|
|
54
|
+
SCENV_ADD_CONTEXT: "addContext",
|
|
55
55
|
SCENV_PROMPT: "prompt",
|
|
56
56
|
SCENV_IGNORE_ENV: "ignoreEnv",
|
|
57
57
|
SCENV_IGNORE_CONTEXT: "ignoreContext",
|
|
58
58
|
SCENV_SAVE_PROMPT: "shouldSavePrompt",
|
|
59
59
|
SCENV_SAVE_CONTEXT_TO: "saveContextTo",
|
|
60
|
+
SCENV_CONTEXT_DIR: "contextDir",
|
|
60
61
|
SCENV_LOG_LEVEL: "logLevel"
|
|
61
62
|
};
|
|
62
63
|
var programmaticConfig = {};
|
|
@@ -84,7 +85,7 @@ function configFromEnv() {
|
|
|
84
85
|
for (const [envKey, configKey] of Object.entries(envKeyMap)) {
|
|
85
86
|
const val = process.env[envKey];
|
|
86
87
|
if (val === void 0 || val === "") continue;
|
|
87
|
-
if (configKey === "
|
|
88
|
+
if (configKey === "context" || configKey === "addContext") {
|
|
88
89
|
out[configKey] = val.split(",").map((s) => s.trim()).filter(Boolean);
|
|
89
90
|
} else if (configKey === "ignoreEnv" || configKey === "ignoreContext") {
|
|
90
91
|
out[configKey] = val === "1" || val === "true" || val.toLowerCase() === "yes";
|
|
@@ -96,6 +97,8 @@ function configFromEnv() {
|
|
|
96
97
|
out[configKey] = v;
|
|
97
98
|
} else if (configKey === "saveContextTo") {
|
|
98
99
|
out.saveContextTo = val;
|
|
100
|
+
} else if (configKey === "contextDir") {
|
|
101
|
+
out.contextDir = val;
|
|
99
102
|
} else if (configKey === "logLevel") {
|
|
100
103
|
const v = val.toLowerCase();
|
|
101
104
|
if (LOG_LEVELS.includes(v)) out.logLevel = v;
|
|
@@ -110,12 +113,20 @@ function loadConfigFile(configDir) {
|
|
|
110
113
|
const raw = readFileSync(path, "utf-8");
|
|
111
114
|
const parsed = JSON.parse(raw);
|
|
112
115
|
const out = {};
|
|
113
|
-
if (Array.isArray(parsed.
|
|
114
|
-
out.
|
|
116
|
+
if (Array.isArray(parsed.context))
|
|
117
|
+
out.context = parsed.context.filter(
|
|
115
118
|
(x) => typeof x === "string"
|
|
116
119
|
);
|
|
117
|
-
if (Array.isArray(parsed.
|
|
118
|
-
out.
|
|
120
|
+
else if (Array.isArray(parsed.contexts))
|
|
121
|
+
out.context = parsed.contexts.filter(
|
|
122
|
+
(x) => typeof x === "string"
|
|
123
|
+
);
|
|
124
|
+
if (Array.isArray(parsed.addContext))
|
|
125
|
+
out.addContext = parsed.addContext.filter(
|
|
126
|
+
(x) => typeof x === "string"
|
|
127
|
+
);
|
|
128
|
+
else if (Array.isArray(parsed.addContexts))
|
|
129
|
+
out.addContext = parsed.addContexts.filter(
|
|
119
130
|
(x) => typeof x === "string"
|
|
120
131
|
);
|
|
121
132
|
if (typeof parsed.prompt === "string" && ["always", "never", "fallback", "no-env"].includes(parsed.prompt))
|
|
@@ -129,6 +140,8 @@ function loadConfigFile(configDir) {
|
|
|
129
140
|
out.shouldSavePrompt = parsed.shouldSavePrompt ?? parsed.savePrompt;
|
|
130
141
|
if (typeof parsed.saveContextTo === "string")
|
|
131
142
|
out.saveContextTo = parsed.saveContextTo;
|
|
143
|
+
if (typeof parsed.contextDir === "string") out.contextDir = parsed.contextDir;
|
|
144
|
+
else if (typeof parsed.contextsDir === "string") out.contextDir = parsed.contextsDir;
|
|
132
145
|
if (typeof parsed.root === "string") out.root = parsed.root;
|
|
133
146
|
if (typeof parsed.logLevel === "string" && LOG_LEVELS.includes(parsed.logLevel))
|
|
134
147
|
out.logLevel = parsed.logLevel;
|
|
@@ -144,11 +157,11 @@ function loadConfigFile(configDir) {
|
|
|
144
157
|
}
|
|
145
158
|
}
|
|
146
159
|
function mergeContexts(fileConfig, envConfig, progConfig) {
|
|
147
|
-
const fromFile = fileConfig.
|
|
148
|
-
const fromEnvAdd = envConfig.
|
|
149
|
-
const fromEnvReplace = envConfig.
|
|
150
|
-
const fromProgAdd = progConfig.
|
|
151
|
-
const fromProgReplace = progConfig.
|
|
160
|
+
const fromFile = fileConfig.context ?? fileConfig.addContext ?? [];
|
|
161
|
+
const fromEnvAdd = envConfig.addContext ?? [];
|
|
162
|
+
const fromEnvReplace = envConfig.context;
|
|
163
|
+
const fromProgAdd = progConfig.addContext ?? [];
|
|
164
|
+
const fromProgReplace = progConfig.context;
|
|
152
165
|
const replace = fromProgReplace ?? fromEnvReplace;
|
|
153
166
|
if (replace !== void 0) return replace;
|
|
154
167
|
const base = [...fromFile];
|
|
@@ -172,8 +185,8 @@ function loadConfig(root) {
|
|
|
172
185
|
...envConfig,
|
|
173
186
|
...programmaticConfig
|
|
174
187
|
};
|
|
175
|
-
merged.
|
|
176
|
-
delete merged.
|
|
188
|
+
merged.context = mergeContexts(fileConfig, envConfig, programmaticConfig);
|
|
189
|
+
delete merged.addContext;
|
|
177
190
|
if (configDir && !merged.root) merged.root = configDir;
|
|
178
191
|
else if (!merged.root) merged.root = startDir;
|
|
179
192
|
return merged;
|
|
@@ -208,16 +221,43 @@ var configLoadedLogged = false;
|
|
|
208
221
|
function resetLogState() {
|
|
209
222
|
configLoadedLogged = false;
|
|
210
223
|
}
|
|
224
|
+
var CONFIG_LOG_KEYS = [
|
|
225
|
+
"root",
|
|
226
|
+
"context",
|
|
227
|
+
"prompt",
|
|
228
|
+
"ignoreEnv",
|
|
229
|
+
"ignoreContext",
|
|
230
|
+
"set",
|
|
231
|
+
"shouldSavePrompt",
|
|
232
|
+
"saveContextTo",
|
|
233
|
+
"contextDir",
|
|
234
|
+
"logLevel"
|
|
235
|
+
];
|
|
236
|
+
function configForLog(config) {
|
|
237
|
+
return Object.fromEntries(
|
|
238
|
+
CONFIG_LOG_KEYS.filter((k) => config[k] !== void 0).map((k) => [k, config[k]])
|
|
239
|
+
);
|
|
240
|
+
}
|
|
211
241
|
function logConfigLoaded(config) {
|
|
212
242
|
if (configLoadedLogged) return;
|
|
213
|
-
if (getLevelNum() < LEVEL_NUM.info) return;
|
|
214
243
|
configLoadedLogged = true;
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
244
|
+
const levelNum = LEVEL_NUM[config.logLevel ?? "none"];
|
|
245
|
+
if (levelNum >= LEVEL_NUM.info) {
|
|
246
|
+
const parts = [
|
|
247
|
+
"root=" + (config.root ?? "(cwd)"),
|
|
248
|
+
"context=" + JSON.stringify(config.context ?? [])
|
|
249
|
+
];
|
|
250
|
+
if (config.prompt !== void 0) parts.push("prompt=" + config.prompt);
|
|
251
|
+
if (config.ignoreEnv === true) parts.push("ignoreEnv=true");
|
|
252
|
+
if (config.ignoreContext === true) parts.push("ignoreContext=true");
|
|
253
|
+
if (config.saveContextTo !== void 0) parts.push("saveContextTo=" + config.saveContextTo);
|
|
254
|
+
if (config.contextDir !== void 0) parts.push("contextDir=" + config.contextDir);
|
|
255
|
+
if (config.shouldSavePrompt !== void 0) parts.push("shouldSavePrompt=" + config.shouldSavePrompt);
|
|
256
|
+
log("info", "config loaded", parts.join(" "));
|
|
257
|
+
}
|
|
258
|
+
if (levelNum >= LEVEL_NUM.debug) {
|
|
259
|
+
log("debug", "config (full)", JSON.stringify(configForLog(config)));
|
|
260
|
+
}
|
|
221
261
|
}
|
|
222
262
|
function log(level, msg, ...args) {
|
|
223
263
|
const configured = getLevelNum();
|
|
@@ -239,7 +279,7 @@ import {
|
|
|
239
279
|
readdirSync,
|
|
240
280
|
statSync
|
|
241
281
|
} from "fs";
|
|
242
|
-
import { join as join2, dirname as dirname2 } from "path";
|
|
282
|
+
import { join as join2, dirname as dirname2, isAbsolute } from "path";
|
|
243
283
|
var CONTEXT_SUFFIX = ".context.json";
|
|
244
284
|
function discoverContextPathsInternal(dir, found) {
|
|
245
285
|
let entries;
|
|
@@ -308,7 +348,7 @@ function getMergedContextValues() {
|
|
|
308
348
|
const root = config.root ?? process.cwd();
|
|
309
349
|
const paths = discoverContextPaths(root);
|
|
310
350
|
const out = {};
|
|
311
|
-
for (const contextName of config.
|
|
351
|
+
for (const contextName of config.context ?? []) {
|
|
312
352
|
const filePath = paths.get(contextName);
|
|
313
353
|
if (!filePath) {
|
|
314
354
|
log("warn", `context "${contextName}" not found (no *.context.json)`);
|
|
@@ -340,7 +380,8 @@ function getContextWritePath(contextName) {
|
|
|
340
380
|
const paths = discoverContextPaths(root);
|
|
341
381
|
const existing = paths.get(contextName);
|
|
342
382
|
if (existing) return existing;
|
|
343
|
-
|
|
383
|
+
const saveDir = config.contextDir ? isAbsolute(config.contextDir) ? config.contextDir : join2(root, config.contextDir) : root;
|
|
384
|
+
return join2(saveDir, `${contextName}${CONTEXT_SUFFIX}`);
|
|
344
385
|
}
|
|
345
386
|
function writeToContext(contextName, key, value) {
|
|
346
387
|
const path = getContextWritePath(contextName);
|
|
@@ -358,26 +399,49 @@ function writeToContext(contextName, key, value) {
|
|
|
358
399
|
}
|
|
359
400
|
|
|
360
401
|
// src/variable.ts
|
|
361
|
-
var
|
|
402
|
+
var CONTEXT_REF_FULL_REGEX = /^@([^:]+):(.+)$/;
|
|
403
|
+
var CONTEXT_REF_SHORT_REGEX = /^@([^:]+)$/;
|
|
404
|
+
var ENV_REF_REGEX = /\$([A-Za-z_][A-Za-z0-9_]*|\{[A-Za-z_][A-Za-z0-9_]*\})/g;
|
|
362
405
|
var MAX_CONTEXT_REF_DEPTH = 10;
|
|
363
|
-
function
|
|
406
|
+
function expandEnvReferences(str) {
|
|
407
|
+
return str.replace(ENV_REF_REGEX, (_, name) => {
|
|
408
|
+
const key = name.startsWith("{") ? name.slice(1, -1) : name;
|
|
409
|
+
return process.env[key] ?? "";
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
function resolveContextReference(raw, currentKey, depth = 0) {
|
|
364
413
|
if (depth >= MAX_CONTEXT_REF_DEPTH) {
|
|
365
414
|
throw new Error(
|
|
366
415
|
`Context reference resolution exceeded max depth (${MAX_CONTEXT_REF_DEPTH}): possible circular reference`
|
|
367
416
|
);
|
|
368
417
|
}
|
|
369
|
-
const
|
|
370
|
-
|
|
371
|
-
|
|
418
|
+
const afterEnv = expandEnvReferences(raw);
|
|
419
|
+
let contextName;
|
|
420
|
+
let refKey;
|
|
421
|
+
const fullMatch = afterEnv.match(CONTEXT_REF_FULL_REGEX);
|
|
422
|
+
if (fullMatch) {
|
|
423
|
+
[, contextName, refKey] = fullMatch;
|
|
424
|
+
} else {
|
|
425
|
+
const shortMatch = afterEnv.match(CONTEXT_REF_SHORT_REGEX);
|
|
426
|
+
if (!shortMatch) return afterEnv;
|
|
427
|
+
contextName = shortMatch[1];
|
|
428
|
+
if (currentKey === void 0) {
|
|
429
|
+
throw new Error(
|
|
430
|
+
`Context reference @${contextName} (short form) cannot be resolved without a variable key. Use @${contextName}:<key> to specify a key.`
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
refKey = currentKey;
|
|
434
|
+
}
|
|
372
435
|
const ctx = getContext(contextName);
|
|
373
436
|
const resolved = ctx[refKey];
|
|
374
437
|
if (resolved === void 0) {
|
|
375
438
|
const hasContext = Object.keys(ctx).length > 0;
|
|
376
|
-
const
|
|
439
|
+
const displayRef = refKey === currentKey ? `@${contextName}` : `@${contextName}:${refKey}`;
|
|
440
|
+
const msg = hasContext ? `Context reference ${displayRef} could not be resolved: key "${refKey}" is not defined in context "${contextName}".` : `Context reference ${displayRef} could not be resolved: context "${contextName}" not found (no ${contextName}.context.json).`;
|
|
377
441
|
log("error", msg);
|
|
378
442
|
throw new Error(msg);
|
|
379
443
|
}
|
|
380
|
-
return resolveContextReference(resolved, depth + 1);
|
|
444
|
+
return resolveContextReference(resolved, currentKey, depth + 1);
|
|
381
445
|
}
|
|
382
446
|
function defaultKeyFromName(name) {
|
|
383
447
|
return name.toLowerCase().replace(/\s+/g, "_").replace(/[^a-z0-9_]/gi, "");
|
|
@@ -403,7 +467,7 @@ function scenv(name, options = {}) {
|
|
|
403
467
|
log("trace", `resolveRaw: checking set for key=${key}`);
|
|
404
468
|
if (config.set?.[key] !== void 0) {
|
|
405
469
|
log("trace", `resolveRaw: set hit key=${key}`);
|
|
406
|
-
const raw = resolveContextReference(config.set[key]);
|
|
470
|
+
const raw = resolveContextReference(config.set[key], key);
|
|
407
471
|
return { raw, source: "set" };
|
|
408
472
|
}
|
|
409
473
|
if (!config.ignoreEnv) {
|
|
@@ -411,7 +475,7 @@ function scenv(name, options = {}) {
|
|
|
411
475
|
const envVal = process.env[envKey];
|
|
412
476
|
if (envVal !== void 0 && envVal !== "") {
|
|
413
477
|
log("trace", "resolveRaw: env hit");
|
|
414
|
-
const raw = resolveContextReference(envVal);
|
|
478
|
+
const raw = resolveContextReference(envVal, key);
|
|
415
479
|
return { raw, source: "env" };
|
|
416
480
|
}
|
|
417
481
|
}
|
|
@@ -420,7 +484,7 @@ function scenv(name, options = {}) {
|
|
|
420
484
|
const ctx = getMergedContextValues();
|
|
421
485
|
if (ctx[key] !== void 0) {
|
|
422
486
|
log("trace", `resolveRaw: context hit key=${key}`);
|
|
423
|
-
const raw = resolveContextReference(ctx[key]);
|
|
487
|
+
const raw = resolveContextReference(ctx[key], key);
|
|
424
488
|
return { raw, source: "context" };
|
|
425
489
|
}
|
|
426
490
|
}
|
|
@@ -447,7 +511,7 @@ function scenv(name, options = {}) {
|
|
|
447
511
|
`prompt decision key=${key} prompt=${config.prompt ?? "fallback"} hadValue=${hadValue} hadEnv=${hadEnv} -> ${doPrompt ? "prompt" : "no prompt"}`
|
|
448
512
|
);
|
|
449
513
|
const effectiveDefault = overrides?.default !== void 0 ? overrides.default : defaultValue;
|
|
450
|
-
const resolvedDefault = effectiveDefault === void 0 ? void 0 : typeof effectiveDefault === "string" ? resolveContextReference(effectiveDefault) : effectiveDefault;
|
|
514
|
+
const resolvedDefault = effectiveDefault === void 0 ? void 0 : typeof effectiveDefault === "string" ? resolveContextReference(effectiveDefault, key) : effectiveDefault;
|
|
451
515
|
let wasPrompted = false;
|
|
452
516
|
let value;
|
|
453
517
|
let resolvedFrom;
|
|
@@ -461,7 +525,7 @@ function scenv(name, options = {}) {
|
|
|
461
525
|
}
|
|
462
526
|
const defaultForPrompt = raw !== void 0 ? raw : resolvedDefault;
|
|
463
527
|
let promptedValue = await Promise.resolve(fn(name, defaultForPrompt));
|
|
464
|
-
value = typeof promptedValue === "string" ? resolveContextReference(promptedValue) : promptedValue;
|
|
528
|
+
value = typeof promptedValue === "string" ? resolveContextReference(promptedValue, key) : promptedValue;
|
|
465
529
|
wasPrompted = true;
|
|
466
530
|
resolvedFrom = "prompt";
|
|
467
531
|
} else if (raw !== void 0) {
|
|
@@ -516,7 +580,7 @@ function scenv(name, options = {}) {
|
|
|
516
580
|
}
|
|
517
581
|
if (!doSave) return final;
|
|
518
582
|
const callbacks = getCallbacks();
|
|
519
|
-
const contextNames = config.
|
|
583
|
+
const contextNames = config.context ?? [];
|
|
520
584
|
let ctxToSave;
|
|
521
585
|
if (config.saveContextTo === "ask") {
|
|
522
586
|
if (typeof callbacks.onAskContext !== "function") {
|
|
@@ -560,10 +624,10 @@ function scenv(name, options = {}) {
|
|
|
560
624
|
}
|
|
561
625
|
contextName = await callbacks.onAskContext(
|
|
562
626
|
name,
|
|
563
|
-
config.
|
|
627
|
+
config.context ?? []
|
|
564
628
|
);
|
|
565
629
|
}
|
|
566
|
-
if (!contextName) contextName = config.
|
|
630
|
+
if (!contextName) contextName = config.context?.[0] ?? "default";
|
|
567
631
|
writeToContext(contextName, key, String(validated.data));
|
|
568
632
|
log("info", `Saved key=${key} to context ${contextName}`);
|
|
569
633
|
}
|
|
@@ -577,9 +641,9 @@ function parseScenvArgs(argv) {
|
|
|
577
641
|
while (i < argv.length) {
|
|
578
642
|
const arg = argv[i];
|
|
579
643
|
if (arg === "--context" && argv[i + 1] !== void 0) {
|
|
580
|
-
config.
|
|
644
|
+
config.context = argv[++i].split(",").map((s) => s.trim()).filter(Boolean);
|
|
581
645
|
} else if (arg === "--add-context" && argv[i + 1] !== void 0) {
|
|
582
|
-
config.
|
|
646
|
+
config.addContext = argv[++i].split(",").map((s) => s.trim()).filter(Boolean);
|
|
583
647
|
} else if (arg === "--prompt" && argv[i + 1] !== void 0) {
|
|
584
648
|
const v = argv[++i].toLowerCase();
|
|
585
649
|
if (["always", "never", "fallback", "no-env"].includes(v)) {
|
|
@@ -603,6 +667,8 @@ function parseScenvArgs(argv) {
|
|
|
603
667
|
}
|
|
604
668
|
} else if (arg === "--save-context-to" && argv[i + 1] !== void 0) {
|
|
605
669
|
config.saveContextTo = argv[++i];
|
|
670
|
+
} else if (arg === "--context-dir" && argv[i + 1] !== void 0) {
|
|
671
|
+
config.contextDir = argv[++i];
|
|
606
672
|
} else if ((arg === "--log-level" || arg === "--log") && argv[i + 1] !== void 0) {
|
|
607
673
|
const v = argv[++i].toLowerCase();
|
|
608
674
|
if (LOG_LEVELS.includes(v)) {
|