Skip to content

CS-10630: Reimplement boxel realm remove command#4655

Merged
FadhlanR merged 3 commits intomainfrom
cs-10630-reimplement-boxel-realm-remove-command
May 7, 2026
Merged

CS-10630: Reimplement boxel realm remove command#4655
FadhlanR merged 3 commits intomainfrom
cs-10630-reimplement-boxel-realm-remove-command

Conversation

@FadhlanR
Copy link
Copy Markdown
Contributor

@FadhlanR FadhlanR commented May 5, 2026

Summary

  • Port boxel remove from the standalone cardstack/boxel-cli into packages/boxel-cli/src/commands/realm/remove.ts as boxel realm remove <realm-url>.
  • Hard delete: calls DELETE /_delete-realm (filesystem + indexer + registry), then unlinks the URL from the active profile's app.boxel.realms Matrix account_data list. Mirrors the host UI's workspace delete flow (packages/host/app/components/operator-mode/workspace-chooser/workspace.gts:999-1005) and inverts the existing boxel realm create/_create-realm pattern.
  • After removal the realm name is recyclable: boxel realm create <same-name> now succeeds.

Why hard delete

The first iteration of this PR was soft-only (Matrix unlink only). Hassan flagged that as misleading — the user thinks they removed it, but server files / index rows / registry entry remain, and recreating with the same name fails with "already exists". boxel realm remove was the asymmetric outlier next to the host UI and realm create. Now it matches.

Safety

  • TTY confirmation prompt before the destructive call; bypass with -y, --yes.
  • Non-interactive invocation without --yes errors out instead of deleting silently.
  • --dry-run previews previousCount → nextCount without sending either request.
  • 403 from the server (caller doesn't own the realm) surfaces a clear "you do not own this realm" error; no silent fallback to soft-unlink.
  • removeRealm() returns a structured result on every path (never process.exits), with serverDeleted / unlinked flags so callers can detect the rare partial-success state (server gone, Matrix list still references the URL).

Linear

CS-10630 — part of Incorporate Boxel CLI to Monorepo.

Test plan

  • pnpm --filter @cardstack/boxel-cli lint:types
  • pnpm --filter @cardstack/boxel-cli lint:js
  • pnpm --filter @cardstack/boxel-cli exec vitest run tests/integration/realm-remove.test.ts (7 tests pass: hard-delete, name-recyclable, notInList, dry-run-server-silent, trailing-slash, 403-not-owner, no-active-profile). Requires test-PG (port 55436) and Synapse running locally with realm users registered.
  • Manual TTY: boxel realm remove <url> prompts y/N, deletes on y, cancels on N.
  • Manual: boxel realm remove <url> --yes skips the prompt.
  • Manual: boxel realm remove <url> < /dev/null errors out (non-TTY without --yes).
  • Manual: boxel realm remove <url> --dry-run prints the count delta and exits without writing.
  • Manual: after remove, boxel realm create <same-name> "Same Name" succeeds (no "already exists" error).
  • Manual: boxel realm remove <url-of-realm-you-do-not-own> fails with a clear 403 message and exit code 1.

🤖 Generated with Claude Code

@FadhlanR FadhlanR marked this pull request as ready for review May 5, 2026 13:35
@FadhlanR FadhlanR requested a review from a team May 5, 2026 13:36
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 845a5fc7fc

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/boxel-cli/src/lib/auth.ts Outdated
@habdelra
Copy link
Copy Markdown
Contributor

habdelra commented May 5, 2026

so this seems very subtle--its doesn't actually remove the realm, it just unlinks the realm from the user's matrix data. if the user tries to make a new realm with the same name they will get an error that this realm already exists--which is probably confusing since they thought they removed it. is it really the intent that we are just "unlinking" the realm here instead of removing it? if so should we name it something that makes that more clear?

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR ports the standalone CLI “remove realm” behavior into the monorepo as boxel realm remove <realm-url>, implementing a soft remove that updates the active profile’s app.boxel.realms Matrix account_data list (without deleting server-side realm files).

Changes:

  • Added a new boxel realm remove command with --dry-run and --yes support plus a programmatic removeRealm() API.
  • Introduced ProfileManager.removeFromUserRealms() and removeRealmFromMatrixAccountData() to mirror existing “add realm” helpers.
  • Added an integration test suite covering removal, dry-run behavior, normalization, and “soft remove” semantics.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
packages/boxel-cli/tests/integration/realm-remove.test.ts New integration coverage for removing realms from the user list and verifying server realms remain accessible.
packages/boxel-cli/src/lib/profile-manager.ts Adds removeFromUserRealms() wrapper to update Matrix account_data via the auth helper.
packages/boxel-cli/src/lib/auth.ts Adds removeRealmFromMatrixAccountData() to remove a realm URL from app.boxel.realms.
packages/boxel-cli/src/commands/realm/remove.ts New command implementation + programmatic removeRealm() API with confirmation/dry-run flows.
packages/boxel-cli/src/commands/realm/index.ts Registers the new remove subcommand under boxel realm.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/boxel-cli/src/lib/auth.ts Outdated
Comment thread packages/boxel-cli/src/commands/realm/remove.ts
@FadhlanR
Copy link
Copy Markdown
Contributor Author

FadhlanR commented May 6, 2026

so this seems very subtle--its doesn't actually remove the realm, it just unlinks the realm from the user's matrix data. if the user tries to make a new realm with the same name they will get an error that this realm already exists--which is probably confusing since they thought they removed it. is it really the intent that we are just "unlinking" the realm here instead of removing it? if so should we name it something that makes that more clear?

That's exactly the issue. The first pass ported the soft-only behavior verbatim from the standalone CLI, which predates the monorepo's /_delete-realm endpoint. Meanwhile the host UI's workspace delete (workspace.gts:999-1005) and boxel realm create/_create-realm were both already on the hard-delete-plus-Matrix-update pattern, so realm remove was the odd one out.

Fixed in 8151e40:

  • removeRealm() now calls DELETE /_delete-realm first, then unlinks the URL from app.boxel.realms. Mirrors the host UI flow and inverts realm create.
  • 403 (caller doesn't own the realm) → clear "you do not own this realm" error, no silent fallback.
  • Result type carries serverDeleted / unlinked so the CLI can report the rare partial-success state (server gone, Matrix list stale).
  • Integration tests restructured around createRealm() (so realms are properly namespaced for the namespace check). Added a test that confirms the original symptom you described — recreating with the same name after remove now succeeds — plus a 403-not-owner test (registers a second Synapse user) and a dry-run-doesn't-hit-server test.

FadhlanR added a commit that referenced this pull request May 6, 2026
Addresses Codex P1 + Copilot review feedback on PR #4655.

Before: `removeRealmFromMatrixAccountData()` did raw `includes` /
`filter` on the stored list, while `removeRealm()` normalized its
input via `ensureTrailingSlash`. If `app.boxel.realms` held a
legacy entry like `https://host/realm` (no trailing slash) and the
user invoked `boxel realm remove https://host/realm/`, the precheck
in remove.ts saw a normalized match and proceeded, but the auth
helper found no exact match and returned `false`. With the new
hard-delete flow this manifested as: server files gone, Matrix
list still references the (now-orphan) URL, and the CLI printed
"Removed" with no warning.

- `removeRealmFromMatrixAccountData()` now compares stored entries
  via `ensureTrailingSlash` and drops every match, so duplicates
  like `realm` + `realm/` are both cleaned out in one PUT.
- `removeRealm()` computes `nextCount` from the actual normalized
  match count instead of `previousCount - 1`, and treats a `false`
  return after a passing precheck as a hard error (concurrent edit
  race) rather than silently reporting success.
- New integration test seeds account_data with both URL shapes for
  the same realm and asserts both are removed in a single call.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
FadhlanR and others added 3 commits May 6, 2026 10:04
Soft-removes a realm URL from the active profile's `app.boxel.realms`
Matrix account_data list. Server-side files are untouched.

- New `removeRealm()` programmatic API in commands/realm/remove.ts
  (returns a result object on every code path; never prompts, never
  calls process.exit).
- CLI wrapper with TTY confirmation, `-y/--yes` to skip, and
  `--dry-run` to preview the change.
- `removeRealmFromMatrixAccountData()` helper in lib/auth.ts and
  `ProfileManager.removeFromUserRealms()` for reuse by other commands.
- Integration tests against a real test realm server cover: happy
  path, not-in-list, dry-run, trailing-slash normalization, soft-
  remove (server files preserved), and no-active-profile.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Addresses Hassan's PR feedback that the original soft-only behavior
was misleading: a user who "removed" a realm couldn't recreate one
with the same name, because server files / index / registry rows
were untouched.

- `removeRealm()` now calls `DELETE /_delete-realm` first, then
  unlinks the URL from `app.boxel.realms`. Mirrors the host UI's
  workspace delete flow (workspace.gts:999-1005) and inverts the
  `boxel realm create` -> `/_create-realm` symmetry.
- 403 from the server (caller does not own the realm) surfaces a
  clear "you do not own this realm" error; no silent fallback to
  soft-unlink.
- `RemoveRealmResult` extended with `serverDeleted` / `unlinked`
  so the CLI can report the rare partial-success state (server
  gone, Matrix list stale).
- Integration tests restructured around `createRealm()` so realms
  are properly registered + namespaced for the namespace check in
  `_delete-realm`. Adds: name-recyclable-after-removal (the exact
  Hassan concern), dry-run-does-not-hit-server, and 403-not-owner
  using a second Synapse user.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Addresses Codex P1 + Copilot review feedback on PR #4655.

Before: `removeRealmFromMatrixAccountData()` did raw `includes` /
`filter` on the stored list, while `removeRealm()` normalized its
input via `ensureTrailingSlash`. If `app.boxel.realms` held a
legacy entry like `https://host/realm` (no trailing slash) and the
user invoked `boxel realm remove https://host/realm/`, the precheck
in remove.ts saw a normalized match and proceeded, but the auth
helper found no exact match and returned `false`. With the new
hard-delete flow this manifested as: server files gone, Matrix
list still references the (now-orphan) URL, and the CLI printed
"Removed" with no warning.

- `removeRealmFromMatrixAccountData()` now compares stored entries
  via `ensureTrailingSlash` and drops every match, so duplicates
  like `realm` + `realm/` are both cleaned out in one PUT.
- `removeRealm()` computes `nextCount` from the actual normalized
  match count instead of `previousCount - 1`, and treats a `false`
  return after a passing precheck as a hard error (concurrent edit
  race) rather than silently reporting success.
- New integration test seeds account_data with both URL shapes for
  the same realm and asserts both are removed in a single call.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@FadhlanR FadhlanR force-pushed the cs-10630-reimplement-boxel-realm-remove-command branch from 16a4ad6 to 65268ad Compare May 6, 2026 03:04
@FadhlanR FadhlanR merged commit c680584 into main May 7, 2026
24 checks passed
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.

3 participants