import { readdir, stat, open } from "node:fs/promises"; import { existsSync } from "node:fs"; import { join, relative } from "node:path"; /** * Marker chant writes at the top of files it generates (worker/workflow/activities * bootstrap for Temporal Ops, etc.). Discovery must not treat these as authored * infra source: they hold no Declarables, they self-execute on import (the worker * bootstrap calls `run()`), and they use runtime patterns the authored-source lint * rules forbid (dynamic `process.env[...]` access, spreads). Skipping them keeps * `chant lint`/`build` from importing or linting the tool's own output. */ export const GENERATED_MARKER = "Generated by chant — do not edit directly"; /** True when a file's header carries the {@link GENERATED_MARKER}. Reads only the head. */ async function isGeneratedFile(file: string): Promise { let fh; try { fh = await open(file, "r"); const { buffer, bytesRead } = await fh.read({ buffer: Buffer.alloc(256), position: 0 }); return buffer.toString("utf8", 0, bytesRead).includes(GENERATED_MARKER); } catch { return false; } finally { await fh?.close(); } } /** * Recursively find all TypeScript infrastructure files in a directory * @param path - The directory path to search * @returns Array of file paths to .ts files (excluding test files and chant-generated files) */ export async function findInfraFiles(path: string): Promise { const files: string[] = []; let sourceRoot: string | null = null; async function scanDirectory(dir: string): Promise { let entries; try { entries = await readdir(dir, { withFileTypes: true }); } catch (error) { // Skip directories we can't read return; } for (const entry of entries) { const fullPath = join(dir, entry.name); // Skip node_modules if (entry.isDirectory() && entry.name === "node_modules") { continue; } if (entry.isDirectory()) { // Child project boundary — a directory with its own chant.config.ts // is a separate scope, but only if we've already found the project's // own source root. The first config directory is the source root, not // a child project. const configPath = join(fullPath, "chant.config.ts"); if (existsSync(configPath)) { if (sourceRoot === null) { sourceRoot = fullPath; } else { continue; } } await scanDirectory(fullPath); } else if (entry.isFile()) { // Include only .ts files, exclude test files and chant-generated files if ( entry.name.endsWith(".ts") && !entry.name.endsWith(".test.ts") && !entry.name.endsWith(".spec.ts") && !(await isGeneratedFile(fullPath)) ) { files.push(fullPath); } } } } await scanDirectory(path); return files; }