import { createHash } from 'node:crypto'; import { execFile } from 'node:child_process'; import { readFile, stat, writeFile } from 'node:fs/promises'; import path from 'node:path'; import { promisify } from 'node:util'; import { fileURLToPath } from 'node:url'; const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); const excludedFiles = new Set(['SOURCE_MANIFEST.txt']); const execFileAsync = promisify(execFile); async function collect() { const { stdout } = await execFileAsync( 'git', ['ls-files', '--cached', '--others', '--exclude-standard', '-z'], { cwd: root, encoding: 'buffer', maxBuffer: 16 * 1024 * 1024 }, ); const relativePaths = stdout .toString('utf8') .split('\0') .filter(Boolean) .filter((relative) => !excludedFiles.has(relative)); const existing = []; for (const relative of relativePaths) { const absolute = path.resolve(root, relative); try { if ((await stat(absolute)).isFile()) existing.push(absolute); } catch (error) { if (error?.code !== 'ENOENT') throw error; } } return existing; } const packageJson = JSON.parse(await readFile(path.join(root, 'package.json'), 'utf8')); const files = (await collect()).sort((left, right) => left.localeCompare(right, 'en')); const lines = [ `ForgeFlow ${packageJson.version} source manifest`, 'SHA-256 BYTES PATH', '(The manifest includes tracked and non-ignored source files, excluding itself.)' ]; for (const absolute of files) { const bytes = await readFile(absolute); const size = (await stat(absolute)).size; const digest = createHash('sha256').update(bytes).digest('hex'); const relative = path.relative(root, absolute).replaceAll('\\', '/'); lines.push(`${digest} ${String(size).padStart(12)} ${relative}`); } await writeFile(path.join(root, 'SOURCE_MANIFEST.txt'), `${lines.join('\n')}\n`, 'utf8'); console.log(`Wrote ${files.length} entries for ForgeFlow ${packageJson.version}.`);