@olwiba/dx 0.0.24 → 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 +181 -0
- package/dist/env-check.d.ts +66 -0
- package/dist/env-check.js +126 -0
- package/dist/env-check.js.map +1 -0
- package/package.json +5 -1
package/dist/cli.js
CHANGED
|
@@ -2353,12 +2353,147 @@ var init_generate_assets = __esm({
|
|
|
2353
2353
|
}
|
|
2354
2354
|
});
|
|
2355
2355
|
|
|
2356
|
+
// src/env-check.ts
|
|
2357
|
+
var env_check_exports = {};
|
|
2358
|
+
__export(env_check_exports, {
|
|
2359
|
+
checkEnv: () => checkEnv,
|
|
2360
|
+
formatEnvReport: () => formatEnvReport,
|
|
2361
|
+
parseEnv: () => parseEnv
|
|
2362
|
+
});
|
|
2363
|
+
function parseEnv(source) {
|
|
2364
|
+
const keys = /* @__PURE__ */ new Map();
|
|
2365
|
+
const duplicates = [];
|
|
2366
|
+
const malformed = [];
|
|
2367
|
+
source.split(/\r?\n/).forEach((raw, index) => {
|
|
2368
|
+
const line = raw.trim();
|
|
2369
|
+
if (line === "" || line.startsWith("#")) return;
|
|
2370
|
+
const withoutExport = line.replace(/^export\s+/, "");
|
|
2371
|
+
const eq = withoutExport.indexOf("=");
|
|
2372
|
+
if (eq <= 0) {
|
|
2373
|
+
malformed.push({ line: index + 1, text: withoutExport.slice(0, 24) });
|
|
2374
|
+
return;
|
|
2375
|
+
}
|
|
2376
|
+
const key = withoutExport.slice(0, eq).trim();
|
|
2377
|
+
const value = withoutExport.slice(eq + 1).trim();
|
|
2378
|
+
if (keys.has(key)) duplicates.push(key);
|
|
2379
|
+
keys.set(key, value !== "" && value !== '""' && value !== "''");
|
|
2380
|
+
});
|
|
2381
|
+
return { keys, duplicates, malformed };
|
|
2382
|
+
}
|
|
2383
|
+
function findRenameCandidate(unknownKey, knownKeys) {
|
|
2384
|
+
const normalise = (key) => key.replace(/[^A-Z0-9]/gi, "").toUpperCase();
|
|
2385
|
+
const target = normalise(unknownKey);
|
|
2386
|
+
return knownKeys.find((known) => {
|
|
2387
|
+
const candidate = normalise(known);
|
|
2388
|
+
if (candidate === target) return true;
|
|
2389
|
+
return candidate.length >= 6 && (target.endsWith(candidate) || target.startsWith(candidate));
|
|
2390
|
+
});
|
|
2391
|
+
}
|
|
2392
|
+
function checkEnv({
|
|
2393
|
+
example,
|
|
2394
|
+
actual,
|
|
2395
|
+
/** Keys allowed to be absent, e.g. optional credentials. */
|
|
2396
|
+
optional = []
|
|
2397
|
+
}) {
|
|
2398
|
+
const exampleEnv = parseEnv(example);
|
|
2399
|
+
const actualEnv = parseEnv(actual);
|
|
2400
|
+
const optionalSet = new Set(optional);
|
|
2401
|
+
const exampleKeys = [...exampleEnv.keys.keys()];
|
|
2402
|
+
const findings = [];
|
|
2403
|
+
let okCount = 0;
|
|
2404
|
+
for (const key of exampleKeys) {
|
|
2405
|
+
const exampleDeclaresValue = exampleEnv.keys.get(key) === true;
|
|
2406
|
+
const isOptional = optionalSet.has(key) || !exampleDeclaresValue;
|
|
2407
|
+
if (!actualEnv.keys.has(key)) {
|
|
2408
|
+
if (!isOptional) findings.push({ kind: "missing", key });
|
|
2409
|
+
continue;
|
|
2410
|
+
}
|
|
2411
|
+
const hasValue = actualEnv.keys.get(key) === true;
|
|
2412
|
+
if (!hasValue && !isOptional) {
|
|
2413
|
+
findings.push({ kind: "empty", key });
|
|
2414
|
+
continue;
|
|
2415
|
+
}
|
|
2416
|
+
okCount += 1;
|
|
2417
|
+
}
|
|
2418
|
+
for (const key of actualEnv.keys.keys()) {
|
|
2419
|
+
if (exampleEnv.keys.has(key)) continue;
|
|
2420
|
+
const looksLike = findRenameCandidate(key, exampleKeys);
|
|
2421
|
+
findings.push(looksLike ? { kind: "renamed", key, looksLike } : { kind: "unknown", key });
|
|
2422
|
+
}
|
|
2423
|
+
for (const key of actualEnv.duplicates) findings.push({ kind: "duplicate", key });
|
|
2424
|
+
for (const line of actualEnv.malformed) findings.push({ kind: "malformed", ...line });
|
|
2425
|
+
return {
|
|
2426
|
+
findings,
|
|
2427
|
+
okCount,
|
|
2428
|
+
exampleCount: exampleEnv.keys.size,
|
|
2429
|
+
actualCount: actualEnv.keys.size
|
|
2430
|
+
};
|
|
2431
|
+
}
|
|
2432
|
+
function formatEnvReport(result) {
|
|
2433
|
+
const lines = [];
|
|
2434
|
+
lines.push(
|
|
2435
|
+
` ${result.actualCount} keys checked against ${result.exampleCount} in the example, ${result.okCount} correct`
|
|
2436
|
+
);
|
|
2437
|
+
if (result.findings.length === 0) {
|
|
2438
|
+
lines.push("\nPASS \u2014 environment matches the example.");
|
|
2439
|
+
return lines.join("\n");
|
|
2440
|
+
}
|
|
2441
|
+
for (const kind of ORDER) {
|
|
2442
|
+
const group = result.findings.filter((finding) => finding.kind === kind);
|
|
2443
|
+
if (group.length === 0) continue;
|
|
2444
|
+
lines.push(`
|
|
2445
|
+
${LABELS[kind]}:`);
|
|
2446
|
+
for (const finding of group) {
|
|
2447
|
+
if (finding.kind === "renamed") {
|
|
2448
|
+
lines.push(` \u2717 ${finding.key} \u2192 did you mean ${finding.looksLike}?`);
|
|
2449
|
+
} else if (finding.kind === "malformed") {
|
|
2450
|
+
lines.push(` \u2717 line ${finding.line}: ${finding.text}\u2026`);
|
|
2451
|
+
} else {
|
|
2452
|
+
lines.push(` \u2717 ${finding.key}`);
|
|
2453
|
+
}
|
|
2454
|
+
}
|
|
2455
|
+
}
|
|
2456
|
+
const renamed = result.findings.filter((finding) => finding.kind === "renamed").length;
|
|
2457
|
+
if (renamed > 0) {
|
|
2458
|
+
lines.push(
|
|
2459
|
+
`
|
|
2460
|
+
${renamed} key(s) look renamed. The setting they were meant to carry is unset,
|
|
2461
|
+
which usually means a default is in force that nobody chose.`
|
|
2462
|
+
);
|
|
2463
|
+
}
|
|
2464
|
+
lines.push(`
|
|
2465
|
+
FAIL \u2014 ${result.findings.length} finding(s).`);
|
|
2466
|
+
return lines.join("\n");
|
|
2467
|
+
}
|
|
2468
|
+
var ORDER, LABELS;
|
|
2469
|
+
var init_env_check = __esm({
|
|
2470
|
+
"src/env-check.ts"() {
|
|
2471
|
+
ORDER = [
|
|
2472
|
+
"renamed",
|
|
2473
|
+
"missing",
|
|
2474
|
+
"empty",
|
|
2475
|
+
"malformed",
|
|
2476
|
+
"duplicate",
|
|
2477
|
+
"unknown"
|
|
2478
|
+
];
|
|
2479
|
+
LABELS = {
|
|
2480
|
+
renamed: "Renamed \u2014 set under a name nothing reads",
|
|
2481
|
+
missing: "Missing",
|
|
2482
|
+
empty: "Present but empty",
|
|
2483
|
+
malformed: "Malformed line",
|
|
2484
|
+
duplicate: "Set more than once",
|
|
2485
|
+
unknown: "Unknown \u2014 not in the example"
|
|
2486
|
+
};
|
|
2487
|
+
}
|
|
2488
|
+
});
|
|
2489
|
+
|
|
2356
2490
|
// src/skills.ts
|
|
2357
2491
|
function isSafeSkillSlug(slug) {
|
|
2358
2492
|
return slug !== "." && slug !== ".." && /^[A-Za-z0-9._-]+$/.test(slug);
|
|
2359
2493
|
}
|
|
2360
2494
|
|
|
2361
2495
|
// src/cli.ts
|
|
2496
|
+
var BREAK = "\n";
|
|
2362
2497
|
var DEFAULT_SOURCE = "https://olwiba.com/skills/manifest.json";
|
|
2363
2498
|
var [command, subcommand] = process.argv.slice(2);
|
|
2364
2499
|
if (command === "skills" && subcommand === "install") {
|
|
@@ -2376,6 +2511,8 @@ if (command === "skills" && subcommand === "install") {
|
|
|
2376
2511
|
await runAsciiGif();
|
|
2377
2512
|
} else if (command === "generate-assets") {
|
|
2378
2513
|
await runGenerateAssets();
|
|
2514
|
+
} else if (command === "env-check" || command === "env") {
|
|
2515
|
+
process.exitCode = await runEnvCheck();
|
|
2379
2516
|
} else {
|
|
2380
2517
|
process.stdout.write(
|
|
2381
2518
|
"Usage:\n dx skills install [--source <url>] [--target claude|amp] [--all] [--name a,b,c]\n dx worktree cleanup [repo-name-or-path] [--repos-root <path>] [--remote <name>] [--dry-run] [--force] [--no-fetch]\n dx ascii-gif --text <text> --out <file.gif>\n dx generate-assets --name <app> --icon <lucide-icon> --color <#hex> [--out <dir>] [--og-component <svg-or-image-path>]\n"
|
|
@@ -2597,3 +2734,47 @@ function parseNumberFlag(value) {
|
|
|
2597
2734
|
const parsed = Number(value);
|
|
2598
2735
|
return Number.isFinite(parsed) ? parsed : void 0;
|
|
2599
2736
|
}
|
|
2737
|
+
async function runEnvCheck() {
|
|
2738
|
+
const { readFileSync: readFileSync3, existsSync: existsSync2 } = await import('fs');
|
|
2739
|
+
const { checkEnv: checkEnv2, formatEnvReport: formatEnvReport2 } = await Promise.resolve().then(() => (init_env_check(), env_check_exports));
|
|
2740
|
+
const flags = parseFlags(process.argv.slice(3));
|
|
2741
|
+
const examplePath = flags.example ?? ".env.example";
|
|
2742
|
+
if (!existsSync2(examplePath)) {
|
|
2743
|
+
process.stderr.write(
|
|
2744
|
+
`No example file at ${examplePath}.${BREAK}It is the schema this compares against; pass --example to point elsewhere.` + BREAK
|
|
2745
|
+
);
|
|
2746
|
+
return 1;
|
|
2747
|
+
}
|
|
2748
|
+
const example = readFileSync3(examplePath, "utf8");
|
|
2749
|
+
let actual;
|
|
2750
|
+
if (flags.file) {
|
|
2751
|
+
if (!existsSync2(flags.file)) {
|
|
2752
|
+
process.stderr.write(`No environment file at ${flags.file}.${BREAK}`);
|
|
2753
|
+
return 1;
|
|
2754
|
+
}
|
|
2755
|
+
actual = readFileSync3(flags.file, "utf8");
|
|
2756
|
+
} else {
|
|
2757
|
+
if (stdin.isTTY) {
|
|
2758
|
+
process.stdout.write(
|
|
2759
|
+
`Paste the environment below, then press ${process.platform === "win32" ? "Ctrl+Z and Enter" : "Ctrl+D"}.${BREAK}Nothing is stored or transmitted.` + BREAK + BREAK
|
|
2760
|
+
);
|
|
2761
|
+
}
|
|
2762
|
+
actual = await readAllStdin();
|
|
2763
|
+
if (actual.trim() === "") {
|
|
2764
|
+
process.stderr.write("Nothing to check. Pass --file <path> or paste an environment." + BREAK);
|
|
2765
|
+
return 1;
|
|
2766
|
+
}
|
|
2767
|
+
}
|
|
2768
|
+
const result = checkEnv2({
|
|
2769
|
+
example,
|
|
2770
|
+
actual,
|
|
2771
|
+
optional: flags.optional ? flags.optional.split(",").map((key) => key.trim()).filter(Boolean) : []
|
|
2772
|
+
});
|
|
2773
|
+
process.stdout.write(`${formatEnvReport2(result)}${BREAK}`);
|
|
2774
|
+
return result.findings.length > 0 ? 1 : 0;
|
|
2775
|
+
}
|
|
2776
|
+
async function readAllStdin() {
|
|
2777
|
+
const chunks = [];
|
|
2778
|
+
for await (const chunk of stdin) chunks.push(Buffer.from(chunk));
|
|
2779
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
2780
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compares a real environment against the example that documents it.
|
|
3
|
+
*
|
|
4
|
+
* Pure string comparison, deliberately. The input is a file full of live
|
|
5
|
+
* credentials, so it is never sent anywhere, never written to disk, and never
|
|
6
|
+
* echoed back — the report names keys and says nothing about values.
|
|
7
|
+
*
|
|
8
|
+
* `.env.example` is the schema. Every repository already has one, which is what
|
|
9
|
+
* makes this usable outside the repositories that have a typed schema, and
|
|
10
|
+
* those are exactly the ones where environments have been rotting unnoticed.
|
|
11
|
+
*/
|
|
12
|
+
type EnvFinding = {
|
|
13
|
+
kind: 'missing';
|
|
14
|
+
key: string;
|
|
15
|
+
} | {
|
|
16
|
+
kind: 'unknown';
|
|
17
|
+
key: string;
|
|
18
|
+
} | {
|
|
19
|
+
kind: 'renamed';
|
|
20
|
+
key: string;
|
|
21
|
+
looksLike: string;
|
|
22
|
+
} | {
|
|
23
|
+
kind: 'empty';
|
|
24
|
+
key: string;
|
|
25
|
+
} | {
|
|
26
|
+
kind: 'malformed';
|
|
27
|
+
line: number;
|
|
28
|
+
text: string;
|
|
29
|
+
} | {
|
|
30
|
+
kind: 'duplicate';
|
|
31
|
+
key: string;
|
|
32
|
+
};
|
|
33
|
+
interface EnvCheckResult {
|
|
34
|
+
findings: EnvFinding[];
|
|
35
|
+
/** Keys present in both, non-empty. Counted, never listed. */
|
|
36
|
+
okCount: number;
|
|
37
|
+
exampleCount: number;
|
|
38
|
+
actualCount: number;
|
|
39
|
+
}
|
|
40
|
+
interface ParsedEnv {
|
|
41
|
+
/** Key to whether it has a non-empty value. Values themselves are discarded. */
|
|
42
|
+
keys: Map<string, boolean>;
|
|
43
|
+
duplicates: string[];
|
|
44
|
+
malformed: Array<{
|
|
45
|
+
line: number;
|
|
46
|
+
text: string;
|
|
47
|
+
}>;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Reads keys and discards values immediately.
|
|
51
|
+
*
|
|
52
|
+
* The value never leaves this function, so nothing downstream can leak one by
|
|
53
|
+
* accident — including a future change to the report format.
|
|
54
|
+
*/
|
|
55
|
+
declare function parseEnv(source: string): ParsedEnv;
|
|
56
|
+
declare function checkEnv({ example, actual,
|
|
57
|
+
/** Keys allowed to be absent, e.g. optional credentials. */
|
|
58
|
+
optional, }: {
|
|
59
|
+
example: string;
|
|
60
|
+
actual: string;
|
|
61
|
+
optional?: string[];
|
|
62
|
+
}): EnvCheckResult;
|
|
63
|
+
/** Formats a report. Contains key names and counts only, never a value. */
|
|
64
|
+
declare function formatEnvReport(result: EnvCheckResult): string;
|
|
65
|
+
|
|
66
|
+
export { type EnvCheckResult, type EnvFinding, checkEnv, formatEnvReport, parseEnv };
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// src/env-check.ts
|
|
2
|
+
function parseEnv(source) {
|
|
3
|
+
const keys = /* @__PURE__ */ new Map();
|
|
4
|
+
const duplicates = [];
|
|
5
|
+
const malformed = [];
|
|
6
|
+
source.split(/\r?\n/).forEach((raw, index) => {
|
|
7
|
+
const line = raw.trim();
|
|
8
|
+
if (line === "" || line.startsWith("#")) return;
|
|
9
|
+
const withoutExport = line.replace(/^export\s+/, "");
|
|
10
|
+
const eq = withoutExport.indexOf("=");
|
|
11
|
+
if (eq <= 0) {
|
|
12
|
+
malformed.push({ line: index + 1, text: withoutExport.slice(0, 24) });
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
const key = withoutExport.slice(0, eq).trim();
|
|
16
|
+
const value = withoutExport.slice(eq + 1).trim();
|
|
17
|
+
if (keys.has(key)) duplicates.push(key);
|
|
18
|
+
keys.set(key, value !== "" && value !== '""' && value !== "''");
|
|
19
|
+
});
|
|
20
|
+
return { keys, duplicates, malformed };
|
|
21
|
+
}
|
|
22
|
+
function findRenameCandidate(unknownKey, knownKeys) {
|
|
23
|
+
const normalise = (key) => key.replace(/[^A-Z0-9]/gi, "").toUpperCase();
|
|
24
|
+
const target = normalise(unknownKey);
|
|
25
|
+
return knownKeys.find((known) => {
|
|
26
|
+
const candidate = normalise(known);
|
|
27
|
+
if (candidate === target) return true;
|
|
28
|
+
return candidate.length >= 6 && (target.endsWith(candidate) || target.startsWith(candidate));
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
function checkEnv({
|
|
32
|
+
example,
|
|
33
|
+
actual,
|
|
34
|
+
/** Keys allowed to be absent, e.g. optional credentials. */
|
|
35
|
+
optional = []
|
|
36
|
+
}) {
|
|
37
|
+
const exampleEnv = parseEnv(example);
|
|
38
|
+
const actualEnv = parseEnv(actual);
|
|
39
|
+
const optionalSet = new Set(optional);
|
|
40
|
+
const exampleKeys = [...exampleEnv.keys.keys()];
|
|
41
|
+
const findings = [];
|
|
42
|
+
let okCount = 0;
|
|
43
|
+
for (const key of exampleKeys) {
|
|
44
|
+
const exampleDeclaresValue = exampleEnv.keys.get(key) === true;
|
|
45
|
+
const isOptional = optionalSet.has(key) || !exampleDeclaresValue;
|
|
46
|
+
if (!actualEnv.keys.has(key)) {
|
|
47
|
+
if (!isOptional) findings.push({ kind: "missing", key });
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
const hasValue = actualEnv.keys.get(key) === true;
|
|
51
|
+
if (!hasValue && !isOptional) {
|
|
52
|
+
findings.push({ kind: "empty", key });
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
okCount += 1;
|
|
56
|
+
}
|
|
57
|
+
for (const key of actualEnv.keys.keys()) {
|
|
58
|
+
if (exampleEnv.keys.has(key)) continue;
|
|
59
|
+
const looksLike = findRenameCandidate(key, exampleKeys);
|
|
60
|
+
findings.push(looksLike ? { kind: "renamed", key, looksLike } : { kind: "unknown", key });
|
|
61
|
+
}
|
|
62
|
+
for (const key of actualEnv.duplicates) findings.push({ kind: "duplicate", key });
|
|
63
|
+
for (const line of actualEnv.malformed) findings.push({ kind: "malformed", ...line });
|
|
64
|
+
return {
|
|
65
|
+
findings,
|
|
66
|
+
okCount,
|
|
67
|
+
exampleCount: exampleEnv.keys.size,
|
|
68
|
+
actualCount: actualEnv.keys.size
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
var ORDER = [
|
|
72
|
+
"renamed",
|
|
73
|
+
"missing",
|
|
74
|
+
"empty",
|
|
75
|
+
"malformed",
|
|
76
|
+
"duplicate",
|
|
77
|
+
"unknown"
|
|
78
|
+
];
|
|
79
|
+
var LABELS = {
|
|
80
|
+
renamed: "Renamed \u2014 set under a name nothing reads",
|
|
81
|
+
missing: "Missing",
|
|
82
|
+
empty: "Present but empty",
|
|
83
|
+
malformed: "Malformed line",
|
|
84
|
+
duplicate: "Set more than once",
|
|
85
|
+
unknown: "Unknown \u2014 not in the example"
|
|
86
|
+
};
|
|
87
|
+
function formatEnvReport(result) {
|
|
88
|
+
const lines = [];
|
|
89
|
+
lines.push(
|
|
90
|
+
` ${result.actualCount} keys checked against ${result.exampleCount} in the example, ${result.okCount} correct`
|
|
91
|
+
);
|
|
92
|
+
if (result.findings.length === 0) {
|
|
93
|
+
lines.push("\nPASS \u2014 environment matches the example.");
|
|
94
|
+
return lines.join("\n");
|
|
95
|
+
}
|
|
96
|
+
for (const kind of ORDER) {
|
|
97
|
+
const group = result.findings.filter((finding) => finding.kind === kind);
|
|
98
|
+
if (group.length === 0) continue;
|
|
99
|
+
lines.push(`
|
|
100
|
+
${LABELS[kind]}:`);
|
|
101
|
+
for (const finding of group) {
|
|
102
|
+
if (finding.kind === "renamed") {
|
|
103
|
+
lines.push(` \u2717 ${finding.key} \u2192 did you mean ${finding.looksLike}?`);
|
|
104
|
+
} else if (finding.kind === "malformed") {
|
|
105
|
+
lines.push(` \u2717 line ${finding.line}: ${finding.text}\u2026`);
|
|
106
|
+
} else {
|
|
107
|
+
lines.push(` \u2717 ${finding.key}`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const renamed = result.findings.filter((finding) => finding.kind === "renamed").length;
|
|
112
|
+
if (renamed > 0) {
|
|
113
|
+
lines.push(
|
|
114
|
+
`
|
|
115
|
+
${renamed} key(s) look renamed. The setting they were meant to carry is unset,
|
|
116
|
+
which usually means a default is in force that nobody chose.`
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
lines.push(`
|
|
120
|
+
FAIL \u2014 ${result.findings.length} finding(s).`);
|
|
121
|
+
return lines.join("\n");
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export { checkEnv, formatEnvReport, parseEnv };
|
|
125
|
+
//# sourceMappingURL=env-check.js.map
|
|
126
|
+
//# sourceMappingURL=env-check.js.map
|
|
@@ -0,0 +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 // 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"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@olwiba/dx",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.28",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -39,6 +39,10 @@
|
|
|
39
39
|
"./generate-assets": {
|
|
40
40
|
"import": "./dist/generate-assets.js",
|
|
41
41
|
"types": "./dist/generate-assets.d.ts"
|
|
42
|
+
},
|
|
43
|
+
"./env-check": {
|
|
44
|
+
"import": "./dist/env-check.js",
|
|
45
|
+
"types": "./dist/env-check.d.ts"
|
|
42
46
|
}
|
|
43
47
|
},
|
|
44
48
|
"files": [
|