scenv 0.1.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 +451 -0
- package/dist/index.d.cts +89 -0
- package/dist/index.d.ts +89 -0
- package/dist/index.js +423 -0
- package/package.json +44 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
configure: () => configure,
|
|
24
|
+
discoverContextPaths: () => discoverContextPaths,
|
|
25
|
+
getCallbacks: () => getCallbacks,
|
|
26
|
+
getContextValues: () => getContextValues,
|
|
27
|
+
loadConfig: () => loadConfig,
|
|
28
|
+
parseSenvArgs: () => parseSenvArgs,
|
|
29
|
+
resetConfig: () => resetConfig,
|
|
30
|
+
senv: () => senv
|
|
31
|
+
});
|
|
32
|
+
module.exports = __toCommonJS(index_exports);
|
|
33
|
+
|
|
34
|
+
// src/config.ts
|
|
35
|
+
var import_node_fs = require("fs");
|
|
36
|
+
var import_node_path = require("path");
|
|
37
|
+
var CONFIG_FILENAME = "senv.config.json";
|
|
38
|
+
var envKeyMap = {
|
|
39
|
+
SENV_CONTEXT: "contexts",
|
|
40
|
+
SENV_ADD_CONTEXTS: "addContexts",
|
|
41
|
+
SENV_PROMPT: "prompt",
|
|
42
|
+
SENV_IGNORE_ENV: "ignoreEnv",
|
|
43
|
+
SENV_IGNORE_CONTEXT: "ignoreContext",
|
|
44
|
+
SENV_SAVE_PROMPT: "savePrompt",
|
|
45
|
+
SENV_SAVE_CONTEXT_TO: "saveContextTo"
|
|
46
|
+
};
|
|
47
|
+
var programmaticConfig = {};
|
|
48
|
+
var programmaticCallbacks = {};
|
|
49
|
+
function getCallbacks() {
|
|
50
|
+
return { ...programmaticCallbacks };
|
|
51
|
+
}
|
|
52
|
+
function findConfigDir(startDir) {
|
|
53
|
+
let dir = startDir;
|
|
54
|
+
const root = (0, import_node_path.dirname)(dir);
|
|
55
|
+
for (; ; ) {
|
|
56
|
+
const candidate = (0, import_node_path.join)(dir, CONFIG_FILENAME);
|
|
57
|
+
if ((0, import_node_fs.existsSync)(candidate)) return dir;
|
|
58
|
+
if (dir === root) break;
|
|
59
|
+
dir = (0, import_node_path.dirname)(dir);
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
function configFromEnv() {
|
|
64
|
+
const out = {};
|
|
65
|
+
for (const [envKey, configKey] of Object.entries(envKeyMap)) {
|
|
66
|
+
const val = process.env[envKey];
|
|
67
|
+
if (val === void 0 || val === "") continue;
|
|
68
|
+
if (configKey === "contexts" || configKey === "addContexts") {
|
|
69
|
+
out[configKey] = val.split(",").map((s) => s.trim()).filter(Boolean);
|
|
70
|
+
} else if (configKey === "ignoreEnv" || configKey === "ignoreContext") {
|
|
71
|
+
out[configKey] = val === "1" || val === "true" || val.toLowerCase() === "yes";
|
|
72
|
+
} else if (configKey === "prompt" || configKey === "savePrompt") {
|
|
73
|
+
const v = val.toLowerCase();
|
|
74
|
+
if (configKey === "prompt" && (v === "always" || v === "never" || v === "fallback" || v === "no-env"))
|
|
75
|
+
out[configKey] = v;
|
|
76
|
+
if (configKey === "savePrompt" && (v === "always" || v === "never" || v === "ask"))
|
|
77
|
+
out[configKey] = v;
|
|
78
|
+
} else if (configKey === "saveContextTo") {
|
|
79
|
+
out.saveContextTo = val;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return out;
|
|
83
|
+
}
|
|
84
|
+
function loadConfigFile(configDir) {
|
|
85
|
+
const path = (0, import_node_path.join)(configDir, CONFIG_FILENAME);
|
|
86
|
+
if (!(0, import_node_fs.existsSync)(path)) return {};
|
|
87
|
+
try {
|
|
88
|
+
const raw = (0, import_node_fs.readFileSync)(path, "utf-8");
|
|
89
|
+
const parsed = JSON.parse(raw);
|
|
90
|
+
const out = {};
|
|
91
|
+
if (Array.isArray(parsed.contexts))
|
|
92
|
+
out.contexts = parsed.contexts.filter(
|
|
93
|
+
(x) => typeof x === "string"
|
|
94
|
+
);
|
|
95
|
+
if (Array.isArray(parsed.addContexts))
|
|
96
|
+
out.addContexts = parsed.addContexts.filter(
|
|
97
|
+
(x) => typeof x === "string"
|
|
98
|
+
);
|
|
99
|
+
if (typeof parsed.prompt === "string" && ["always", "never", "fallback", "no-env"].includes(parsed.prompt))
|
|
100
|
+
out.prompt = parsed.prompt;
|
|
101
|
+
if (typeof parsed.ignoreEnv === "boolean") out.ignoreEnv = parsed.ignoreEnv;
|
|
102
|
+
if (typeof parsed.ignoreContext === "boolean")
|
|
103
|
+
out.ignoreContext = parsed.ignoreContext;
|
|
104
|
+
if (parsed.set && typeof parsed.set === "object" && !Array.isArray(parsed.set))
|
|
105
|
+
out.set = parsed.set;
|
|
106
|
+
if (typeof parsed.savePrompt === "string" && ["always", "never", "ask"].includes(parsed.savePrompt))
|
|
107
|
+
out.savePrompt = parsed.savePrompt;
|
|
108
|
+
if (typeof parsed.saveContextTo === "string")
|
|
109
|
+
out.saveContextTo = parsed.saveContextTo;
|
|
110
|
+
if (typeof parsed.root === "string") out.root = parsed.root;
|
|
111
|
+
return out;
|
|
112
|
+
} catch {
|
|
113
|
+
return {};
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
function mergeContexts(fileConfig, envConfig, progConfig) {
|
|
117
|
+
const fromFile = fileConfig.contexts ?? fileConfig.addContexts ?? [];
|
|
118
|
+
const fromEnvAdd = envConfig.addContexts ?? [];
|
|
119
|
+
const fromEnvReplace = envConfig.contexts;
|
|
120
|
+
const fromProgAdd = progConfig.addContexts ?? [];
|
|
121
|
+
const fromProgReplace = progConfig.contexts;
|
|
122
|
+
const replace = fromProgReplace ?? fromEnvReplace;
|
|
123
|
+
if (replace !== void 0) return replace;
|
|
124
|
+
const base = [...fromFile];
|
|
125
|
+
const add = [...fromEnvAdd, ...fromProgAdd];
|
|
126
|
+
const seen = new Set(base);
|
|
127
|
+
for (const c of add) {
|
|
128
|
+
if (!seen.has(c)) {
|
|
129
|
+
seen.add(c);
|
|
130
|
+
base.push(c);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return base;
|
|
134
|
+
}
|
|
135
|
+
function loadConfig(root) {
|
|
136
|
+
const startDir = root ?? programmaticConfig.root ?? process.cwd();
|
|
137
|
+
const configDir = findConfigDir(startDir);
|
|
138
|
+
const fileConfig = configDir ? loadConfigFile(configDir) : {};
|
|
139
|
+
const envConfig = configFromEnv();
|
|
140
|
+
const merged = {
|
|
141
|
+
...fileConfig,
|
|
142
|
+
...envConfig,
|
|
143
|
+
...programmaticConfig
|
|
144
|
+
};
|
|
145
|
+
merged.contexts = mergeContexts(fileConfig, envConfig, programmaticConfig);
|
|
146
|
+
delete merged.addContexts;
|
|
147
|
+
if (configDir && !merged.root) merged.root = configDir;
|
|
148
|
+
else if (!merged.root) merged.root = startDir;
|
|
149
|
+
return merged;
|
|
150
|
+
}
|
|
151
|
+
function configure(partial) {
|
|
152
|
+
const { callbacks, ...configPartial } = partial;
|
|
153
|
+
programmaticConfig = { ...programmaticConfig, ...configPartial };
|
|
154
|
+
if (callbacks) {
|
|
155
|
+
programmaticCallbacks = { ...programmaticCallbacks, ...callbacks };
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
function resetConfig() {
|
|
159
|
+
programmaticConfig = {};
|
|
160
|
+
programmaticCallbacks = {};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// src/context.ts
|
|
164
|
+
var import_node_fs2 = require("fs");
|
|
165
|
+
var import_node_path2 = require("path");
|
|
166
|
+
var CONTEXT_SUFFIX = ".context.json";
|
|
167
|
+
function discoverContextPathsInternal(dir, found) {
|
|
168
|
+
let entries;
|
|
169
|
+
try {
|
|
170
|
+
entries = (0, import_node_fs2.readdirSync)(dir, { withFileTypes: true }).map((d) => ({
|
|
171
|
+
name: d.name,
|
|
172
|
+
path: (0, import_node_path2.join)(dir, d.name)
|
|
173
|
+
}));
|
|
174
|
+
} catch {
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
for (const { name, path } of entries) {
|
|
178
|
+
if (name.endsWith(CONTEXT_SUFFIX) && !name.startsWith(".")) {
|
|
179
|
+
const contextName = name.slice(0, -CONTEXT_SUFFIX.length);
|
|
180
|
+
if (!found.has(contextName)) found.set(contextName, path);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
for (const { name, path } of entries) {
|
|
184
|
+
if (name === "." || name === "..") continue;
|
|
185
|
+
try {
|
|
186
|
+
if ((0, import_node_fs2.statSync)(path).isDirectory())
|
|
187
|
+
discoverContextPathsInternal(path, found);
|
|
188
|
+
} catch {
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
function discoverContextPaths(dir, found = /* @__PURE__ */ new Map()) {
|
|
193
|
+
discoverContextPathsInternal(dir, found);
|
|
194
|
+
return found;
|
|
195
|
+
}
|
|
196
|
+
function getContextValues() {
|
|
197
|
+
const config = loadConfig();
|
|
198
|
+
if (config.ignoreContext) return {};
|
|
199
|
+
const root = config.root ?? process.cwd();
|
|
200
|
+
const paths = discoverContextPaths(root);
|
|
201
|
+
const out = {};
|
|
202
|
+
for (const contextName of config.contexts ?? []) {
|
|
203
|
+
const filePath = paths.get(contextName);
|
|
204
|
+
if (!filePath) continue;
|
|
205
|
+
try {
|
|
206
|
+
const raw = (0, import_node_fs2.readFileSync)(filePath, "utf-8");
|
|
207
|
+
const data = JSON.parse(raw);
|
|
208
|
+
for (const [k, v] of Object.entries(data)) {
|
|
209
|
+
if (typeof v === "string") out[k] = v;
|
|
210
|
+
}
|
|
211
|
+
} catch {
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return out;
|
|
215
|
+
}
|
|
216
|
+
function getContextWritePath(contextName) {
|
|
217
|
+
const config = loadConfig();
|
|
218
|
+
const root = config.root ?? process.cwd();
|
|
219
|
+
const paths = discoverContextPaths(root);
|
|
220
|
+
const existing = paths.get(contextName);
|
|
221
|
+
if (existing) return existing;
|
|
222
|
+
return (0, import_node_path2.join)(root, `${contextName}${CONTEXT_SUFFIX}`);
|
|
223
|
+
}
|
|
224
|
+
function writeToContext(contextName, key, value) {
|
|
225
|
+
const path = getContextWritePath(contextName);
|
|
226
|
+
let data = {};
|
|
227
|
+
try {
|
|
228
|
+
const raw = (0, import_node_fs2.readFileSync)(path, "utf-8");
|
|
229
|
+
data = JSON.parse(raw);
|
|
230
|
+
} catch {
|
|
231
|
+
}
|
|
232
|
+
data[key] = value;
|
|
233
|
+
const dir = (0, import_node_path2.dirname)(path);
|
|
234
|
+
(0, import_node_fs2.mkdirSync)(dir, { recursive: true });
|
|
235
|
+
(0, import_node_fs2.writeFileSync)(path, JSON.stringify(data, null, 2) + "\n", "utf-8");
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// src/prompt-default.ts
|
|
239
|
+
var import_node_readline = require("readline");
|
|
240
|
+
function defaultPrompt(name, defaultValue) {
|
|
241
|
+
const defaultStr = defaultValue !== void 0 && defaultValue !== null ? String(defaultValue) : "";
|
|
242
|
+
const message = defaultStr ? `Enter ${name} [${defaultStr}]: ` : `Enter ${name}: `;
|
|
243
|
+
const rl = (0, import_node_readline.createInterface)({ input: process.stdin, output: process.stdout });
|
|
244
|
+
return new Promise((resolve, reject) => {
|
|
245
|
+
rl.question(message, (answer) => {
|
|
246
|
+
rl.close();
|
|
247
|
+
const trimmed = answer.trim();
|
|
248
|
+
const value = trimmed !== "" ? trimmed : defaultStr;
|
|
249
|
+
resolve(value);
|
|
250
|
+
});
|
|
251
|
+
rl.on("error", (err) => {
|
|
252
|
+
rl.close();
|
|
253
|
+
reject(err);
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// src/variable.ts
|
|
259
|
+
function defaultKeyFromName(name) {
|
|
260
|
+
return name.toLowerCase().replace(/\s+/g, "_").replace(/[^a-z0-9_]/gi, "");
|
|
261
|
+
}
|
|
262
|
+
function defaultEnvFromKey(key) {
|
|
263
|
+
return key.toUpperCase().replace(/-/g, "_");
|
|
264
|
+
}
|
|
265
|
+
function normalizeValidatorResult(result) {
|
|
266
|
+
if (typeof result === "boolean") {
|
|
267
|
+
return result ? { success: true } : { success: false };
|
|
268
|
+
}
|
|
269
|
+
if (result.success === true) return result;
|
|
270
|
+
return result;
|
|
271
|
+
}
|
|
272
|
+
function senv(name, options = {}) {
|
|
273
|
+
const key = options.key ?? defaultKeyFromName(name);
|
|
274
|
+
const envKey = options.env ?? defaultEnvFromKey(key);
|
|
275
|
+
const validator = options.validator;
|
|
276
|
+
const promptFn = options.prompt;
|
|
277
|
+
const defaultValue = options.default;
|
|
278
|
+
async function resolveRaw() {
|
|
279
|
+
const config = loadConfig();
|
|
280
|
+
if (config.set?.[key] !== void 0) return config.set[key];
|
|
281
|
+
if (!config.ignoreEnv) {
|
|
282
|
+
const envVal = process.env[envKey];
|
|
283
|
+
if (envVal !== void 0 && envVal !== "") return envVal;
|
|
284
|
+
}
|
|
285
|
+
if (!config.ignoreContext) {
|
|
286
|
+
const ctx = getContextValues();
|
|
287
|
+
if (ctx[key] !== void 0) return ctx[key];
|
|
288
|
+
}
|
|
289
|
+
if (defaultValue !== void 0) {
|
|
290
|
+
return String(defaultValue);
|
|
291
|
+
}
|
|
292
|
+
return void 0;
|
|
293
|
+
}
|
|
294
|
+
function shouldPrompt(config, hadValue, hadEnv) {
|
|
295
|
+
const mode = config.prompt ?? "fallback";
|
|
296
|
+
if (mode === "never") return false;
|
|
297
|
+
if (mode === "always") return true;
|
|
298
|
+
if (mode === "fallback") return !hadValue;
|
|
299
|
+
if (mode === "no-env") return !hadEnv;
|
|
300
|
+
return false;
|
|
301
|
+
}
|
|
302
|
+
async function getResolvedValue() {
|
|
303
|
+
const config = loadConfig();
|
|
304
|
+
const raw = await resolveRaw();
|
|
305
|
+
const hadEnv = !config.ignoreEnv && process.env[envKey] !== void 0 && process.env[envKey] !== "";
|
|
306
|
+
const hadValue = raw !== void 0;
|
|
307
|
+
let wasPrompted = false;
|
|
308
|
+
let value;
|
|
309
|
+
if (shouldPrompt(config, hadValue, hadEnv)) {
|
|
310
|
+
const defaultForPrompt = raw !== void 0 ? raw : defaultValue;
|
|
311
|
+
const fn = promptFn ?? defaultPrompt;
|
|
312
|
+
value = await Promise.resolve(fn(name, defaultForPrompt));
|
|
313
|
+
wasPrompted = true;
|
|
314
|
+
} else if (raw !== void 0) {
|
|
315
|
+
value = raw;
|
|
316
|
+
} else if (defaultValue !== void 0) {
|
|
317
|
+
value = defaultValue;
|
|
318
|
+
} else {
|
|
319
|
+
throw new Error(`Missing value for variable "${name}" (key: ${key})`);
|
|
320
|
+
}
|
|
321
|
+
return { value, raw, hadEnv, wasPrompted };
|
|
322
|
+
}
|
|
323
|
+
function validate(value) {
|
|
324
|
+
if (!validator) return { success: true, data: value };
|
|
325
|
+
const result = validator(value);
|
|
326
|
+
const normalized = normalizeValidatorResult(result);
|
|
327
|
+
if (normalized.success) {
|
|
328
|
+
const data = "data" in normalized && normalized.data !== void 0 ? normalized.data : value;
|
|
329
|
+
return { success: true, data };
|
|
330
|
+
}
|
|
331
|
+
return {
|
|
332
|
+
success: false,
|
|
333
|
+
error: "error" in normalized ? normalized.error : void 0
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
async function get() {
|
|
337
|
+
const { value, wasPrompted } = await getResolvedValue();
|
|
338
|
+
const validated = validate(value);
|
|
339
|
+
if (!validated.success) {
|
|
340
|
+
throw new Error(
|
|
341
|
+
`Validation failed for "${name}": ${validated.error ?? "unknown"}`
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
const final = validated.data;
|
|
345
|
+
if (wasPrompted) {
|
|
346
|
+
const config = loadConfig();
|
|
347
|
+
const savePrompt = config.savePrompt ?? "never";
|
|
348
|
+
const shouldAskSave = savePrompt === "always" || savePrompt === "ask" && wasPrompted;
|
|
349
|
+
if (shouldAskSave) {
|
|
350
|
+
const callbacks = getCallbacks();
|
|
351
|
+
const ctxToSave = callbacks.onAskSaveAfterPrompt && await callbacks.onAskSaveAfterPrompt(
|
|
352
|
+
name,
|
|
353
|
+
final,
|
|
354
|
+
config.contexts ?? []
|
|
355
|
+
);
|
|
356
|
+
if (ctxToSave) writeToContext(ctxToSave, key, String(final));
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
return final;
|
|
360
|
+
}
|
|
361
|
+
async function safeGet() {
|
|
362
|
+
try {
|
|
363
|
+
const v = await get();
|
|
364
|
+
return { success: true, value: v };
|
|
365
|
+
} catch (err) {
|
|
366
|
+
return { success: false, error: err };
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
async function save(value) {
|
|
370
|
+
const toSave = value ?? (await getResolvedValue()).value;
|
|
371
|
+
const validated = validate(toSave);
|
|
372
|
+
if (!validated.success) {
|
|
373
|
+
throw new Error(
|
|
374
|
+
`Validation failed for "${name}": ${validated.error ?? "unknown"}`
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
const config = loadConfig();
|
|
378
|
+
let contextName = config.saveContextTo;
|
|
379
|
+
if (contextName === "ask") {
|
|
380
|
+
const callbacks = getCallbacks();
|
|
381
|
+
if (typeof callbacks.onAskContext === "function") {
|
|
382
|
+
contextName = await callbacks.onAskContext(
|
|
383
|
+
name,
|
|
384
|
+
config.contexts ?? []
|
|
385
|
+
);
|
|
386
|
+
} else {
|
|
387
|
+
contextName = config.contexts?.[0] ?? "default";
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
if (!contextName) contextName = config.contexts?.[0] ?? "default";
|
|
391
|
+
writeToContext(contextName, key, String(validated.data));
|
|
392
|
+
}
|
|
393
|
+
return { get, safeGet, save };
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// src/cli-args.ts
|
|
397
|
+
function parseSenvArgs(argv) {
|
|
398
|
+
const config = {};
|
|
399
|
+
let i = 0;
|
|
400
|
+
while (i < argv.length) {
|
|
401
|
+
const arg = argv[i];
|
|
402
|
+
if (arg === "--context" && argv[i + 1] !== void 0) {
|
|
403
|
+
config.contexts = argv[++i].split(",").map((s) => s.trim()).filter(Boolean);
|
|
404
|
+
} else if (arg === "--add-context" && argv[i + 1] !== void 0) {
|
|
405
|
+
config.addContexts = argv[++i].split(",").map((s) => s.trim()).filter(Boolean);
|
|
406
|
+
} else if (arg === "--prompt" && argv[i + 1] !== void 0) {
|
|
407
|
+
const v = argv[++i].toLowerCase();
|
|
408
|
+
if (["always", "never", "fallback", "no-env"].includes(v)) {
|
|
409
|
+
config.prompt = v;
|
|
410
|
+
}
|
|
411
|
+
} else if (arg === "--ignore-env") {
|
|
412
|
+
config.ignoreEnv = true;
|
|
413
|
+
} else if (arg === "--ignore-context") {
|
|
414
|
+
config.ignoreContext = true;
|
|
415
|
+
} else if (arg === "--set" && argv[i + 1] !== void 0) {
|
|
416
|
+
const pair = argv[++i];
|
|
417
|
+
const eq = pair.indexOf("=");
|
|
418
|
+
if (eq > 0) {
|
|
419
|
+
config.set = config.set ?? {};
|
|
420
|
+
config.set[pair.slice(0, eq).trim()] = pair.slice(eq + 1).trim();
|
|
421
|
+
}
|
|
422
|
+
} else if (arg === "--save-prompt" && argv[i + 1] !== void 0) {
|
|
423
|
+
const v = argv[++i].toLowerCase();
|
|
424
|
+
if (["always", "never", "ask"].includes(v)) {
|
|
425
|
+
config.savePrompt = v;
|
|
426
|
+
}
|
|
427
|
+
} else if (arg === "--save-context-to" && argv[i + 1] !== void 0) {
|
|
428
|
+
config.saveContextTo = argv[++i];
|
|
429
|
+
} else if (arg.startsWith("--set=")) {
|
|
430
|
+
const pair = arg.slice(6);
|
|
431
|
+
const eq = pair.indexOf("=");
|
|
432
|
+
if (eq > 0) {
|
|
433
|
+
config.set = config.set ?? {};
|
|
434
|
+
config.set[pair.slice(0, eq).trim()] = pair.slice(eq + 1).trim();
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
i++;
|
|
438
|
+
}
|
|
439
|
+
return config;
|
|
440
|
+
}
|
|
441
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
442
|
+
0 && (module.exports = {
|
|
443
|
+
configure,
|
|
444
|
+
discoverContextPaths,
|
|
445
|
+
getCallbacks,
|
|
446
|
+
getContextValues,
|
|
447
|
+
loadConfig,
|
|
448
|
+
parseSenvArgs,
|
|
449
|
+
resetConfig,
|
|
450
|
+
senv
|
|
451
|
+
});
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
type PromptMode = "always" | "never" | "fallback" | "no-env";
|
|
2
|
+
type SavePromptMode = "always" | "never" | "ask";
|
|
3
|
+
interface SenvConfig {
|
|
4
|
+
/** Replace all contexts with this list (CLI: --context a,b,c) */
|
|
5
|
+
contexts?: string[];
|
|
6
|
+
/** Merge these contexts with existing (CLI: --add-context a,b,c) */
|
|
7
|
+
addContexts?: string[];
|
|
8
|
+
/** When to prompt for variable value */
|
|
9
|
+
prompt?: PromptMode;
|
|
10
|
+
/** Ignore environment variables during resolution */
|
|
11
|
+
ignoreEnv?: boolean;
|
|
12
|
+
/** Ignore loaded context during resolution */
|
|
13
|
+
ignoreContext?: boolean;
|
|
14
|
+
/** Override values: key -> string (CLI: --set key=val) */
|
|
15
|
+
set?: Record<string, string>;
|
|
16
|
+
/** When to ask "save for next time?" */
|
|
17
|
+
savePrompt?: SavePromptMode;
|
|
18
|
+
/** Where to save: context name or "ask" */
|
|
19
|
+
saveContextTo?: "ask" | string;
|
|
20
|
+
/** Root directory for config/context search (default: cwd) */
|
|
21
|
+
root?: string;
|
|
22
|
+
}
|
|
23
|
+
interface SenvCallbacks {
|
|
24
|
+
/** When user was just prompted for a value and savePrompt is ask/always: (variableName, value, contextNames) => context name to save to, or null to skip */
|
|
25
|
+
onAskSaveAfterPrompt?: (name: string, value: unknown, contextNames: string[]) => Promise<string | null>;
|
|
26
|
+
/** When saveContextTo is "ask": (variableName, contextNames) => context name to save to */
|
|
27
|
+
onAskContext?: (name: string, contextNames: string[]) => Promise<string>;
|
|
28
|
+
}
|
|
29
|
+
declare function getCallbacks(): SenvCallbacks;
|
|
30
|
+
/**
|
|
31
|
+
* Load full config: file (from root or cwd) <- env <- programmatic. Precedence: programmatic > env > file.
|
|
32
|
+
*/
|
|
33
|
+
declare function loadConfig(root?: string): SenvConfig;
|
|
34
|
+
/**
|
|
35
|
+
* Set programmatic config (e.g. from CLI flags). Merged on top of env and file in loadConfig().
|
|
36
|
+
*/
|
|
37
|
+
declare function configure(partial: Partial<SenvConfig> & {
|
|
38
|
+
callbacks?: SenvCallbacks;
|
|
39
|
+
}): void;
|
|
40
|
+
/**
|
|
41
|
+
* Reset programmatic config (mainly for tests).
|
|
42
|
+
*/
|
|
43
|
+
declare function resetConfig(): void;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Recursively find all *.context.json files under dir. Returns map: contextName -> absolute path (first found wins).
|
|
47
|
+
*/
|
|
48
|
+
declare function discoverContextPaths(dir: string, found?: Map<string, string>): Map<string, string>;
|
|
49
|
+
/**
|
|
50
|
+
* Load context files in the order of config.contexts; merge into one flat map (later context overwrites earlier for same key).
|
|
51
|
+
*/
|
|
52
|
+
declare function getContextValues(): Record<string, string>;
|
|
53
|
+
|
|
54
|
+
type ValidatorResult<T> = boolean | {
|
|
55
|
+
success: true;
|
|
56
|
+
data?: T;
|
|
57
|
+
} | {
|
|
58
|
+
success: false;
|
|
59
|
+
error?: unknown;
|
|
60
|
+
};
|
|
61
|
+
type PromptFn<T> = (name: string, defaultValue: T) => T | Promise<T>;
|
|
62
|
+
interface SenvVariableOptions<T> {
|
|
63
|
+
key?: string;
|
|
64
|
+
env?: string;
|
|
65
|
+
default?: T;
|
|
66
|
+
validator?: (val: T) => ValidatorResult<T>;
|
|
67
|
+
prompt?: PromptFn<T>;
|
|
68
|
+
}
|
|
69
|
+
interface SenvVariable<T> {
|
|
70
|
+
get(): Promise<T>;
|
|
71
|
+
safeGet(): Promise<{
|
|
72
|
+
success: true;
|
|
73
|
+
value: T;
|
|
74
|
+
} | {
|
|
75
|
+
success: false;
|
|
76
|
+
error?: unknown;
|
|
77
|
+
}>;
|
|
78
|
+
save(value?: T): Promise<void>;
|
|
79
|
+
}
|
|
80
|
+
declare function senv<T>(name: string, options?: SenvVariableOptions<T>): SenvVariable<T>;
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Parse argv (e.g. process.argv.slice(2)) into SenvConfig for configure().
|
|
84
|
+
* Supports: --context a,b,c --add-context x,y --prompt fallback --ignore-env --ignore-context
|
|
85
|
+
* --set key=value --save-prompt ask --save-context-to prod
|
|
86
|
+
*/
|
|
87
|
+
declare function parseSenvArgs(argv: string[]): Partial<SenvConfig>;
|
|
88
|
+
|
|
89
|
+
export { type PromptMode, type SavePromptMode, type SenvCallbacks, type SenvConfig, type SenvVariable, configure, discoverContextPaths, getCallbacks, getContextValues, loadConfig, parseSenvArgs, resetConfig, senv };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
type PromptMode = "always" | "never" | "fallback" | "no-env";
|
|
2
|
+
type SavePromptMode = "always" | "never" | "ask";
|
|
3
|
+
interface SenvConfig {
|
|
4
|
+
/** Replace all contexts with this list (CLI: --context a,b,c) */
|
|
5
|
+
contexts?: string[];
|
|
6
|
+
/** Merge these contexts with existing (CLI: --add-context a,b,c) */
|
|
7
|
+
addContexts?: string[];
|
|
8
|
+
/** When to prompt for variable value */
|
|
9
|
+
prompt?: PromptMode;
|
|
10
|
+
/** Ignore environment variables during resolution */
|
|
11
|
+
ignoreEnv?: boolean;
|
|
12
|
+
/** Ignore loaded context during resolution */
|
|
13
|
+
ignoreContext?: boolean;
|
|
14
|
+
/** Override values: key -> string (CLI: --set key=val) */
|
|
15
|
+
set?: Record<string, string>;
|
|
16
|
+
/** When to ask "save for next time?" */
|
|
17
|
+
savePrompt?: SavePromptMode;
|
|
18
|
+
/** Where to save: context name or "ask" */
|
|
19
|
+
saveContextTo?: "ask" | string;
|
|
20
|
+
/** Root directory for config/context search (default: cwd) */
|
|
21
|
+
root?: string;
|
|
22
|
+
}
|
|
23
|
+
interface SenvCallbacks {
|
|
24
|
+
/** When user was just prompted for a value and savePrompt is ask/always: (variableName, value, contextNames) => context name to save to, or null to skip */
|
|
25
|
+
onAskSaveAfterPrompt?: (name: string, value: unknown, contextNames: string[]) => Promise<string | null>;
|
|
26
|
+
/** When saveContextTo is "ask": (variableName, contextNames) => context name to save to */
|
|
27
|
+
onAskContext?: (name: string, contextNames: string[]) => Promise<string>;
|
|
28
|
+
}
|
|
29
|
+
declare function getCallbacks(): SenvCallbacks;
|
|
30
|
+
/**
|
|
31
|
+
* Load full config: file (from root or cwd) <- env <- programmatic. Precedence: programmatic > env > file.
|
|
32
|
+
*/
|
|
33
|
+
declare function loadConfig(root?: string): SenvConfig;
|
|
34
|
+
/**
|
|
35
|
+
* Set programmatic config (e.g. from CLI flags). Merged on top of env and file in loadConfig().
|
|
36
|
+
*/
|
|
37
|
+
declare function configure(partial: Partial<SenvConfig> & {
|
|
38
|
+
callbacks?: SenvCallbacks;
|
|
39
|
+
}): void;
|
|
40
|
+
/**
|
|
41
|
+
* Reset programmatic config (mainly for tests).
|
|
42
|
+
*/
|
|
43
|
+
declare function resetConfig(): void;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Recursively find all *.context.json files under dir. Returns map: contextName -> absolute path (first found wins).
|
|
47
|
+
*/
|
|
48
|
+
declare function discoverContextPaths(dir: string, found?: Map<string, string>): Map<string, string>;
|
|
49
|
+
/**
|
|
50
|
+
* Load context files in the order of config.contexts; merge into one flat map (later context overwrites earlier for same key).
|
|
51
|
+
*/
|
|
52
|
+
declare function getContextValues(): Record<string, string>;
|
|
53
|
+
|
|
54
|
+
type ValidatorResult<T> = boolean | {
|
|
55
|
+
success: true;
|
|
56
|
+
data?: T;
|
|
57
|
+
} | {
|
|
58
|
+
success: false;
|
|
59
|
+
error?: unknown;
|
|
60
|
+
};
|
|
61
|
+
type PromptFn<T> = (name: string, defaultValue: T) => T | Promise<T>;
|
|
62
|
+
interface SenvVariableOptions<T> {
|
|
63
|
+
key?: string;
|
|
64
|
+
env?: string;
|
|
65
|
+
default?: T;
|
|
66
|
+
validator?: (val: T) => ValidatorResult<T>;
|
|
67
|
+
prompt?: PromptFn<T>;
|
|
68
|
+
}
|
|
69
|
+
interface SenvVariable<T> {
|
|
70
|
+
get(): Promise<T>;
|
|
71
|
+
safeGet(): Promise<{
|
|
72
|
+
success: true;
|
|
73
|
+
value: T;
|
|
74
|
+
} | {
|
|
75
|
+
success: false;
|
|
76
|
+
error?: unknown;
|
|
77
|
+
}>;
|
|
78
|
+
save(value?: T): Promise<void>;
|
|
79
|
+
}
|
|
80
|
+
declare function senv<T>(name: string, options?: SenvVariableOptions<T>): SenvVariable<T>;
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Parse argv (e.g. process.argv.slice(2)) into SenvConfig for configure().
|
|
84
|
+
* Supports: --context a,b,c --add-context x,y --prompt fallback --ignore-env --ignore-context
|
|
85
|
+
* --set key=value --save-prompt ask --save-context-to prod
|
|
86
|
+
*/
|
|
87
|
+
declare function parseSenvArgs(argv: string[]): Partial<SenvConfig>;
|
|
88
|
+
|
|
89
|
+
export { type PromptMode, type SavePromptMode, type SenvCallbacks, type SenvConfig, type SenvVariable, configure, discoverContextPaths, getCallbacks, getContextValues, loadConfig, parseSenvArgs, resetConfig, senv };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
// src/config.ts
|
|
2
|
+
import { readFileSync, existsSync } from "fs";
|
|
3
|
+
import { dirname, join } from "path";
|
|
4
|
+
var CONFIG_FILENAME = "senv.config.json";
|
|
5
|
+
var envKeyMap = {
|
|
6
|
+
SENV_CONTEXT: "contexts",
|
|
7
|
+
SENV_ADD_CONTEXTS: "addContexts",
|
|
8
|
+
SENV_PROMPT: "prompt",
|
|
9
|
+
SENV_IGNORE_ENV: "ignoreEnv",
|
|
10
|
+
SENV_IGNORE_CONTEXT: "ignoreContext",
|
|
11
|
+
SENV_SAVE_PROMPT: "savePrompt",
|
|
12
|
+
SENV_SAVE_CONTEXT_TO: "saveContextTo"
|
|
13
|
+
};
|
|
14
|
+
var programmaticConfig = {};
|
|
15
|
+
var programmaticCallbacks = {};
|
|
16
|
+
function getCallbacks() {
|
|
17
|
+
return { ...programmaticCallbacks };
|
|
18
|
+
}
|
|
19
|
+
function findConfigDir(startDir) {
|
|
20
|
+
let dir = startDir;
|
|
21
|
+
const root = dirname(dir);
|
|
22
|
+
for (; ; ) {
|
|
23
|
+
const candidate = join(dir, CONFIG_FILENAME);
|
|
24
|
+
if (existsSync(candidate)) return dir;
|
|
25
|
+
if (dir === root) break;
|
|
26
|
+
dir = dirname(dir);
|
|
27
|
+
}
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
function configFromEnv() {
|
|
31
|
+
const out = {};
|
|
32
|
+
for (const [envKey, configKey] of Object.entries(envKeyMap)) {
|
|
33
|
+
const val = process.env[envKey];
|
|
34
|
+
if (val === void 0 || val === "") continue;
|
|
35
|
+
if (configKey === "contexts" || configKey === "addContexts") {
|
|
36
|
+
out[configKey] = val.split(",").map((s) => s.trim()).filter(Boolean);
|
|
37
|
+
} else if (configKey === "ignoreEnv" || configKey === "ignoreContext") {
|
|
38
|
+
out[configKey] = val === "1" || val === "true" || val.toLowerCase() === "yes";
|
|
39
|
+
} else if (configKey === "prompt" || configKey === "savePrompt") {
|
|
40
|
+
const v = val.toLowerCase();
|
|
41
|
+
if (configKey === "prompt" && (v === "always" || v === "never" || v === "fallback" || v === "no-env"))
|
|
42
|
+
out[configKey] = v;
|
|
43
|
+
if (configKey === "savePrompt" && (v === "always" || v === "never" || v === "ask"))
|
|
44
|
+
out[configKey] = v;
|
|
45
|
+
} else if (configKey === "saveContextTo") {
|
|
46
|
+
out.saveContextTo = val;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return out;
|
|
50
|
+
}
|
|
51
|
+
function loadConfigFile(configDir) {
|
|
52
|
+
const path = join(configDir, CONFIG_FILENAME);
|
|
53
|
+
if (!existsSync(path)) return {};
|
|
54
|
+
try {
|
|
55
|
+
const raw = readFileSync(path, "utf-8");
|
|
56
|
+
const parsed = JSON.parse(raw);
|
|
57
|
+
const out = {};
|
|
58
|
+
if (Array.isArray(parsed.contexts))
|
|
59
|
+
out.contexts = parsed.contexts.filter(
|
|
60
|
+
(x) => typeof x === "string"
|
|
61
|
+
);
|
|
62
|
+
if (Array.isArray(parsed.addContexts))
|
|
63
|
+
out.addContexts = parsed.addContexts.filter(
|
|
64
|
+
(x) => typeof x === "string"
|
|
65
|
+
);
|
|
66
|
+
if (typeof parsed.prompt === "string" && ["always", "never", "fallback", "no-env"].includes(parsed.prompt))
|
|
67
|
+
out.prompt = parsed.prompt;
|
|
68
|
+
if (typeof parsed.ignoreEnv === "boolean") out.ignoreEnv = parsed.ignoreEnv;
|
|
69
|
+
if (typeof parsed.ignoreContext === "boolean")
|
|
70
|
+
out.ignoreContext = parsed.ignoreContext;
|
|
71
|
+
if (parsed.set && typeof parsed.set === "object" && !Array.isArray(parsed.set))
|
|
72
|
+
out.set = parsed.set;
|
|
73
|
+
if (typeof parsed.savePrompt === "string" && ["always", "never", "ask"].includes(parsed.savePrompt))
|
|
74
|
+
out.savePrompt = parsed.savePrompt;
|
|
75
|
+
if (typeof parsed.saveContextTo === "string")
|
|
76
|
+
out.saveContextTo = parsed.saveContextTo;
|
|
77
|
+
if (typeof parsed.root === "string") out.root = parsed.root;
|
|
78
|
+
return out;
|
|
79
|
+
} catch {
|
|
80
|
+
return {};
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function mergeContexts(fileConfig, envConfig, progConfig) {
|
|
84
|
+
const fromFile = fileConfig.contexts ?? fileConfig.addContexts ?? [];
|
|
85
|
+
const fromEnvAdd = envConfig.addContexts ?? [];
|
|
86
|
+
const fromEnvReplace = envConfig.contexts;
|
|
87
|
+
const fromProgAdd = progConfig.addContexts ?? [];
|
|
88
|
+
const fromProgReplace = progConfig.contexts;
|
|
89
|
+
const replace = fromProgReplace ?? fromEnvReplace;
|
|
90
|
+
if (replace !== void 0) return replace;
|
|
91
|
+
const base = [...fromFile];
|
|
92
|
+
const add = [...fromEnvAdd, ...fromProgAdd];
|
|
93
|
+
const seen = new Set(base);
|
|
94
|
+
for (const c of add) {
|
|
95
|
+
if (!seen.has(c)) {
|
|
96
|
+
seen.add(c);
|
|
97
|
+
base.push(c);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return base;
|
|
101
|
+
}
|
|
102
|
+
function loadConfig(root) {
|
|
103
|
+
const startDir = root ?? programmaticConfig.root ?? process.cwd();
|
|
104
|
+
const configDir = findConfigDir(startDir);
|
|
105
|
+
const fileConfig = configDir ? loadConfigFile(configDir) : {};
|
|
106
|
+
const envConfig = configFromEnv();
|
|
107
|
+
const merged = {
|
|
108
|
+
...fileConfig,
|
|
109
|
+
...envConfig,
|
|
110
|
+
...programmaticConfig
|
|
111
|
+
};
|
|
112
|
+
merged.contexts = mergeContexts(fileConfig, envConfig, programmaticConfig);
|
|
113
|
+
delete merged.addContexts;
|
|
114
|
+
if (configDir && !merged.root) merged.root = configDir;
|
|
115
|
+
else if (!merged.root) merged.root = startDir;
|
|
116
|
+
return merged;
|
|
117
|
+
}
|
|
118
|
+
function configure(partial) {
|
|
119
|
+
const { callbacks, ...configPartial } = partial;
|
|
120
|
+
programmaticConfig = { ...programmaticConfig, ...configPartial };
|
|
121
|
+
if (callbacks) {
|
|
122
|
+
programmaticCallbacks = { ...programmaticCallbacks, ...callbacks };
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
function resetConfig() {
|
|
126
|
+
programmaticConfig = {};
|
|
127
|
+
programmaticCallbacks = {};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// src/context.ts
|
|
131
|
+
import {
|
|
132
|
+
readFileSync as readFileSync2,
|
|
133
|
+
writeFileSync,
|
|
134
|
+
mkdirSync,
|
|
135
|
+
readdirSync,
|
|
136
|
+
statSync
|
|
137
|
+
} from "fs";
|
|
138
|
+
import { join as join2, dirname as dirname2 } from "path";
|
|
139
|
+
var CONTEXT_SUFFIX = ".context.json";
|
|
140
|
+
function discoverContextPathsInternal(dir, found) {
|
|
141
|
+
let entries;
|
|
142
|
+
try {
|
|
143
|
+
entries = readdirSync(dir, { withFileTypes: true }).map((d) => ({
|
|
144
|
+
name: d.name,
|
|
145
|
+
path: join2(dir, d.name)
|
|
146
|
+
}));
|
|
147
|
+
} catch {
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
for (const { name, path } of entries) {
|
|
151
|
+
if (name.endsWith(CONTEXT_SUFFIX) && !name.startsWith(".")) {
|
|
152
|
+
const contextName = name.slice(0, -CONTEXT_SUFFIX.length);
|
|
153
|
+
if (!found.has(contextName)) found.set(contextName, path);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
for (const { name, path } of entries) {
|
|
157
|
+
if (name === "." || name === "..") continue;
|
|
158
|
+
try {
|
|
159
|
+
if (statSync(path).isDirectory())
|
|
160
|
+
discoverContextPathsInternal(path, found);
|
|
161
|
+
} catch {
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
function discoverContextPaths(dir, found = /* @__PURE__ */ new Map()) {
|
|
166
|
+
discoverContextPathsInternal(dir, found);
|
|
167
|
+
return found;
|
|
168
|
+
}
|
|
169
|
+
function getContextValues() {
|
|
170
|
+
const config = loadConfig();
|
|
171
|
+
if (config.ignoreContext) return {};
|
|
172
|
+
const root = config.root ?? process.cwd();
|
|
173
|
+
const paths = discoverContextPaths(root);
|
|
174
|
+
const out = {};
|
|
175
|
+
for (const contextName of config.contexts ?? []) {
|
|
176
|
+
const filePath = paths.get(contextName);
|
|
177
|
+
if (!filePath) continue;
|
|
178
|
+
try {
|
|
179
|
+
const raw = readFileSync2(filePath, "utf-8");
|
|
180
|
+
const data = JSON.parse(raw);
|
|
181
|
+
for (const [k, v] of Object.entries(data)) {
|
|
182
|
+
if (typeof v === "string") out[k] = v;
|
|
183
|
+
}
|
|
184
|
+
} catch {
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return out;
|
|
188
|
+
}
|
|
189
|
+
function getContextWritePath(contextName) {
|
|
190
|
+
const config = loadConfig();
|
|
191
|
+
const root = config.root ?? process.cwd();
|
|
192
|
+
const paths = discoverContextPaths(root);
|
|
193
|
+
const existing = paths.get(contextName);
|
|
194
|
+
if (existing) return existing;
|
|
195
|
+
return join2(root, `${contextName}${CONTEXT_SUFFIX}`);
|
|
196
|
+
}
|
|
197
|
+
function writeToContext(contextName, key, value) {
|
|
198
|
+
const path = getContextWritePath(contextName);
|
|
199
|
+
let data = {};
|
|
200
|
+
try {
|
|
201
|
+
const raw = readFileSync2(path, "utf-8");
|
|
202
|
+
data = JSON.parse(raw);
|
|
203
|
+
} catch {
|
|
204
|
+
}
|
|
205
|
+
data[key] = value;
|
|
206
|
+
const dir = dirname2(path);
|
|
207
|
+
mkdirSync(dir, { recursive: true });
|
|
208
|
+
writeFileSync(path, JSON.stringify(data, null, 2) + "\n", "utf-8");
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// src/prompt-default.ts
|
|
212
|
+
import { createInterface } from "readline";
|
|
213
|
+
function defaultPrompt(name, defaultValue) {
|
|
214
|
+
const defaultStr = defaultValue !== void 0 && defaultValue !== null ? String(defaultValue) : "";
|
|
215
|
+
const message = defaultStr ? `Enter ${name} [${defaultStr}]: ` : `Enter ${name}: `;
|
|
216
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
217
|
+
return new Promise((resolve, reject) => {
|
|
218
|
+
rl.question(message, (answer) => {
|
|
219
|
+
rl.close();
|
|
220
|
+
const trimmed = answer.trim();
|
|
221
|
+
const value = trimmed !== "" ? trimmed : defaultStr;
|
|
222
|
+
resolve(value);
|
|
223
|
+
});
|
|
224
|
+
rl.on("error", (err) => {
|
|
225
|
+
rl.close();
|
|
226
|
+
reject(err);
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// src/variable.ts
|
|
232
|
+
function defaultKeyFromName(name) {
|
|
233
|
+
return name.toLowerCase().replace(/\s+/g, "_").replace(/[^a-z0-9_]/gi, "");
|
|
234
|
+
}
|
|
235
|
+
function defaultEnvFromKey(key) {
|
|
236
|
+
return key.toUpperCase().replace(/-/g, "_");
|
|
237
|
+
}
|
|
238
|
+
function normalizeValidatorResult(result) {
|
|
239
|
+
if (typeof result === "boolean") {
|
|
240
|
+
return result ? { success: true } : { success: false };
|
|
241
|
+
}
|
|
242
|
+
if (result.success === true) return result;
|
|
243
|
+
return result;
|
|
244
|
+
}
|
|
245
|
+
function senv(name, options = {}) {
|
|
246
|
+
const key = options.key ?? defaultKeyFromName(name);
|
|
247
|
+
const envKey = options.env ?? defaultEnvFromKey(key);
|
|
248
|
+
const validator = options.validator;
|
|
249
|
+
const promptFn = options.prompt;
|
|
250
|
+
const defaultValue = options.default;
|
|
251
|
+
async function resolveRaw() {
|
|
252
|
+
const config = loadConfig();
|
|
253
|
+
if (config.set?.[key] !== void 0) return config.set[key];
|
|
254
|
+
if (!config.ignoreEnv) {
|
|
255
|
+
const envVal = process.env[envKey];
|
|
256
|
+
if (envVal !== void 0 && envVal !== "") return envVal;
|
|
257
|
+
}
|
|
258
|
+
if (!config.ignoreContext) {
|
|
259
|
+
const ctx = getContextValues();
|
|
260
|
+
if (ctx[key] !== void 0) return ctx[key];
|
|
261
|
+
}
|
|
262
|
+
if (defaultValue !== void 0) {
|
|
263
|
+
return String(defaultValue);
|
|
264
|
+
}
|
|
265
|
+
return void 0;
|
|
266
|
+
}
|
|
267
|
+
function shouldPrompt(config, hadValue, hadEnv) {
|
|
268
|
+
const mode = config.prompt ?? "fallback";
|
|
269
|
+
if (mode === "never") return false;
|
|
270
|
+
if (mode === "always") return true;
|
|
271
|
+
if (mode === "fallback") return !hadValue;
|
|
272
|
+
if (mode === "no-env") return !hadEnv;
|
|
273
|
+
return false;
|
|
274
|
+
}
|
|
275
|
+
async function getResolvedValue() {
|
|
276
|
+
const config = loadConfig();
|
|
277
|
+
const raw = await resolveRaw();
|
|
278
|
+
const hadEnv = !config.ignoreEnv && process.env[envKey] !== void 0 && process.env[envKey] !== "";
|
|
279
|
+
const hadValue = raw !== void 0;
|
|
280
|
+
let wasPrompted = false;
|
|
281
|
+
let value;
|
|
282
|
+
if (shouldPrompt(config, hadValue, hadEnv)) {
|
|
283
|
+
const defaultForPrompt = raw !== void 0 ? raw : defaultValue;
|
|
284
|
+
const fn = promptFn ?? defaultPrompt;
|
|
285
|
+
value = await Promise.resolve(fn(name, defaultForPrompt));
|
|
286
|
+
wasPrompted = true;
|
|
287
|
+
} else if (raw !== void 0) {
|
|
288
|
+
value = raw;
|
|
289
|
+
} else if (defaultValue !== void 0) {
|
|
290
|
+
value = defaultValue;
|
|
291
|
+
} else {
|
|
292
|
+
throw new Error(`Missing value for variable "${name}" (key: ${key})`);
|
|
293
|
+
}
|
|
294
|
+
return { value, raw, hadEnv, wasPrompted };
|
|
295
|
+
}
|
|
296
|
+
function validate(value) {
|
|
297
|
+
if (!validator) return { success: true, data: value };
|
|
298
|
+
const result = validator(value);
|
|
299
|
+
const normalized = normalizeValidatorResult(result);
|
|
300
|
+
if (normalized.success) {
|
|
301
|
+
const data = "data" in normalized && normalized.data !== void 0 ? normalized.data : value;
|
|
302
|
+
return { success: true, data };
|
|
303
|
+
}
|
|
304
|
+
return {
|
|
305
|
+
success: false,
|
|
306
|
+
error: "error" in normalized ? normalized.error : void 0
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
async function get() {
|
|
310
|
+
const { value, wasPrompted } = await getResolvedValue();
|
|
311
|
+
const validated = validate(value);
|
|
312
|
+
if (!validated.success) {
|
|
313
|
+
throw new Error(
|
|
314
|
+
`Validation failed for "${name}": ${validated.error ?? "unknown"}`
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
const final = validated.data;
|
|
318
|
+
if (wasPrompted) {
|
|
319
|
+
const config = loadConfig();
|
|
320
|
+
const savePrompt = config.savePrompt ?? "never";
|
|
321
|
+
const shouldAskSave = savePrompt === "always" || savePrompt === "ask" && wasPrompted;
|
|
322
|
+
if (shouldAskSave) {
|
|
323
|
+
const callbacks = getCallbacks();
|
|
324
|
+
const ctxToSave = callbacks.onAskSaveAfterPrompt && await callbacks.onAskSaveAfterPrompt(
|
|
325
|
+
name,
|
|
326
|
+
final,
|
|
327
|
+
config.contexts ?? []
|
|
328
|
+
);
|
|
329
|
+
if (ctxToSave) writeToContext(ctxToSave, key, String(final));
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
return final;
|
|
333
|
+
}
|
|
334
|
+
async function safeGet() {
|
|
335
|
+
try {
|
|
336
|
+
const v = await get();
|
|
337
|
+
return { success: true, value: v };
|
|
338
|
+
} catch (err) {
|
|
339
|
+
return { success: false, error: err };
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
async function save(value) {
|
|
343
|
+
const toSave = value ?? (await getResolvedValue()).value;
|
|
344
|
+
const validated = validate(toSave);
|
|
345
|
+
if (!validated.success) {
|
|
346
|
+
throw new Error(
|
|
347
|
+
`Validation failed for "${name}": ${validated.error ?? "unknown"}`
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
const config = loadConfig();
|
|
351
|
+
let contextName = config.saveContextTo;
|
|
352
|
+
if (contextName === "ask") {
|
|
353
|
+
const callbacks = getCallbacks();
|
|
354
|
+
if (typeof callbacks.onAskContext === "function") {
|
|
355
|
+
contextName = await callbacks.onAskContext(
|
|
356
|
+
name,
|
|
357
|
+
config.contexts ?? []
|
|
358
|
+
);
|
|
359
|
+
} else {
|
|
360
|
+
contextName = config.contexts?.[0] ?? "default";
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
if (!contextName) contextName = config.contexts?.[0] ?? "default";
|
|
364
|
+
writeToContext(contextName, key, String(validated.data));
|
|
365
|
+
}
|
|
366
|
+
return { get, safeGet, save };
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// src/cli-args.ts
|
|
370
|
+
function parseSenvArgs(argv) {
|
|
371
|
+
const config = {};
|
|
372
|
+
let i = 0;
|
|
373
|
+
while (i < argv.length) {
|
|
374
|
+
const arg = argv[i];
|
|
375
|
+
if (arg === "--context" && argv[i + 1] !== void 0) {
|
|
376
|
+
config.contexts = argv[++i].split(",").map((s) => s.trim()).filter(Boolean);
|
|
377
|
+
} else if (arg === "--add-context" && argv[i + 1] !== void 0) {
|
|
378
|
+
config.addContexts = argv[++i].split(",").map((s) => s.trim()).filter(Boolean);
|
|
379
|
+
} else if (arg === "--prompt" && argv[i + 1] !== void 0) {
|
|
380
|
+
const v = argv[++i].toLowerCase();
|
|
381
|
+
if (["always", "never", "fallback", "no-env"].includes(v)) {
|
|
382
|
+
config.prompt = v;
|
|
383
|
+
}
|
|
384
|
+
} else if (arg === "--ignore-env") {
|
|
385
|
+
config.ignoreEnv = true;
|
|
386
|
+
} else if (arg === "--ignore-context") {
|
|
387
|
+
config.ignoreContext = true;
|
|
388
|
+
} else if (arg === "--set" && argv[i + 1] !== void 0) {
|
|
389
|
+
const pair = argv[++i];
|
|
390
|
+
const eq = pair.indexOf("=");
|
|
391
|
+
if (eq > 0) {
|
|
392
|
+
config.set = config.set ?? {};
|
|
393
|
+
config.set[pair.slice(0, eq).trim()] = pair.slice(eq + 1).trim();
|
|
394
|
+
}
|
|
395
|
+
} else if (arg === "--save-prompt" && argv[i + 1] !== void 0) {
|
|
396
|
+
const v = argv[++i].toLowerCase();
|
|
397
|
+
if (["always", "never", "ask"].includes(v)) {
|
|
398
|
+
config.savePrompt = v;
|
|
399
|
+
}
|
|
400
|
+
} else if (arg === "--save-context-to" && argv[i + 1] !== void 0) {
|
|
401
|
+
config.saveContextTo = argv[++i];
|
|
402
|
+
} else if (arg.startsWith("--set=")) {
|
|
403
|
+
const pair = arg.slice(6);
|
|
404
|
+
const eq = pair.indexOf("=");
|
|
405
|
+
if (eq > 0) {
|
|
406
|
+
config.set = config.set ?? {};
|
|
407
|
+
config.set[pair.slice(0, eq).trim()] = pair.slice(eq + 1).trim();
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
i++;
|
|
411
|
+
}
|
|
412
|
+
return config;
|
|
413
|
+
}
|
|
414
|
+
export {
|
|
415
|
+
configure,
|
|
416
|
+
discoverContextPaths,
|
|
417
|
+
getCallbacks,
|
|
418
|
+
getContextValues,
|
|
419
|
+
loadConfig,
|
|
420
|
+
parseSenvArgs,
|
|
421
|
+
resetConfig,
|
|
422
|
+
senv
|
|
423
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "scenv",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Environment and context variables with runtime-configurable resolution",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/PKWadsy/senv.git"
|
|
8
|
+
},
|
|
9
|
+
"keywords": [
|
|
10
|
+
"env",
|
|
11
|
+
"config",
|
|
12
|
+
"context",
|
|
13
|
+
"variables"
|
|
14
|
+
],
|
|
15
|
+
"type": "module",
|
|
16
|
+
"main": "dist/index.cjs",
|
|
17
|
+
"module": "dist/index.js",
|
|
18
|
+
"types": "dist/index.d.ts",
|
|
19
|
+
"exports": {
|
|
20
|
+
".": {
|
|
21
|
+
"import": "./dist/index.js",
|
|
22
|
+
"require": "./dist/index.cjs",
|
|
23
|
+
"types": "./dist/index.d.ts"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"dist"
|
|
28
|
+
],
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@types/node": "^20.0.0",
|
|
31
|
+
"@vitest/coverage-v8": "^2.1.9",
|
|
32
|
+
"tsup": "^8.0.0",
|
|
33
|
+
"typescript": "^5.3.0",
|
|
34
|
+
"vitest": "^2.0.0"
|
|
35
|
+
},
|
|
36
|
+
"engines": {
|
|
37
|
+
"node": ">=18"
|
|
38
|
+
},
|
|
39
|
+
"scripts": {
|
|
40
|
+
"build": "tsup src/index.ts --format cjs,esm --dts --clean",
|
|
41
|
+
"test": "vitest run",
|
|
42
|
+
"test:watch": "vitest"
|
|
43
|
+
}
|
|
44
|
+
}
|