/*
Canonical Node build-time filesystem primitives shared across every packages/os/* command and validator: recursive directory collection, existence checks, JSON reads.
- Do not write files — all writes flow through writeFileAtomic / WorkspaceIO (RFC-0258/RFC-0267).
- Do not run in the browser — this module is server-only (Node fs/promises).
- RFC-0303: extracted as the single canonical home for fs helpers previously duplicated across packages/os/*.
*/
// Server-only. Do not import from browser/client scripts.
import { readdir, readFile, stat } from "node:fs/promises";
import { join } from "node:path";
import { parse as yamlParse } from "yaml";
export interface CollectFilesOptions {
/** Only include files whose name ends with one of these (e.g. [".md", ".astro"]). Omit = all files. */
extensions?: string[];
/** Return true to skip a directory or file entry by name. Default: name starts with "-" or "old-". */
ignore?: (name: string) => boolean;
/** Include directory paths in the result too. Default false (files only). */
withDirs?: boolean;
}
function defaultIgnore(name: string): boolean {
return name.startsWith("-") || name.startsWith("old-");
}
/**
* Recursively collects file paths under `root`.
* Swallows readdir errors per-branch by returning no entries for that branch.
*/
export async function collectFiles(
root: string,
options: CollectFilesOptions = {},
): Promise {
const { extensions, ignore = defaultIgnore, withDirs = false } = options;
const results: string[] = [];
async function walk(dir: string): Promise {
let entries;
try {
entries = await readdir(dir, { withFileTypes: true });
} catch {
return;
}
for (const entry of entries) {
if (ignore(entry.name)) continue;
const full = join(dir, entry.name);
if (entry.isDirectory()) {
if (withDirs) results.push(full);
await walk(full);
continue;
}
if (!entry.isFile()) continue;
if (extensions && !extensions.some((ext) => entry.name.endsWith(ext))) continue;
results.push(full);
}
}
await walk(root);
return results;
}
/**
* Best-effort existence check for a path (file or directory).
* Returns `false` on any filesystem error (ENOENT, EACCES, etc.).
*/
export async function fileExists(path: string): Promise {
try {
await stat(path);
return true;
} catch {
return false;
}
}
export async function readJsonFile(path: string): Promise {
const raw = await readFile(path, "utf-8");
return JSON.parse(raw) as T;
}
export async function readYamlFile(path: string): Promise {
const raw = await readFile(path, "utf-8");
return yamlParse(raw) as T;
}