/*
walkFiles — the single file-discovery and file-reading seam for stack plugins
(RFC-1100). Returns workspace-relative paths with forward slashes and reads file
contents in bulk, so validators never import node:fs directly.
- Does not write files — writes flow through writeFileIfChanged / WorkspaceIO.
- Does not apply implicit ignore rules (no dotfile or prefix skipping) — callers scope by directory and filter.
- Does not run in the browser — server-only (Node fs/promises).
- RFC-1100: initial shared walker — walkFiles plus readTextFile/readTextFiles/readBinaryFiles companions; replaces per-validator private readdir walkers in stack plugins.
- RFC-1100: step 1 — shared tool/fs/check seams (walkFiles, runTool, defineStackChecks)
- RFC-1249: wire werkstatt.commands.validate into packages.check + retire CMD-OUTPUT debt
*/
// Server-only. Do not import from browser/client scripts.
import { join } from "node:path";
import { createDefaultIO, type WorkspaceIO } from "@warpgogol/werkstatt-shared/kernel/workspace-io";
export interface WalkFilesOptions {
/** Recurse into subdirectories. Default true. */
recursive?: boolean;
/** Return true to include a file by its directory-relative path (forward slashes). */
filter?: (relPath: string) => boolean;
}
/**
* Lists files under `dir` as relative paths with forward slashes.
* Missing or unreadable directories yield an empty array.
*/
export async function walkFiles(dir: string, options: WalkFilesOptions = {}): Promise {
const { recursive = true, filter } = options;
const results: string[] = [];
// fs.walk.lint: allow — canonical WorkspaceIO-based walker (walkFiles) — collectFiles uses node readdir, not io
async function walk(
current: string,
relPrefix: string,
io: WorkspaceIO = createDefaultIO().io,
): Promise {
let entries;
try {
entries = await io.readdir(current);
} catch {
return;
}
for (const entry of entries) {
const relPath = relPrefix ? `${relPrefix}/${entry.name}` : entry.name;
if (entry.isDirectory) {
if (recursive) {
await walk(join(current, entry.name), relPath);
}
continue;
}
if (!entry.isFile) continue;
if (filter && !filter(relPath)) continue;
results.push(relPath);
}
}
await walk(dir, "");
return results;
}
/**
* Reads a single file as utf-8. Returns null when the file is missing or unreadable.
*/
export async function readTextFile(
path: string,
io: WorkspaceIO = createDefaultIO().io,
): Promise {
try {
return await io.readFile(path);
} catch {
return null;
}
}
/**
* Reads multiple files under `dir` as utf-8, keyed by their walkFiles-relative path.
* Unreadable entries are omitted from the result.
*/
export async function readTextFiles(dir: string, relPaths: string[]): Promise