github / github/spec-kit

[Feature]: `specify preset update` single update convenience wrapper around the existing remove and add operations

Đang mở
#4,427 13 bình luận 0 reaction 0 người được giao Xem trên GitHub
feature-assess feature-go triage-nice-to-have
Ngôn ngữ chính
Python
Star
137k
Fork
12.3k
Merge trung bình
2 ngày 12 giờ
Pull request đã merge (30 ngày)
159

Mô tả

## Summary

There is currently no `specify preset update ` command. The only way to move an installed preset to a new version is `preset remove ` followed by `preset add `. That workaround has real correctness problems beyond performance, because both `remove()` and `install_from_directory()` independently trigger full-stack reconciliation (composed commands/skills, and — when `constitution-sync` is enabled — `memory/constitution.md`) whenever the touched preset provides a constitution template. A version bump therefore reconciles the constitution twice: once against the stack with the preset temporarily absent, once against the reinstalled stack — with a real window in between where on-disk state reflects neither the old nor the new version.

This was raised as a valid feature-parity gap in #4415, with a request to open a tracking issue including acceptance criteria for: single/all updates, catalog-only eligibility, preserving priority/enabled state, rollback on failure, and command/skill/constitution reconciliation.

## Current behavior (traced in source)

- `PresetManager.install_from_directory` (`presets/__init__.py`) refuses to install over an already-installed preset id unless `force=True`, which itself calls `self.remove()` first — i.e. there is no in-place update path, only teardown+rebuild.
- `PresetResolver.collect_all_layers` / `resolve_content` recompute composed content from the *entire* installed-preset priority stack on every reconciliation call — never a diff against a single preset's previous version.
- `_reconcile_constitution` / `_materialize_constitution_template` unconditionally overwrite `.specify/memory/constitution.md` (when `constitution-sync` is enabled and the file is still detected as machine-generated) with no check for whether the resolved content actually changed.
- `PresetRegistry` stores `priority` and `enabled` per installed preset, but neither `remove()` nor `install_from_directory()` reads the *previous* entry back when reinstalling — so a manual remove+add workaround also risks losing an explicit `--priority` (if omitted, it silently falls back to the default of 10) and always resets `enabled` to `True` regardless of whether the preset was previously disabled.
- Neither `remove` nor `add` ever holds both the old and new manifest at the same time, so today there's no way to report what an update actually changed.

## Proposed command

```
specify preset update [--from URL | --dev PATH] [--priority N] [--all]
```
## ~~Acceptance criteria~~

### ~~Single-preset update~~
- [ ] ~~`specify preset update ` updates one named, already-installed preset to a new version from a supplied source (`--from URL` / `--dev PATH`) or, when eligible (see catalog-eligibility below), by re-resolving from its original catalog source.~~
- [ ] ~~Running `preset update` against a preset id that is **not** installed fails with a clear error directing the user to `preset add` instead — it must not silently fall through to an install path.~~
- [ ] ~~Old vs. new `preset.yml` manifests are diffed (added / removed / changed template names) before any files are touched, and this diff drives which command/skill files are registered vs. unregistered — not a blind re-register of everything the new manifest declares.~~

### ~~Bulk update (`--all`)~~
- [ ] ~~`specify preset update --all` checks every installed preset for a newer compatible version and updates only those that have one; presets already at the latest version are reported as skipped, not reinstalled with identical content.~~
- [ ] ~~A failure updating one preset in a bulk run is isolated to that preset (full rollback per the rollback criteria below) and does not abort or skip processing of the remaining presets.~~
- [ ] ~~Bulk update produces a per-preset outcome summary (updated / already-latest / failed-with-reason / skipped-and-why).~~
- [ ] ~~Behavior for **disabled** presets under `--all` is explicitly decided and documented (skip entirely vs. update files but leave disabled) rather than left as an implementation accident.~~

### ~~Catalog-only eligibility~~
- [ ] ~~When no `--from`/`--dev` is supplied, automatic re-resolution of "the newer version" is only attempted when the preset's original install source is a catalog entry (or otherwise durably re-resolvable); presets installed via `--dev` or a one-off `--from URL` fail with a clear "source not re-resolvable, supply --from/--dev explicitly" error rather than failing silently or guessing.~~
- [ ] ~~Update re-applies the same catalog `install-allowed` / discovery-only check that `preset add` already enforces (`_install_allowed`) — a discovery-only catalog preset cannot be updated any more than it can be freshly installed, if that's still the intended catalog semantics.~~
- [ ] ~~This dovetails with #3533 (recording install source/ref for extensions, presets, and workflows installed via `--from`, to support `update`) — provenance persisted for that issue should be reused here rather than re-invented per artifact type.~~

### ~~Preserving priority / enabled state~~
- [ ] ~~Omitting `--priority` on `preset update` preserves the preset's currently registered priority (read via the existing `PresetRegistry.get(pack_id)["priority"]`) rather than resetting to the default of 10.~~
- [ ] ~~Passing `--priority N` explicitly still allows a deliberate reprioritize as part of the same update call.~~
- [ ] ~~A preset's `enabled`/`disabled` state (set via `preset enable`/`preset disable`) is preserved across an update — updating a disabled preset must not silently re-enable it.~~

### ~~Rollback on failure~~
- [ ] ~~Compatibility/manifest validation failures on the new version occur before any live preset directory or registry entry is modified — a validation failure is a true no-op.~~
- [ ] ~~New content is staged (e.g. `presets/.staging/`) and validated before being swapped in; if staging or pre-swap validation fails, the previously installed version remains fully installed and functional.~~
- [ ] ~~The swap from old to new content is atomic at the directory level (rename-based, not a recursive file-by-file overwrite), minimizing any window where the preset directory is partial or missing.~~
- [ ] ~~A documented recovery path exists for the rare case of a crash during the swap step itself (e.g. a `.bak` directory retained, or a `preset repair` command).~~
- [ ] ~~A reconciliation failure *after* a successful swap does not roll back the already-successful content/registry update (the new version is genuinely installed) — it surfaces a clear, actionable warning instead of silently succeeding or incorrectly reverting a good install.~~

### ~~Command / skill / constitution reconciliation~~
- [ ] ~~Command and skill registration reconciliation runs exactly once per update, over the union of added/removed/changed names from the manifest diff — not once via an implicit "remove" pass and again via an implicit "add" pass.~~
- [ ] ~~`memory/constitution.md` reconciliation (when `constitution-sync` is enabled) runs at most once per update, against the final post-update stack — never against an intermediate stack with the preset transiently absent.~~
- [ ] ~~Reconciliation compares the newly resolved constitution content's hash against the current on-disk content and skips the write entirely when they match, so a version bump that doesn't change the resolved constitution produces zero file changes.~~
- [ ] ~~The existing "don't clobber a hand-edited constitution" guard (`_constitution_is_generated`, based on the provenance sidecar hash) is reused unchanged by `update()`, not reimplemented.~~
- [ ] ~~`preset update` prints a real diff summary (e.g. `+2 commands, -1 command, constitution unchanged, priority kept at 10`), since it's the first place both the old and new manifest are available together.~~

### ~~Compatibility with existing workflows~~
- [ ] ~~`preset add` and `preset remove` continue to behave exactly as they do today; `preset update` is additive, not a replacement.~~
- [ ] ~~A new preset version that renames the preset id or otherwise restructures its layout in a way a diff can't reconcile causes `update` to fail clearly and point the user to the remove+add workaround, rather than attempting a partial/corrupt diff-based update.~~

## Implementation

I plan to work on this together with @markuswondrak.

## Related

- #4415 — originating discussion
- #3533 — install source/ref provenance for `--from` installs, needed for catalog-eligible auto-resolution here
- #3950, #3873, #3995 — constitution-sync design and reconciliation-on-force work this builds on

Hướng dẫn đóng góp

Mở hướng dẫn đóng góp

Hướng nghiên cứu

Start with PresetManager.install_from_directory and remove() in presets/__init__.py, then trace PresetResolver.collect_all_layers/resolve_content, PresetRegistry.get(), and the constitution reconciliation helpers. Define the update command around the stated single and bulk-update criteria, preserving registry state and validating before changes. Done includes isolated rollback behavior, one final reconciliation, and a useful outcome or diff summary.

Do mô hình lập chỉ mục viết ra từ nội dung của issue.

Đánh giá

Công nghệ
python
Lĩnh vực
cli, tooling
Loại issue
Tính năng
Độ khó
5/5
Thời gian dự kiến
Hơn một tuần
Mức độ hoạt động
Sôi nổi
Độ rõ ràng
Khá rõ ràng
Mức phù hợp với người mới
35/100

Nhận issue mới trong hộp thư của bạn

Bản tóm tắt ngắn những issue GitHub phù hợp với người mới.