@olwiba/dx 0.0.27 → 0.0.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +4 -3
- package/dist/env-check.js +4 -3
- package/dist/env-check.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -2402,13 +2402,14 @@ function checkEnv({
|
|
|
2402
2402
|
const findings = [];
|
|
2403
2403
|
let okCount = 0;
|
|
2404
2404
|
for (const key of exampleKeys) {
|
|
2405
|
+
const exampleDeclaresValue = exampleEnv.keys.get(key) === true;
|
|
2406
|
+
const isOptional = optionalSet.has(key) || !exampleDeclaresValue;
|
|
2405
2407
|
if (!actualEnv.keys.has(key)) {
|
|
2406
|
-
if (!
|
|
2408
|
+
if (!isOptional) findings.push({ kind: "missing", key });
|
|
2407
2409
|
continue;
|
|
2408
2410
|
}
|
|
2409
2411
|
const hasValue = actualEnv.keys.get(key) === true;
|
|
2410
|
-
|
|
2411
|
-
if (!hasValue && exampleHasValue && !optionalSet.has(key)) {
|
|
2412
|
+
if (!hasValue && !isOptional) {
|
|
2412
2413
|
findings.push({ kind: "empty", key });
|
|
2413
2414
|
continue;
|
|
2414
2415
|
}
|
package/dist/env-check.js
CHANGED
|
@@ -41,13 +41,14 @@ function checkEnv({
|
|
|
41
41
|
const findings = [];
|
|
42
42
|
let okCount = 0;
|
|
43
43
|
for (const key of exampleKeys) {
|
|
44
|
+
const exampleDeclaresValue = exampleEnv.keys.get(key) === true;
|
|
45
|
+
const isOptional = optionalSet.has(key) || !exampleDeclaresValue;
|
|
44
46
|
if (!actualEnv.keys.has(key)) {
|
|
45
|
-
if (!
|
|
47
|
+
if (!isOptional) findings.push({ kind: "missing", key });
|
|
46
48
|
continue;
|
|
47
49
|
}
|
|
48
50
|
const hasValue = actualEnv.keys.get(key) === true;
|
|
49
|
-
|
|
50
|
-
if (!hasValue && exampleHasValue && !optionalSet.has(key)) {
|
|
51
|
+
if (!hasValue && !isOptional) {
|
|
51
52
|
findings.push({ kind: "empty", key });
|
|
52
53
|
continue;
|
|
53
54
|
}
|
package/dist/env-check.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/env-check.ts"],"names":[],"mappings":"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","file":"env-check.js","sourcesContent":["/**\n * Compares a real environment against the example that documents it.\n *\n * Pure string comparison, deliberately. The input is a file full of live\n * credentials, so it is never sent anywhere, never written to disk, and never\n * echoed back — the report names keys and says nothing about values.\n *\n * `.env.example` is the schema. Every repository already has one, which is what\n * makes this usable outside the repositories that have a typed schema, and\n * those are exactly the ones where environments have been rotting unnoticed.\n */\n\nexport type EnvFinding =\n | { kind: 'missing'; key: string }\n | { kind: 'unknown'; key: string }\n | { kind: 'renamed'; key: string; looksLike: string }\n | { kind: 'empty'; key: string }\n | { kind: 'malformed'; line: number; text: string }\n | { kind: 'duplicate'; key: string };\n\nexport interface EnvCheckResult {\n findings: EnvFinding[];\n /** Keys present in both, non-empty. Counted, never listed. */\n okCount: number;\n exampleCount: number;\n actualCount: number;\n}\n\ninterface ParsedEnv {\n /** Key to whether it has a non-empty value. Values themselves are discarded. */\n keys: Map<string, boolean>;\n duplicates: string[];\n malformed: Array<{ line: number; text: string }>;\n}\n\n/**\n * Reads keys and discards values immediately.\n *\n * The value never leaves this function, so nothing downstream can leak one by\n * accident — including a future change to the report format.\n */\nexport function parseEnv(source: string): ParsedEnv {\n const keys = new Map<string, boolean>();\n const duplicates: string[] = [];\n const malformed: Array<{ line: number; text: string }> = [];\n\n source.split(/\\r?\\n/).forEach((raw, index) => {\n const line = raw.trim();\n if (line === '' || line.startsWith('#')) return;\n\n // `export FOO=bar` is valid in a shell-sourced file.\n const withoutExport = line.replace(/^export\\s+/, '');\n const eq = withoutExport.indexOf('=');\n\n if (eq <= 0) {\n // Reported by line number without the text where it could hold a value:\n // a line missing its `=` may still be a pasted secret.\n malformed.push({ line: index + 1, text: withoutExport.slice(0, 24) });\n return;\n }\n\n const key = withoutExport.slice(0, eq).trim();\n const value = withoutExport.slice(eq + 1).trim();\n\n if (keys.has(key)) duplicates.push(key);\n // A later assignment wins in most loaders, so the last one decides.\n keys.set(key, value !== '' && value !== '\"\"' && value !== \"''\");\n });\n\n return { keys, duplicates, malformed };\n}\n\n/**\n * Whether an unrecognised key looks like a renamed version of a known one.\n *\n * This is the finding that matters. A stale key reads as configured — someone\n * scanning the file sees the name they expect in all but a prefix and stops\n * looking — so reporting it as a generic unknown would not tell anyone that the\n * setting it was meant to carry is unset.\n */\nfunction findRenameCandidate(unknownKey: string, knownKeys: string[]): string | undefined {\n const normalise = (key: string) => key.replace(/[^A-Z0-9]/gi, '').toUpperCase();\n const target = normalise(unknownKey);\n\n return knownKeys.find((known) => {\n const candidate = normalise(known);\n if (candidate === target) return true;\n // A prefix or suffix on an otherwise identical name: VITE_FOO against FOO.\n return (\n candidate.length >= 6 &&\n (target.endsWith(candidate) || target.startsWith(candidate))\n );\n });\n}\n\nexport function checkEnv({\n example,\n actual,\n /** Keys allowed to be absent, e.g. optional credentials. */\n optional = [],\n}: {\n example: string;\n actual: string;\n optional?: string[];\n}): EnvCheckResult {\n const exampleEnv = parseEnv(example);\n const actualEnv = parseEnv(actual);\n const optionalSet = new Set(optional);\n\n const exampleKeys = [...exampleEnv.keys.keys()];\n const findings: EnvFinding[] = [];\n let okCount = 0;\n\n for (const key of exampleKeys) {\n if (!actualEnv.keys.has(key)) {\n if (!optionalSet.has(key)) findings.push({ kind: 'missing', key });\n continue;\n }\n\n const hasValue = actualEnv.keys.get(key) === true;\n const exampleHasValue = exampleEnv.keys.get(key) === true;\n\n // Blank where the example shows a value: the example is demonstrating that\n // something belongs there. Blank in both is a deliberate opt-out.\n if (!hasValue && exampleHasValue && !optionalSet.has(key)) {\n findings.push({ kind: 'empty', key });\n continue;\n }\n\n okCount += 1;\n }\n\n for (const key of actualEnv.keys.keys()) {\n if (exampleEnv.keys.has(key)) continue;\n\n const looksLike = findRenameCandidate(key, exampleKeys);\n findings.push(looksLike ? { kind: 'renamed', key, looksLike } : { kind: 'unknown', key });\n }\n\n for (const key of actualEnv.duplicates) findings.push({ kind: 'duplicate', key });\n for (const line of actualEnv.malformed) findings.push({ kind: 'malformed', ...line });\n\n return {\n findings,\n okCount,\n exampleCount: exampleEnv.keys.size,\n actualCount: actualEnv.keys.size,\n };\n}\n\nconst ORDER: EnvFinding['kind'][] = [\n 'renamed',\n 'missing',\n 'empty',\n 'malformed',\n 'duplicate',\n 'unknown',\n];\n\nconst LABELS: Record<EnvFinding['kind'], string> = {\n renamed: 'Renamed — set under a name nothing reads',\n missing: 'Missing',\n empty: 'Present but empty',\n malformed: 'Malformed line',\n duplicate: 'Set more than once',\n unknown: 'Unknown — not in the example',\n};\n\n/** Formats a report. Contains key names and counts only, never a value. */\nexport function formatEnvReport(result: EnvCheckResult): string {\n const lines: string[] = [];\n lines.push(\n ` ${result.actualCount} keys checked against ${result.exampleCount} in the example, ${result.okCount} correct`,\n );\n\n if (result.findings.length === 0) {\n lines.push('\\nPASS — environment matches the example.');\n return lines.join('\\n');\n }\n\n for (const kind of ORDER) {\n const group = result.findings.filter((finding) => finding.kind === kind);\n if (group.length === 0) continue;\n\n lines.push(`\\n${LABELS[kind]}:`);\n for (const finding of group) {\n if (finding.kind === 'renamed') {\n lines.push(` ✗ ${finding.key} → did you mean ${finding.looksLike}?`);\n } else if (finding.kind === 'malformed') {\n lines.push(` ✗ line ${finding.line}: ${finding.text}…`);\n } else {\n lines.push(` ✗ ${finding.key}`);\n }\n }\n }\n\n // Renames are called out because they are the ones that read as configured.\n const renamed = result.findings.filter((finding) => finding.kind === 'renamed').length;\n if (renamed > 0) {\n lines.push(\n `\\n${renamed} key(s) look renamed. The setting they were meant to carry is unset,` +\n '\\nwhich usually means a default is in force that nobody chose.',\n );\n }\n\n lines.push(`\\nFAIL — ${result.findings.length} finding(s).`);\n return lines.join('\\n');\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/env-check.ts"],"names":[],"mappings":"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","file":"env-check.js","sourcesContent":["/**\n * Compares a real environment against the example that documents it.\n *\n * Pure string comparison, deliberately. The input is a file full of live\n * credentials, so it is never sent anywhere, never written to disk, and never\n * echoed back — the report names keys and says nothing about values.\n *\n * `.env.example` is the schema. Every repository already has one, which is what\n * makes this usable outside the repositories that have a typed schema, and\n * those are exactly the ones where environments have been rotting unnoticed.\n */\n\nexport type EnvFinding =\n | { kind: 'missing'; key: string }\n | { kind: 'unknown'; key: string }\n | { kind: 'renamed'; key: string; looksLike: string }\n | { kind: 'empty'; key: string }\n | { kind: 'malformed'; line: number; text: string }\n | { kind: 'duplicate'; key: string };\n\nexport interface EnvCheckResult {\n findings: EnvFinding[];\n /** Keys present in both, non-empty. Counted, never listed. */\n okCount: number;\n exampleCount: number;\n actualCount: number;\n}\n\ninterface ParsedEnv {\n /** Key to whether it has a non-empty value. Values themselves are discarded. */\n keys: Map<string, boolean>;\n duplicates: string[];\n malformed: Array<{ line: number; text: string }>;\n}\n\n/**\n * Reads keys and discards values immediately.\n *\n * The value never leaves this function, so nothing downstream can leak one by\n * accident — including a future change to the report format.\n */\nexport function parseEnv(source: string): ParsedEnv {\n const keys = new Map<string, boolean>();\n const duplicates: string[] = [];\n const malformed: Array<{ line: number; text: string }> = [];\n\n source.split(/\\r?\\n/).forEach((raw, index) => {\n const line = raw.trim();\n if (line === '' || line.startsWith('#')) return;\n\n // `export FOO=bar` is valid in a shell-sourced file.\n const withoutExport = line.replace(/^export\\s+/, '');\n const eq = withoutExport.indexOf('=');\n\n if (eq <= 0) {\n // Reported by line number without the text where it could hold a value:\n // a line missing its `=` may still be a pasted secret.\n malformed.push({ line: index + 1, text: withoutExport.slice(0, 24) });\n return;\n }\n\n const key = withoutExport.slice(0, eq).trim();\n const value = withoutExport.slice(eq + 1).trim();\n\n if (keys.has(key)) duplicates.push(key);\n // A later assignment wins in most loaders, so the last one decides.\n keys.set(key, value !== '' && value !== '\"\"' && value !== \"''\");\n });\n\n return { keys, duplicates, malformed };\n}\n\n/**\n * Whether an unrecognised key looks like a renamed version of a known one.\n *\n * This is the finding that matters. A stale key reads as configured — someone\n * scanning the file sees the name they expect in all but a prefix and stops\n * looking — so reporting it as a generic unknown would not tell anyone that the\n * setting it was meant to carry is unset.\n */\nfunction findRenameCandidate(unknownKey: string, knownKeys: string[]): string | undefined {\n const normalise = (key: string) => key.replace(/[^A-Z0-9]/gi, '').toUpperCase();\n const target = normalise(unknownKey);\n\n return knownKeys.find((known) => {\n const candidate = normalise(known);\n if (candidate === target) return true;\n // A prefix or suffix on an otherwise identical name: VITE_FOO against FOO.\n return (\n candidate.length >= 6 &&\n (target.endsWith(candidate) || target.startsWith(candidate))\n );\n });\n}\n\nexport function checkEnv({\n example,\n actual,\n /** Keys allowed to be absent, e.g. optional credentials. */\n optional = [],\n}: {\n example: string;\n actual: string;\n optional?: string[];\n}): EnvCheckResult {\n const exampleEnv = parseEnv(example);\n const actualEnv = parseEnv(actual);\n const optionalSet = new Set(optional);\n\n const exampleKeys = [...exampleEnv.keys.keys()];\n const findings: EnvFinding[] = [];\n let okCount = 0;\n\n for (const key of exampleKeys) {\n // A blank in the example is the example saying this slot is optional — it\n // is showing the name without claiming a value belongs there. Absent then\n // means the same as blank, and reporting it drowns the real findings in\n // unset credentials for services this deployment does not use.\n const exampleDeclaresValue = exampleEnv.keys.get(key) === true;\n const isOptional = optionalSet.has(key) || !exampleDeclaresValue;\n\n if (!actualEnv.keys.has(key)) {\n if (!isOptional) findings.push({ kind: 'missing', key });\n continue;\n }\n\n const hasValue = actualEnv.keys.get(key) === true;\n\n // Blank where the example shows a value: the example is demonstrating that\n // something belongs there. Blank in both is a deliberate opt-out.\n if (!hasValue && !isOptional) {\n findings.push({ kind: 'empty', key });\n continue;\n }\n\n okCount += 1;\n }\n\n for (const key of actualEnv.keys.keys()) {\n if (exampleEnv.keys.has(key)) continue;\n\n const looksLike = findRenameCandidate(key, exampleKeys);\n findings.push(looksLike ? { kind: 'renamed', key, looksLike } : { kind: 'unknown', key });\n }\n\n for (const key of actualEnv.duplicates) findings.push({ kind: 'duplicate', key });\n for (const line of actualEnv.malformed) findings.push({ kind: 'malformed', ...line });\n\n return {\n findings,\n okCount,\n exampleCount: exampleEnv.keys.size,\n actualCount: actualEnv.keys.size,\n };\n}\n\nconst ORDER: EnvFinding['kind'][] = [\n 'renamed',\n 'missing',\n 'empty',\n 'malformed',\n 'duplicate',\n 'unknown',\n];\n\nconst LABELS: Record<EnvFinding['kind'], string> = {\n renamed: 'Renamed — set under a name nothing reads',\n missing: 'Missing',\n empty: 'Present but empty',\n malformed: 'Malformed line',\n duplicate: 'Set more than once',\n unknown: 'Unknown — not in the example',\n};\n\n/** Formats a report. Contains key names and counts only, never a value. */\nexport function formatEnvReport(result: EnvCheckResult): string {\n const lines: string[] = [];\n lines.push(\n ` ${result.actualCount} keys checked against ${result.exampleCount} in the example, ${result.okCount} correct`,\n );\n\n if (result.findings.length === 0) {\n lines.push('\\nPASS — environment matches the example.');\n return lines.join('\\n');\n }\n\n for (const kind of ORDER) {\n const group = result.findings.filter((finding) => finding.kind === kind);\n if (group.length === 0) continue;\n\n lines.push(`\\n${LABELS[kind]}:`);\n for (const finding of group) {\n if (finding.kind === 'renamed') {\n lines.push(` ✗ ${finding.key} → did you mean ${finding.looksLike}?`);\n } else if (finding.kind === 'malformed') {\n lines.push(` ✗ line ${finding.line}: ${finding.text}…`);\n } else {\n lines.push(` ✗ ${finding.key}`);\n }\n }\n }\n\n // Renames are called out because they are the ones that read as configured.\n const renamed = result.findings.filter((finding) => finding.kind === 'renamed').length;\n if (renamed > 0) {\n lines.push(\n `\\n${renamed} key(s) look renamed. The setting they were meant to carry is unset,` +\n '\\nwhich usually means a default is in force that nobody chose.',\n );\n }\n\n lines.push(`\\nFAIL — ${result.findings.length} finding(s).`);\n return lines.join('\\n');\n}\n"]}
|