scenv 0.7.0 → 0.9.0

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