2.4 KiB
plan
Purpose
Invokes plugin.plan(answers, ctx) and validates the returned FileOp[]
against FileOpsSchema before letting them flow into apply. This is the
quality gate that prevents a buggy plugin from feeding malformed ops into
the host's transactional apply path. Both plugin-thrown errors and
schema-validation failures surface as INSTALLER.PLAN_FAILED so callers
have a single code to switch on.
Features
- Schema validation via
FileOpsSchema— every op kind, path, contents type, and conflict policy is checked at the boundary. - Wrapped in OTel span
installer.plancarryingplugin.id,plugin.version, anddry_run. - Distinguishes
planfailures (plugin threw) from invalid-shape failures (plan returned bad ops) only viamessageKey— both surface as the same code, which keeps consumer error handlers simple. - Original error chained via
causeso the underlying Zod error or plugin exception is recoverable for logging.
Basic Example
import { runPlan } from "@suaveplan/installer";
const ops = await runPlan(myPlugin, answers, ctx);
console.log(`plan returned ${ops.length} ops`);
Advanced Example
import { InstallerError, runPlan } from "@suaveplan/installer";
try {
const ops = await runPlan(myPlugin, answers, ctx);
return ops;
} catch (err) {
if (
err instanceof InstallerError &&
err.code === "INSTALLER.PLAN_FAILED"
) {
const issues = err.context?.["issues"];
if (issues) {
console.error("Plugin returned invalid ops:", issues);
} else {
console.error("Plugin plan threw:", err.cause);
}
}
throw err;
}
API Reference
runPlan<TAnswers, THandle>(plugin, answers, ctx): Promise<ReadonlyArray<FileOp>>- Errors:
INSTALLER.PLAN_FAILEDwith either:installer.plan.invalid_opsandcontext.issuesfor schema failuresinstaller.plan.threwandcauseset to the plugin's exception
Implementation Notes
Validation runs after the plugin returns, so a plugin can build ops
incrementally without seeing intermediate validation errors. The schema
also fixes the common mistake of returning a single op instead of an
array — FileOpsSchema requires Array<FileOp> so that gets caught at
the boundary instead of crashing later in apply. If the plugin already
threw an InstallerError, we re-throw it directly rather than wrapping
twice — this preserves cause chains all the way to the top.