@karmaniverous/get-dotenv 4.6.0-0 → 5.0.0-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/README.md +130 -23
- package/dist/cliHost.cjs +1089 -0
- package/dist/cliHost.d.cts +191 -0
- package/dist/cliHost.d.mts +191 -0
- package/dist/cliHost.d.ts +191 -0
- package/dist/cliHost.mjs +1085 -0
- package/dist/config.cjs +247 -0
- package/dist/config.d.cts +53 -0
- package/dist/config.d.mts +53 -0
- package/dist/config.d.ts +53 -0
- package/dist/config.mjs +242 -0
- package/dist/env-overlay.cjs +163 -0
- package/dist/env-overlay.d.cts +48 -0
- package/dist/env-overlay.d.mts +48 -0
- package/dist/env-overlay.d.ts +48 -0
- package/dist/env-overlay.mjs +161 -0
- package/dist/getdotenv.cli.mjs +2788 -734
- package/dist/index.cjs +902 -280
- package/dist/index.d.cts +122 -64
- package/dist/index.d.mts +122 -64
- package/dist/index.d.ts +122 -64
- package/dist/index.mjs +904 -283
- package/dist/plugins-aws.cjs +618 -0
- package/dist/plugins-aws.d.cts +176 -0
- package/dist/plugins-aws.d.mts +176 -0
- package/dist/plugins-aws.d.ts +176 -0
- package/dist/plugins-aws.mjs +616 -0
- package/dist/plugins-batch.cjs +569 -0
- package/dist/plugins-batch.d.cts +198 -0
- package/dist/plugins-batch.d.mts +198 -0
- package/dist/plugins-batch.d.ts +198 -0
- package/dist/plugins-batch.mjs +567 -0
- package/dist/plugins-init.cjs +282 -0
- package/dist/plugins-init.d.cts +180 -0
- package/dist/plugins-init.d.mts +180 -0
- package/dist/plugins-init.d.ts +180 -0
- package/dist/plugins-init.mjs +280 -0
- package/getdotenv.config.json +19 -0
- package/package.json +88 -17
- package/templates/cli/ts/index.ts +9 -0
- package/templates/cli/ts/plugins/hello.ts +17 -0
- package/templates/config/js/getdotenv.config.js +15 -0
- package/templates/config/json/local/getdotenv.config.local.json +7 -0
- package/templates/config/json/public/getdotenv.config.json +12 -0
- package/templates/config/public/getdotenv.config.json +13 -0
- package/templates/config/ts/getdotenv.config.ts +16 -0
- package/templates/config/yaml/local/getdotenv.config.local.yaml +7 -0
- package/templates/config/yaml/public/getdotenv.config.yaml +10 -0
package/dist/index.cjs
CHANGED
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
var commander = require('commander');
|
|
4
|
-
var execa = require('execa');
|
|
5
4
|
var globby = require('globby');
|
|
6
5
|
var packageDirectory = require('package-directory');
|
|
7
6
|
var path = require('path');
|
|
7
|
+
var execa = require('execa');
|
|
8
8
|
var fs = require('fs-extra');
|
|
9
9
|
var url = require('url');
|
|
10
|
-
var crypto = require('crypto');
|
|
11
10
|
var nanoid = require('nanoid');
|
|
12
11
|
var dotenv = require('dotenv');
|
|
12
|
+
var crypto = require('crypto');
|
|
13
|
+
var YAML = require('yaml');
|
|
14
|
+
var zod = require('zod');
|
|
13
15
|
|
|
16
|
+
var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
|
|
14
17
|
/**
|
|
15
18
|
* Dotenv expansion utilities.
|
|
16
19
|
*
|
|
@@ -147,25 +150,90 @@ const dotenvExpandAll = (values = {}, options = {}) => Object.keys(values).reduc
|
|
|
147
150
|
*/
|
|
148
151
|
const dotenvExpandFromProcessEnv = (value) => dotenvExpand(value, process.env);
|
|
149
152
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
pathsDelimiter
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
153
|
+
/**
|
|
154
|
+
* Attach legacy root flags to a Commander program.
|
|
155
|
+
* Uses provided defaults to render help labels without coupling to generators.
|
|
156
|
+
*/
|
|
157
|
+
const attachRootOptions = (program, defaults, opts) => {
|
|
158
|
+
const { defaultEnv, dotenvToken, dynamicPath, env, excludeDynamic, excludeEnv, excludeGlobal, excludePrivate, excludePublic, loadProcess, log, outputPath, paths, pathsDelimiter, pathsDelimiterPattern, privateToken, scripts, shell, varsAssignor, varsAssignorPattern, varsDelimiter, varsDelimiterPattern, } = defaults ?? {};
|
|
159
|
+
const va = typeof defaults?.varsAssignor === 'string' ? defaults.varsAssignor : '=';
|
|
160
|
+
const vd = typeof defaults?.varsDelimiter === 'string' ? defaults.varsDelimiter : ' ';
|
|
161
|
+
// Build initial chain.
|
|
162
|
+
let p = program
|
|
163
|
+
.enablePositionalOptions()
|
|
164
|
+
.passThroughOptions()
|
|
165
|
+
.option('-e, --env <string>', `target environment (dotenv-expanded)`, dotenvExpandFromProcessEnv, env);
|
|
166
|
+
p = p.option('-v, --vars <string>', `extra variables expressed as delimited key-value pairs (dotenv-expanded): ${[
|
|
167
|
+
['KEY1', 'VAL1'],
|
|
168
|
+
['KEY2', 'VAL2'],
|
|
169
|
+
]
|
|
170
|
+
.map((v) => v.join(va))
|
|
171
|
+
.join(vd)}`, dotenvExpandFromProcessEnv);
|
|
172
|
+
// Optional legacy root command flag (kept for generated CLI compatibility).
|
|
173
|
+
// Default is OFF; the generator opts in explicitly.
|
|
174
|
+
{
|
|
175
|
+
p = p.option('-c, --command <string>', 'command executed according to the --shell option, conflicts with cmd subcommand (dotenv-expanded)', dotenvExpandFromProcessEnv);
|
|
176
|
+
}
|
|
177
|
+
p = p
|
|
178
|
+
.option('-o, --output-path <string>', 'consolidated output file (dotenv-expanded)', dotenvExpandFromProcessEnv, outputPath)
|
|
179
|
+
.addOption(new commander.Option('-s, --shell [string]', (() => {
|
|
180
|
+
let defaultLabel = '';
|
|
181
|
+
if (shell !== undefined) {
|
|
182
|
+
if (typeof shell === 'boolean') {
|
|
183
|
+
defaultLabel = ' (default OS shell)';
|
|
184
|
+
}
|
|
185
|
+
else if (typeof shell === 'string') {
|
|
186
|
+
// Safe string interpolation
|
|
187
|
+
defaultLabel = ` (default ${shell})`;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return `command execution shell, no argument for default OS shell or provide shell string${defaultLabel}`;
|
|
191
|
+
})()).conflicts('shellOff'))
|
|
192
|
+
.addOption(new commander.Option('-S, --shell-off', `command execution shell OFF${!shell ? ' (default)' : ''}`).conflicts('shell'))
|
|
193
|
+
.addOption(new commander.Option('-p, --load-process', `load variables to process.env ON${loadProcess ? ' (default)' : ''}`).conflicts('loadProcessOff'))
|
|
194
|
+
.addOption(new commander.Option('-P, --load-process-off', `load variables to process.env OFF${!loadProcess ? ' (default)' : ''}`).conflicts('loadProcess'))
|
|
195
|
+
.addOption(new commander.Option('-a, --exclude-all', `exclude all dotenv variables from loading ON${excludeDynamic &&
|
|
196
|
+
((excludeEnv && excludeGlobal) || (excludePrivate && excludePublic))
|
|
197
|
+
? ' (default)'
|
|
198
|
+
: ''}`).conflicts('excludeAllOff'))
|
|
199
|
+
.addOption(new commander.Option('-A, --exclude-all-off', `exclude all dotenv variables from loading OFF (default)`).conflicts('excludeAll'))
|
|
200
|
+
.addOption(new commander.Option('-z, --exclude-dynamic', `exclude dynamic dotenv variables from loading ON${excludeDynamic ? ' (default)' : ''}`).conflicts('excludeDynamicOff'))
|
|
201
|
+
.addOption(new commander.Option('-Z, --exclude-dynamic-off', `exclude dynamic dotenv variables from loading OFF${!excludeDynamic ? ' (default)' : ''}`).conflicts('excludeDynamic'))
|
|
202
|
+
.addOption(new commander.Option('-n, --exclude-env', `exclude environment-specific dotenv variables from loading${excludeEnv ? ' (default)' : ''}`).conflicts('excludeEnvOff'))
|
|
203
|
+
.addOption(new commander.Option('-N, --exclude-env-off', `exclude environment-specific dotenv variables from loading OFF${!excludeEnv ? ' (default)' : ''}`).conflicts('excludeEnv'))
|
|
204
|
+
.addOption(new commander.Option('-g, --exclude-global', `exclude global dotenv variables from loading ON${excludeGlobal ? ' (default)' : ''}`).conflicts('excludeGlobalOff'))
|
|
205
|
+
.addOption(new commander.Option('-G, --exclude-global-off', `exclude global dotenv variables from loading OFF${!excludeGlobal ? ' (default)' : ''}`).conflicts('excludeGlobal'))
|
|
206
|
+
.addOption(new commander.Option('-r, --exclude-private', `exclude private dotenv variables from loading ON${excludePrivate ? ' (default)' : ''}`).conflicts('excludePrivateOff'))
|
|
207
|
+
.addOption(new commander.Option('-R, --exclude-private-off', `exclude private dotenv variables from loading OFF${!excludePrivate ? ' (default)' : ''}`).conflicts('excludePrivate'))
|
|
208
|
+
.addOption(new commander.Option('-u, --exclude-public', `exclude public dotenv variables from loading ON${excludePublic ? ' (default)' : ''}`).conflicts('excludePublicOff'))
|
|
209
|
+
.addOption(new commander.Option('-U, --exclude-public-off', `exclude public dotenv variables from loading OFF${!excludePublic ? ' (default)' : ''}`).conflicts('excludePublic'))
|
|
210
|
+
.addOption(new commander.Option('-l, --log', `console log loaded variables ON${log ? ' (default)' : ''}`).conflicts('logOff'))
|
|
211
|
+
.addOption(new commander.Option('-L, --log-off', `console log loaded variables OFF${!log ? ' (default)' : ''}`).conflicts('log'))
|
|
212
|
+
.option('--capture', 'capture child process stdio for commands (tests/CI)')
|
|
213
|
+
.option('--default-env <string>', 'default target environment', dotenvExpandFromProcessEnv, defaultEnv)
|
|
214
|
+
.option('--dotenv-token <string>', 'dotenv-expanded token indicating a dotenv file', dotenvExpandFromProcessEnv, dotenvToken)
|
|
215
|
+
.option('--dynamic-path <string>', 'dynamic variables path (.js or .ts; .ts is auto-compiled when esbuild is available, otherwise precompile)', dotenvExpandFromProcessEnv, dynamicPath)
|
|
216
|
+
.option('--paths <string>', 'dotenv-expanded delimited list of paths to dotenv directory', dotenvExpandFromProcessEnv, paths)
|
|
217
|
+
.option('--paths-delimiter <string>', 'paths delimiter string', pathsDelimiter)
|
|
218
|
+
.option('--paths-delimiter-pattern <string>', 'paths delimiter regex pattern', pathsDelimiterPattern)
|
|
219
|
+
.option('--private-token <string>', 'dotenv-expanded token indicating private variables', dotenvExpandFromProcessEnv, privateToken)
|
|
220
|
+
.option('--vars-delimiter <string>', 'vars delimiter string', varsDelimiter)
|
|
221
|
+
.option('--vars-delimiter-pattern <string>', 'vars delimiter regex pattern', varsDelimiterPattern)
|
|
222
|
+
.option('--vars-assignor <string>', 'vars assignment operator string', varsAssignor)
|
|
223
|
+
.option('--vars-assignor-pattern <string>', 'vars assignment operator regex pattern', varsAssignorPattern)
|
|
224
|
+
// Hidden scripts pipe-through (stringified)
|
|
225
|
+
.addOption(new commander.Option('--scripts <string>')
|
|
226
|
+
.default(JSON.stringify(scripts))
|
|
227
|
+
.hideHelp());
|
|
228
|
+
// Diagnostics: opt-in tracing; optional variadic keys after the flag.
|
|
229
|
+
p = p.option('--trace [keys...]', 'emit diagnostics for child env composition (optional keys)');
|
|
230
|
+
return p;
|
|
167
231
|
};
|
|
168
232
|
|
|
233
|
+
/**
|
|
234
|
+
* Batch services (neutral): resolve command and shell settings.
|
|
235
|
+
* Shared by the generator path and the batch plugin to avoid circular deps.
|
|
236
|
+
*/
|
|
169
237
|
/**
|
|
170
238
|
* Resolve a command string from the {@link Scripts} table.
|
|
171
239
|
* A script may be expressed as a string or an object with a `cmd` property.
|
|
@@ -190,6 +258,143 @@ const resolveShell = (scripts, command, shell) => scripts && typeof scripts[comm
|
|
|
190
258
|
? (scripts[command].shell ?? false)
|
|
191
259
|
: (shell ?? false);
|
|
192
260
|
|
|
261
|
+
// Minimal tokenizer for shell-off execution:
|
|
262
|
+
// Splits by whitespace while preserving quoted segments (single or double quotes).
|
|
263
|
+
const tokenize = (command) => {
|
|
264
|
+
const out = [];
|
|
265
|
+
let cur = '';
|
|
266
|
+
let quote = null;
|
|
267
|
+
for (let i = 0; i < command.length; i++) {
|
|
268
|
+
const c = command.charAt(i);
|
|
269
|
+
if (quote) {
|
|
270
|
+
if (c === quote) {
|
|
271
|
+
// Support doubled quotes inside a quoted segment (Windows/PowerShell style):
|
|
272
|
+
// "" -> " and '' -> '
|
|
273
|
+
const next = command.charAt(i + 1);
|
|
274
|
+
if (next === quote) {
|
|
275
|
+
cur += quote;
|
|
276
|
+
i += 1; // skip the second quote
|
|
277
|
+
}
|
|
278
|
+
else {
|
|
279
|
+
// end of quoted segment
|
|
280
|
+
quote = null;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
else {
|
|
284
|
+
cur += c;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
else {
|
|
288
|
+
if (c === '"' || c === "'") {
|
|
289
|
+
quote = c;
|
|
290
|
+
}
|
|
291
|
+
else if (/\s/.test(c)) {
|
|
292
|
+
if (cur) {
|
|
293
|
+
out.push(cur);
|
|
294
|
+
cur = '';
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
else {
|
|
298
|
+
cur += c;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
if (cur)
|
|
303
|
+
out.push(cur);
|
|
304
|
+
return out;
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
const dbg = (...args) => {
|
|
308
|
+
if (process.env.GETDOTENV_DEBUG) {
|
|
309
|
+
// Use stderr to avoid interfering with stdout assertions
|
|
310
|
+
console.error('[getdotenv:run]', ...args);
|
|
311
|
+
}
|
|
312
|
+
};
|
|
313
|
+
// Strip repeated symmetric outer quotes (single or double) until stable.
|
|
314
|
+
// This is safe for argv arrays passed to execa (no quoting needed) and avoids
|
|
315
|
+
// passing quote characters through to Node (e.g., for `node -e "<code>"`).
|
|
316
|
+
// Handles stacked quotes from shells like PowerShell: """code""" -> code.
|
|
317
|
+
const stripOuterQuotes = (s) => {
|
|
318
|
+
let out = s;
|
|
319
|
+
// Repeatedly trim only when the entire string is wrapped in matching quotes.
|
|
320
|
+
// Stop as soon as the ends are asymmetric or no quotes remain.
|
|
321
|
+
while (out.length >= 2) {
|
|
322
|
+
const a = out.charAt(0);
|
|
323
|
+
const b = out.charAt(out.length - 1);
|
|
324
|
+
const symmetric = (a === '"' && b === '"') || (a === "'" && b === "'");
|
|
325
|
+
if (!symmetric)
|
|
326
|
+
break;
|
|
327
|
+
out = out.slice(1, -1);
|
|
328
|
+
}
|
|
329
|
+
return out;
|
|
330
|
+
};
|
|
331
|
+
// Convert NodeJS.ProcessEnv (string | undefined values) to the shape execa
|
|
332
|
+
// expects (Readonly<Partial<Record<string, string>>>), dropping undefineds.
|
|
333
|
+
const sanitizeEnv = (env) => {
|
|
334
|
+
if (!env)
|
|
335
|
+
return undefined;
|
|
336
|
+
const entries = Object.entries(env).filter((e) => typeof e[1] === 'string');
|
|
337
|
+
return entries.length > 0 ? Object.fromEntries(entries) : undefined;
|
|
338
|
+
};
|
|
339
|
+
const runCommand = async (command, shell, opts) => {
|
|
340
|
+
if (shell === false) {
|
|
341
|
+
let file;
|
|
342
|
+
let args = [];
|
|
343
|
+
if (Array.isArray(command)) {
|
|
344
|
+
file = command[0];
|
|
345
|
+
args = command.slice(1).map(stripOuterQuotes);
|
|
346
|
+
}
|
|
347
|
+
else {
|
|
348
|
+
const tokens = tokenize(command);
|
|
349
|
+
file = tokens[0];
|
|
350
|
+
args = tokens.slice(1);
|
|
351
|
+
}
|
|
352
|
+
if (!file)
|
|
353
|
+
return 0;
|
|
354
|
+
dbg('exec (plain)', { file, args, stdio: opts.stdio });
|
|
355
|
+
// Build options without injecting undefined properties (exactOptionalPropertyTypes).
|
|
356
|
+
const envSan = sanitizeEnv(opts.env);
|
|
357
|
+
const plainOpts = {};
|
|
358
|
+
if (opts.cwd !== undefined)
|
|
359
|
+
plainOpts.cwd = opts.cwd;
|
|
360
|
+
if (envSan !== undefined)
|
|
361
|
+
plainOpts.env = envSan;
|
|
362
|
+
if (opts.stdio !== undefined)
|
|
363
|
+
plainOpts.stdio = opts.stdio;
|
|
364
|
+
const result = await execa.execa(file, args, plainOpts);
|
|
365
|
+
if (opts.stdio === 'pipe' && result.stdout) {
|
|
366
|
+
process.stdout.write(result.stdout + (result.stdout.endsWith('\n') ? '' : '\n'));
|
|
367
|
+
}
|
|
368
|
+
const exit = result?.exitCode;
|
|
369
|
+
dbg('exit (plain)', { exitCode: exit });
|
|
370
|
+
return typeof exit === 'number' ? exit : Number.NaN;
|
|
371
|
+
}
|
|
372
|
+
else {
|
|
373
|
+
const commandStr = Array.isArray(command) ? command.join(' ') : command;
|
|
374
|
+
dbg('exec (shell)', {
|
|
375
|
+
shell: typeof shell === 'string' ? shell : 'custom',
|
|
376
|
+
stdio: opts.stdio,
|
|
377
|
+
command: commandStr,
|
|
378
|
+
});
|
|
379
|
+
const envSan = sanitizeEnv(opts.env);
|
|
380
|
+
const shellOpts = { shell };
|
|
381
|
+
if (opts.cwd !== undefined)
|
|
382
|
+
shellOpts.cwd = opts.cwd;
|
|
383
|
+
if (envSan !== undefined)
|
|
384
|
+
shellOpts.env = envSan;
|
|
385
|
+
if (opts.stdio !== undefined)
|
|
386
|
+
shellOpts.stdio = opts.stdio;
|
|
387
|
+
const result = await execa.execaCommand(commandStr, shellOpts);
|
|
388
|
+
const out = result?.stdout;
|
|
389
|
+
if (opts.stdio === 'pipe' && out) {
|
|
390
|
+
process.stdout.write(out + (out.endsWith('\n') ? '' : '\n'));
|
|
391
|
+
}
|
|
392
|
+
const exit = result?.exitCode;
|
|
393
|
+
dbg('exit (shell)', { exitCode: exit });
|
|
394
|
+
return typeof exit === 'number' ? exit : Number.NaN;
|
|
395
|
+
}
|
|
396
|
+
};
|
|
397
|
+
|
|
193
398
|
const globPaths = async ({ globs, logger, pkgCwd, rootPath, }) => {
|
|
194
399
|
let cwd = process.cwd();
|
|
195
400
|
if (pkgCwd) {
|
|
@@ -214,8 +419,10 @@ const globPaths = async ({ globs, logger, pkgCwd, rootPath, }) => {
|
|
|
214
419
|
return { absRootPath, paths };
|
|
215
420
|
};
|
|
216
421
|
const execShellCommandBatch = async ({ command, getDotenvCliOptions, globs, ignoreErrors, list, logger, pkgCwd, rootPath, shell, }) => {
|
|
217
|
-
|
|
218
|
-
|
|
422
|
+
const capture = process.env.GETDOTENV_STDIO === 'pipe' ||
|
|
423
|
+
Boolean(getDotenvCliOptions?.capture); // Require a command only when not listing. In list mode, a command is optional.
|
|
424
|
+
if (!command && !list) {
|
|
425
|
+
logger.error(`No command provided. Use --command or --list.`);
|
|
219
426
|
process.exit(0);
|
|
220
427
|
}
|
|
221
428
|
const { absRootPath, paths } = await globPaths({
|
|
@@ -231,7 +438,13 @@ const execShellCommandBatch = async ({ command, getDotenvCliOptions, globs, igno
|
|
|
231
438
|
logger.info('');
|
|
232
439
|
const headerRootPath = `ROOT: ${absRootPath}`;
|
|
233
440
|
const headerGlobs = `GLOBS: ${globs}`;
|
|
234
|
-
|
|
441
|
+
// Prepare a safe label for the header (avoid undefined in template)
|
|
442
|
+
const commandLabel = Array.isArray(command)
|
|
443
|
+
? command.join(' ')
|
|
444
|
+
: typeof command === 'string' && command.length > 0
|
|
445
|
+
? command
|
|
446
|
+
: '';
|
|
447
|
+
const headerCommand = list ? `CMD: (list only)` : `CMD: ${commandLabel}`;
|
|
235
448
|
logger.info('*'.repeat(Math.max(headerTitle.length, headerRootPath.length, headerGlobs.length, headerCommand.length)));
|
|
236
449
|
logger.info(headerTitle);
|
|
237
450
|
logger.info('');
|
|
@@ -251,17 +464,25 @@ const execShellCommandBatch = async ({ command, getDotenvCliOptions, globs, igno
|
|
|
251
464
|
logger.info(headerCommand);
|
|
252
465
|
// Execute command.
|
|
253
466
|
try {
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
467
|
+
const hasCmd = (typeof command === 'string' && command.length > 0) ||
|
|
468
|
+
(Array.isArray(command) && command.length > 0);
|
|
469
|
+
if (hasCmd) {
|
|
470
|
+
await runCommand(command, shell, {
|
|
471
|
+
cwd: path,
|
|
472
|
+
env: {
|
|
473
|
+
...process.env,
|
|
474
|
+
getDotenvCliOptions: getDotenvCliOptions
|
|
475
|
+
? JSON.stringify(getDotenvCliOptions)
|
|
476
|
+
: undefined,
|
|
477
|
+
},
|
|
478
|
+
stdio: capture ? 'pipe' : 'inherit',
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
else {
|
|
482
|
+
// Should not occur due to the early guard; retain for type safety.
|
|
483
|
+
logger.error(`No command provided. Use --command or --list.`);
|
|
484
|
+
process.exit(0);
|
|
485
|
+
}
|
|
265
486
|
}
|
|
266
487
|
catch (error) {
|
|
267
488
|
if (!ignoreErrors) {
|
|
@@ -381,72 +602,43 @@ const cmdCommand = new commander.Command()
|
|
|
381
602
|
});
|
|
382
603
|
|
|
383
604
|
/**
|
|
384
|
-
* Create the root Commander command with
|
|
385
|
-
* Pure builder: no side-effects; the caller attaches
|
|
605
|
+
* Create the root Commander command with legacy root options (via cliCore)
|
|
606
|
+
* and built-in subcommands. Pure builder: no side-effects; the caller attaches
|
|
607
|
+
* lifecycle hooks separately.
|
|
386
608
|
*/
|
|
387
609
|
const createRootCommand = (opts) => {
|
|
388
|
-
const
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
.name(alias)
|
|
394
|
-
.description(description)
|
|
395
|
-
.enablePositionalOptions()
|
|
396
|
-
.passThroughOptions()
|
|
397
|
-
.option('-e, --env <string>', `target environment (dotenv-expanded)`, dotenvExpandFromProcessEnv, env)
|
|
398
|
-
.option('-v, --vars <string>', `extra variables expressed as delimited key-value pairs (dotenv-expanded): ${[
|
|
399
|
-
['KEY1', 'VAL1'],
|
|
400
|
-
['KEY2', 'VAL2'],
|
|
401
|
-
]
|
|
402
|
-
.map((v) => v.join(varsAssignor ?? '='))
|
|
403
|
-
.join(varsDelimiter ?? ' ')}`, dotenvExpandFromProcessEnv)
|
|
404
|
-
.option('-c, --command <string>', 'command executed according to the --shell option, conflicts with cmd subcommand (dotenv-expanded)', dotenvExpandFromProcessEnv)
|
|
405
|
-
.option('-o, --output-path <string>', 'consolidated output file (dotenv-expanded)', dotenvExpandFromProcessEnv, outputPath)
|
|
406
|
-
.addOption(new commander.Option('-s, --shell [string]', (() => {
|
|
407
|
-
const defaultLabel = shell
|
|
408
|
-
? ` (default ${typeof shell === 'boolean' ? 'OS shell' : shell})`
|
|
409
|
-
: '';
|
|
410
|
-
return `command execution shell, no argument for default OS shell or provide shell string${defaultLabel}`;
|
|
411
|
-
})()).conflicts('shellOff'))
|
|
412
|
-
.addOption(new commander.Option('-S, --shell-off', `command execution shell OFF${!shell ? ' (default)' : ''}`).conflicts('shell'))
|
|
413
|
-
.addOption(new commander.Option('-p, --load-process', `load variables to process.env ON${loadProcess ? ' (default)' : ''}`).conflicts('loadProcessOff'))
|
|
414
|
-
.addOption(new commander.Option('-P, --load-process-off', `load variables to process.env OFF${!loadProcess ? ' (default)' : ''}`).conflicts('loadProcess'))
|
|
415
|
-
.addOption(new commander.Option('-a, --exclude-all', `exclude all dotenv variables from loading ON${excludeAll ? ' (default)' : ''}`).conflicts('excludeAllOff'))
|
|
416
|
-
.addOption(new commander.Option('-A, --exclude-all-off', `exclude all dotenv variables from loading OFF${!excludeAll ? ' (default)' : ''}`).conflicts('excludeAll'))
|
|
417
|
-
.addOption(new commander.Option('-z, --exclude-dynamic', `exclude dynamic dotenv variables from loading ON${excludeDynamic ? ' (default)' : ''}`).conflicts('excludeDynamicOff'))
|
|
418
|
-
.addOption(new commander.Option('-Z, --exclude-dynamic-off', `exclude dynamic dotenv variables from loading OFF${!excludeDynamic ? ' (default)' : ''}`).conflicts('excludeDynamic'))
|
|
419
|
-
.addOption(new commander.Option('-n, --exclude-env', `exclude environment-specific dotenv variables from loading${excludeEnv ? ' (default)' : ''}`).conflicts('excludeEnvOff'))
|
|
420
|
-
.addOption(new commander.Option('-N, --exclude-env-off', `exclude environment-specific dotenv variables from loading OFF${!excludeEnv ? ' (default)' : ''}`).conflicts('excludeEnv'))
|
|
421
|
-
.addOption(new commander.Option('-g, --exclude-global', `exclude global dotenv variables from loading ON${excludeGlobal ? ' (default)' : ''}`).conflicts('excludeGlobalOff'))
|
|
422
|
-
.addOption(new commander.Option('-G, --exclude-global-off', `exclude global dotenv variables from loading OFF${!excludeGlobal ? ' (default)' : ''}`).conflicts('excludeGlobal'))
|
|
423
|
-
.addOption(new commander.Option('-r, --exclude-private', `exclude private dotenv variables from loading ON${excludePrivate ? ' (default)' : ''}`).conflicts('excludePrivateOff'))
|
|
424
|
-
.addOption(new commander.Option('-R, --exclude-private-off', `exclude private dotenv variables from loading OFF${!excludePrivate ? ' (default)' : ''}`).conflicts('excludePrivate'))
|
|
425
|
-
.addOption(new commander.Option('-u, --exclude-public', `exclude public dotenv variables from loading ON${excludePublic ? ' (default)' : ''}`).conflicts('excludePublicOff'))
|
|
426
|
-
.addOption(new commander.Option('-U, --exclude-public-off', `exclude public dotenv variables from loading OFF${!excludePublic ? ' (default)' : ''}`).conflicts('excludePublic'))
|
|
427
|
-
.addOption(new commander.Option('-l, --log', `console log loaded variables ON${log ? ' (default)' : ''}`).conflicts('logOff'))
|
|
428
|
-
.addOption(new commander.Option('-L, --log-off', `console log loaded variables OFF${!log ? ' (default)' : ''}`).conflicts('log'))
|
|
429
|
-
.addOption(new commander.Option('-d, --debug', `debug mode ON${debug ? ' (default)' : ''}`).conflicts('debugOff'))
|
|
430
|
-
.addOption(new commander.Option('-D, --debug-off', `debug mode OFF${!debug ? ' (default)' : ''}`).conflicts('debug'))
|
|
431
|
-
.option('--default-env <string>', 'default target environment', dotenvExpandFromProcessEnv, defaultEnv)
|
|
432
|
-
.option('--dotenv-token <string>', 'dotenv-expanded token indicating a dotenv file', dotenvExpandFromProcessEnv, dotenvToken)
|
|
433
|
-
.option('--dynamic-path <string>', 'dynamic variables path (.js or .ts; .ts is auto-compiled when esbuild is available, otherwise precompile)', dotenvExpandFromProcessEnv, dynamicPath)
|
|
434
|
-
.option('--paths <string>', 'dotenv-expanded delimited list of paths to dotenv directory', dotenvExpandFromProcessEnv, paths)
|
|
435
|
-
.option('--paths-delimiter <string>', 'paths delimiter string', pathsDelimiter)
|
|
436
|
-
.option('--paths-delimiter-pattern <string>', 'paths delimiter regex pattern', pathsDelimiterPattern)
|
|
437
|
-
.option('--private-token <string>', 'dotenv-expanded token indicating private variables', dotenvExpandFromProcessEnv, privateToken)
|
|
438
|
-
.option('--vars-delimiter <string>', 'vars delimiter string', varsDelimiter)
|
|
439
|
-
.option('--vars-delimiter-pattern <string>', 'vars delimiter regex pattern', varsDelimiterPattern)
|
|
440
|
-
.option('--vars-assignor <string>', 'vars assignment operator string', varsAssignor)
|
|
441
|
-
.option('--vars-assignor-pattern <string>', 'vars assignment operator regex pattern', varsAssignorPattern)
|
|
442
|
-
.addOption(new commander.Option('--scripts <string>')
|
|
443
|
-
.default(JSON.stringify(scripts))
|
|
444
|
-
.hideHelp())
|
|
445
|
-
.addCommand(batchCommand)
|
|
446
|
-
.addCommand(cmdCommand, { isDefault: true });
|
|
610
|
+
const program = new commander.Command().name(opts.alias).description(opts.description);
|
|
611
|
+
// Attach legacy root flags using shared cliCore builder to keep parity.
|
|
612
|
+
attachRootOptions(program, opts);
|
|
613
|
+
// Subcommands
|
|
614
|
+
program.addCommand(batchCommand).addCommand(cmdCommand, { isDefault: true });
|
|
447
615
|
return program;
|
|
448
616
|
};
|
|
449
617
|
|
|
618
|
+
// Base root CLI defaults (shared; kept untyped here to avoid cross-layer deps).
|
|
619
|
+
const baseRootOptionDefaults = {
|
|
620
|
+
dotenvToken: '.env',
|
|
621
|
+
loadProcess: true,
|
|
622
|
+
logger: console,
|
|
623
|
+
paths: './',
|
|
624
|
+
pathsDelimiter: ' ',
|
|
625
|
+
privateToken: 'local',
|
|
626
|
+
scripts: {
|
|
627
|
+
'git-status': {
|
|
628
|
+
cmd: 'git branch --show-current && git status -s -u',
|
|
629
|
+
shell: true,
|
|
630
|
+
},
|
|
631
|
+
},
|
|
632
|
+
shell: true,
|
|
633
|
+
vars: '',
|
|
634
|
+
varsAssignor: '=',
|
|
635
|
+
varsDelimiter: ' ',
|
|
636
|
+
// tri-state flags default to unset unless explicitly provided
|
|
637
|
+
// (debug/log/exclude* resolved via flag utils)
|
|
638
|
+
};
|
|
639
|
+
|
|
640
|
+
const baseGetDotenvCliOptions = baseRootOptionDefaults;
|
|
641
|
+
|
|
450
642
|
/** @internal */
|
|
451
643
|
const isPlainObject = (value) => value !== null &&
|
|
452
644
|
typeof value === 'object' &&
|
|
@@ -499,8 +691,7 @@ const getDotenvOptionsFilename = 'getdotenv.config.json';
|
|
|
499
691
|
*/
|
|
500
692
|
const defineDynamic = (d) => d;
|
|
501
693
|
/**
|
|
502
|
-
* Converts programmatic CLI options to `getDotenv` options.
|
|
503
|
-
*
|
|
694
|
+
* Converts programmatic CLI options to `getDotenv` options. *
|
|
504
695
|
* @param cliOptions - CLI options. Defaults to `{}`.
|
|
505
696
|
*
|
|
506
697
|
* @returns `getDotenv` options.
|
|
@@ -509,28 +700,49 @@ const getDotenvCliOptions2Options = ({ paths, pathsDelimiter, pathsDelimiterPatt
|
|
|
509
700
|
/**
|
|
510
701
|
* Convert CLI-facing string options into {@link GetDotenvOptions}.
|
|
511
702
|
*
|
|
512
|
-
* - Splits {@link GetDotenvCliOptions.paths} using either a delimiter
|
|
513
|
-
* or a regular expression pattern into a string array.
|
|
514
|
-
* - Parses {@link GetDotenvCliOptions.vars} as space-separated `KEY=VALUE`
|
|
703
|
+
* - Splits {@link GetDotenvCliOptions.paths} using either a delimiter * or a regular expression pattern into a string array. * - Parses {@link GetDotenvCliOptions.vars} as space-separated `KEY=VALUE`
|
|
515
704
|
* pairs (configurable delimiters) into a {@link ProcessEnv}.
|
|
516
705
|
* - Drops CLI-only keys that have no programmatic equivalent.
|
|
517
706
|
*
|
|
518
707
|
* @remarks
|
|
519
708
|
* Follows exact-optional semantics by not emitting undefined-valued entries.
|
|
520
709
|
*/
|
|
521
|
-
// Drop CLI-only keys
|
|
522
|
-
|
|
710
|
+
// Drop CLI-only keys (debug/scripts) without relying on Record casts.
|
|
711
|
+
// Create a shallow copy then delete optional CLI-only keys if present.
|
|
712
|
+
const restObj = { ...rest };
|
|
713
|
+
delete restObj.debug;
|
|
714
|
+
delete restObj.scripts;
|
|
523
715
|
const splitBy = (value, delim, pattern) => (value ? value.split(pattern ? RegExp(pattern) : (delim ?? ' ')) : []);
|
|
524
|
-
|
|
525
|
-
|
|
716
|
+
// Tolerate vars as either a CLI string ("A=1 B=2") or an object map.
|
|
717
|
+
let parsedVars;
|
|
718
|
+
if (typeof vars === 'string') {
|
|
719
|
+
const kvPairs = splitBy(vars, varsDelimiter, varsDelimiterPattern).map((v) => v.split(varsAssignorPattern
|
|
526
720
|
? RegExp(varsAssignorPattern)
|
|
527
|
-
: (varsAssignor ?? '=')))
|
|
528
|
-
|
|
529
|
-
|
|
721
|
+
: (varsAssignor ?? '=')));
|
|
722
|
+
parsedVars = Object.fromEntries(kvPairs);
|
|
723
|
+
}
|
|
724
|
+
else if (vars && typeof vars === 'object' && !Array.isArray(vars)) {
|
|
725
|
+
// Keep only string or undefined values to match ProcessEnv.
|
|
726
|
+
const entries = Object.entries(vars).filter(([k, v]) => typeof k === 'string' && (typeof v === 'string' || v === undefined));
|
|
727
|
+
parsedVars = Object.fromEntries(entries);
|
|
728
|
+
}
|
|
729
|
+
// Drop undefined-valued entries at the converter stage to match ProcessEnv
|
|
730
|
+
// expectations and the compat test assertions.
|
|
731
|
+
if (parsedVars) {
|
|
732
|
+
parsedVars = Object.fromEntries(Object.entries(parsedVars).filter(([, v]) => v !== undefined));
|
|
733
|
+
}
|
|
734
|
+
// Tolerate paths as either a delimited string or string[]
|
|
735
|
+
// Use a locally cast union type to avoid lint warnings about always-falsy conditions
|
|
736
|
+
// under the RootOptionsShape (which declares paths as string | undefined).
|
|
737
|
+
const pathsAny = paths;
|
|
738
|
+
const pathsOut = Array.isArray(pathsAny)
|
|
739
|
+
? pathsAny.filter((p) => typeof p === 'string')
|
|
740
|
+
: splitBy(pathsAny, pathsDelimiter, pathsDelimiterPattern);
|
|
741
|
+
// Preserve exactOptionalPropertyTypes: only include keys when defined.
|
|
530
742
|
return {
|
|
531
|
-
...
|
|
532
|
-
paths:
|
|
533
|
-
vars: parsedVars,
|
|
743
|
+
...restObj,
|
|
744
|
+
...(pathsOut.length > 0 ? { paths: pathsOut } : {}),
|
|
745
|
+
...(parsedVars !== undefined ? { vars: parsedVars } : {}),
|
|
534
746
|
};
|
|
535
747
|
};
|
|
536
748
|
const resolveGetDotenvOptions = async (customOptions) => {
|
|
@@ -603,6 +815,169 @@ const resolveGetDotenvCliGenerateOptions = async ({ importMetaUrl, ...customOpti
|
|
|
603
815
|
return merged;
|
|
604
816
|
};
|
|
605
817
|
|
|
818
|
+
/**
|
|
819
|
+
* Resolve a tri-state optional boolean flag under exactOptionalPropertyTypes.
|
|
820
|
+
* - If the user explicitly enabled the flag, return true.
|
|
821
|
+
* - If the user explicitly disabled (the "...-off" variant), return undefined (unset).
|
|
822
|
+
* - Otherwise, adopt the default (true → set; false/undefined → unset).
|
|
823
|
+
*
|
|
824
|
+
* @param exclude - The "on" flag value as parsed by Commander.
|
|
825
|
+
* @param excludeOff - The "off" toggle (present when specified) as parsed by Commander.
|
|
826
|
+
* @param defaultValue - The generator default to adopt when no explicit toggle is present.
|
|
827
|
+
* @returns boolean | undefined — use `undefined` to indicate "unset" (do not emit).
|
|
828
|
+
*
|
|
829
|
+
* @example
|
|
830
|
+
* ```ts
|
|
831
|
+
* resolveExclusion(undefined, undefined, true); // => true
|
|
832
|
+
* ```
|
|
833
|
+
*/
|
|
834
|
+
const resolveExclusion = (exclude, excludeOff, defaultValue) => exclude ? true : excludeOff ? undefined : defaultValue ? true : undefined;
|
|
835
|
+
/**
|
|
836
|
+
* Resolve an optional flag with "--exclude-all" overrides.
|
|
837
|
+
* If excludeAll is set and the individual "...-off" is not, force true.
|
|
838
|
+
* If excludeAllOff is set and the individual flag is not explicitly set, unset.
|
|
839
|
+
* Otherwise, adopt the default (true → set; false/undefined → unset).
|
|
840
|
+
*
|
|
841
|
+
* @param exclude - Individual include/exclude flag.
|
|
842
|
+
* @param excludeOff - Individual "...-off" flag.
|
|
843
|
+
* @param defaultValue - Default for the individual flag.
|
|
844
|
+
* @param excludeAll - Global "exclude-all" flag.
|
|
845
|
+
* @param excludeAllOff - Global "exclude-all-off" flag.
|
|
846
|
+
*
|
|
847
|
+
* @example
|
|
848
|
+
* resolveExclusionAll(undefined, undefined, false, true, undefined) =\> true
|
|
849
|
+
*/
|
|
850
|
+
const resolveExclusionAll = (exclude, excludeOff, defaultValue, excludeAll, excludeAllOff) =>
|
|
851
|
+
// Order of precedence:
|
|
852
|
+
// 1) Individual explicit "on" wins outright.
|
|
853
|
+
// 2) Individual explicit "off" wins over any global.
|
|
854
|
+
// 3) Global exclude-all forces true when not explicitly turned off.
|
|
855
|
+
// 4) Global exclude-all-off unsets when the individual wasn't explicitly enabled.
|
|
856
|
+
// 5) Fall back to the default (true => set; false/undefined => unset).
|
|
857
|
+
(() => {
|
|
858
|
+
// Individual "on"
|
|
859
|
+
if (exclude === true)
|
|
860
|
+
return true;
|
|
861
|
+
// Individual "off"
|
|
862
|
+
if (excludeOff === true)
|
|
863
|
+
return undefined;
|
|
864
|
+
// Global "exclude-all" ON (unless explicitly turned off)
|
|
865
|
+
if (excludeAll === true)
|
|
866
|
+
return true;
|
|
867
|
+
// Global "exclude-all-off" (unless explicitly enabled)
|
|
868
|
+
if (excludeAllOff === true)
|
|
869
|
+
return undefined;
|
|
870
|
+
// Default
|
|
871
|
+
return defaultValue ? true : undefined;
|
|
872
|
+
})();
|
|
873
|
+
/**
|
|
874
|
+
* exactOptionalPropertyTypes-safe setter for optional boolean flags:
|
|
875
|
+
* delete when undefined; assign when defined — without requiring an index signature on T.
|
|
876
|
+
*
|
|
877
|
+
* @typeParam T - Target object type.
|
|
878
|
+
* @param obj - The object to write to.
|
|
879
|
+
* @param key - The optional boolean property key of {@link T}.
|
|
880
|
+
* @param value - The value to set or `undefined` to unset.
|
|
881
|
+
*
|
|
882
|
+
* @remarks
|
|
883
|
+
* Writes through a local `Record<string, unknown>` view to avoid requiring an index signature on {@link T}.
|
|
884
|
+
*/
|
|
885
|
+
const setOptionalFlag = (obj, key, value) => {
|
|
886
|
+
const target = obj;
|
|
887
|
+
const k = key;
|
|
888
|
+
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
|
889
|
+
if (value === undefined)
|
|
890
|
+
delete target[k];
|
|
891
|
+
else
|
|
892
|
+
target[k] = value;
|
|
893
|
+
};
|
|
894
|
+
|
|
895
|
+
/**
|
|
896
|
+
* Merge and normalize raw Commander options (current + parent + defaults)
|
|
897
|
+
* into a GetDotenvCliOptions-like object. Types are intentionally wide to
|
|
898
|
+
* avoid cross-layer coupling; callers may cast as needed.
|
|
899
|
+
*/
|
|
900
|
+
const resolveCliOptions = (rawCliOptions, defaults, parentJson) => {
|
|
901
|
+
const parent = typeof parentJson === 'string' && parentJson.length > 0
|
|
902
|
+
? JSON.parse(parentJson)
|
|
903
|
+
: undefined;
|
|
904
|
+
const { command, debugOff, excludeAll, excludeAllOff, excludeDynamicOff, excludeEnvOff, excludeGlobalOff, excludePrivateOff, excludePublicOff, loadProcessOff, logOff, scripts, shellOff, ...rest } = rawCliOptions;
|
|
905
|
+
const current = { ...rest };
|
|
906
|
+
if (typeof scripts === 'string') {
|
|
907
|
+
try {
|
|
908
|
+
current.scripts = JSON.parse(scripts);
|
|
909
|
+
}
|
|
910
|
+
catch {
|
|
911
|
+
// ignore parse errors; leave scripts undefined
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
const merged = defaultsDeep({}, defaults, parent ?? {}, current);
|
|
915
|
+
const d = defaults;
|
|
916
|
+
setOptionalFlag(merged, 'debug', resolveExclusion(merged.debug, debugOff, d.debug));
|
|
917
|
+
setOptionalFlag(merged, 'excludeDynamic', resolveExclusionAll(merged.excludeDynamic, excludeDynamicOff, d.excludeDynamic, excludeAll, excludeAllOff));
|
|
918
|
+
setOptionalFlag(merged, 'excludeEnv', resolveExclusionAll(merged.excludeEnv, excludeEnvOff, d.excludeEnv, excludeAll, excludeAllOff));
|
|
919
|
+
setOptionalFlag(merged, 'excludeGlobal', resolveExclusionAll(merged.excludeGlobal, excludeGlobalOff, d.excludeGlobal, excludeAll, excludeAllOff));
|
|
920
|
+
setOptionalFlag(merged, 'excludePrivate', resolveExclusionAll(merged.excludePrivate, excludePrivateOff, d.excludePrivate, excludeAll, excludeAllOff));
|
|
921
|
+
setOptionalFlag(merged, 'excludePublic', resolveExclusionAll(merged.excludePublic, excludePublicOff, d.excludePublic, excludeAll, excludeAllOff));
|
|
922
|
+
setOptionalFlag(merged, 'log', resolveExclusion(merged.log, logOff, d.log));
|
|
923
|
+
setOptionalFlag(merged, 'loadProcess', resolveExclusion(merged.loadProcess, loadProcessOff, d.loadProcess));
|
|
924
|
+
// Normalize shell for predictability: explicit default shell per OS.
|
|
925
|
+
const defaultShell = process.platform === 'win32' ? 'powershell.exe' : '/bin/bash';
|
|
926
|
+
let resolvedShell = merged.shell;
|
|
927
|
+
if (shellOff)
|
|
928
|
+
resolvedShell = false;
|
|
929
|
+
else if (resolvedShell === true || resolvedShell === undefined) {
|
|
930
|
+
resolvedShell = defaultShell;
|
|
931
|
+
}
|
|
932
|
+
else if (typeof resolvedShell !== 'string' &&
|
|
933
|
+
typeof defaults.shell === 'string') {
|
|
934
|
+
resolvedShell = defaults.shell;
|
|
935
|
+
}
|
|
936
|
+
merged.shell = resolvedShell;
|
|
937
|
+
const cmd = typeof command === 'string' ? command : undefined;
|
|
938
|
+
return cmd !== undefined ? { merged, command: cmd } : { merged };
|
|
939
|
+
};
|
|
940
|
+
|
|
941
|
+
const applyKv = (current, kv) => {
|
|
942
|
+
if (!kv || Object.keys(kv).length === 0)
|
|
943
|
+
return current;
|
|
944
|
+
const expanded = dotenvExpandAll(kv, { ref: current, progressive: true });
|
|
945
|
+
return { ...current, ...expanded };
|
|
946
|
+
};
|
|
947
|
+
const applyConfigSlice = (current, cfg, env) => {
|
|
948
|
+
if (!cfg)
|
|
949
|
+
return current;
|
|
950
|
+
// kind axis: global then env (env overrides global)
|
|
951
|
+
const afterGlobal = applyKv(current, cfg.vars);
|
|
952
|
+
const envKv = env && cfg.envVars ? cfg.envVars[env] : undefined;
|
|
953
|
+
return applyKv(afterGlobal, envKv);
|
|
954
|
+
};
|
|
955
|
+
/**
|
|
956
|
+
* Overlay config-provided values onto a base ProcessEnv using precedence axes:
|
|
957
|
+
* - kind: env \> global
|
|
958
|
+
* - privacy: local \> public
|
|
959
|
+
* - source: project \> packaged \> base
|
|
960
|
+
*
|
|
961
|
+
* Programmatic explicit vars (if provided) override all config slices.
|
|
962
|
+
* Progressive expansion is applied within each slice.
|
|
963
|
+
*/
|
|
964
|
+
const overlayEnv = ({ base, env, configs, programmaticVars, }) => {
|
|
965
|
+
let current = { ...base };
|
|
966
|
+
// Source: packaged (public -> local)
|
|
967
|
+
current = applyConfigSlice(current, configs.packaged, env);
|
|
968
|
+
// Packaged "local" is not expected by policy; if present, honor it.
|
|
969
|
+
// We do not have a separate object for packaged.local in sources, keep as-is.
|
|
970
|
+
// Source: project (public -> local)
|
|
971
|
+
current = applyConfigSlice(current, configs.project?.public, env);
|
|
972
|
+
current = applyConfigSlice(current, configs.project?.local, env);
|
|
973
|
+
// Programmatic explicit vars (top of static tier)
|
|
974
|
+
if (programmaticVars) {
|
|
975
|
+
const toApply = Object.fromEntries(Object.entries(programmaticVars).filter(([_k, v]) => typeof v === 'string'));
|
|
976
|
+
current = applyKv(current, toApply);
|
|
977
|
+
}
|
|
978
|
+
return current;
|
|
979
|
+
};
|
|
980
|
+
|
|
606
981
|
/**
|
|
607
982
|
* Asynchronously read a dotenv file & parse it into an object.
|
|
608
983
|
*
|
|
@@ -618,65 +993,95 @@ const readDotenv = async (path) => {
|
|
|
618
993
|
}
|
|
619
994
|
};
|
|
620
995
|
|
|
621
|
-
const importDefault = async (fileUrl) => {
|
|
996
|
+
const importDefault$1 = async (fileUrl) => {
|
|
622
997
|
const mod = (await import(fileUrl));
|
|
623
998
|
return mod.default;
|
|
624
999
|
};
|
|
625
|
-
/**
|
|
626
|
-
* @internal Compute a short hash from path + mtime for cache filenames.
|
|
627
|
-
*/
|
|
628
1000
|
const cacheHash = (absPath, mtimeMs) => crypto.createHash('sha1')
|
|
629
1001
|
.update(absPath)
|
|
630
1002
|
.update(String(mtimeMs))
|
|
631
1003
|
.digest('hex')
|
|
632
1004
|
.slice(0, 12);
|
|
633
1005
|
/**
|
|
634
|
-
*
|
|
635
|
-
* - .
|
|
636
|
-
* - .ts/.tsx: try direct import (in case a TS loader is active), otherwise:
|
|
637
|
-
* - esbuild (if present): bundle to a temp ESM file and import it
|
|
638
|
-
* - fallback: typescript.transpileModule (single-file), then import temp file
|
|
1006
|
+
* Remove older compiled cache files for a given source base name, keeping
|
|
1007
|
+
* at most `keep` most-recent files. Errors are ignored by design.
|
|
639
1008
|
*/
|
|
640
|
-
const
|
|
641
|
-
|
|
642
|
-
|
|
1009
|
+
const cleanupOldCacheFiles = async (cacheDir, baseName, keep = Math.max(1, Number.parseInt(process.env.GETDOTENV_CACHE_KEEP ?? '2'))) => {
|
|
1010
|
+
try {
|
|
1011
|
+
const entries = await fs.readdir(cacheDir);
|
|
1012
|
+
const mine = entries
|
|
1013
|
+
.filter((f) => f.startsWith(`${baseName}.`) && f.endsWith('.mjs'))
|
|
1014
|
+
.map((f) => path.join(cacheDir, f));
|
|
1015
|
+
if (mine.length <= keep)
|
|
1016
|
+
return;
|
|
1017
|
+
const stats = await Promise.all(mine.map(async (p) => ({ p, mtimeMs: (await fs.stat(p)).mtimeMs })));
|
|
1018
|
+
stats.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
1019
|
+
const toDelete = stats.slice(keep).map((s) => s.p);
|
|
1020
|
+
await Promise.all(toDelete.map(async (p) => {
|
|
1021
|
+
try {
|
|
1022
|
+
await fs.remove(p);
|
|
1023
|
+
}
|
|
1024
|
+
catch {
|
|
1025
|
+
// best-effort cleanup
|
|
1026
|
+
}
|
|
1027
|
+
}));
|
|
1028
|
+
}
|
|
1029
|
+
catch {
|
|
1030
|
+
// best-effort cleanup
|
|
1031
|
+
}
|
|
1032
|
+
};
|
|
1033
|
+
/**
|
|
1034
|
+
* Load a module default export from a JS/TS file with robust fallbacks:
|
|
1035
|
+
* - .js/.mjs/.cjs: direct import * - .ts/.mts/.cts/.tsx:
|
|
1036
|
+
* 1) try direct import (if a TS loader is active),
|
|
1037
|
+
* 2) esbuild bundle to a temp ESM file,
|
|
1038
|
+
* 3) typescript.transpileModule fallback for simple modules.
|
|
1039
|
+
*
|
|
1040
|
+
* @param absPath - absolute path to source file
|
|
1041
|
+
* @param cacheDirName - cache subfolder under .tsbuild
|
|
1042
|
+
*/
|
|
1043
|
+
const loadModuleDefault = async (absPath, cacheDirName) => {
|
|
643
1044
|
const ext = path.extname(absPath).toLowerCase();
|
|
644
1045
|
const fileUrl = url.pathToFileURL(absPath).toString();
|
|
645
|
-
if (
|
|
646
|
-
return importDefault(fileUrl);
|
|
1046
|
+
if (!['.ts', '.mts', '.cts', '.tsx'].includes(ext)) {
|
|
1047
|
+
return importDefault$1(fileUrl);
|
|
647
1048
|
}
|
|
648
|
-
// Try direct import (
|
|
1049
|
+
// Try direct import first (TS loader active)
|
|
649
1050
|
try {
|
|
650
|
-
const dyn = await importDefault(fileUrl);
|
|
1051
|
+
const dyn = await importDefault$1(fileUrl);
|
|
651
1052
|
if (dyn)
|
|
652
1053
|
return dyn;
|
|
653
1054
|
}
|
|
654
1055
|
catch {
|
|
655
|
-
|
|
1056
|
+
/* fall through */
|
|
656
1057
|
}
|
|
657
1058
|
const stat = await fs.stat(absPath);
|
|
658
1059
|
const hash = cacheHash(absPath, stat.mtimeMs);
|
|
659
|
-
const cacheDir = path.resolve('.tsbuild',
|
|
1060
|
+
const cacheDir = path.resolve('.tsbuild', cacheDirName);
|
|
1061
|
+
await fs.ensureDir(cacheDir);
|
|
660
1062
|
const cacheFile = path.join(cacheDir, `${path.basename(absPath)}.${hash}.mjs`);
|
|
661
|
-
// Try esbuild
|
|
1063
|
+
// Try esbuild
|
|
662
1064
|
try {
|
|
663
1065
|
const esbuild = (await import('esbuild'));
|
|
664
|
-
await fs.ensureDir(cacheDir);
|
|
665
1066
|
await esbuild.build({
|
|
666
1067
|
entryPoints: [absPath],
|
|
667
1068
|
bundle: true,
|
|
668
1069
|
platform: 'node',
|
|
669
1070
|
format: 'esm',
|
|
670
|
-
target: '
|
|
1071
|
+
target: 'node20',
|
|
671
1072
|
outfile: cacheFile,
|
|
672
1073
|
sourcemap: false,
|
|
673
1074
|
logLevel: 'silent',
|
|
674
1075
|
});
|
|
675
|
-
|
|
1076
|
+
const result = await importDefault$1(url.pathToFileURL(cacheFile).toString());
|
|
1077
|
+
// Best-effort: trim older cache files for this source.
|
|
1078
|
+
await cleanupOldCacheFiles(cacheDir, path.basename(absPath));
|
|
1079
|
+
return result;
|
|
676
1080
|
}
|
|
677
1081
|
catch {
|
|
678
|
-
|
|
1082
|
+
/* fall through to TS transpile */
|
|
679
1083
|
}
|
|
1084
|
+
// TypeScript transpile fallback
|
|
680
1085
|
try {
|
|
681
1086
|
const ts = (await import('typescript'));
|
|
682
1087
|
const code = await fs.readFile(absPath, 'utf-8');
|
|
@@ -686,16 +1091,19 @@ const loadDynamicFromPath = async (absPath) => {
|
|
|
686
1091
|
target: 'ES2022',
|
|
687
1092
|
moduleResolution: 'NodeNext',
|
|
688
1093
|
},
|
|
689
|
-
});
|
|
690
|
-
await fs.
|
|
691
|
-
await
|
|
692
|
-
|
|
1094
|
+
}).outputText;
|
|
1095
|
+
await fs.writeFile(cacheFile, out, 'utf-8');
|
|
1096
|
+
const result = await importDefault$1(url.pathToFileURL(cacheFile).toString());
|
|
1097
|
+
// Best-effort: trim older cache files for this source.
|
|
1098
|
+
await cleanupOldCacheFiles(cacheDir, path.basename(absPath));
|
|
1099
|
+
return result;
|
|
693
1100
|
}
|
|
694
1101
|
catch {
|
|
695
|
-
|
|
696
|
-
|
|
1102
|
+
// Caller decides final error wording; rethrow for upstream mapping.
|
|
1103
|
+
throw new Error(`Unable to load JS/TS module: ${absPath}. Install 'esbuild' or ensure a TS loader.`);
|
|
697
1104
|
}
|
|
698
1105
|
};
|
|
1106
|
+
|
|
699
1107
|
/**
|
|
700
1108
|
* Asynchronously process dotenv files of the form `.env[.<ENV>][.<PRIVATE_TOKEN>]`
|
|
701
1109
|
*
|
|
@@ -780,7 +1188,16 @@ const getDotenv = async (options = {}) => {
|
|
|
780
1188
|
}
|
|
781
1189
|
else if (dynamicPath) {
|
|
782
1190
|
const absDynamicPath = path.resolve(dynamicPath);
|
|
783
|
-
|
|
1191
|
+
if (await fs.exists(absDynamicPath)) {
|
|
1192
|
+
try {
|
|
1193
|
+
dynamic = await loadModuleDefault(absDynamicPath, 'getdotenv-dynamic');
|
|
1194
|
+
}
|
|
1195
|
+
catch {
|
|
1196
|
+
// Preserve legacy error text for compatibility with tests/docs.
|
|
1197
|
+
throw new Error(`Unable to load dynamic TypeScript file: ${absDynamicPath}. ` +
|
|
1198
|
+
`Install 'esbuild' (devDependency) to enable TypeScript dynamic modules.`);
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
784
1201
|
}
|
|
785
1202
|
if (dynamic) {
|
|
786
1203
|
try {
|
|
@@ -819,72 +1236,320 @@ const getDotenv = async (options = {}) => {
|
|
|
819
1236
|
};
|
|
820
1237
|
|
|
821
1238
|
/**
|
|
822
|
-
*
|
|
823
|
-
*
|
|
824
|
-
* -
|
|
825
|
-
* -
|
|
826
|
-
*
|
|
827
|
-
* @param exclude - The "on" flag value as parsed by Commander.
|
|
828
|
-
* @param excludeOff - The "off" toggle (present when specified) as parsed by Commander.
|
|
829
|
-
* @param defaultValue - The generator default to adopt when no explicit toggle is present.
|
|
830
|
-
* @returns boolean | undefined — use `undefined` to indicate "unset" (do not emit).
|
|
831
|
-
*
|
|
832
|
-
* @example
|
|
833
|
-
* ```ts
|
|
834
|
-
* resolveExclusion(undefined, undefined, true); // => true
|
|
835
|
-
* ```
|
|
1239
|
+
* Zod schemas for configuration files discovered by the new loader. *
|
|
1240
|
+
* Notes:
|
|
1241
|
+
* - RAW: all fields optional; shapes are stringly-friendly (paths may be string[] or string).
|
|
1242
|
+
* - RESOLVED: normalized shapes (paths always string[]).
|
|
1243
|
+
* - For this step (JSON/YAML only), any defined `dynamic` will be rejected by the loader.
|
|
836
1244
|
*/
|
|
837
|
-
|
|
1245
|
+
// String-only env value map
|
|
1246
|
+
const stringMap = zod.z.record(zod.z.string(), zod.z.string());
|
|
1247
|
+
const envStringMap = zod.z.record(zod.z.string(), stringMap);
|
|
1248
|
+
// Allow string[] or single string for "paths" in RAW; normalize later.
|
|
1249
|
+
const rawPathsSchema = zod.z.union([zod.z.array(zod.z.string()), zod.z.string()]).optional();
|
|
1250
|
+
const getDotenvConfigSchemaRaw = zod.z.object({
|
|
1251
|
+
dotenvToken: zod.z.string().optional(),
|
|
1252
|
+
privateToken: zod.z.string().optional(),
|
|
1253
|
+
paths: rawPathsSchema,
|
|
1254
|
+
loadProcess: zod.z.boolean().optional(),
|
|
1255
|
+
log: zod.z.boolean().optional(),
|
|
1256
|
+
shell: zod.z.union([zod.z.string(), zod.z.boolean()]).optional(),
|
|
1257
|
+
scripts: zod.z.record(zod.z.string(), zod.z.unknown()).optional(), // Scripts validation left wide; generator validates elsewhere
|
|
1258
|
+
vars: stringMap.optional(), // public, global
|
|
1259
|
+
envVars: envStringMap.optional(), // public, per-env
|
|
1260
|
+
// Dynamic in config (JS/TS only). JSON/YAML loader will reject if set.
|
|
1261
|
+
dynamic: zod.z.unknown().optional(),
|
|
1262
|
+
// Per-plugin config bag; validated by plugins/host when used.
|
|
1263
|
+
plugins: zod.z.record(zod.z.string(), zod.z.unknown()).optional(),
|
|
1264
|
+
});
|
|
1265
|
+
// Normalize paths to string[]
|
|
1266
|
+
const normalizePaths = (p) => p === undefined ? undefined : Array.isArray(p) ? p : [p];
|
|
1267
|
+
const getDotenvConfigSchemaResolved = getDotenvConfigSchemaRaw.transform((raw) => ({
|
|
1268
|
+
...raw,
|
|
1269
|
+
paths: normalizePaths(raw.paths),
|
|
1270
|
+
}));
|
|
1271
|
+
|
|
1272
|
+
// Discovery candidates (first match wins per scope/privacy).
|
|
1273
|
+
// Order preserves historical JSON/YAML precedence; JS/TS added afterwards.
|
|
1274
|
+
const PUBLIC_FILENAMES = [
|
|
1275
|
+
'getdotenv.config.json',
|
|
1276
|
+
'getdotenv.config.yaml',
|
|
1277
|
+
'getdotenv.config.yml',
|
|
1278
|
+
'getdotenv.config.js',
|
|
1279
|
+
'getdotenv.config.mjs',
|
|
1280
|
+
'getdotenv.config.cjs',
|
|
1281
|
+
'getdotenv.config.ts',
|
|
1282
|
+
'getdotenv.config.mts',
|
|
1283
|
+
'getdotenv.config.cts',
|
|
1284
|
+
];
|
|
1285
|
+
const LOCAL_FILENAMES = [
|
|
1286
|
+
'getdotenv.config.local.json',
|
|
1287
|
+
'getdotenv.config.local.yaml',
|
|
1288
|
+
'getdotenv.config.local.yml',
|
|
1289
|
+
'getdotenv.config.local.js',
|
|
1290
|
+
'getdotenv.config.local.mjs',
|
|
1291
|
+
'getdotenv.config.local.cjs',
|
|
1292
|
+
'getdotenv.config.local.ts',
|
|
1293
|
+
'getdotenv.config.local.mts',
|
|
1294
|
+
'getdotenv.config.local.cts',
|
|
1295
|
+
];
|
|
1296
|
+
const isYaml = (p) => ['.yaml', '.yml'].includes(path.extname(p).toLowerCase());
|
|
1297
|
+
const isJson = (p) => path.extname(p).toLowerCase() === '.json';
|
|
1298
|
+
const isJsOrTs = (p) => ['.js', '.mjs', '.cjs', '.ts', '.mts', '.cts'].includes(path.extname(p).toLowerCase());
|
|
1299
|
+
// --- Internal JS/TS module loader helpers (default export) ---
|
|
1300
|
+
const importDefault = async (fileUrl) => {
|
|
1301
|
+
const mod = (await import(fileUrl));
|
|
1302
|
+
return mod.default;
|
|
1303
|
+
};
|
|
1304
|
+
const cacheName = (absPath, suffix) => {
|
|
1305
|
+
// sanitized filename with suffix; recompile on mtime changes not tracked here (simplified)
|
|
1306
|
+
const base = path.basename(absPath).replace(/[^a-zA-Z0-9._-]/g, '_');
|
|
1307
|
+
return `${base}.${suffix}.mjs`;
|
|
1308
|
+
};
|
|
1309
|
+
const ensureDir = async (dir) => {
|
|
1310
|
+
await fs.ensureDir(dir);
|
|
1311
|
+
return dir;
|
|
1312
|
+
};
|
|
1313
|
+
const loadJsTsDefault = async (absPath) => {
|
|
1314
|
+
const fileUrl = url.pathToFileURL(absPath).toString();
|
|
1315
|
+
const ext = path.extname(absPath).toLowerCase();
|
|
1316
|
+
if (ext === '.js' || ext === '.mjs' || ext === '.cjs') {
|
|
1317
|
+
return importDefault(fileUrl);
|
|
1318
|
+
}
|
|
1319
|
+
// Try direct import first in case a TS loader is active.
|
|
1320
|
+
try {
|
|
1321
|
+
const val = await importDefault(fileUrl);
|
|
1322
|
+
if (val)
|
|
1323
|
+
return val;
|
|
1324
|
+
}
|
|
1325
|
+
catch {
|
|
1326
|
+
/* fallthrough */
|
|
1327
|
+
}
|
|
1328
|
+
// esbuild bundle to a temp ESM file
|
|
1329
|
+
try {
|
|
1330
|
+
const esbuild = (await import('esbuild'));
|
|
1331
|
+
const outDir = await ensureDir(path.resolve('.tsbuild', 'getdotenv-config'));
|
|
1332
|
+
const outfile = path.join(outDir, cacheName(absPath, 'bundle'));
|
|
1333
|
+
await esbuild.build({
|
|
1334
|
+
entryPoints: [absPath],
|
|
1335
|
+
bundle: true,
|
|
1336
|
+
platform: 'node',
|
|
1337
|
+
format: 'esm',
|
|
1338
|
+
target: 'node20',
|
|
1339
|
+
outfile,
|
|
1340
|
+
sourcemap: false,
|
|
1341
|
+
logLevel: 'silent',
|
|
1342
|
+
});
|
|
1343
|
+
return await importDefault(url.pathToFileURL(outfile).toString());
|
|
1344
|
+
}
|
|
1345
|
+
catch {
|
|
1346
|
+
/* fallthrough to TS transpile */
|
|
1347
|
+
}
|
|
1348
|
+
// typescript.transpileModule simple transpile (single-file)
|
|
1349
|
+
try {
|
|
1350
|
+
const ts = (await import('typescript'));
|
|
1351
|
+
const src = await fs.readFile(absPath, 'utf-8');
|
|
1352
|
+
const out = ts.transpileModule(src, {
|
|
1353
|
+
compilerOptions: {
|
|
1354
|
+
module: 'ESNext',
|
|
1355
|
+
target: 'ES2022',
|
|
1356
|
+
moduleResolution: 'NodeNext',
|
|
1357
|
+
},
|
|
1358
|
+
}).outputText;
|
|
1359
|
+
const outDir = await ensureDir(path.resolve('.tsbuild', 'getdotenv-config'));
|
|
1360
|
+
const outfile = path.join(outDir, cacheName(absPath, 'ts'));
|
|
1361
|
+
await fs.writeFile(outfile, out, 'utf-8');
|
|
1362
|
+
return await importDefault(url.pathToFileURL(outfile).toString());
|
|
1363
|
+
}
|
|
1364
|
+
catch {
|
|
1365
|
+
throw new Error(`Unable to load JS/TS config: ${absPath}. Install 'esbuild' for robust bundling or ensure a TS loader.`);
|
|
1366
|
+
}
|
|
1367
|
+
};
|
|
838
1368
|
/**
|
|
839
|
-
*
|
|
840
|
-
*
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
1369
|
+
* Discover JSON/YAML config files in the packaged root and project root.
|
|
1370
|
+
* Order: packaged public → project public → project local. */
|
|
1371
|
+
const discoverConfigFiles = async (importMetaUrl) => {
|
|
1372
|
+
const files = [];
|
|
1373
|
+
// Packaged root via importMetaUrl (optional)
|
|
1374
|
+
if (importMetaUrl) {
|
|
1375
|
+
const fromUrl = url.fileURLToPath(importMetaUrl);
|
|
1376
|
+
const packagedRoot = await packageDirectory.packageDirectory({ cwd: fromUrl });
|
|
1377
|
+
if (packagedRoot) {
|
|
1378
|
+
for (const name of PUBLIC_FILENAMES) {
|
|
1379
|
+
const p = path.join(packagedRoot, name);
|
|
1380
|
+
if (await fs.pathExists(p)) {
|
|
1381
|
+
files.push({ path: p, privacy: 'public', scope: 'packaged' });
|
|
1382
|
+
break; // only one public file expected per scope
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
// By policy, packaged .local is not expected; skip even if present.
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
// Project root (from current working directory)
|
|
1389
|
+
const projectRoot = await packageDirectory.packageDirectory();
|
|
1390
|
+
if (projectRoot) {
|
|
1391
|
+
for (const name of PUBLIC_FILENAMES) {
|
|
1392
|
+
const p = path.join(projectRoot, name);
|
|
1393
|
+
if (await fs.pathExists(p)) {
|
|
1394
|
+
files.push({ path: p, privacy: 'public', scope: 'project' });
|
|
1395
|
+
break;
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
for (const name of LOCAL_FILENAMES) {
|
|
1399
|
+
const p = path.join(projectRoot, name);
|
|
1400
|
+
if (await fs.pathExists(p)) {
|
|
1401
|
+
files.push({ path: p, privacy: 'local', scope: 'project' });
|
|
1402
|
+
break;
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
return files;
|
|
1407
|
+
};
|
|
1408
|
+
/**
|
|
1409
|
+
* Load a single config file (JSON/YAML). JS/TS is not supported in this step.
|
|
1410
|
+
* Validates with Zod RAW schema, then normalizes to RESOLVED.
|
|
849
1411
|
*
|
|
850
|
-
*
|
|
851
|
-
*
|
|
1412
|
+
* For JSON/YAML: if a "dynamic" property is present, throws with guidance.
|
|
1413
|
+
* For JS/TS: default export is loaded; "dynamic" is allowed.
|
|
1414
|
+
*/
|
|
1415
|
+
const loadConfigFile = async (filePath) => {
|
|
1416
|
+
let raw = {};
|
|
1417
|
+
try {
|
|
1418
|
+
const abs = path.resolve(filePath);
|
|
1419
|
+
if (isJsOrTs(abs)) {
|
|
1420
|
+
// JS/TS support: load default export via robust pipeline.
|
|
1421
|
+
const mod = await loadJsTsDefault(abs);
|
|
1422
|
+
raw = mod ?? {};
|
|
1423
|
+
}
|
|
1424
|
+
else {
|
|
1425
|
+
const txt = await fs.readFile(abs, 'utf-8');
|
|
1426
|
+
raw = isJson(abs) ? JSON.parse(txt) : isYaml(abs) ? YAML.parse(txt) : {};
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
catch (err) {
|
|
1430
|
+
throw new Error(`Failed to read/parse config: ${filePath}. ${String(err)}`);
|
|
1431
|
+
}
|
|
1432
|
+
// Validate RAW
|
|
1433
|
+
const parsed = getDotenvConfigSchemaRaw.safeParse(raw);
|
|
1434
|
+
if (!parsed.success) {
|
|
1435
|
+
const msgs = parsed.error.issues
|
|
1436
|
+
.map((i) => `${i.path.join('.')}: ${i.message}`)
|
|
1437
|
+
.join('\n');
|
|
1438
|
+
throw new Error(`Invalid config ${filePath}:\n${msgs}`);
|
|
1439
|
+
}
|
|
1440
|
+
// Disallow dynamic in JSON/YAML; allow in JS/TS
|
|
1441
|
+
if (!isJsOrTs(filePath) && parsed.data.dynamic !== undefined) {
|
|
1442
|
+
throw new Error(`Config ${filePath} specifies "dynamic"; JSON/YAML configs cannot include dynamic in this step. Use JS/TS config.`);
|
|
1443
|
+
}
|
|
1444
|
+
return getDotenvConfigSchemaResolved.parse(parsed.data);
|
|
1445
|
+
};
|
|
1446
|
+
/**
|
|
1447
|
+
* Discover and load configs into resolved shapes, ordered by scope/privacy.
|
|
1448
|
+
* JSON/YAML/JS/TS supported; first match per scope/privacy applies.
|
|
852
1449
|
*/
|
|
853
|
-
const
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
1450
|
+
const resolveGetDotenvConfigSources = async (importMetaUrl) => {
|
|
1451
|
+
const discovered = await discoverConfigFiles(importMetaUrl);
|
|
1452
|
+
const result = {};
|
|
1453
|
+
for (const f of discovered) {
|
|
1454
|
+
const cfg = await loadConfigFile(f.path);
|
|
1455
|
+
if (f.scope === 'packaged') {
|
|
1456
|
+
// packaged public only
|
|
1457
|
+
result.packaged = cfg;
|
|
1458
|
+
}
|
|
1459
|
+
else {
|
|
1460
|
+
result.project ??= {};
|
|
1461
|
+
if (f.privacy === 'public')
|
|
1462
|
+
result.project.public = cfg;
|
|
1463
|
+
else
|
|
1464
|
+
result.project.local = cfg;
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
return result;
|
|
1468
|
+
};
|
|
1469
|
+
|
|
860
1470
|
/**
|
|
861
|
-
*
|
|
862
|
-
*
|
|
1471
|
+
* Resolve dotenv values using the config-loader/overlay path (always-on in
|
|
1472
|
+
* host/generator flows; no-op when no config files are present).
|
|
863
1473
|
*
|
|
864
|
-
*
|
|
865
|
-
*
|
|
866
|
-
*
|
|
867
|
-
*
|
|
868
|
-
*
|
|
869
|
-
*
|
|
870
|
-
* Writes through a local `Record<string, unknown>` view to avoid requiring an index signature on {@link T}.
|
|
1474
|
+
* Order:
|
|
1475
|
+
* 1) Compute base from files only (exclude dynamic; ignore programmatic vars).
|
|
1476
|
+
* 2) Discover packaged + project config sources and overlay onto base.
|
|
1477
|
+
* 3) Apply dynamics in order:
|
|
1478
|
+
* programmatic dynamic \> config dynamic (packaged → project public → project local)
|
|
1479
|
+
* \> file dynamicPath. * 4) Optionally write outputPath, log, and merge into process.env.
|
|
871
1480
|
*/
|
|
872
|
-
const
|
|
873
|
-
|
|
874
|
-
const
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
1481
|
+
const resolveDotenvWithConfigLoader = async (validated) => {
|
|
1482
|
+
// 1) Base from files, no dynamic, no programmatic vars
|
|
1483
|
+
const base = await getDotenv({
|
|
1484
|
+
...validated,
|
|
1485
|
+
// Build a pure base without side effects or logging.
|
|
1486
|
+
excludeDynamic: true,
|
|
1487
|
+
vars: {},
|
|
1488
|
+
log: false,
|
|
1489
|
+
loadProcess: false,
|
|
1490
|
+
outputPath: undefined,
|
|
1491
|
+
});
|
|
1492
|
+
// 2) Discover config sources (packaged via this module's import.meta.url)
|
|
1493
|
+
const sources = await resolveGetDotenvConfigSources((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)));
|
|
1494
|
+
const dotenv = overlayEnv({
|
|
1495
|
+
base,
|
|
1496
|
+
env: validated.env ?? validated.defaultEnv,
|
|
1497
|
+
configs: sources,
|
|
1498
|
+
...(validated.vars ? { programmaticVars: validated.vars } : {}),
|
|
1499
|
+
});
|
|
1500
|
+
// Helper to apply a dynamic map progressively.
|
|
1501
|
+
const applyDynamic = (target, dynamic, env) => {
|
|
1502
|
+
if (!dynamic)
|
|
1503
|
+
return;
|
|
1504
|
+
for (const key of Object.keys(dynamic)) {
|
|
1505
|
+
const value = typeof dynamic[key] === 'function'
|
|
1506
|
+
? dynamic[key](target, env)
|
|
1507
|
+
: dynamic[key];
|
|
1508
|
+
Object.assign(target, { [key]: value });
|
|
1509
|
+
}
|
|
1510
|
+
};
|
|
1511
|
+
// 3) Apply dynamics in order
|
|
1512
|
+
applyDynamic(dotenv, validated.dynamic, validated.env ?? validated.defaultEnv);
|
|
1513
|
+
applyDynamic(dotenv, (sources.packaged?.dynamic ?? undefined), validated.env ?? validated.defaultEnv);
|
|
1514
|
+
applyDynamic(dotenv, (sources.project?.public?.dynamic ?? undefined), validated.env ?? validated.defaultEnv);
|
|
1515
|
+
applyDynamic(dotenv, (sources.project?.local?.dynamic ?? undefined), validated.env ?? validated.defaultEnv);
|
|
1516
|
+
// file dynamicPath (lowest)
|
|
1517
|
+
if (validated.dynamicPath) {
|
|
1518
|
+
const absDynamicPath = path.resolve(validated.dynamicPath);
|
|
1519
|
+
try {
|
|
1520
|
+
const dyn = await loadModuleDefault(absDynamicPath, 'getdotenv-dynamic-host');
|
|
1521
|
+
applyDynamic(dotenv, dyn, validated.env ?? validated.defaultEnv);
|
|
1522
|
+
}
|
|
1523
|
+
catch {
|
|
1524
|
+
throw new Error(`Unable to load dynamic from ${validated.dynamicPath}`);
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
// 4) Output/log/process merge
|
|
1528
|
+
if (validated.outputPath) {
|
|
1529
|
+
await fs.writeFile(validated.outputPath, Object.keys(dotenv).reduce((contents, key) => {
|
|
1530
|
+
const value = dotenv[key] ?? '';
|
|
1531
|
+
return `${contents}${key}=${value.includes('\n') ? `"${value}"` : value}\n`;
|
|
1532
|
+
}, ''), { encoding: 'utf-8' });
|
|
1533
|
+
}
|
|
1534
|
+
const logger = validated.logger ?? console;
|
|
1535
|
+
if (validated.log)
|
|
1536
|
+
logger.log(dotenv);
|
|
1537
|
+
if (validated.loadProcess)
|
|
1538
|
+
Object.assign(process.env, dotenv);
|
|
1539
|
+
return dotenv;
|
|
880
1540
|
};
|
|
881
1541
|
|
|
1542
|
+
/**
|
|
1543
|
+
* Omit a "logger" key from an options object in a typed manner.
|
|
1544
|
+
*/
|
|
1545
|
+
const omitLogger = (obj) => {
|
|
1546
|
+
const { logger: _omitted, ...rest } = obj;
|
|
1547
|
+
return rest;
|
|
1548
|
+
};
|
|
882
1549
|
/**
|
|
883
1550
|
* Build the Commander preSubcommand hook using the provided context.
|
|
884
|
-
*
|
|
885
|
-
*
|
|
886
|
-
* - Merge parent CLI options with current invocation (parent \< current).
|
|
887
|
-
* - Resolve tri-state flags, including `--exclude-all` overrides.
|
|
1551
|
+
* * Responsibilities:
|
|
1552
|
+
* - Merge parent CLI options with current invocation (parent \< current). * - Resolve tri-state flags, including `--exclude-all` overrides.
|
|
888
1553
|
* - Normalize the shell setting to a concrete value (string | boolean).
|
|
889
1554
|
* - Persist merged options on the command instance and pass to subcommands.
|
|
890
1555
|
* - Execute {@link getDotenv} and optional post-hook.
|
|
@@ -894,95 +1559,52 @@ const setOptionalFlag = (obj, key, value) => {
|
|
|
894
1559
|
* @returns An async hook suitable for Commander’s `preSubcommand`.
|
|
895
1560
|
*
|
|
896
1561
|
* @example `program.hook('preSubcommand', makePreSubcommandHook(ctx));`
|
|
897
|
-
*/
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
if (mergedGetDotenvCliOptions.debug)
|
|
939
|
-
logger.debug('\n*** merged GetDotenvCliOptions ***\n', {
|
|
940
|
-
mergedGetDotenvCliOptions,
|
|
941
|
-
});
|
|
942
|
-
// Execute pre-hook.
|
|
943
|
-
if (preHook) {
|
|
944
|
-
await preHook(mergedGetDotenvCliOptions);
|
|
945
|
-
if (mergedGetDotenvCliOptions.debug)
|
|
946
|
-
logger.debug('\n*** GetDotenvCliOptions after pre-hook ***\n', mergedGetDotenvCliOptions);
|
|
947
|
-
}
|
|
948
|
-
// Persist GetDotenvCliOptions in command for subcommand access.
|
|
949
|
-
thisCommand.getDotenvCliOptions = mergedGetDotenvCliOptions;
|
|
950
|
-
// Execute getdotenv.
|
|
951
|
-
const dotenv = await getDotenv(getDotenvCliOptions2Options(mergedGetDotenvCliOptions));
|
|
952
|
-
if (mergedGetDotenvCliOptions.debug)
|
|
953
|
-
logger.debug('\n*** getDotenv output ***\n', dotenv);
|
|
954
|
-
// Execute post-hook.
|
|
955
|
-
if (postHook)
|
|
956
|
-
await postHook(dotenv);
|
|
957
|
-
// Execute command.
|
|
958
|
-
const args = thisCommand.args ?? [];
|
|
959
|
-
const isCommand = typeof command === 'string' && command.length > 0;
|
|
960
|
-
if (isCommand && args.length > 0) {
|
|
961
|
-
const lr = logger;
|
|
962
|
-
(lr.error ?? lr.log)(`--command option conflicts with cmd subcommand.`);
|
|
963
|
-
process.exit(0);
|
|
964
|
-
}
|
|
965
|
-
if (typeof command === 'string' && command.length > 0) {
|
|
966
|
-
const cmd = resolveCommand(mergedGetDotenvCliOptions.scripts, command);
|
|
967
|
-
if (mergedGetDotenvCliOptions.debug)
|
|
968
|
-
logger.debug('\n*** command ***\n', cmd);
|
|
969
|
-
const envSafe = {
|
|
970
|
-
...mergedGetDotenvCliOptions,
|
|
971
|
-
};
|
|
972
|
-
delete envSafe.logger;
|
|
973
|
-
await execa.execaCommand(cmd, {
|
|
974
|
-
env: {
|
|
975
|
-
...process.env,
|
|
976
|
-
getDotenvCliOptions: JSON.stringify(envSafe),
|
|
977
|
-
},
|
|
978
|
-
shell: resolveShell(mergedGetDotenvCliOptions.scripts, command, mergedGetDotenvCliOptions.shell),
|
|
979
|
-
stdio: 'inherit',
|
|
980
|
-
});
|
|
981
|
-
}
|
|
1562
|
+
*/
|
|
1563
|
+
const makePreSubcommandHook = ({ logger, preHook, postHook, defaults, }) => {
|
|
1564
|
+
return async (thisCommand) => {
|
|
1565
|
+
// Get raw CLI options from commander.
|
|
1566
|
+
const rawCliOptions = thisCommand.opts();
|
|
1567
|
+
const { merged: mergedGetDotenvCliOptions, command: commandOpt } = resolveCliOptions(rawCliOptions, defaults, process.env.getDotenvCliOptions);
|
|
1568
|
+
// Optional debug logging retained via mergedGetDotenvCliOptions.debug if desired. // Execute pre-hook.
|
|
1569
|
+
if (preHook) {
|
|
1570
|
+
await preHook(mergedGetDotenvCliOptions);
|
|
1571
|
+
if (mergedGetDotenvCliOptions.debug)
|
|
1572
|
+
logger.debug('\n*** GetDotenvCliOptions after pre-hook ***\n', mergedGetDotenvCliOptions);
|
|
1573
|
+
}
|
|
1574
|
+
// Persist GetDotenvCliOptions in command for subcommand access.
|
|
1575
|
+
thisCommand.getDotenvCliOptions =
|
|
1576
|
+
mergedGetDotenvCliOptions;
|
|
1577
|
+
// Execute getdotenv via always-on config loader/overlay path.
|
|
1578
|
+
const serviceOptions = getDotenvCliOptions2Options(mergedGetDotenvCliOptions);
|
|
1579
|
+
const dotenv = await resolveDotenvWithConfigLoader(serviceOptions);
|
|
1580
|
+
// Execute post-hook.
|
|
1581
|
+
if (postHook)
|
|
1582
|
+
await postHook(dotenv); // Execute command.
|
|
1583
|
+
const args = thisCommand.args ?? [];
|
|
1584
|
+
const isCommand = typeof commandOpt === 'string' && commandOpt.length > 0;
|
|
1585
|
+
if (isCommand && args.length > 0) {
|
|
1586
|
+
const lr = logger;
|
|
1587
|
+
(lr.error ?? lr.log)(`--command option conflicts with cmd subcommand.`);
|
|
1588
|
+
process.exit(0);
|
|
1589
|
+
}
|
|
1590
|
+
if (typeof commandOpt === 'string' && commandOpt.length > 0) {
|
|
1591
|
+
const cmd = resolveCommand(mergedGetDotenvCliOptions.scripts, commandOpt);
|
|
1592
|
+
if (mergedGetDotenvCliOptions.debug)
|
|
1593
|
+
logger.debug('\n*** command ***\n', cmd);
|
|
1594
|
+
// Build a logger-free bag for env round-trip.
|
|
1595
|
+
const envSafe = omitLogger(mergedGetDotenvCliOptions);
|
|
1596
|
+
await execa.execaCommand(cmd, {
|
|
1597
|
+
env: { ...process.env, getDotenvCliOptions: JSON.stringify(envSafe) },
|
|
1598
|
+
shell: resolveShell(mergedGetDotenvCliOptions.scripts, commandOpt, mergedGetDotenvCliOptions.shell),
|
|
1599
|
+
stdio: 'inherit',
|
|
1600
|
+
});
|
|
1601
|
+
}
|
|
1602
|
+
};
|
|
982
1603
|
};
|
|
983
1604
|
|
|
984
1605
|
/**
|
|
985
|
-
* Generate a Commander CLI Command for get-dotenv.
|
|
1606
|
+
* Generate a Commander CLI Command for get-dotenv.
|
|
1607
|
+
* Orchestration only: delegates building and lifecycle hooks.
|
|
986
1608
|
*/
|
|
987
1609
|
const generateGetDotenvCli = async (customOptions) => {
|
|
988
1610
|
const options = await resolveGetDotenvCliGenerateOptions(customOptions);
|