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 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" || configKey === "shouldSavePrompt") {
105
+ } else if (configKey === "prompt") {
129
106
  const v = val.toLowerCase();
130
- if (configKey === "prompt" && (v === "always" || v === "never" || v === "fallback" || v === "no-env"))
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
- log(
265
- "info",
266
- "config loaded",
267
- "root=" + (config.root ?? "(cwd)"),
268
- "context=" + JSON.stringify(config.context ?? [])
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
- const mode = config.shouldSavePrompt ?? (config.prompt === "never" ? "never" : "ask");
571
- if (mode === "never") return final;
572
- let doSave;
573
- if (mode === "ask") {
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
- let contextName = config.saveContextTo;
621
- if (contextName === "ask") {
622
- const callbacks = getCallbacks();
623
- if (typeof callbacks.onAskContext !== "function") {
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
- /** After a prompt: "never" = don't save, "always" = save without asking, "ask" = call onAskWhetherToSave (then save if true). */
42
- shouldSavePrompt?: SavePromptMode;
43
- /** Target context for saving: a context name, or `"ask"` to use {@link ScenvCallbacks.onAskContext}. */
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. All have built-in readline defaults when not set.
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 defaults merged in for any unset callback).
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: defaultPrompt, onAskWhetherToSave, onAskContext (always defined).
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. Respects {@link ScenvConfig.context}
134
- * order and {@link ScenvConfig.ignoreContext}. Each context file is a JSON object of string
135
- * key-value pairs; later contexts overwrite earlier for the same key. Used during variable
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 a context file (e.g. for next run). Target context comes from config.saveContextTo or onAskContext.
209
- * If you don't pass a value, the last resolved value is used. Does not prompt "save?"; it saves.
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 a context file. The target context comes from config.saveContextTo or from the onAskContext callback
240
- * when saveContextTo is "ask". save() does not ask "save?"; it saves. Optional "save after prompt" behavior
241
- * is controlled by config.shouldSavePrompt and callbacks.onAskWhetherToSave.
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-prompt always|never|ask` – shouldSavePrompt.
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 SavePromptMode, type ScenvCallbacks, type ScenvConfig, type ScenvVariable, configure, discoverContextPaths, getCallbacks, getContext, getMergedContextValues, loadConfig, parseScenvArgs, resetConfig, resetLogState, scenv };
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
- /** After a prompt: "never" = don't save, "always" = save without asking, "ask" = call onAskWhetherToSave (then save if true). */
42
- shouldSavePrompt?: SavePromptMode;
43
- /** Target context for saving: a context name, or `"ask"` to use {@link ScenvCallbacks.onAskContext}. */
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. All have built-in readline defaults when not set.
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 defaults merged in for any unset callback).
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: defaultPrompt, onAskWhetherToSave, onAskContext (always defined).
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. Respects {@link ScenvConfig.context}
134
- * order and {@link ScenvConfig.ignoreContext}. Each context file is a JSON object of string
135
- * key-value pairs; later contexts overwrite earlier for the same key. Used during variable
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 a context file (e.g. for next run). Target context comes from config.saveContextTo or onAskContext.
209
- * If you don't pass a value, the last resolved value is used. Does not prompt "save?"; it saves.
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 a context file. The target context comes from config.saveContextTo or from the onAskContext callback
240
- * when saveContextTo is "ask". save() does not ask "save?"; it saves. Optional "save after prompt" behavior
241
- * is controlled by config.shouldSavePrompt and callbacks.onAskWhetherToSave.
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-prompt always|never|ask` – shouldSavePrompt.
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 SavePromptMode, type ScenvCallbacks, type ScenvConfig, type ScenvVariable, configure, discoverContextPaths, getCallbacks, getContext, getMergedContextValues, loadConfig, parseScenvArgs, resetConfig, resetLogState, scenv };
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" || configKey === "shouldSavePrompt") {
65
+ } else if (configKey === "prompt") {
93
66
  const v = val.toLowerCase();
94
- if (configKey === "prompt" && (v === "always" || v === "never" || v === "fallback" || v === "no-env"))
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
- log(
229
- "info",
230
- "config loaded",
231
- "root=" + (config.root ?? "(cwd)"),
232
- "context=" + JSON.stringify(config.context ?? [])
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
- const mode = config.shouldSavePrompt ?? (config.prompt === "never" ? "never" : "ask");
541
- if (mode === "never") return final;
542
- let doSave;
543
- if (mode === "ask") {
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
- let contextName = config.saveContextTo;
591
- if (contextName === "ask") {
592
- const callbacks = getCallbacks();
593
- if (typeof callbacks.onAskContext !== "function") {
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
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scenv",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "Environment and context variables with runtime-configurable resolution",
5
5
  "repository": {
6
6
  "type": "git",