simpler sync
This commit is contained in:
parent
1685cc135d
commit
d43e1c1c17
|
|
@ -1,9 +1,9 @@
|
||||||
import type { Manifest } from '@types'
|
import type { Manifest } from '@types'
|
||||||
import { loadGitignore } from '@gitignore'
|
import { loadGitignore } from '@gitignore'
|
||||||
import { computeHash, generateManifest } from '%sync'
|
import { generateManifest } from '%sync'
|
||||||
import color from 'kleur'
|
import color from 'kleur'
|
||||||
import { diffLines } from 'diff'
|
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 { dirname, join } from 'path'
|
||||||
import { del, download, get, getManifest, handleError, makeUrl, post, put } from '../http'
|
import { del, download, get, getManifest, handleError, makeUrl, post, put } from '../http'
|
||||||
import { confirm, prompt } from '../prompts'
|
import { confirm, prompt } from '../prompts'
|
||||||
|
|
@ -64,7 +64,7 @@ export async function getApp(name: string) {
|
||||||
console.log(color.green(`✓ Downloaded ${name}`))
|
console.log(color.green(`✓ Downloaded ${name}`))
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function pushApp() {
|
export async function pushApp(options: { quiet?: boolean } = {}) {
|
||||||
if (!isApp()) {
|
if (!isApp()) {
|
||||||
console.error(notAppError())
|
console.error(notAppError())
|
||||||
return
|
return
|
||||||
|
|
@ -107,7 +107,7 @@ export async function pushApp() {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (toUpload.length === 0 && toDelete.length === 0) {
|
if (toUpload.length === 0 && toDelete.length === 0) {
|
||||||
console.log('Already up to date')
|
if (!options.quiet) console.log('Already up to date')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -171,7 +171,7 @@ export async function pushApp() {
|
||||||
console.log(color.green(`✓ Deployed and activated version ${version}`))
|
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()) {
|
if (!isApp()) {
|
||||||
console.error(notAppError())
|
console.error(notAppError())
|
||||||
return
|
return
|
||||||
|
|
@ -207,7 +207,7 @@ export async function pullApp(options: { force?: boolean } = {}) {
|
||||||
const toDelete = localOnly
|
const toDelete = localOnly
|
||||||
|
|
||||||
if (toDownload.length === 0 && toDelete.length === 0) {
|
if (toDownload.length === 0 && toDelete.length === 0) {
|
||||||
console.log('Already up to date')
|
if (!options.quiet) console.log('Already up to date')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -398,39 +398,32 @@ export async function syncApp() {
|
||||||
}
|
}
|
||||||
|
|
||||||
const appName = getAppName()
|
const appName = getAppName()
|
||||||
const gitignore = loadGitignore(process.cwd())
|
|
||||||
const localHashes = new Map<string, string>()
|
|
||||||
|
|
||||||
// Initialize local hashes
|
// Verify app exists on server
|
||||||
const manifest = generateManifest(process.cwd(), appName)
|
const result = await getManifest(appName)
|
||||||
for (const [path, info] of Object.entries(manifest.files)) {
|
if (result === null) return
|
||||||
localHashes.set(path, info.hash)
|
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)}...`)
|
console.log(`Syncing ${color.bold(appName)}...`)
|
||||||
|
|
||||||
// Watch local files
|
// Initial sync: pull remote changes, then push local changes
|
||||||
const watcher = watch(process.cwd(), { recursive: true }, async (_event, filename) => {
|
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
|
if (!filename || gitignore.shouldExclude(filename)) return
|
||||||
|
if (pushTimer) clearTimeout(pushTimer)
|
||||||
const fullPath = join(process.cwd(), filename)
|
pushTimer = setTimeout(() => pushApp({ quiet: true }), 500)
|
||||||
|
|
||||||
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}`)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// Connect to SSE for remote changes
|
// Connect to SSE for remote changes → pull
|
||||||
const url = makeUrl(`/api/sync/apps/${appName}/watch`)
|
const url = makeUrl(`/api/sync/apps/${appName}/watch`)
|
||||||
let res: Response
|
let res: Response
|
||||||
try {
|
try {
|
||||||
|
|
@ -452,11 +445,12 @@ export async function syncApp() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(` Connected to server, watching for changes...`)
|
console.log(` Connected, watching for changes...`)
|
||||||
|
|
||||||
const reader = res.body.getReader()
|
const reader = res.body.getReader()
|
||||||
const decoder = new TextDecoder()
|
const decoder = new TextDecoder()
|
||||||
let buffer = ''
|
let buffer = ''
|
||||||
|
let pullTimer: Timer | null = null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
while (true) {
|
while (true) {
|
||||||
|
|
@ -469,30 +463,13 @@ export async function syncApp() {
|
||||||
|
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
if (!line.startsWith('data: ')) continue
|
if (!line.startsWith('data: ')) continue
|
||||||
const event = JSON.parse(line.slice(6)) as { type: 'change' | 'delete', path: string, hash?: string }
|
if (pullTimer) clearTimeout(pullTimer)
|
||||||
|
pullTimer = setTimeout(() => pullApp({ force: true, quiet: true }), 500)
|
||||||
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)`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
|
if (pushTimer) clearTimeout(pushTimer)
|
||||||
|
if (pullTimer) clearTimeout(pullTimer)
|
||||||
watcher.close()
|
watcher.close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user