scenv 0.6.0 → 0.7.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/dist/index.cjs +100 -92
- package/dist/index.d.cts +38 -32
- package/dist/index.d.ts +38 -32
- package/dist/index.js +98 -93
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -25,12 +25,16 @@ __export(index_exports, {
|
|
|
25
25
|
discoverContextPaths: () => discoverContextPaths,
|
|
26
26
|
getCallbacks: () => getCallbacks,
|
|
27
27
|
getContext: () => getContext,
|
|
28
|
+
getContextWritePath: () => getContextWritePath,
|
|
29
|
+
getInMemoryContext: () => getInMemoryContext,
|
|
28
30
|
getMergedContextValues: () => getMergedContextValues,
|
|
29
31
|
loadConfig: () => loadConfig,
|
|
30
32
|
parseScenvArgs: () => parseScenvArgs,
|
|
31
33
|
resetConfig: () => resetConfig,
|
|
34
|
+
resetInMemoryContext: () => resetInMemoryContext,
|
|
32
35
|
resetLogState: () => resetLogState,
|
|
33
|
-
scenv: () => scenv
|
|
36
|
+
scenv: () => scenv,
|
|
37
|
+
setInMemoryContext: () => setInMemoryContext
|
|
34
38
|
});
|
|
35
39
|
module.exports = __toCommonJS(index_exports);
|
|
36
40
|
|
|
@@ -40,19 +44,6 @@ var import_node_path = require("path");
|
|
|
40
44
|
|
|
41
45
|
// src/prompt-default.ts
|
|
42
46
|
var import_node_readline = require("readline");
|
|
43
|
-
function ask(message) {
|
|
44
|
-
const rl = (0, import_node_readline.createInterface)({ input: process.stdin, output: process.stdout });
|
|
45
|
-
return new Promise((resolve, reject) => {
|
|
46
|
-
rl.question(message, (answer) => {
|
|
47
|
-
rl.close();
|
|
48
|
-
resolve(answer.trim());
|
|
49
|
-
});
|
|
50
|
-
rl.on("error", (err) => {
|
|
51
|
-
rl.close();
|
|
52
|
-
reject(err);
|
|
53
|
-
});
|
|
54
|
-
});
|
|
55
|
-
}
|
|
56
47
|
function defaultPrompt(name, defaultValue) {
|
|
57
48
|
const defaultStr = defaultValue !== void 0 && defaultValue !== null ? String(defaultValue) : "";
|
|
58
49
|
const message = defaultStr ? `Enter ${name} [${defaultStr}]: ` : `Enter ${name}: `;
|
|
@@ -70,17 +61,6 @@ function defaultPrompt(name, defaultValue) {
|
|
|
70
61
|
});
|
|
71
62
|
});
|
|
72
63
|
}
|
|
73
|
-
async function defaultAskWhetherToSave(name, _value) {
|
|
74
|
-
const answer = await ask(`Save "${name}" for next time? (y/n): `);
|
|
75
|
-
const v = answer.toLowerCase();
|
|
76
|
-
return v === "y" || v === "yes" || v === "1" || v === "true";
|
|
77
|
-
}
|
|
78
|
-
async function defaultAskContext(name, contextNames) {
|
|
79
|
-
const hint = contextNames.length > 0 ? ` (${contextNames.join(", ")})` : "";
|
|
80
|
-
const answer = await ask(`Save "${name}" to which context?${hint}: `);
|
|
81
|
-
if (answer) return answer;
|
|
82
|
-
return contextNames[0] ?? "default";
|
|
83
|
-
}
|
|
84
64
|
|
|
85
65
|
// src/config.ts
|
|
86
66
|
var LOG_LEVELS = ["none", "trace", "debug", "info", "warn", "error"];
|
|
@@ -91,7 +71,6 @@ var envKeyMap = {
|
|
|
91
71
|
SCENV_PROMPT: "prompt",
|
|
92
72
|
SCENV_IGNORE_ENV: "ignoreEnv",
|
|
93
73
|
SCENV_IGNORE_CONTEXT: "ignoreContext",
|
|
94
|
-
SCENV_SAVE_PROMPT: "shouldSavePrompt",
|
|
95
74
|
SCENV_SAVE_CONTEXT_TO: "saveContextTo",
|
|
96
75
|
SCENV_CONTEXT_DIR: "contextDir",
|
|
97
76
|
SCENV_LOG_LEVEL: "logLevel"
|
|
@@ -100,9 +79,7 @@ var programmaticConfig = {};
|
|
|
100
79
|
var programmaticCallbacks = {};
|
|
101
80
|
function getCallbacks() {
|
|
102
81
|
return {
|
|
103
|
-
defaultPrompt: programmaticCallbacks.defaultPrompt ?? defaultPrompt
|
|
104
|
-
onAskWhetherToSave: programmaticCallbacks.onAskWhetherToSave ?? defaultAskWhetherToSave,
|
|
105
|
-
onAskContext: programmaticCallbacks.onAskContext ?? defaultAskContext
|
|
82
|
+
defaultPrompt: programmaticCallbacks.defaultPrompt ?? defaultPrompt
|
|
106
83
|
};
|
|
107
84
|
}
|
|
108
85
|
function findConfigDir(startDir) {
|
|
@@ -125,11 +102,9 @@ function configFromEnv() {
|
|
|
125
102
|
out[configKey] = val.split(",").map((s) => s.trim()).filter(Boolean);
|
|
126
103
|
} else if (configKey === "ignoreEnv" || configKey === "ignoreContext") {
|
|
127
104
|
out[configKey] = val === "1" || val === "true" || val.toLowerCase() === "yes";
|
|
128
|
-
} else if (configKey === "prompt"
|
|
105
|
+
} else if (configKey === "prompt") {
|
|
129
106
|
const v = val.toLowerCase();
|
|
130
|
-
if (
|
|
131
|
-
out[configKey] = v;
|
|
132
|
-
if (configKey === "shouldSavePrompt" && (v === "always" || v === "never" || v === "ask"))
|
|
107
|
+
if (v === "always" || v === "never" || v === "fallback" || v === "no-env")
|
|
133
108
|
out[configKey] = v;
|
|
134
109
|
} else if (configKey === "saveContextTo") {
|
|
135
110
|
out.saveContextTo = val;
|
|
@@ -172,8 +147,6 @@ function loadConfigFile(configDir) {
|
|
|
172
147
|
out.ignoreContext = parsed.ignoreContext;
|
|
173
148
|
if (parsed.set && typeof parsed.set === "object" && !Array.isArray(parsed.set))
|
|
174
149
|
out.set = parsed.set;
|
|
175
|
-
if (typeof (parsed.shouldSavePrompt ?? parsed.savePrompt) === "string" && ["always", "never", "ask"].includes(parsed.shouldSavePrompt ?? parsed.savePrompt))
|
|
176
|
-
out.shouldSavePrompt = parsed.shouldSavePrompt ?? parsed.savePrompt;
|
|
177
150
|
if (typeof parsed.saveContextTo === "string")
|
|
178
151
|
out.saveContextTo = parsed.saveContextTo;
|
|
179
152
|
if (typeof parsed.contextDir === "string") out.contextDir = parsed.contextDir;
|
|
@@ -257,16 +230,41 @@ var configLoadedLogged = false;
|
|
|
257
230
|
function resetLogState() {
|
|
258
231
|
configLoadedLogged = false;
|
|
259
232
|
}
|
|
233
|
+
var CONFIG_LOG_KEYS = [
|
|
234
|
+
"root",
|
|
235
|
+
"context",
|
|
236
|
+
"prompt",
|
|
237
|
+
"ignoreEnv",
|
|
238
|
+
"ignoreContext",
|
|
239
|
+
"set",
|
|
240
|
+
"saveContextTo",
|
|
241
|
+
"contextDir",
|
|
242
|
+
"logLevel"
|
|
243
|
+
];
|
|
244
|
+
function configForLog(config) {
|
|
245
|
+
return Object.fromEntries(
|
|
246
|
+
CONFIG_LOG_KEYS.filter((k) => config[k] !== void 0).map((k) => [k, config[k]])
|
|
247
|
+
);
|
|
248
|
+
}
|
|
260
249
|
function logConfigLoaded(config) {
|
|
261
250
|
if (configLoadedLogged) return;
|
|
262
|
-
if (getLevelNum() < LEVEL_NUM.info) return;
|
|
263
251
|
configLoadedLogged = true;
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
252
|
+
const levelNum = LEVEL_NUM[config.logLevel ?? "none"];
|
|
253
|
+
if (levelNum >= LEVEL_NUM.info) {
|
|
254
|
+
const parts = [
|
|
255
|
+
"root=" + (config.root ?? "(cwd)"),
|
|
256
|
+
"context=" + JSON.stringify(config.context ?? [])
|
|
257
|
+
];
|
|
258
|
+
if (config.prompt !== void 0) parts.push("prompt=" + config.prompt);
|
|
259
|
+
if (config.ignoreEnv === true) parts.push("ignoreEnv=true");
|
|
260
|
+
if (config.ignoreContext === true) parts.push("ignoreContext=true");
|
|
261
|
+
if (config.saveContextTo !== void 0) parts.push("saveContextTo=" + config.saveContextTo);
|
|
262
|
+
if (config.contextDir !== void 0) parts.push("contextDir=" + config.contextDir);
|
|
263
|
+
log("info", "config loaded", parts.join(" "));
|
|
264
|
+
}
|
|
265
|
+
if (levelNum >= LEVEL_NUM.debug) {
|
|
266
|
+
log("debug", "config (full)", JSON.stringify(configForLog(config)));
|
|
267
|
+
}
|
|
270
268
|
}
|
|
271
269
|
function log(level, msg, ...args) {
|
|
272
270
|
const configured = getLevelNum();
|
|
@@ -284,6 +282,16 @@ function log(level, msg, ...args) {
|
|
|
284
282
|
var import_node_fs2 = require("fs");
|
|
285
283
|
var import_node_path2 = require("path");
|
|
286
284
|
var CONTEXT_SUFFIX = ".context.json";
|
|
285
|
+
var inMemoryContext = {};
|
|
286
|
+
function getInMemoryContext() {
|
|
287
|
+
return inMemoryContext;
|
|
288
|
+
}
|
|
289
|
+
function setInMemoryContext(key, value) {
|
|
290
|
+
inMemoryContext[key] = value;
|
|
291
|
+
}
|
|
292
|
+
function resetInMemoryContext() {
|
|
293
|
+
inMemoryContext = {};
|
|
294
|
+
}
|
|
287
295
|
function discoverContextPathsInternal(dir, found) {
|
|
288
296
|
let entries;
|
|
289
297
|
try {
|
|
@@ -351,6 +359,14 @@ function getMergedContextValues() {
|
|
|
351
359
|
const root = config.root ?? process.cwd();
|
|
352
360
|
const paths = discoverContextPaths(root);
|
|
353
361
|
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
|
+
}
|
|
354
370
|
for (const contextName of config.context ?? []) {
|
|
355
371
|
const filePath = paths.get(contextName);
|
|
356
372
|
if (!filePath) {
|
|
@@ -378,6 +394,9 @@ function getMergedContextValues() {
|
|
|
378
394
|
return out;
|
|
379
395
|
}
|
|
380
396
|
function getContextWritePath(contextName) {
|
|
397
|
+
if ((0, import_node_path2.isAbsolute)(contextName) || contextName.includes(import_node_path2.sep)) {
|
|
398
|
+
return contextName.endsWith(CONTEXT_SUFFIX) ? contextName : contextName + CONTEXT_SUFFIX;
|
|
399
|
+
}
|
|
381
400
|
const config = loadConfig();
|
|
382
401
|
const root = config.root ?? process.cwd();
|
|
383
402
|
const paths = discoverContextPaths(root);
|
|
@@ -386,6 +405,26 @@ function getContextWritePath(contextName) {
|
|
|
386
405
|
const saveDir = config.contextDir ? (0, import_node_path2.isAbsolute)(config.contextDir) ? config.contextDir : (0, import_node_path2.join)(root, config.contextDir) : root;
|
|
387
406
|
return (0, import_node_path2.join)(saveDir, `${contextName}${CONTEXT_SUFFIX}`);
|
|
388
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
|
+
}
|
|
427
|
+
}
|
|
389
428
|
function writeToContext(contextName, key, value) {
|
|
390
429
|
const path = getContextWritePath(contextName);
|
|
391
430
|
let data = {};
|
|
@@ -482,6 +521,12 @@ function scenv(name, options = {}) {
|
|
|
482
521
|
return { raw, source: "env" };
|
|
483
522
|
}
|
|
484
523
|
}
|
|
524
|
+
const mem = getInMemoryContext();
|
|
525
|
+
if (mem[key] !== void 0) {
|
|
526
|
+
log("trace", `resolveRaw: in-memory hit key=${key}`);
|
|
527
|
+
const raw = resolveContextReference(mem[key], key);
|
|
528
|
+
return { raw, source: "context" };
|
|
529
|
+
}
|
|
485
530
|
if (!config.ignoreContext) {
|
|
486
531
|
log("trace", "resolveRaw: checking context");
|
|
487
532
|
const ctx = getMergedContextValues();
|
|
@@ -567,36 +612,11 @@ function scenv(name, options = {}) {
|
|
|
567
612
|
const final = validated.data;
|
|
568
613
|
if (wasPrompted) {
|
|
569
614
|
const config = loadConfig();
|
|
570
|
-
|
|
571
|
-
if (
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
const callbacks2 = getCallbacks();
|
|
575
|
-
if (typeof callbacks2.onAskWhetherToSave !== "function") {
|
|
576
|
-
throw new Error(
|
|
577
|
-
`shouldSavePrompt is "ask" but onAskWhetherToSave callback is not set. Configure callbacks via configure({ callbacks: { onAskWhetherToSave: ... } }).`
|
|
578
|
-
);
|
|
579
|
-
}
|
|
580
|
-
doSave = await callbacks2.onAskWhetherToSave(name, final);
|
|
581
|
-
} else {
|
|
582
|
-
doSave = true;
|
|
615
|
+
setInMemoryContext(key, String(final));
|
|
616
|
+
if (config.saveContextTo) {
|
|
617
|
+
writeToContext(config.saveContextTo, key, String(final));
|
|
618
|
+
log("info", `Saved key=${key} to saveContextTo ${config.saveContextTo}`);
|
|
583
619
|
}
|
|
584
|
-
if (!doSave) return final;
|
|
585
|
-
const callbacks = getCallbacks();
|
|
586
|
-
const contextNames = config.context ?? [];
|
|
587
|
-
let ctxToSave;
|
|
588
|
-
if (config.saveContextTo === "ask") {
|
|
589
|
-
if (typeof callbacks.onAskContext !== "function") {
|
|
590
|
-
throw new Error(
|
|
591
|
-
`saveContextTo is "ask" but onAskContext callback is not set. Configure callbacks via configure({ callbacks: { onAskContext: ... } }).`
|
|
592
|
-
);
|
|
593
|
-
}
|
|
594
|
-
ctxToSave = await callbacks.onAskContext(name, contextNames);
|
|
595
|
-
} else {
|
|
596
|
-
ctxToSave = config.saveContextTo ?? contextNames[0] ?? "default";
|
|
597
|
-
}
|
|
598
|
-
writeToContext(ctxToSave, key, String(final));
|
|
599
|
-
log("info", `Saved key=${key} to context ${ctxToSave}`);
|
|
600
620
|
}
|
|
601
621
|
return final;
|
|
602
622
|
}
|
|
@@ -617,22 +637,11 @@ function scenv(name, options = {}) {
|
|
|
617
637
|
throw new Error(errMsg);
|
|
618
638
|
}
|
|
619
639
|
const config = loadConfig();
|
|
620
|
-
|
|
621
|
-
if (
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
throw new Error(
|
|
625
|
-
`saveContextTo is "ask" but onAskContext callback is not set. Configure callbacks via configure({ callbacks: { onAskContext: ... } }).`
|
|
626
|
-
);
|
|
627
|
-
}
|
|
628
|
-
contextName = await callbacks.onAskContext(
|
|
629
|
-
name,
|
|
630
|
-
config.context ?? []
|
|
631
|
-
);
|
|
640
|
+
setInMemoryContext(key, String(validated.data));
|
|
641
|
+
if (config.saveContextTo) {
|
|
642
|
+
writeToContext(config.saveContextTo, key, String(validated.data));
|
|
643
|
+
log("info", `Saved key=${key} to saveContextTo ${config.saveContextTo}`);
|
|
632
644
|
}
|
|
633
|
-
if (!contextName) contextName = config.context?.[0] ?? "default";
|
|
634
|
-
writeToContext(contextName, key, String(validated.data));
|
|
635
|
-
log("info", `Saved key=${key} to context ${contextName}`);
|
|
636
645
|
}
|
|
637
646
|
return { get, safeGet, save };
|
|
638
647
|
}
|
|
@@ -663,11 +672,6 @@ function parseScenvArgs(argv) {
|
|
|
663
672
|
config.set = config.set ?? {};
|
|
664
673
|
config.set[pair.slice(0, eq).trim()] = pair.slice(eq + 1).trim();
|
|
665
674
|
}
|
|
666
|
-
} else if (arg === "--save-prompt" && argv[i + 1] !== void 0) {
|
|
667
|
-
const v = argv[++i].toLowerCase();
|
|
668
|
-
if (["always", "never", "ask"].includes(v)) {
|
|
669
|
-
config.shouldSavePrompt = v;
|
|
670
|
-
}
|
|
671
675
|
} else if (arg === "--save-context-to" && argv[i + 1] !== void 0) {
|
|
672
676
|
config.saveContextTo = argv[++i];
|
|
673
677
|
} else if (arg === "--context-dir" && argv[i + 1] !== void 0) {
|
|
@@ -701,10 +705,14 @@ function parseScenvArgs(argv) {
|
|
|
701
705
|
discoverContextPaths,
|
|
702
706
|
getCallbacks,
|
|
703
707
|
getContext,
|
|
708
|
+
getContextWritePath,
|
|
709
|
+
getInMemoryContext,
|
|
704
710
|
getMergedContextValues,
|
|
705
711
|
loadConfig,
|
|
706
712
|
parseScenvArgs,
|
|
707
713
|
resetConfig,
|
|
714
|
+
resetInMemoryContext,
|
|
708
715
|
resetLogState,
|
|
709
|
-
scenv
|
|
716
|
+
scenv,
|
|
717
|
+
setInMemoryContext
|
|
710
718
|
});
|
package/dist/index.d.cts
CHANGED
|
@@ -6,13 +6,6 @@
|
|
|
6
6
|
* - `"no-env"` – Prompt when the env var is not set (even if context has a value).
|
|
7
7
|
*/
|
|
8
8
|
type PromptMode = "always" | "never" | "fallback" | "no-env";
|
|
9
|
-
/**
|
|
10
|
-
* What to do with the value after the user was just prompted for it.
|
|
11
|
-
* - `"never"` – Do not save; discard for next time.
|
|
12
|
-
* - `"always"` – Save it (no prompt). Use saveContextTo or onAskContext only to pick where.
|
|
13
|
-
* - `"ask"` – Call {@link ScenvCallbacks.onAskWhetherToSave}; if true save, if false don't save.
|
|
14
|
-
*/
|
|
15
|
-
type SavePromptMode = "always" | "never" | "ask";
|
|
16
9
|
/**
|
|
17
10
|
* Valid log levels. Use with {@link ScenvConfig.logLevel}.
|
|
18
11
|
* Messages at or above the configured level are written to stderr.
|
|
@@ -38,10 +31,8 @@ interface ScenvConfig {
|
|
|
38
31
|
ignoreContext?: boolean;
|
|
39
32
|
/** Override values by key (CLI: `--set key=value`). Takes precedence over env and context. */
|
|
40
33
|
set?: Record<string, string>;
|
|
41
|
-
/**
|
|
42
|
-
|
|
43
|
-
/** Target context for saving: a context name, or `"ask"` to use {@link ScenvCallbacks.onAskContext}. */
|
|
44
|
-
saveContextTo?: "ask" | string;
|
|
34
|
+
/** 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
|
+
saveContextTo?: string;
|
|
45
36
|
/** 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
37
|
contextDir?: string;
|
|
47
38
|
/** Root directory for config file search and context discovery. Default is cwd or the directory containing scenv.config.json. */
|
|
@@ -57,22 +48,16 @@ interface ScenvConfig {
|
|
|
57
48
|
*/
|
|
58
49
|
type DefaultPromptFn = (name: string, defaultValue: unknown) => unknown | Promise<unknown>;
|
|
59
50
|
/**
|
|
60
|
-
* Callbacks for interactive behaviour.
|
|
61
|
-
* Pass to {@link configure} via `configure({ callbacks: { ... } })`.
|
|
51
|
+
* Callbacks for interactive behaviour. Pass to {@link configure} via `configure({ callbacks: { ... } })`.
|
|
62
52
|
*/
|
|
63
53
|
interface ScenvCallbacks {
|
|
64
54
|
/** Used when a variable does not define its own `prompt`. Variable-level `prompt` overrides this. */
|
|
65
55
|
defaultPrompt?: DefaultPromptFn;
|
|
66
|
-
/** Called only when {@link ScenvConfig.shouldSavePrompt} is "ask" and the user was just prompted. Return true to save, false to skip. (With "always", we save without calling this.) */
|
|
67
|
-
onAskWhetherToSave?: (name: string, value: unknown) => Promise<boolean>;
|
|
68
|
-
/** Called when the save destination is ambiguous: saveContextTo is "ask", or after a prompt when the user said yes and saveContextTo is "ask". Return the context name to write to. */
|
|
69
|
-
onAskContext?: (name: string, contextNames: string[]) => Promise<string>;
|
|
70
56
|
}
|
|
71
57
|
/**
|
|
72
|
-
* Returns the current callbacks (with built-in
|
|
73
|
-
* Useful for inspection or to pass a subset to another layer. Each call returns a new object.
|
|
58
|
+
* Returns the current callbacks (with built-in default for defaultPrompt when not set).
|
|
74
59
|
*
|
|
75
|
-
* @returns Copy of the effective callbacks
|
|
60
|
+
* @returns Copy of the effective callbacks.
|
|
76
61
|
*/
|
|
77
62
|
declare function getCallbacks(): ScenvCallbacks;
|
|
78
63
|
/**
|
|
@@ -108,6 +93,19 @@ declare function resetConfig(): void;
|
|
|
108
93
|
*/
|
|
109
94
|
declare function resetLogState(): void;
|
|
110
95
|
|
|
96
|
+
/**
|
|
97
|
+
* Returns the current in-memory context (key → value). Used during resolution before file contexts.
|
|
98
|
+
* Modifying the returned object mutates the store.
|
|
99
|
+
*/
|
|
100
|
+
declare function getInMemoryContext(): Record<string, string>;
|
|
101
|
+
/**
|
|
102
|
+
* Sets a key-value pair in the in-memory context. Used when saving after prompt or save() when saveContextTo is unset, and always updated when a value is saved so the next get() sees it.
|
|
103
|
+
*/
|
|
104
|
+
declare function setInMemoryContext(key: string, value: string): void;
|
|
105
|
+
/**
|
|
106
|
+
* Clears the in-memory context. Mainly for tests. Call in beforeEach to get a clean slate.
|
|
107
|
+
*/
|
|
108
|
+
declare function resetInMemoryContext(): void;
|
|
111
109
|
/**
|
|
112
110
|
* Recursively discovers all `*.context.json` files under a directory. Context name is the
|
|
113
111
|
* filename without the suffix (e.g. `dev.context.json` → "dev"). First file found for a
|
|
@@ -130,14 +128,23 @@ declare function discoverContextPaths(dir: string, found?: Map<string, string>):
|
|
|
130
128
|
*/
|
|
131
129
|
declare function getContext(contextName: string, root?: string): Record<string, string>;
|
|
132
130
|
/**
|
|
133
|
-
* Loads and merges context values from the current config.
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
* resolution (set > env > context > default).
|
|
131
|
+
* Loads and merges context values from the current config. If {@link ScenvConfig.saveContextTo}
|
|
132
|
+
* is set, that context (file path or name) is loaded first; then {@link ScenvConfig.context}
|
|
133
|
+
* order. Respects {@link ScenvConfig.ignoreContext}. Later contexts overwrite earlier for the same key.
|
|
134
|
+
* Used during variable resolution (set > env > in-memory > merged context > default).
|
|
137
135
|
*
|
|
138
136
|
* @returns A flat record of key → string value. Empty if ignoreContext is true or no context loaded.
|
|
139
137
|
*/
|
|
140
138
|
declare function getMergedContextValues(): Record<string, string>;
|
|
139
|
+
/**
|
|
140
|
+
* Returns the file path used for a context name or path when saving.
|
|
141
|
+
* - 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 config.root, returns its path; else uses config.contextDir (if set) or root.
|
|
143
|
+
*
|
|
144
|
+
* @param contextName - Context name (e.g. "dev", "prod") or file path without suffix (e.g. "/path/to/myfile" → myfile.context.json).
|
|
145
|
+
* @returns Absolute path to the context JSON file.
|
|
146
|
+
*/
|
|
147
|
+
declare function getContextWritePath(contextName: string): string;
|
|
141
148
|
|
|
142
149
|
/**
|
|
143
150
|
* Return type for a variable's optional `validator` function. Use a boolean for simple
|
|
@@ -205,8 +212,8 @@ interface ScenvVariable<T> {
|
|
|
205
212
|
error?: unknown;
|
|
206
213
|
}>;
|
|
207
214
|
/**
|
|
208
|
-
* Write the value to
|
|
209
|
-
* If you don't pass a value, the last resolved value is used. Does not prompt
|
|
215
|
+
* Write the value to the save target: if config.saveContextTo is set, to that context file; otherwise to in-memory only.
|
|
216
|
+
* If you don't pass a value, the last resolved value is used. Does not prompt; it saves. The value is always stored in-memory so the next get() sees it.
|
|
210
217
|
*/
|
|
211
218
|
save(value?: T): Promise<void>;
|
|
212
219
|
}
|
|
@@ -236,9 +243,9 @@ interface ScenvVariable<T> {
|
|
|
236
243
|
* ## save()
|
|
237
244
|
*
|
|
238
245
|
* The variable has a save(value?) method. It writes the value (or the last resolved value if you omit it)
|
|
239
|
-
* to
|
|
240
|
-
*
|
|
241
|
-
*
|
|
246
|
+
* to the save target (config.saveContextTo file if set, otherwise in-memory). save() does not ask; it saves.
|
|
247
|
+
* When the user is prompted during get(), the value is always saved (to saveContextTo file if set, and always to in-memory)
|
|
248
|
+
* so a second get() on the same variable does not prompt again.
|
|
242
249
|
*
|
|
243
250
|
* @typeParam T - Value type (default string). Use a validator to coerce to number, boolean, etc.
|
|
244
251
|
* @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").
|
|
@@ -262,8 +269,7 @@ declare function scenv<T>(name: string, options?: ScenvVariableOptions<T>): Scen
|
|
|
262
269
|
* - `--ignore-env` – Set ignoreEnv to true.
|
|
263
270
|
* - `--ignore-context` – Set ignoreContext to true.
|
|
264
271
|
* - `--set key=value` or `--set=key=value` – Add to set overrides (multiple allowed).
|
|
265
|
-
* - `--save-
|
|
266
|
-
* - `--save-context-to name` – saveContextTo.
|
|
272
|
+
* - `--save-context-to pathOrName` – saveContextTo (path or context name without .context.json).
|
|
267
273
|
* - `--context-dir path` – contextDir (directory to save context files to by default).
|
|
268
274
|
* - `--log-level level`, `--log level`, `--log=level` – logLevel.
|
|
269
275
|
*
|
|
@@ -272,4 +278,4 @@ declare function scenv<T>(name: string, options?: ScenvVariableOptions<T>): Scen
|
|
|
272
278
|
*/
|
|
273
279
|
declare function parseScenvArgs(argv: string[]): Partial<ScenvConfig>;
|
|
274
280
|
|
|
275
|
-
export { type DefaultPromptFn, type GetOptions, LOG_LEVELS, type LogLevel, type PromptMode, type
|
|
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 };
|
package/dist/index.d.ts
CHANGED
|
@@ -6,13 +6,6 @@
|
|
|
6
6
|
* - `"no-env"` – Prompt when the env var is not set (even if context has a value).
|
|
7
7
|
*/
|
|
8
8
|
type PromptMode = "always" | "never" | "fallback" | "no-env";
|
|
9
|
-
/**
|
|
10
|
-
* What to do with the value after the user was just prompted for it.
|
|
11
|
-
* - `"never"` – Do not save; discard for next time.
|
|
12
|
-
* - `"always"` – Save it (no prompt). Use saveContextTo or onAskContext only to pick where.
|
|
13
|
-
* - `"ask"` – Call {@link ScenvCallbacks.onAskWhetherToSave}; if true save, if false don't save.
|
|
14
|
-
*/
|
|
15
|
-
type SavePromptMode = "always" | "never" | "ask";
|
|
16
9
|
/**
|
|
17
10
|
* Valid log levels. Use with {@link ScenvConfig.logLevel}.
|
|
18
11
|
* Messages at or above the configured level are written to stderr.
|
|
@@ -38,10 +31,8 @@ interface ScenvConfig {
|
|
|
38
31
|
ignoreContext?: boolean;
|
|
39
32
|
/** Override values by key (CLI: `--set key=value`). Takes precedence over env and context. */
|
|
40
33
|
set?: Record<string, string>;
|
|
41
|
-
/**
|
|
42
|
-
|
|
43
|
-
/** Target context for saving: a context name, or `"ask"` to use {@link ScenvCallbacks.onAskContext}. */
|
|
44
|
-
saveContextTo?: "ask" | string;
|
|
34
|
+
/** 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
|
+
saveContextTo?: string;
|
|
45
36
|
/** 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
37
|
contextDir?: string;
|
|
47
38
|
/** Root directory for config file search and context discovery. Default is cwd or the directory containing scenv.config.json. */
|
|
@@ -57,22 +48,16 @@ interface ScenvConfig {
|
|
|
57
48
|
*/
|
|
58
49
|
type DefaultPromptFn = (name: string, defaultValue: unknown) => unknown | Promise<unknown>;
|
|
59
50
|
/**
|
|
60
|
-
* Callbacks for interactive behaviour.
|
|
61
|
-
* Pass to {@link configure} via `configure({ callbacks: { ... } })`.
|
|
51
|
+
* Callbacks for interactive behaviour. Pass to {@link configure} via `configure({ callbacks: { ... } })`.
|
|
62
52
|
*/
|
|
63
53
|
interface ScenvCallbacks {
|
|
64
54
|
/** Used when a variable does not define its own `prompt`. Variable-level `prompt` overrides this. */
|
|
65
55
|
defaultPrompt?: DefaultPromptFn;
|
|
66
|
-
/** Called only when {@link ScenvConfig.shouldSavePrompt} is "ask" and the user was just prompted. Return true to save, false to skip. (With "always", we save without calling this.) */
|
|
67
|
-
onAskWhetherToSave?: (name: string, value: unknown) => Promise<boolean>;
|
|
68
|
-
/** Called when the save destination is ambiguous: saveContextTo is "ask", or after a prompt when the user said yes and saveContextTo is "ask". Return the context name to write to. */
|
|
69
|
-
onAskContext?: (name: string, contextNames: string[]) => Promise<string>;
|
|
70
56
|
}
|
|
71
57
|
/**
|
|
72
|
-
* Returns the current callbacks (with built-in
|
|
73
|
-
* Useful for inspection or to pass a subset to another layer. Each call returns a new object.
|
|
58
|
+
* Returns the current callbacks (with built-in default for defaultPrompt when not set).
|
|
74
59
|
*
|
|
75
|
-
* @returns Copy of the effective callbacks
|
|
60
|
+
* @returns Copy of the effective callbacks.
|
|
76
61
|
*/
|
|
77
62
|
declare function getCallbacks(): ScenvCallbacks;
|
|
78
63
|
/**
|
|
@@ -108,6 +93,19 @@ declare function resetConfig(): void;
|
|
|
108
93
|
*/
|
|
109
94
|
declare function resetLogState(): void;
|
|
110
95
|
|
|
96
|
+
/**
|
|
97
|
+
* Returns the current in-memory context (key → value). Used during resolution before file contexts.
|
|
98
|
+
* Modifying the returned object mutates the store.
|
|
99
|
+
*/
|
|
100
|
+
declare function getInMemoryContext(): Record<string, string>;
|
|
101
|
+
/**
|
|
102
|
+
* Sets a key-value pair in the in-memory context. Used when saving after prompt or save() when saveContextTo is unset, and always updated when a value is saved so the next get() sees it.
|
|
103
|
+
*/
|
|
104
|
+
declare function setInMemoryContext(key: string, value: string): void;
|
|
105
|
+
/**
|
|
106
|
+
* Clears the in-memory context. Mainly for tests. Call in beforeEach to get a clean slate.
|
|
107
|
+
*/
|
|
108
|
+
declare function resetInMemoryContext(): void;
|
|
111
109
|
/**
|
|
112
110
|
* Recursively discovers all `*.context.json` files under a directory. Context name is the
|
|
113
111
|
* filename without the suffix (e.g. `dev.context.json` → "dev"). First file found for a
|
|
@@ -130,14 +128,23 @@ declare function discoverContextPaths(dir: string, found?: Map<string, string>):
|
|
|
130
128
|
*/
|
|
131
129
|
declare function getContext(contextName: string, root?: string): Record<string, string>;
|
|
132
130
|
/**
|
|
133
|
-
* Loads and merges context values from the current config.
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
* resolution (set > env > context > default).
|
|
131
|
+
* Loads and merges context values from the current config. If {@link ScenvConfig.saveContextTo}
|
|
132
|
+
* is set, that context (file path or name) is loaded first; then {@link ScenvConfig.context}
|
|
133
|
+
* order. Respects {@link ScenvConfig.ignoreContext}. Later contexts overwrite earlier for the same key.
|
|
134
|
+
* Used during variable resolution (set > env > in-memory > merged context > default).
|
|
137
135
|
*
|
|
138
136
|
* @returns A flat record of key → string value. Empty if ignoreContext is true or no context loaded.
|
|
139
137
|
*/
|
|
140
138
|
declare function getMergedContextValues(): Record<string, string>;
|
|
139
|
+
/**
|
|
140
|
+
* Returns the file path used for a context name or path when saving.
|
|
141
|
+
* - 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 config.root, returns its path; else uses config.contextDir (if set) or root.
|
|
143
|
+
*
|
|
144
|
+
* @param contextName - Context name (e.g. "dev", "prod") or file path without suffix (e.g. "/path/to/myfile" → myfile.context.json).
|
|
145
|
+
* @returns Absolute path to the context JSON file.
|
|
146
|
+
*/
|
|
147
|
+
declare function getContextWritePath(contextName: string): string;
|
|
141
148
|
|
|
142
149
|
/**
|
|
143
150
|
* Return type for a variable's optional `validator` function. Use a boolean for simple
|
|
@@ -205,8 +212,8 @@ interface ScenvVariable<T> {
|
|
|
205
212
|
error?: unknown;
|
|
206
213
|
}>;
|
|
207
214
|
/**
|
|
208
|
-
* Write the value to
|
|
209
|
-
* If you don't pass a value, the last resolved value is used. Does not prompt
|
|
215
|
+
* Write the value to the save target: if config.saveContextTo is set, to that context file; otherwise to in-memory only.
|
|
216
|
+
* If you don't pass a value, the last resolved value is used. Does not prompt; it saves. The value is always stored in-memory so the next get() sees it.
|
|
210
217
|
*/
|
|
211
218
|
save(value?: T): Promise<void>;
|
|
212
219
|
}
|
|
@@ -236,9 +243,9 @@ interface ScenvVariable<T> {
|
|
|
236
243
|
* ## save()
|
|
237
244
|
*
|
|
238
245
|
* The variable has a save(value?) method. It writes the value (or the last resolved value if you omit it)
|
|
239
|
-
* to
|
|
240
|
-
*
|
|
241
|
-
*
|
|
246
|
+
* to the save target (config.saveContextTo file if set, otherwise in-memory). save() does not ask; it saves.
|
|
247
|
+
* When the user is prompted during get(), the value is always saved (to saveContextTo file if set, and always to in-memory)
|
|
248
|
+
* so a second get() on the same variable does not prompt again.
|
|
242
249
|
*
|
|
243
250
|
* @typeParam T - Value type (default string). Use a validator to coerce to number, boolean, etc.
|
|
244
251
|
* @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").
|
|
@@ -262,8 +269,7 @@ declare function scenv<T>(name: string, options?: ScenvVariableOptions<T>): Scen
|
|
|
262
269
|
* - `--ignore-env` – Set ignoreEnv to true.
|
|
263
270
|
* - `--ignore-context` – Set ignoreContext to true.
|
|
264
271
|
* - `--set key=value` or `--set=key=value` – Add to set overrides (multiple allowed).
|
|
265
|
-
* - `--save-
|
|
266
|
-
* - `--save-context-to name` – saveContextTo.
|
|
272
|
+
* - `--save-context-to pathOrName` – saveContextTo (path or context name without .context.json).
|
|
267
273
|
* - `--context-dir path` – contextDir (directory to save context files to by default).
|
|
268
274
|
* - `--log-level level`, `--log level`, `--log=level` – logLevel.
|
|
269
275
|
*
|
|
@@ -272,4 +278,4 @@ declare function scenv<T>(name: string, options?: ScenvVariableOptions<T>): Scen
|
|
|
272
278
|
*/
|
|
273
279
|
declare function parseScenvArgs(argv: string[]): Partial<ScenvConfig>;
|
|
274
280
|
|
|
275
|
-
export { type DefaultPromptFn, type GetOptions, LOG_LEVELS, type LogLevel, type PromptMode, type
|
|
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 };
|
package/dist/index.js
CHANGED
|
@@ -4,19 +4,6 @@ import { dirname, join } from "path";
|
|
|
4
4
|
|
|
5
5
|
// src/prompt-default.ts
|
|
6
6
|
import { createInterface } from "readline";
|
|
7
|
-
function ask(message) {
|
|
8
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
9
|
-
return new Promise((resolve, reject) => {
|
|
10
|
-
rl.question(message, (answer) => {
|
|
11
|
-
rl.close();
|
|
12
|
-
resolve(answer.trim());
|
|
13
|
-
});
|
|
14
|
-
rl.on("error", (err) => {
|
|
15
|
-
rl.close();
|
|
16
|
-
reject(err);
|
|
17
|
-
});
|
|
18
|
-
});
|
|
19
|
-
}
|
|
20
7
|
function defaultPrompt(name, defaultValue) {
|
|
21
8
|
const defaultStr = defaultValue !== void 0 && defaultValue !== null ? String(defaultValue) : "";
|
|
22
9
|
const message = defaultStr ? `Enter ${name} [${defaultStr}]: ` : `Enter ${name}: `;
|
|
@@ -34,17 +21,6 @@ function defaultPrompt(name, defaultValue) {
|
|
|
34
21
|
});
|
|
35
22
|
});
|
|
36
23
|
}
|
|
37
|
-
async function defaultAskWhetherToSave(name, _value) {
|
|
38
|
-
const answer = await ask(`Save "${name}" for next time? (y/n): `);
|
|
39
|
-
const v = answer.toLowerCase();
|
|
40
|
-
return v === "y" || v === "yes" || v === "1" || v === "true";
|
|
41
|
-
}
|
|
42
|
-
async function defaultAskContext(name, contextNames) {
|
|
43
|
-
const hint = contextNames.length > 0 ? ` (${contextNames.join(", ")})` : "";
|
|
44
|
-
const answer = await ask(`Save "${name}" to which context?${hint}: `);
|
|
45
|
-
if (answer) return answer;
|
|
46
|
-
return contextNames[0] ?? "default";
|
|
47
|
-
}
|
|
48
24
|
|
|
49
25
|
// src/config.ts
|
|
50
26
|
var LOG_LEVELS = ["none", "trace", "debug", "info", "warn", "error"];
|
|
@@ -55,7 +31,6 @@ var envKeyMap = {
|
|
|
55
31
|
SCENV_PROMPT: "prompt",
|
|
56
32
|
SCENV_IGNORE_ENV: "ignoreEnv",
|
|
57
33
|
SCENV_IGNORE_CONTEXT: "ignoreContext",
|
|
58
|
-
SCENV_SAVE_PROMPT: "shouldSavePrompt",
|
|
59
34
|
SCENV_SAVE_CONTEXT_TO: "saveContextTo",
|
|
60
35
|
SCENV_CONTEXT_DIR: "contextDir",
|
|
61
36
|
SCENV_LOG_LEVEL: "logLevel"
|
|
@@ -64,9 +39,7 @@ var programmaticConfig = {};
|
|
|
64
39
|
var programmaticCallbacks = {};
|
|
65
40
|
function getCallbacks() {
|
|
66
41
|
return {
|
|
67
|
-
defaultPrompt: programmaticCallbacks.defaultPrompt ?? defaultPrompt
|
|
68
|
-
onAskWhetherToSave: programmaticCallbacks.onAskWhetherToSave ?? defaultAskWhetherToSave,
|
|
69
|
-
onAskContext: programmaticCallbacks.onAskContext ?? defaultAskContext
|
|
42
|
+
defaultPrompt: programmaticCallbacks.defaultPrompt ?? defaultPrompt
|
|
70
43
|
};
|
|
71
44
|
}
|
|
72
45
|
function findConfigDir(startDir) {
|
|
@@ -89,11 +62,9 @@ function configFromEnv() {
|
|
|
89
62
|
out[configKey] = val.split(",").map((s) => s.trim()).filter(Boolean);
|
|
90
63
|
} else if (configKey === "ignoreEnv" || configKey === "ignoreContext") {
|
|
91
64
|
out[configKey] = val === "1" || val === "true" || val.toLowerCase() === "yes";
|
|
92
|
-
} else if (configKey === "prompt"
|
|
65
|
+
} else if (configKey === "prompt") {
|
|
93
66
|
const v = val.toLowerCase();
|
|
94
|
-
if (
|
|
95
|
-
out[configKey] = v;
|
|
96
|
-
if (configKey === "shouldSavePrompt" && (v === "always" || v === "never" || v === "ask"))
|
|
67
|
+
if (v === "always" || v === "never" || v === "fallback" || v === "no-env")
|
|
97
68
|
out[configKey] = v;
|
|
98
69
|
} else if (configKey === "saveContextTo") {
|
|
99
70
|
out.saveContextTo = val;
|
|
@@ -136,8 +107,6 @@ function loadConfigFile(configDir) {
|
|
|
136
107
|
out.ignoreContext = parsed.ignoreContext;
|
|
137
108
|
if (parsed.set && typeof parsed.set === "object" && !Array.isArray(parsed.set))
|
|
138
109
|
out.set = parsed.set;
|
|
139
|
-
if (typeof (parsed.shouldSavePrompt ?? parsed.savePrompt) === "string" && ["always", "never", "ask"].includes(parsed.shouldSavePrompt ?? parsed.savePrompt))
|
|
140
|
-
out.shouldSavePrompt = parsed.shouldSavePrompt ?? parsed.savePrompt;
|
|
141
110
|
if (typeof parsed.saveContextTo === "string")
|
|
142
111
|
out.saveContextTo = parsed.saveContextTo;
|
|
143
112
|
if (typeof parsed.contextDir === "string") out.contextDir = parsed.contextDir;
|
|
@@ -221,16 +190,41 @@ var configLoadedLogged = false;
|
|
|
221
190
|
function resetLogState() {
|
|
222
191
|
configLoadedLogged = false;
|
|
223
192
|
}
|
|
193
|
+
var CONFIG_LOG_KEYS = [
|
|
194
|
+
"root",
|
|
195
|
+
"context",
|
|
196
|
+
"prompt",
|
|
197
|
+
"ignoreEnv",
|
|
198
|
+
"ignoreContext",
|
|
199
|
+
"set",
|
|
200
|
+
"saveContextTo",
|
|
201
|
+
"contextDir",
|
|
202
|
+
"logLevel"
|
|
203
|
+
];
|
|
204
|
+
function configForLog(config) {
|
|
205
|
+
return Object.fromEntries(
|
|
206
|
+
CONFIG_LOG_KEYS.filter((k) => config[k] !== void 0).map((k) => [k, config[k]])
|
|
207
|
+
);
|
|
208
|
+
}
|
|
224
209
|
function logConfigLoaded(config) {
|
|
225
210
|
if (configLoadedLogged) return;
|
|
226
|
-
if (getLevelNum() < LEVEL_NUM.info) return;
|
|
227
211
|
configLoadedLogged = true;
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
212
|
+
const levelNum = LEVEL_NUM[config.logLevel ?? "none"];
|
|
213
|
+
if (levelNum >= LEVEL_NUM.info) {
|
|
214
|
+
const parts = [
|
|
215
|
+
"root=" + (config.root ?? "(cwd)"),
|
|
216
|
+
"context=" + JSON.stringify(config.context ?? [])
|
|
217
|
+
];
|
|
218
|
+
if (config.prompt !== void 0) parts.push("prompt=" + config.prompt);
|
|
219
|
+
if (config.ignoreEnv === true) parts.push("ignoreEnv=true");
|
|
220
|
+
if (config.ignoreContext === true) parts.push("ignoreContext=true");
|
|
221
|
+
if (config.saveContextTo !== void 0) parts.push("saveContextTo=" + config.saveContextTo);
|
|
222
|
+
if (config.contextDir !== void 0) parts.push("contextDir=" + config.contextDir);
|
|
223
|
+
log("info", "config loaded", parts.join(" "));
|
|
224
|
+
}
|
|
225
|
+
if (levelNum >= LEVEL_NUM.debug) {
|
|
226
|
+
log("debug", "config (full)", JSON.stringify(configForLog(config)));
|
|
227
|
+
}
|
|
234
228
|
}
|
|
235
229
|
function log(level, msg, ...args) {
|
|
236
230
|
const configured = getLevelNum();
|
|
@@ -250,10 +244,21 @@ import {
|
|
|
250
244
|
writeFileSync,
|
|
251
245
|
mkdirSync,
|
|
252
246
|
readdirSync,
|
|
253
|
-
statSync
|
|
247
|
+
statSync,
|
|
248
|
+
existsSync as existsSync2
|
|
254
249
|
} from "fs";
|
|
255
|
-
import { join as join2, dirname as dirname2, isAbsolute } from "path";
|
|
250
|
+
import { join as join2, dirname as dirname2, isAbsolute, sep } from "path";
|
|
256
251
|
var CONTEXT_SUFFIX = ".context.json";
|
|
252
|
+
var inMemoryContext = {};
|
|
253
|
+
function getInMemoryContext() {
|
|
254
|
+
return inMemoryContext;
|
|
255
|
+
}
|
|
256
|
+
function setInMemoryContext(key, value) {
|
|
257
|
+
inMemoryContext[key] = value;
|
|
258
|
+
}
|
|
259
|
+
function resetInMemoryContext() {
|
|
260
|
+
inMemoryContext = {};
|
|
261
|
+
}
|
|
257
262
|
function discoverContextPathsInternal(dir, found) {
|
|
258
263
|
let entries;
|
|
259
264
|
try {
|
|
@@ -321,6 +326,14 @@ function getMergedContextValues() {
|
|
|
321
326
|
const root = config.root ?? process.cwd();
|
|
322
327
|
const paths = discoverContextPaths(root);
|
|
323
328
|
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
|
+
}
|
|
324
337
|
for (const contextName of config.context ?? []) {
|
|
325
338
|
const filePath = paths.get(contextName);
|
|
326
339
|
if (!filePath) {
|
|
@@ -348,6 +361,9 @@ function getMergedContextValues() {
|
|
|
348
361
|
return out;
|
|
349
362
|
}
|
|
350
363
|
function getContextWritePath(contextName) {
|
|
364
|
+
if (isAbsolute(contextName) || contextName.includes(sep)) {
|
|
365
|
+
return contextName.endsWith(CONTEXT_SUFFIX) ? contextName : contextName + CONTEXT_SUFFIX;
|
|
366
|
+
}
|
|
351
367
|
const config = loadConfig();
|
|
352
368
|
const root = config.root ?? process.cwd();
|
|
353
369
|
const paths = discoverContextPaths(root);
|
|
@@ -356,6 +372,26 @@ function getContextWritePath(contextName) {
|
|
|
356
372
|
const saveDir = config.contextDir ? isAbsolute(config.contextDir) ? config.contextDir : join2(root, config.contextDir) : root;
|
|
357
373
|
return join2(saveDir, `${contextName}${CONTEXT_SUFFIX}`);
|
|
358
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
|
+
}
|
|
394
|
+
}
|
|
359
395
|
function writeToContext(contextName, key, value) {
|
|
360
396
|
const path = getContextWritePath(contextName);
|
|
361
397
|
let data = {};
|
|
@@ -452,6 +488,12 @@ function scenv(name, options = {}) {
|
|
|
452
488
|
return { raw, source: "env" };
|
|
453
489
|
}
|
|
454
490
|
}
|
|
491
|
+
const mem = getInMemoryContext();
|
|
492
|
+
if (mem[key] !== void 0) {
|
|
493
|
+
log("trace", `resolveRaw: in-memory hit key=${key}`);
|
|
494
|
+
const raw = resolveContextReference(mem[key], key);
|
|
495
|
+
return { raw, source: "context" };
|
|
496
|
+
}
|
|
455
497
|
if (!config.ignoreContext) {
|
|
456
498
|
log("trace", "resolveRaw: checking context");
|
|
457
499
|
const ctx = getMergedContextValues();
|
|
@@ -537,36 +579,11 @@ function scenv(name, options = {}) {
|
|
|
537
579
|
const final = validated.data;
|
|
538
580
|
if (wasPrompted) {
|
|
539
581
|
const config = loadConfig();
|
|
540
|
-
|
|
541
|
-
if (
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
const callbacks2 = getCallbacks();
|
|
545
|
-
if (typeof callbacks2.onAskWhetherToSave !== "function") {
|
|
546
|
-
throw new Error(
|
|
547
|
-
`shouldSavePrompt is "ask" but onAskWhetherToSave callback is not set. Configure callbacks via configure({ callbacks: { onAskWhetherToSave: ... } }).`
|
|
548
|
-
);
|
|
549
|
-
}
|
|
550
|
-
doSave = await callbacks2.onAskWhetherToSave(name, final);
|
|
551
|
-
} else {
|
|
552
|
-
doSave = true;
|
|
582
|
+
setInMemoryContext(key, String(final));
|
|
583
|
+
if (config.saveContextTo) {
|
|
584
|
+
writeToContext(config.saveContextTo, key, String(final));
|
|
585
|
+
log("info", `Saved key=${key} to saveContextTo ${config.saveContextTo}`);
|
|
553
586
|
}
|
|
554
|
-
if (!doSave) return final;
|
|
555
|
-
const callbacks = getCallbacks();
|
|
556
|
-
const contextNames = config.context ?? [];
|
|
557
|
-
let ctxToSave;
|
|
558
|
-
if (config.saveContextTo === "ask") {
|
|
559
|
-
if (typeof callbacks.onAskContext !== "function") {
|
|
560
|
-
throw new Error(
|
|
561
|
-
`saveContextTo is "ask" but onAskContext callback is not set. Configure callbacks via configure({ callbacks: { onAskContext: ... } }).`
|
|
562
|
-
);
|
|
563
|
-
}
|
|
564
|
-
ctxToSave = await callbacks.onAskContext(name, contextNames);
|
|
565
|
-
} else {
|
|
566
|
-
ctxToSave = config.saveContextTo ?? contextNames[0] ?? "default";
|
|
567
|
-
}
|
|
568
|
-
writeToContext(ctxToSave, key, String(final));
|
|
569
|
-
log("info", `Saved key=${key} to context ${ctxToSave}`);
|
|
570
587
|
}
|
|
571
588
|
return final;
|
|
572
589
|
}
|
|
@@ -587,22 +604,11 @@ function scenv(name, options = {}) {
|
|
|
587
604
|
throw new Error(errMsg);
|
|
588
605
|
}
|
|
589
606
|
const config = loadConfig();
|
|
590
|
-
|
|
591
|
-
if (
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
throw new Error(
|
|
595
|
-
`saveContextTo is "ask" but onAskContext callback is not set. Configure callbacks via configure({ callbacks: { onAskContext: ... } }).`
|
|
596
|
-
);
|
|
597
|
-
}
|
|
598
|
-
contextName = await callbacks.onAskContext(
|
|
599
|
-
name,
|
|
600
|
-
config.context ?? []
|
|
601
|
-
);
|
|
607
|
+
setInMemoryContext(key, String(validated.data));
|
|
608
|
+
if (config.saveContextTo) {
|
|
609
|
+
writeToContext(config.saveContextTo, key, String(validated.data));
|
|
610
|
+
log("info", `Saved key=${key} to saveContextTo ${config.saveContextTo}`);
|
|
602
611
|
}
|
|
603
|
-
if (!contextName) contextName = config.context?.[0] ?? "default";
|
|
604
|
-
writeToContext(contextName, key, String(validated.data));
|
|
605
|
-
log("info", `Saved key=${key} to context ${contextName}`);
|
|
606
612
|
}
|
|
607
613
|
return { get, safeGet, save };
|
|
608
614
|
}
|
|
@@ -633,11 +639,6 @@ function parseScenvArgs(argv) {
|
|
|
633
639
|
config.set = config.set ?? {};
|
|
634
640
|
config.set[pair.slice(0, eq).trim()] = pair.slice(eq + 1).trim();
|
|
635
641
|
}
|
|
636
|
-
} else if (arg === "--save-prompt" && argv[i + 1] !== void 0) {
|
|
637
|
-
const v = argv[++i].toLowerCase();
|
|
638
|
-
if (["always", "never", "ask"].includes(v)) {
|
|
639
|
-
config.shouldSavePrompt = v;
|
|
640
|
-
}
|
|
641
642
|
} else if (arg === "--save-context-to" && argv[i + 1] !== void 0) {
|
|
642
643
|
config.saveContextTo = argv[++i];
|
|
643
644
|
} else if (arg === "--context-dir" && argv[i + 1] !== void 0) {
|
|
@@ -670,10 +671,14 @@ export {
|
|
|
670
671
|
discoverContextPaths,
|
|
671
672
|
getCallbacks,
|
|
672
673
|
getContext,
|
|
674
|
+
getContextWritePath,
|
|
675
|
+
getInMemoryContext,
|
|
673
676
|
getMergedContextValues,
|
|
674
677
|
loadConfig,
|
|
675
678
|
parseScenvArgs,
|
|
676
679
|
resetConfig,
|
|
680
|
+
resetInMemoryContext,
|
|
677
681
|
resetLogState,
|
|
678
|
-
scenv
|
|
682
|
+
scenv,
|
|
683
|
+
setInMemoryContext
|
|
679
684
|
};
|