Initial Commit
This commit is contained in:
205
openspec/archive/2026-04-28-installer-port/proposal.md
Normal file
205
openspec/archive/2026-04-28-installer-port/proposal.md
Normal file
@@ -0,0 +1,205 @@
|
||||
# OpenSpec Change: installer-port
|
||||
|
||||
- **Change ID**: installer-port
|
||||
- **Packages**: `@suaveplan/installer` (new), `@suaveplan/install-agent-skills` (new)
|
||||
- **Status**: PROPOSED
|
||||
- **Date**: 2026-04-28
|
||||
|
||||
## Summary
|
||||
|
||||
Introduce `@suaveplan/installer`, a thin plugin host for project boilerplating and asset installation, and `@suaveplan/install-agent-skills`, the inaugural plugin/bundle that installs the full Suaveplan agent-skills set into a target project from npm — no repo clone required.
|
||||
|
||||
The installer is a generic, modular pipeline (`detect → prompts → plan → apply → postInstall`) built on top of three primitives that already exist on genesis: `@suaveplan/cli` (UX), `@suaveplan/templates` (rendering), `@suaveplan/validation` (Zod prompt schemas). Plugins are vertical (`install-agent-skills`, future `install-web-server`, `install-spa`, …) and each ships as its own npm package consumable via `npx @suaveplan/installer add <plugin>`.
|
||||
|
||||
## Why
|
||||
|
||||
Skills today require users to clone the repo to copy files out — a known pain point. We also want a unified path for boilerplating web servers, SPAs, desktop apps, mobile apps, etc., without each generator reinventing argv parsing, prompts, dry-run, conflict resolution, and rollback.
|
||||
|
||||
A plugin host solves both. The lifecycle (detect/prompts/plan/apply/postInstall) is identical for skills and for boilerplate; the rendering substrate (`@suaveplan/templates`) is identical; the UX substrate (`@suaveplan/cli`) is identical. Without a host, every generator duplicates ~600 lines of orchestration.
|
||||
|
||||
We ship the host together with one real plugin (`install-agent-skills`) so the contract is validated by a non-trivial consumer on day one — not a paper interface.
|
||||
|
||||
## Why a single bundle for skills
|
||||
|
||||
Earlier sketch proposed one npm package per skill. Rejected: ~90 skills × separate packages multiplies publish pipelines, changelogs, and resolution churn. Users almost always want "the suaveplan skill set" or a curated subset, not a cherry-pick. `install-agent-skills` ships **all skills as a single versioned bundle** with internal selectivity via `--only` / `--exclude` flags. If a future skill grows heavy assets (large datasets, models) that bloat the bundle for users who don't need them, that one peels out as `@suaveplan/skill-<heavy>` — bundle remains the default.
|
||||
|
||||
## Why `templates` is not a plugin
|
||||
|
||||
Plugins are vertical (skills, web-server, spa) — what the user chose to install. `@suaveplan/templates` is a rendering primitive every plugin uses internally. Modelling it as a plugin would invert the dependency: plugins would have to opt into rendering. Wrong shape. Templates stays an internal primitive of `installer` and of every plugin that needs interpolation.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Layers
|
||||
|
||||
```
|
||||
@suaveplan/cli ← argv, prompts, spinner, output (existing)
|
||||
@suaveplan/templates ← interpolation (existing)
|
||||
@suaveplan/validation ← Zod prompt schemas (existing)
|
||||
@suaveplan/error ← structured errors (existing)
|
||||
│
|
||||
▼
|
||||
@suaveplan/installer ← plugin host + lifecycle (this change)
|
||||
│
|
||||
├── @suaveplan/install-agent-skills (this change)
|
||||
├── @suaveplan/install-web-server (future)
|
||||
├── @suaveplan/install-spa (future)
|
||||
└── @suaveplan/install-<vertical> (future)
|
||||
```
|
||||
|
||||
### Plugin contract
|
||||
|
||||
The host's only public surface is the plugin contract:
|
||||
|
||||
```ts
|
||||
export interface InstallerContext {
|
||||
readonly cwd: string;
|
||||
readonly logger: LoggerLike;
|
||||
readonly fs: InstallerFs; // virtual filesystem (dry-run safe)
|
||||
readonly templates: TemplateEngine; // shared template engine instance
|
||||
readonly cli: InstallerCli; // prompts, spinner, output
|
||||
readonly signal: AbortSignal;
|
||||
}
|
||||
|
||||
export interface InstallerPlugin<TAnswers, THandle = void> {
|
||||
readonly id: string;
|
||||
readonly version: string;
|
||||
detect?(ctx: InstallerContext): Promise<DetectResult>;
|
||||
prompts?(ctx: InstallerContext): PromptSchema<TAnswers>;
|
||||
plan(answers: TAnswers, ctx: InstallerContext): Promise<FileOp[]>;
|
||||
apply(ops: FileOp[], ctx: InstallerContext): Promise<THandle>;
|
||||
postInstall?(handle: THandle, ctx: InstallerContext): Promise<void>;
|
||||
}
|
||||
|
||||
export type FileOp =
|
||||
| { kind: "write"; path: string; contents: Uint8Array; mode?: number }
|
||||
| { kind: "patch-json"; path: string; merge: JsonMerge }
|
||||
| { kind: "patch-text"; path: string; edits: TextEdit[] }
|
||||
| { kind: "mkdir"; path: string }
|
||||
| { kind: "delete"; path: string }
|
||||
| { kind: "rename"; from: string; to: string };
|
||||
```
|
||||
|
||||
Generics (`TAnswers`, `THandle`) flow end-to-end so `prompts` returns typed answers and `apply` returns a typed handle the plugin can hand to `postInstall`. No `any`.
|
||||
|
||||
### Lifecycle
|
||||
|
||||
1. **detect** — plugin reports what is already present (e.g. existing `agent-skills/` directory, conflicting files).
|
||||
2. **prompts** — Zod-validated answer schema; `cli` renders prompts in TTY, accepts `--answers <file>` non-interactively.
|
||||
3. **plan** — pure function: produces `FileOp[]` against the virtual fs. No real writes yet.
|
||||
4. **apply** — host materialises ops to disk inside a transaction (staged-tmp + atomic rename per file; rollback on failure). `--dry-run` prints the plan and exits before this step.
|
||||
5. **postInstall** — plugin runs follow-up commands (e.g. `bun install`, registers entries, prints next-steps).
|
||||
|
||||
### Conflict resolution
|
||||
|
||||
Each `FileOp` declares an explicit conflict policy: `error | overwrite | skip-if-exists | merge | prompt`. The default is `error`. `--force` collapses to `overwrite`. `--no-interactive` rejects `prompt` ops.
|
||||
|
||||
### Manifest
|
||||
|
||||
Every plugin exports a `manifest.json` next to its entry. Schema (`PluginManifestSchema`) is owned by `@suaveplan/installer` and validated at load time. The manifest declares: `id`, `version`, `description`, `engines`, `peerInstallers`, `assets` (paths inside the plugin package), and the entry module.
|
||||
|
||||
## `@suaveplan/install-agent-skills`
|
||||
|
||||
The first vertical plugin. Ships every Suaveplan skill (`SKILL.md` + co-located assets) bundled inside the package under `assets/skills/<name>/`. The plugin's `plan` step:
|
||||
|
||||
1. Reads its bundled manifest (`assets/skills/manifest.json`).
|
||||
2. Filters by `--only <names>` / `--exclude <names>` if supplied; defaults to **all** skills.
|
||||
3. Detects target project's existing skill directory (`.suaveplan/skills/` by default; configurable).
|
||||
4. Emits `FileOp[]`: `mkdir` for each skill folder, `write` for each file, `patch-json` for the project's skill index file (creates if missing).
|
||||
5. `postInstall` prints the count installed and next-step guidance.
|
||||
|
||||
Bundle format (inside the published package):
|
||||
|
||||
```
|
||||
assets/skills/
|
||||
manifest.json # { skills: [{ id, version, dir, files }] }
|
||||
crypto/
|
||||
SKILL.md
|
||||
examples/...
|
||||
events/
|
||||
SKILL.md
|
||||
…
|
||||
```
|
||||
|
||||
Versioning aligns with the monorepo: one bump per release, one changelog entry covering all skill changes.
|
||||
|
||||
## What changes
|
||||
|
||||
### 1. Two new packages
|
||||
|
||||
- `packages/platform/installer/` → `@suaveplan/installer@0.0.0`
|
||||
- `packages/platform/install-agent-skills/` → `@suaveplan/install-agent-skills@0.0.0`
|
||||
|
||||
(`platform` plugin matches `@suaveplan/cli` and `@suaveplan/readline`, the other CLI-tier packages.)
|
||||
|
||||
### 2. Workspace deps
|
||||
|
||||
`@suaveplan/installer`:
|
||||
- `@suaveplan/cli`, `@suaveplan/templates`, `@suaveplan/validation`, `@suaveplan/error`, `@suaveplan/types`, `@suaveplan/events`, `@suaveplan/logger`, `@opentelemetry/api`, `zod`. All `workspace:^` for internal, `catalog:` for external.
|
||||
|
||||
`@suaveplan/install-agent-skills`:
|
||||
- `@suaveplan/installer`, `@suaveplan/error`, `@suaveplan/types`, `zod`. All `workspace:^`.
|
||||
|
||||
### 3. Subpath exports
|
||||
|
||||
`@suaveplan/installer`: `.`, `./types`, `./errors`, `./host`, `./fs`, `./manifest`, `./testing` (test doubles for `InstallerContext`).
|
||||
|
||||
`@suaveplan/install-agent-skills`: `.`, `./manifest`. The `./manifest` subpath exposes the bundled skills index for tooling that wants to enumerate without invoking the installer.
|
||||
|
||||
### 4. CLI bin
|
||||
|
||||
`@suaveplan/installer` ships a `suaveplan-install` bin (also exposed as `npx @suaveplan/installer …`):
|
||||
|
||||
```bash
|
||||
npx @suaveplan/installer add agent-skills # default: all skills
|
||||
npx @suaveplan/installer add agent-skills --only crypto,events
|
||||
npx @suaveplan/installer add agent-skills --exclude legacy
|
||||
npx @suaveplan/installer add agent-skills --dry-run
|
||||
npx @suaveplan/installer add agent-skills --answers ./answers.json
|
||||
npx @suaveplan/installer list # discovered plugins
|
||||
```
|
||||
|
||||
Plugins are discovered by package-name convention (`@suaveplan/install-*` and `@<scope>/suaveplan-install-*` for third parties). Installer resolves them through the consumer's `node_modules` — same model as `eslint-plugin-*`.
|
||||
|
||||
### 5. Error codes
|
||||
|
||||
`INSTALLER` namespace via `defineCodes("INSTALLER", [...])`:
|
||||
|
||||
- `INSTALLER.PLUGIN_NOT_FOUND`
|
||||
- `INSTALLER.PLUGIN_LOAD_FAILED`
|
||||
- `INSTALLER.PLUGIN_INVALID_MANIFEST`
|
||||
- `INSTALLER.PROMPT_VALIDATION_FAILED`
|
||||
- `INSTALLER.PLAN_FAILED`
|
||||
- `INSTALLER.APPLY_CONFLICT`
|
||||
- `INSTALLER.APPLY_FAILED`
|
||||
- `INSTALLER.ROLLBACK_FAILED`
|
||||
- `INSTALLER.ABORTED`
|
||||
|
||||
`INSTALL_SKILLS` namespace for the skills plugin:
|
||||
|
||||
- `INSTALL_SKILLS.MANIFEST_INVALID`
|
||||
- `INSTALL_SKILLS.UNKNOWN_SKILL`
|
||||
- `INSTALL_SKILLS.TARGET_DIR_INVALID`
|
||||
|
||||
### 6. Observability
|
||||
|
||||
Every host stage (`detect`, `prompts`, `plan`, `apply`, `postInstall`) is wrapped in an OTel span (`installer.<stage>`) with attributes `plugin.id`, `plugin.version`, `dry_run`. `apply` records counters for files written / patched / skipped / conflicts.
|
||||
|
||||
### 7. Testing
|
||||
|
||||
- `@suaveplan/installer/testing` exports an in-memory `InstallerFs`, a no-TTY `InstallerCli` that scripts answers, and a `runPlugin(plugin, { answers })` harness for integration-style plugin tests with zero real disk I/O.
|
||||
- `@suaveplan/install-agent-skills` uses the harness to verify the bundled manifest plans correctly under `--only` / `--exclude` filters.
|
||||
|
||||
## Out of scope (future changes)
|
||||
|
||||
- Future verticals (`install-web-server`, `install-spa`, `install-desktop-app`, `install-mobile-app`) ship as separate change-ids once the host is stable.
|
||||
- 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).
|
||||
- A `create-suaveplan` thin wrapper bin that bundles installer + a curated default plugin set is a future packaging-only change; not introduced here.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- `bunx turbo lint typecheck test --filter=@suaveplan/installer` and `--filter=@suaveplan/install-agent-skills` green.
|
||||
- `bunx turbo test:coverage` ≥ 100 % istanbul on touched modules in both packages.
|
||||
- Sibling `.md` for every implementation `.ts` (`openspec/AGENTS.md` §4).
|
||||
- Spec deltas folded into `openspec/specs/installer/spec.md` and `openspec/specs/install-agent-skills/spec.md` on archive.
|
||||
- `INSTALLER-01`…`INSTALLER-09` and `INSTALL_SKILLS-01`…`INSTALL_SKILLS-05` have `**Verified by:**` lines pointing to `*.spec-conformance.test.ts`.
|
||||
- End-to-end test: `runPlugin(agentSkillsPlugin, { answers: { skills: "all" } })` against an in-memory fs writes the full skill set and reports zero conflicts on a fresh target.
|
||||
@@ -0,0 +1,33 @@
|
||||
# Install-Agent-Skills — Spec Delta (installer-port)
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### INSTALL_SKILLS-01 — Default plan installs every bundled skill
|
||||
|
||||
When no `--only` or `--exclude` filter is supplied, `agentSkillsPlugin.plan(answers, ctx)` MUST emit `FileOp[]` covering every entry in the bundled `assets/skills/manifest.json`. The plan MUST contain exactly one `mkdir` op per skill directory, one `write` op per file enumerated under that skill's `files` array, and exactly one `patch-json` op targeting the project skill index (creating it when absent). The default `targetDir` MUST be `.suaveplan/skills` relative to `ctx.cwd`.
|
||||
|
||||
**Verified by:** `src/plan/plan.spec-conformance.test.ts` (`INSTALL_SKILLS-01`).
|
||||
|
||||
### INSTALL_SKILLS-02 — `--only` and `--exclude` filters
|
||||
|
||||
When `answers.only` is a non-empty array, the plan MUST contain ops only for skills whose `id` appears in `only`. When `answers.exclude` is a non-empty array, the plan MUST omit ops for skills whose `id` appears in `exclude`. When both are supplied, `only` is applied first and `exclude` second. An `only` or `exclude` entry not present in the bundled manifest MUST surface as `InstallSkillsError(INSTALL_SKILLS.UNKNOWN_SKILL)` carrying the unknown id.
|
||||
|
||||
**Verified by:** `src/plan/plan.spec-conformance.test.ts` (`INSTALL_SKILLS-02`).
|
||||
|
||||
### INSTALL_SKILLS-03 — Skill-index patch shape
|
||||
|
||||
The `patch-json` op targeting the project skill index MUST use `JsonMerge` to upsert an entry of shape `{ id: string, version: string, dir: string, source: "@suaveplan/install-agent-skills", installedAt: string }` for each installed skill, where `installedAt` is an ISO 8601 UTC timestamp captured at plan time. The merge MUST replace any prior entry with the same `id` (idempotent re-install). The index file path MUST default to `<targetDir>/index.json` and MUST be created with conflict policy `merge`.
|
||||
|
||||
**Verified by:** `src/plan/plan.spec-conformance.test.ts` (`INSTALL_SKILLS-03`).
|
||||
|
||||
### INSTALL_SKILLS-04 — Bundled manifest validation
|
||||
|
||||
`loadBundledManifest()` MUST Zod-validate `assets/skills/manifest.json` at runtime. Required schema: `{ skills: Array<{ id: string, version: string (semver), dir: string, files: string[] }> }`. Validation failures MUST surface as `InstallSkillsError(INSTALL_SKILLS.MANIFEST_INVALID, { cause })`. The loader MUST NOT cache across invocations of `runPlugin` in the testing harness so that test fixtures can swap in alternate manifests.
|
||||
|
||||
**Verified by:** `src/manifest-loader.spec-conformance.test.ts` (`INSTALL_SKILLS-04`).
|
||||
|
||||
### INSTALL_SKILLS-05 — End-to-end install on fresh target
|
||||
|
||||
`runPlugin(agentSkillsPlugin, { answers: { targetDir: "/x" } })` against an empty `MemoryFs` MUST result in: every skill directory created under `/x`, every skill file written with the expected bytes, the skill index file written at `/x/index.json` with one entry per installed skill, zero conflicts reported, and a `postInstall` summary recording the installed count. The same harness invocation re-run against the resulting fs MUST be idempotent under default conflict policy `merge` for the index and `skip-if-exists` for already-present files (no `INSTALLER.APPLY_CONFLICT` raised).
|
||||
|
||||
**Verified by:** `src/plugin.e2e.spec-conformance.test.ts` (`INSTALL_SKILLS-05`).
|
||||
@@ -0,0 +1,57 @@
|
||||
# Installer — Spec Delta (installer-port)
|
||||
|
||||
## ADDED 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.<stage>` 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 <file>` 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<string, string>), `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 `@<scope>/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<InstallerErrorCode>`. 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`).
|
||||
123
openspec/archive/2026-04-28-installer-port/tasks.md
Normal file
123
openspec/archive/2026-04-28-installer-port/tasks.md
Normal file
@@ -0,0 +1,123 @@
|
||||
# Tasks: installer-port
|
||||
|
||||
## 1. Scaffold — `@suaveplan/installer`
|
||||
|
||||
- [x] 1.1 Create `packages/platform/installer/` with `package.json` (`@suaveplan/installer@0.0.0`), `tsconfig.json` extending `@suaveplan/typescript-config/library.json`, `vitest.config.ts` via `createTestConfig()`, root `README.md`.
|
||||
- [x] 1.2 Workspace deps: `@suaveplan/cli`, `@suaveplan/templates`, `@suaveplan/validation`, `@suaveplan/error`, `@suaveplan/types`, `@suaveplan/events`, `@suaveplan/logger`, `@opentelemetry/api`, `zod`. All `workspace:^` for internal, `catalog:` for external.
|
||||
- [x] 1.3 Subpath exports: `.`, `./types`, `./errors`, `./host`, `./fs`, `./manifest`, `./testing`.
|
||||
- [x] 1.4 `bin` field: `{ "suaveplan-install": "./dist/bin/suaveplan-install.js" }`.
|
||||
|
||||
## 2. Types and contract
|
||||
|
||||
- [x] 2.1 `src/types/installer.types.ts` — `InstallerContext`, `InstallerPlugin<TAnswers, THandle>`, `InstallerFs`, `InstallerCli`, `DetectResult`, `PromptSchema<T>`, `JsonMerge`, `TextEdit`.
|
||||
- [x] 2.2 `src/types/file-op.types.ts` — `FileOp` discriminated union (`write` | `patch-json` | `patch-text` | `mkdir` | `delete` | `rename`) with per-variant `conflict?: ConflictPolicy`.
|
||||
- [x] 2.3 `src/error-codes.ts` — `defineCodes("INSTALLER", [...])` with codes from proposal §5. Sibling `InstallerError` extends `SuaveplanError<InstallerErrorCode>`.
|
||||
- [x] 2.4 `src/schemas/` — Zod boundary schemas for `PluginManifest`, `FileOp`, CLI flags. Marker `// schema: boundary-only`.
|
||||
|
||||
## 3. Virtual filesystem (`./fs`)
|
||||
|
||||
- [x] 3.1 `src/fs/installer-fs.ts` — `InstallerFs` interface (`exists`, `readFile`, `readJson`, `stat`, `listDir`, `pathOf`).
|
||||
- [x] 3.2 `src/fs/disk-fs.ts` — concrete disk-backed implementation with abort propagation.
|
||||
- [x] 3.3 `src/fs/memory-fs.ts` — in-memory implementation (used by `./testing`).
|
||||
|
||||
## 4. Host pipeline (`./host`)
|
||||
|
||||
- [x] 4.1 `src/host/host.ts` — `Host` class orchestrating `detect → prompts → plan → apply → postInstall`. Each stage wrapped in OTel span `installer.<stage>` with `plugin.id`, `plugin.version`, `dry_run` attributes.
|
||||
- [x] 4.2 `src/host/detect.ts` — runs `plugin.detect?`, normalises result, emits `installer.detect` event.
|
||||
- [x] 4.3 `src/host/prompts.ts` — Zod-validates answers; `--answers <file>` bypass; `--no-interactive` rejects ambiguous prompts. Failure → `INSTALLER.PROMPT_VALIDATION_FAILED`.
|
||||
- [x] 4.4 `src/host/plan.ts` — invokes `plugin.plan(answers, ctx)`, validates returned `FileOp[]` against schema, returns plan record.
|
||||
- [x] 4.5 `src/host/apply/apply.ts` — staged-tmp + atomic rename per file, transaction journal, rollback on failure. `--dry-run` prints plan and exits before this stage.
|
||||
- [x] 4.6 `src/host/apply/conflict.ts` — resolves `error | overwrite | skip-if-exists | merge | prompt` per `FileOp`. `--force` collapses to `overwrite`.
|
||||
- [x] 4.7 `src/host/apply/json-merge.ts` — implements `JsonMerge` (deep-merge with array-replace semantics matching `@suaveplan/config`).
|
||||
- [x] 4.8 `src/host/apply/text-patch.ts` — applies `TextEdit[]` (anchor + insert/replace/delete), preserves trailing-newline policy.
|
||||
- [x] 4.9 `src/host/post-install.ts` — runs `plugin.postInstall?(handle, ctx)` with abort propagation.
|
||||
|
||||
## 5. Plugin manifest (`./manifest`)
|
||||
|
||||
- [x] 5.1 `src/manifest/manifest.ts` — `PluginManifestSchema` (Zod), `loadManifest(packagePath)`, `validateManifest(raw)`. Failure → `INSTALLER.PLUGIN_INVALID_MANIFEST`.
|
||||
- [x] 5.2 `src/manifest/discover.ts` — resolves `@suaveplan/install-*` and `@<scope>/suaveplan-install-*` from consumer `node_modules`. `INSTALLER.PLUGIN_NOT_FOUND` when missing.
|
||||
|
||||
## 6. CLI bin
|
||||
|
||||
- [x] 6.1 `src/bin/suaveplan-install.ts` — `cli("suaveplan-install", "0.1.0")` wiring `add <plugin>`, `list`, `--dry-run`, `--force`, `--no-interactive`, `--only`, `--exclude`, `--answers <file>`.
|
||||
- [x] 6.2 `add <plugin>` command — discovers + loads plugin, builds `InstallerContext`, runs `Host`.
|
||||
- [x] 6.3 `list` command — enumerates installed plugins from manifest discovery.
|
||||
|
||||
## 7. Testing harness (`./testing`)
|
||||
|
||||
- [x] 7.1 `src/testing/run-plugin.ts` — `runPlugin(plugin, { answers, fs?, cli? })` returns `{ ops, applied, fs }` for assertion. Zero real disk I/O.
|
||||
- [x] 7.2 `src/testing/script-cli.ts` — non-TTY `InstallerCli` that returns scripted answers and records prompts asked.
|
||||
- [x] 7.3 `src/testing/installer-fs.test-helpers.ts` — `createMemoryFs(seed?)`, `assertWrote(fs, path, predicate)`.
|
||||
|
||||
## 8. Spec — `@suaveplan/installer`
|
||||
|
||||
- [x] 8.1 `openspec/changes/installer-port/specs/installer/spec.md` with `INSTALLER-01`…`INSTALLER-09`, each carrying `**Verified by:**`.
|
||||
|
||||
## 9. Conformance tests — `@suaveplan/installer`
|
||||
|
||||
- [x] 9.1 `src/host/host.spec-conformance.test.ts` — `INSTALLER-01` (lifecycle order), `INSTALLER-02` (idempotent re-run), `INSTALLER-03` (abort propagation).
|
||||
- [x] 9.2 `src/host/apply/apply.spec-conformance.test.ts` — `INSTALLER-04` (transactional apply + rollback), `INSTALLER-05` (conflict policy).
|
||||
- [x] 9.3 `src/host/prompts.spec-conformance.test.ts` — `INSTALLER-06` (Zod validation, non-interactive bypass).
|
||||
- [x] 9.4 `src/manifest/manifest.spec-conformance.test.ts` — `INSTALLER-07` (manifest schema), `INSTALLER-08` (plugin discovery).
|
||||
- [x] 9.5 `src/error-codes.spec-conformance.test.ts` — `INSTALLER-09` (frozen registry, namespaced codes).
|
||||
|
||||
## 10. Scaffold — `@suaveplan/install-agent-skills`
|
||||
|
||||
- [x] 10.1 Create `packages/platform/install-agent-skills/` with `package.json` (`@suaveplan/install-agent-skills@0.0.0`), `tsconfig.json`, `vitest.config.ts`, `README.md`.
|
||||
- [x] 10.2 Workspace deps: `@suaveplan/installer`, `@suaveplan/error`, `@suaveplan/types`, `zod`. All `workspace:^`.
|
||||
- [x] 10.3 Subpath exports: `.`, `./manifest`.
|
||||
|
||||
## 11. Skills bundle assembly
|
||||
|
||||
- [x] 11.1 `scripts/build-skills-bundle.ts` — copies every `SKILL.md` + asset directory into `packages/platform/install-agent-skills/assets/skills/<name>/`.
|
||||
- [x] 11.2 `assets/skills/manifest.json` — generated `{ skills: [{ id, version, dir, files }] }`. Zod-validated at build time.
|
||||
- [x] 11.3 Pre-publish hook: `bun run build:bundle` runs the assembler before `bunx turbo build`.
|
||||
|
||||
## 12. Plugin implementation
|
||||
|
||||
- [x] 12.1 `src/plugin.ts` — exports default `agentSkillsPlugin: InstallerPlugin<AgentSkillsAnswers, AgentSkillsHandle>`.
|
||||
- [x] 12.2 `src/detect/detect.ts` — checks for `<targetDir>/skills/` and reports already-present skills.
|
||||
- [x] 12.3 `src/prompts/prompts.ts` — Zod schema: `targetDir` (default `.suaveplan/skills`), `only?: string[]`, `exclude?: string[]`, `overwrite?: boolean`.
|
||||
- [x] 12.4 `src/plan/plan.ts` — reads bundled manifest, applies `--only` / `--exclude` filter, emits `mkdir` + `write` ops + `patch-json` for skill index.
|
||||
- [x] 12.5 `src/apply/apply.ts` — thin pass-through; transaction lives in the host.
|
||||
- [x] 12.6 `src/post-install/post-install.ts` — prints `installed N skills to <dir>` plus next-steps.
|
||||
- [x] 12.7 `src/manifest-loader.ts` — runtime loader for the bundled `assets/skills/manifest.json`. Failure → `INSTALL_SKILLS.MANIFEST_INVALID`.
|
||||
- [x] 12.8 `src/error-codes.ts` — `defineCodes("INSTALL_SKILLS", [...])`.
|
||||
|
||||
## 13. Spec — `@suaveplan/install-agent-skills`
|
||||
|
||||
- [x] 13.1 `openspec/changes/installer-port/specs/install-agent-skills/spec.md` with `INSTALL_SKILLS-01`…`INSTALL_SKILLS-05`, each with `**Verified by:**`.
|
||||
|
||||
## 14. Conformance tests — `@suaveplan/install-agent-skills`
|
||||
|
||||
- [x] 14.1 `src/plan/plan.spec-conformance.test.ts` — `INSTALL_SKILLS-01` (default = all skills), `INSTALL_SKILLS-02` (`--only` / `--exclude` filter), `INSTALL_SKILLS-03` (skill-index patch shape).
|
||||
- [x] 14.2 `src/manifest-loader.spec-conformance.test.ts` — `INSTALL_SKILLS-04` (manifest validation).
|
||||
- [x] 14.3 `src/plugin.e2e.spec-conformance.test.ts` — `INSTALL_SKILLS-05` (end-to-end via `runPlugin` against memory fs writes full skill set, zero conflicts on fresh target).
|
||||
|
||||
## 15. Docs (sibling `.md` per implementation `.ts`)
|
||||
|
||||
- [x] 15.1 `@suaveplan/installer` — `.md` for every non-test/non-types/non-barrel `.ts` per `openspec/AGENTS.md` §4. Basic + Advanced examples mandatory.
|
||||
- [x] 15.2 `@suaveplan/install-agent-skills` — same.
|
||||
- [x] 15.3 Root `README.md` for both packages with quickstart.
|
||||
|
||||
## 16. Quality gates
|
||||
|
||||
- [x] 16.1 `bunx turbo lint typecheck test --filter=@suaveplan/installer` green.
|
||||
- [x] 16.2 `bunx turbo lint typecheck test --filter=@suaveplan/install-agent-skills` green.
|
||||
- [x] 16.3 `bunx turbo test:coverage --filter=@suaveplan/installer` 100 % istanbul on touched modules.
|
||||
- [x] 16.4 `bunx turbo test:coverage --filter=@suaveplan/install-agent-skills` 100 % istanbul on touched modules.
|
||||
- [x] 16.5 No banned patterns in `packages/platform/installer/src/` or `packages/platform/install-agent-skills/src/` per `openspec/AGENTS.md` §2.
|
||||
- [x] 16.6 No `biome-ignore` in either `src/` per `openspec/AGENTS.md` §2a.
|
||||
- [x] 16.7 File-size check: every `src/*.ts` ≤ 300 lines, every `*.test.ts` ≤ 500 lines.
|
||||
- [x] 16.8 Sibling `.md` audit per `openspec/AGENTS.md` §4 returns zero gaps.
|
||||
|
||||
## 17. Manifest + listing refresh
|
||||
|
||||
- [x] 17.1 Add `installer` and `install-agent-skills` entries to `CLAUDE.md` under `## Packages`.
|
||||
- [x] 17.2 `bun scripts/folder-reorg/generate-manifest.ts` to refresh `MANIFEST.md`.
|
||||
|
||||
## 18. Archive
|
||||
|
||||
- [x] 18.1 Fold `specs/installer/spec.md` delta into `openspec/specs/installer/spec.md`.
|
||||
- [x] 18.2 Fold `specs/install-agent-skills/spec.md` delta into `openspec/specs/install-agent-skills/spec.md`.
|
||||
- [x] 18.3 Move `openspec/changes/installer-port/` to `openspec/archive/2026-04-28-installer-port/`.
|
||||
Reference in New Issue
Block a user