@karmaniverous/get-dotenv 6.2.4 → 6.4.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.
Files changed (49) hide show
  1. package/README.md +1 -0
  2. package/dist/chunks/{AwsRestJsonProtocol-Bq1HE-Ln.mjs → AwsRestJsonProtocol-fYZqn-kW.mjs} +2 -2
  3. package/dist/chunks/{createCli-BY6_cfZr.mjs → createCli-BnRdfRRL.mjs} +7 -6
  4. package/dist/chunks/{externalDataInterceptor-CbsdEYa-.mjs → externalDataInterceptor-CILOLqbB.mjs} +2 -2
  5. package/dist/chunks/{getSSOTokenFromFile-hUSpR7Wf.mjs → getSSOTokenFromFile-BwMkZ_yT.mjs} +1 -1
  6. package/dist/chunks/{index-C_wqbTwI.mjs → index-0-nP97ri.mjs} +7 -6
  7. package/dist/chunks/{index-CeCufHlm.mjs → index-70Dm0f1N.mjs} +11 -10
  8. package/dist/chunks/{index-BPYF6K_G.mjs → index-BhVOypA1.mjs} +9 -8
  9. package/dist/chunks/{index-cIunyiUQ.mjs → index-CcwT4HJK.mjs} +6 -5
  10. package/dist/chunks/{index-BpCF5UKx.mjs → index-D8UL3w94.mjs} +6 -5
  11. package/dist/chunks/{index-Cu7rdyqN.mjs → index-DLNhHC15.mjs} +9 -8
  12. package/dist/chunks/{index-Dp1Ip6Ra.mjs → index-DWqbxY8Y.mjs} +11 -10
  13. package/dist/chunks/{index-c7zKtEuy.mjs → index-Du51s-Z0.mjs} +8 -7
  14. package/dist/chunks/{index-B5JKTBOL.mjs → index-DuSz0ul6.mjs} +8 -7
  15. package/dist/chunks/{index-DWAtHEA-.mjs → index-OeNCYa8T.mjs} +6 -5
  16. package/dist/chunks/{index-DyU5pKKi.mjs → index-_FP0whjC.mjs} +6 -5
  17. package/dist/chunks/{index-BEJFiHMX.mjs → index-o5zJ9PWL.mjs} +15 -15
  18. package/dist/chunks/{index-Bc3h0a95.mjs → index-r0Me7-sT.mjs} +112 -6
  19. package/dist/chunks/{loadSso-w1eTVg0O.mjs → loadSso-CLR1fKci.mjs} +8 -7
  20. package/dist/chunks/{loader-DnhPeGfq.mjs → loader-CePOf74i.mjs} +1 -0
  21. package/dist/chunks/{parseKnownFiles-B9cDK21V.mjs → parseKnownFiles-BQvmJ0HK.mjs} +1 -1
  22. package/dist/chunks/readDotenvCascade-DfFkWMjs.mjs +546 -0
  23. package/dist/chunks/{readMergedOptions-Nt0TR7dX.mjs → readMergedOptions-B7VdLROn.mjs} +62 -272
  24. package/dist/chunks/{resolveCliOptions-TFRzhB2c.mjs → resolveCliOptions-pgUXHJtj.mjs} +2 -1
  25. package/dist/chunks/{sdk-stream-mixin-BZoJ5jy9.mjs → sdk-stream-mixin-ecbbBR0l.mjs} +1 -1
  26. package/dist/chunks/{spawnEnv-CN8a7cNR.mjs → spawnEnv-CQwFu7ZJ.mjs} +2 -1
  27. package/dist/chunks/{types-DJ-BGABd.mjs → types-CVDR-Sjk.mjs} +1 -1
  28. package/dist/cli.d.ts +218 -84
  29. package/dist/cli.mjs +10 -10
  30. package/dist/cliHost.d.ts +218 -84
  31. package/dist/cliHost.mjs +8 -7
  32. package/dist/config.mjs +2 -1
  33. package/dist/env-overlay.d.ts +304 -2
  34. package/dist/env-overlay.mjs +38 -1
  35. package/dist/getdotenv.cli.mjs +10 -10
  36. package/dist/index.d.ts +703 -86
  37. package/dist/index.mjs +862 -13
  38. package/dist/plugins-aws.d.ts +153 -19
  39. package/dist/plugins-aws.mjs +5 -4
  40. package/dist/plugins-batch.d.ts +153 -19
  41. package/dist/plugins-batch.mjs +5 -4
  42. package/dist/plugins-cmd.d.ts +153 -19
  43. package/dist/plugins-cmd.mjs +7 -6
  44. package/dist/plugins-init.d.ts +153 -19
  45. package/dist/plugins-init.mjs +4 -4
  46. package/dist/plugins.d.ts +153 -19
  47. package/dist/plugins.mjs +9 -9
  48. package/package.json +1 -1
  49. package/dist/chunks/overlayEnv-Bs2kVayG.mjs +0 -234
package/dist/index.mjs CHANGED
@@ -1,27 +1,31 @@
1
- export { c as createCli } from './chunks/createCli-BY6_cfZr.mjs';
2
- export { G as GetDotenvCli, e as baseRootOptionDefaults, a as defineDynamic, b as defineGetDotenvConfig, d as definePlugin, g as getDotenv, c as getDotenvCliOptions2Options, i as interpolateDeep, m as maybeWarnEntropy, r as readMergedOptions, f as redactDisplay, h as redactObject } from './chunks/readMergedOptions-Nt0TR7dX.mjs';
3
- export { b as buildSpawnEnv, s as shouldCapture } from './chunks/spawnEnv-CN8a7cNR.mjs';
4
- export { d as defineScripts, g as groupPlugins } from './chunks/types-DJ-BGABd.mjs';
5
- import 'fs-extra';
1
+ export { c as createCli } from './chunks/createCli-BnRdfRRL.mjs';
2
+ import { e as resolveGetDotenvOptions, w as writeDotenvFile } from './chunks/readMergedOptions-B7VdLROn.mjs';
3
+ export { G as GetDotenvCli, b as baseRootOptionDefaults, f as defineDynamic, h as defineGetDotenvConfig, d as definePlugin, g as getDotenvCliOptions2Options, i as interpolateDeep, r as readMergedOptions } from './chunks/readMergedOptions-B7VdLROn.mjs';
4
+ export { d as buildSpawnEnv, s as shouldCapture } from './chunks/spawnEnv-CQwFu7ZJ.mjs';
5
+ export { d as defineScripts, g as groupPlugins } from './chunks/types-CVDR-Sjk.mjs';
6
+ import fs from 'fs-extra';
6
7
  import 'crypto';
7
- import 'path';
8
+ import path$1 from 'path';
8
9
  import 'url';
9
10
  export { z } from 'zod';
11
+ import { nanoid } from 'nanoid';
12
+ import { r as redactObject, m as maybeWarnEntropy } from './chunks/index-r0Me7-sT.mjs';
13
+ export { a as redactDisplay, t as traceChildEnv } from './chunks/index-r0Me7-sT.mjs';
14
+ import { f as readDotenv, d as dotenvExpandAll, a as applyDynamicMap, c as loadAndApplyDynamic } from './chunks/readDotenvCascade-DfFkWMjs.mjs';
15
+ export { g as dotenvExpand, h as dotenvExpandFromProcessEnv } from './chunks/readDotenvCascade-DfFkWMjs.mjs';
16
+ import path from 'node:path';
10
17
  import 'dotenv';
11
- export { t as traceChildEnv } from './chunks/index-Bc3h0a95.mjs';
12
- export { d as dotenvExpand, a as dotenvExpandAll, b as dotenvExpandFromProcessEnv } from './chunks/overlayEnv-Bs2kVayG.mjs';
13
- import './chunks/loader-DnhPeGfq.mjs';
18
+ import './chunks/loader-CePOf74i.mjs';
14
19
  import 'package-directory';
15
20
  import 'yaml';
16
21
  import './chunks/loadModuleDefault-Dj8B3Stt.mjs';
17
- import 'nanoid';
18
22
  import 'execa';
19
23
  import './chunks/helpConfig-CGejgwWW.mjs';
20
- import './chunks/resolveCliOptions-TFRzhB2c.mjs';
24
+ import './chunks/resolveCliOptions-pgUXHJtj.mjs';
21
25
  import './chunks/validate-CDl0rE6k.mjs';
22
26
  import './plugins-aws.mjs';
23
27
  import '@commander-js/extra-typings';
24
- import './chunks/index-CeCufHlm.mjs';
28
+ import './chunks/index-70Dm0f1N.mjs';
25
29
  import 'buffer';
26
30
  import 'os';
27
31
  import 'node:fs/promises';
@@ -36,5 +40,850 @@ import 'globby';
36
40
  import './plugins-init.mjs';
37
41
  import 'node:process';
38
42
  import 'readline/promises';
39
- import 'node:path';
40
43
  import 'node:url';
44
+
45
+ /**
46
+ * Dotenv editor types (format-preserving).
47
+ *
48
+ * Requirements addressed:
49
+ * - Provide a format-preserving dotenv edit surface (pure text pipeline + FS adapter).
50
+ * - Preserve comments/blank lines/unknown lines and separator spacing; support merge vs sync.
51
+ * - Deterministic target selection across getdotenv `paths` with optional template bootstrap.
52
+ *
53
+ * Notes:
54
+ * - The parser intentionally preserves unknown lines verbatim rather than rejecting them.
55
+ * - The editor focuses on `.env`-style KEY=VALUE lines; anything not recognized is preserved as raw text.
56
+ *
57
+ * @packageDocumentation
58
+ */
59
+ /**
60
+ * Narrow helper for internal use: detect own properties safely.
61
+ *
62
+ * @internal
63
+ */
64
+ const hasOwn = (obj, key) => Object.prototype.hasOwnProperty.call(obj, key);
65
+
66
+ /**
67
+ * Apply edits to a parsed dotenv document while preserving formatting.
68
+ *
69
+ * Requirements addressed:
70
+ * - Mode: merge vs sync.
71
+ * - Duplicate key strategy: all/first/last.
72
+ * - undefined behavior: skip (default).
73
+ * - null behavior: delete (default).
74
+ * - Quote preservation where safe; upgrade quoting when required (multiline, whitespace safety, inline comment safety).
75
+ *
76
+ * @packageDocumentation
77
+ */
78
+ const coerceToString = (v) => {
79
+ if (typeof v === 'string')
80
+ return v;
81
+ if (typeof v === 'number')
82
+ return String(v);
83
+ if (typeof v === 'boolean')
84
+ return v ? 'true' : 'false';
85
+ if (v === null || v === undefined)
86
+ return '';
87
+ return JSON.stringify(v);
88
+ };
89
+ const needsQuoted = (value, hasInlineSuffix) => {
90
+ if (value.includes('\n'))
91
+ return true;
92
+ if (value.includes('\r'))
93
+ return true;
94
+ // Preserve correctness: leading/trailing whitespace should be quoted.
95
+ if (/^\s|\s$/.test(value))
96
+ return true;
97
+ // Inline comment safety: if there is a suffix (comment) and the value contains '#', quote it.
98
+ if (hasInlineSuffix && value.includes('#'))
99
+ return true;
100
+ return false;
101
+ };
102
+ const escapeDoubleQuoted = (value) =>
103
+ // Minimal escaping: escape only `"`. (Do not force-escape backslashes.)
104
+ value.replace(/"/g, '\\"');
105
+ const toFileEol = (valueLf, fileEol) => fileEol === '\n' ? valueLf : valueLf.replace(/\n/g, '\r\n');
106
+ const renderValueToken = (args) => {
107
+ const { value, preferQuote, hasInlineSuffix, fileEol } = args;
108
+ const multiline = value.includes('\n');
109
+ if (multiline) {
110
+ // Multiline correctness: use double quotes.
111
+ return {
112
+ token: `"${toFileEol(escapeDoubleQuoted(value), fileEol)}"`,
113
+ quote: '"',
114
+ };
115
+ }
116
+ const mustQuote = needsQuoted(value, hasInlineSuffix);
117
+ if (!mustQuote && preferQuote === null)
118
+ return { token: value, quote: null };
119
+ // Try to preserve original quote style when safe.
120
+ if (preferQuote === "'" && !value.includes("'") && !mustQuote) {
121
+ return { token: `'${value}'`, quote: "'" };
122
+ }
123
+ if (preferQuote === "'" && !value.includes("'")) {
124
+ return { token: `'${value}'`, quote: "'" };
125
+ }
126
+ // Prefer double quotes as the general safe fallback.
127
+ return { token: `"${escapeDoubleQuoted(value)}"`, quote: '"' };
128
+ };
129
+ const pickIndexes = (idxs, strategy) => {
130
+ if (idxs.length === 0)
131
+ return [];
132
+ if (strategy === 'first') {
133
+ const first = idxs[0];
134
+ return first === undefined ? [] : [first];
135
+ }
136
+ if (strategy === 'last') {
137
+ const last = idxs[idxs.length - 1];
138
+ return last === undefined ? [] : [last];
139
+ }
140
+ return idxs.slice();
141
+ };
142
+ const segmentEndEol = (seg) => {
143
+ if ('eol' in seg)
144
+ return seg.eol;
145
+ // Raw segments store EOL inside raw; best-effort detect tail.
146
+ const raw = seg.raw;
147
+ if (raw.endsWith('\r\n'))
148
+ return '\r\n';
149
+ if (raw.endsWith('\n'))
150
+ return '\n';
151
+ return '';
152
+ };
153
+ const rebuildAssignmentRaw = (args) => {
154
+ const { seg, valueLf, fileEol } = args;
155
+ const { token, quote } = renderValueToken({
156
+ value: valueLf,
157
+ preferQuote: seg.quote,
158
+ hasInlineSuffix: seg.suffix.length > 0,
159
+ fileEol,
160
+ });
161
+ const line = `${seg.prefix}${seg.key}${seg.separator}${seg.valuePadding}${token}${seg.suffix}`;
162
+ const eol = segmentEndEol(seg);
163
+ const raw = eol ? line + eol : line;
164
+ return { ...seg, raw, value: valueLf, quote, eol };
165
+ };
166
+ const rebuildBareAsAssignment = (args) => {
167
+ const { seg, valueLf, fileEol, defaultSeparator } = args;
168
+ const { token, quote } = renderValueToken({
169
+ value: valueLf,
170
+ preferQuote: null,
171
+ hasInlineSuffix: seg.suffix.length > 0,
172
+ fileEol,
173
+ });
174
+ const separator = defaultSeparator;
175
+ const line = `${seg.prefix}${seg.key}${separator}${token}${seg.suffix}`;
176
+ const eol = segmentEndEol(seg);
177
+ const raw = eol ? line + eol : line;
178
+ return {
179
+ kind: 'assignment',
180
+ raw,
181
+ key: seg.key,
182
+ prefix: seg.prefix,
183
+ separator,
184
+ valuePadding: '',
185
+ quote,
186
+ value: valueLf,
187
+ suffix: seg.suffix,
188
+ eol,
189
+ };
190
+ };
191
+ const buildNewAssignmentAtEnd = (args) => {
192
+ const { key, valueLf, fileEol, defaultSeparator, endEol } = args;
193
+ const { token, quote } = renderValueToken({
194
+ value: valueLf,
195
+ preferQuote: null,
196
+ hasInlineSuffix: false,
197
+ fileEol,
198
+ });
199
+ const line = `${key}${defaultSeparator}${token}`;
200
+ const raw = endEol ? line + endEol : line;
201
+ return {
202
+ kind: 'assignment',
203
+ raw,
204
+ key,
205
+ prefix: '',
206
+ separator: defaultSeparator,
207
+ valuePadding: '',
208
+ quote,
209
+ value: valueLf,
210
+ suffix: '',
211
+ eol: endEol,
212
+ };
213
+ };
214
+ /**
215
+ * Apply a set of key/value updates to a parsed dotenv document.
216
+ *
217
+ * @param doc - Parsed dotenv document.
218
+ * @param updates - Key/value update map.
219
+ * @param opts - Editing options.
220
+ * @returns A new {@link DotenvDocument} with edits applied.
221
+ *
222
+ * @public
223
+ */
224
+ function applyDotenvEdits(doc, updates, opts) {
225
+ const mode = opts?.mode ?? 'merge';
226
+ const duplicateKeys = opts?.duplicateKeys ?? 'all';
227
+ const defaultSeparator = opts?.defaultSeparator ?? '=';
228
+ const segs = doc.segments.slice();
229
+ // Index key-bearing segments.
230
+ const byKey = new Map();
231
+ for (let i = 0; i < segs.length; i++) {
232
+ const s = segs[i];
233
+ if (!s)
234
+ continue;
235
+ if (s.kind === 'assignment' || s.kind === 'bare') {
236
+ const list = byKey.get(s.key) ?? [];
237
+ list.push(i);
238
+ byKey.set(s.key, list);
239
+ }
240
+ }
241
+ const deleteIdx = new Set();
242
+ const replace = new Map();
243
+ // Sync deletions: remove any existing key lines not present in update map (own props).
244
+ if (mode === 'sync') {
245
+ for (const [key, idxs] of byKey.entries()) {
246
+ if (!hasOwn(updates, key)) {
247
+ for (const i of idxs)
248
+ deleteIdx.add(i);
249
+ }
250
+ }
251
+ }
252
+ // Apply update-driven deletes and replacements.
253
+ for (const key of Object.keys(updates)) {
254
+ const v = updates[key];
255
+ const idxsAll = byKey.get(key) ?? [];
256
+ if (v === null) {
257
+ for (const i of pickIndexes(idxsAll, duplicateKeys))
258
+ deleteIdx.add(i);
259
+ continue;
260
+ }
261
+ if (v === undefined) {
262
+ // Note: in sync mode, this key counts as present (do not delete); replacements are skipped.
263
+ continue;
264
+ }
265
+ const valueLf = coerceToString(v);
266
+ for (const i of pickIndexes(idxsAll, duplicateKeys)) {
267
+ const seg = segs[i];
268
+ if (!seg)
269
+ continue;
270
+ if (seg.kind === 'assignment') {
271
+ replace.set(i, rebuildAssignmentRaw({ seg, valueLf, fileEol: doc.fileEol }));
272
+ }
273
+ else if (seg.kind === 'bare') {
274
+ replace.set(i, rebuildBareAsAssignment({
275
+ seg,
276
+ valueLf,
277
+ fileEol: doc.fileEol,
278
+ defaultSeparator,
279
+ }));
280
+ }
281
+ }
282
+ }
283
+ // Rebuild the segment list, applying deletions and replacements.
284
+ const out = [];
285
+ for (let i = 0; i < segs.length; i++) {
286
+ if (deleteIdx.has(i))
287
+ continue;
288
+ const repl = replace.get(i);
289
+ const original = segs[i];
290
+ if (repl)
291
+ out.push(repl);
292
+ else if (original)
293
+ out.push(original);
294
+ }
295
+ // Merge mode: append missing keys (only when provided with a concrete value).
296
+ if (mode === 'merge') {
297
+ const existingKeys = new Set();
298
+ for (const s of out) {
299
+ if (s.kind === 'assignment' || s.kind === 'bare')
300
+ existingKeys.add(s.key);
301
+ }
302
+ // Use the document EOL when inserting new lines; final newline presence is handled by render().
303
+ const endEol = doc.fileEol;
304
+ for (const key of Object.keys(updates)) {
305
+ if (existingKeys.has(key))
306
+ continue;
307
+ const v = updates[key];
308
+ if (v === undefined)
309
+ continue;
310
+ if (v === null)
311
+ continue;
312
+ const valueLf = coerceToString(v);
313
+ out.push(buildNewAssignmentAtEnd({
314
+ key,
315
+ valueLf,
316
+ fileEol: doc.fileEol,
317
+ defaultSeparator,
318
+ endEol,
319
+ }));
320
+ }
321
+ }
322
+ return { ...doc, segments: out };
323
+ }
324
+
325
+ /**
326
+ * Parse a dotenv file into a format-preserving document model.
327
+ *
328
+ * Requirements addressed:
329
+ * - Preserve comments, blank lines, ordering, and unknown lines verbatim.
330
+ * - Preserve separator spacing around `=`.
331
+ * - Support multiline quoted values (double/single quotes) by grouping physical lines.
332
+ *
333
+ * @packageDocumentation
334
+ */
335
+ const detectFileEol = (txt) => txt.includes('\r\n') ? '\r\n' : '\n';
336
+ const endsWithNewline$1 = (txt) => txt.endsWith('\n') || txt.endsWith('\r\n');
337
+ const splitLinesWithEol = (txt) => {
338
+ const out = [];
339
+ let start = 0;
340
+ for (let i = 0; i < txt.length; i++) {
341
+ const ch = txt.charAt(i);
342
+ if (ch !== '\n')
343
+ continue;
344
+ const isCrlf = i > 0 && txt.charAt(i - 1) === '\r';
345
+ const line = txt.slice(start, isCrlf ? i - 1 : i);
346
+ out.push({ line, eol: isCrlf ? '\r\n' : '\n' });
347
+ start = i + 1;
348
+ }
349
+ if (start < txt.length) {
350
+ out.push({ line: txt.slice(start), eol: '' });
351
+ }
352
+ else if (txt.length === 0) {
353
+ out.push({ line: '', eol: '' });
354
+ }
355
+ return out;
356
+ };
357
+ const normalizeInnerNewlinesToLf = (v) => v.replace(/\r\n/g, '\n');
358
+ const findFirstNonWhitespace = (s) => {
359
+ for (let i = 0; i < s.length; i++) {
360
+ if (!/\s/.test(s.charAt(i)))
361
+ return i;
362
+ }
363
+ return -1;
364
+ };
365
+ const splitInlineCommentUnquoted = (rhs) => {
366
+ // Match a `#` that is preceded by whitespace; keep all whitespace before `#` with the suffix.
367
+ for (let i = 0; i < rhs.length; i++) {
368
+ const ch = rhs.charAt(i);
369
+ if (ch !== '#')
370
+ continue;
371
+ if (i === 0)
372
+ return { value: '', suffix: rhs };
373
+ const prev = rhs.charAt(i - 1);
374
+ if (!/\s/.test(prev))
375
+ continue;
376
+ let j = i;
377
+ while (j > 0 && /\s/.test(rhs.charAt(j - 1)))
378
+ j--;
379
+ return { value: rhs.slice(0, j), suffix: rhs.slice(j) };
380
+ }
381
+ return { value: rhs, suffix: '' };
382
+ };
383
+ const tryParseQuotedValueAcrossLines = (lines, startIndex, startRhs, quote) => {
384
+ // Parse the quoted value beginning at the first quote in startRhs.
385
+ // Returns null if a closing quote is never found (to avoid corrupting segmentation).
386
+ let idx = startIndex;
387
+ let rhs = startRhs;
388
+ const firstQuoteIdx = rhs.indexOf(quote);
389
+ if (firstQuoteIdx < 0)
390
+ return null;
391
+ // Value text begins after the opening quote.
392
+ let cursor = firstQuoteIdx + 1;
393
+ let escaped = false;
394
+ let valueOut = '';
395
+ while (idx < lines.length) {
396
+ for (; cursor < rhs.length; cursor++) {
397
+ const ch = rhs.charAt(cursor);
398
+ if (escaped) {
399
+ valueOut += ch;
400
+ escaped = false;
401
+ continue;
402
+ }
403
+ if (ch === '\\') {
404
+ // Preserve backslashes; caller may choose to re-escape on render.
405
+ valueOut += ch;
406
+ escaped = true;
407
+ continue;
408
+ }
409
+ if (ch === quote) {
410
+ // Closing quote: suffix is the remainder of this physical line after the closing quote.
411
+ return {
412
+ endIndex: idx,
413
+ value: normalizeInnerNewlinesToLf(valueOut),
414
+ suffix: rhs.slice(cursor + 1),
415
+ };
416
+ }
417
+ valueOut += ch;
418
+ }
419
+ // Move to next physical line; insert a newline into the value.
420
+ idx += 1;
421
+ if (idx >= lines.length)
422
+ break;
423
+ // If the file ended without a newline, we still treat the join as a newline inside the quoted value.
424
+ valueOut += '\n';
425
+ const next = lines[idx];
426
+ if (!next)
427
+ break;
428
+ rhs = next.line;
429
+ cursor = 0;
430
+ escaped = false;
431
+ }
432
+ return null;
433
+ };
434
+ const parseKeyPrefix = (line) => {
435
+ // Allow: indentation + optional "export " + KEY
436
+ // KEY must be a shell-ish identifier.
437
+ const m = /^(\s*(?:export\s+)?)([A-Za-z_][A-Za-z0-9_]*)(.*)$/.exec(line);
438
+ if (!m)
439
+ return null;
440
+ const prefix = m[1] ?? '';
441
+ const key = m[2] ?? '';
442
+ const rest = m[3] ?? '';
443
+ if (!key)
444
+ return null;
445
+ return { prefix, key, rest };
446
+ };
447
+ /**
448
+ * Parse dotenv text into a document model that preserves formatting.
449
+ *
450
+ * @param text - Dotenv file contents as UTF-8 text.
451
+ * @returns A parsed {@link DotenvDocument}.
452
+ *
453
+ * @public
454
+ */
455
+ function parseDotenvDocument(text) {
456
+ const fileEol = detectFileEol(text);
457
+ const trailingNewline = endsWithNewline$1(text);
458
+ const lines = splitLinesWithEol(text);
459
+ const segments = [];
460
+ for (let i = 0; i < lines.length; i++) {
461
+ const cur = lines[i];
462
+ if (!cur)
463
+ continue;
464
+ const line = cur.line;
465
+ const eol = cur.eol;
466
+ const parsed = parseKeyPrefix(line);
467
+ if (!parsed) {
468
+ const rawSeg = { kind: 'raw', raw: line + eol };
469
+ segments.push(rawSeg);
470
+ continue;
471
+ }
472
+ const { prefix, key, rest } = parsed;
473
+ const mAssign = /^(\s*=\s*)(.*)$/.exec(rest);
474
+ if (mAssign) {
475
+ const separator = mAssign[1] ?? '=';
476
+ const rhs = mAssign[2] ?? '';
477
+ // Preserve whitespace between separator and first token.
478
+ const nonWs = findFirstNonWhitespace(rhs);
479
+ const valuePadding = nonWs >= 0 ? rhs.slice(0, nonWs) : rhs;
480
+ const tokenStart = nonWs >= 0 ? rhs.charAt(nonWs) : '';
481
+ // Try quoted parsing first (single-line or multiline).
482
+ if (tokenStart === '"' || tokenStart === "'") {
483
+ const quote = tokenStart === '"' ? '"' : "'";
484
+ const parsedQuoted = tryParseQuotedValueAcrossLines(lines, i, rhs, quote);
485
+ if (parsedQuoted) {
486
+ const endIndex = parsedQuoted.endIndex;
487
+ const raw = lines
488
+ .slice(i, endIndex + 1)
489
+ .map((l) => l.line + l.eol)
490
+ .join('');
491
+ const endEol = lines[endIndex]?.eol ?? '';
492
+ const seg = {
493
+ kind: 'assignment',
494
+ raw,
495
+ key,
496
+ prefix,
497
+ separator,
498
+ valuePadding,
499
+ quote,
500
+ value: parsedQuoted.value,
501
+ suffix: parsedQuoted.suffix,
502
+ eol: endEol,
503
+ };
504
+ segments.push(seg);
505
+ i = endIndex;
506
+ continue;
507
+ }
508
+ // If quote is unclosed, preserve the line verbatim as raw.
509
+ // This avoids accidentally “parsing” malformed lines and losing formatting.
510
+ const rawSeg = { kind: 'raw', raw: line + eol };
511
+ segments.push(rawSeg);
512
+ continue;
513
+ }
514
+ // Unquoted single-line value: split inline comments.
515
+ const { value, suffix } = splitInlineCommentUnquoted(rhs);
516
+ const seg = {
517
+ kind: 'assignment',
518
+ raw: line + eol,
519
+ key,
520
+ prefix,
521
+ separator,
522
+ valuePadding,
523
+ quote: null,
524
+ value,
525
+ suffix,
526
+ eol,
527
+ };
528
+ segments.push(seg);
529
+ continue;
530
+ }
531
+ // Bare-key placeholder (KEY or KEY # comment). Only accept whitespace/comment in rest.
532
+ const mBare = /^(\s*)(#.*)?$/.exec(rest);
533
+ if (mBare) {
534
+ const suffix = (mBare[1] ?? '') + (mBare[2] ?? '');
535
+ const seg = {
536
+ kind: 'bare',
537
+ raw: line + eol,
538
+ key,
539
+ prefix,
540
+ suffix,
541
+ eol,
542
+ };
543
+ segments.push(seg);
544
+ continue;
545
+ }
546
+ // Unknown/unsupported syntax for this line: preserve verbatim.
547
+ const rawSeg = { kind: 'raw', raw: line + eol };
548
+ segments.push(rawSeg);
549
+ }
550
+ return { fileEol, trailingNewline, segments };
551
+ }
552
+
553
+ /**
554
+ * Render a parsed dotenv document back to text.
555
+ *
556
+ * Requirements addressed:
557
+ * - Preserve existing EOLs by default; support forcing LF/CRLF.
558
+ * - Preserve trailing newline presence/absence.
559
+ *
560
+ * @packageDocumentation
561
+ */
562
+ const normalizeEol = (txt, eol) => txt.replace(/\r?\n/g, eol);
563
+ const stripOneFinalNewline = (txt) => {
564
+ if (txt.endsWith('\r\n'))
565
+ return txt.slice(0, -2);
566
+ if (txt.endsWith('\n'))
567
+ return txt.slice(0, -1);
568
+ return txt;
569
+ };
570
+ const endsWithNewline = (txt) => txt.endsWith('\n') || txt.endsWith('\r\n');
571
+ /**
572
+ * Render a {@link DotenvDocument} to text.
573
+ *
574
+ * @param doc - Document to render.
575
+ * @param eolMode - EOL policy (`preserve` | `lf` | `crlf`).
576
+ * @returns Rendered dotenv text.
577
+ *
578
+ * @public
579
+ */
580
+ function renderDotenvDocument(doc, eolMode = 'preserve') {
581
+ const joined = doc.segments.map((s) => s.raw).join('');
582
+ const targetEol = eolMode === 'crlf' ? '\r\n' : eolMode === 'lf' ? '\n' : doc.fileEol;
583
+ const normalized = eolMode === 'preserve' ? joined : normalizeEol(joined, targetEol);
584
+ // Preserve final newline presence/absence.
585
+ if (doc.trailingNewline) {
586
+ return endsWithNewline(normalized) ? normalized : normalized + targetEol;
587
+ }
588
+ return stripOneFinalNewline(normalized);
589
+ }
590
+
591
+ /**
592
+ * High-level convenience API for editing dotenv text in memory.
593
+ *
594
+ * Requirements addressed:
595
+ * - Pure/text layer: parse → apply edits → render (no FS).
596
+ *
597
+ * @packageDocumentation
598
+ */
599
+ /**
600
+ * Edit dotenv text with format preservation.
601
+ *
602
+ * @param text - Existing dotenv text.
603
+ * @param updates - Update map of keys to values.
604
+ * @param options - Edit options (merge vs sync, duplicates, null/undefined behavior, EOL policy).
605
+ * @returns Updated dotenv text.
606
+ *
607
+ * @public
608
+ */
609
+ function editDotenvText(text, updates, options = {}) {
610
+ const doc = parseDotenvDocument(text);
611
+ const edited = applyDotenvEdits(doc, updates, {
612
+ ...(options.mode ? { mode: options.mode } : {}),
613
+ ...(options.duplicateKeys ? { duplicateKeys: options.duplicateKeys } : {}),
614
+ ...(options.undefinedBehavior
615
+ ? { undefinedBehavior: options.undefinedBehavior }
616
+ : {}),
617
+ ...(options.nullBehavior ? { nullBehavior: options.nullBehavior } : {}),
618
+ ...(typeof options.defaultSeparator === 'string'
619
+ ? { defaultSeparator: options.defaultSeparator }
620
+ : {}),
621
+ });
622
+ return renderDotenvDocument(edited, options.eol ?? 'preserve');
623
+ }
624
+
625
+ /**
626
+ * @packageDocumentation
627
+ * Deterministic dotenv target selection across a multi-path cascade.
628
+ *
629
+ * This module extracts the “which file should we edit?” logic from the FS-level editor
630
+ * so plugins/tools can reuse the same selection semantics as {@link editDotenvFile}.
631
+ *
632
+ * Notes:
633
+ * - Selection is based on `paths` only (directories), consistent with get-dotenv overlay precedence.
634
+ * - Default search order is reverse (last path wins).
635
+ * - Template discovery is supported via `<target>.<templateExtension>` and returns the template path
636
+ * when the concrete target is missing.
637
+ */
638
+ const resolveEnvName = (env, defaultEnv) => typeof env === 'string' && env.length > 0
639
+ ? env
640
+ : typeof defaultEnv === 'string' && defaultEnv.length > 0
641
+ ? defaultEnv
642
+ : undefined;
643
+ /**
644
+ * Build the dotenv filename for a selector (scope × privacy) using the same naming
645
+ * convention as get-dotenv.
646
+ *
647
+ * @param opts - Filename selector options.
648
+ * @returns The filename token (for example, `.env.dev.local`).
649
+ * @throws Error when `scope` is `'env'` and neither `env` nor `defaultEnv` is provided.
650
+ *
651
+ * @public
652
+ */
653
+ function buildDotenvTargetFilename(opts) {
654
+ const dotenvToken = opts.dotenvToken ?? '.env';
655
+ const privateToken = opts.privateToken ?? 'local';
656
+ const envName = resolveEnvName(opts.env, opts.defaultEnv);
657
+ const parts = [dotenvToken];
658
+ if (opts.scope === 'env') {
659
+ if (!envName) {
660
+ throw new Error(`Unable to resolve env-scoped dotenv filename: env is required.`);
661
+ }
662
+ parts.push(envName);
663
+ }
664
+ if (opts.privacy === 'private')
665
+ parts.push(privateToken);
666
+ return parts.join('.');
667
+ }
668
+ const orderedPaths = (pathsIn, order) => {
669
+ const list = pathsIn.slice();
670
+ return order === 'forward' ? list : list.reverse();
671
+ };
672
+ /**
673
+ * Resolve a deterministic dotenv target across `paths` based on scope/privacy selectors.
674
+ *
675
+ * Selection rules:
676
+ * - Iterates `paths` in the requested search order.
677
+ * - Returns the first existing target file.
678
+ * - Otherwise, returns the first sibling template file (`<target>.<templateExtension>`) when present.
679
+ * - Throws when neither a target nor a template exists anywhere under `paths`.
680
+ *
681
+ * @param opts - Resolver options (paths + selector + fs port).
682
+ * @returns The resolved target path and optional template path.
683
+ *
684
+ * @public
685
+ */
686
+ async function resolveDotenvTarget(opts) {
687
+ const filename = buildDotenvTargetFilename(opts);
688
+ const templateExtension = opts.templateExtension ?? 'template';
689
+ const searchOrder = opts.searchOrder ?? 'reverse';
690
+ const pathsOrdered = orderedPaths(opts.paths, searchOrder);
691
+ for (const dir of pathsOrdered) {
692
+ const targetPath = path.resolve(dir, filename);
693
+ if (await opts.fs.pathExists(targetPath)) {
694
+ return { targetPath, filename };
695
+ }
696
+ const templatePath = `${targetPath}.${templateExtension}`;
697
+ if (await opts.fs.pathExists(templatePath)) {
698
+ return { targetPath, templatePath, filename };
699
+ }
700
+ }
701
+ throw new Error(`Unable to locate dotenv target "${filename}" under provided paths, and no template was found.`);
702
+ }
703
+
704
+ /**
705
+ * FS-level dotenv editor adapter (target resolution + template bootstrap).
706
+ *
707
+ * Requirements addressed:
708
+ * - Deterministic target selection across getdotenv `paths` only.
709
+ * - Scope axis (global|env) × privacy axis (public|private).
710
+ * - Template bootstrap: copy `<target>.<templateExtension>` to `<target>` when needed.
711
+ * - Edit in place while preserving formatting via the pure text editor.
712
+ *
713
+ * @packageDocumentation
714
+ */
715
+ const defaultFs = {
716
+ pathExists: async (p) => fs.pathExists(p),
717
+ readFile: async (p) => fs.readFile(p, 'utf-8'),
718
+ writeFile: async (p, contents) => fs.writeFile(p, contents, 'utf-8'),
719
+ copyFile: async (src, dest) => fs.copyFile(src, dest),
720
+ };
721
+ /**
722
+ * Edit a dotenv file selected by scope/privacy across a list of search paths.
723
+ *
724
+ * @param updates - Update map of keys to values.
725
+ * @param options - Target selection options + edit options.
726
+ * @returns An {@link EditDotenvFileResult}.
727
+ *
728
+ * @public
729
+ */
730
+ async function editDotenvFile(updates, options) {
731
+ const fsPort = options.fs ?? defaultFs;
732
+ const dotenvToken = options.dotenvToken ?? '.env';
733
+ const privateToken = options.privateToken ?? 'local';
734
+ const templateExtension = options.templateExtension ?? 'template';
735
+ const searchOrder = options.searchOrder ?? 'reverse';
736
+ const resolved = await resolveDotenvTarget({
737
+ fs: fsPort,
738
+ paths: options.paths,
739
+ dotenvToken,
740
+ privateToken,
741
+ ...(typeof options.env === 'string' && options.env.length > 0
742
+ ? { env: options.env }
743
+ : {}),
744
+ ...(typeof options.defaultEnv === 'string' && options.defaultEnv.length > 0
745
+ ? { defaultEnv: options.defaultEnv }
746
+ : {}),
747
+ scope: options.scope,
748
+ privacy: options.privacy,
749
+ templateExtension,
750
+ searchOrder,
751
+ });
752
+ let createdFromTemplate = false;
753
+ if (resolved.templatePath) {
754
+ // Template bootstrap: copy template as the destination sibling.
755
+ // Only copy if the target does not already exist.
756
+ if (!(await fsPort.pathExists(resolved.targetPath))) {
757
+ await fsPort.copyFile(resolved.templatePath, resolved.targetPath);
758
+ createdFromTemplate = true;
759
+ }
760
+ }
761
+ const before = await fsPort.readFile(resolved.targetPath);
762
+ const after = editDotenvText(before, updates, {
763
+ ...(options.mode ? { mode: options.mode } : {}),
764
+ ...(options.duplicateKeys ? { duplicateKeys: options.duplicateKeys } : {}),
765
+ ...(options.undefinedBehavior
766
+ ? { undefinedBehavior: options.undefinedBehavior }
767
+ : {}),
768
+ ...(options.nullBehavior ? { nullBehavior: options.nullBehavior } : {}),
769
+ ...(options.eol ? { eol: options.eol } : {}),
770
+ ...(typeof options.defaultSeparator === 'string'
771
+ ? { defaultSeparator: options.defaultSeparator }
772
+ : {}),
773
+ });
774
+ const changed = before !== after;
775
+ if (changed) {
776
+ await fsPort.writeFile(resolved.targetPath, after);
777
+ }
778
+ return { path: resolved.targetPath, createdFromTemplate, changed };
779
+ }
780
+
781
+ async function getDotenv(options = {}) {
782
+ // Apply defaults.
783
+ const { defaultEnv, dotenvToken = '.env', dynamicPath, env, excludeDynamic = false, excludeEnv = false, excludeGlobal = false, excludePrivate = false, excludePublic = false, loadProcess = false, log = false, logger = console, outputPath, paths = [], privateToken = 'local', vars = {}, } = await resolveGetDotenvOptions(options);
784
+ // Read .env files.
785
+ const loaded = paths.length
786
+ ? await paths.reduce(async (e, p) => {
787
+ const publicGlobal = excludePublic || excludeGlobal
788
+ ? Promise.resolve({})
789
+ : readDotenv(path$1.resolve(p, dotenvToken));
790
+ const publicEnv = excludePublic || excludeEnv || (!env && !defaultEnv)
791
+ ? Promise.resolve({})
792
+ : readDotenv(path$1.resolve(p, `${dotenvToken}.${env ?? defaultEnv ?? ''}`));
793
+ const privateGlobal = excludePrivate || excludeGlobal
794
+ ? Promise.resolve({})
795
+ : readDotenv(path$1.resolve(p, `${dotenvToken}.${privateToken}`));
796
+ const privateEnv = excludePrivate || excludeEnv || (!env && !defaultEnv)
797
+ ? Promise.resolve({})
798
+ : readDotenv(path$1.resolve(p, `${dotenvToken}.${env ?? defaultEnv ?? ''}.${privateToken}`));
799
+ const [eResolved, publicGlobalResolved, publicEnvResolved, privateGlobalResolved, privateEnvResolved,] = await Promise.all([
800
+ e,
801
+ publicGlobal,
802
+ publicEnv,
803
+ privateGlobal,
804
+ privateEnv,
805
+ ]);
806
+ return {
807
+ ...eResolved,
808
+ ...publicGlobalResolved,
809
+ ...publicEnvResolved,
810
+ ...privateGlobalResolved,
811
+ ...privateEnvResolved,
812
+ };
813
+ }, Promise.resolve({}))
814
+ : {};
815
+ const outputKey = nanoid();
816
+ const dotenv = dotenvExpandAll({
817
+ ...loaded,
818
+ ...vars,
819
+ ...(outputPath ? { [outputKey]: outputPath } : {}),
820
+ }, { progressive: true });
821
+ // Process dynamic variables. Programmatic option takes precedence over path.
822
+ if (!excludeDynamic) {
823
+ // A2 precedence: programmatic dynamic < dynamicPath (dynamicPath wins when present)
824
+ if (options.dynamic && Object.keys(options.dynamic).length > 0) {
825
+ try {
826
+ applyDynamicMap(dotenv, options.dynamic, env ?? defaultEnv);
827
+ }
828
+ catch {
829
+ throw new Error(`Unable to evaluate dynamic variables.`);
830
+ }
831
+ }
832
+ // dynamicPath is evaluated even when programmatic `dynamic` is present.
833
+ if (dynamicPath) {
834
+ const absDynamicPath = path$1.resolve(dynamicPath);
835
+ await loadAndApplyDynamic(dotenv, absDynamicPath, env ?? defaultEnv, 'getdotenv-dynamic');
836
+ }
837
+ }
838
+ // Write output file.
839
+ let resultDotenv = dotenv;
840
+ if (outputPath) {
841
+ const outputPathResolved = dotenv[outputKey];
842
+ if (!outputPathResolved)
843
+ throw new Error('Output path not found.');
844
+ const { [outputKey]: _omitted, ...dotenvForOutput } = dotenv;
845
+ await writeDotenvFile(outputPathResolved, dotenvForOutput);
846
+ resultDotenv = dotenvForOutput;
847
+ }
848
+ // Log result.
849
+ if (log) {
850
+ const redactFlag = options.redact ?? false;
851
+ const redactPatterns = options.redactPatterns ?? undefined;
852
+ const redOpts = {};
853
+ if (redactFlag)
854
+ redOpts.redact = true;
855
+ if (redactFlag && Array.isArray(redactPatterns))
856
+ redOpts.redactPatterns = redactPatterns;
857
+ const bag = redactFlag
858
+ ? redactObject(resultDotenv, redOpts)
859
+ : { ...resultDotenv };
860
+ logger.log(bag);
861
+ // Entropy warnings: once-per-key-per-run (presentation only)
862
+ const warnEntropyVal = options.warnEntropy ?? true;
863
+ const entropyThresholdVal = options
864
+ .entropyThreshold;
865
+ const entropyMinLengthVal = options
866
+ .entropyMinLength;
867
+ const entropyWhitelistVal = options.entropyWhitelist;
868
+ const entOpts = {};
869
+ if (typeof warnEntropyVal === 'boolean')
870
+ entOpts.warnEntropy = warnEntropyVal;
871
+ if (typeof entropyThresholdVal === 'number')
872
+ entOpts.entropyThreshold = entropyThresholdVal;
873
+ if (typeof entropyMinLengthVal === 'number')
874
+ entOpts.entropyMinLength = entropyMinLengthVal;
875
+ if (Array.isArray(entropyWhitelistVal))
876
+ entOpts.entropyWhitelist = entropyWhitelistVal;
877
+ for (const [k, v] of Object.entries(resultDotenv)) {
878
+ maybeWarnEntropy(k, v, v !== undefined ? 'dotenv' : 'unset', entOpts, (line) => {
879
+ logger.log(line);
880
+ });
881
+ }
882
+ }
883
+ // Load process.env.
884
+ if (loadProcess)
885
+ Object.assign(process.env, resultDotenv);
886
+ return resultDotenv;
887
+ }
888
+
889
+ export { applyDotenvEdits, dotenvExpandAll, editDotenvFile, editDotenvText, getDotenv, maybeWarnEntropy, parseDotenvDocument, redactObject, renderDotenvDocument };