Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View File

@@ -0,0 +1,145 @@
# OpenSpec Change: add-installer-upgrade
- **Change ID**: add-installer-upgrade
- **Package**: `@suaveplan/installer`
- **Status**: PROPOSED
- **Date**: 2026-05-17
- **Design**: ./design.md
- **Spec delta**: ./specs/installer/spec.md
## Why
`openspec/archive/2026-04-28-installer-port/proposal.md` §"Out of scope" deferred
the upgrade flow:
> Plugin upgrade flow (`upgrade <plugin>` to bring an existing install up to
> current bundle version) is a follow-up — initial scope is install + reinstall
> (idempotent re-run with conflict policy).
The installer therefore ships `add <plugin>` and `list` but has no answer for a
user who installed `@suaveplan/install-agent-skills@0.5.0` six months ago and
wants the changes that landed in `0.7.0`. Today their only options are
`add agent-skills --force` (which overwrites local edits without diffing) or a
manual `rm -rf` (which destroys their in-flight skill edits). Both are unsafe.
The `add` path is also unable to run plugin-authored migration scripts, so any
breaking layout change in a plugin bundle (e.g. `skills/<id>/SKILL.md` →
`skills/<id>/<id>.md`) silently leaves stale files behind that the user has to
hand-prune.
## What Changes
Add a third top-level installer command, `suaveplan-install upgrade <plugin>`,
that resolves the **installed** plugin version from the on-disk state file,
resolves the **target** version from `node_modules`, computes the file-level
diff between the two bundles (added / removed / changed), and applies it
transactionally with the same staged-tmp + rollback strategy `apply` already
uses for `add`. Pre-flight gates protect against in-flight edits (dirty target
tree) and missing lockfiles. Plugins MAY ship an optional `upgrade.ts`
migration script per from-version → to-version pair; the host invokes them in
order inside the same transaction.
### Surface added
- `suaveplan-install upgrade <plugin>` CLI verb with flags
`--dry-run`, `--force`, `--no-interactive`, `--answers <file>`,
`--from <version>` (override detected), `--to <version>` (target; defaults
to whatever resolves from `node_modules`), `--allow-dirty` (skip dirty-tree
guard — destructive, opt-in only).
- `Host.upgrade(plugin, options)` public entry point alongside `Host.run`.
- A persisted state file (`.suaveplan/installer-state.json`) capturing
`{ pluginId, version, installedAt, fileHashes }` per installed plugin so
diff and dirty-tree detection are deterministic on subsequent upgrades.
- A `MigrationContext` and `MigrationScript` contract under
`@suaveplan/installer/migrations` for plugin-authored scripts.
- 7 new `INSTALLER.*` codes: `UPGRADE_NOT_INSTALLED`,
`UPGRADE_VERSION_NOT_FOUND`, `UPGRADE_SAME_VERSION`, `UPGRADE_DIRTY_TREE`,
`UPGRADE_STATE_INVALID`, `UPGRADE_MIGRATION_FAILED`,
`UPGRADE_LOCKFILE_MISSING`.
### Behaviour guarantees
- Re-running `upgrade <plugin>` when installed == target is a no-op exiting
with `INSTALLER.UPGRADE_SAME_VERSION` (treated as success under `--dry-run`).
- Failure at any point (diff compute, migration script throw, atomic rename
error) rolls the target tree back to the pre-upgrade snapshot. Partial
state is never persisted.
- The state file is updated atomically (`write → fsync → rename`) only after
the transaction commits.
## Source mapping
| Source / archive reference | What it tells us |
|---|---|
| `openspec/archive/2026-04-28-installer-port/proposal.md` §"Out of scope" bullet 2 | The deferred upgrade flow is the scope of this change. |
| `openspec/archive/2026-04-28-installer-port/proposal.md` §"Architecture → Lifecycle" | Reuse the existing `detect → prompts → plan → apply → postInstall` pipeline; upgrade adds a diff-and-migrate stage between `plan` and `apply`. |
| `openspec/specs/installer/spec.md` `INSTALLER-04` (transactional apply) | Upgrade MUST reuse the same staged-tmp + rollback machinery — no parallel transaction model. |
| `openspec/specs/installer/spec.md` `INSTALLER-05` (conflict policy) | Per-file conflict policy already exists; upgrade extends it with a "user-modified-since-install" case detected via the state-file hashes. |
| `openspec/specs/installer/spec.md` `INSTALLER-09` (frozen registry) | New codes MUST extend `defineCodes("INSTALLER", [...])` — no parallel registry. |
| `packages/tooling/installer/src/bin/suaveplan-install.ts` | Existing CLI shape: `cli().command("add <plugin>")…`. The new verb attaches identically: `.command("upgrade <plugin>")`. |
| `packages/tooling/installer/src/host/host.ts` | `Host` class is the orchestration seam; `Host.upgrade` slots in alongside `Host.run`. |
| `packages/tooling/installer/src/host/apply/apply.ts` | Staged-tmp transaction implementation is reused as-is; upgrade composes it. |
| `packages/tooling/installer/src/manifest/discover.ts` | Version resolution from `node_modules` already exists for `add`; upgrade reuses `findPlugin(cwd, pluginId)`. |
| `packages/tooling/installer/src/error-codes.ts` | New codes APPEND to the existing tuple; the registry stays frozen. |
## Impact
### Affected dependents
Run `bunx turbo ls --filter=...@suaveplan/installer` to enumerate. Current
dependents:
- `@suaveplan/install-agent-skills` — [x] tests run / [x] docs reviewed /
[ ] no action needed. The skills plugin is the canonical upgrade target;
its existing `runPlugin` harness gains an `upgradePlugin` sibling so its
conformance tests cover both flows. The plugin itself receives a tiny
optional `upgrade.ts` exporting an empty migration table (no breaking
layout changes yet); this proves the migration contract loads even when
the table is empty.
No other dependents currently consume `@suaveplan/installer`.
## Out of scope
- **Downgrade.** `upgrade` only moves forward. A separate
`add-installer-downgrade` change can extend the diff engine in reverse;
the migration contract is intentionally directional (`from`/`to` versions)
so downgrade is a different scope.
- **Cross-plugin coordination.** Upgrading `install-agent-skills` does not
trigger an upgrade of a hypothetical `install-web-server`. Each plugin is
upgraded independently; `peerInstallers` field warnings are a future
packaging change.
- **Background / scheduled upgrade daemon.** Upgrade is operator-initiated
via the CLI; there is no auto-update behaviour.
- **Network resolution.** The "target version" is whatever resolves from
`node_modules`; the installer never talks to npm directly. Bumping the
installed package version remains the user's package-manager step.
## Verification
Every requirement in `specs/installer/spec.md` carries a `**Verified by:**`
line pointing to a conformance test in `src/host/upgrade/` or
`src/state/`. The full set:
- `INSTALLER-UPGRADE-01` Lifecycle order → `src/host/upgrade/upgrade.spec-conformance.test.ts::"runs detect → resolve → diff → migrate → apply → postInstall in order"`
- `INSTALLER-UPGRADE-02` Idempotent same-version no-op → `src/host/upgrade/upgrade.spec-conformance.test.ts::"exits with UPGRADE_SAME_VERSION when installed equals target"`
- `INSTALLER-UPGRADE-03` Dirty-tree guard → `src/host/upgrade/dirty-tree.spec-conformance.test.ts::"throws UPGRADE_DIRTY_TREE when state-file hash diverges from disk"`
- `INSTALLER-UPGRADE-04` Lockfile pre-flight → `src/host/upgrade/preflight.spec-conformance.test.ts::"throws UPGRADE_LOCKFILE_MISSING when no bun.lockb / package-lock.json / pnpm-lock.yaml exists"`
- `INSTALLER-UPGRADE-05` Transactional diff application → `src/host/upgrade/apply-diff.spec-conformance.test.ts::"rolls back snapshot on staged-rename failure"`
- `INSTALLER-UPGRADE-06` Migration script contract → `src/host/upgrade/migrations.spec-conformance.test.ts::"runs upgrade.ts scripts in semver order between from and to"`
- `INSTALLER-UPGRADE-07` Migration failure rollback → `src/host/upgrade/migrations.spec-conformance.test.ts::"rolls back transaction and surfaces UPGRADE_MIGRATION_FAILED on script throw"`
- `INSTALLER-UPGRADE-08` State-file integrity → `src/state/state-file.spec-conformance.test.ts::"atomically writes installer-state.json only on successful commit"`
- `INSTALLER-UPGRADE-09` Error-code registry namespacing → `src/error-codes.spec-conformance.test.ts::"upgrade codes are frozen and namespaced INSTALLER.*"`
- `INSTALLER-UPGRADE-10` Version-not-found surface → `src/host/upgrade/upgrade.spec-conformance.test.ts::"throws UPGRADE_VERSION_NOT_FOUND when plugin not in node_modules"`
- `INSTALLER-UPGRADE-11` Not-installed surface → `src/host/upgrade/upgrade.spec-conformance.test.ts::"throws UPGRADE_NOT_INSTALLED when no state-file entry exists"`
- `INSTALLER-UPGRADE-12` CLI flag wiring → `src/bin/suaveplan-install.spec-conformance.test.ts::"upgrade verb forwards --dry-run, --force, --allow-dirty, --from, --to to Host.upgrade"`
## Acceptance criteria
- `bunx turbo lint typecheck test --filter=@suaveplan/installer` green.
- `bunx turbo test:coverage --filter=@suaveplan/installer` 100% istanbul on
every new module under `src/host/upgrade/`, `src/state/`, and the
upgrade-specific additions to `src/bin/suaveplan-install.ts`.
- Sibling `.md` for every new implementation `.ts` per `openspec/AGENTS.md` §4.
- `openspec validate add-installer-upgrade --strict` green.
- Spec delta folded into `openspec/specs/installer/spec.md` on archive.