Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/boxel-cli/src/commands/realm/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { Command } from 'commander';
import { registerCreateCommand } from './create';
import { registerPullCommand } from './pull';
import { registerPushCommand } from './push';
import { registerSyncCommand } from './sync';

export function registerRealmCommand(program: Command): void {
let realm = program
Expand All @@ -11,4 +12,5 @@ export function registerRealmCommand(program: Command): void {
registerCreateCommand(realm);
registerPullCommand(realm);
registerPushCommand(realm);
registerSyncCommand(realm);
}
86 changes: 7 additions & 79 deletions packages/boxel-cli/src/commands/realm/push.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,85 +12,13 @@ import {
getProfileManager,
type ProfileManager,
} from '../../lib/profile-manager';
import * as fs from 'fs/promises';
import * as path from 'path';
import * as crypto from 'crypto';

interface SyncManifest {
realmUrl: string;
files: Record<string, string>; // relativePath -> contentHash
remoteMtimes?: Record<string, number>; // relativePath -> last-seen server mtime
}

function isValidManifest(value: unknown): value is SyncManifest {
if (typeof value !== 'object' || value === null) return false;
const v = value as Record<string, unknown>;
if (typeof v.realmUrl !== 'string') return false;
if (typeof v.files !== 'object' || v.files === null) return false;
for (const hash of Object.values(v.files as Record<string, unknown>)) {
if (typeof hash !== 'string') return false;
}
if (v.remoteMtimes !== undefined) {
if (typeof v.remoteMtimes !== 'object' || v.remoteMtimes === null) {
return false;
}
for (const mtime of Object.values(
v.remoteMtimes as Record<string, unknown>,
)) {
if (typeof mtime !== 'number') return false;
}
}
return true;
}

async function pathExists(p: string): Promise<boolean> {
try {
await fs.access(p);
return true;
} catch {
return false;
}
}

async function computeFileHash(filePath: string): Promise<string> {
const content = await fs.readFile(filePath);
return crypto.createHash('md5').update(content).digest('hex');
}

async function loadManifest(localDir: string): Promise<SyncManifest | null> {
const manifestPath = path.join(localDir, '.boxel-sync.json');
let content: string;
try {
content = await fs.readFile(manifestPath, 'utf8');
} catch (err: any) {
if (err.code === 'ENOENT') return null;
throw err;
}

let parsed: unknown;
try {
parsed = JSON.parse(content);
} catch {
return null;
}

if (!isValidManifest(parsed)) {
console.warn(
'Warning: .boxel-sync.json is malformed or has an unexpected shape; falling back to a full upload.',
);
return null;
}

return parsed;
}

async function saveManifest(
localDir: string,
manifest: SyncManifest,
): Promise<void> {
const manifestPath = path.join(localDir, '.boxel-sync.json');
await fs.writeFile(manifestPath, JSON.stringify(manifest, null, 2));
}
import {
type SyncManifest,
computeFileHash,
loadManifest,
saveManifest,
pathExists,
} from '../../lib/sync-manifest';

interface PushOptions extends SyncOptions {
deleteRemote?: boolean;
Expand Down
Loading