Skills › Visual Explainer › visual-explainer › files.mjs
files.mjs
40 lines · plugins/visual-explainer/kit/files.mjs at 9d5f880
1// Reading cited files from the repo, shared by the plan renderer (code exhibits, at="path:line") and the state-machine model
2// (its imports and the at: of a hand-written step). Only files under the root, and never one named like a secrets file.
3import { readFileSync, realpathSync, statSync } from "node:fs";
4import { relative, resolve, sep } from "node:path";
6const SECRET_NAME = /(^|\/)\.(git|ssh|aws|gnupg|kube)(\/|$)|(^|\/)(\.env(\.[^/]*)?|\.npmrc|\.netrc|\.pypirc|\.pgpass|\.git-credentials|id_(rsa|dsa|ecdsa|ed25519)[^/]*|credentials[^/]*|secrets?(\.[^/]*)?)$|\.(pem|key|p12|pfx|tfstate|kdbx|keystore)$/i;
8/** A cached reader for paths relative to realRoot: { text, abs } or { error } (plus missing: true when nothing is there). */
9export function fileReader(realRoot) {
10 const cache = new Map();
11 return (path) => {
12 if (cache.has(path)) return cache.get(path);
13 let out;
14 try {
15 const requested = relative(realRoot, resolve(realRoot, path)).split(sep).join("/");
16 const real = realpathSync(resolve(realRoot, path));
17 const rel = relative(realRoot, real);
18 if (rel.startsWith("..") || rel.startsWith(sep) || /^[a-z]:/i.test(rel)) out = { error: `${path} is outside ${realRoot}` };
19 // check the cited name and the link target both, so a link named .env cannot pass as config.txt
20 else if (SECRET_NAME.test(requested) || SECRET_NAME.test(rel.split(sep).join("/"))) out = { error: `${path} looks like a secrets file; not reading it` };
21 else if (!statSync(real).isFile()) out = { error: `${path} is not a file` };
22 else out = { text: readFileSync(real, "utf8").replace(/\r\n/g, "\n"), abs: real };
23 } catch {
24 out = { missing: true, error: `${path} not found under ${realRoot}` };
25 }
26 cache.set(path, out);
27 return out;
28 };
29}
31/** Checks at="path" or at="path:line": { error } when it is malformed or names nothing, and abs when the file exists.
32 * A path alone may name a file that does not exist yet (a plan creates it); a line must exist. */
33export function checkAt(at, read) {
34 const [path, line] = at.split(":");
35 const r = read(path);
36 if (line !== undefined && !/^[1-9]\d*$/.test(line)) return { error: `at="${at}"; write at="path" or at="path:line" with a line number from 1` };
37 if (r.error && (!r.missing || line)) return { error: `at="${at}": ${r.error}` };
38 if (!r.missing && line && +line > r.text.split("\n").length) return { error: `at="${at}", but ${path} has ${r.text.split("\n").length} lines`, abs: r.abs };
39 return { abs: r.abs };
40}