# Installer — Spec ## Requirements ### INSTALLER-01 — Lifecycle order `Host.run(plugin, options)` MUST execute plugin stages in the strict order `detect → prompts → plan → apply → postInstall`. Optional stages (`detect`, `prompts`, `postInstall`) MUST be skipped when not declared on the plugin without affecting the order of the remaining stages. Each stage MUST be wrapped in an OpenTelemetry span named `installer.` carrying attributes `plugin.id`, `plugin.version`, and `dry_run`. **Verified by:** `src/host/host.spec-conformance.test.ts` (`INSTALLER-01`). ### INSTALLER-02 — Idempotent re-run with conflict policy Running the same plugin twice against the same target MUST be safe. On the second run, every emitted `FileOp` MUST be resolved against its declared `conflict` policy (`error | overwrite | skip-if-exists | merge | prompt`). The default conflict policy MUST be `error`. The `--force` flag MUST collapse all policies to `overwrite`. The `--no-interactive` flag MUST cause `prompt` policies to fail with `INSTALLER.APPLY_CONFLICT` instead of blocking. **Verified by:** `src/host/host.spec-conformance.test.ts` (`INSTALLER-02`), `src/host/apply/apply.spec-conformance.test.ts` (`INSTALLER-05`). ### INSTALLER-03 — Abort propagation `InstallerContext.signal: AbortSignal` MUST be threaded through every stage. When the signal aborts mid-run, the host MUST stop the current stage at the next async boundary, roll back any partially applied ops, and surface `InstallerError(INSTALLER.ABORTED)`. Plugins receiving the signal MUST honour it; the host MUST NOT silently swallow `AbortError`. **Verified by:** `src/host/host.spec-conformance.test.ts` (`INSTALLER-03`). ### INSTALLER-04 — Transactional apply with rollback `apply` MUST materialise `FileOp[]` through a staged-tmp + atomic-rename strategy: every `write` and `patch-*` op writes to a sibling tmp path under the target directory, then renames into place only after every op in the plan has staged successfully. Any failure during staging MUST trigger a reverse-order rollback that deletes the staged tmp files and restores any pre-existing files moved aside. Rollback failures MUST surface as `InstallerError(INSTALLER.ROLLBACK_FAILED, { cause })` carrying the original apply error as nested cause. `--dry-run` MUST print the plan and return before any tmp files are written. **Verified by:** `src/host/apply/apply.spec-conformance.test.ts` (`INSTALLER-04`). ### INSTALLER-05 — Conflict policy resolution For each `FileOp` whose target path already exists, the host MUST consult the op's `conflict` field and: `error` → throw `INSTALLER.APPLY_CONFLICT`; `overwrite` → replace; `skip-if-exists` → drop the op silently and record it in the plan summary; `merge` → apply `JsonMerge` for `patch-json` and `TextEdit[]` for `patch-text` (and is rejected for raw `write` ops with `INSTALLER.APPLY_CONFLICT`); `prompt` → ask via `InstallerCli.confirm`, defaulting to `error` semantics on rejection or in `--no-interactive` mode. **Verified by:** `src/host/apply/apply.spec-conformance.test.ts` (`INSTALLER-05`). ### INSTALLER-06 — Prompt validation `Host.runPrompts(plugin, ctx)` MUST Zod-validate the answers produced by `plugin.prompts?.()` before passing them to `plan`. Validation failures MUST surface as `InstallerError(INSTALLER.PROMPT_VALIDATION_FAILED, { cause: zodIssue })`. When `--answers ` is supplied, the host MUST load the file, validate against the same schema, and bypass interactive prompting entirely. `--no-interactive` MUST cause any plugin that demands additional prompts (beyond what `--answers` supplied) to fail with `INSTALLER.PROMPT_VALIDATION_FAILED`. **Verified by:** `src/host/prompts.spec-conformance.test.ts` (`INSTALLER-06`). ### INSTALLER-07 — Manifest schema is enforced at load time `loadManifest(packagePath)` MUST resolve the plugin package's `manifest.json` and validate it against `PluginManifestSchema`. Required fields: `id` (string), `version` (semver), `description` (string), `entry` (relative module path), `engines` (Record), `assets` (string[]). Optional fields: `peerInstallers` (string[]). Validation failures MUST surface as `InstallerError(INSTALLER.PLUGIN_INVALID_MANIFEST, { cause })`. **Verified by:** `src/manifest/manifest.spec-conformance.test.ts` (`INSTALLER-07`). ### INSTALLER-08 — Plugin discovery from consumer node_modules `discoverPlugins(cwd)` MUST resolve packages matching `@suaveplan/install-*` (first-party) and `@/suaveplan-install-*` (third-party convention) from the consumer's `node_modules` tree, including hoisted and pnpm-style nested layouts. Missing plugins MUST surface as `InstallerError(INSTALLER.PLUGIN_NOT_FOUND)` carrying the requested id. Load failures (require/import throws, malformed exports) MUST surface as `InstallerError(INSTALLER.PLUGIN_LOAD_FAILED, { cause })`. **Verified by:** `src/manifest/manifest.spec-conformance.test.ts` (`INSTALLER-08`). ### INSTALLER-09 — Error-code registry is namespaced and frozen `INSTALLER_CODES` MUST be declared via `defineCodes("INSTALLER", [...])` from `@suaveplan/error`. `Object.isFrozen(INSTALLER_CODES)` MUST be `true`. Every code value MUST start with `INSTALLER.`. `InstallerError` MUST extend `SuaveplanError`. The registry MUST contain at minimum: `PLUGIN_NOT_FOUND`, `PLUGIN_LOAD_FAILED`, `PLUGIN_INVALID_MANIFEST`, `PROMPT_VALIDATION_FAILED`, `PLAN_FAILED`, `APPLY_CONFLICT`, `APPLY_FAILED`, `ROLLBACK_FAILED`, `ABORTED`. **Verified by:** `src/error-codes.spec-conformance.test.ts` (`INSTALLER-09`).