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

Commit 3a07b66

Browse files
authored
Merge pull request #8 from cardstack/feature/sync-cleanup-prompt
Add failed download cleanup prompt and documentation updates
2 parents f80821c + 46263a0 commit 3a07b66

4 files changed

Lines changed: 211 additions & 18 deletions

File tree

.claude/CLAUDE.md

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,39 @@ npx boxel profile switch username # Switch by partial match
9797

9898
---
9999

100+
## Local Workspace Organization
101+
102+
When syncing multiple workspaces locally, organize them by **domain/username/realm** to mirror the Matrix ID structure (`@username:domain`):
103+
104+
```
105+
boxel-workspaces/
106+
├── boxel.ai/ # Production domain
107+
│ └── acme-corp/ # Username
108+
│ ├── personal/ # Realm
109+
│ ├── project-atlas/
110+
│ └── inventory-tracker/
111+
└── stack.cards/ # Staging domain
112+
└── acme-corp/
113+
└── sandbox/
114+
```
115+
116+
**Benefits:**
117+
- Clear separation between production and staging environments
118+
- Matches the `@username:domain` profile ID format
119+
- Easy to identify which profile/environment a workspace belongs to
120+
- Supports multiple users on the same machine
121+
122+
**First-time sync to this structure:**
123+
```bash
124+
# Production workspace
125+
boxel pull https://app.boxel.ai/acme-corp/project-atlas/ ./boxel-workspaces/boxel.ai/acme-corp/project-atlas
126+
127+
# Staging workspace
128+
boxel pull https://realms-staging.stack.cards/acme-corp/sandbox/ ./boxel-workspaces/stack.cards/acme-corp/sandbox
129+
```
130+
131+
---
132+
100133
## Available Skills
101134

102135
### `/track` - Track Local Edits
@@ -136,15 +169,40 @@ boxel status . --pull # Auto-pull remote changes
136169
boxel check ./file.json --sync # Check single file
137170
```
138171

139-
### Sync
172+
### Pull, Push, Sync (Command Relationship)
173+
174+
| Command | Direction | Purpose | Deletes Local | Deletes Remote |
175+
|---------|-----------|---------|---------------|----------------|
176+
| `pull` | Remote → Local | Fresh download | with `--delete` | never |
177+
| `push` | Local → Remote | Deploy changes | never | with `--delete` |
178+
| `sync` | Both ways | Stay in sync | with `--prefer-remote` | with `--prefer-local` |
179+
140180
```bash
141181
boxel sync . # Interactive sync
142182
boxel sync . --prefer-local # Keep local + sync deletions
143183
boxel sync . --prefer-remote # Keep remote
144184
boxel sync . --prefer-newest # Keep newest version
145185
boxel sync . --delete # Sync deletions both ways
146186
boxel sync . --dry-run # Preview only
187+
188+
boxel push ./local <url> # One-way push (local → remote)
189+
boxel push ./local <url> --delete # Push and remove orphaned remote files
190+
boxel pull <url> ./local # One-way pull (remote → local)
191+
```
192+
193+
**Failed download cleanup:** When `sync` encounters files that return 500 errors (broken/corrupted on server), it will prompt you to delete them:
147194
```
195+
⚠️ 3 file(s) failed to download (server error):
196+
- Staff/broken-card.json
197+
- Student/corrupted.json
198+
199+
These files may be broken on the server. Delete them from remote? [y/N]
200+
```
201+
202+
> **Safety tip:** Before any destructive operation, create a checkpoint with a descriptive message:
203+
> ```bash
204+
> boxel history . -m "Before cleanup: removing broken server files"
205+
> ```
148206
149207
### Track ⇆ (Local File Watching)
150208
```bash
@@ -348,6 +406,21 @@ boxel realms --llm
348406

349407
## Critical Patterns
350408

409+
### ⚠️ SAFETY FIRST: Checkpoint Before Destructive Operations
410+
**Always create a checkpoint with a descriptive message before:**
411+
- Deleting files from server (`--prefer-local`, `push --delete`)
412+
- Restoring to an earlier checkpoint
413+
- Bulk cleanup operations
414+
- Removing card definitions or instances
415+
416+
```bash
417+
boxel history . -m "Before cleanup: removing sample data and unused definitions"
418+
# Now safe to proceed with destructive operation
419+
boxel sync . --prefer-local
420+
```
421+
422+
This ensures you can always recover if something goes wrong. The checkpoint message helps identify what state to restore to.
423+
351424
### 0. ALWAYS Write Source Code, Never Compiled Output
352425
When editing `.gts` files, **always write clean idiomatic source code**:
353426
```gts
@@ -563,3 +636,9 @@ Headers:
563636
### Switching environments (prod/staging)
564637
- Add profiles for each environment
565638
- Switch with: `boxel profile switch <username>`
639+
640+
### "500 Internal Server Error" on specific files
641+
- These files are broken/corrupted on the server
642+
- Sync will prompt you to delete them after completion
643+
- Or use `boxel push . <url> --delete` to remove all orphaned remote files
644+
- Check if card definitions have errors in Boxel web UI

README.md

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,14 @@ MATRIX_PASSWORD=your-password
186186

187187
### Sync Operations
188188

189+
**Pull, Push, and Sync relationship:**
190+
191+
| Command | Direction | Purpose | Deletes Local | Deletes Remote |
192+
|---------|-----------|---------|---------------|----------------|
193+
| `pull` | Remote → Local | Fresh download | with `--delete` | never |
194+
| `push` | Local → Remote | Deploy changes | never | with `--delete` |
195+
| `sync` | Both ways | Stay in sync | with `--prefer-remote` | with `--prefer-local` |
196+
189197
```bash
190198
boxel sync . # Bidirectional sync (interactive)
191199
boxel sync . --prefer-local # Keep local on conflicts, sync deletions to server
@@ -195,9 +203,24 @@ boxel sync . --delete # Sync deletions both ways
195203
boxel sync . --dry-run # Preview only
196204

197205
boxel push ./local <url> # One-way push (local → remote)
206+
boxel push ./local <url> --delete # Push and remove orphaned remote files
198207
boxel pull <url> ./local # One-way pull (remote → local)
199208
```
200209

210+
**Failed download cleanup:** When `sync` encounters files that return 500 errors (broken on server), it will prompt you to delete them:
211+
```
212+
⚠️ 3 file(s) failed to download (server error):
213+
- Staff/broken-card.json
214+
- Student/corrupted.json
215+
216+
These files may be broken on the server. Delete them from remote? [y/N]
217+
```
218+
219+
> **Safety tip:** Before any destructive operation (deleting files, restoring checkpoints), create a checkpoint with a descriptive message:
220+
> ```bash
221+
> boxel history . -m "Before cleanup: removing broken server files"
222+
> ```
223+
201224
### Track & Watch
202225
203226
```bash
@@ -348,6 +371,20 @@ boxel sync . --prefer-local # Push to Boxel server
348371

349372
## Critical Patterns
350373

374+
### 0. Checkpoint Before Destructive Operations
375+
**Always create a checkpoint with a descriptive message before:**
376+
- Deleting files from server (`--prefer-local`, `push --delete`)
377+
- Restoring to an earlier checkpoint
378+
- Bulk cleanup operations
379+
380+
```bash
381+
boxel history . -m "Before cleanup: removing sample data"
382+
# Now safe to proceed with destructive operation
383+
boxel sync . --prefer-local
384+
```
385+
386+
This ensures you can always recover if something goes wrong.
387+
351388
### 1. Always Use `--prefer-local` After Restore
352389
```bash
353390
boxel history . -r 3 # Deletes files locally
@@ -388,6 +425,38 @@ export class MyCard extends CardDef {
388425

389426
---
390427

428+
## Local Workspace Organization
429+
430+
When syncing multiple workspaces locally, organize them by **domain/username/realm** to mirror the Matrix ID structure (`@username:domain`):
431+
432+
```
433+
boxel-workspaces/
434+
├── boxel.ai/ # Production domain
435+
│ └── acme-corp/ # Username
436+
│ ├── personal/ # Realm
437+
│ ├── project-atlas/
438+
│ └── inventory-tracker/
439+
└── stack.cards/ # Staging domain
440+
└── acme-corp/
441+
└── sandbox/
442+
```
443+
444+
**Benefits:**
445+
- Clear separation between production and staging environments
446+
- Matches the `@username:domain` profile ID format
447+
- Easy to identify which profile/environment a workspace belongs to
448+
449+
**First-time sync to this structure:**
450+
```bash
451+
# Production workspace
452+
boxel pull https://app.boxel.ai/username/realm/ ./boxel-workspaces/boxel.ai/username/realm
453+
454+
# Staging workspace
455+
boxel pull https://realms-staging.stack.cards/username/realm/ ./boxel-workspaces/stack.cards/username/realm
456+
```
457+
458+
---
459+
391460
## File Structure
392461

393462
```
@@ -508,6 +577,7 @@ cat ./Type/card-id.json
508577
| Files reverting after restore | Stop watch first, use `--prefer-local` after |
509578
| Watch not detecting changes | Check interval, verify workspace URL |
510579
| Definition changes not reflected | `boxel touch . Instance/file.json` |
580+
| "500 Internal Server Error" on files | Broken on server - sync will prompt to delete, or use `push --delete` |
511581

512582
---
513583

src/commands/milestone.ts

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { CheckpointManager } from '../lib/checkpoint-manager.js';
22
import * as path from 'path';
3-
import * as fs from 'fs';
43

54
// ANSI color codes
65
const FG_GREEN = '\x1b[32m';
@@ -23,17 +22,11 @@ export async function milestoneCommand(
2322
// Resolve workspace path
2423
const workspaceDir = path.resolve(workspace);
2524

26-
// Check if it's a synced workspace
27-
const manifestPath = path.join(workspaceDir, '.boxel-sync.json');
28-
if (!fs.existsSync(manifestPath)) {
29-
console.error('Error: No .boxel-sync.json found. Run sync first to establish tracking.');
30-
process.exit(1);
31-
}
32-
3325
const manager = new CheckpointManager(workspaceDir);
3426

27+
// Check if checkpoint history exists (created by pull, push, sync, or watch)
3528
if (!manager.isInitialized()) {
36-
console.error('Error: No checkpoint history found. Checkpoints are created during sync/watch.');
29+
console.error('Error: No checkpoint history found. Run pull, sync, or watch first to create checkpoints.');
3730
process.exit(1);
3831
}
3932

src/commands/sync.ts

Lines changed: 59 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -111,8 +111,24 @@ async function promptUser(question: string, options: string[]): Promise<string>
111111
});
112112
}
113113

114+
// Helper function to prompt yes/no
115+
async function promptYesNo(question: string): Promise<boolean> {
116+
const rl = readline.createInterface({
117+
input: process.stdin,
118+
output: process.stdout,
119+
});
120+
121+
return new Promise((resolve) => {
122+
rl.question(question + ' ', (answer) => {
123+
rl.close();
124+
resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes');
125+
});
126+
});
127+
}
128+
114129
class RealmSyncer extends RealmSyncBase {
115130
hasError = false;
131+
failedPulls: { relativePath: string; error: string }[] = [];
116132

117133
constructor(
118134
private syncOptions: SyncCommandOptions,
@@ -123,6 +139,11 @@ class RealmSyncer extends RealmSyncBase {
123139
super(syncOptions, matrixUrl, username, password);
124140
}
125141

142+
// Public method to delete a file from the remote server
143+
async deleteRemoteFile(relativePath: string): Promise<void> {
144+
return this.deleteFile(relativePath);
145+
}
146+
126147
private getConflictStrategy(): ConflictStrategy {
127148
if (this.syncOptions.preferLocal) return 'local';
128149
if (this.syncOptions.preferRemote) return 'remote';
@@ -265,6 +286,7 @@ class RealmSyncer extends RealmSyncBase {
265286
}
266287

267288
// Execute pulls
289+
const failedPulls: { relativePath: string; error: string }[] = [];
268290
if (pullActions.length > 0) {
269291
console.log(`\nPulling ${pullActions.length} files from remote...`);
270292
for (const action of pullActions) {
@@ -278,11 +300,19 @@ class RealmSyncer extends RealmSyncBase {
278300
};
279301
} catch (error) {
280302
this.hasError = true;
303+
const errorMsg = error instanceof Error ? error.message : String(error);
281304
console.error(`Error pulling ${action.relativePath}:`, error);
305+
// Track 500 errors for potential cleanup
306+
if (errorMsg.includes('500') || errorMsg.includes('Internal Server Error')) {
307+
failedPulls.push({ relativePath: action.relativePath, error: errorMsg });
308+
}
282309
}
283310
}
284311
}
285312

313+
// Store failed pulls for post-sync cleanup prompt
314+
this.failedPulls = failedPulls;
315+
286316
// Handle local deletions (files deleted on server) - always sync these
287317
// Create checkpoint BEFORE deleting so we can recover
288318
if (deleteLocalActions.length > 0) {
@@ -604,14 +634,10 @@ export async function syncCommand(
604634
explicitUrl: string,
605635
options: SyncCommandOptionsInput,
606636
): Promise<void> {
607-
const matrixUrl = process.env.MATRIX_URL;
608-
const matrixUsername = process.env.MATRIX_USERNAME;
609-
const matrixPassword = process.env.MATRIX_PASSWORD;
610-
611-
if (!matrixUrl || !matrixUsername || !matrixPassword) {
612-
console.error('Missing Matrix credentials in environment variables');
613-
process.exit(1);
614-
}
637+
// Determine workspace URL for profile detection (use explicit URL or resolve later)
638+
const urlForProfile = explicitUrl || (workspaceRef.startsWith('http') ? workspaceRef : '');
639+
const { matrixUrl, username: matrixUsername, password: matrixPassword } =
640+
await validateMatrixEnvVars(urlForProfile);
615641

616642
let localDir: string;
617643
let workspaceUrl: string;
@@ -675,6 +701,31 @@ export async function syncCommand(
675701
await syncer.initialize();
676702
await syncer.sync();
677703

704+
// Handle failed pulls - offer to delete broken files from server
705+
if (syncer.failedPulls.length > 0) {
706+
console.log(`\n⚠️ ${syncer.failedPulls.length} file(s) failed to download (server error):`);
707+
for (const failed of syncer.failedPulls) {
708+
console.log(` - ${failed.relativePath}`);
709+
}
710+
711+
const shouldDelete = await promptYesNo(
712+
'\nThese files may be broken on the server. Delete them from remote? [y/N]'
713+
);
714+
715+
if (shouldDelete) {
716+
console.log('\nDeleting broken files from server...');
717+
for (const failed of syncer.failedPulls) {
718+
try {
719+
await syncer.deleteRemoteFile(failed.relativePath);
720+
console.log(` Deleted: ${failed.relativePath}`);
721+
} catch (error) {
722+
console.error(` Failed to delete ${failed.relativePath}:`, error);
723+
}
724+
}
725+
console.log('Cleanup completed.');
726+
}
727+
}
728+
678729
if (syncer.hasError) {
679730
console.log('Sync completed with errors. View logs for details.');
680731
process.exit(2);

0 commit comments

Comments
 (0)