Skip to content

Restructure CLI commands, add default workspace root, update docs#15

Merged
christse merged 6 commits intomainfrom
pr-review/command-restructure-and-defaults
Apr 20, 2026
Merged

Restructure CLI commands, add default workspace root, update docs#15
christse merged 6 commits intomainfrom
pr-review/command-restructure-and-defaults

Conversation

@christse
Copy link
Copy Markdown
Contributor

Summary

Implements 11 review suggestions from Buck (P1–P4), plus adds ~/boxel-workspaces/ as the cross-platform default workspace root.

Command restructuring

  • boxel doctor parent command groups maintenance tools: repair-realm, repair-realms, consolidate-workspaces, force-reindex — signals these are "fix things" commands, not daily workflow (like brew doctor)
  • boxel workspace-list (renamed from list) — clarifies this fetches your UI workspace list from Matrix, not server-side realm config
  • boxel realms migrated to positional subcommands (add, remove, init, llm) — matches boxel profile pattern
  • Hidden backwards-compat aliases for all renamed commands

Safety improvements

  • repair-realms no longer overwrites index.json by default — prevents destroying customized index files (was breaking Checkly prerendering). Opt-in with --fix-index
  • track --push now shows error details when uploads fail — previously swallowed result.errors array
  • Name repair only overwrites missing/broken names, not valid ones that differ from the endpoint slug
  • realms remove validates the path exists before removing

Default workspace root: ~/boxel-workspaces/

  • All platforms (macOS, Linux, Windows) use ~/boxel-workspaces/ as the default root
  • pull no longer requires a local-dir argument — defaults to ~/boxel-workspaces/<domain>/<owner>/<realm>
  • Full realm server hostname as folder name (e.g. realms-staging.stack.cards/, not stack.cards/) eliminates staging/production ambiguity
  • consolidate-workspaces and legacy path warnings default to scanning ~/boxel-workspaces/

Terminology: Realm vs Workspace

  • Realm = server-side data store (used for: realms, repair-realm, .realm.json)
  • Workspace = UI-facing concept via Matrix (used for: workspace-list, consolidate-workspaces, .boxel-workspaces.json)
  • Documented in README with rationale

Documentation

  • All .claude/commands/*.md skills, AGENTS.md, CLAUDE.md, README.md, docs/realm-repair.md updated with new command names
  • Onboarding flow updated: boxel pull <url> (no local path needed)
  • Profile-agnostic design documented — directory paths encode realm server identity, no per-profile config needed

Tests

28 new tests covering:

  • defaultWorkspacesRoot() returns ~/boxel-workspaces/
  • Full hostname preservation in workspace paths
  • isBadName() / selectNextName() — valid names are NOT overwritten
  • Batch repair defaults (fixIndex, touchIndex, matchEndpoint all false)
  • removeRealm validation
  • All 191 tests pass

Test plan

  • boxel pull <url> without local-dir defaults to ~/boxel-workspaces/<domain>/<owner>/<realm>
  • boxel doctor consolidate-workspaces scans ~/boxel-workspaces/ by default
  • boxel doctor repair-realms does NOT overwrite index.json unless --fix-index passed
  • boxel realms add ./path works (positional subcommand)
  • boxel realms --add ./path still works (hidden backwards compat)
  • boxel workspace-list and boxel list both work
  • track --push shows error details on failure

🤖 Generated with [Claude Code](https://claude.com/claude-code)

@christse christse requested a review from backspace February 14, 2026 19:28
@backspace
Copy link
Copy Markdown
Collaborator

backspace commented Feb 17, 2026

  • repair-realms no longer overwrites index.json by default — prevents destroying customized index files (was breaking Checkly prerendering). Opt-in with --fix-index

This doesn’t seem to be true, I ran npm build && npm link and then repaired the realm that backs the Checkly published realm:

❯ boxel doctor repair-realm https://realms-staging.stack.cards/buck/load-testing/
[buck · stack.cards]

Logging into Matrix...
Repairing realm: https://realms-staging.stack.cards/buck/load-testing/
  Index: normalizing cards-grid relationship and touching meta
  Backup: index.json -> index.backup-20260217t135026952z.json
  load-testing: updated index.json
✅ Realm repair complete.

It would be good to start using versions so I can confirm I’m indeed on the newest; if updating manually is too annoying we could follow Embroider’s prerelease pattern: 4.1.1-unstable.57baf37

❯ boxel --version
1.0.0

ETA I am indeed on the newer version because workspace-list worked.

@backspace
Copy link
Copy Markdown
Collaborator

backspace commented Feb 17, 2026

Test plan, one line at a time:

  • boxel pull <url> without local-dir defaults to ~/boxel-workspaces/<domain>/<owner>/<realm>
❯ boxel pull https://realms-staging.stack.cards/boxel-homepage/
Logging into Matrix...
Matrix login successful
Starting pull from https://realms-staging.stack.cards/boxel-homepage/ to /Users/b/boxel-workspaces/realms-staging.stack.cards/boxel-homepage/boxel-homepage
Testing workspace access...
Workspace access verified
Found 151 files in remote workspace
Found 0 files in local directory
Created directory: /Users/b/boxel-workspaces/realms-staging.stack.cards/boxel-homepage/boxel-homepage
Downloading: boxel-ai-website/animated-grid.gts
  Downloaded: boxel-ai-website/animated-grid.gts

The owner of this realm is @boxel_homepage_realm:stack.cards, so it’s not actually writing to …<owner>/<realm>. This is a weird one though

But this one showed up as unknown-owner… I guess it’s weird too because it’s a published realm

❯ boxel pull https://gabbro.staging.boxel.build/
Logging into Matrix...
Matrix login successful
Starting pull from https://gabbro.staging.boxel.build/ to /Users/b/boxel-workspaces/gabbro.staging.boxel.build/unknown-owner/workspace
Testing workspace access...
Workspace access verified
Found 67 files in remote workspace
Found 0 files in local directory
Created directory: /Users/b/boxel-workspaces/gabbro.staging.boxel.build/unknown-owner/workspace
Downloading: boxel-skills/index.json
  Downloaded: boxel-skills/index.json

This behaved as expected:

❯ boxel pull https://realms-staging.stack.cards/buck/toremove/
Logging into Matrix...
Matrix login successful
Starting pull from https://realms-staging.stack.cards/buck/toremove/ to /Users/b/boxel-workspaces/realms-staging.stack.cards/buck/toremove

@backspace
Copy link
Copy Markdown
Collaborator

backspace commented Feb 17, 2026

TLDR:

  • I don’t think we should keep --add
  • --fix-index is always running, --no-fix-index should instead be the default IMO

Details

  • boxel doctor consolidate-workspaces scans ~/boxel-workspaces/ by default
❯ boxel doctor consolidate-workspaces
Found 1 legacy workspace path(s):

- load-testing -> realms-staging.stack.cards/buck/load-testing

Moved 1 workspace directory.
  • boxel doctor repair-realms does NOT overwrite index.json unless --fix-index passed

This is not true! I have to explicitly use --no-fix-index:

❯ boxel doctor repair-realm --no-fix-index https://realms-staging.stack.cards/buck/load-testing/
[buck · stack.cards]

Logging into Matrix...
Repairing realm: https://realms-staging.stack.cards/buck/load-testing/
  load-testing: no changes
✅ Realm repair complete.
  • boxel realms add ./path works (positional subcommand)
❯ cd ~/Downloads
❯ boxel realms add ./load-testing
Created .boxel-workspaces.json
Added realm: load-testing (./load-testing)
  • boxel realms --add ./path still works (hidden backwards compat)

This works but do we want backward compatibility? IMO we might as well make a clean break as this is not officially released.

  • boxel workspace-list and boxel list both work

In this case I think we can treat list as a shorthand.

  • track --push shows error details on failure

❯ boxel track --push
⇆ Tracking local changes: toremove
Directory: /Users/b/Downloads/Downloads/toremove
Debounce: 3s, Min interval: 10s
Push: enabled → https://realms-staging.stack.cards/buck/toremove/
Press Ctrl+C to stop

[11:44:27 AM] 🔔 Changes detected (1 pending)

[11:44:30 AM] 📦 Creating checkpoint (1 changes)...

  • 1 new: big.png
    ⇆ Checkpoint: e90e277 [MAJOR] Push: Add big.png

Uploading 1 files in 1 batch(es) (3556KB total)
[Batch 1/1] 1 files (1 .png)
✗ Batch failed: HTTP 413: Payload Too Large

Upload complete: 0 succeeded, 1 failed (1910ms)
⚠️ Push: 0 succeeded, 1 failed
✗ batch: HTTP 413: Payload Too Large

christse pushed a commit that referenced this pull request Apr 20, 2026
Commander's .option('--no-fix-index') makes the underlying boolean
default to TRUE, which was the opposite of what PR #15 advertised.
Running `boxel doctor repair-realm <url>` silently rewrote
index.json/cards-grid.json by default, destroying customized index
files (caught by backspace on Checkly-prerendered realm).

Flip to .option('--fix-index') across all 4 command surfaces:
- repair-realm (hidden top-level)
- repair-realms (hidden top-level)
- doctor repair-realm
- doctor repair-realms

Now the default matches the documentation: index.json is untouched
unless --fix-index is explicitly passed.

Added 2 regression tests that exercise the commander parsing directly
(the previous tests only checked the handler-level fallback, which
ran AFTER commander had already set fixIndex=true).

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
@christse christse force-pushed the pr-review/command-restructure-and-defaults branch from 0b7ad86 to 8f81ff8 Compare April 20, 2026 20:11
christse pushed a commit that referenced this pull request Apr 20, 2026
Buck reported on PR #15 that `boxel pull <url>` without a local-dir
argument produced weird paths for published realms:

- 1-segment URL https://realms-staging.stack.cards/boxel-homepage/
  → ~/boxel-workspaces/realms-staging.stack.cards/boxel-homepage/boxel-homepage
  (the fallback `parts[1] ?? parts[0]` duplicated the realm name as owner)

- 0-segment URL https://gabbro.staging.boxel.build/
  → ~/boxel-workspaces/gabbro.staging.boxel.build/unknown-owner/workspace
  (invented placeholder segments)

Fix: adapt the layout depth to match what the URL actually provides.

- 0 segments → <host>/
- 1 segment  → <host>/<realm>/
- 2+ segments → <host>/<owner>/<realm>/ (unchanged)

findManifestPaths() now also walks 2-level <root>/<domain>/<realm>/
layouts so the legacy-path warning and doctor consolidate-workspaces
still detect all manifests after the layout change.

Added 4 regression tests covering both edge cases.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
christse pushed a commit that referenced this pull request Apr 20, 2026
PR #15 introduced positional subcommands (realms add/remove/init/llm)
and kept the old flag forms as hidden aliases for backwards compat.
Since the restructure has not yet been released (still on PR), there
are no users to migrate. Clean break is simpler.

Per backspace's PR #15 review: "do we want backward compatibility?
IMO we might as well make a clean break as this is not officially
released."

- Remove 9 `.addOption(new Option('--xxx').hideHelp())` calls
- Remove legacy flag dispatcher `realmsCommand()` and RealmsOptions
- Drop unused `Option` import from commander

`boxel realms` with no subcommand still shows the list. The `list`
alias for `workspace-list` stays (backspace: "I think we can treat
list as a shorthand").

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
christse pushed a commit that referenced this pull request Apr 20, 2026
Minor-version bump (not patch) because PR #15 changes visible command
names: `list` → `workspace-list`, `realms --add` → `realms add`,
`boxel doctor` parent for repair/consolidate. Plus the two bug fixes
@backspace caught on review.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
@christse
Copy link
Copy Markdown
Contributor Author

Rebased onto main (1.0.1) and addressed all three comments @backspace. Now at 8f81ff8, shipping as 1.1.0.

Bugs you caught

1. --fix-index was default-on (your comment #1 + #3)

You were right, the PR description lied. The cause was commander subtlety: .option('--no-fix-index') makes the boolean default to true, so anyone running boxel doctor repair-realm <url> got their index.json rewritten. That's how your Checkly-prerendered realm lost its custom index.

Fix in commit b9646da — flipped all four command surfaces to .option('--fix-index'). Now you have to opt in explicitly, matching the docs. Added regression tests that parse commander directly; the previous tests only exercised the handler's ?? false fallback, which ran after commander had already set the bad default, so they couldn't catch this.

Verify:

boxel doctor repair-realm https://realms-staging.stack.cards/buck/load-testing/
# no "updated index.json" output

2. Pull path extraction duplicated / invented segments (your comment #2)

Fix in dba568c. The old parts[1] ?? parts[0] ?? 'workspace' fallback chain didn't know when the URL genuinely had no owner. New behavior adapts to segment count:

URL Old path New path
realms-staging.stack.cards/boxel-homepage/ .../boxel-homepage/boxel-homepage .../boxel-homepage/
gabbro.staging.boxel.build/ .../unknown-owner/workspace gabbro.staging.boxel.build/
realms-staging.stack.cards/buck/toremove/ .../buck/toremove .../buck/toremove (unchanged)

Also updated findManifestPaths() to walk 2-level canonical layouts so legacy-path detection and consolidate-workspaces still find manifests for 1-segment realms.

3. Drop --add alias (your comment #3)

Done in 638519e. Took your "clean break" reasoning and dropped the whole flag-based legacy interface, not just --add (the flag interface is all coupled, and nothing's released so there's no migration cost). list stays as a shorthand for workspace-list as you suggested.

Version stamping

Bumped to 1.1.0 (minor — command surface changes). You mentioned Embroider's 4.1.1-unstable.57baf37 pattern; that'd need a separate setup (CI + prepublishOnly stamping) and I'd rather not bundle it into this PR. Happy to do it as a follow-up if you want.

Ship state

  • 199/199 tests pass (28 new from the restructure + 6 I added: 2 for --fix-index commander defaults, 4 for 0/1-segment URLs)
  • boxel --version reports 1.1.0
  • Rebased cleanly onto main; only src/index.ts needed conflict resolution (kept both the 1.0.1 parsePositiveInt/createRequire work and the 0b7ad86 Option/workspace-paths imports)

Ready for another look.

Copy link
Copy Markdown

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 restructures the Boxel CLI command surface (grouping maintenance commands under boxel doctor, renaming listworkspace-list, and moving realms to positional subcommands), while also introducing ~/boxel-workspaces/ as the default local workspace root and updating docs/tests accordingly.

Changes:

  • Add defaultWorkspacesRoot() and update workspace path derivation to preserve full hostnames and support 0/1-segment “published realm” URLs.
  • Restructure CLI commands (doctor, workspace-list, realms <subcommand>) and add hidden backward-compat command aliases.
  • Update docs/changelog and add new tests for path behavior and repair flag defaults.

Reviewed changes

Copilot reviewed 18 out of 18 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
src/lib/workspace-paths.ts Adds defaultWorkspacesRoot(), removes hostname normalization, and updates structured path logic + manifest scanning depth.
src/lib/workspace-resolver.ts Switches URL-based default local dir resolution to ~/boxel-workspaces/.
src/index.ts Implements new command tree (doctor, workspace-list, positional realms) and updates pull default directory logic.
src/commands/consolidate.ts Defaults consolidate scan root to defaultWorkspacesRoot().
src/commands/repair.ts Makes batch repair defaults safer (opt-in index rewrite/touch, preserve valid names).
src/commands/realms.ts Splits realms management into explicit subcommand handlers (list/add/remove/init/llm).
src/commands/track.ts Surfaces batch upload error details during track --push.
test/lib/workspace-paths.test.ts Adds coverage for hostname preservation and 0/1-segment URL layouts; tests default root.
test/lib/realm-config.test.ts Adds unit tests for addRealm/removeRealm.
test/commands/repair.test.ts Adds regression coverage for commander flag parsing; adds name-selection tests.
README.md Updates terminology and onboarding/docs for new command names and default workspace root.
docs/realm-repair.md Updates repair command docs to boxel doctor ....
CHANGELOG.md Adds 1.1.0 entry documenting command/path changes and regressions fixed.
AGENTS.md Updates agent onboarding/command references to new names.
.claude/commands/setup.md Updates onboarding steps (workspace-list, pull default root).
.claude/commands/repair.md Updates repair workflow docs to boxel doctor ....
.claude/CLAUDE.md Updates onboarding and reference command lists to new command names and default root.
package.json Bumps version to 1.1.0.

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

Comment on lines +11 to +15
const result = relativeStructuredPathForWorkspaceUrl(
'https://app.boxel.ai/acme-corp/project-atlas/'
);
expect(result).toBe('app.boxel.ai/acme-corp/project-atlas');
});
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

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

These expectations hardcode POSIX-style separators (e.g. app.boxel.ai/acme-corp/project-atlas), but relativeStructuredPathForWorkspaceUrl() uses path.join(...), which returns backslashes on Windows. This will fail on Windows runners/local dev. Consider building expected values with path.join(...) (or normalize separators in the assertion) so the test is platform-agnostic.

Copilot uses AI. Check for mistakes.
Comment on lines +2 to +41

// Test the repair name logic by importing the relevant helpers
// We test the exported functions indirectly through repairSingleRealm behavior

// Since repairSingleRealm is not directly testable (it makes HTTP calls),
// we test the name-selection logic by extracting the key functions.

// Import the module to test internal logic
// We use the same pattern as checkpoint-manager tests (@ts-expect-error for private access)

describe('repair name logic', () => {
// Replicate the isBadName logic from repair.ts
function isBadName(value: unknown): boolean {
if (typeof value !== 'string' || value.trim().length === 0) {
return true;
}
return value.trim().toLowerCase() === 'unknown workspace';
}

function titleCaseFromEndpoint(realmUrl: string): string {
const parts = new URL(realmUrl).pathname.replace(/^\/|\/$/g, '').split('/');
const endpoint = parts[parts.length - 1] ?? 'workspace';
return endpoint
.split('-')
.filter(Boolean)
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
.join(' ');
}

// Replicate the name selection logic from repairSingleRealm
function selectNextName(
currentName: string | undefined,
desiredName: string,
force: boolean,
): string | undefined {
return force || isBadName(currentName)
? desiredName
: currentName;
}

Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

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

This test file re-implements isBadName/selectNextName/titleCaseFromEndpoint instead of exercising the real logic in src/commands/repair.ts, so it won’t catch regressions if the production code changes. Consider extracting these helpers into exported functions (or a small lib module) and importing them here, or mocking fetch to test repairSingleRealm behavior end-to-end.

Suggested change
// Test the repair name logic by importing the relevant helpers
// We test the exported functions indirectly through repairSingleRealm behavior
// Since repairSingleRealm is not directly testable (it makes HTTP calls),
// we test the name-selection logic by extracting the key functions.
// Import the module to test internal logic
// We use the same pattern as checkpoint-manager tests (@ts-expect-error for private access)
describe('repair name logic', () => {
// Replicate the isBadName logic from repair.ts
function isBadName(value: unknown): boolean {
if (typeof value !== 'string' || value.trim().length === 0) {
return true;
}
return value.trim().toLowerCase() === 'unknown workspace';
}
function titleCaseFromEndpoint(realmUrl: string): string {
const parts = new URL(realmUrl).pathname.replace(/^\/|\/$/g, '').split('/');
const endpoint = parts[parts.length - 1] ?? 'workspace';
return endpoint
.split('-')
.filter(Boolean)
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
.join(' ');
}
// Replicate the name selection logic from repairSingleRealm
function selectNextName(
currentName: string | undefined,
desiredName: string,
force: boolean,
): string | undefined {
return force || isBadName(currentName)
? desiredName
: currentName;
}
import * as fs from 'node:fs';
// Test the repair name logic by loading the real helper implementations from repair.ts
// rather than duplicating them in the test.
function extractFunctionDeclaration(source: string, functionName: string): string {
const start = source.indexOf(`function ${functionName}`);
if (start === -1) {
throw new Error(`Could not find function ${functionName} in src/commands/repair.ts`);
}
const bodyStart = source.indexOf('{', start);
if (bodyStart === -1) {
throw new Error(`Could not find function body for ${functionName} in src/commands/repair.ts`);
}
let depth = 0;
for (let index = bodyStart; index < source.length; index += 1) {
const char = source[index];
if (char === '{') {
depth += 1;
} else if (char === '}') {
depth -= 1;
if (depth === 0) {
return source.slice(start, index + 1);
}
}
}
throw new Error(`Could not extract complete function ${functionName} from src/commands/repair.ts`);
}
type RepairNameHelpers = {
isBadName: (value: unknown) => boolean;
titleCaseFromEndpoint: (realmUrl: string) => string;
selectNextName: (
currentName: string | undefined,
desiredName: string,
force: boolean,
) => string | undefined;
};
function loadRepairNameHelpers(): RepairNameHelpers {
const source = fs.readFileSync(
new URL('../../src/commands/repair.ts', import.meta.url),
'utf8',
);
const helperSource = [
extractFunctionDeclaration(source, 'isBadName'),
extractFunctionDeclaration(source, 'titleCaseFromEndpoint'),
extractFunctionDeclaration(source, 'selectNextName'),
].join('\n\n');
return new Function(
`${helperSource}\nreturn { isBadName, titleCaseFromEndpoint, selectNextName };`,
)() as RepairNameHelpers;
}
const {
isBadName,
titleCaseFromEndpoint,
selectNextName,
} = loadRepairNameHelpers();
describe('repair name logic', () => {

Copilot uses AI. Check for mistakes.
@@ -0,0 +1,168 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

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

Unused imports (vi, beforeEach, afterEach) add noise and can trip stricter lint/TS settings if enabled later. Consider removing them or using them in the tests.

Suggested change
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { describe, it, expect } from 'vitest';

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +4
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

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

Unused imports (beforeEach, afterEach, fs, path, os) aren’t used anywhere in this test file. Consider removing them to keep the tests minimal and avoid future lint/TS unused-var failures.

Suggested change
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
import { describe, it, expect } from 'vitest';

Copilot uses AI. Check for mistakes.
Comment thread src/index.ts
doctorCmd
.command('consolidate-workspaces')
.description('Fix local workspace directory structure (move to domain/owner/realm)')
.argument('[root-dir]', 'Root directory to scan (default: current directory)')
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

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

doctor consolidate-workspaces help text says the default root-dir is the current directory, but consolidateWorkspacesCommand() now defaults to defaultWorkspacesRoot() when rootDir is omitted. Consider updating the argument description (and the hidden legacy command’s description if desired) so the CLI help matches behavior.

Suggested change
.argument('[root-dir]', 'Root directory to scan (default: current directory)')
.argument('[root-dir]', 'Root directory to scan (default: workspaces root)')

Copilot uses AI. Check for mistakes.
Comment thread test/commands/repair.test.ts Outdated
Comment on lines +117 to +137
it('defaults to not fixing index in batch mode', () => {
// Verify the default values match what we set in the code
const options: { fixIndex?: boolean; touchIndex?: boolean; matchEndpoint?: boolean } = {};
const fixIndex = options.fixIndex ?? false;
const touchIndex = options.touchIndex ?? false;
const matchEndpoint = options.matchEndpoint ?? false;

expect(fixIndex).toBe(false);
expect(touchIndex).toBe(false);
expect(matchEndpoint).toBe(false);
});

it('respects explicit opt-in for index fixing', () => {
const options = { fixIndex: true, touchIndex: true, matchEndpoint: true };
const fixIndex = options.fixIndex ?? false;
const touchIndex = options.touchIndex ?? false;
const matchEndpoint = options.matchEndpoint ?? false;

expect(fixIndex).toBe(true);
expect(touchIndex).toBe(true);
expect(matchEndpoint).toBe(true);
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

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

The “batch defaults” tests are currently only validating how ?? false works on a local object, not the actual CLI parsing or repairRealmsCommand defaults. This is effectively a no-op regression test. Consider asserting defaults by invoking commander parsing against the real option definitions (similar to the commander regression test below), or by calling the command handler with parsed options.

Suggested change
it('defaults to not fixing index in batch mode', () => {
// Verify the default values match what we set in the code
const options: { fixIndex?: boolean; touchIndex?: boolean; matchEndpoint?: boolean } = {};
const fixIndex = options.fixIndex ?? false;
const touchIndex = options.touchIndex ?? false;
const matchEndpoint = options.matchEndpoint ?? false;
expect(fixIndex).toBe(false);
expect(touchIndex).toBe(false);
expect(matchEndpoint).toBe(false);
});
it('respects explicit opt-in for index fixing', () => {
const options = { fixIndex: true, touchIndex: true, matchEndpoint: true };
const fixIndex = options.fixIndex ?? false;
const touchIndex = options.touchIndex ?? false;
const matchEndpoint = options.matchEndpoint ?? false;
expect(fixIndex).toBe(true);
expect(touchIndex).toBe(true);
expect(matchEndpoint).toBe(true);
it('defaults batch flags from commander parsing', async () => {
const { Command } = await import('commander');
const cmd = new Command()
.option('--fix-index', 'Rewrite index.json/cards-grid.json starter cards')
.option('--no-touch-index', 'Skip touch mutation in index.json meta')
.option('--match-endpoint', 'Match names from endpoint slugs when repairing realms');
cmd.parse(['node', 'test', 'batch-file.json'], { from: 'node' });
const opts = cmd.opts();
expect(opts.fixIndex).toBeUndefined();
expect((opts.fixIndex as boolean | undefined) ?? false).toBe(false);
expect(opts.touchIndex).toBe(true);
expect((opts.touchIndex as boolean | undefined) ?? false).toBe(true);
expect(opts.matchEndpoint).toBeUndefined();
expect((opts.matchEndpoint as boolean | undefined) ?? false).toBe(false);
});
it('respects explicit batch flag opt-ins and opt-outs', async () => {
const { Command } = await import('commander');
const cmd = new Command()
.option('--fix-index', 'Rewrite index.json/cards-grid.json starter cards')
.option('--no-touch-index', 'Skip touch mutation in index.json meta')
.option('--match-endpoint', 'Match names from endpoint slugs when repairing realms');
cmd.parse(['node', 'test', '--fix-index', '--no-touch-index', '--match-endpoint', 'batch-file.json'], {
from: 'node',
});
const opts = cmd.opts();
expect(opts.fixIndex).toBe(true);
expect((opts.fixIndex as boolean | undefined) ?? false).toBe(true);
expect(opts.touchIndex).toBe(false);
expect((opts.touchIndex as boolean | undefined) ?? false).toBe(false);
expect(opts.matchEndpoint).toBe(true);
expect((opts.matchEndpoint as boolean | undefined) ?? false).toBe(true);

Copilot uses AI. Check for mistakes.
Comment thread src/commands/track.ts
batchSize: 10,
definitionsFirst: true,
quiet: !options.verbose,
quiet: false,
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

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

uploadWithBatching is now always called with quiet: false, which forces verbose batch progress output even when track is run without --verbose (and regardless of --quiet). Since the goal here is to surface result.errors, you can keep the new per-error logging while still passing a quiet value derived from CLI flags (e.g. quiet: !options.verbose) to avoid extra noise in normal usage.

Suggested change
quiet: false,
quiet: options.quiet || !options.verbose,

Copilot uses AI. Check for mistakes.
Comment thread src/index.ts
.description('Pull files from a Boxel workspace to a local directory')
.argument('<workspace-url>', 'The URL of the source workspace (e.g., https://app.boxel.ai/demo/)')
.argument('<local-dir>', 'The local directory to sync files to')
.argument('[local-dir]', 'The local directory to sync files to (default: ~/boxel-workspaces/<domain>/<owner>/<realm>)')
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

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

The help text says the default pull path is ~/boxel-workspaces/<domain>/<owner>/<realm>, but relativeStructuredPathForWorkspaceUrl() can now produce 0- or 1-segment layouts (<host>/ or <host>/<realm>/) for published realms without an owner. Consider updating the CLI argument description to reflect the actual path shapes so users aren’t surprised.

Suggested change
.argument('[local-dir]', 'The local directory to sync files to (default: ~/boxel-workspaces/<domain>/<owner>/<realm>)')
.argument('[local-dir]', 'The local directory to sync files to (default: ~/boxel-workspaces/<domain>/, ~/boxel-workspaces/<domain>/<realm>/, or ~/boxel-workspaces/<domain>/<owner>/<realm>, depending on the workspace URL)')

Copilot uses AI. Check for mistakes.
Comment thread CHANGELOG.md
- **`boxel doctor` parent command** groups maintenance tasks: `doctor repair-realm`, `doctor repair-realms`, `doctor consolidate-workspaces`, `doctor force-reindex`. Keeps the root command tree focused on day-to-day sync operations.
- **`boxel workspace-list`** (formerly `boxel list`) lists Boxel workspaces the active profile can access. `boxel list` stays as a shorthand alias.
- **`boxel realms <subcommand>`** — positional subcommands replace the old flag-based interface. `boxel realms add ./path`, `realms remove ./path`, `realms init`, `realms list`, `realms llm`. `boxel realms` with no args shows the list.
- **Default workspace root at `~/boxel-workspaces/<domain>/<owner>/<realm>/`.** `boxel pull <url>` now works without a local-dir argument — it derives a canonical path from the URL and creates the directory. `doctor consolidate-workspaces` scans `~/boxel-workspaces/` by default.
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

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

This changelog bullet states the default workspace root layout is always ~/boxel-workspaces/<domain>/<owner>/<realm>/, but the new URL parsing explicitly supports 0- and 1-segment published realms (<host>/ and <host>/<realm>/). Consider adjusting the wording here to match the actual path derivation so the release notes don’t mislead users.

Suggested change
- **Default workspace root at `~/boxel-workspaces/<domain>/<owner>/<realm>/`.** `boxel pull <url>` now works without a local-dir argument — it derives a canonical path from the URL and creates the directory. `doctor consolidate-workspaces` scans `~/boxel-workspaces/` by default.
- **Default workspace root under `~/boxel-workspaces/`.** `boxel pull <url>` now works without a local-dir argument — it derives a canonical subpath from the URL and creates the directory: 0 segments → `<host>/`, 1 segment → `<host>/<realm>/`, 2+ segments → `<host>/<owner>/<realm>/`. `doctor consolidate-workspaces` scans `~/boxel-workspaces/` by default.

Copilot uses AI. Check for mistakes.
christse and others added 6 commits April 20, 2026 16:31
PR review changes (P1–P4):
- Show push error details in track --push (quiet: false + error loop)
- Conservative defaults for repair-realms (fixIndex/touchIndex/matchEndpoint off)
- Full realm server hostname in folder naming (no normalization)
- Simplified name selection: only overwrite bad names, not mismatched ones
- Validate realms remove (error if path not found)
- Rename list → workspace-list (with list alias)
- Migrate realms to positional subcommands (realms add/remove/init/llm)
- Add boxel doctor parent command (repair-realm, repair-realms, consolidate-workspaces, force-reindex)
- Hidden backwards-compat aliases for all renamed commands

Default workspace root:
- Add defaultWorkspacesRoot() → ~/boxel-workspaces/ (all platforms)
- Wire into workspace-resolver, consolidate, legacy path warning
- Make pull local-dir argument optional (defaults to ~/boxel-workspaces/)

Documentation:
- Update all .md files with new command nomenclature
- Add terminology section (realm vs workspace) to README
- Document ~/boxel-workspaces/ as default root everywhere
- Update onboarding flow to use boxel pull <url>

Tests:
- 28 new tests: workspace-paths, realm-config, repair (isBadName, selectNextName, batch defaults)

Co-Authored-By: Claude Opus 4.6 <[email protected]>
Commander's .option('--no-fix-index') makes the underlying boolean
default to TRUE, which was the opposite of what PR #15 advertised.
Running `boxel doctor repair-realm <url>` silently rewrote
index.json/cards-grid.json by default, destroying customized index
files (caught by backspace on Checkly-prerendered realm).

Flip to .option('--fix-index') across all 4 command surfaces:
- repair-realm (hidden top-level)
- repair-realms (hidden top-level)
- doctor repair-realm
- doctor repair-realms

Now the default matches the documentation: index.json is untouched
unless --fix-index is explicitly passed.

Added 2 regression tests that exercise the commander parsing directly
(the previous tests only checked the handler-level fallback, which
ran AFTER commander had already set fixIndex=true).

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Buck reported on PR #15 that `boxel pull <url>` without a local-dir
argument produced weird paths for published realms:

- 1-segment URL https://realms-staging.stack.cards/boxel-homepage/
  → ~/boxel-workspaces/realms-staging.stack.cards/boxel-homepage/boxel-homepage
  (the fallback `parts[1] ?? parts[0]` duplicated the realm name as owner)

- 0-segment URL https://gabbro.staging.boxel.build/
  → ~/boxel-workspaces/gabbro.staging.boxel.build/unknown-owner/workspace
  (invented placeholder segments)

Fix: adapt the layout depth to match what the URL actually provides.

- 0 segments → <host>/
- 1 segment  → <host>/<realm>/
- 2+ segments → <host>/<owner>/<realm>/ (unchanged)

findManifestPaths() now also walks 2-level <root>/<domain>/<realm>/
layouts so the legacy-path warning and doctor consolidate-workspaces
still detect all manifests after the layout change.

Added 4 regression tests covering both edge cases.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
PR #15 introduced positional subcommands (realms add/remove/init/llm)
and kept the old flag forms as hidden aliases for backwards compat.
Since the restructure has not yet been released (still on PR), there
are no users to migrate. Clean break is simpler.

Per backspace's PR #15 review: "do we want backward compatibility?
IMO we might as well make a clean break as this is not officially
released."

- Remove 9 `.addOption(new Option('--xxx').hideHelp())` calls
- Remove legacy flag dispatcher `realmsCommand()` and RealmsOptions
- Drop unused `Option` import from commander

`boxel realms` with no subcommand still shows the list. The `list`
alias for `workspace-list` stays (backspace: "I think we can treat
list as a shorthand").

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Minor-version bump (not patch) because PR #15 changes visible command
names: `list` → `workspace-list`, `realms --add` → `realms add`,
`boxel doctor` parent for repair/consolidate. Plus the two bug fixes
@backspace caught on review.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Review follow-ups for PR #15:

- docs/realm-repair.md still documented the old `--no-fix-index`
  form after b9646da flipped the flag to `--fix-index`. Updated
  the description to match the new opt-in behavior and explain
  why it's off by default (breaks Checkly-style prerendering).

- test/commands/repair.test.ts had a misleading block titled
  "repair-realms batch defaults" that actually tested the
  handler's `?? false` fallback, not the commander CLI defaults.
  Renamed and clarified so it's obvious what it covers; the real
  commander-default coverage lives in the "commander flag parsing"
  block added in b9646da.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
@christse christse force-pushed the pr-review/command-restructure-and-defaults branch from 8f81ff8 to 357fa77 Compare April 20, 2026 20:32
@christse
Copy link
Copy Markdown
Contributor Author

Rebased onto main post-1.0.2 and addressed review nits I caught while self-reviewing.

New commit 357fa77:

  • docs/realm-repair.md:36 still documented the old --no-fix-index form (lying about the flip). Updated to describe --fix-index as opt-in and explain why (destructive to customized index files).
  • test/commands/repair.test.ts had a block titled "repair-realms batch defaults" that actually tested the handler's ?? false fallback on an empty object — never exercised commander defaults at all. Renamed to "handler fallback behavior" so it's obvious what it covers. The real commander coverage is in the commander flag parsing block that parses commander directly.

Rebase notes: Straightforward conflicts in package.json (1.0.2 + 1.1.0) and CHANGELOG.md (interleaved entries). Resolved by keeping both release notes stacked and taking 1.1.0 as the version.

Final state: 199/199 tests pass, head at 357fa77, mergeable clean.

Ready to merge.

@christse christse merged commit ddbf1e2 into main Apr 20, 2026
@christse christse deleted the pr-review/command-restructure-and-defaults branch April 20, 2026 20:35
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