Skip to content
This repository was archived by the owner on Jun 4, 2026. It is now read-only.

Commit c8074ef

Browse files
christseclaude
andcommitted
Add milestone, share/gather, realms commands and multi-realm watch
- Add milestone command to mark important checkpoints - Add share/gather commands for GitHub workflow - Add realms command for multi-realm configuration - Update watch to support multiple realms simultaneously - Update CLAUDE.md with npm run dev invocation instructions - Enhanced checkpoint manager with milestone support Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 050f739 commit c8074ef

10 files changed

Lines changed: 1633 additions & 122 deletions

File tree

.claude/CLAUDE.md

Lines changed: 88 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,24 @@
11
# Boxel CLI - Claude Code Integration
22

3+
## How to Run Boxel Commands
4+
5+
**IMPORTANT:** In this development repo, the `boxel` CLI is not globally installed. Always run commands using:
6+
7+
```bash
8+
npm run dev -- <command> [args]
9+
```
10+
11+
Examples:
12+
```bash
13+
npm run dev -- sync . # NOT: boxel sync .
14+
npm run dev -- history ./workspace # NOT: boxel history ./workspace
15+
npm run dev -- milestone ./workspace 1 -n "Name"
16+
```
17+
18+
The `--` separates npm arguments from the CLI arguments. All documentation below shows `boxel <command>` for brevity, but always use `npm run dev -- <command>` when executing.
19+
20+
---
21+
322
## Auto-Activate Boxel Development Skill
423

524
**IMPORTANT:** When the user is doing ANY of the following, automatically read and follow `.claude/commands/boxel-development.md`:
@@ -78,7 +97,7 @@ npm run dev -- list
7897
### Step 5: First Sync
7998
Help them sync their first workspace:
8099
```bash
81-
npm run dev -- sync @username/workspace
100+
npm run dev -- sync @username/workspace ./workspace-name
82101
```
83102

84103
---
@@ -128,11 +147,28 @@ boxel sync . --dry-run # Preview only
128147

129148
### Watch
130149
```bash
131-
boxel watch . # Default: 30s interval, 5s debounce
150+
boxel watch # Watch all configured realms (from .boxel-workspaces.json)
151+
boxel watch . # Watch single workspace
152+
boxel watch . ./other-realm # Watch multiple realms simultaneously
132153
boxel watch . -i 5 -d 3 # Active: 5s interval, 3s debounce
133154
boxel watch . -q # Quiet mode
134155
```
135156

157+
**Multi-realm watching:** Useful when code lives in one realm and data in another. Each realm gets its own checkpoint tracking and debouncing.
158+
159+
### Realms (Multi-Realm Configuration)
160+
```bash
161+
boxel realms # List configured realms
162+
boxel realms --init # Create .boxel-workspaces.json
163+
boxel realms --add ./path # Add a realm
164+
boxel realms --add ./code --purpose "Card definitions" --patterns "*.gts" --default
165+
boxel realms --add ./data --purpose "Data instances" --card-types "BlogPost,Product"
166+
boxel realms --llm # Output LLM guidance for file placement
167+
boxel realms --remove ./path # Remove a realm
168+
```
169+
170+
**File placement guidance:** The `--llm` output tells Claude which realm to use for different file types and card types.
171+
136172
### History & Restore
137173
```bash
138174
boxel history . # View checkpoints
@@ -158,6 +194,24 @@ boxel pull <url> ./local # One-way pull
158194
boxel push ./local <url> # One-way push
159195
```
160196

197+
### Share & Gather (GitHub Workflow)
198+
```bash
199+
boxel share . -t /path/to/repo -b branch-name --no-pr # Share to GitHub repo
200+
boxel gather . -s /path/to/repo # Pull from GitHub repo
201+
```
202+
203+
**Share** copies workspace state to a GitHub repo branch:
204+
- Preserves repo-level files (package.json, LICENSE, README, etc.)
205+
- Skips realm-specific files (.realm.json, index.json, cards-grid.json)
206+
- Creates branch and commits changes
207+
208+
**Gather** pulls changes from GitHub back to workspace:
209+
- Symmetric to share
210+
- Preserves workspace's realm-specific files
211+
212+
**Pushing to GitHub:** Use GitHub Desktop to push branches (no CLI auth configured).
213+
After share creates the branch locally, open GitHub Desktop and push.
214+
161215
### `/boxel-development` - Default Vibe Coding Skill
162216
The **Boxel Development** skill is auto-enabled for vibe coding. It provides comprehensive guidance for:
163217
- Card definitions (.gts files)
@@ -197,6 +251,18 @@ boxel history . -r 3 # Restore to #3
197251
boxel sync . --prefer-local # ESSENTIAL: sync deletions to server
198252
```
199253

254+
### Share Milestone to GitHub
255+
```bash
256+
boxel share . -t /path/to/boxel-home -b boxel/feature-name --no-pr
257+
# Then push via GitHub Desktop
258+
```
259+
260+
### Gather Updates from GitHub
261+
```bash
262+
boxel gather . -s /path/to/boxel-home
263+
boxel sync . --prefer-local # Push gathered changes to Boxel server
264+
```
265+
200266
Or simply:
201267
```
202268
/restore 3
@@ -209,6 +275,26 @@ Or simply:
209275
boxel history . # View what changed
210276
```
211277

278+
### Multi-Realm Development
279+
When working with multiple realms (e.g., code + data separation):
280+
281+
```bash
282+
# Configure realms once
283+
boxel realms --add ./code-realm --purpose "Card definitions" --patterns "*.gts" --default
284+
boxel realms --add ./data-realm --purpose "Content instances" --card-types "BlogPost,Product"
285+
286+
# Watch all configured realms
287+
boxel watch
288+
289+
# Check where to put a new file
290+
boxel realms --llm
291+
```
292+
293+
**File placement heuristics:**
294+
- `.gts` files → realm with `*.gts` pattern (usually code realm)
295+
- Card instances → realm configured for that card type
296+
- Ambiguous → use the default realm
297+
212298
---
213299

214300
## Critical Patterns

src/commands/gather.ts

Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
import { CheckpointManager } from '../lib/checkpoint-manager.js';
2+
import { spawnSync } from 'child_process';
3+
import * as path from 'path';
4+
import * as fs from 'fs';
5+
6+
// ANSI color codes
7+
const FG_GREEN = '\x1b[32m';
8+
const FG_YELLOW = '\x1b[33m';
9+
const FG_CYAN = '\x1b[36m';
10+
const FG_RED = '\x1b[31m';
11+
const BOLD = '\x1b[1m';
12+
const RESET = '\x1b[0m';
13+
14+
interface GatherOptions {
15+
source: string;
16+
subfolder?: string;
17+
branch?: string;
18+
dryRun?: boolean;
19+
noCheckpoint?: boolean;
20+
}
21+
22+
export async function gatherCommand(
23+
workspace: string,
24+
options: GatherOptions
25+
): Promise<void> {
26+
const workspaceDir = path.resolve(workspace);
27+
const sourceDir = path.resolve(options.source);
28+
29+
// Validate workspace
30+
const manifestPath = path.join(workspaceDir, '.boxel-sync.json');
31+
if (!fs.existsSync(manifestPath)) {
32+
console.error(`${FG_RED}Error:${RESET} No .boxel-sync.json found in workspace.`);
33+
process.exit(1);
34+
}
35+
36+
// Validate source is a git repo
37+
if (!fs.existsSync(path.join(sourceDir, '.git'))) {
38+
console.error(`${FG_RED}Error:${RESET} Source directory is not a git repository: ${sourceDir}`);
39+
process.exit(1);
40+
}
41+
42+
// Determine source subfolder in git repo
43+
let subfolder = options.subfolder;
44+
if (!subfolder) {
45+
// Try auto-detect, but fall back to root if not found
46+
const detected = detectSubfolder(workspaceDir);
47+
if (detected && fs.existsSync(path.join(sourceDir, detected))) {
48+
subfolder = detected;
49+
}
50+
}
51+
52+
const srcContentDir = (subfolder && subfolder !== '.') ? path.join(sourceDir, subfolder) : sourceDir;
53+
54+
if (!fs.existsSync(srcContentDir)) {
55+
console.error(`${FG_RED}Error:${RESET} Source directory not found: ${srcContentDir}`);
56+
process.exit(1);
57+
}
58+
59+
console.log(`\n${FG_CYAN}Source:${RESET} ${srcContentDir}`);
60+
console.log(`${FG_CYAN}Target:${RESET} ${workspaceDir}`);
61+
62+
// Optionally checkout a specific branch
63+
if (options.branch) {
64+
console.log(`${FG_CYAN}Branch:${RESET} ${options.branch}`);
65+
try {
66+
gitInDir(sourceDir, 'checkout', options.branch);
67+
} catch (e) {
68+
console.error(`${FG_RED}Error:${RESET} Could not checkout branch: ${options.branch}`);
69+
process.exit(1);
70+
}
71+
}
72+
73+
// Get current branch for info
74+
const currentBranch = gitInDir(sourceDir, 'rev-parse', '--abbrev-ref', 'HEAD').trim();
75+
const currentCommit = gitInDir(sourceDir, 'rev-parse', '--short', 'HEAD').trim();
76+
console.log(`${FG_CYAN}Git state:${RESET} ${currentBranch} @ ${currentCommit}`);
77+
78+
// Get list of files to copy from source
79+
const files = getContentFiles(srcContentDir);
80+
console.log(`\nFound ${FG_GREEN}${files.length}${RESET} files to gather`);
81+
82+
if (options.dryRun) {
83+
console.log(`\n${FG_YELLOW}Dry run - no changes will be made${RESET}\n`);
84+
console.log('Would copy:');
85+
for (const file of files.slice(0, 15)) {
86+
console.log(` ${file}`);
87+
}
88+
if (files.length > 15) {
89+
console.log(` ... and ${files.length - 15} more`);
90+
}
91+
return;
92+
}
93+
94+
// Files to skip (preserve workspace's version)
95+
// - .realm.json: realm config (name, icon, background)
96+
// - .boxel-sync.json: local sync state
97+
// - index.json: contains realm-specific URLs and metadata
98+
// - cards-grid.json: realm index card
99+
const skipFiles = new Set(['.realm.json', '.boxel-sync.json', 'index.json', 'cards-grid.json']);
100+
101+
// Copy files from source to workspace
102+
console.log(`\n${FG_CYAN}Copying files...${RESET}`);
103+
let copied = 0;
104+
let skipped = 0;
105+
106+
for (const file of files) {
107+
// Skip files that should preserve workspace's version
108+
if (skipFiles.has(file)) {
109+
skipped++;
110+
continue;
111+
}
112+
113+
const srcPath = path.join(srcContentDir, file);
114+
const destPath = path.join(workspaceDir, file);
115+
116+
// Ensure directory exists
117+
const dir = path.dirname(destPath);
118+
if (!fs.existsSync(dir)) {
119+
fs.mkdirSync(dir, { recursive: true });
120+
}
121+
122+
fs.copyFileSync(srcPath, destPath);
123+
copied++;
124+
}
125+
126+
console.log(`Copied ${FG_GREEN}${copied}${RESET} files${skipped > 0 ? ` (skipped ${skipped} preserved files)` : ''}`);
127+
128+
// Create checkpoint
129+
if (!options.noCheckpoint) {
130+
const manager = new CheckpointManager(workspaceDir);
131+
if (manager.isInitialized()) {
132+
console.log(`\n${FG_CYAN}Creating checkpoint...${RESET}`);
133+
134+
const changes = files
135+
.filter(f => !skipFiles.has(f))
136+
.map(f => ({
137+
file: f,
138+
status: 'modified' as const,
139+
}));
140+
141+
const checkpoint = manager.createCheckpoint(
142+
'manual',
143+
changes,
144+
`Gather from ${currentBranch}@${currentCommit}`
145+
);
146+
147+
if (checkpoint) {
148+
console.log(`${FG_GREEN}Checkpoint:${RESET} ${checkpoint.shortHash}`);
149+
}
150+
}
151+
}
152+
153+
console.log(`\n${FG_GREEN}Done!${RESET}`);
154+
console.log(`\nNext steps:`);
155+
console.log(` ${FG_CYAN}boxel sync . --prefer-local${RESET} Push gathered changes to Boxel server`);
156+
}
157+
158+
function getContentFiles(dir: string): string[] {
159+
const files: string[] = [];
160+
161+
const scan = (currentDir: string, prefix: string = '') => {
162+
const entries = fs.readdirSync(currentDir, { withFileTypes: true });
163+
for (const entry of entries) {
164+
// Skip git and common non-content files
165+
if (entry.name === '.git' ||
166+
entry.name === '.github' ||
167+
entry.name === '.vscode' ||
168+
entry.name === 'node_modules' ||
169+
entry.name === '.DS_Store' ||
170+
entry.name === 'package.json' ||
171+
entry.name === 'pnpm-lock.yaml' ||
172+
entry.name === 'package-lock.json' ||
173+
entry.name === 'yarn.lock' ||
174+
entry.name === 'tsconfig.json' ||
175+
entry.name === 'LICENSE' ||
176+
entry.name === 'README.md' ||
177+
entry.name === 'CHANGELOG.md' ||
178+
entry.name === '.boxelignore' ||
179+
entry.name === '.editorconfig' ||
180+
entry.name === '.eslintrc.js' ||
181+
entry.name === '.prettierrc.js' ||
182+
entry.name === '.gitignore' ||
183+
entry.name === '.npmrc' ||
184+
entry.name === '.nvmrc') {
185+
continue;
186+
}
187+
188+
const relPath = prefix ? `${prefix}/${entry.name}` : entry.name;
189+
190+
if (entry.isDirectory()) {
191+
scan(path.join(currentDir, entry.name), relPath);
192+
} else {
193+
files.push(relPath);
194+
}
195+
}
196+
};
197+
198+
scan(dir);
199+
return files;
200+
}
201+
202+
function detectSubfolder(workspaceDir: string): string | undefined {
203+
// Check manifest for workspace URL to detect subfolder
204+
const manifest = JSON.parse(fs.readFileSync(path.join(workspaceDir, '.boxel-sync.json'), 'utf-8'));
205+
const url = manifest.workspaceUrl || '';
206+
207+
// Extract workspace name from URL
208+
const match = url.match(/\/([^\/]+)\/?$/);
209+
if (match) {
210+
return match[1];
211+
}
212+
213+
return undefined;
214+
}
215+
216+
function gitInDir(dir: string, ...args: string[]): string {
217+
const result = spawnSync('git', args, {
218+
cwd: dir,
219+
encoding: 'utf-8',
220+
});
221+
222+
if (result.error) {
223+
throw result.error;
224+
}
225+
226+
if (result.status !== 0) {
227+
throw new Error(`git ${args.join(' ')} failed: ${result.stderr}`);
228+
}
229+
230+
return result.stdout;
231+
}

0 commit comments

Comments
 (0)