diff --git a/README.md b/README.md index 6f4d2a13..503ea1a4 100644 --- a/README.md +++ b/README.md @@ -162,10 +162,10 @@ release artifact. Current release versions are: | Package | Version | Release role | | --- | ---: | --- | -| `@openspec-ui/core` | 0.15.0 | Shared behavior and persistence contract | -| `openspec-ui-vscode` | 0.9.0 | VS Code delivery | -| `@openspec-ui/server` | 1.6.0 | Standalone server delivery | -| `@openspec-ui/webui` | 1.7.0 | Shared browser UI | +| `@openspec-ui/core` | 0.16.0 | Shared behavior and persistence contract | +| `openspec-ui-vscode` | 0.10.0 | VS Code delivery | +| `@openspec-ui/server` | 1.7.0 | Standalone server delivery | +| `@openspec-ui/webui` | 1.8.0 | Shared browser UI | | `@openspec-ui/cli` | 0.1.0 | CI merge-gate delivery | `@vscode/vsce` (the extension's packager) already names the built diff --git a/openspec/changes/template-catalog-v2/.openspec.yaml b/openspec/changes/template-catalog-v2/.openspec.yaml new file mode 100644 index 00000000..14963146 --- /dev/null +++ b/openspec/changes/template-catalog-v2/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-08-17 diff --git a/openspec/changes/template-catalog-v2/design.md b/openspec/changes/template-catalog-v2/design.md new file mode 100644 index 00000000..77ba0c5b --- /dev/null +++ b/openspec/changes/template-catalog-v2/design.md @@ -0,0 +1,111 @@ +## Context + +`template-catalog.ts`'s `templateDir(workspaceRoot, id)` already guards +path construction with `assertValidTemplateId()` (`^[a-z0-9][a-z0-9-]*$` +— no `..`, no path separators), used by `customizeTemplate()` today. +`deleteProjectTemplate` reuses this exact helper rather than building a +second path — the same safety guarantee `customizeTemplate` already has +applies automatically, not by re-derivation. + +Built-in templates are TypeScript modules in `packages/core/src/ +templates/*.ts` (see `template-catalog`'s own earlier design.md, +"Built-in templates ship as TypeScript modules, not files on disk") — +there is no file on disk to delete for them even in principle; "delete" +only ever makes sense for the project-level, file-backed form. + +## Goals / Non-Goals + +**Goals:** +- Symmetric CRUD for project-level templates: create (customize), + read (list), update (edit files directly, already true), delete (new). +- Both delivery targets get the same capability, matching every other + templates action so far (Customize, Insert both work in standalone and + VS Code). + +**Non-Goals:** +- No delete action for built-in templates, anywhere — not exposed in + either tree/tab's UI at all (see Decisions), not just disabled. +- No soft-delete/trash/undo. A project-level template is a plain + directory of files a user already edits directly with no round-trip + requirement (per `template-catalog`'s existing "editable by the user + through normal file editing" requirement) — deleting it is exactly as + reversible as deleting any other file the user manages themselves + (i.e., via their own git history/editor undo, not a product-level + trash can). Matches this repository's own stated philosophy that + destructive actions get a confirm step, not an undo system, elsewhere + (`deleteChange`). +- No new "are you sure" abstraction — reuses the existing + `vscode.window.showWarningMessage(..., { modal: true }, "Delete")` + pattern `deleteChange` already established, not a new confirmation + component. + +## Decisions + +### Delete built-in templates is not offered at all, not merely disabled + +Rejected showing a "Delete" action on built-in tree items that always +errors or is grayed out: built-in templates are code shipped with the +extension/server, not workspace data — offering a delete affordance that +can never do anything is confusing UI, not a safety feature. The existing +context-menu `"when": "viewItem == openspec-ui.projectTemplate"` pattern +(already used for `insertTemplateIntoChange`'s project-only half, and for +`copyTasksAsTemplate`) is reused verbatim for the new command — no new +`when`-clause vocabulary needed, `viewItem` already distinguishes the two +origins. + +### `deleteProjectTemplate` reuses `templateDir()`, not a second lookup + +Guarantees the exact same `assertValidTemplateId()` guard `customizeTemplate` +already relies on, and produces a clear, existing-shaped error +(`UnknownProjectTemplateError`, mirroring `UnknownBuiltInTemplateError`) +when the id has no matching directory — rather than a raw `ENOENT` from +`fs.rm` leaking through, which `sendJson` on the server side would report +as a generic 500, not the deliberate 404 every other +"resource not found" case in this API gets. + +### Server responds 404 for an unknown project template, matching `customizeTemplate`'s 404 for an unknown built-in id + +Consistent with the existing `handleTemplatesCustomizeRequest`'s +`UnknownBuiltInTemplateError → 404` mapping — the new delete endpoint +follows the identical shape (`UnknownProjectTemplateError → 404`) rather +than inventing a different status code for a symmetric "not found" case. + +### New built-in templates: one language-agnostic, two language-specific + +`flat-to-hexagonal-architecture` is written without assuming a source +language (the original brainstorm explicitly asked "independent of +language, or dependent?" — answered here as independent: the proposal/ +design/tasks content describes layering and dependency-direction changes +that apply the same way regardless of implementation language, with +inline fill-in markers for language-specific file paths, same convention +`python-sqlalchemy-alembic` already uses for project-specific blanks). +`flask-to-fastapi` and `node-vitest-testing-baseline` are deliberately +language/framework-specific, like the existing seed template — a +language-agnostic *and* fully concrete template for the same pattern +would need two content variants to stay actionable, which is unnecessary +complexity for a first example of this pattern. + +## Risks / Trade-offs + +- **[Risk]** `fs.rm(dir, { recursive: true })` permanently removes + whatever a user has since edited into that directory — there is no + recovery path from within the product. → **Mitigation**: accepted, + same as Non-Goals states; mitigated by the existing modal confirmation + step, and users are expected to have these files under their own git + history like any other workspace content. +- **[Risk]** Three more built-in templates is more ongoing-accuracy + surface to keep current as frameworks evolve (same risk already + accepted for the seed template in `template-catalog`'s own design.md). + → **Mitigation**: kept each deliberately narrow (a handful of + well-established, version-stable steps), same scoping discipline as + the seed template, not an attempt at a comprehensive guide. + +## Migration Plan + +- No data migration; purely additive (new function/endpoint/command, + three new built-in template modules). +- Version bump (minor) for `@openspec-ui/core`, `@openspec-ui/server`, + `@openspec-ui/webui`, `openspec-ui-vscode`. +- Rollback: revert the package changes together; no persisted state + beyond project-level template directories a user explicitly created or + deleted themselves. diff --git a/openspec/changes/template-catalog-v2/proposal.md b/openspec/changes/template-catalog-v2/proposal.md new file mode 100644 index 00000000..11370d7c --- /dev/null +++ b/openspec/changes/template-catalog-v2/proposal.md @@ -0,0 +1,67 @@ +## Why + +`openspec/specs/template-catalog/spec.md`'s "Project-level templates live +in the user's repository" requirement says project templates are "fully +editable by the user through normal file editing" — but there is no way +to *remove* one through either delivery target once created, only +through manually deleting the directory outside the product. This is a +specific gap raised in review while dogfooding "Customize" for real: +after customizing `python-sqlalchemy-alembic` to try the feature, there +was no way to clean it back up from the VS Code tree or the standalone +Templates tab — a real, asymmetric CRUD gap (create/read/update via file +editing all exist, delete does not), not an intentional non-goal (the +original design.md's Non-Goals list covers "no create from scratch" and +"no auto-reconciliation," never delete). + +Separately, the built-in catalog currently has exactly one seed template +(`python-sqlalchemy-alembic`) — a Python-only example, in a repository +whose own tooling is TypeScript/Node.js, and whose project-detection +scope (per this session's original brainstorm) explicitly names Python, +JavaScript, ASP.NET Core, and Node.js as target languages. Two more +patterns were already discussed and approved as good candidates +(Flask→FastAPI migration, flat→hexagonal architecture migration); this +proposal also adds a Node.js/TypeScript-oriented template so the catalog +is not exclusively Python. + +## What Changes + +- Add `deleteProjectTemplate(workspaceRoot, id)` to `packages/core` — + removes `openspec/templates//` entirely. Built-in templates are + code (`packages/core/src/templates/*.ts`), not deletable through this + or any UI — this is explicitly project-level only, per review feedback. +- Expose it as `POST /api/templates/delete` (standalone) and a VS Code + command `openspec-ui.deleteProjectTemplate`, scoped to + `viewItem == openspec-ui.projectTemplate` only (the tree's built-in + items never get a delete action). Both require an explicit confirm + step before removing anything, matching `deleteChange`'s existing + confirmation-modal pattern. +- Add three built-in templates: `flask-to-fastapi` (Python), a + language-agnostic `flat-to-hexagonal-architecture`, and + `node-vitest-testing-baseline` (Node.js/TypeScript) — registered in + `packages/core/src/templates/index.ts` alongside the existing seed. + +## Capabilities + +### New Capabilities + +(none) + +### Modified Capabilities + +- `template-catalog`: project-level templates can now be deleted through + both delivery targets, not only created/edited/read; the built-in + catalog grows from one seed template to four, covering more of the + languages this product's own project-detection scope already names. + +## Impact + +- `packages/core/src/template-catalog.ts` (new function, new error + class), `packages/core/src/templates/` (three new files + registry + update). +- `packages/server/src/rest.ts`, `server.ts` (new endpoint). +- `packages/webui/src/template-catalog-client.ts` (new client function), + `standalone-entry.tsx` (delete button, project templates only). +- `packages/extension/src/commands.ts`, `package.json` (new command + + context-menu entry), `src/tree/templates-tree.ts` untouched (context + value already distinguishes built-in vs. project). +- No change to the command/event protocol. diff --git a/openspec/changes/template-catalog-v2/smoke-test-notes.md b/openspec/changes/template-catalog-v2/smoke-test-notes.md new file mode 100644 index 00000000..bcbf9e6d --- /dev/null +++ b/openspec/changes/template-catalog-v2/smoke-test-notes.md @@ -0,0 +1,50 @@ +## Standalone (real browser, real server, real filesystem) + +Built and started the real server (`npm run build --workspace +@openspec-ui/server`, `npm run start --workspace @openspec-ui/server -- +C:/Prog/OpenSpec-UI 4317 --allow-external-cwd`), opened it in the Browser +pane, pointed the workspace root at this repository itself, and drove the +Templates tab for real: + +- **All 4 built-in templates listed correctly**: the seed + `python-sqlalchemy-alembic` plus the three new ones + (`flask-to-fastapi`, `flat-to-hexagonal-architecture`, + `node-vitest-testing-baseline`), each with "Select"/"Customize", no + "Delete" — confirms built-in items never offer delete, for real, not + just by `when`-clause inspection. +- **The real leftover project template from earlier manual testing** + (`openspec/templates/python-sqlalchemy-alembic/`, created via + "Customize" before this change existed) showed a "Delete" button. +- **Confirmation gate is real, not decorative**: clicking "Delete" first + triggered a native `window.confirm()`; the Browser tool's headless + environment auto-dismisses native dialogs (returns `false`), and the + directory was confirmed still present on disk afterward (`ls + openspec/templates/` still showed it) — proving the code path actually + respects a declined confirmation rather than deleting unconditionally. +- **Confirmed delete**: overrode `window.confirm` to return `true` (via + the Browser tool's JS execution, not a code change) and clicked + "Delete" again. The UI immediately showed "Deleted + openspec/templates/python-sqlalchemy-alembic/." and the row disappeared + from the table. Checked the actual filesystem afterward + (`ls openspec/templates/`) — the directory is gone. This is the exact + artifact the user asked about being unable to delete; it is now + cleaned up for real, not just a mocked test. + +## VS Code extension: unit-level only, not driven live + +Could not drive the actual VS Code desktop UI from this environment (no +desktop-app automation tool available, only a web Browser). Coverage for +this side rests on: 27 passing `commands.test.ts` tests (including the +new delete command's modal-gate, success, decline, and unknown-id-warning +cases) plus the fact that the VS Code command calls the exact same +`deleteProjectTemplate()` core function just verified working end-to-end +above — same gap class noted in `agent-selection`'s own smoke-test-notes +("webview-level DOM interaction not separately driven"). + +## Original "pwsh" error report + +Not reproduced or explained by this change — see the in-chat investigation: +`customizeTemplate`'s command handler and its core implementation are +confirmed pure `fs.mkdir`/`fs.writeFile`, with zero process/terminal/shell +involvement anywhere in the call path. Whatever the user saw was not +caused by this repository's own template-customize code. diff --git a/openspec/changes/template-catalog-v2/specs/template-catalog/spec.md b/openspec/changes/template-catalog-v2/specs/template-catalog/spec.md new file mode 100644 index 00000000..5fa29f1e --- /dev/null +++ b/openspec/changes/template-catalog-v2/specs/template-catalog/spec.md @@ -0,0 +1,29 @@ +## ADDED Requirements + +### Requirement: Project-level templates can be deleted through the product + +The system SHALL let the user permanently delete a project-level +template (`openspec/templates//` and its contents) through both +delivery targets, after an explicit confirmation step. Built-in +templates SHALL NOT offer a delete action anywhere in either delivery +target's UI — they have no on-disk representation in the target +workspace to delete. + +#### Scenario: Deleting an existing project-level template + +- **WHEN** the user confirms deletion of project-level template `X` +- **THEN** `openspec/templates/X/` no longer exists, and `X` no longer + appears in the template list + +#### Scenario: Deleting an unknown project-level template id + +- **WHEN** the requested id has no matching `openspec/templates//` + directory +- **THEN** the system reports a clear "not found" error and makes no + filesystem change + +#### Scenario: Built-in tree/tab items never offer delete + +- **WHEN** the user views a built-in template in either delivery + target's UI +- **THEN** no delete action is available for it diff --git a/openspec/changes/template-catalog-v2/tasks.md b/openspec/changes/template-catalog-v2/tasks.md new file mode 100644 index 00000000..e7623c89 --- /dev/null +++ b/openspec/changes/template-catalog-v2/tasks.md @@ -0,0 +1,96 @@ +## 1. Core: delete + new built-in templates + +- [x] 1.1 `packages/core/src/template-catalog.ts`: add + `UnknownProjectTemplateError` and `deleteProjectTemplate(workspaceRoot, + id)` — reuses `templateDir()` for the safe path, throws + `UnknownProjectTemplateError` if the directory does not exist, + otherwise `fs.rm(dir, { recursive: true, force: true })`. +- [x] 1.2 `template-catalog.test.ts`: deletes an existing project + template (directory gone afterward); throws + `UnknownProjectTemplateError` for an unknown id, without touching the + filesystem. 2 new tests, 11/11 passing. +- [x] 1.3 Add `packages/core/src/templates/flask-to-fastapi.ts`, + `flat-to-hexagonal-architecture.ts`, `node-vitest-testing-baseline.ts`; + register all three in `templates/index.ts`'s `BUILT_IN_TEMPLATES`. +- [x] 1.4 Export `deleteProjectTemplate`/`UnknownProjectTemplateError` + from `index.ts` (not `browser.ts` — same fs-backed reasoning as the + rest of `template-catalog.ts`'s write path). + Covered automatically by the existing `export * from + "./template-catalog.js"` in `index.ts` — no new export line needed. + +## 2. Server: delete endpoint + +- [x] 2.1 `packages/server/src/rest.ts`: `handleTemplatesDeleteRequest` + (`{cwd, id}` in, `authorizeCwd`'d, 404 on + `UnknownProjectTemplateError`, 200 `{ ok: true }` otherwise); wire + `POST /api/templates/delete` in `server.ts`. Reuses the existing + `isTemplatesCustomizeRequest` validator (`{cwd, id}` is the identical + shape) instead of duplicating a validator. +- [x] 2.2 `server.test.ts`: deletes a real project-level template fixture + (200, directory gone — verified via a follow-up `/api/templates/list` + call); 404 for an unknown id. 2 new tests, 32/32 passing. + +## 3. Webui: client + standalone UI + +- [x] 3.1 `template-catalog-client.ts`: `deleteProjectTemplate(request, + cwd, id)` thin wrapper, throwing on non-200 with the server's error + message. +- [x] 3.2 `template-catalog-client.test.ts`: covers success + not-found. + 2 new tests, 6/6 passing. +- [x] 3.3 `standalone-entry.tsx`: "Delete" button in the Templates tab, + rendered only for `origin === "project"` entries. No existing + confirmation pattern was found anywhere in `webui` (`deleteChange` has + no standalone UI at all — VS Code-only) — used the platform-native + `window.confirm()` rather than inventing a custom modal component, + matching this codebase's general "use the native primitive" bias + elsewhere (native `