* Read + validate a plan JSON file. Returns the parsed `CliPlanInput` * on success, throws a typed `VALIDATION_ERROR` envelope on any * schema problem (missing fields, wrong types, oversize body, etc.). * * Stat-first guard mirrors piece-2's `readCodeFileGuarded` — reject * obvious oversize fil
(path: string)
| 1972 | * plans the cap is 256 KB (vs. 350 KB for code). |
| 1973 | */ |
| 1974 | function readPlanFromGuarded(path: string): CliPlanInput { |
| 1975 | const absolute = resolveAbsolute(path); |
| 1976 | |
| 1977 | let stat; |
| 1978 | try { |
| 1979 | stat = statSync(absolute); |
| 1980 | } catch (err) { |
| 1981 | const code = (err as NodeJS.ErrnoException).code; |
| 1982 | if (code === 'ENOENT') { |
| 1983 | throw localValidationError('plan-from', `file does not exist: ${path}`); |
| 1984 | } |
| 1985 | if (code === 'EACCES') { |
| 1986 | throw localValidationError('plan-from', `permission denied reading ${path}`); |
| 1987 | } |
| 1988 | const reason = err instanceof Error ? err.message : 'unknown error'; |
| 1989 | throw localValidationError('plan-from', `cannot stat ${path}: ${reason}`); |
| 1990 | } |
| 1991 | if (stat.size > MAX_PLAN_BODY_BYTES) { |
| 1992 | throw ApiError.fromEnvelope({ |
| 1993 | error: { |
| 1994 | code: 'PAYLOAD_TOO_LARGE', |
| 1995 | message: `Plan body exceeds the 256 KB CLI cap (${stat.size} bytes).`, |
| 1996 | nextAction: 'Split into multiple smaller tests or trim step descriptions.', |
| 1997 | requestId: 'local', |
| 1998 | details: { |
| 1999 | field: 'plan-from', |
| 2000 | sizeBytes: stat.size, |
| 2001 | maxBytes: MAX_PLAN_BODY_BYTES, |
| 2002 | }, |
| 2003 | }, |
| 2004 | }); |
| 2005 | } |
| 2006 | |
| 2007 | let raw; |
| 2008 | try { |
| 2009 | raw = stripBom(readFileSync(absolute, 'utf8')); |
| 2010 | } catch (err) { |
| 2011 | const reason = err instanceof Error ? err.message : 'unknown error'; |
| 2012 | throw localValidationError('plan-from', `cannot read ${path}: ${reason}`); |
| 2013 | } |
| 2014 | |
| 2015 | let parsed: unknown; |
| 2016 | try { |
| 2017 | parsed = JSON.parse(raw); |
| 2018 | } catch (err) { |
| 2019 | const reason = err instanceof Error ? err.message : 'unknown error'; |
| 2020 | throw localValidationError('plan-from', `not valid JSON: ${reason}`); |
| 2021 | } |
| 2022 | |
| 2023 | return assertPlanShape(parsed); |
| 2024 | } |
| 2025 | |
| 2026 | /** |
| 2027 | * Type-narrow + validate a parsed plan input. Pulled out so the same |
no test coverage detected