Terminal file viewer/editor + per-window app-command lockdown #60

Merged
jknapp merged 37 commits from feat/terminal-file-viewer into main 2026-09-23 17:05:50 +00:00
Showing only changes of commit ccff01a13a - Show all commits
+140 -15
View File
@@ -135,25 +135,150 @@ function importSpecs(file: string): string[] {
];
}
/** Best-effort removal of block/line comments and string/template literals, so the namespace-usage
* scan below doesn't trip on the alias appearing in prose or as text data. Regex-based, not a real
* parser — good enough for this test's purpose, not a general-purpose stripper. */
function stripCommentsAndStrings(text: string): string {
return text
.replace(/\/\*[\s\S]*?\*\//g, "")
.replace(/\/\/.*$/gm, "")
.replace(/`(?:\\.|[^`\\])*`/g, "``")
.replace(/"(?:\\.|[^"\\])*"/g, '""')
.replace(/'(?:\\.|[^'\\])*'/g, "''");
/** Scans a double- or single-quoted string starting at `text[start]` (the opening quote) and
* returns the index just past its closing quote, honoring backslash escapes. Throws (fail-closed)
* on an unterminated string rather than silently running to end-of-file. */
function skipQuoted(text: string, start: number, file: string): number {
const quote = text[start];
let i = start + 1;
while (i < text.length) {
if (text[i] === "\\") {
i += 2;
continue;
}
if (text[i] === quote) return i + 1;
i++;
}
throw new Error(`${relative(srcDir, file)}: unterminated ${quote === '"' ? "double" : "single"}-quoted string`);
}
/** Scans a `${...}` substitution's contents starting right after the `${`, tracking nesting of
* `{`/`(`/`[` (and skipping over any string/template/comment inside) so an unrelated `}` doesn't
* end the substitution early. Returns the raw source text of the substitution (not including the
* closing `}`) and the index just past that `}`. Throws (fail-closed) if it runs off the end
* without finding a balancing `}` — an unparseable substitution is not silently ignored. */
function scanSubstitution(text: string, start: number, file: string): { content: string; endIndex: number } {
let i = start;
let depth = 0;
while (i < text.length) {
const c = text[i];
if (c === "\\") {
i += 2;
continue;
}
if (c === '"' || c === "'") {
i = skipQuoted(text, i, file);
continue;
}
if (c === "`") {
i = maskTemplate(text, i, file).endIndex;
continue;
}
if (c === "/" && text[i + 1] === "*") {
const end = text.indexOf("*/", i + 2);
if (end === -1) throw new Error(`${relative(srcDir, file)}: unterminated block comment`);
i = end + 2;
continue;
}
if (c === "/" && text[i + 1] === "/") {
const end = text.indexOf("\n", i + 2);
i = end === -1 ? text.length : end;
continue;
}
if (c === "{" || c === "(" || c === "[") {
depth++;
i++;
continue;
}
if (c === ")" || c === "]") {
depth--;
i++;
continue;
}
if (c === "}") {
if (depth === 0) return { content: text.slice(start, i), endIndex: i + 1 };
depth--;
i++;
continue;
}
i++;
}
throw new Error(`${relative(srcDir, file)}: unterminated template substitution ("\${...}" never closes)`);
}
/** Masks a backtick template literal starting at `text[start]` (the opening backtick): literal
* template *text* is dropped, but every `${...}` substitution's source is kept — recursively
* stripped of its own comments/strings/nested templates via `stripCommentsAndStrings` — since a
* namespace alias referenced only inside a substitution is passed by reference to whatever
* consumes it (a tagged template hands each substitution to the tag function unstringified,
* exactly like `fn(alias)`) and must stay visible to the occurrence scan. Throws (fail-closed) on
* an unterminated template. */
function maskTemplate(text: string, start: number, file: string): { result: string; endIndex: number } {
let i = start + 1;
let result = "";
while (i < text.length) {
const c = text[i];
if (c === "\\") {
i += 2;
continue;
}
if (c === "`") return { result, endIndex: i + 1 };
if (c === "$" && text[i + 1] === "{") {
const { content, endIndex } = scanSubstitution(text, i + 2, file);
result += ` ${stripCommentsAndStrings(content, file)} `;
i = endIndex;
continue;
}
i++;
}
throw new Error(`${relative(srcDir, file)}: unterminated template literal`);
}
/** Best-effort removal of block/line comments and string/template literal *text*, so the
* namespace-usage scan below doesn't trip on the alias appearing in prose or as string data — while
* keeping the source of any `${...}` template substitution intact, recursively, since that's live
* code that can reference the alias. A small hand-rolled scanner, not a full parser: good enough
* for this test's purpose. Throws (fail-closed) on anything it can't make sense of — an
* unterminated string, comment or template — rather than silently truncating. */
function stripCommentsAndStrings(text: string, file: string): string {
let out = "";
let i = 0;
while (i < text.length) {
const c = text[i];
if (c === "/" && text[i + 1] === "*") {
const end = text.indexOf("*/", i + 2);
if (end === -1) throw new Error(`${relative(srcDir, file)}: unterminated block comment`);
i = end + 2;
continue;
}
if (c === "/" && text[i + 1] === "/") {
const end = text.indexOf("\n", i + 2);
i = end === -1 ? text.length : end;
continue;
}
if (c === '"' || c === "'") {
i = skipQuoted(text, i, file);
continue;
}
if (c === "`") {
const masked = maskTemplate(text, i, file);
out += masked.result;
i = masked.endIndex;
continue;
}
out += c;
i++;
}
return out;
}
/** Every occurrence of a namespace-imported alias, after its import line, must be a plain
* `alias.identifier` member access — `fn(alias)`, `const y = alias`, `alias["name"]` and
* `alias[expr]` all hand the whole namespace object somewhere this scan can't follow, which is
* exactly the silent-smuggling shape this test exists to close. Any such occurrence is fail-closed
* rather than silently accepted. */
* `alias.identifier` member access — `fn(alias)`, `const y = alias`, `alias["name"]`,
* `alias[expr]`, and a tagged-template substitution like `` tag`${alias}` `` all hand the whole
* namespace object somewhere this scan can't follow, which is exactly the silent-smuggling shape
* this test exists to close. Any such occurrence is fail-closed rather than silently accepted. */
function assertNamespaceUsedAsMemberAccessOnly(file: string, text: string, alias: string, importEnd: number) {
const rest = stripCommentsAndStrings(text.slice(importEnd));
const rest = stripCommentsAndStrings(text.slice(importEnd), file);
const occurrence = new RegExp(`\\b${alias}\\b(\\.[A-Za-z_$][\\w$]*)?`, "g");
for (const m of rest.matchAll(occurrence)) {
if (!m[1]) {