envprism 0.0.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.
@@ -0,0 +1,421 @@
1
+ import { basename, join } from "pathe";
2
+ import { stat, readdir, readFile } from "node:fs/promises";
3
+ function resolveBase(files, override) {
4
+ if (files.length === 0) return null;
5
+ if (override) {
6
+ const match = files.find(
7
+ (f) => f.path === override || basename(f.path) === override
8
+ );
9
+ if (!match) {
10
+ throw new Error(
11
+ `--base ${override} did not match any discovered env file`
12
+ );
13
+ }
14
+ return match;
15
+ }
16
+ const example = files.find((f) => basename(f.path) === ".env.example");
17
+ if (example) return example;
18
+ return files[0] ?? null;
19
+ }
20
+ const DRIFT_STATES = /* @__PURE__ */ new Set([
21
+ "differs",
22
+ "missing",
23
+ "extra"
24
+ ]);
25
+ function computeDiff(matrix) {
26
+ const others = matrix.files.filter((f) => f !== matrix.base);
27
+ const files = others.map((f) => buildFileReport(matrix, f));
28
+ return {
29
+ base: matrix.base.path,
30
+ files,
31
+ inSync: files.every((f) => f.drift === 0)
32
+ };
33
+ }
34
+ function buildFileReport(matrix, file) {
35
+ const keys = {};
36
+ let drift = 0;
37
+ for (const key of matrix.keys) {
38
+ const { state } = matrix.cell(key, file);
39
+ keys[key] = state;
40
+ if (DRIFT_STATES.has(state)) drift++;
41
+ }
42
+ return { path: file.path, keys, drift };
43
+ }
44
+ function formatDiffText(report) {
45
+ const baseName = basename(report.base);
46
+ const otherNames = report.files.map((f) => basename(f.path));
47
+ const lines = [];
48
+ lines.push(`Base: ${baseName} (vs. ${otherNames.join(", ")})`);
49
+ lines.push("");
50
+ if (report.files.length === 0) {
51
+ lines.push("No other env files to compare.");
52
+ return lines.join("\n") + "\n";
53
+ }
54
+ const driftKeys = /* @__PURE__ */ new Set();
55
+ for (const f of report.files) {
56
+ for (const [k, s] of Object.entries(f.keys)) {
57
+ if (DRIFT_STATES.has(s)) driftKeys.add(k);
58
+ }
59
+ }
60
+ if (driftKeys.size === 0) {
61
+ lines.push("All env files are in sync with the base.");
62
+ return lines.join("\n") + "\n";
63
+ }
64
+ const keyWidth = Math.max(3, ...[...driftKeys].map((k) => k.length));
65
+ const colWidth = Math.max(12, ...otherNames.map((n) => n.length));
66
+ lines.push(
67
+ formatRow("KEY", otherNames, keyWidth, colWidth, (n) => n.padEnd(colWidth))
68
+ );
69
+ for (const key of [...driftKeys].sort()) {
70
+ const cells = report.files.map((f) => stateLabel(f.keys[key] ?? "missing"));
71
+ lines.push(
72
+ formatRow(key, cells, keyWidth, colWidth, (n) => n.padEnd(colWidth))
73
+ );
74
+ }
75
+ lines.push("");
76
+ const totalDrift = report.files.reduce((sum, f) => sum + f.drift, 0);
77
+ lines.push(
78
+ `${driftKeys.size} key(s) differ across ${report.files.length} file(s) (${totalDrift} cell drift).`
79
+ );
80
+ return lines.join("\n") + "\n";
81
+ }
82
+ function formatRow(key, cells, keyWidth, colWidth, pad) {
83
+ return [key.padEnd(keyWidth), ...cells.map(pad)].join(" ");
84
+ }
85
+ function stateLabel(state) {
86
+ switch (state) {
87
+ case "same":
88
+ return "— same";
89
+ case "differs":
90
+ return "≠ differs";
91
+ case "missing":
92
+ return "✗ missing";
93
+ case "extra":
94
+ return "★ extra";
95
+ case "base":
96
+ return "· base";
97
+ }
98
+ }
99
+ const KEY_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
100
+ function parseEnv(source, path = "") {
101
+ const trailingNewline = source.endsWith("\n");
102
+ const body = trailingNewline ? source.slice(0, -1) : source;
103
+ const lines = body.length === 0 ? [] : body.split("\n");
104
+ const entries = [];
105
+ for (const raw of lines) {
106
+ entries.push(parseLine(raw));
107
+ }
108
+ return { path, entries, trailingNewline };
109
+ }
110
+ function parseLine(raw) {
111
+ if (raw.trim().length === 0) {
112
+ return { kind: "blank", raw };
113
+ }
114
+ const trimmedStart = raw.replace(/^\s+/, "");
115
+ if (trimmedStart.startsWith("#")) {
116
+ return { kind: "comment", raw };
117
+ }
118
+ const kv = tryParseKv(raw);
119
+ if (kv) return kv;
120
+ return { kind: "comment", raw };
121
+ }
122
+ function tryParseKv(raw) {
123
+ let rest = raw;
124
+ const leadingWs = rest.match(/^[ \t]*/)?.[0] ?? "";
125
+ rest = rest.slice(leadingWs.length);
126
+ let exportPrefix = false;
127
+ if (rest.startsWith("export ")) {
128
+ exportPrefix = true;
129
+ rest = rest.slice("export ".length).replace(/^[ \t]*/, "");
130
+ }
131
+ const eqIdx = rest.indexOf("=");
132
+ if (eqIdx < 0) return null;
133
+ const key = rest.slice(0, eqIdx).trimEnd();
134
+ if (!KEY_RE.test(key)) return null;
135
+ const after = rest.slice(eqIdx + 1);
136
+ const valueStart = after.replace(/^[ \t]*/, "");
137
+ after.slice(0, after.length - valueStart.length);
138
+ const parsed = parseValue(valueStart);
139
+ if (!parsed) return null;
140
+ return {
141
+ kind: "kv",
142
+ key,
143
+ rawValue: parsed.rawValue,
144
+ value: parsed.value,
145
+ quoting: parsed.quoting,
146
+ exportPrefix,
147
+ inlineComment: parsed.inlineComment,
148
+ raw
149
+ };
150
+ }
151
+ function parseValue(input) {
152
+ if (input.length === 0) {
153
+ return { rawValue: "", value: "", quoting: "none", inlineComment: "" };
154
+ }
155
+ const first = input[0];
156
+ if (first === '"' || first === "'") {
157
+ const close = findClosingQuote(input, first);
158
+ if (close < 0) return null;
159
+ const rawValue = input.slice(1, close);
160
+ const value = first === '"' ? decodeDoubleQuoted(rawValue) : rawValue;
161
+ const tail = input.slice(close + 1);
162
+ const inlineComment2 = extractInlineComment(tail);
163
+ return {
164
+ rawValue,
165
+ value,
166
+ quoting: first === '"' ? "double" : "single",
167
+ inlineComment: inlineComment2
168
+ };
169
+ }
170
+ const hashIdx = findUnquotedCommentStart(input);
171
+ const valuePart = (hashIdx < 0 ? input : input.slice(0, hashIdx)).trimEnd();
172
+ const inlineComment = hashIdx < 0 ? "" : input.slice(valuePart.length);
173
+ return {
174
+ rawValue: valuePart,
175
+ value: valuePart,
176
+ quoting: "none",
177
+ inlineComment
178
+ };
179
+ }
180
+ function findClosingQuote(input, quote) {
181
+ for (let i = 1; i < input.length; i++) {
182
+ const ch = input[i];
183
+ if (quote === '"' && ch === "\\") {
184
+ i++;
185
+ continue;
186
+ }
187
+ if (ch === quote) return i;
188
+ }
189
+ return -1;
190
+ }
191
+ function findUnquotedCommentStart(input) {
192
+ for (let i = 0; i < input.length; i++) {
193
+ if (input[i] !== "#") continue;
194
+ if (i === 0) return i;
195
+ const prev = input[i - 1];
196
+ if (prev === " " || prev === " ") return i;
197
+ }
198
+ return -1;
199
+ }
200
+ function extractInlineComment(tail) {
201
+ return tail;
202
+ }
203
+ function decodeDoubleQuoted(raw) {
204
+ return raw.replace(/\\(.)/g, (_, ch) => {
205
+ switch (ch) {
206
+ case "n":
207
+ return "\n";
208
+ case "r":
209
+ return "\r";
210
+ case "t":
211
+ return " ";
212
+ case "\\":
213
+ return "\\";
214
+ case '"':
215
+ return '"';
216
+ default:
217
+ return `\\${ch}`;
218
+ }
219
+ });
220
+ }
221
+ const SKIP_SUFFIXES = [".swp", "~", ".bak"];
222
+ async function discoverEnvFiles(paths) {
223
+ const filePaths = /* @__PURE__ */ new Set();
224
+ for (const p of paths) {
225
+ const info = await stat(p);
226
+ if (info.isDirectory()) {
227
+ const entries = await readdir(p);
228
+ for (const name of entries) {
229
+ if (!looksLikeEnvFile(name)) continue;
230
+ filePaths.add(join(p, name));
231
+ }
232
+ } else {
233
+ filePaths.add(p);
234
+ }
235
+ }
236
+ const sorted = [...filePaths].sort(envPathOrder);
237
+ const files = [];
238
+ for (const filePath of sorted) {
239
+ const source = await readFile(filePath, "utf8");
240
+ files.push(parseEnv(source, filePath));
241
+ }
242
+ return files;
243
+ }
244
+ function looksLikeEnvFile(name) {
245
+ if (!name.startsWith(".env")) return false;
246
+ if (SKIP_SUFFIXES.some((s) => name.endsWith(s))) return false;
247
+ return true;
248
+ }
249
+ function envPathOrder(a, b) {
250
+ const an = basename(a);
251
+ const bn = basename(b);
252
+ if (an === ".env.example") return -1;
253
+ if (bn === ".env.example") return 1;
254
+ return an.localeCompare(bn);
255
+ }
256
+ const SEP_CHARS = /* @__PURE__ */ new Set(["=", "-", "#", "~", "*"]);
257
+ const SPACE_CHARS = /* @__PURE__ */ new Set([" ", " "]);
258
+ function computeSections(base) {
259
+ const out = /* @__PURE__ */ new Map();
260
+ let current = null;
261
+ for (let i = 0; i < base.entries.length; i++) {
262
+ const name = detectSectionName(base.entries, i);
263
+ if (name !== null) current = name;
264
+ const e = base.entries[i];
265
+ if (e.kind === "kv" && current) out.set(e.key, current);
266
+ }
267
+ return out;
268
+ }
269
+ function detectSectionName(entries, idx) {
270
+ const e = entries[idx];
271
+ if (!e || e.kind !== "comment") return null;
272
+ if (isDecorativeLine(e.raw)) return null;
273
+ const inline = parseInlineBanner(e.raw);
274
+ if (inline !== null) return inline;
275
+ const text = stripCommentPrefix(e.raw);
276
+ if (!text) return null;
277
+ if (isDecorative(entries[idx - 1]) || isDecorative(entries[idx + 1])) {
278
+ return text;
279
+ }
280
+ return null;
281
+ }
282
+ function isDecorative(e) {
283
+ if (!e || e.kind !== "comment") return false;
284
+ return isDecorativeLine(e.raw);
285
+ }
286
+ function isDecorativeLine(raw) {
287
+ const start = skipSpaces(raw, 0);
288
+ if (start >= raw.length || raw[start] !== "#") return false;
289
+ let i = start + 1;
290
+ let sawSeparator = false;
291
+ while (i < raw.length) {
292
+ const ch = raw[i];
293
+ if (SEP_CHARS.has(ch)) sawSeparator = true;
294
+ else if (!SPACE_CHARS.has(ch)) return false;
295
+ i++;
296
+ }
297
+ return sawSeparator;
298
+ }
299
+ function parseInlineBanner(raw) {
300
+ const start = skipSpaces(raw, 0);
301
+ if (start >= raw.length || raw[start] !== "#") return null;
302
+ let i = skipSpaces(raw, start + 1);
303
+ if (i >= raw.length || !SEP_CHARS.has(raw[i])) return null;
304
+ let leadCount = 0;
305
+ while (i < raw.length && SEP_CHARS.has(raw[i])) {
306
+ leadCount++;
307
+ i++;
308
+ }
309
+ if (leadCount < 2) return null;
310
+ let j = raw.length;
311
+ while (j > i && SPACE_CHARS.has(raw[j - 1])) j--;
312
+ if (j <= i || !SEP_CHARS.has(raw[j - 1])) return null;
313
+ let trailCount = 0;
314
+ while (j > i && SEP_CHARS.has(raw[j - 1])) {
315
+ trailCount++;
316
+ j--;
317
+ }
318
+ if (trailCount < 2) return null;
319
+ const inner = raw.slice(i, j).trim();
320
+ return inner.length > 0 ? inner : null;
321
+ }
322
+ function stripCommentPrefix(raw) {
323
+ let i = skipSpaces(raw, 0);
324
+ while (i < raw.length && raw[i] === "#") i++;
325
+ i = skipSpaces(raw, i);
326
+ let j = raw.length;
327
+ while (j > i && (SPACE_CHARS.has(raw[j - 1]) || SEP_CHARS.has(raw[j - 1]))) {
328
+ j--;
329
+ }
330
+ return raw.slice(i, j);
331
+ }
332
+ function skipSpaces(raw, from) {
333
+ let i = from;
334
+ while (i < raw.length && SPACE_CHARS.has(raw[i])) i++;
335
+ return i;
336
+ }
337
+ function buildMatrix(files, base) {
338
+ const keys = collectKeys(files, base);
339
+ const lookups = /* @__PURE__ */ new Map();
340
+ for (const file of files) {
341
+ lookups.set(file, indexKv(file));
342
+ }
343
+ const baseIndex = lookups.get(base);
344
+ if (!baseIndex) {
345
+ throw new Error("base file is not in the files list");
346
+ }
347
+ const sections = computeSections(base);
348
+ return {
349
+ keys,
350
+ files,
351
+ base,
352
+ sectionOf(key) {
353
+ return sections.get(key);
354
+ },
355
+ cell(key, file) {
356
+ const ownIndex = lookups.get(file);
357
+ if (!ownIndex) {
358
+ throw new Error(`file not in matrix: ${file.path}`);
359
+ }
360
+ const own = ownIndex.get(key);
361
+ const baseEntry = baseIndex.get(key);
362
+ if (file === base) {
363
+ return {
364
+ state: own ? "base" : "missing",
365
+ value: own?.value
366
+ };
367
+ }
368
+ if (!own && !baseEntry) {
369
+ return { state: "missing", value: void 0 };
370
+ }
371
+ if (!own) {
372
+ return { state: "missing", value: void 0 };
373
+ }
374
+ if (!baseEntry) {
375
+ return { state: "extra", value: own.value };
376
+ }
377
+ return {
378
+ state: own.value === baseEntry.value ? "same" : "differs",
379
+ value: own.value
380
+ };
381
+ }
382
+ };
383
+ }
384
+ function collectKeys(files, base) {
385
+ const seen = /* @__PURE__ */ new Set();
386
+ const out = [];
387
+ for (const e of base.entries) {
388
+ if (e.kind === "kv" && !seen.has(e.key)) {
389
+ seen.add(e.key);
390
+ out.push(e.key);
391
+ }
392
+ }
393
+ const extras = /* @__PURE__ */ new Set();
394
+ for (const file of files) {
395
+ if (file === base) continue;
396
+ for (const e of file.entries) {
397
+ if (e.kind === "kv" && !seen.has(e.key)) {
398
+ extras.add(e.key);
399
+ }
400
+ }
401
+ }
402
+ for (const k of [...extras].sort()) out.push(k);
403
+ return out;
404
+ }
405
+ function indexKv(file) {
406
+ const m = /* @__PURE__ */ new Map();
407
+ for (const e of file.entries) {
408
+ if (e.kind === "kv") m.set(e.key, e);
409
+ }
410
+ return m;
411
+ }
412
+ export {
413
+ computeSections as a,
414
+ buildMatrix as b,
415
+ computeDiff as c,
416
+ discoverEnvFiles as d,
417
+ formatDiffText as f,
418
+ parseEnv as p,
419
+ resolveBase as r
420
+ };
421
+ //# sourceMappingURL=matrix-nBq_GY2v.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"matrix-nBq_GY2v.mjs","sources":["../../src/core/base.ts","../../src/core/diff.ts","../../src/core/parse.ts","../../src/core/discover.ts","../../src/core/sections.ts","../../src/core/matrix.ts"],"sourcesContent":["import { basename } from 'pathe';\nimport type { EnvFile } from './types.ts';\n\n/**\n * Resolve which {@link EnvFile} acts as the base (reference) for diff. Order:\n *\n * 1. `override` path argument, matched by full path or basename.\n * 2. `.env.example` if present.\n * 3. The first file (already sorted by {@link discoverEnvFiles}).\n *\n * Returns `null` if `files` is empty.\n */\nexport function resolveBase(\n files: EnvFile[],\n override?: string\n): EnvFile | null {\n if (files.length === 0) return null;\n\n if (override) {\n const match = files.find(\n (f) => f.path === override || basename(f.path) === override\n );\n if (!match) {\n throw new Error(\n `--base ${override} did not match any discovered env file`\n );\n }\n return match;\n }\n\n const example = files.find((f) => basename(f.path) === '.env.example');\n if (example) return example;\n\n return files[0] ?? null;\n}\n","import { basename } from 'pathe';\nimport type { Matrix, CellState } from './matrix.ts';\nimport type { EnvFile } from './types.ts';\n\nexport interface DiffReport {\n base: string;\n /** Compared files, excluding the base. */\n files: DiffFileReport[];\n /** True when no non-base cell is `differs`, `missing`, or `extra`. */\n inSync: boolean;\n}\n\nexport interface DiffFileReport {\n path: string;\n keys: Record<string, CellState>;\n /** Number of keys with state !== 'same'. */\n drift: number;\n}\n\nconst DRIFT_STATES: ReadonlySet<CellState> = new Set([\n 'differs',\n 'missing',\n 'extra'\n]);\n\nexport function computeDiff(matrix: Matrix): DiffReport {\n const others = matrix.files.filter((f) => f !== matrix.base);\n const files = others.map((f) => buildFileReport(matrix, f));\n return {\n base: matrix.base.path,\n files,\n inSync: files.every((f) => f.drift === 0)\n };\n}\n\nfunction buildFileReport(matrix: Matrix, file: EnvFile): DiffFileReport {\n const keys: Record<string, CellState> = {};\n let drift = 0;\n for (const key of matrix.keys) {\n const { state } = matrix.cell(key, file);\n keys[key] = state;\n if (DRIFT_STATES.has(state)) drift++;\n }\n return { path: file.path, keys, drift };\n}\n\nexport function formatDiffText(report: DiffReport): string {\n const baseName = basename(report.base);\n const otherNames = report.files.map((f) => basename(f.path));\n const lines: string[] = [];\n lines.push(`Base: ${baseName} (vs. ${otherNames.join(', ')})`);\n lines.push('');\n\n if (report.files.length === 0) {\n lines.push('No other env files to compare.');\n return lines.join('\\n') + '\\n';\n }\n\n const driftKeys = new Set<string>();\n for (const f of report.files) {\n for (const [k, s] of Object.entries(f.keys)) {\n if (DRIFT_STATES.has(s)) driftKeys.add(k);\n }\n }\n\n if (driftKeys.size === 0) {\n lines.push('All env files are in sync with the base.');\n return lines.join('\\n') + '\\n';\n }\n\n const keyWidth = Math.max(3, ...[...driftKeys].map((k) => k.length));\n const colWidth = Math.max(12, ...otherNames.map((n) => n.length));\n\n lines.push(\n formatRow('KEY', otherNames, keyWidth, colWidth, (n) => n.padEnd(colWidth))\n );\n for (const key of [...driftKeys].sort()) {\n const cells = report.files.map((f) => stateLabel(f.keys[key] ?? 'missing'));\n lines.push(\n formatRow(key, cells, keyWidth, colWidth, (n) => n.padEnd(colWidth))\n );\n }\n\n lines.push('');\n const totalDrift = report.files.reduce((sum, f) => sum + f.drift, 0);\n lines.push(\n `${driftKeys.size} key(s) differ across ${report.files.length} file(s) (${totalDrift} cell drift).`\n );\n\n return lines.join('\\n') + '\\n';\n}\n\nfunction formatRow(\n key: string,\n cells: string[],\n keyWidth: number,\n colWidth: number,\n pad: (s: string) => string\n): string {\n return [key.padEnd(keyWidth), ...cells.map(pad)].join(' ');\n}\n\nfunction stateLabel(state: CellState): string {\n switch (state) {\n case 'same':\n return '— same';\n case 'differs':\n return '≠ differs';\n case 'missing':\n return '✗ missing';\n case 'extra':\n return '★ extra';\n case 'base':\n return '· base';\n }\n}\n","import type { EnvEntry, EnvFile, KvEntry, Quoting } from './types.ts';\n\nconst KEY_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;\n\n/**\n * Parse a `.env` file into a structured, round-trippable representation.\n * Each entry stores its original raw line so the serializer can emit it\n * byte-for-byte when nothing changes.\n */\nexport function parseEnv(source: string, path = ''): EnvFile {\n const trailingNewline = source.endsWith('\\n');\n const body = trailingNewline ? source.slice(0, -1) : source;\n const lines = body.length === 0 ? [] : body.split('\\n');\n\n const entries: EnvEntry[] = [];\n for (const raw of lines) {\n entries.push(parseLine(raw));\n }\n\n return { path, entries, trailingNewline };\n}\n\nfunction parseLine(raw: string): EnvEntry {\n if (raw.trim().length === 0) {\n return { kind: 'blank', raw };\n }\n\n // Comment line: leading whitespace then `#`.\n const trimmedStart = raw.replace(/^\\s+/, '');\n if (trimmedStart.startsWith('#')) {\n return { kind: 'comment', raw };\n }\n\n const kv = tryParseKv(raw);\n if (kv) return kv;\n\n // Unrecognised line — treat as a comment-like passthrough so round-trip\n // still works. Diff/matrix code ignores non-kv entries.\n return { kind: 'comment', raw };\n}\n\nfunction tryParseKv(raw: string): KvEntry | null {\n let rest = raw;\n // Optional leading whitespace is preserved in `raw`; the structured key\n // identifier itself is captured separately so we can rebuild the line.\n const leadingWs = rest.match(/^[ \\t]*/)?.[0] ?? '';\n rest = rest.slice(leadingWs.length);\n\n let exportPrefix = false;\n if (rest.startsWith('export ')) {\n exportPrefix = true;\n rest = rest.slice('export '.length).replace(/^[ \\t]*/, '');\n }\n\n const eqIdx = rest.indexOf('=');\n if (eqIdx < 0) return null;\n\n const key = rest.slice(0, eqIdx).trimEnd();\n if (!KEY_RE.test(key)) return null;\n\n const after = rest.slice(eqIdx + 1);\n const valueStart = after.replace(/^[ \\t]*/, '');\n const valueLeadingWs = after.slice(0, after.length - valueStart.length);\n\n const parsed = parseValue(valueStart);\n if (!parsed) return null;\n\n // Preserve trailing whitespace before any inline comment so round-trip\n // matches the source exactly (we don't need to model it separately — `raw`\n // is what gets emitted).\n void leadingWs;\n void valueLeadingWs;\n\n return {\n kind: 'kv',\n key,\n rawValue: parsed.rawValue,\n value: parsed.value,\n quoting: parsed.quoting,\n exportPrefix,\n inlineComment: parsed.inlineComment,\n raw\n };\n}\n\ninterface ParsedValue {\n rawValue: string;\n value: string;\n quoting: Quoting;\n inlineComment: string;\n}\n\nfunction parseValue(input: string): ParsedValue | null {\n if (input.length === 0) {\n return { rawValue: '', value: '', quoting: 'none', inlineComment: '' };\n }\n\n const first = input[0];\n if (first === '\"' || first === \"'\") {\n const close = findClosingQuote(input, first);\n if (close < 0) return null;\n const rawValue = input.slice(1, close);\n const value = first === '\"' ? decodeDoubleQuoted(rawValue) : rawValue;\n const tail = input.slice(close + 1);\n const inlineComment = extractInlineComment(tail);\n return {\n rawValue,\n value,\n quoting: first === '\"' ? 'double' : 'single',\n inlineComment\n };\n }\n\n // Unquoted: value runs until ` #` or end-of-line, trailing whitespace\n // belongs to the inline-comment slot (so round-trip is preserved via `raw`).\n const hashIdx = findUnquotedCommentStart(input);\n const valuePart = (hashIdx < 0 ? input : input.slice(0, hashIdx)).trimEnd();\n const inlineComment = hashIdx < 0 ? '' : input.slice(valuePart.length);\n return {\n rawValue: valuePart,\n value: valuePart,\n quoting: 'none',\n inlineComment\n };\n}\n\nfunction findClosingQuote(input: string, quote: '\"' | \"'\"): number {\n for (let i = 1; i < input.length; i++) {\n const ch = input[i];\n if (quote === '\"' && ch === '\\\\') {\n i++;\n continue;\n }\n if (ch === quote) return i;\n }\n return -1;\n}\n\nfunction findUnquotedCommentStart(input: string): number {\n for (let i = 0; i < input.length; i++) {\n if (input[i] !== '#') continue;\n if (i === 0) return i;\n const prev = input[i - 1];\n if (prev === ' ' || prev === '\\t') return i;\n }\n return -1;\n}\n\nfunction extractInlineComment(tail: string): string {\n // Tail is everything after the closing quote. It is either empty, pure\n // whitespace, or whitespace + `#...`. Preserve it verbatim.\n return tail;\n}\n\nfunction decodeDoubleQuoted(raw: string): string {\n return raw.replace(/\\\\(.)/g, (_, ch: string) => {\n switch (ch) {\n case 'n':\n return '\\n';\n case 'r':\n return '\\r';\n case 't':\n return '\\t';\n case '\\\\':\n return '\\\\';\n case '\"':\n return '\"';\n default:\n return `\\\\${ch}`;\n }\n });\n}\n","import { readdir, readFile, stat } from 'node:fs/promises';\nimport { basename, join } from 'pathe';\nimport { parseEnv } from './parse.ts';\nimport type { EnvFile } from './types.ts';\n\nconst SKIP_SUFFIXES = ['.swp', '~', '.bak'];\n\n/**\n * Discover `.env*` files in the given path(s). Each path may be a directory\n * (glob-like discovery happens at its top level) or an explicit file. Editor\n * and backup files are skipped.\n *\n * Files are returned sorted: `.env.example` first if present, then the rest\n * alphabetically. Base-resolution lives in `base.ts` and depends on this order.\n */\nexport async function discoverEnvFiles(paths: string[]): Promise<EnvFile[]> {\n const filePaths = new Set<string>();\n\n for (const p of paths) {\n const info = await stat(p);\n if (info.isDirectory()) {\n const entries = await readdir(p);\n for (const name of entries) {\n if (!looksLikeEnvFile(name)) continue;\n filePaths.add(join(p, name));\n }\n } else {\n filePaths.add(p);\n }\n }\n\n const sorted = [...filePaths].sort(envPathOrder);\n const files: EnvFile[] = [];\n for (const filePath of sorted) {\n const source = await readFile(filePath, 'utf8');\n files.push(parseEnv(source, filePath));\n }\n return files;\n}\n\nfunction looksLikeEnvFile(name: string): boolean {\n if (!name.startsWith('.env')) return false;\n if (SKIP_SUFFIXES.some((s) => name.endsWith(s))) return false;\n return true;\n}\n\nfunction envPathOrder(a: string, b: string): number {\n const an = basename(a);\n const bn = basename(b);\n if (an === '.env.example') return -1;\n if (bn === '.env.example') return 1;\n return an.localeCompare(bn);\n}\n","import type { EnvEntry, EnvFile } from './types.ts';\n\nconst SEP_CHARS = new Set(['=', '-', '#', '~', '*']);\nconst SPACE_CHARS = new Set([' ', '\\t']);\n\n/**\n * Walk the base file in source order and decide which section each kv entry\n * belongs to. Returns a `key → section` map; keys with no inferred section\n * are absent from the map.\n *\n * A \"section header\" is one of:\n * 1. An inline banner: `# === Section name ===` (or `---`, `~~~`, `***`).\n * 2. A block banner — a single comment line `# Section name` whose\n * immediately preceding or following comment line is purely decorative\n * (e.g. `# ===========================`).\n *\n * The detected name applies to every subsequent kv entry until the next\n * detected section header.\n */\nexport function computeSections(base: EnvFile): Map<string, string> {\n const out = new Map<string, string>();\n let current: string | null = null;\n for (let i = 0; i < base.entries.length; i++) {\n const name = detectSectionName(base.entries, i);\n if (name !== null) current = name;\n const e = base.entries[i]!;\n if (e.kind === 'kv' && current) out.set(e.key, current);\n }\n return out;\n}\n\nfunction detectSectionName(entries: EnvEntry[], idx: number): string | null {\n const e = entries[idx];\n if (!e || e.kind !== 'comment') return null;\n if (isDecorativeLine(e.raw)) return null;\n\n const inline = parseInlineBanner(e.raw);\n if (inline !== null) return inline;\n\n const text = stripCommentPrefix(e.raw);\n if (!text) return null;\n\n if (isDecorative(entries[idx - 1]) || isDecorative(entries[idx + 1])) {\n return text;\n }\n return null;\n}\n\nfunction isDecorative(e: EnvEntry | undefined): boolean {\n if (!e || e.kind !== 'comment') return false;\n return isDecorativeLine(e.raw);\n}\n\n/**\n * True for comment lines whose body is nothing but separator chars and\n * whitespace (e.g. \"# ======\", \"###\", \"# -=-=-=-\").\n *\n * Implemented with a manual scan rather than a regex with overlapping\n * `\\s*` / `[\\s…]+` groups, which CodeQL flags as ReDoS-prone.\n */\nfunction isDecorativeLine(raw: string): boolean {\n const start = skipSpaces(raw, 0);\n if (start >= raw.length || raw[start] !== '#') return false;\n let i = start + 1;\n let sawSeparator = false;\n while (i < raw.length) {\n const ch = raw[i]!;\n if (SEP_CHARS.has(ch)) sawSeparator = true;\n else if (!SPACE_CHARS.has(ch)) return false;\n i++;\n }\n return sawSeparator;\n}\n\n/**\n * Parse a single-line inline banner like `# === Name ===` (also `---`,\n * `~~~`, `***`) and return the inner name, or null if the line isn't an\n * inline banner.\n *\n * Manual scan, again to avoid the overlapping `\\s*` regex backtracking\n * CodeQL warned about.\n */\nfunction parseInlineBanner(raw: string): string | null {\n const start = skipSpaces(raw, 0);\n if (start >= raw.length || raw[start] !== '#') return null;\n\n // After '#', skip spaces, then read 2+ separator chars (single char repeated).\n let i = skipSpaces(raw, start + 1);\n if (i >= raw.length || !SEP_CHARS.has(raw[i]!)) return null;\n let leadCount = 0;\n while (i < raw.length && SEP_CHARS.has(raw[i]!)) {\n leadCount++;\n i++;\n }\n if (leadCount < 2) return null;\n\n // Walk from the end inward to find the trailing separator run.\n let j = raw.length;\n while (j > i && SPACE_CHARS.has(raw[j - 1]!)) j--;\n if (j <= i || !SEP_CHARS.has(raw[j - 1]!)) return null;\n let trailCount = 0;\n while (j > i && SEP_CHARS.has(raw[j - 1]!)) {\n trailCount++;\n j--;\n }\n if (trailCount < 2) return null;\n\n // Between the two separator runs lives the name (after stripping spaces).\n const inner = raw.slice(i, j).trim();\n return inner.length > 0 ? inner : null;\n}\n\nfunction stripCommentPrefix(raw: string): string {\n // Leading whitespace, then one or more '#', then whitespace, then content.\n let i = skipSpaces(raw, 0);\n while (i < raw.length && raw[i] === '#') i++;\n i = skipSpaces(raw, i);\n // Trim trailing whitespace and decorative chars.\n let j = raw.length;\n while (\n j > i &&\n (SPACE_CHARS.has(raw[j - 1]!) || SEP_CHARS.has(raw[j - 1]!))\n ) {\n j--;\n }\n return raw.slice(i, j);\n}\n\nfunction skipSpaces(raw: string, from: number): number {\n let i = from;\n while (i < raw.length && SPACE_CHARS.has(raw[i]!)) i++;\n return i;\n}\n","import { computeSections } from './sections.ts';\nimport type { EnvFile, KvEntry } from './types.ts';\n\nexport type CellState = 'same' | 'differs' | 'missing' | 'extra' | 'base';\n\nexport interface Cell {\n state: CellState;\n /** Decoded value if present in this file, else `undefined`. */\n value: string | undefined;\n}\n\nexport interface Matrix {\n /** Union of all keys across all files, base keys first (in source order). */\n keys: string[];\n /** Files in display order; base is included. */\n files: EnvFile[];\n base: EnvFile;\n cell(key: string, file: EnvFile): Cell;\n /**\n * Section name inferred from the base file's comment banners, or\n * `undefined` for keys outside any section (and for keys only present in\n * non-base files).\n */\n sectionOf(key: string): string | undefined;\n}\n\nexport function buildMatrix(files: EnvFile[], base: EnvFile): Matrix {\n const keys = collectKeys(files, base);\n const lookups = new Map<EnvFile, Map<string, KvEntry>>();\n for (const file of files) {\n lookups.set(file, indexKv(file));\n }\n const baseIndex = lookups.get(base);\n if (!baseIndex) {\n throw new Error('base file is not in the files list');\n }\n const sections = computeSections(base);\n\n return {\n keys,\n files,\n base,\n sectionOf(key) {\n return sections.get(key);\n },\n cell(key, file) {\n const ownIndex = lookups.get(file);\n if (!ownIndex) {\n throw new Error(`file not in matrix: ${file.path}`);\n }\n const own = ownIndex.get(key);\n const baseEntry = baseIndex.get(key);\n\n if (file === base) {\n return {\n state: own ? 'base' : 'missing',\n value: own?.value\n };\n }\n\n if (!own && !baseEntry) {\n return { state: 'missing', value: undefined };\n }\n if (!own) {\n return { state: 'missing', value: undefined };\n }\n if (!baseEntry) {\n return { state: 'extra', value: own.value };\n }\n return {\n state: own.value === baseEntry.value ? 'same' : 'differs',\n value: own.value\n };\n }\n };\n}\n\nfunction collectKeys(files: EnvFile[], base: EnvFile): string[] {\n const seen = new Set<string>();\n const out: string[] = [];\n\n // Base file keys first in their authored order so the matrix view tracks\n // the .env.example layout most users curate.\n for (const e of base.entries) {\n if (e.kind === 'kv' && !seen.has(e.key)) {\n seen.add(e.key);\n out.push(e.key);\n }\n }\n // Then any extras only present in non-base files, in alpha order so the\n // result is deterministic.\n const extras = new Set<string>();\n for (const file of files) {\n if (file === base) continue;\n for (const e of file.entries) {\n if (e.kind === 'kv' && !seen.has(e.key)) {\n extras.add(e.key);\n }\n }\n }\n for (const k of [...extras].sort()) out.push(k);\n return out;\n}\n\nfunction indexKv(file: EnvFile): Map<string, KvEntry> {\n const m = new Map<string, KvEntry>();\n for (const e of file.entries) {\n if (e.kind === 'kv') m.set(e.key, e);\n }\n return m;\n}\n"],"names":["inlineComment"],"mappings":"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"}
package/dist/index.mjs ADDED
@@ -0,0 +1,16 @@
1
+ import { b, c, a, d, f, p, r } from "./chunks/matrix-nBq_GY2v.mjs";
2
+ import { i, m, r as r2, s } from "./chunks/mask-Bv9W6Ei8.mjs";
3
+ export {
4
+ b as buildMatrix,
5
+ c as computeDiff,
6
+ a as computeSections,
7
+ d as discoverEnvFiles,
8
+ f as formatDiffText,
9
+ i as isSecretKey,
10
+ m as maskValue,
11
+ p as parseEnv,
12
+ r2 as rebuildKvLine,
13
+ r as resolveBase,
14
+ s as serializeEnv
15
+ };
16
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","sources":[],"sourcesContent":[],"names":[],"mappings":";;"}
package/package.json ADDED
@@ -0,0 +1,88 @@
1
+ {
2
+ "$schema": "https://www.schemastore.org/package.json",
3
+ "name": "envprism",
4
+ "version": "0.0.0",
5
+ "description": "TUI-based env file manager — refract one set of variables into many environment views.",
6
+ "keywords": [
7
+ "bun",
8
+ "cli",
9
+ "commitlint",
10
+ "conventional-commits",
11
+ "dependabot",
12
+ "dotenv",
13
+ "env",
14
+ "husky",
15
+ "kirchdev",
16
+ "opentui",
17
+ "oxfmt",
18
+ "oxlint",
19
+ "pnpm",
20
+ "release-please",
21
+ "tui"
22
+ ],
23
+ "homepage": "https://github.com/TitusKirch/envprism#readme",
24
+ "bugs": {
25
+ "url": "https://github.com/TitusKirch/envprism/issues"
26
+ },
27
+ "license": "MIT",
28
+ "author": "Titus Kirch <titus.kirch@kirch.dev>",
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "git+https://github.com/TitusKirch/envprism.git"
32
+ },
33
+ "bin": {
34
+ "envprism": "./dist/bin/envprism.mjs"
35
+ },
36
+ "files": [
37
+ "dist"
38
+ ],
39
+ "type": "module",
40
+ "exports": {
41
+ ".": {
42
+ "types": "./dist/index.d.ts",
43
+ "import": "./dist/index.mjs"
44
+ }
45
+ },
46
+ "publishConfig": {
47
+ "access": "public",
48
+ "provenance": true
49
+ },
50
+ "dependencies": {
51
+ "@opentui/core": "^0.2.15",
52
+ "citty": "^0.1.6",
53
+ "consola": "^3.4.0",
54
+ "pathe": "^2.0.0"
55
+ },
56
+ "devDependencies": {
57
+ "@commitlint/cli": "^21.0.1",
58
+ "@commitlint/config-conventional": "^21.0.1",
59
+ "@types/node": "^24.0.0",
60
+ "@vitest/coverage-v8": "^3.2.4",
61
+ "husky": "^9.1.7",
62
+ "lint-staged": "^17.0.5",
63
+ "oxfmt": "0.51.0",
64
+ "oxlint": "1.66.0",
65
+ "taze": "^19.13.0",
66
+ "typescript": "^5.6.0",
67
+ "vite": "^6.0.0",
68
+ "vitest": "^3.0.0"
69
+ },
70
+ "engines": {
71
+ "bun": ">=1.3"
72
+ },
73
+ "scripts": {
74
+ "build": "vite build",
75
+ "dev": "vite build --watch",
76
+ "lint": "oxlint . --deny-warnings",
77
+ "lint:fix": "oxlint . --fix --deny-warnings",
78
+ "format": "oxfmt --check .",
79
+ "format:fix": "oxfmt .",
80
+ "typecheck": "tsc --noEmit",
81
+ "test": "vitest run --passWithNoTests",
82
+ "test:watch": "vitest",
83
+ "check": "pnpm lint && pnpm format && pnpm typecheck",
84
+ "check:fix": "pnpm lint:fix && pnpm format:fix",
85
+ "taze": "taze",
86
+ "taze:w": "taze -w"
87
+ }
88
+ }