From d43e1c1c17813fe0f6872582f7c72292100bacf5 Mon Sep 17 00:00:00 2001 From: Chris Wanstrath Date: Mon, 9 Feb 2026 20:36:58 -0800 Subject: [PATCH] simpler sync --- src/cli/commands/sync.ts | 83 +++++++++++++++------------------------- 1 file changed, 30 insertions(+), 53 deletions(-) diff --git a/src/cli/commands/sync.ts b/src/cli/commands/sync.ts index cf97a3e..da7565d 100644 --- a/src/cli/commands/sync.ts +++ b/src/cli/commands/sync.ts @@ -1,9 +1,9 @@ import type { Manifest } from '@types' import { loadGitignore } from '@gitignore' -import { computeHash, generateManifest } from '%sync' +import { generateManifest } from '%sync' import color from 'kleur' import { diffLines } from 'diff' -import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, unlinkSync, watch, writeFileSync } from 'fs' +import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, unlinkSync, watch, writeFileSync } from 'fs' import { dirname, join } from 'path' import { del, download, get, getManifest, handleError, makeUrl, post, put } from '../http' import { confirm, prompt } from '../prompts' @@ -64,7 +64,7 @@ export async function getApp(name: string) { console.log(color.green(`✓ Downloaded ${name}`)) } -export async function pushApp() { +export async function pushApp(options: { quiet?: boolean } = {}) { if (!isApp()) { console.error(notAppError()) return @@ -107,7 +107,7 @@ export async function pushApp() { } if (toUpload.length === 0 && toDelete.length === 0) { - console.log('Already up to date') + if (!options.quiet) console.log('Already up to date') return } @@ -171,7 +171,7 @@ export async function pushApp() { console.log(color.green(`✓ Deployed and activated version ${version}`)) } -export async function pullApp(options: { force?: boolean } = {}) { +export async function pullApp(options: { force?: boolean, quiet?: boolean } = {}) { if (!isApp()) { console.error(notAppError()) return @@ -207,7 +207,7 @@ export async function pullApp(options: { force?: boolean } = {}) { const toDelete = localOnly if (toDownload.length === 0 && toDelete.length === 0) { - console.log('Already up to date') + if (!options.quiet) console.log('Already up to date') return } @@ -398,39 +398,32 @@ export async function syncApp() { } const appName = getAppName() - const gitignore = loadGitignore(process.cwd()) - const localHashes = new Map() - // Initialize local hashes - const manifest = generateManifest(process.cwd(), appName) - for (const [path, info] of Object.entries(manifest.files)) { - localHashes.set(path, info.hash) + // Verify app exists on server + const result = await getManifest(appName) + if (result === null) return + if (!result.exists) { + console.error(`App ${color.bold(appName)} doesn't exist on server. Run ${color.bold('toes push')} first.`) + return } console.log(`Syncing ${color.bold(appName)}...`) - // Watch local files - const watcher = watch(process.cwd(), { recursive: true }, async (_event, filename) => { + // Initial sync: pull remote changes, then push local changes + await pullApp({ force: true, quiet: true }) + await pushApp({ quiet: true }) + + const gitignore = loadGitignore(process.cwd()) + + // Watch local files with debounce → push + let pushTimer: Timer | null = null + const watcher = watch(process.cwd(), { recursive: true }, (_event, filename) => { if (!filename || gitignore.shouldExclude(filename)) return - - const fullPath = join(process.cwd(), filename) - - if (existsSync(fullPath) && statSync(fullPath).isFile()) { - const content = readFileSync(fullPath) - const hash = computeHash(content) - if (localHashes.get(filename) !== hash) { - localHashes.set(filename, hash) - await put(`/api/sync/apps/${appName}/files/${filename}`, content) - console.log(` ${color.green('↑')} ${filename}`) - } - } else if (!existsSync(fullPath)) { - localHashes.delete(filename) - await del(`/api/sync/apps/${appName}/files/${filename}`) - console.log(` ${color.red('✗')} ${filename}`) - } + if (pushTimer) clearTimeout(pushTimer) + pushTimer = setTimeout(() => pushApp({ quiet: true }), 500) }) - // Connect to SSE for remote changes + // Connect to SSE for remote changes → pull const url = makeUrl(`/api/sync/apps/${appName}/watch`) let res: Response try { @@ -452,11 +445,12 @@ export async function syncApp() { return } - console.log(` Connected to server, watching for changes...`) + console.log(` Connected, watching for changes...`) const reader = res.body.getReader() const decoder = new TextDecoder() let buffer = '' + let pullTimer: Timer | null = null try { while (true) { @@ -469,30 +463,13 @@ export async function syncApp() { for (const line of lines) { if (!line.startsWith('data: ')) continue - const event = JSON.parse(line.slice(6)) as { type: 'change' | 'delete', path: string, hash?: string } - - if (event.type === 'change') { - // Skip if we already have this version (handles echo from our own changes) - if (localHashes.get(event.path) === event.hash) continue - const content = await download(`/api/sync/apps/${appName}/files/${event.path}`) - if (content) { - const fullPath = join(process.cwd(), event.path) - mkdirSync(dirname(fullPath), { recursive: true }) - writeFileSync(fullPath, content) - localHashes.set(event.path, event.hash!) - console.log(` ${color.green('↓')} ${event.path}`) - } - } else if (event.type === 'delete') { - const fullPath = join(process.cwd(), event.path) - if (existsSync(fullPath)) { - unlinkSync(fullPath) - localHashes.delete(event.path) - console.log(` ${color.red('✗')} ${event.path} (remote)`) - } - } + if (pullTimer) clearTimeout(pullTimer) + pullTimer = setTimeout(() => pullApp({ force: true, quiet: true }), 500) } } } finally { + if (pushTimer) clearTimeout(pushTimer) + if (pullTimer) clearTimeout(pullTimer) watcher.close() } }