Add boxel consolidate-workspaces command (CS-10632)#4780
Conversation
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>
|
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. |
There was a problem hiding this comment.
💡 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); |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
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)".
There was a problem hiding this comment.
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.jsonmanifests and moving directories into<root>/<domain>/<owner>/<realm>. - Introduces
src/lib/realm-local-paths.tsto maprealmUrl→ 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
preActionhook.
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.
| 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); |
There was a problem hiding this comment.
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.
| 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); | ||
| } |
There was a problem hiding this comment.
Addressed in bf38b9b with defence in depth:
isSafePathSegmentrejects any segment whose decoded form contains/,\, or NUL, or is exactly.or..— applied to the domain, owner, and realm segments.findMisplacedLocalRealmDirshas a newisWithin(absoluteRoot, expectedDir)containment check that drops any resolved destination not strictly insiderootDir.
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. |
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>
Summary
Ports the legacy
boxel consolidate-workspacescommand from the standalone boxel-cli into the monorepo atpackages/boxel-cli/. Sibling of the other CS-10519 ("Incorporate Boxel CLI to Monorepo") child tickets.boxel consolidate-workspaces [root-dir]with--dry-run. Walks the root, finds local realm mirror dirs (anything with a.boxel-sync.jsonwhoserealmUrlidentifies a remote realm), and moves each one to its canonical<root>/<domain>/<owner>/<realm>location.warnIfMisplacedLocalRealmDirs, wired into the program-levelpreActionhook so anyboxelcommand surfaces the warning once per process. Silenced by--quietorBOXEL_DISABLE_PATH_WARNING=1.realmUrlonly (the monorepo manifest schema). The legacyworkspaceUrlfield 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 everyboxelcommand in that cwd). Malformed → silently skipped.isSafePathSegmentrejects any decoded segment containing/,\, or NUL, or equal to./..— defence against percent-encoded path-separator attacks (%2F,%5C,%00).isWithincontainment check afterpath.resolveensures the computed destination must stay strictly insiderootDir.Files
src/lib/realm-local-paths.ts— URL→path mapping, scanner, warn hooksrc/commands/consolidate-workspaces.ts— Commander registration + impltests/lib/realm-local-paths.test.ts— 28 lib tests (incl. malformed URL + traversal cases)tests/commands/consolidate-workspaces.test.ts— 5 command testssrc/build-program.ts— register command + wirewarnIfMisplacedLocalRealmDirsinto the existingpreActionhook (aftersetQuietso the hook respects--quiet)plugin/skills/realm-sync/SKILL.md— incidental regen frompnpm 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 passpnpm lint— eslint + tsc cleanpnpm build:pluginsucceedsdist/index.js: dry-run, real run, idempotent re-run--quietandBOXEL_DISABLE_PATH_WARNING=1realmUrlin a manifest doesn't crash unrelated commands;consolidate-workspacessilently skips it🤖 Generated with Claude Code