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

Commit 5f2a86d

Browse files
ChrisChris
authored andcommitted
realm.json handling
1 parent 3a07b66 commit 5f2a86d

8 files changed

Lines changed: 52 additions & 39 deletions

File tree

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ dist/
88
.env
99
.env.local
1010

11-
# Sync artifacts (workspace-specific)
11+
# Synced workspaces (never commit workspace content)
12+
stack.cards/
13+
boxel.ai/
1214
.boxel-sync.json
1315
.boxel-history/
1416

src/commands/history.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ function scanWorkspaceForChanges(workspaceDir: string): CheckpointChange[] {
1717
for (const entry of entries) {
1818
// Skip internal files
1919
if (entry.name.startsWith('.boxel-') || entry.name === '.git') continue;
20-
if (entry.name.startsWith('.') && entry.name !== '.realm.json') continue;
20+
if (entry.name.startsWith('.')) continue;
2121

2222
const fullPath = path.join(dir, entry.name);
2323
const relativePath = prefix ? `${prefix}/${entry.name}` : entry.name;

src/commands/push.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { RealmSyncBase, validateMatrixEnvVars, type SyncOptions } from '../lib/realm-sync-base.js';
1+
import { RealmSyncBase, validateMatrixEnvVars, isProtectedFile, type SyncOptions } from '../lib/realm-sync-base.js';
22
import { CheckpointManager, type CheckpointChange } from '../lib/checkpoint-manager.js';
33
import * as fs from 'fs';
44
import * as path from 'path';
@@ -88,6 +88,7 @@ class RealmPusher extends RealmSyncBase {
8888
console.log('Workspace URL changed, will upload all files');
8989
}
9090
for (const [relativePath, localPath] of localFiles) {
91+
if (isProtectedFile(relativePath)) continue;
9192
filesToUpload.set(relativePath, localPath);
9293
}
9394
} else {
@@ -96,6 +97,10 @@ class RealmPusher extends RealmSyncBase {
9697
let skipped = 0;
9798

9899
for (const [relativePath, localPath] of localFiles) {
100+
if (isProtectedFile(relativePath)) {
101+
skipped++;
102+
continue;
103+
}
99104
const currentHash = computeFileHash(localPath);
100105
const previousHash = manifest.files[relativePath];
101106

@@ -135,6 +140,13 @@ class RealmPusher extends RealmSyncBase {
135140
const remoteFiles = await this.getRemoteFileList();
136141
const filesToDelete = new Set(remoteFiles.keys());
137142

143+
// Never delete protected files from the server
144+
for (const relativePath of filesToDelete) {
145+
if (isProtectedFile(relativePath)) {
146+
filesToDelete.delete(relativePath);
147+
}
148+
}
149+
138150
for (const relativePath of localFiles.keys()) {
139151
filesToDelete.delete(relativePath);
140152
}

src/commands/sync.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { RealmSyncBase, validateMatrixEnvVars, type SyncOptions } from '../lib/realm-sync-base.js';
1+
import { RealmSyncBase, validateMatrixEnvVars, isProtectedFile, type SyncOptions } from '../lib/realm-sync-base.js';
22
import { resolveWorkspace } from '../lib/workspace-resolver.js';
33
import { MatrixClient } from '../lib/matrix-client.js';
44
import { CheckpointManager, type CheckpointChange } from '../lib/checkpoint-manager.js';
@@ -413,10 +413,15 @@ class RealmSyncer extends RealmSyncBase {
413413
isFirstSync: boolean,
414414
): FileAction[] {
415415
const actions: FileAction[] = [];
416-
// Use remoteFiles for existence check (includes dotfiles like .realm.json)
417416
const allPaths = new Set([...localFiles.keys(), ...remoteFiles.keys()]);
418417

419418
for (const relativePath of allPaths) {
419+
// Protected files (e.g. .realm.json) are server-managed and must never be
420+
// pushed, deleted, or overwritten on the server via the CLI.
421+
if (isProtectedFile(relativePath)) {
422+
continue;
423+
}
424+
420425
const localPath = localFiles.get(relativePath);
421426
const remoteMtime = remoteMtimes.get(relativePath);
422427
const baseState = manifest?.files[relativePath];
@@ -461,7 +466,7 @@ class RealmSyncer extends RealmSyncBase {
461466
const localNew = hasLocal && !hasBase;
462467
const localDeleted = !hasLocal && hasBase;
463468

464-
// For remote change detection: if file exists but no mtime available (e.g., .realm.json),
469+
// For remote change detection: if file exists but no mtime available,
465470
// we can't detect changes, so assume unchanged
466471
const remoteChanged = hasRemote && hasBase && remoteMtime !== undefined && remoteMtime !== baseState.remoteMtime;
467472
const remoteNew = hasRemote && !hasBase;

src/commands/track.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ export async function trackCommand(
5050
for (const entry of entries) {
5151
// Skip internal files
5252
if (entry.name.startsWith('.boxel-') || entry.name === '.git') continue;
53-
if (entry.name.startsWith('.') && entry.name !== '.realm.json') continue;
53+
if (entry.name.startsWith('.')) continue;
5454

5555
const fullPath = path.join(dir, entry.name);
5656
const relativePath = prefix ? `${prefix}/${entry.name}` : entry.name;
@@ -153,7 +153,7 @@ export async function trackCommand(
153153
for (const entry of entries) {
154154
// Skip internal files
155155
if (entry.name.startsWith('.boxel-') || entry.name === '.git') continue;
156-
if (entry.name.startsWith('.') && entry.name !== '.realm.json') continue;
156+
if (entry.name.startsWith('.')) continue;
157157

158158
const fullPath = path.join(dir, entry.name);
159159
const relativePath = prefix ? `${prefix}/${entry.name}` : entry.name;
@@ -243,7 +243,7 @@ export async function trackCommand(
243243

244244
// Skip internal files
245245
if (filename.startsWith('.boxel-') || filename.includes('.git')) return;
246-
if (filename.startsWith('.') && filename !== '.realm.json') return;
246+
if (filename.startsWith('.')) return;
247247

248248
// Debounced check for changes
249249
checkForChanges();

src/commands/watch.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { MatrixClient } from '../lib/matrix-client.js';
44
import { RealmAuthClient } from '../lib/realm-auth-client.js';
55
import { resolveWorkspace } from '../lib/workspace-resolver.js';
66
import { CheckpointManager, type CheckpointChange } from '../lib/checkpoint-manager.js';
7+
import { isProtectedFile } from '../lib/realm-sync-base.js';
78
import { createHash } from 'crypto';
89
import { getEditingFiles } from '../lib/edit-lock.js';
910
import { getProfileManager, formatProfileBadge } from '../lib/profile-manager.js';
@@ -266,6 +267,7 @@ export async function watchCommand(
266267
let hasNewChanges = false;
267268

268269
for (const [file, mtime] of Object.entries(remoteMtimes)) {
270+
if (isProtectedFile(file)) continue;
269271
if (!(file in realm.lastKnownState)) {
270272
if (!realm.pendingChanges.has(file) || realm.pendingChanges.get(file)!.mtime !== mtime) {
271273
realm.pendingChanges.set(file, { status: 'added', mtime });

src/lib/checkpoint-manager.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { execSync, spawnSync } from 'child_process';
22
import * as fs from 'fs';
33
import * as path from 'path';
4+
import { isProtectedFile } from './realm-sync-base.js';
45

56
export interface Checkpoint {
67
hash: string;
@@ -105,7 +106,7 @@ export class CheckpointManager {
105106
if (entry.name === '.boxel-history' || entry.name === '.boxel-sync.json') {
106107
continue;
107108
}
108-
if (entry.name.startsWith('.') && entry.name !== '.realm.json') {
109+
if (entry.name.startsWith('.')) {
109110
continue;
110111
}
111112

@@ -286,14 +287,12 @@ export class CheckpointManager {
286287
// - More than 3 files changed
287288
// - Any .gts file changed (card definition)
288289
// - Any file added or deleted
289-
// - .realm.json changed
290290

291291
if (changes.length > 3) return true;
292292

293293
for (const change of changes) {
294294
if (change.status === 'added' || change.status === 'deleted') return true;
295295
if (change.file.endsWith('.gts')) return true;
296-
if (change.file === '.realm.json') return true;
297296
}
298297

299298
return false;
@@ -468,6 +467,7 @@ export class CheckpointManager {
468467

469468
// Remove files that don't exist in the checkpoint
470469
for (const file of workspaceFiles) {
470+
if (isProtectedFile(file)) continue;
471471
if (!historyFiles.includes(file)) {
472472
const filePath = path.join(this.workspaceDir, file);
473473
fs.unlinkSync(filePath);
@@ -476,6 +476,7 @@ export class CheckpointManager {
476476

477477
// Copy files from history to workspace
478478
for (const file of historyFiles) {
479+
if (isProtectedFile(file)) continue;
479480
const srcPath = path.join(this.gitDir, file);
480481
const destPath = path.join(this.workspaceDir, file);
481482

src/lib/realm-sync-base.ts

Lines changed: 18 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,14 @@ import ignoreModule from 'ignore';
66
const ignore = ignoreModule.default || ignoreModule;
77
type Ignore = ReturnType<typeof ignore>;
88

9+
// Files that must never be pushed, deleted, or overwritten on the server via CLI.
10+
// These are server-managed config files - corrupting them can break a realm.
11+
export const PROTECTED_FILES = new Set(['.realm.json']);
12+
13+
export function isProtectedFile(relativePath: string): boolean {
14+
return PROTECTED_FILES.has(relativePath);
15+
}
16+
917
export const SupportedMimeType = {
1018
CardJson: 'application/vnd.card+json',
1119
CardSource: 'application/vnd.card+source',
@@ -142,27 +150,6 @@ export abstract class RealmSyncBase {
142150
throw error;
143151
}
144152

145-
// Check for .realm.json in root directory
146-
if (!dir) {
147-
try {
148-
const realmJsonUrl = this.buildFileUrl('.realm.json');
149-
const jwt = await this.realmAuthClient.getJWT();
150-
151-
const response = await fetch(realmJsonUrl, {
152-
method: 'HEAD',
153-
headers: {
154-
Authorization: jwt,
155-
},
156-
});
157-
158-
if (response.ok) {
159-
files.set('.realm.json', true);
160-
}
161-
} catch {
162-
console.log('Note: .realm.json not found in remote realm');
163-
}
164-
}
165-
166153
return files;
167154
}
168155

@@ -289,6 +276,11 @@ export abstract class RealmSyncBase {
289276
}
290277

291278
protected async uploadFile(relativePath: string, localPath: string): Promise<void> {
279+
if (isProtectedFile(relativePath)) {
280+
console.log(` Skipped (protected): ${relativePath}`);
281+
return;
282+
}
283+
292284
console.log(`Uploading: ${relativePath}`);
293285

294286
if (this.options.dryRun) {
@@ -362,6 +354,11 @@ export abstract class RealmSyncBase {
362354
}
363355

364356
protected async deleteFile(relativePath: string): Promise<void> {
357+
if (isProtectedFile(relativePath)) {
358+
console.log(` Skipped (protected): ${relativePath}`);
359+
return;
360+
}
361+
365362
console.log(`Deleting remote: ${relativePath}`);
366363

367364
if (this.options.dryRun) {
@@ -451,9 +448,6 @@ export abstract class RealmSyncBase {
451448
}
452449

453450
if (fileName.startsWith('.')) {
454-
if (fileName === '.realm.json') {
455-
return false;
456-
}
457451
return true;
458452
}
459453

@@ -467,9 +461,6 @@ export abstract class RealmSyncBase {
467461
private shouldIgnoreRemoteFile(relativePath: string): boolean {
468462
const fileName = path.basename(relativePath);
469463
if (fileName.startsWith('.')) {
470-
if (fileName === '.realm.json') {
471-
return false;
472-
}
473464
return true;
474465
}
475466
return false;

0 commit comments

Comments
 (0)