toes history

This commit is contained in:
Chris Wanstrath 2026-02-09 21:42:40 -08:00
parent 115d3199e8
commit 79a0471383
4 changed files with 120 additions and 1 deletions

View File

@ -13,4 +13,4 @@ export {
stopApp, stopApp,
} from './manage' } from './manage'
export { statsApp } from './stats' export { statsApp } from './stats'
export { cleanApp, diffApp, getApp, pullApp, pushApp, rollbackApp, stashApp, stashListApp, stashPopApp, statusApp, syncApp, versionsApp } from './sync' export { cleanApp, diffApp, getApp, historyApp, pullApp, pushApp, rollbackApp, stashApp, stashListApp, stashPopApp, statusApp, syncApp, versionsApp } from './sync'

View File

@ -24,6 +24,55 @@ interface ManifestDiff {
remoteManifest: Manifest | null remoteManifest: Manifest | null
} }
export async function historyApp(name?: string) {
const appName = resolveAppName(name)
if (!appName) return
type HistoryEntry = {
version: string
current: boolean
added: string[]
modified: string[]
deleted: string[]
}
type HistoryResponse = { history: HistoryEntry[] }
const result = await get<HistoryResponse>(`/api/sync/apps/${appName}/history`)
if (!result) return
if (result.history.length === 0) {
console.log(`No versions found for ${color.bold(appName)}`)
return
}
console.log(`History for ${color.bold(appName)}:\n`)
for (const entry of result.history) {
const date = formatVersion(entry.version)
const label = entry.current ? ` ${color.green('→')} ${color.bold(entry.version)}` : ` ${entry.version}`
const suffix = entry.current ? ` ${color.green('(current)')}` : ''
console.log(`${label} ${color.gray(date)}${suffix}`)
const hasChanges = entry.added.length > 0 || entry.modified.length > 0 || entry.deleted.length > 0
if (!hasChanges) {
console.log(color.gray(' No changes'))
}
for (const file of entry.added) {
console.log(` ${color.green('+')} ${file}`)
}
for (const file of entry.modified) {
console.log(` ${color.yellow('~')} ${file}`)
}
for (const file of entry.deleted) {
console.log(` ${color.red('-')} ${file}`)
}
console.log()
}
}
export async function getApp(name: string) { export async function getApp(name: string) {
console.log(`Fetching ${color.bold(name)} from server...`) console.log(`Fetching ${color.bold(name)} from server...`)

View File

@ -11,6 +11,7 @@ import {
envRm, envRm,
envSet, envSet,
getApp, getApp,
historyApp,
infoApp, infoApp,
listApps, listApps,
logApp, logApp,
@ -258,6 +259,13 @@ program
.argument('[name]', 'app name (uses current directory if omitted)') .argument('[name]', 'app name (uses current directory if omitted)')
.action(versionsApp) .action(versionsApp)
program
.command('history')
.helpGroup('Config:')
.description('Show file changes between versions')
.argument('[name]', 'app name (uses current directory if omitted)')
.action(historyApp)
program program
.command('rollback') .command('rollback')
.helpGroup('Config:') .helpGroup('Config:')

View File

@ -50,6 +50,68 @@ router.get('/apps/:app/versions', c => {
return c.json({ versions, current: currentVersion }) return c.json({ versions, current: currentVersion })
}) })
router.get('/apps/:app/history', c => {
const appName = c.req.param('app')
if (!appName) return c.json({ error: 'App not found' }, 404)
const appDir = safePath(APPS_DIR, appName)
if (!appDir) return c.json({ error: 'Invalid path' }, 400)
if (!existsSync(appDir)) return c.json({ error: 'App not found' }, 404)
const currentLink = join(appDir, 'current')
const currentVersion = existsSync(currentLink)
? realpathSync(currentLink).split('/').pop()
: null
const entries = readdirSync(appDir, { withFileTypes: true })
const versions = entries
.filter(e => e.isDirectory() && /^\d{8}-\d{6}$/.test(e.name))
.map(e => e.name)
.sort()
.reverse() // Newest first
// Generate manifests for each version
const manifests = new Map<string, Record<string, { hash: string }>>()
for (const version of versions) {
const versionPath = join(appDir, version)
const manifest = generateManifest(versionPath, appName)
manifests.set(version, manifest.files)
}
// Diff consecutive pairs
const history = versions.map((version, i) => {
const current = version === currentVersion
const files = manifests.get(version)!
const olderVersion = versions[i + 1]
const olderFiles = olderVersion ? manifests.get(olderVersion)! : {}
const added: string[] = []
const modified: string[] = []
const deleted: string[] = []
// Files in this version
for (const [path, info] of Object.entries(files)) {
const older = olderFiles[path]
if (!older) {
added.push(path)
} else if (older.hash !== info.hash) {
modified.push(path)
}
}
// Files removed in this version
for (const path of Object.keys(olderFiles)) {
if (!files[path]) {
deleted.push(path)
}
}
return { version, current, added: added.sort(), modified: modified.sort(), deleted: deleted.sort() }
})
return c.json({ history })
})
router.get('/apps/:app/manifest', c => { router.get('/apps/:app/manifest', c => {
const appName = c.req.param('app') const appName = c.req.param('app')
if (!appName) return c.json({ error: 'App not found' }, 404) if (!appName) return c.json({ error: 'App not found' }, 404)