scenv 0.6.1 → 0.8.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,29 +44,16 @@ 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}: `;
59
50
  const rl = (0, import_node_readline.createInterface)({ input: process.stdin, output: process.stdout });
60
- return new Promise((resolve, reject) => {
51
+ return new Promise((resolve2, reject) => {
61
52
  rl.question(message, (answer) => {
62
53
  rl.close();
63
54
  const trimmed = answer.trim();
64
55
  const value = trimmed !== "" ? trimmed : defaultStr;
65
- resolve(value);
56
+ resolve2(value);
66
57
  });
67
58
  rl.on("error", (err) => {
68
59
  rl.close();
@@ -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,18 +71,15 @@ 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
- SCENV_CONTEXT_DIR: "contextDir",
75
+ SCENV_SAVE_MODE: "saveMode",
97
76
  SCENV_LOG_LEVEL: "logLevel"
98
77
  };
99
78
  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,16 +102,16 @@ 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;
136
- } else if (configKey === "contextDir") {
137
- out.contextDir = val;
111
+ } else if (configKey === "saveMode") {
112
+ const v = val.toLowerCase();
113
+ if (v === "all" || v === "prompts-only")
114
+ out[configKey] = v;
138
115
  } else if (configKey === "logLevel") {
139
116
  const v = val.toLowerCase();
140
117
  if (LOG_LEVELS.includes(v)) out.logLevel = v;
@@ -172,12 +149,10 @@ function loadConfigFile(configDir) {
172
149
  out.ignoreContext = parsed.ignoreContext;
173
150
  if (parsed.set && typeof parsed.set === "object" && !Array.isArray(parsed.set))
174
151
  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
152
  if (typeof parsed.saveContextTo === "string")
178
153
  out.saveContextTo = parsed.saveContextTo;
179
- if (typeof parsed.contextDir === "string") out.contextDir = parsed.contextDir;
180
- else if (typeof parsed.contextsDir === "string") out.contextDir = parsed.contextsDir;
154
+ if (typeof parsed.saveMode === "string" && ["all", "prompts-only"].includes(parsed.saveMode))
155
+ out.saveMode = parsed.saveMode;
181
156
  if (typeof parsed.root === "string") out.root = parsed.root;
182
157
  if (typeof parsed.logLevel === "string" && LOG_LEVELS.includes(parsed.logLevel))
183
158
  out.logLevel = parsed.logLevel;
@@ -264,9 +239,8 @@ var CONFIG_LOG_KEYS = [
264
239
  "ignoreEnv",
265
240
  "ignoreContext",
266
241
  "set",
267
- "shouldSavePrompt",
268
242
  "saveContextTo",
269
- "contextDir",
243
+ "saveMode",
270
244
  "logLevel"
271
245
  ];
272
246
  function configForLog(config) {
@@ -287,8 +261,7 @@ function logConfigLoaded(config) {
287
261
  if (config.ignoreEnv === true) parts.push("ignoreEnv=true");
288
262
  if (config.ignoreContext === true) parts.push("ignoreContext=true");
289
263
  if (config.saveContextTo !== void 0) parts.push("saveContextTo=" + config.saveContextTo);
290
- if (config.contextDir !== void 0) parts.push("contextDir=" + config.contextDir);
291
- if (config.shouldSavePrompt !== void 0) parts.push("shouldSavePrompt=" + config.shouldSavePrompt);
264
+ if (config.saveMode !== void 0) parts.push("saveMode=" + config.saveMode);
292
265
  log("info", "config loaded", parts.join(" "));
293
266
  }
294
267
  if (levelNum >= LEVEL_NUM.debug) {
@@ -311,6 +284,16 @@ function log(level, msg, ...args) {
311
284
  var import_node_fs2 = require("fs");
312
285
  var import_node_path2 = require("path");
313
286
  var CONTEXT_SUFFIX = ".context.json";
287
+ var inMemoryContext = {};
288
+ function getInMemoryContext() {
289
+ return inMemoryContext;
290
+ }
291
+ function setInMemoryContext(key, value) {
292
+ inMemoryContext[key] = value;
293
+ }
294
+ function resetInMemoryContext() {
295
+ inMemoryContext = {};
296
+ }
314
297
  function discoverContextPathsInternal(dir, found) {
315
298
  let entries;
316
299
  try {
@@ -347,10 +330,23 @@ function discoverContextPaths(dir, found = /* @__PURE__ */ new Map()) {
347
330
  return found;
348
331
  }
349
332
  function getContext(contextName, root) {
350
- const config = loadConfig();
351
- const searchRoot = root ?? config.root ?? process.cwd();
352
- const paths = discoverContextPaths(searchRoot);
353
- const filePath = paths.get(contextName);
333
+ let filePath;
334
+ if (root !== void 0) {
335
+ const paths = discoverContextPaths(root);
336
+ filePath = paths.get(contextName);
337
+ } else {
338
+ const cwd = process.cwd();
339
+ const paths = discoverContextPaths(cwd);
340
+ filePath = paths.get(contextName);
341
+ if (!filePath) {
342
+ const config = loadConfig();
343
+ const projectRoot = config.root ?? cwd;
344
+ if (projectRoot !== cwd) {
345
+ const rootPaths = discoverContextPaths(projectRoot);
346
+ filePath = rootPaths.get(contextName);
347
+ }
348
+ }
349
+ }
354
350
  if (!filePath) {
355
351
  log("trace", `getContext: context "${contextName}" not found`);
356
352
  return {};
@@ -375,8 +371,8 @@ function getMergedContextValues() {
375
371
  const config = loadConfig();
376
372
  logConfigLoaded(config);
377
373
  if (config.ignoreContext) return {};
378
- const root = config.root ?? process.cwd();
379
- const paths = discoverContextPaths(root);
374
+ const searchRoot = process.cwd();
375
+ const paths = discoverContextPaths(searchRoot);
380
376
  const out = {};
381
377
  for (const contextName of config.context ?? []) {
382
378
  const filePath = paths.get(contextName);
@@ -405,13 +401,15 @@ function getMergedContextValues() {
405
401
  return out;
406
402
  }
407
403
  function getContextWritePath(contextName) {
404
+ if ((0, import_node_path2.isAbsolute)(contextName) || contextName.includes(import_node_path2.sep)) {
405
+ return contextName.endsWith(CONTEXT_SUFFIX) ? contextName : contextName + CONTEXT_SUFFIX;
406
+ }
408
407
  const config = loadConfig();
409
- const root = config.root ?? process.cwd();
410
- const paths = discoverContextPaths(root);
408
+ const paths = discoverContextPaths(process.cwd());
411
409
  const existing = paths.get(contextName);
412
410
  if (existing) return existing;
413
- const saveDir = config.contextDir ? (0, import_node_path2.isAbsolute)(config.contextDir) ? config.contextDir : (0, import_node_path2.join)(root, config.contextDir) : root;
414
- return (0, import_node_path2.join)(saveDir, `${contextName}${CONTEXT_SUFFIX}`);
411
+ const projectRoot = config.root ?? process.cwd();
412
+ return (0, import_node_path2.join)(projectRoot, `${contextName}${CONTEXT_SUFFIX}`);
415
413
  }
416
414
  function writeToContext(contextName, key, value) {
417
415
  const path = getContextWritePath(contextName);
@@ -509,6 +507,12 @@ function scenv(name, options = {}) {
509
507
  return { raw, source: "env" };
510
508
  }
511
509
  }
510
+ const mem = getInMemoryContext();
511
+ if (mem[key] !== void 0) {
512
+ log("trace", `resolveRaw: in-memory hit key=${key}`);
513
+ const raw = resolveContextReference(mem[key], key);
514
+ return { raw, source: "context" };
515
+ }
512
516
  if (!config.ignoreContext) {
513
517
  log("trace", "resolveRaw: checking context");
514
518
  const ctx = getMergedContextValues();
@@ -592,38 +596,14 @@ function scenv(name, options = {}) {
592
596
  throw new Error(errMsg);
593
597
  }
594
598
  const final = validated.data;
595
- if (wasPrompted) {
596
- const config = loadConfig();
597
- const mode = config.shouldSavePrompt ?? (config.prompt === "never" ? "never" : "ask");
598
- if (mode === "never") return final;
599
- let doSave;
600
- if (mode === "ask") {
601
- const callbacks2 = getCallbacks();
602
- if (typeof callbacks2.onAskWhetherToSave !== "function") {
603
- throw new Error(
604
- `shouldSavePrompt is "ask" but onAskWhetherToSave callback is not set. Configure callbacks via configure({ callbacks: { onAskWhetherToSave: ... } }).`
605
- );
606
- }
607
- doSave = await callbacks2.onAskWhetherToSave(name, final);
608
- } else {
609
- doSave = true;
610
- }
611
- if (!doSave) return final;
612
- const callbacks = getCallbacks();
613
- const contextNames = config.context ?? [];
614
- let ctxToSave;
615
- if (config.saveContextTo === "ask") {
616
- if (typeof callbacks.onAskContext !== "function") {
617
- throw new Error(
618
- `saveContextTo is "ask" but onAskContext callback is not set. Configure callbacks via configure({ callbacks: { onAskContext: ... } }).`
619
- );
620
- }
621
- ctxToSave = await callbacks.onAskContext(name, contextNames);
622
- } else {
623
- ctxToSave = config.saveContextTo ?? contextNames[0] ?? "default";
599
+ const config = loadConfig();
600
+ if (wasPrompted) setInMemoryContext(key, String(final));
601
+ if (config.saveContextTo) {
602
+ const saveMode = config.saveMode ?? "all";
603
+ if (saveMode === "all" || wasPrompted) {
604
+ writeToContext(config.saveContextTo, key, String(final));
605
+ log("info", `Saved key=${key} to saveContextTo ${config.saveContextTo}`);
624
606
  }
625
- writeToContext(ctxToSave, key, String(final));
626
- log("info", `Saved key=${key} to context ${ctxToSave}`);
627
607
  }
628
608
  return final;
629
609
  }
@@ -644,33 +624,25 @@ function scenv(name, options = {}) {
644
624
  throw new Error(errMsg);
645
625
  }
646
626
  const config = loadConfig();
647
- let contextName = config.saveContextTo;
648
- if (contextName === "ask") {
649
- const callbacks = getCallbacks();
650
- if (typeof callbacks.onAskContext !== "function") {
651
- throw new Error(
652
- `saveContextTo is "ask" but onAskContext callback is not set. Configure callbacks via configure({ callbacks: { onAskContext: ... } }).`
653
- );
654
- }
655
- contextName = await callbacks.onAskContext(
656
- name,
657
- config.context ?? []
658
- );
627
+ setInMemoryContext(key, String(validated.data));
628
+ if (config.saveContextTo) {
629
+ writeToContext(config.saveContextTo, key, String(validated.data));
630
+ log("info", `Saved key=${key} to saveContextTo ${config.saveContextTo}`);
659
631
  }
660
- if (!contextName) contextName = config.context?.[0] ?? "default";
661
- writeToContext(contextName, key, String(validated.data));
662
- log("info", `Saved key=${key} to context ${contextName}`);
663
632
  }
664
633
  return { get, safeGet, save };
665
634
  }
666
635
 
667
636
  // src/cli-args.ts
637
+ var import_node_path3 = require("path");
668
638
  function parseScenvArgs(argv) {
669
639
  const config = {};
670
640
  let i = 0;
671
641
  while (i < argv.length) {
672
642
  const arg = argv[i];
673
- if (arg === "--context" && argv[i + 1] !== void 0) {
643
+ if (arg === "--root" && argv[i + 1] !== void 0) {
644
+ config.root = (0, import_node_path3.resolve)(argv[++i]);
645
+ } else if (arg === "--context" && argv[i + 1] !== void 0) {
674
646
  config.context = argv[++i].split(",").map((s) => s.trim()).filter(Boolean);
675
647
  } else if (arg === "--add-context" && argv[i + 1] !== void 0) {
676
648
  config.addContext = argv[++i].split(",").map((s) => s.trim()).filter(Boolean);
@@ -690,15 +662,13 @@ function parseScenvArgs(argv) {
690
662
  config.set = config.set ?? {};
691
663
  config.set[pair.slice(0, eq).trim()] = pair.slice(eq + 1).trim();
692
664
  }
693
- } else if (arg === "--save-prompt" && argv[i + 1] !== void 0) {
694
- const v = argv[++i].toLowerCase();
695
- if (["always", "never", "ask"].includes(v)) {
696
- config.shouldSavePrompt = v;
697
- }
698
665
  } else if (arg === "--save-context-to" && argv[i + 1] !== void 0) {
699
666
  config.saveContextTo = argv[++i];
700
- } else if (arg === "--context-dir" && argv[i + 1] !== void 0) {
701
- config.contextDir = argv[++i];
667
+ } else if (arg === "--save-mode" && argv[i + 1] !== void 0) {
668
+ const v = argv[++i].toLowerCase();
669
+ if (v === "all" || v === "prompts-only") {
670
+ config.saveMode = v;
671
+ }
702
672
  } else if ((arg === "--log-level" || arg === "--log") && argv[i + 1] !== void 0) {
703
673
  const v = argv[++i].toLowerCase();
704
674
  if (LOG_LEVELS.includes(v)) {
@@ -728,10 +698,14 @@ function parseScenvArgs(argv) {
728
698
  discoverContextPaths,
729
699
  getCallbacks,
730
700
  getContext,
701
+ getContextWritePath,
702
+ getInMemoryContext,
731
703
  getMergedContextValues,
732
704
  loadConfig,
733
705
  parseScenvArgs,
734
706
  resetConfig,
707
+ resetInMemoryContext,
735
708
  resetLogState,
736
- scenv
709
+ scenv,
710
+ setInMemoryContext
737
711
  });
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.
@@ -20,6 +13,12 @@ type SavePromptMode = "always" | "never" | "ask";
20
13
  declare const LOG_LEVELS: readonly ["none", "trace", "debug", "info", "warn", "error"];
21
14
  /** Log level type. `"none"` disables logging; higher values are more verbose. */
22
15
  type LogLevel = (typeof LOG_LEVELS)[number];
16
+ /**
17
+ * When to write resolved values to saveContextTo during get().
18
+ * - `"all"` – Save every resolved variable (from set, env, context, or prompt). Useful for re-running with the same values.
19
+ * - `"prompts-only"` – Save only when the user was prompted. Default is `"all"`.
20
+ */
21
+ type SaveMode = "all" | "prompts-only";
23
22
  /**
24
23
  * Full scenv configuration. Built from file (scenv.config.json), environment (SCENV_*),
25
24
  * and programmatic config (configure()), with programmatic > env > file precedence.
@@ -38,13 +37,11 @@ interface ScenvConfig {
38
37
  ignoreContext?: boolean;
39
38
  /** Override values by key (CLI: `--set key=value`). Takes precedence over env and context. */
40
39
  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;
45
- /** Directory to save context files to when the context is not already discovered. Relative to root unless absolute. If unset, new context files are saved under root. */
46
- contextDir?: string;
47
- /** Root directory for config file search and context discovery. Default is cwd or the directory containing scenv.config.json. */
40
+ /** Optional path or context name (without .context.json) where to save resolved values. If set, all saves go here and this context is used before prompting. If unset, values are saved to an in-memory context only (same process). */
41
+ saveContextTo?: string;
42
+ /** When to write to saveContextTo during get(): "all" (default) saves every resolved variable; "prompts-only" saves only when the user was prompted. */
43
+ saveMode?: SaveMode;
44
+ /** Project root: where to search for scenv.config.json and where new context files are saved. Defaults to the directory containing scenv.config.json (when found) or cwd. Context files are discovered from cwd. */
48
45
  root?: string;
49
46
  /** Logging level. Default is `"none"`. Messages go to stderr. */
50
47
  logLevel?: LogLevel;
@@ -57,22 +54,16 @@ interface ScenvConfig {
57
54
  */
58
55
  type DefaultPromptFn = (name: string, defaultValue: unknown) => unknown | Promise<unknown>;
59
56
  /**
60
- * Callbacks for interactive behaviour. All have built-in readline defaults when not set.
61
- * Pass to {@link configure} via `configure({ callbacks: { ... } })`.
57
+ * Callbacks for interactive behaviour. Pass to {@link configure} via `configure({ callbacks: { ... } })`.
62
58
  */
63
59
  interface ScenvCallbacks {
64
60
  /** Used when a variable does not define its own `prompt`. Variable-level `prompt` overrides this. */
65
61
  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
62
  }
71
63
  /**
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.
64
+ * Returns the current callbacks (with built-in default for defaultPrompt when not set).
74
65
  *
75
- * @returns Copy of the effective callbacks: defaultPrompt, onAskWhetherToSave, onAskContext (always defined).
66
+ * @returns Copy of the effective callbacks.
76
67
  */
77
68
  declare function getCallbacks(): ScenvCallbacks;
78
69
  /**
@@ -108,6 +99,19 @@ declare function resetConfig(): void;
108
99
  */
109
100
  declare function resetLogState(): void;
110
101
 
102
+ /**
103
+ * Returns the current in-memory context (key → value). Used during resolution before file contexts.
104
+ * Modifying the returned object mutates the store.
105
+ */
106
+ declare function getInMemoryContext(): Record<string, string>;
107
+ /**
108
+ * 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.
109
+ */
110
+ declare function setInMemoryContext(key: string, value: string): void;
111
+ /**
112
+ * Clears the in-memory context. Mainly for tests. Call in beforeEach to get a clean slate.
113
+ */
114
+ declare function resetInMemoryContext(): void;
111
115
  /**
112
116
  * Recursively discovers all `*.context.json` files under a directory. Context name is the
113
117
  * filename without the suffix (e.g. `dev.context.json` → "dev"). First file found for a
@@ -122,22 +126,32 @@ declare function discoverContextPaths(dir: string, found?: Map<string, string>):
122
126
  /**
123
127
  * Loads key-value pairs from a single context file. Used when resolving @context:key references.
124
128
  * Does not depend on config.context or ignoreContext; the context file is read if it exists
125
- * under the config root.
129
+ * under the search directory.
126
130
  *
127
131
  * @param contextName - Name of the context (e.g. "prod", "dev") — file is contextName.context.json.
128
- * @param root - Optional root directory to search. If omitted, uses loadConfig().root.
132
+ * @param root - Optional directory to search. If omitted, searches from process.cwd() then from project root (config.root) if the context is not found under cwd.
129
133
  * @returns A flat record of key → string value from that context file. Empty if file not found or invalid.
130
134
  */
131
135
  declare function getContext(contextName: string, root?: string): Record<string, string>;
132
136
  /**
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).
137
+ * Loads and merges context values from the current config from {@link ScenvConfig.context} only.
138
+ * saveContextTo is not used for resolution; it is only a write target. To use the same context
139
+ * for reading, add it explicitly via context or addContext.
140
+ * Respects {@link ScenvConfig.ignoreContext}. Later contexts overwrite earlier for the same key.
141
+ * Used during variable resolution (set > env > in-memory > merged context > default).
137
142
  *
138
143
  * @returns A flat record of key → string value. Empty if ignoreContext is true or no context loaded.
139
144
  */
140
145
  declare function getMergedContextValues(): Record<string, string>;
146
+ /**
147
+ * Returns the file path used for a context name or path when saving.
148
+ * - If contextName is path-like (absolute or contains path separator), returns that path with .context.json appended if not already present.
149
+ * - Otherwise, if that context was already discovered under cwd, returns its path; else saves under project root (config.root or cwd).
150
+ *
151
+ * @param contextName - Context name (e.g. "dev", "prod") or file path without suffix (e.g. "/path/to/myfile" → myfile.context.json).
152
+ * @returns Absolute path to the context JSON file.
153
+ */
154
+ declare function getContextWritePath(contextName: string): string;
141
155
 
142
156
  /**
143
157
  * Return type for a variable's optional `validator` function. Use a boolean for simple
@@ -205,8 +219,8 @@ interface ScenvVariable<T> {
205
219
  error?: unknown;
206
220
  }>;
207
221
  /**
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.
222
+ * Write the value to the save target: if config.saveContextTo is set, to that context file; otherwise to in-memory only.
223
+ * 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
224
  */
211
225
  save(value?: T): Promise<void>;
212
226
  }
@@ -236,9 +250,9 @@ interface ScenvVariable<T> {
236
250
  * ## save()
237
251
  *
238
252
  * 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.
253
+ * to the save target (config.saveContextTo file if set, otherwise in-memory). save() does not ask; it saves.
254
+ * When the user is prompted during get(), the value is always saved (to saveContextTo file if set, and always to in-memory)
255
+ * so a second get() on the same variable does not prompt again.
242
256
  *
243
257
  * @typeParam T - Value type (default string). Use a validator to coerce to number, boolean, etc.
244
258
  * @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").
@@ -256,15 +270,15 @@ declare function scenv<T>(name: string, options?: ScenvVariableOptions<T>): Scen
256
270
  * Typical use: `configure(parseScenvArgs(process.argv.slice(2)))`. Unrecognized flags are ignored.
257
271
  *
258
272
  * Supported flags:
273
+ * - `--root path` – Project root (where to find scenv.config.json and where new context files are saved). Relative paths are resolved from cwd.
259
274
  * - `--context a,b,c` – Set context list (replace).
260
275
  * - `--add-context x,y` – Add context names.
261
276
  * - `--prompt always|never|fallback|no-env` – Prompt mode.
262
277
  * - `--ignore-env` – Set ignoreEnv to true.
263
278
  * - `--ignore-context` – Set ignoreContext to true.
264
279
  * - `--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.
267
- * - `--context-dir path` – contextDir (directory to save context files to by default).
280
+ * - `--save-context-to pathOrName` – saveContextTo (path or context name without .context.json).
281
+ * - `--save-mode all|prompts-only` – When to write to saveContextTo during get(); default is all.
268
282
  * - `--log-level level`, `--log level`, `--log=level` – logLevel.
269
283
  *
270
284
  * @param argv - Array of CLI arguments (e.g. process.argv.slice(2)).
@@ -272,4 +286,4 @@ declare function scenv<T>(name: string, options?: ScenvVariableOptions<T>): Scen
272
286
  */
273
287
  declare function parseScenvArgs(argv: string[]): Partial<ScenvConfig>;
274
288
 
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 };
289
+ export { type DefaultPromptFn, type GetOptions, LOG_LEVELS, type LogLevel, type PromptMode, type SaveMode, 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.
@@ -20,6 +13,12 @@ type SavePromptMode = "always" | "never" | "ask";
20
13
  declare const LOG_LEVELS: readonly ["none", "trace", "debug", "info", "warn", "error"];
21
14
  /** Log level type. `"none"` disables logging; higher values are more verbose. */
22
15
  type LogLevel = (typeof LOG_LEVELS)[number];
16
+ /**
17
+ * When to write resolved values to saveContextTo during get().
18
+ * - `"all"` – Save every resolved variable (from set, env, context, or prompt). Useful for re-running with the same values.
19
+ * - `"prompts-only"` – Save only when the user was prompted. Default is `"all"`.
20
+ */
21
+ type SaveMode = "all" | "prompts-only";
23
22
  /**
24
23
  * Full scenv configuration. Built from file (scenv.config.json), environment (SCENV_*),
25
24
  * and programmatic config (configure()), with programmatic > env > file precedence.
@@ -38,13 +37,11 @@ interface ScenvConfig {
38
37
  ignoreContext?: boolean;
39
38
  /** Override values by key (CLI: `--set key=value`). Takes precedence over env and context. */
40
39
  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;
45
- /** Directory to save context files to when the context is not already discovered. Relative to root unless absolute. If unset, new context files are saved under root. */
46
- contextDir?: string;
47
- /** Root directory for config file search and context discovery. Default is cwd or the directory containing scenv.config.json. */
40
+ /** Optional path or context name (without .context.json) where to save resolved values. If set, all saves go here and this context is used before prompting. If unset, values are saved to an in-memory context only (same process). */
41
+ saveContextTo?: string;
42
+ /** When to write to saveContextTo during get(): "all" (default) saves every resolved variable; "prompts-only" saves only when the user was prompted. */
43
+ saveMode?: SaveMode;
44
+ /** Project root: where to search for scenv.config.json and where new context files are saved. Defaults to the directory containing scenv.config.json (when found) or cwd. Context files are discovered from cwd. */
48
45
  root?: string;
49
46
  /** Logging level. Default is `"none"`. Messages go to stderr. */
50
47
  logLevel?: LogLevel;
@@ -57,22 +54,16 @@ interface ScenvConfig {
57
54
  */
58
55
  type DefaultPromptFn = (name: string, defaultValue: unknown) => unknown | Promise<unknown>;
59
56
  /**
60
- * Callbacks for interactive behaviour. All have built-in readline defaults when not set.
61
- * Pass to {@link configure} via `configure({ callbacks: { ... } })`.
57
+ * Callbacks for interactive behaviour. Pass to {@link configure} via `configure({ callbacks: { ... } })`.
62
58
  */
63
59
  interface ScenvCallbacks {
64
60
  /** Used when a variable does not define its own `prompt`. Variable-level `prompt` overrides this. */
65
61
  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
62
  }
71
63
  /**
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.
64
+ * Returns the current callbacks (with built-in default for defaultPrompt when not set).
74
65
  *
75
- * @returns Copy of the effective callbacks: defaultPrompt, onAskWhetherToSave, onAskContext (always defined).
66
+ * @returns Copy of the effective callbacks.
76
67
  */
77
68
  declare function getCallbacks(): ScenvCallbacks;
78
69
  /**
@@ -108,6 +99,19 @@ declare function resetConfig(): void;
108
99
  */
109
100
  declare function resetLogState(): void;
110
101
 
102
+ /**
103
+ * Returns the current in-memory context (key → value). Used during resolution before file contexts.
104
+ * Modifying the returned object mutates the store.
105
+ */
106
+ declare function getInMemoryContext(): Record<string, string>;
107
+ /**
108
+ * 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.
109
+ */
110
+ declare function setInMemoryContext(key: string, value: string): void;
111
+ /**
112
+ * Clears the in-memory context. Mainly for tests. Call in beforeEach to get a clean slate.
113
+ */
114
+ declare function resetInMemoryContext(): void;
111
115
  /**
112
116
  * Recursively discovers all `*.context.json` files under a directory. Context name is the
113
117
  * filename without the suffix (e.g. `dev.context.json` → "dev"). First file found for a
@@ -122,22 +126,32 @@ declare function discoverContextPaths(dir: string, found?: Map<string, string>):
122
126
  /**
123
127
  * Loads key-value pairs from a single context file. Used when resolving @context:key references.
124
128
  * Does not depend on config.context or ignoreContext; the context file is read if it exists
125
- * under the config root.
129
+ * under the search directory.
126
130
  *
127
131
  * @param contextName - Name of the context (e.g. "prod", "dev") — file is contextName.context.json.
128
- * @param root - Optional root directory to search. If omitted, uses loadConfig().root.
132
+ * @param root - Optional directory to search. If omitted, searches from process.cwd() then from project root (config.root) if the context is not found under cwd.
129
133
  * @returns A flat record of key → string value from that context file. Empty if file not found or invalid.
130
134
  */
131
135
  declare function getContext(contextName: string, root?: string): Record<string, string>;
132
136
  /**
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).
137
+ * Loads and merges context values from the current config from {@link ScenvConfig.context} only.
138
+ * saveContextTo is not used for resolution; it is only a write target. To use the same context
139
+ * for reading, add it explicitly via context or addContext.
140
+ * Respects {@link ScenvConfig.ignoreContext}. Later contexts overwrite earlier for the same key.
141
+ * Used during variable resolution (set > env > in-memory > merged context > default).
137
142
  *
138
143
  * @returns A flat record of key → string value. Empty if ignoreContext is true or no context loaded.
139
144
  */
140
145
  declare function getMergedContextValues(): Record<string, string>;
146
+ /**
147
+ * Returns the file path used for a context name or path when saving.
148
+ * - If contextName is path-like (absolute or contains path separator), returns that path with .context.json appended if not already present.
149
+ * - Otherwise, if that context was already discovered under cwd, returns its path; else saves under project root (config.root or cwd).
150
+ *
151
+ * @param contextName - Context name (e.g. "dev", "prod") or file path without suffix (e.g. "/path/to/myfile" → myfile.context.json).
152
+ * @returns Absolute path to the context JSON file.
153
+ */
154
+ declare function getContextWritePath(contextName: string): string;
141
155
 
142
156
  /**
143
157
  * Return type for a variable's optional `validator` function. Use a boolean for simple
@@ -205,8 +219,8 @@ interface ScenvVariable<T> {
205
219
  error?: unknown;
206
220
  }>;
207
221
  /**
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.
222
+ * Write the value to the save target: if config.saveContextTo is set, to that context file; otherwise to in-memory only.
223
+ * 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
224
  */
211
225
  save(value?: T): Promise<void>;
212
226
  }
@@ -236,9 +250,9 @@ interface ScenvVariable<T> {
236
250
  * ## save()
237
251
  *
238
252
  * 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.
253
+ * to the save target (config.saveContextTo file if set, otherwise in-memory). save() does not ask; it saves.
254
+ * When the user is prompted during get(), the value is always saved (to saveContextTo file if set, and always to in-memory)
255
+ * so a second get() on the same variable does not prompt again.
242
256
  *
243
257
  * @typeParam T - Value type (default string). Use a validator to coerce to number, boolean, etc.
244
258
  * @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").
@@ -256,15 +270,15 @@ declare function scenv<T>(name: string, options?: ScenvVariableOptions<T>): Scen
256
270
  * Typical use: `configure(parseScenvArgs(process.argv.slice(2)))`. Unrecognized flags are ignored.
257
271
  *
258
272
  * Supported flags:
273
+ * - `--root path` – Project root (where to find scenv.config.json and where new context files are saved). Relative paths are resolved from cwd.
259
274
  * - `--context a,b,c` – Set context list (replace).
260
275
  * - `--add-context x,y` – Add context names.
261
276
  * - `--prompt always|never|fallback|no-env` – Prompt mode.
262
277
  * - `--ignore-env` – Set ignoreEnv to true.
263
278
  * - `--ignore-context` – Set ignoreContext to true.
264
279
  * - `--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.
267
- * - `--context-dir path` – contextDir (directory to save context files to by default).
280
+ * - `--save-context-to pathOrName` – saveContextTo (path or context name without .context.json).
281
+ * - `--save-mode all|prompts-only` – When to write to saveContextTo during get(); default is all.
268
282
  * - `--log-level level`, `--log level`, `--log=level` – logLevel.
269
283
  *
270
284
  * @param argv - Array of CLI arguments (e.g. process.argv.slice(2)).
@@ -272,4 +286,4 @@ declare function scenv<T>(name: string, options?: ScenvVariableOptions<T>): Scen
272
286
  */
273
287
  declare function parseScenvArgs(argv: string[]): Partial<ScenvConfig>;
274
288
 
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 };
289
+ export { type DefaultPromptFn, type GetOptions, LOG_LEVELS, type LogLevel, type PromptMode, type SaveMode, 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,29 +4,16 @@ 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}: `;
23
10
  const rl = createInterface({ input: process.stdin, output: process.stdout });
24
- return new Promise((resolve, reject) => {
11
+ return new Promise((resolve2, reject) => {
25
12
  rl.question(message, (answer) => {
26
13
  rl.close();
27
14
  const trimmed = answer.trim();
28
15
  const value = trimmed !== "" ? trimmed : defaultStr;
29
- resolve(value);
16
+ resolve2(value);
30
17
  });
31
18
  rl.on("error", (err) => {
32
19
  rl.close();
@@ -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,18 +31,15 @@ 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
- SCENV_CONTEXT_DIR: "contextDir",
35
+ SCENV_SAVE_MODE: "saveMode",
61
36
  SCENV_LOG_LEVEL: "logLevel"
62
37
  };
63
38
  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,16 +62,16 @@ 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;
100
- } else if (configKey === "contextDir") {
101
- out.contextDir = val;
71
+ } else if (configKey === "saveMode") {
72
+ const v = val.toLowerCase();
73
+ if (v === "all" || v === "prompts-only")
74
+ out[configKey] = v;
102
75
  } else if (configKey === "logLevel") {
103
76
  const v = val.toLowerCase();
104
77
  if (LOG_LEVELS.includes(v)) out.logLevel = v;
@@ -136,12 +109,10 @@ function loadConfigFile(configDir) {
136
109
  out.ignoreContext = parsed.ignoreContext;
137
110
  if (parsed.set && typeof parsed.set === "object" && !Array.isArray(parsed.set))
138
111
  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
112
  if (typeof parsed.saveContextTo === "string")
142
113
  out.saveContextTo = parsed.saveContextTo;
143
- if (typeof parsed.contextDir === "string") out.contextDir = parsed.contextDir;
144
- else if (typeof parsed.contextsDir === "string") out.contextDir = parsed.contextsDir;
114
+ if (typeof parsed.saveMode === "string" && ["all", "prompts-only"].includes(parsed.saveMode))
115
+ out.saveMode = parsed.saveMode;
145
116
  if (typeof parsed.root === "string") out.root = parsed.root;
146
117
  if (typeof parsed.logLevel === "string" && LOG_LEVELS.includes(parsed.logLevel))
147
118
  out.logLevel = parsed.logLevel;
@@ -228,9 +199,8 @@ var CONFIG_LOG_KEYS = [
228
199
  "ignoreEnv",
229
200
  "ignoreContext",
230
201
  "set",
231
- "shouldSavePrompt",
232
202
  "saveContextTo",
233
- "contextDir",
203
+ "saveMode",
234
204
  "logLevel"
235
205
  ];
236
206
  function configForLog(config) {
@@ -251,8 +221,7 @@ function logConfigLoaded(config) {
251
221
  if (config.ignoreEnv === true) parts.push("ignoreEnv=true");
252
222
  if (config.ignoreContext === true) parts.push("ignoreContext=true");
253
223
  if (config.saveContextTo !== void 0) parts.push("saveContextTo=" + config.saveContextTo);
254
- if (config.contextDir !== void 0) parts.push("contextDir=" + config.contextDir);
255
- if (config.shouldSavePrompt !== void 0) parts.push("shouldSavePrompt=" + config.shouldSavePrompt);
224
+ if (config.saveMode !== void 0) parts.push("saveMode=" + config.saveMode);
256
225
  log("info", "config loaded", parts.join(" "));
257
226
  }
258
227
  if (levelNum >= LEVEL_NUM.debug) {
@@ -277,10 +246,21 @@ import {
277
246
  writeFileSync,
278
247
  mkdirSync,
279
248
  readdirSync,
280
- statSync
249
+ statSync,
250
+ existsSync as existsSync2
281
251
  } from "fs";
282
- import { join as join2, dirname as dirname2, isAbsolute } from "path";
252
+ import { join as join2, dirname as dirname2, isAbsolute, sep } from "path";
283
253
  var CONTEXT_SUFFIX = ".context.json";
254
+ var inMemoryContext = {};
255
+ function getInMemoryContext() {
256
+ return inMemoryContext;
257
+ }
258
+ function setInMemoryContext(key, value) {
259
+ inMemoryContext[key] = value;
260
+ }
261
+ function resetInMemoryContext() {
262
+ inMemoryContext = {};
263
+ }
284
264
  function discoverContextPathsInternal(dir, found) {
285
265
  let entries;
286
266
  try {
@@ -317,10 +297,23 @@ function discoverContextPaths(dir, found = /* @__PURE__ */ new Map()) {
317
297
  return found;
318
298
  }
319
299
  function getContext(contextName, root) {
320
- const config = loadConfig();
321
- const searchRoot = root ?? config.root ?? process.cwd();
322
- const paths = discoverContextPaths(searchRoot);
323
- const filePath = paths.get(contextName);
300
+ let filePath;
301
+ if (root !== void 0) {
302
+ const paths = discoverContextPaths(root);
303
+ filePath = paths.get(contextName);
304
+ } else {
305
+ const cwd = process.cwd();
306
+ const paths = discoverContextPaths(cwd);
307
+ filePath = paths.get(contextName);
308
+ if (!filePath) {
309
+ const config = loadConfig();
310
+ const projectRoot = config.root ?? cwd;
311
+ if (projectRoot !== cwd) {
312
+ const rootPaths = discoverContextPaths(projectRoot);
313
+ filePath = rootPaths.get(contextName);
314
+ }
315
+ }
316
+ }
324
317
  if (!filePath) {
325
318
  log("trace", `getContext: context "${contextName}" not found`);
326
319
  return {};
@@ -345,8 +338,8 @@ function getMergedContextValues() {
345
338
  const config = loadConfig();
346
339
  logConfigLoaded(config);
347
340
  if (config.ignoreContext) return {};
348
- const root = config.root ?? process.cwd();
349
- const paths = discoverContextPaths(root);
341
+ const searchRoot = process.cwd();
342
+ const paths = discoverContextPaths(searchRoot);
350
343
  const out = {};
351
344
  for (const contextName of config.context ?? []) {
352
345
  const filePath = paths.get(contextName);
@@ -375,13 +368,15 @@ function getMergedContextValues() {
375
368
  return out;
376
369
  }
377
370
  function getContextWritePath(contextName) {
371
+ if (isAbsolute(contextName) || contextName.includes(sep)) {
372
+ return contextName.endsWith(CONTEXT_SUFFIX) ? contextName : contextName + CONTEXT_SUFFIX;
373
+ }
378
374
  const config = loadConfig();
379
- const root = config.root ?? process.cwd();
380
- const paths = discoverContextPaths(root);
375
+ const paths = discoverContextPaths(process.cwd());
381
376
  const existing = paths.get(contextName);
382
377
  if (existing) return existing;
383
- const saveDir = config.contextDir ? isAbsolute(config.contextDir) ? config.contextDir : join2(root, config.contextDir) : root;
384
- return join2(saveDir, `${contextName}${CONTEXT_SUFFIX}`);
378
+ const projectRoot = config.root ?? process.cwd();
379
+ return join2(projectRoot, `${contextName}${CONTEXT_SUFFIX}`);
385
380
  }
386
381
  function writeToContext(contextName, key, value) {
387
382
  const path = getContextWritePath(contextName);
@@ -479,6 +474,12 @@ function scenv(name, options = {}) {
479
474
  return { raw, source: "env" };
480
475
  }
481
476
  }
477
+ const mem = getInMemoryContext();
478
+ if (mem[key] !== void 0) {
479
+ log("trace", `resolveRaw: in-memory hit key=${key}`);
480
+ const raw = resolveContextReference(mem[key], key);
481
+ return { raw, source: "context" };
482
+ }
482
483
  if (!config.ignoreContext) {
483
484
  log("trace", "resolveRaw: checking context");
484
485
  const ctx = getMergedContextValues();
@@ -562,38 +563,14 @@ function scenv(name, options = {}) {
562
563
  throw new Error(errMsg);
563
564
  }
564
565
  const final = validated.data;
565
- if (wasPrompted) {
566
- const config = loadConfig();
567
- const mode = config.shouldSavePrompt ?? (config.prompt === "never" ? "never" : "ask");
568
- if (mode === "never") return final;
569
- let doSave;
570
- if (mode === "ask") {
571
- const callbacks2 = getCallbacks();
572
- if (typeof callbacks2.onAskWhetherToSave !== "function") {
573
- throw new Error(
574
- `shouldSavePrompt is "ask" but onAskWhetherToSave callback is not set. Configure callbacks via configure({ callbacks: { onAskWhetherToSave: ... } }).`
575
- );
576
- }
577
- doSave = await callbacks2.onAskWhetherToSave(name, final);
578
- } else {
579
- doSave = true;
580
- }
581
- if (!doSave) return final;
582
- const callbacks = getCallbacks();
583
- const contextNames = config.context ?? [];
584
- let ctxToSave;
585
- if (config.saveContextTo === "ask") {
586
- if (typeof callbacks.onAskContext !== "function") {
587
- throw new Error(
588
- `saveContextTo is "ask" but onAskContext callback is not set. Configure callbacks via configure({ callbacks: { onAskContext: ... } }).`
589
- );
590
- }
591
- ctxToSave = await callbacks.onAskContext(name, contextNames);
592
- } else {
593
- ctxToSave = config.saveContextTo ?? contextNames[0] ?? "default";
566
+ const config = loadConfig();
567
+ if (wasPrompted) setInMemoryContext(key, String(final));
568
+ if (config.saveContextTo) {
569
+ const saveMode = config.saveMode ?? "all";
570
+ if (saveMode === "all" || wasPrompted) {
571
+ writeToContext(config.saveContextTo, key, String(final));
572
+ log("info", `Saved key=${key} to saveContextTo ${config.saveContextTo}`);
594
573
  }
595
- writeToContext(ctxToSave, key, String(final));
596
- log("info", `Saved key=${key} to context ${ctxToSave}`);
597
574
  }
598
575
  return final;
599
576
  }
@@ -614,33 +591,25 @@ function scenv(name, options = {}) {
614
591
  throw new Error(errMsg);
615
592
  }
616
593
  const config = loadConfig();
617
- let contextName = config.saveContextTo;
618
- if (contextName === "ask") {
619
- const callbacks = getCallbacks();
620
- if (typeof callbacks.onAskContext !== "function") {
621
- throw new Error(
622
- `saveContextTo is "ask" but onAskContext callback is not set. Configure callbacks via configure({ callbacks: { onAskContext: ... } }).`
623
- );
624
- }
625
- contextName = await callbacks.onAskContext(
626
- name,
627
- config.context ?? []
628
- );
594
+ setInMemoryContext(key, String(validated.data));
595
+ if (config.saveContextTo) {
596
+ writeToContext(config.saveContextTo, key, String(validated.data));
597
+ log("info", `Saved key=${key} to saveContextTo ${config.saveContextTo}`);
629
598
  }
630
- if (!contextName) contextName = config.context?.[0] ?? "default";
631
- writeToContext(contextName, key, String(validated.data));
632
- log("info", `Saved key=${key} to context ${contextName}`);
633
599
  }
634
600
  return { get, safeGet, save };
635
601
  }
636
602
 
637
603
  // src/cli-args.ts
604
+ import { resolve } from "path";
638
605
  function parseScenvArgs(argv) {
639
606
  const config = {};
640
607
  let i = 0;
641
608
  while (i < argv.length) {
642
609
  const arg = argv[i];
643
- if (arg === "--context" && argv[i + 1] !== void 0) {
610
+ if (arg === "--root" && argv[i + 1] !== void 0) {
611
+ config.root = resolve(argv[++i]);
612
+ } else if (arg === "--context" && argv[i + 1] !== void 0) {
644
613
  config.context = argv[++i].split(",").map((s) => s.trim()).filter(Boolean);
645
614
  } else if (arg === "--add-context" && argv[i + 1] !== void 0) {
646
615
  config.addContext = argv[++i].split(",").map((s) => s.trim()).filter(Boolean);
@@ -660,15 +629,13 @@ function parseScenvArgs(argv) {
660
629
  config.set = config.set ?? {};
661
630
  config.set[pair.slice(0, eq).trim()] = pair.slice(eq + 1).trim();
662
631
  }
663
- } else if (arg === "--save-prompt" && argv[i + 1] !== void 0) {
664
- const v = argv[++i].toLowerCase();
665
- if (["always", "never", "ask"].includes(v)) {
666
- config.shouldSavePrompt = v;
667
- }
668
632
  } else if (arg === "--save-context-to" && argv[i + 1] !== void 0) {
669
633
  config.saveContextTo = argv[++i];
670
- } else if (arg === "--context-dir" && argv[i + 1] !== void 0) {
671
- config.contextDir = argv[++i];
634
+ } else if (arg === "--save-mode" && argv[i + 1] !== void 0) {
635
+ const v = argv[++i].toLowerCase();
636
+ if (v === "all" || v === "prompts-only") {
637
+ config.saveMode = v;
638
+ }
672
639
  } else if ((arg === "--log-level" || arg === "--log") && argv[i + 1] !== void 0) {
673
640
  const v = argv[++i].toLowerCase();
674
641
  if (LOG_LEVELS.includes(v)) {
@@ -697,10 +664,14 @@ export {
697
664
  discoverContextPaths,
698
665
  getCallbacks,
699
666
  getContext,
667
+ getContextWritePath,
668
+ getInMemoryContext,
700
669
  getMergedContextValues,
701
670
  loadConfig,
702
671
  parseScenvArgs,
703
672
  resetConfig,
673
+ resetInMemoryContext,
704
674
  resetLogState,
705
- scenv
675
+ scenv,
676
+ setInMemoryContext
706
677
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scenv",
3
- "version": "0.6.1",
3
+ "version": "0.8.0",
4
4
  "description": "Environment and context variables with runtime-configurable resolution",
5
5
  "repository": {
6
6
  "type": "git",