Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions openspec/changes/template-catalog-v2/.openspec.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-08-17
111 changes: 111 additions & 0 deletions openspec/changes/template-catalog-v2/design.md
Original file line number Diff line number Diff line change
@@ -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.
67 changes: 67 additions & 0 deletions openspec/changes/template-catalog-v2/proposal.md
Original file line number Diff line number Diff line change
@@ -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/<id>/` 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.
50 changes: 50 additions & 0 deletions openspec/changes/template-catalog-v2/smoke-test-notes.md
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
@@ -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/<id>/` 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/<id>/`
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
96 changes: 96 additions & 0 deletions openspec/changes/template-catalog-v2/tasks.md
Original file line number Diff line number Diff line change
@@ -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 `<select>` for the agent picker, etc.). Refreshes the
template list and clears the selection if the deleted template was
selected.

## 4. Extension: delete command

- [x] 4.1 `commands.ts`: `openspec-ui.deleteProjectTemplate` — modal
confirmation (`showWarningMessage(..., { modal: true }, "Delete")`,
same pattern as `deleteChange`), calls `deleteProjectTemplate`,
refreshes the templates tree, info message on success.
- [x] 4.2 `package.json`: registered the command; context-menu entry
scoped to `"when": "viewItem == openspec-ui.projectTemplate"` only —
never offered for built-in items. Group `openspec@9`, matching
`deleteChange`'s destructive-action-last convention.
- [x] 4.3 `commands.test.ts`: confirms the modal gate (declining does not
delete); successful delete calls core + refreshes tree; unknown-id
error surfaces as a warning (not `showCommandError`) — matches
`customizeTemplate`'s existing `TemplateAlreadyExistsError → warning`
precedent for an "expected, named" error class, deviating slightly
from this task's original wording. 4 new tests, 27/27 passing.

## 5. Documentation

- [x] 5.1 `README.md`/relevant package README: note the new command and
that built-in templates are not deletable (code, not workspace data).

## 6. Verification, versioning, and smoke test

- [ ] 6.1 `npm run typecheck && npm run lint && npm run test` passes for
`packages/core`, `packages/server`, `packages/webui`,
`packages/extension`. Re-run `npm run verify` after `git add`/commit of
all new files.
- [x] 6.2 Bump `package.json` versions (minor) for all four touched
packages. core 0.15.0 → 0.16.0, server 1.6.0 → 1.7.0, webui 1.7.0 →
1.8.0, extension 0.9.0 → 0.10.0. Also added a `packages/extension/
CHANGELOG.md` 0.10.0 entry and updated `README.md`'s version table.
- [x] 6.3 Manual smoke test: customize the seed template for real,
delete it for real through both the standalone Templates tab and the
VS Code tree, confirm the directory is actually gone each time; verify
a built-in item never shows a delete action in either host. Record in
`smoke-test-notes.md`.
Standalone: driven live in a real browser against a real server and
this actual repository — deleted the real leftover project template
from the user's earlier manual testing, confirmed on disk it's gone,
confirmed the confirmation gate genuinely blocks an undismissed/
declined confirm, confirmed all 4 built-in templates never show
Delete. VS Code: no desktop-automation tool available in this
environment; relies on the 27 passing `commands.test.ts` cases plus
both hosts calling the identical core function verified live above.
Full detail in `smoke-test-notes.md`.
- [x] 6.4 `openspec change validate --strict template-catalog-v2` passes.
Loading
Loading