Loading files
This file is pinned to the selected release. Download the original to open it in your design tool.
ff4fe56b2This file is pinned to the selected release. Download the original to open it in your design tool.
import { statfs } from 'node:fs/promises';
/**
* A run-blocking environment failure. Distinct from a mid-run error: nothing
* has been written yet, so the message alone is the whole user experience.
* The formatted message carries the reason, why copperhead refuses to run,
* and concrete remedy steps — every CLI catch path prints err.message, so
* embedding the explanation here means no call site needs special rendering.
*/
export class PreflightError extends Error {
constructor(
readonly reason: string,
readonly why: string,
readonly remedy: string[],
) {
super(formatPreflightFailure(reason, why, remedy));
this.name = 'PreflightError';
}
}
export function formatPreflightFailure(reason: string, why: string, remedy: string[]): string {
const steps = remedy.map((step, i) => ` ${i + 1}. ${step}`);
return [reason, '', `why it failed: ${why}`, 'to fix:', ...steps].join('\n');
}
export function isNotFoundError(err: any): boolean {
if (!err) return false;
if (err.code === 'ENOENT') return true;
if (process.platform === 'win32') {
const msg = String(err.stderr || err.message || '');
if (err.exitCode === 9009) {
return (
msg.includes('is not recognized') ||
msg.includes('cannot find the path specified')
);
}
if (err.exitCode === 1) {
return msg.includes('is not recognized');
}
}
return false;
}
/** Default minimum free space to start a run: 2 GiB. A create run emits gerbers,
* STEP, SVG renders and KiCad local history; 2 GiB is comfortably above a
* single board's output while still catching a nearly-full disk. */
export const DEFAULT_MIN_FREE_BYTES = 2 * 1024 * 1024 * 1024;
const gib = (n: number): string => `${(n / 1024 / 1024 / 1024).toFixed(1)} GiB`;
/**
* Free bytes available to this (unprivileged) user on the filesystem holding
* `dir`, or null when the platform/Node build cannot report it — callers treat
* null as "unknown" and skip the check rather than blocking a legitimate run.
*/
export async function freeDiskBytes(dir: string): Promise<number | null> {
try {
const fs = await statfs(dir);
return fs.bavail * fs.bsize;
} catch {
return null;
}
}
/**
* Refuse to start when free disk is below `minFreeBytes` (4.1). A long run can
* fill the disk mid-stage — gerbers/STEP/SVG plus unbounded KiCad local history
* — and then fail with an opaque `ENOSPC` after doing real, expensive work. A
* preflight fails fast with an actionable message instead. An unknown reading
* (unsupported platform) skips the check.
*/
export async function assertDiskSpace(dir: string, minFreeBytes = DEFAULT_MIN_FREE_BYTES): Promise<void> {
const free = await freeDiskBytes(dir);
if (free === null || free >= minFreeBytes) return;
throw new PreflightError(
`not enough free disk space to start (${gib(free)} available, ${gib(minFreeBytes)} required)`,
'a create run writes fabrication outputs and KiCad local history and can fill the disk mid-stage, failing with an opaque ENOSPC only after doing real work',
[
'free up space on the volume holding this repo',
'or lower the threshold with COPPERHEAD_MIN_FREE_MB (e.g. COPPERHEAD_MIN_FREE_MB=500)',
'then re-run',
],
);
}