scenv 0.3.3 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +135 -19
- package/dist/index.d.cts +9 -2
- package/dist/index.d.ts +9 -2
- package/dist/index.js +133 -19
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -20,6 +20,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/index.ts
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
|
+
LOG_LEVELS: () => LOG_LEVELS,
|
|
23
24
|
configure: () => configure,
|
|
24
25
|
discoverContextPaths: () => discoverContextPaths,
|
|
25
26
|
getCallbacks: () => getCallbacks,
|
|
@@ -27,6 +28,7 @@ __export(index_exports, {
|
|
|
27
28
|
loadConfig: () => loadConfig,
|
|
28
29
|
parseScenvArgs: () => parseScenvArgs,
|
|
29
30
|
resetConfig: () => resetConfig,
|
|
31
|
+
resetLogState: () => resetLogState,
|
|
30
32
|
scenv: () => scenv
|
|
31
33
|
});
|
|
32
34
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -34,6 +36,7 @@ module.exports = __toCommonJS(index_exports);
|
|
|
34
36
|
// src/config.ts
|
|
35
37
|
var import_node_fs = require("fs");
|
|
36
38
|
var import_node_path = require("path");
|
|
39
|
+
var LOG_LEVELS = ["none", "trace", "debug", "info", "warn", "error"];
|
|
37
40
|
var CONFIG_FILENAME = "scenv.config.json";
|
|
38
41
|
var envKeyMap = {
|
|
39
42
|
SCENV_CONTEXT: "contexts",
|
|
@@ -42,7 +45,8 @@ var envKeyMap = {
|
|
|
42
45
|
SCENV_IGNORE_ENV: "ignoreEnv",
|
|
43
46
|
SCENV_IGNORE_CONTEXT: "ignoreContext",
|
|
44
47
|
SCENV_SAVE_PROMPT: "savePrompt",
|
|
45
|
-
SCENV_SAVE_CONTEXT_TO: "saveContextTo"
|
|
48
|
+
SCENV_SAVE_CONTEXT_TO: "saveContextTo",
|
|
49
|
+
SCENV_LOG_LEVEL: "logLevel"
|
|
46
50
|
};
|
|
47
51
|
var programmaticConfig = {};
|
|
48
52
|
var programmaticCallbacks = {};
|
|
@@ -77,6 +81,9 @@ function configFromEnv() {
|
|
|
77
81
|
out[configKey] = v;
|
|
78
82
|
} else if (configKey === "saveContextTo") {
|
|
79
83
|
out.saveContextTo = val;
|
|
84
|
+
} else if (configKey === "logLevel") {
|
|
85
|
+
const v = val.toLowerCase();
|
|
86
|
+
if (LOG_LEVELS.includes(v)) out.logLevel = v;
|
|
80
87
|
}
|
|
81
88
|
}
|
|
82
89
|
return out;
|
|
@@ -108,8 +115,16 @@ function loadConfigFile(configDir) {
|
|
|
108
115
|
if (typeof parsed.saveContextTo === "string")
|
|
109
116
|
out.saveContextTo = parsed.saveContextTo;
|
|
110
117
|
if (typeof parsed.root === "string") out.root = parsed.root;
|
|
118
|
+
if (typeof parsed.logLevel === "string" && LOG_LEVELS.includes(parsed.logLevel))
|
|
119
|
+
out.logLevel = parsed.logLevel;
|
|
111
120
|
return out;
|
|
112
|
-
} catch {
|
|
121
|
+
} catch (err) {
|
|
122
|
+
const envLevel = process.env.SCENV_LOG_LEVEL?.toLowerCase();
|
|
123
|
+
if (envLevel && envLevel !== "none" && ["trace", "debug", "info", "warn", "error"].includes(envLevel)) {
|
|
124
|
+
console.error(
|
|
125
|
+
`[scenv] error: failed to parse ${path}: ${err instanceof Error ? err.message : String(err)}`
|
|
126
|
+
);
|
|
127
|
+
}
|
|
113
128
|
return {};
|
|
114
129
|
}
|
|
115
130
|
}
|
|
@@ -160,6 +175,47 @@ function resetConfig() {
|
|
|
160
175
|
programmaticCallbacks = {};
|
|
161
176
|
}
|
|
162
177
|
|
|
178
|
+
// src/log.ts
|
|
179
|
+
var LEVEL_NUM = {
|
|
180
|
+
none: -1,
|
|
181
|
+
trace: 0,
|
|
182
|
+
debug: 1,
|
|
183
|
+
info: 2,
|
|
184
|
+
warn: 3,
|
|
185
|
+
error: 4
|
|
186
|
+
};
|
|
187
|
+
function getLevelNum() {
|
|
188
|
+
const config = loadConfig();
|
|
189
|
+
const level = config.logLevel ?? "none";
|
|
190
|
+
return LEVEL_NUM[level];
|
|
191
|
+
}
|
|
192
|
+
var configLoadedLogged = false;
|
|
193
|
+
function resetLogState() {
|
|
194
|
+
configLoadedLogged = false;
|
|
195
|
+
}
|
|
196
|
+
function logConfigLoaded(config) {
|
|
197
|
+
if (configLoadedLogged) return;
|
|
198
|
+
if (getLevelNum() < LEVEL_NUM.info) return;
|
|
199
|
+
configLoadedLogged = true;
|
|
200
|
+
log(
|
|
201
|
+
"info",
|
|
202
|
+
"config loaded",
|
|
203
|
+
"root=" + (config.root ?? "(cwd)"),
|
|
204
|
+
"contexts=" + JSON.stringify(config.contexts ?? [])
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
function log(level, msg, ...args) {
|
|
208
|
+
const configured = getLevelNum();
|
|
209
|
+
if (configured < 0) return;
|
|
210
|
+
if (LEVEL_NUM[level] < configured) return;
|
|
211
|
+
const prefix = `[scenv] ${level}:`;
|
|
212
|
+
if (args.length === 0) {
|
|
213
|
+
console.error(prefix, msg);
|
|
214
|
+
} else {
|
|
215
|
+
console.error(prefix, msg, ...args);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
163
219
|
// src/context.ts
|
|
164
220
|
var import_node_fs2 = require("fs");
|
|
165
221
|
var import_node_path2 = require("path");
|
|
@@ -191,24 +247,43 @@ function discoverContextPathsInternal(dir, found) {
|
|
|
191
247
|
}
|
|
192
248
|
function discoverContextPaths(dir, found = /* @__PURE__ */ new Map()) {
|
|
193
249
|
discoverContextPathsInternal(dir, found);
|
|
250
|
+
log(
|
|
251
|
+
"debug",
|
|
252
|
+
"context discovery",
|
|
253
|
+
"dir=" + dir,
|
|
254
|
+
"found=" + JSON.stringify([...found.entries()].map(([n, p]) => ({ name: n, path: p })))
|
|
255
|
+
);
|
|
194
256
|
return found;
|
|
195
257
|
}
|
|
196
258
|
function getContextValues() {
|
|
197
259
|
const config = loadConfig();
|
|
260
|
+
logConfigLoaded(config);
|
|
198
261
|
if (config.ignoreContext) return {};
|
|
199
262
|
const root = config.root ?? process.cwd();
|
|
200
263
|
const paths = discoverContextPaths(root);
|
|
201
264
|
const out = {};
|
|
202
265
|
for (const contextName of config.contexts ?? []) {
|
|
203
266
|
const filePath = paths.get(contextName);
|
|
204
|
-
if (!filePath)
|
|
267
|
+
if (!filePath) {
|
|
268
|
+
log("warn", `context "${contextName}" not found (no *.context.json)`);
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
205
271
|
try {
|
|
206
272
|
const raw = (0, import_node_fs2.readFileSync)(filePath, "utf-8");
|
|
207
273
|
const data = JSON.parse(raw);
|
|
274
|
+
const keys = [];
|
|
208
275
|
for (const [k, v] of Object.entries(data)) {
|
|
209
|
-
if (typeof v === "string")
|
|
276
|
+
if (typeof v === "string") {
|
|
277
|
+
out[k] = v;
|
|
278
|
+
keys.push(k);
|
|
279
|
+
}
|
|
210
280
|
}
|
|
211
|
-
|
|
281
|
+
log("debug", `context "${contextName}" loaded keys=${JSON.stringify(keys)}`);
|
|
282
|
+
} catch (err) {
|
|
283
|
+
log(
|
|
284
|
+
"warn",
|
|
285
|
+
`context "${contextName}" unreadable or invalid JSON: ${err instanceof Error ? err.message : String(err)}`
|
|
286
|
+
);
|
|
212
287
|
}
|
|
213
288
|
}
|
|
214
289
|
return out;
|
|
@@ -230,6 +305,7 @@ function writeToContext(contextName, key, value) {
|
|
|
230
305
|
} catch {
|
|
231
306
|
}
|
|
232
307
|
data[key] = value;
|
|
308
|
+
log("trace", "writeToContext", "path=" + path, "key=" + key);
|
|
233
309
|
const dir = (0, import_node_path2.dirname)(path);
|
|
234
310
|
(0, import_node_fs2.mkdirSync)(dir, { recursive: true });
|
|
235
311
|
(0, import_node_fs2.writeFileSync)(path, JSON.stringify(data, null, 2) + "\n", "utf-8");
|
|
@@ -257,16 +333,29 @@ function scenv(name, options = {}) {
|
|
|
257
333
|
const defaultValue = options.default;
|
|
258
334
|
async function resolveRaw() {
|
|
259
335
|
const config = loadConfig();
|
|
260
|
-
|
|
336
|
+
log("trace", `resolveRaw: checking set for key=${key}`);
|
|
337
|
+
if (config.set?.[key] !== void 0) {
|
|
338
|
+
log("trace", `resolveRaw: set hit key=${key}`);
|
|
339
|
+
return { raw: config.set[key], source: "set" };
|
|
340
|
+
}
|
|
261
341
|
if (!config.ignoreEnv) {
|
|
342
|
+
log("trace", `resolveRaw: checking env ${envKey}`);
|
|
262
343
|
const envVal = process.env[envKey];
|
|
263
|
-
if (envVal !== void 0 && envVal !== "")
|
|
344
|
+
if (envVal !== void 0 && envVal !== "") {
|
|
345
|
+
log("trace", "resolveRaw: env hit");
|
|
346
|
+
return { raw: envVal, source: "env" };
|
|
347
|
+
}
|
|
264
348
|
}
|
|
265
349
|
if (!config.ignoreContext) {
|
|
350
|
+
log("trace", "resolveRaw: checking context");
|
|
266
351
|
const ctx = getContextValues();
|
|
267
|
-
if (ctx[key] !== void 0)
|
|
352
|
+
if (ctx[key] !== void 0) {
|
|
353
|
+
log("trace", `resolveRaw: context hit key=${key}`);
|
|
354
|
+
return { raw: ctx[key], source: "context" };
|
|
355
|
+
}
|
|
268
356
|
}
|
|
269
|
-
|
|
357
|
+
log("trace", "resolveRaw: no value");
|
|
358
|
+
return { raw: void 0, source: void 0 };
|
|
270
359
|
}
|
|
271
360
|
function shouldPrompt(config, hadValue, hadEnv) {
|
|
272
361
|
const mode = config.prompt ?? "fallback";
|
|
@@ -278,13 +367,20 @@ function scenv(name, options = {}) {
|
|
|
278
367
|
}
|
|
279
368
|
async function getResolvedValue(overrides) {
|
|
280
369
|
const config = loadConfig();
|
|
281
|
-
|
|
370
|
+
logConfigLoaded(config);
|
|
371
|
+
const { raw, source } = await resolveRaw();
|
|
282
372
|
const hadEnv = !config.ignoreEnv && process.env[envKey] !== void 0 && process.env[envKey] !== "";
|
|
283
373
|
const hadValue = raw !== void 0;
|
|
374
|
+
const doPrompt = shouldPrompt(config, hadValue, hadEnv);
|
|
375
|
+
log(
|
|
376
|
+
"debug",
|
|
377
|
+
`prompt decision key=${key} prompt=${config.prompt ?? "fallback"} hadValue=${hadValue} hadEnv=${hadEnv} -> ${doPrompt ? "prompt" : "no prompt"}`
|
|
378
|
+
);
|
|
284
379
|
const effectiveDefault = overrides?.default !== void 0 ? overrides.default : defaultValue;
|
|
285
380
|
let wasPrompted = false;
|
|
286
381
|
let value;
|
|
287
|
-
|
|
382
|
+
let resolvedFrom;
|
|
383
|
+
if (doPrompt) {
|
|
288
384
|
const callbacks = getCallbacks();
|
|
289
385
|
const fn = overrides?.prompt ?? promptFn ?? callbacks.defaultPrompt;
|
|
290
386
|
if (typeof fn !== "function") {
|
|
@@ -295,13 +391,17 @@ function scenv(name, options = {}) {
|
|
|
295
391
|
const defaultForPrompt = raw !== void 0 ? raw : effectiveDefault;
|
|
296
392
|
value = await Promise.resolve(fn(name, defaultForPrompt));
|
|
297
393
|
wasPrompted = true;
|
|
394
|
+
resolvedFrom = "prompt";
|
|
298
395
|
} else if (raw !== void 0) {
|
|
299
396
|
value = raw;
|
|
397
|
+
resolvedFrom = source;
|
|
300
398
|
} else if (effectiveDefault !== void 0) {
|
|
301
399
|
value = effectiveDefault;
|
|
400
|
+
resolvedFrom = "default";
|
|
302
401
|
} else {
|
|
303
402
|
throw new Error(`Missing value for variable "${name}" (key: ${key})`);
|
|
304
403
|
}
|
|
404
|
+
log("info", `variable "${name}" (key=${key}) resolved from ${resolvedFrom}`);
|
|
305
405
|
return { value, raw, hadEnv, wasPrompted };
|
|
306
406
|
}
|
|
307
407
|
function validate(value) {
|
|
@@ -321,14 +421,14 @@ function scenv(name, options = {}) {
|
|
|
321
421
|
const { value, wasPrompted } = await getResolvedValue(options2);
|
|
322
422
|
const validated = validate(value);
|
|
323
423
|
if (!validated.success) {
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
);
|
|
424
|
+
const errMsg = `Validation failed for "${name}": ${validated.error ?? "unknown"}`;
|
|
425
|
+
log("error", errMsg);
|
|
426
|
+
throw new Error(errMsg);
|
|
327
427
|
}
|
|
328
428
|
const final = validated.data;
|
|
329
429
|
if (wasPrompted) {
|
|
330
430
|
const config = loadConfig();
|
|
331
|
-
const savePrompt = config.savePrompt ?? "
|
|
431
|
+
const savePrompt = config.savePrompt ?? "ask";
|
|
332
432
|
const shouldAskSave = savePrompt === "always" || savePrompt === "ask" && wasPrompted;
|
|
333
433
|
if (shouldAskSave) {
|
|
334
434
|
const callbacks = getCallbacks();
|
|
@@ -342,7 +442,10 @@ function scenv(name, options = {}) {
|
|
|
342
442
|
final,
|
|
343
443
|
config.contexts ?? []
|
|
344
444
|
);
|
|
345
|
-
if (ctxToSave)
|
|
445
|
+
if (ctxToSave) {
|
|
446
|
+
writeToContext(ctxToSave, key, String(final));
|
|
447
|
+
log("info", `Saved key=${key} to context ${ctxToSave}`);
|
|
448
|
+
}
|
|
346
449
|
}
|
|
347
450
|
}
|
|
348
451
|
return final;
|
|
@@ -359,9 +462,9 @@ function scenv(name, options = {}) {
|
|
|
359
462
|
const toSave = value ?? (await getResolvedValue()).value;
|
|
360
463
|
const validated = validate(toSave);
|
|
361
464
|
if (!validated.success) {
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
);
|
|
465
|
+
const errMsg = `Validation failed for "${name}": ${validated.error ?? "unknown"}`;
|
|
466
|
+
log("error", errMsg);
|
|
467
|
+
throw new Error(errMsg);
|
|
365
468
|
}
|
|
366
469
|
const config = loadConfig();
|
|
367
470
|
let contextName = config.saveContextTo;
|
|
@@ -379,6 +482,7 @@ function scenv(name, options = {}) {
|
|
|
379
482
|
}
|
|
380
483
|
if (!contextName) contextName = config.contexts?.[0] ?? "default";
|
|
381
484
|
writeToContext(contextName, key, String(validated.data));
|
|
485
|
+
log("info", `Saved key=${key} to context ${contextName}`);
|
|
382
486
|
}
|
|
383
487
|
return { get, safeGet, save };
|
|
384
488
|
}
|
|
@@ -416,6 +520,16 @@ function parseScenvArgs(argv) {
|
|
|
416
520
|
}
|
|
417
521
|
} else if (arg === "--save-context-to" && argv[i + 1] !== void 0) {
|
|
418
522
|
config.saveContextTo = argv[++i];
|
|
523
|
+
} else if ((arg === "--log-level" || arg === "--log") && argv[i + 1] !== void 0) {
|
|
524
|
+
const v = argv[++i].toLowerCase();
|
|
525
|
+
if (LOG_LEVELS.includes(v)) {
|
|
526
|
+
config.logLevel = v;
|
|
527
|
+
}
|
|
528
|
+
} else if (arg.startsWith("--log=")) {
|
|
529
|
+
const v = arg.slice(6).toLowerCase();
|
|
530
|
+
if (LOG_LEVELS.includes(v)) {
|
|
531
|
+
config.logLevel = v;
|
|
532
|
+
}
|
|
419
533
|
} else if (arg.startsWith("--set=")) {
|
|
420
534
|
const pair = arg.slice(6);
|
|
421
535
|
const eq = pair.indexOf("=");
|
|
@@ -430,6 +544,7 @@ function parseScenvArgs(argv) {
|
|
|
430
544
|
}
|
|
431
545
|
// Annotate the CommonJS export names for ESM import in node:
|
|
432
546
|
0 && (module.exports = {
|
|
547
|
+
LOG_LEVELS,
|
|
433
548
|
configure,
|
|
434
549
|
discoverContextPaths,
|
|
435
550
|
getCallbacks,
|
|
@@ -437,5 +552,6 @@ function parseScenvArgs(argv) {
|
|
|
437
552
|
loadConfig,
|
|
438
553
|
parseScenvArgs,
|
|
439
554
|
resetConfig,
|
|
555
|
+
resetLogState,
|
|
440
556
|
scenv
|
|
441
557
|
});
|
package/dist/index.d.cts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
type PromptMode = "always" | "never" | "fallback" | "no-env";
|
|
2
2
|
type SavePromptMode = "always" | "never" | "ask";
|
|
3
|
+
declare const LOG_LEVELS: readonly ["none", "trace", "debug", "info", "warn", "error"];
|
|
4
|
+
type LogLevel = (typeof LOG_LEVELS)[number];
|
|
3
5
|
interface ScenvConfig {
|
|
4
6
|
/** Replace all contexts with this list (CLI: --context a,b,c) */
|
|
5
7
|
contexts?: string[];
|
|
@@ -19,6 +21,8 @@ interface ScenvConfig {
|
|
|
19
21
|
saveContextTo?: "ask" | string;
|
|
20
22
|
/** Root directory for config/context search (default: cwd) */
|
|
21
23
|
root?: string;
|
|
24
|
+
/** Log level: none (default), trace, debug, info, warn, error */
|
|
25
|
+
logLevel?: LogLevel;
|
|
22
26
|
}
|
|
23
27
|
/** (name, defaultValue) => value; used when a variable has no prompt option. Overridable per variable. */
|
|
24
28
|
type DefaultPromptFn = (name: string, defaultValue: unknown) => unknown | Promise<unknown>;
|
|
@@ -46,6 +50,9 @@ declare function configure(partial: Partial<ScenvConfig> & {
|
|
|
46
50
|
*/
|
|
47
51
|
declare function resetConfig(): void;
|
|
48
52
|
|
|
53
|
+
/** Reset internal log state (e.g. config-loaded guard). Useful in tests after resetConfig(). */
|
|
54
|
+
declare function resetLogState(): void;
|
|
55
|
+
|
|
49
56
|
/**
|
|
50
57
|
* Recursively find all *.context.json files under dir. Returns map: contextName -> absolute path (first found wins).
|
|
51
58
|
*/
|
|
@@ -93,8 +100,8 @@ declare function scenv<T>(name: string, options?: ScenvVariableOptions<T>): Scen
|
|
|
93
100
|
/**
|
|
94
101
|
* Parse argv (e.g. process.argv.slice(2)) into ScenvConfig for configure().
|
|
95
102
|
* Supports: --context a,b,c --add-context x,y --prompt fallback --ignore-env --ignore-context
|
|
96
|
-
* --set key=value --save-prompt ask --save-context-to prod
|
|
103
|
+
* --set key=value --save-prompt ask --save-context-to prod --log-level trace
|
|
97
104
|
*/
|
|
98
105
|
declare function parseScenvArgs(argv: string[]): Partial<ScenvConfig>;
|
|
99
106
|
|
|
100
|
-
export { type DefaultPromptFn, type GetOptions, type PromptMode, type SavePromptMode, type ScenvCallbacks, type ScenvConfig, type ScenvVariable, configure, discoverContextPaths, getCallbacks, getContextValues, loadConfig, parseScenvArgs, resetConfig, scenv };
|
|
107
|
+
export { type DefaultPromptFn, type GetOptions, LOG_LEVELS, type LogLevel, type PromptMode, type SavePromptMode, type ScenvCallbacks, type ScenvConfig, type ScenvVariable, configure, discoverContextPaths, getCallbacks, getContextValues, loadConfig, parseScenvArgs, resetConfig, resetLogState, scenv };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
type PromptMode = "always" | "never" | "fallback" | "no-env";
|
|
2
2
|
type SavePromptMode = "always" | "never" | "ask";
|
|
3
|
+
declare const LOG_LEVELS: readonly ["none", "trace", "debug", "info", "warn", "error"];
|
|
4
|
+
type LogLevel = (typeof LOG_LEVELS)[number];
|
|
3
5
|
interface ScenvConfig {
|
|
4
6
|
/** Replace all contexts with this list (CLI: --context a,b,c) */
|
|
5
7
|
contexts?: string[];
|
|
@@ -19,6 +21,8 @@ interface ScenvConfig {
|
|
|
19
21
|
saveContextTo?: "ask" | string;
|
|
20
22
|
/** Root directory for config/context search (default: cwd) */
|
|
21
23
|
root?: string;
|
|
24
|
+
/** Log level: none (default), trace, debug, info, warn, error */
|
|
25
|
+
logLevel?: LogLevel;
|
|
22
26
|
}
|
|
23
27
|
/** (name, defaultValue) => value; used when a variable has no prompt option. Overridable per variable. */
|
|
24
28
|
type DefaultPromptFn = (name: string, defaultValue: unknown) => unknown | Promise<unknown>;
|
|
@@ -46,6 +50,9 @@ declare function configure(partial: Partial<ScenvConfig> & {
|
|
|
46
50
|
*/
|
|
47
51
|
declare function resetConfig(): void;
|
|
48
52
|
|
|
53
|
+
/** Reset internal log state (e.g. config-loaded guard). Useful in tests after resetConfig(). */
|
|
54
|
+
declare function resetLogState(): void;
|
|
55
|
+
|
|
49
56
|
/**
|
|
50
57
|
* Recursively find all *.context.json files under dir. Returns map: contextName -> absolute path (first found wins).
|
|
51
58
|
*/
|
|
@@ -93,8 +100,8 @@ declare function scenv<T>(name: string, options?: ScenvVariableOptions<T>): Scen
|
|
|
93
100
|
/**
|
|
94
101
|
* Parse argv (e.g. process.argv.slice(2)) into ScenvConfig for configure().
|
|
95
102
|
* Supports: --context a,b,c --add-context x,y --prompt fallback --ignore-env --ignore-context
|
|
96
|
-
* --set key=value --save-prompt ask --save-context-to prod
|
|
103
|
+
* --set key=value --save-prompt ask --save-context-to prod --log-level trace
|
|
97
104
|
*/
|
|
98
105
|
declare function parseScenvArgs(argv: string[]): Partial<ScenvConfig>;
|
|
99
106
|
|
|
100
|
-
export { type DefaultPromptFn, type GetOptions, type PromptMode, type SavePromptMode, type ScenvCallbacks, type ScenvConfig, type ScenvVariable, configure, discoverContextPaths, getCallbacks, getContextValues, loadConfig, parseScenvArgs, resetConfig, scenv };
|
|
107
|
+
export { type DefaultPromptFn, type GetOptions, LOG_LEVELS, type LogLevel, type PromptMode, type SavePromptMode, type ScenvCallbacks, type ScenvConfig, type ScenvVariable, configure, discoverContextPaths, getCallbacks, getContextValues, loadConfig, parseScenvArgs, resetConfig, resetLogState, scenv };
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// src/config.ts
|
|
2
2
|
import { readFileSync, existsSync } from "fs";
|
|
3
3
|
import { dirname, join } from "path";
|
|
4
|
+
var LOG_LEVELS = ["none", "trace", "debug", "info", "warn", "error"];
|
|
4
5
|
var CONFIG_FILENAME = "scenv.config.json";
|
|
5
6
|
var envKeyMap = {
|
|
6
7
|
SCENV_CONTEXT: "contexts",
|
|
@@ -9,7 +10,8 @@ var envKeyMap = {
|
|
|
9
10
|
SCENV_IGNORE_ENV: "ignoreEnv",
|
|
10
11
|
SCENV_IGNORE_CONTEXT: "ignoreContext",
|
|
11
12
|
SCENV_SAVE_PROMPT: "savePrompt",
|
|
12
|
-
SCENV_SAVE_CONTEXT_TO: "saveContextTo"
|
|
13
|
+
SCENV_SAVE_CONTEXT_TO: "saveContextTo",
|
|
14
|
+
SCENV_LOG_LEVEL: "logLevel"
|
|
13
15
|
};
|
|
14
16
|
var programmaticConfig = {};
|
|
15
17
|
var programmaticCallbacks = {};
|
|
@@ -44,6 +46,9 @@ function configFromEnv() {
|
|
|
44
46
|
out[configKey] = v;
|
|
45
47
|
} else if (configKey === "saveContextTo") {
|
|
46
48
|
out.saveContextTo = val;
|
|
49
|
+
} else if (configKey === "logLevel") {
|
|
50
|
+
const v = val.toLowerCase();
|
|
51
|
+
if (LOG_LEVELS.includes(v)) out.logLevel = v;
|
|
47
52
|
}
|
|
48
53
|
}
|
|
49
54
|
return out;
|
|
@@ -75,8 +80,16 @@ function loadConfigFile(configDir) {
|
|
|
75
80
|
if (typeof parsed.saveContextTo === "string")
|
|
76
81
|
out.saveContextTo = parsed.saveContextTo;
|
|
77
82
|
if (typeof parsed.root === "string") out.root = parsed.root;
|
|
83
|
+
if (typeof parsed.logLevel === "string" && LOG_LEVELS.includes(parsed.logLevel))
|
|
84
|
+
out.logLevel = parsed.logLevel;
|
|
78
85
|
return out;
|
|
79
|
-
} catch {
|
|
86
|
+
} catch (err) {
|
|
87
|
+
const envLevel = process.env.SCENV_LOG_LEVEL?.toLowerCase();
|
|
88
|
+
if (envLevel && envLevel !== "none" && ["trace", "debug", "info", "warn", "error"].includes(envLevel)) {
|
|
89
|
+
console.error(
|
|
90
|
+
`[scenv] error: failed to parse ${path}: ${err instanceof Error ? err.message : String(err)}`
|
|
91
|
+
);
|
|
92
|
+
}
|
|
80
93
|
return {};
|
|
81
94
|
}
|
|
82
95
|
}
|
|
@@ -127,6 +140,47 @@ function resetConfig() {
|
|
|
127
140
|
programmaticCallbacks = {};
|
|
128
141
|
}
|
|
129
142
|
|
|
143
|
+
// src/log.ts
|
|
144
|
+
var LEVEL_NUM = {
|
|
145
|
+
none: -1,
|
|
146
|
+
trace: 0,
|
|
147
|
+
debug: 1,
|
|
148
|
+
info: 2,
|
|
149
|
+
warn: 3,
|
|
150
|
+
error: 4
|
|
151
|
+
};
|
|
152
|
+
function getLevelNum() {
|
|
153
|
+
const config = loadConfig();
|
|
154
|
+
const level = config.logLevel ?? "none";
|
|
155
|
+
return LEVEL_NUM[level];
|
|
156
|
+
}
|
|
157
|
+
var configLoadedLogged = false;
|
|
158
|
+
function resetLogState() {
|
|
159
|
+
configLoadedLogged = false;
|
|
160
|
+
}
|
|
161
|
+
function logConfigLoaded(config) {
|
|
162
|
+
if (configLoadedLogged) return;
|
|
163
|
+
if (getLevelNum() < LEVEL_NUM.info) return;
|
|
164
|
+
configLoadedLogged = true;
|
|
165
|
+
log(
|
|
166
|
+
"info",
|
|
167
|
+
"config loaded",
|
|
168
|
+
"root=" + (config.root ?? "(cwd)"),
|
|
169
|
+
"contexts=" + JSON.stringify(config.contexts ?? [])
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
function log(level, msg, ...args) {
|
|
173
|
+
const configured = getLevelNum();
|
|
174
|
+
if (configured < 0) return;
|
|
175
|
+
if (LEVEL_NUM[level] < configured) return;
|
|
176
|
+
const prefix = `[scenv] ${level}:`;
|
|
177
|
+
if (args.length === 0) {
|
|
178
|
+
console.error(prefix, msg);
|
|
179
|
+
} else {
|
|
180
|
+
console.error(prefix, msg, ...args);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
130
184
|
// src/context.ts
|
|
131
185
|
import {
|
|
132
186
|
readFileSync as readFileSync2,
|
|
@@ -164,24 +218,43 @@ function discoverContextPathsInternal(dir, found) {
|
|
|
164
218
|
}
|
|
165
219
|
function discoverContextPaths(dir, found = /* @__PURE__ */ new Map()) {
|
|
166
220
|
discoverContextPathsInternal(dir, found);
|
|
221
|
+
log(
|
|
222
|
+
"debug",
|
|
223
|
+
"context discovery",
|
|
224
|
+
"dir=" + dir,
|
|
225
|
+
"found=" + JSON.stringify([...found.entries()].map(([n, p]) => ({ name: n, path: p })))
|
|
226
|
+
);
|
|
167
227
|
return found;
|
|
168
228
|
}
|
|
169
229
|
function getContextValues() {
|
|
170
230
|
const config = loadConfig();
|
|
231
|
+
logConfigLoaded(config);
|
|
171
232
|
if (config.ignoreContext) return {};
|
|
172
233
|
const root = config.root ?? process.cwd();
|
|
173
234
|
const paths = discoverContextPaths(root);
|
|
174
235
|
const out = {};
|
|
175
236
|
for (const contextName of config.contexts ?? []) {
|
|
176
237
|
const filePath = paths.get(contextName);
|
|
177
|
-
if (!filePath)
|
|
238
|
+
if (!filePath) {
|
|
239
|
+
log("warn", `context "${contextName}" not found (no *.context.json)`);
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
178
242
|
try {
|
|
179
243
|
const raw = readFileSync2(filePath, "utf-8");
|
|
180
244
|
const data = JSON.parse(raw);
|
|
245
|
+
const keys = [];
|
|
181
246
|
for (const [k, v] of Object.entries(data)) {
|
|
182
|
-
if (typeof v === "string")
|
|
247
|
+
if (typeof v === "string") {
|
|
248
|
+
out[k] = v;
|
|
249
|
+
keys.push(k);
|
|
250
|
+
}
|
|
183
251
|
}
|
|
184
|
-
|
|
252
|
+
log("debug", `context "${contextName}" loaded keys=${JSON.stringify(keys)}`);
|
|
253
|
+
} catch (err) {
|
|
254
|
+
log(
|
|
255
|
+
"warn",
|
|
256
|
+
`context "${contextName}" unreadable or invalid JSON: ${err instanceof Error ? err.message : String(err)}`
|
|
257
|
+
);
|
|
185
258
|
}
|
|
186
259
|
}
|
|
187
260
|
return out;
|
|
@@ -203,6 +276,7 @@ function writeToContext(contextName, key, value) {
|
|
|
203
276
|
} catch {
|
|
204
277
|
}
|
|
205
278
|
data[key] = value;
|
|
279
|
+
log("trace", "writeToContext", "path=" + path, "key=" + key);
|
|
206
280
|
const dir = dirname2(path);
|
|
207
281
|
mkdirSync(dir, { recursive: true });
|
|
208
282
|
writeFileSync(path, JSON.stringify(data, null, 2) + "\n", "utf-8");
|
|
@@ -230,16 +304,29 @@ function scenv(name, options = {}) {
|
|
|
230
304
|
const defaultValue = options.default;
|
|
231
305
|
async function resolveRaw() {
|
|
232
306
|
const config = loadConfig();
|
|
233
|
-
|
|
307
|
+
log("trace", `resolveRaw: checking set for key=${key}`);
|
|
308
|
+
if (config.set?.[key] !== void 0) {
|
|
309
|
+
log("trace", `resolveRaw: set hit key=${key}`);
|
|
310
|
+
return { raw: config.set[key], source: "set" };
|
|
311
|
+
}
|
|
234
312
|
if (!config.ignoreEnv) {
|
|
313
|
+
log("trace", `resolveRaw: checking env ${envKey}`);
|
|
235
314
|
const envVal = process.env[envKey];
|
|
236
|
-
if (envVal !== void 0 && envVal !== "")
|
|
315
|
+
if (envVal !== void 0 && envVal !== "") {
|
|
316
|
+
log("trace", "resolveRaw: env hit");
|
|
317
|
+
return { raw: envVal, source: "env" };
|
|
318
|
+
}
|
|
237
319
|
}
|
|
238
320
|
if (!config.ignoreContext) {
|
|
321
|
+
log("trace", "resolveRaw: checking context");
|
|
239
322
|
const ctx = getContextValues();
|
|
240
|
-
if (ctx[key] !== void 0)
|
|
323
|
+
if (ctx[key] !== void 0) {
|
|
324
|
+
log("trace", `resolveRaw: context hit key=${key}`);
|
|
325
|
+
return { raw: ctx[key], source: "context" };
|
|
326
|
+
}
|
|
241
327
|
}
|
|
242
|
-
|
|
328
|
+
log("trace", "resolveRaw: no value");
|
|
329
|
+
return { raw: void 0, source: void 0 };
|
|
243
330
|
}
|
|
244
331
|
function shouldPrompt(config, hadValue, hadEnv) {
|
|
245
332
|
const mode = config.prompt ?? "fallback";
|
|
@@ -251,13 +338,20 @@ function scenv(name, options = {}) {
|
|
|
251
338
|
}
|
|
252
339
|
async function getResolvedValue(overrides) {
|
|
253
340
|
const config = loadConfig();
|
|
254
|
-
|
|
341
|
+
logConfigLoaded(config);
|
|
342
|
+
const { raw, source } = await resolveRaw();
|
|
255
343
|
const hadEnv = !config.ignoreEnv && process.env[envKey] !== void 0 && process.env[envKey] !== "";
|
|
256
344
|
const hadValue = raw !== void 0;
|
|
345
|
+
const doPrompt = shouldPrompt(config, hadValue, hadEnv);
|
|
346
|
+
log(
|
|
347
|
+
"debug",
|
|
348
|
+
`prompt decision key=${key} prompt=${config.prompt ?? "fallback"} hadValue=${hadValue} hadEnv=${hadEnv} -> ${doPrompt ? "prompt" : "no prompt"}`
|
|
349
|
+
);
|
|
257
350
|
const effectiveDefault = overrides?.default !== void 0 ? overrides.default : defaultValue;
|
|
258
351
|
let wasPrompted = false;
|
|
259
352
|
let value;
|
|
260
|
-
|
|
353
|
+
let resolvedFrom;
|
|
354
|
+
if (doPrompt) {
|
|
261
355
|
const callbacks = getCallbacks();
|
|
262
356
|
const fn = overrides?.prompt ?? promptFn ?? callbacks.defaultPrompt;
|
|
263
357
|
if (typeof fn !== "function") {
|
|
@@ -268,13 +362,17 @@ function scenv(name, options = {}) {
|
|
|
268
362
|
const defaultForPrompt = raw !== void 0 ? raw : effectiveDefault;
|
|
269
363
|
value = await Promise.resolve(fn(name, defaultForPrompt));
|
|
270
364
|
wasPrompted = true;
|
|
365
|
+
resolvedFrom = "prompt";
|
|
271
366
|
} else if (raw !== void 0) {
|
|
272
367
|
value = raw;
|
|
368
|
+
resolvedFrom = source;
|
|
273
369
|
} else if (effectiveDefault !== void 0) {
|
|
274
370
|
value = effectiveDefault;
|
|
371
|
+
resolvedFrom = "default";
|
|
275
372
|
} else {
|
|
276
373
|
throw new Error(`Missing value for variable "${name}" (key: ${key})`);
|
|
277
374
|
}
|
|
375
|
+
log("info", `variable "${name}" (key=${key}) resolved from ${resolvedFrom}`);
|
|
278
376
|
return { value, raw, hadEnv, wasPrompted };
|
|
279
377
|
}
|
|
280
378
|
function validate(value) {
|
|
@@ -294,14 +392,14 @@ function scenv(name, options = {}) {
|
|
|
294
392
|
const { value, wasPrompted } = await getResolvedValue(options2);
|
|
295
393
|
const validated = validate(value);
|
|
296
394
|
if (!validated.success) {
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
);
|
|
395
|
+
const errMsg = `Validation failed for "${name}": ${validated.error ?? "unknown"}`;
|
|
396
|
+
log("error", errMsg);
|
|
397
|
+
throw new Error(errMsg);
|
|
300
398
|
}
|
|
301
399
|
const final = validated.data;
|
|
302
400
|
if (wasPrompted) {
|
|
303
401
|
const config = loadConfig();
|
|
304
|
-
const savePrompt = config.savePrompt ?? "
|
|
402
|
+
const savePrompt = config.savePrompt ?? "ask";
|
|
305
403
|
const shouldAskSave = savePrompt === "always" || savePrompt === "ask" && wasPrompted;
|
|
306
404
|
if (shouldAskSave) {
|
|
307
405
|
const callbacks = getCallbacks();
|
|
@@ -315,7 +413,10 @@ function scenv(name, options = {}) {
|
|
|
315
413
|
final,
|
|
316
414
|
config.contexts ?? []
|
|
317
415
|
);
|
|
318
|
-
if (ctxToSave)
|
|
416
|
+
if (ctxToSave) {
|
|
417
|
+
writeToContext(ctxToSave, key, String(final));
|
|
418
|
+
log("info", `Saved key=${key} to context ${ctxToSave}`);
|
|
419
|
+
}
|
|
319
420
|
}
|
|
320
421
|
}
|
|
321
422
|
return final;
|
|
@@ -332,9 +433,9 @@ function scenv(name, options = {}) {
|
|
|
332
433
|
const toSave = value ?? (await getResolvedValue()).value;
|
|
333
434
|
const validated = validate(toSave);
|
|
334
435
|
if (!validated.success) {
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
);
|
|
436
|
+
const errMsg = `Validation failed for "${name}": ${validated.error ?? "unknown"}`;
|
|
437
|
+
log("error", errMsg);
|
|
438
|
+
throw new Error(errMsg);
|
|
338
439
|
}
|
|
339
440
|
const config = loadConfig();
|
|
340
441
|
let contextName = config.saveContextTo;
|
|
@@ -352,6 +453,7 @@ function scenv(name, options = {}) {
|
|
|
352
453
|
}
|
|
353
454
|
if (!contextName) contextName = config.contexts?.[0] ?? "default";
|
|
354
455
|
writeToContext(contextName, key, String(validated.data));
|
|
456
|
+
log("info", `Saved key=${key} to context ${contextName}`);
|
|
355
457
|
}
|
|
356
458
|
return { get, safeGet, save };
|
|
357
459
|
}
|
|
@@ -389,6 +491,16 @@ function parseScenvArgs(argv) {
|
|
|
389
491
|
}
|
|
390
492
|
} else if (arg === "--save-context-to" && argv[i + 1] !== void 0) {
|
|
391
493
|
config.saveContextTo = argv[++i];
|
|
494
|
+
} else if ((arg === "--log-level" || arg === "--log") && argv[i + 1] !== void 0) {
|
|
495
|
+
const v = argv[++i].toLowerCase();
|
|
496
|
+
if (LOG_LEVELS.includes(v)) {
|
|
497
|
+
config.logLevel = v;
|
|
498
|
+
}
|
|
499
|
+
} else if (arg.startsWith("--log=")) {
|
|
500
|
+
const v = arg.slice(6).toLowerCase();
|
|
501
|
+
if (LOG_LEVELS.includes(v)) {
|
|
502
|
+
config.logLevel = v;
|
|
503
|
+
}
|
|
392
504
|
} else if (arg.startsWith("--set=")) {
|
|
393
505
|
const pair = arg.slice(6);
|
|
394
506
|
const eq = pair.indexOf("=");
|
|
@@ -402,6 +514,7 @@ function parseScenvArgs(argv) {
|
|
|
402
514
|
return config;
|
|
403
515
|
}
|
|
404
516
|
export {
|
|
517
|
+
LOG_LEVELS,
|
|
405
518
|
configure,
|
|
406
519
|
discoverContextPaths,
|
|
407
520
|
getCallbacks,
|
|
@@ -409,5 +522,6 @@ export {
|
|
|
409
522
|
loadConfig,
|
|
410
523
|
parseScenvArgs,
|
|
411
524
|
resetConfig,
|
|
525
|
+
resetLogState,
|
|
412
526
|
scenv
|
|
413
527
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "scenv",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "Environment and context variables with runtime-configurable resolution",
|
|
5
5
|
"repository": { "type": "git", "url": "https://github.com/PKWadsy/scenv" },
|
|
6
6
|
"publishConfig": { "access": "public", "provenance": true },
|