Skip to content

feat: register projects on init and delete from UI - #145

Merged
Minitour merged 2 commits into
version-2.0from
feat/init-register-and-ui-clean
Jul 31, 2026
Merged

feat: register projects on init and delete from UI#145
Minitour merged 2 commits into
version-2.0from
feat/init-register-and-ui-clean

Conversation

@Minitour

@Minitour Minitour commented Jul 31, 2026

Copy link
Copy Markdown
Member

Summary

  • capa init now registers the project in the capa DB and configures the server so it appears in the UI before install/wrap; existing capabilities files are left unchanged
  • UI delete (list + detail) runs the same cleanup as capa clean via DELETE /api/projects/:id, including stopping wrap sessions for that project and pruning its wrap workspaces, while keeping the capabilities file
  • Shared cleanProject is used by both CLI clean and the API; wrap sessions write wrap-session.json for reliable project-scoped stop

Test plan

  • Run capa init in a new directory — capabilities file created and project appears in UI
  • Run capa init again (or after deleting from UI) — file not recreated, project re-registered
  • Delete a project from the UI list and from the detail page — confirms wrap stop warning; project disappears; capabilities file remains
  • With capa wrap running for a project, delete that project from the UI — wrap process is stopped
  • capa clean still works from the CLI on an installed project
  • bun test src/cli/commands/__tests__/init-command.test.ts src/cli/utils/wrap/__tests__/sessions.test.ts

Made with Cursor

capa init now upserts the project and configures the server so it appears in the UI before install/wrap. UI delete runs shared clean (stops wrap sessions, prunes workspaces, clears DB) while keeping the capabilities file.

Co-authored-by: Cursor <cursoragent@cursor.com>
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Register projects during capa init and enable UI-driven project cleanup

✨ Enhancement 🧪 Tests 🕐 40+ Minutes

Grey Divider

AI Description

• Make capa init upsert the project into the DB and configure the server for UI visibility.
• Add DELETE /api/projects/:id to run the same cleanup as capa clean (capabilities kept).
• Persist wrap session metadata to reliably stop project-scoped wrap processes during cleanup.
Diagram

graph TD
  cliInit["CLI: capa init"] --> capsFile["Capabilities file"] --> server["Server: /api/projects"]
  cliInit --> db[("Capa DB")]
  uiDel["Web UI delete"] --> server --> clean["cleanProject()"] --> db
  clean --> wrapProc["Wrap processes"] --> wrapWS["~/.capa/workspaces"]
  clean -. "keeps" .-> capsFile
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Persist wrap session registry in the DB
  • ➕ Centralized source of truth for live sessions
  • ➕ Avoids scanning filesystem and process command lines
  • ➖ DB can be stale on crashes without robust heartbeats/cleanup
  • ➖ Ties wrap lifecycle more tightly to DB availability
2. Server-managed wrap lifecycle (wrap runs via server)
  • ➕ Server can track/terminate sessions without heuristics
  • ➕ Simplifies client-side process discovery
  • ➖ Bigger architectural shift (wrap execution model changes)
  • ➖ Adds coupling and potentially changes UX/permissions model
3. OS-native process-group/job-object termination
  • ➕ Reliable termination of wrap + watcher as a unit
  • ➕ Less dependency on command-line matching
  • ➖ Platform-specific complexity (Windows job objects, POSIX groups)
  • ➖ Harder to implement consistently across spawn strategies

Recommendation: The chosen approach (writing a per-workspace wrap-session.json plus targeted process discovery) is a good incremental design: it stays backward-compatible, improves project-scoped stop reliability, and keeps cleanup logic unified via cleanProject. The main thing to watch is drift between session files and actual processes; if this becomes operationally noisy, consider a DB-backed registry with heartbeats as the next step.

Files changed (17) +821 / -188

Enhancement (11) +527 / -46
init.tsRegister projects during 'capa init' and configure server +114/-19

Register projects during 'capa init' and configure server

• 'capa init' now reads or creates capabilities, ensures server is running, upserts the project into the DB, and calls the server configure endpoint so the project appears in the UI. Adds safety checks to refuse wrap workspaces and to detect conflicting project IDs registered at different paths.

src/cli/commands/init.ts

wrap.tsWrite and clear wrap session metadata for cleanup/stop +23/-3

Write and clear wrap session metadata for cleanup/stop

• Persists 'wrap-session.json' in the wrap cache root containing PIDs and paths for GUI and CLI wrap modes. Ensures session metadata is cleared on normal exit and stop-signal cleanup paths.

src/cli/commands/wrap.ts

session-file.tsIntroduce 'wrap-session.json' read/write helpers +61/-0

Introduce 'wrap-session.json' read/write helpers

• Adds utilities to write, clear, and read wrap session metadata in each cache root. Includes a 'pathsEqual' helper for platform-correct path equality semantics.

src/cli/utils/wrap/session-file.ts

sessions.tsAdd project-scoped wrap stop using session files and argv matching +136/-10

Add project-scoped wrap stop using session files and argv matching

• Extends wrap discovery to return PID + command line, adds robust path normalization and project matching, and reads per-workspace 'wrap-session.json' for more reliable targeting. Introduces 'stopWrapSessionsForProject' to terminate wrap/watch PIDs for a specific project.

src/cli/utils/wrap/sessions.ts

workspace.tsAdd project-scoped workspace pruning +32/-0

Add project-scoped workspace pruning

• Adds 'pruneWorkspacesForProject' to remove wrap cache directories whose marker matches a specific real project path. Reuses workspace marker metadata and handles platform-specific path comparisons.

src/cli/utils/wrap/workspace.ts

index.tsAdd 'DELETE /api/projects/:id' to clean and remove projects +63/-0

Add 'DELETE /api/projects/:id' to clean and remove projects

• Wires a new DELETE route that loads the project, refuses wrap workspace shadow paths, and runs shared 'cleanProject'. Clears server-side watchers and caches for the project and returns cleanup stats/warnings in the JSON response.

src/server/index.ts

session-manager.tsAdd session-manager API to clear cached project capabilities +8/-0

Add session-manager API to clear cached project capabilities

• Introduces 'clearProjectCapabilities' to drop in-memory capability caches and inflight state after project deletion/cleanup. Used to prevent stale capability reads for removed projects.

src/server/session-manager.ts

api.tsExpose projects delete API client +3/-0

Expose projects delete API client

• Adds 'projectsApi.delete(projectId)' wrapping 'DELETE /api/projects/:id' for use by UI mutations.

web-ui/src/features/projects/api.ts

ProjectsTable.tsxAdd delete action to projects table rows +38/-11

Add delete action to projects table rows

• Adds an Actions column with a trash icon per row, including confirmation and disabled state during mutation. Adjusts layout to keep navigation via link while preventing delete clicks from triggering row navigation.

web-ui/src/features/projects/components/ProjectsTable.tsx

hooks.tsAdd 'useDeleteProject' mutation and cache invalidation +12/-0

Add 'useDeleteProject' mutation and cache invalidation

• Introduces a React Query mutation that deletes a project and invalidates/removes project-scoped queries on success. Ensures list and detail views refresh correctly after deletion.

web-ui/src/features/projects/hooks.ts

ProjectDetailPage.tsxAdd delete project button to project detail view +37/-3

Add delete project button to project detail view

• Adds a delete button with confirmation and pending state, invoking 'useDeleteProject'. On success navigates back to the list; on error shows a message.

web-ui/src/pages/ProjectDetailPage.tsx

Refactor (2) +189 / -126
clean-project.tsAdd shared 'cleanProject' teardown routine +153/-0

Add shared 'cleanProject' teardown routine

• Introduces a reusable cleanup function that stops wrap sessions, removes managed artifacts, cleans provider wiring (agents/rules/hooks/MCP), prunes wrap workspaces, and deletes project DB rows. Explicitly preserves capabilities files and returns warnings plus cleanup counters.

src/cli/commands/clean-project.ts

clean.tsRefactor CLI 'clean' to use shared cleanup and report results +36/-126

Refactor CLI 'clean' to use shared cleanup and report results

• Replaces the prior task-based cleanup implementation with a call to 'cleanProject'. Adds concise informational output for stopped wrap sessions, removed files, pruned workspaces, and surfaced warnings.

src/cli/commands/clean.ts

Tests (2) +87 / -2
init-command.test.tsExpand init tests to validate project registration +53/-1

Expand init tests to validate project registration

• Adds fetch mocking and DB assertions to verify 'capa init' registers projects. Introduces a new test ensuring an existing capabilities file is preserved while still registering the project.

src/cli/commands/tests/init-command.test.ts

sessions.test.tsAdd unit tests for project-matching and path normalization +34/-1

Add unit tests for project-matching and path normalization

• Adds coverage for 'commandLineMatchesProject' to match both real project paths and workspace paths. Tests 'normalizePathForMatch' behavior, including Windows lowercasing expectations.

src/cli/utils/wrap/tests/sessions.test.ts

Documentation (2) +18 / -14
projects.jsonAdd delete UI strings and update empty-state copy +5/-2

Add delete UI strings and update empty-state copy

• Adds labels for the Actions column and delete confirmation text that explains wrap/session cleanup and capabilities preservation. Updates the empty-state description to mention 'capa init', 'install', and 'wrap' as registration triggers.

web-ui/src/locales/en/projects.json

ProjectsListPage.tsxUpdate empty-state messaging for registration commands +13/-12

Update empty-state messaging for registration commands

• Replaces the prior string-splitting approach with explicit inline code elements for 'capa init', 'capa install', and 'capa wrap'. Aligns list guidance with the new init registration behavior.

web-ui/src/pages/ProjectsListPage.tsx

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jul 31, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Wrong wrap PID matching ✓ Resolved 🐞 Bug ≡ Correctness
Description
Project-scoped wrap stopping matches project/workspace paths via raw substring search, so a project
like "/Users/me/proj" can match wrap commands for "/Users/me/proj2" and kill unrelated wrap
sessions. This is used by UI delete/clean via stopWrapSessionsForProject →
terminatePids(SIGTERM/SIGKILL).
Code

src/cli/utils/wrap/sessions.ts[R55-64]

+export function commandLineMatchesProject(
+  commandLine: string,
+  realProjectPath: string,
+  extraPaths: string[] = [],
+): boolean {
+  const haystack =
+    process.platform === 'win32' ? commandLine.replace(/\//g, '\\').toLowerCase() : commandLine;
+  const needles = [realProjectPath, ...extraPaths].map(normalizePathForMatch).filter(Boolean);
+  return needles.some((needle) => needle.length > 1 && haystack.includes(needle));
+}
Evidence
The matching predicate is a substring search, and its results are used to select PIDs which are then
SIGTERM/SIGKILL’ed during project clean/delete.

src/cli/utils/wrap/sessions.ts[55-64]
src/cli/utils/wrap/sessions.ts[187-213]
src/cli/commands/clean-project.ts[36-43]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`commandLineMatchesProject()` uses `haystack.includes(needle)` for path detection, which can produce false positives (e.g., `/proj` matches `/proj2`). Those false positives feed into `findWrapPidsForProject()` and then `terminatePids()`, potentially killing the wrong wrap process.
## Issue Context
Project-scoped stopping is now part of `cleanProject()` and therefore UI delete, so correctness needs to be strict: only kill wrap processes that belong to the exact project.
## Fix Focus Areas
- src/cli/utils/wrap/sessions.ts[55-199]
- src/cli/commands/clean-project.ts[36-45]
## Suggested fix direction
- Prefer session-file based discovery (wrap-session.json) as the authoritative source.
- For argv-based fallback, switch from substring matching to parsing known wrap command shapes:
- `capa __wrap_watch__ <real> <workspace> ...`
- `capa wrap <provider> --project <real>`
- Implement token/boundary-aware matching (e.g., match whole argv tokens after shell-quoting rules), not `includes()`.
- Add regression tests for the prefix case (`/proj` must not match `/proj2`).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

2. Stale project on failure ✓ Resolved 🐞 Bug ≡ Correctness
Description
init’s registerProject() upserts the project into the DB before calling POST
/api/projects/:id/configure; if configure fails, init exits but the DB row remains, leaving a broken
project entry. The server’s project list is DB-driven, so the project can appear in the UI even
though capabilities were never successfully configured.
Code

src/cli/commands/init.ts[R33-74]

+  const projectId = generateProjectId(identityPath);
+  const settings = await loadSettings();
+  const db = new CapaDatabase(getDatabasePath(settings));
+  try {
+    const existing = db.getProject(projectId);
+    if (existing) {
+      const existingPath = resolve(existing.path);
+      const samePath =
+        process.platform === 'win32'
+          ? existingPath.toLowerCase() === identityPath.toLowerCase()
+          : existingPath === identityPath;
+      if (!samePath) {
+        throw new Error(
+          `Project id "${projectId}" is already registered at a different path:\n` +
+            `  existing: ${existing.path}\n` +
+            `  this:     ${identityPath}\n` +
+            `Remove the conflicting project or reinstall from the correct directory.`,
+        );
+      }
+    } else {
+      db.upsertProject({ id: projectId, path: identityPath });
+    }
+  } finally {
+    db.close();
+  }
+
+  const response = await fetch(
+    `${serverUrl}/api/projects/${encodeURIComponent(projectId)}/configure`,
+    {
+      method: 'POST',
+      headers: {
+        'Content-Type': 'application/json',
+        Accept: 'application/json',
+      },
+      body: JSON.stringify(capabilities),
+      signal: AbortSignal.timeout(120000),
+    },
+  );
+  if (!response.ok) {
+    const text = await response.text().catch(() => response.statusText);
+    throw new Error(`Failed to configure project: ${text}`);
+  }
Evidence
The CLI writes the DB row before the network call, and the server UI list is derived from DB
projects, so a failure after upsert leaves a persisted entry.

src/cli/commands/init.ts[33-75]
src/server/index.ts[608-628]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`registerProject()` persists the project row before attempting server-side configuration. If the configure request fails (network error, non-OK status, timeout), `initCommand()` exits and the newly created DB row is left behind.
## Issue Context
`GET /api/projects` enumerates projects from the DB and only enriches counts from cached capabilities. A partially-registered project can therefore show up as an empty/unconfigured project.
## Fix Focus Areas
- src/cli/commands/init.ts[21-77]
- src/server/index.ts[608-628]
## Suggested fix direction
- Track whether the project was newly inserted vs pre-existing.
- If the configure call fails and the project was newly inserted, delete it (and associated rows) as rollback.
- Do not delete an existing project row on reconfigure failure; only rollback newly-created registrations.
- Consider surfacing a clearer error message that the project was not registered due to configure failure.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Wrap session write can throw ✓ Resolved 🐞 Bug ☼ Reliability
Description
wrapCommand() writes wrap-session.json synchronously without handling write errors, so
permission/disk issues can throw and abort wrap startup (and in the CLI path can leave the detached
watcher running without normal cleanup). The session file is auxiliary and shouldn’t be able to
crash the wrap flow.
Code

src/cli/commands/wrap.ts[R117-122]

+  writeWrapSession(prepared.cachePath, {
+    pid: process.pid,
+    realProjectPath: prepared.realProjectPath,
+    workspacePath: prepared.workspacePath,
+  });
+
Evidence
The wrap command calls writeWrapSession directly, and writeWrapSession performs an uncaught
synchronous writeFileSync which can throw on common IO failures.

src/cli/commands/wrap.ts[110-122]
src/cli/utils/wrap/session-file.ts[19-31]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`writeWrapSession()` uses `writeFileSync(...)` and callers in `wrapCommand()` do not catch exceptions. A filesystem error can crash `capa wrap` even though the session file is only used for cleanup/stop discovery.
## Issue Context
The write happens before launching the provider and (for GUI mode) before signal handlers/cleanup are fully established.
## Fix Focus Areas
- src/cli/commands/wrap.ts[117-122]
- src/cli/utils/wrap/session-file.ts[19-31]
## Suggested fix direction
- Wrap `writeWrapSession(...)` calls in try/catch and log a warning (do not abort wrap).
- Optionally update `writeWrapSession()` itself to catch and rethrow a typed error, or return a boolean success.
- In the CLI (detached worker) path, if the *second* session-file write fails after the worker starts, ensure the worker is stopped before exiting.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


4. Blocking delete handler IO ✓ Resolved 🐞 Bug ➹ Performance
Description
The new DELETE /api/projects/:id handler runs cleanProject() inside the server request path, and
cleanProject uses synchronous filesystem deletion (statSync/rmSync) that can block the server’s
event loop while removing large managed directories/workspaces. This can freeze the UI/API for the
duration of the cleanup.
Code

src/server/index.ts[R663-667]

+      const result = await cleanProject({
+        projectPath: project.path,
+        projectId,
+        db: this.db,
+      });
Evidence
The server delete handler directly awaits cleanProject, and cleanProject performs synchronous
stat/rm operations over managed files, which will execute on the request-handling thread.

src/server/index.ts[644-667]
src/cli/commands/clean-project.ts[66-83]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`handleDeleteProject()` calls `cleanProject()` during request handling. `cleanProject()` performs synchronous filesystem operations (notably recursive `rmSync`) which will block the Bun server thread, delaying unrelated requests.
## Issue Context
Project deletion can involve removing multiple directories (managed files + wrap workspaces). Doing this synchronously in the API path can make the UI appear hung.
## Fix Focus Areas
- src/server/index.ts[644-691]
- src/cli/commands/clean-project.ts[66-83]
- src/cli/utils/wrap/workspace.ts[267-288]
## Suggested fix direction
- Convert filesystem deletion to async (`fs.promises.rm`, `fs.promises.stat`) or run cleanup in a worker/background job.
- If moving to a background job, consider returning `202 Accepted` + job status endpoint so the UI can show progress.
- At minimum, avoid large `rmSync(..., { recursive: true })` from the request handler thread.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

To customize comments, go to the Qodo configuration screen, or learn more in the docs.

Qodo Logo

Comment thread src/cli/utils/wrap/sessions.ts
Comment thread src/cli/commands/init.ts Outdated
Comment thread src/cli/commands/wrap.ts Outdated
Comment thread src/server/index.ts
Use argv token path equality so /proj cannot kill /proj2 wraps; roll back newly inserted projects when configure fails; make wrap-session writes non-fatal; use async rm in clean/prune paths.

Co-authored-by: Cursor <cursoragent@cursor.com>
@Minitour

Copy link
Copy Markdown
Member Author

Addressed the Qodo review comments:

  1. Wrap pid matching — replaced substring includes() with argv token / pathsEqual matching (plus wrap_watch / --project parsing). Added a regression test for proj vs proj2.
  2. Stale project on configure failure — newly inserted DB rows are rolled back if configure fails; pre-existing projects are left alone.
  3. Wrap session write — writeWrapSession now returns boolean and never throws; wrap continues with a warning.
  4. Blocking delete I/O — managed-file and project wrap-workspace removal now use async fs.promises.rm (avoids sync recursive rmSync on the API path). Full background-job delete was deferred as out of scope.

@Minitour
Minitour merged commit 72da80e into version-2.0 Jul 31, 2026
@Minitour
Minitour deleted the feat/init-register-and-ui-clean branch July 31, 2026 18:47
@Minitour Minitour mentioned this pull request Aug 2, 2026
9 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant