Skip to content

Add boxel consolidate-workspaces command (CS-10632)#4780

Open
FadhlanR wants to merge 2 commits into
mainfrom
cs-10632-reimplement-boxel-consolidate-workspaces-command
Open

Add boxel consolidate-workspaces command (CS-10632)#4780
FadhlanR wants to merge 2 commits into
mainfrom
cs-10632-reimplement-boxel-consolidate-workspaces-command

Conversation

@FadhlanR
Copy link
Copy Markdown
Contributor

@FadhlanR FadhlanR commented May 12, 2026

Summary

Ports the legacy boxel consolidate-workspaces command from the standalone boxel-cli into the monorepo at packages/boxel-cli/. Sibling of the other CS-10519 ("Incorporate Boxel CLI to Monorepo") child tickets.

  • New top-level command: boxel consolidate-workspaces [root-dir] with --dry-run. Walks the root, finds local realm mirror dirs (anything with a .boxel-sync.json whose realmUrl identifies a remote realm), and moves each one to its canonical <root>/<domain>/<owner>/<realm> location.
  • Ports the legacy startup nudge as warnIfMisplacedLocalRealmDirs, wired into the program-level preAction hook so any boxel command surfaces the warning once per process. Silenced by --quiet or BOXEL_DISABLE_PATH_WARNING=1.
  • Recognises realmUrl only (the monorepo manifest schema). The legacy workspaceUrl field is dropped — the monorepo deliberately uses "realm" everywhere.

Linear: CS-10632

Hardening (review feedback)

  • new URL(realmUrl) is wrapped in try/catch so a malformed manifest can't crash the global preAction warn-hook (which would otherwise break every boxel command in that cwd). Malformed → silently skipped.
  • isSafePathSegment rejects any decoded segment containing /, \, or NUL, or equal to . / .. — defence against percent-encoded path-separator attacks (%2F, %5C, %00).
  • isWithin containment check after path.resolve ensures the computed destination must stay strictly inside rootDir.

Files

  • New: src/lib/realm-local-paths.ts — URL→path mapping, scanner, warn hook
  • New: src/commands/consolidate-workspaces.ts — Commander registration + impl
  • New: tests/lib/realm-local-paths.test.ts — 28 lib tests (incl. malformed URL + traversal cases)
  • New: tests/commands/consolidate-workspaces.test.ts — 5 command tests
  • Modified: src/build-program.ts — register command + wire warnIfMisplacedLocalRealmDirs into the existing preAction hook (after setQuiet so the hook respects --quiet)
  • Modified: plugin/skills/realm-sync/SKILL.md — incidental regen from pnpm build:plugin (pre-existing drift, unrelated to this change)

Test plan

  • pnpm test:unit-exclude-smoke — 186 unit tests pass (28 lib + 5 command new; +8 security tests)
  • pnpm build && pnpm exec vitest run tests/smoke.test.ts — 19 smoke tests pass
  • pnpm lint — eslint + tsc clean
  • pnpm build:plugin succeeds
  • Manual E2E smoke against dist/index.js: dry-run, real run, idempotent re-run
  • Warn hook fires once on real-command preAction when cwd contains a misplaced mirror; silenced under --quiet and BOXEL_DISABLE_PATH_WARNING=1
  • Manual security smoke: malformed realmUrl in a manifest doesn't crash unrelated commands; consolidate-workspaces silently skips it

🤖 Generated with Claude Code

Ports the legacy `boxel consolidate-workspaces` command from the standalone
boxel-cli into the monorepo. Walks a root directory, finds local realm
mirror dirs (anything with a `.boxel-sync.json` whose `realmUrl` field
identifies a remote realm), and moves each one to its canonical
`<root>/<domain>/<owner>/<realm>` location. Skips dirs already in the right
place; supports `--dry-run`.

Also ports the legacy startup nudge as `warnIfMisplacedLocalRealmDirs`,
wired into the program's `preAction` hook so every command surfaces the
warning once. Silenced by `--quiet` or `BOXEL_DISABLE_PATH_WARNING=1`.

The monorepo manifest schema only writes `realmUrl` (the legacy
`workspaceUrl` field is dropped) — this command therefore recognises
`realmUrl` exclusively.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@FadhlanR
Copy link
Copy Markdown
Contributor Author

Please ignore the lint error it will be fixed once #4751 is merged. I'll improve the boxel-cli CI, so it wont block multiple open PRs.

@FadhlanR FadhlanR marked this pull request as ready for review May 12, 2026 08:48
@FadhlanR FadhlanR requested a review from a team May 12, 2026 08:49
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: 8813ee444d

ℹ️ 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".

}

export function relativeStructuredPathForRealmUrl(realmUrl: string): string {
const url = new URL(realmUrl);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Skip manifests with invalid realmUrl values

tryReadRealmUrl treats any non-empty string as valid, but relativeStructuredPathForRealmUrl immediately does new URL(realmUrl) without a guard. If a local .boxel-sync.json contains a malformed URL string (for example from manual edits or stale tooling), this throws and aborts both consolidate-workspaces and the new global preAction warning path, so unrelated boxel commands fail in that working tree. This should be handled like other malformed manifest cases by validating/catching and skipping invalid entries instead of throwing.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed in bf38b9b. new URL(realmUrl) now lives inside a try/catch in relativeStructuredPathForRealmUrl; a manifest with a malformed realmUrl is silently skipped instead of throwing. Verified manually: with a bad manifest in cwd, unrelated commands like boxel profile no longer crash via the preAction warn hook. Test: realm-local-paths.test.ts — "does not throw when a manifest contains a malformed realmUrl (regression for preAction crash)".

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

Ports the legacy boxel consolidate-workspaces filesystem migration command into the monorepo CLI (packages/boxel-cli/), along with a once-per-process warning hook that nudges users to consolidate misplaced local realm mirror directories.

Changes:

  • Adds boxel consolidate-workspaces [root-dir] with --dry-run, scanning for .boxel-sync.json manifests and moving directories into <root>/<domain>/<owner>/<realm>.
  • Introduces src/lib/realm-local-paths.ts to map realmUrl → canonical path, scan for misplaced dirs, and emit a preAction warning nudge.
  • Adds unit tests for the new lib + command and wires the warning hook into the CLI preAction hook.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
packages/boxel-cli/src/lib/realm-local-paths.ts New URL→path mapping + scanner + warning hook used by CLI preAction.
packages/boxel-cli/src/commands/consolidate-workspaces.ts New command implementation + Commander registration.
packages/boxel-cli/src/build-program.ts Registers new command and calls the warning hook in preAction.
packages/boxel-cli/tests/lib/realm-local-paths.test.ts Adds unit tests for URL→path mapping, scanner, and warning latch behavior.
packages/boxel-cli/tests/commands/consolidate-workspaces.test.ts Adds command tests for move behavior, dry-run, conflicts, and missing root.
packages/boxel-cli/plugin/skills/realm-sync/SKILL.md Regenerated plugin doc reflecting boxel realm watch subcommand structure.
docs/cs-10632-consolidate-workspaces-plan.md Plan/notes for the port and verification steps.

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

Comment on lines +40 to +49
export function relativeStructuredPathForRealmUrl(realmUrl: string): string {
const url = new URL(realmUrl);
const domain = canonicalDomainFromHost(url.hostname);
const parts = url.pathname
.replace(/^\/|\/$/g, '')
.split('/')
.filter(Boolean);
const owner = parts[0] ?? 'unknown-owner';
const realm = parts[1] ?? parts[0] ?? 'workspace';
return path.join(domain, owner, realm);
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Same as the Codex finding above — addressed in bf38b9b. URL parse is wrapped in try/catch; tryReadRealmUrl still returns the raw string and the URL-validity check happens in relativeStructuredPathForRealmUrl, which now returns string | null. Callers (including the preAction warn hook) skip null entries.

Comment on lines +43 to +50
const parts = url.pathname
.replace(/^\/|\/$/g, '')
.split('/')
.filter(Boolean);
const owner = parts[0] ?? 'unknown-owner';
const realm = parts[1] ?? parts[0] ?? 'workspace';
return path.join(domain, owner, realm);
}
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed in bf38b9b with defence in depth:

  1. isSafePathSegment rejects any segment whose decoded form contains /, \, or NUL, or is exactly . or .. — applied to the domain, owner, and realm segments.
  2. findMisplacedLocalRealmDirs has a new isWithin(absoluteRoot, expectedDir) containment check that drops any resolved destination not strictly inside rootDir.

Note: WHATWG URL parsing already normalizes unencoded .. traversal (e.g. https://stack.cards/../../etc/passwd resolves to /etc/passwd under stack.cards), so the practical attack surface is segments with percent-encoded separators like %2F, %5C, %00. Tests cover both branches: encoded-separator segments → null; traversal-shaped URLs → result stays inside rootDir.


### Create — `packages/boxel-cli/tests/commands/consolidate-workspaces.test.ts`

Vitest, real tmpdir fixtures + monkey-patched `fs.renameSync` for the `EXDEV` path.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Plan doc removed in bf38b9b (moot).

Addresses PR #4780 review:

- `new URL(realmUrl)` could throw on malformed manifests, crashing not
  just `consolidate-workspaces` but every command via the global preAction
  warn-hook. Wrap parse in try/catch; return null and skip.
- Reject path segments whose decoded form contains `/`, `\`, or NUL, or
  is exactly `.` / `..`. WHATWG URL parsing already neutralizes
  unencoded `..` traversal, but defence in depth.
- Add `isWithin` containment check in `findMisplacedLocalRealmDirs` so a
  resolved destination must stay strictly inside `rootDir`.
- `relativeStructuredPathForRealmUrl` and `absoluteStructuredPathForRealmUrl`
  now return `string | null`; callers skip null entries.

Also removes the in-repo plan doc.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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