simplify server

This commit is contained in:
Chris Wanstrath 2026-02-01 21:40:32 -08:00
parent 27c1bfd969
commit a81d61f910
3 changed files with 26 additions and 95 deletions

View File

@ -1,4 +1,4 @@
import { APPS_DIR, allApps, onChange, renameApp, startApp, stopApp, updateAppIcon } from '$apps'
import { APPS_DIR, allApps, onChange, registerApp, renameApp, startApp, stopApp, updateAppIcon } from '$apps'
import type { App as BackendApp } from '$apps'
import type { App as SharedApp } from '@types'
import { generateTemplates, type TemplateType } from '%templates'
@ -101,6 +101,9 @@ router.post('/', async c => {
// Create current symlink
symlinkSync(ts, currentPath)
// Register and start the app
registerApp(name)
return c.json({ ok: true, name })
})

View File

@ -1,4 +1,4 @@
import { APPS_DIR, allApps, removeApp, restartApp, startApp, stopApp } from '$apps'
import { APPS_DIR, allApps, registerApp, removeApp, restartApp } from '$apps'
import { computeHash, generateManifest } from '../sync'
import { loadGitignore } from '@gitignore'
import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, renameSync, rmSync, symlinkSync, unlinkSync, watch, writeFileSync } from 'fs'
@ -223,9 +223,13 @@ router.post('/apps/:app/activate', async c => {
console.error(`Failed to clean up old versions: ${e}`)
}
// Restart app to use new version
// Register new app or restart existing
const app = allApps().find(a => a.name === appName)
if (app?.state === 'running') {
if (!app) {
// New app - register it
registerApp(appName)
} else if (app.state === 'running') {
// Existing app - restart it
try {
await restartApp(appName)
} catch (e) {

View File

@ -1,7 +1,7 @@
import type { App as SharedApp, AppState } from '@types'
import type { Subprocess } from 'bun'
import { DEFAULT_EMOJI } from '@types'
import { existsSync, readdirSync, readFileSync, realpathSync, renameSync, statSync, watch, writeFileSync } from 'fs'
import { existsSync, readdirSync, readFileSync, realpathSync, renameSync, writeFileSync } from 'fs'
import { join, resolve } from 'path'
import { appLog, hostLog, setApps } from './tui'
@ -59,7 +59,6 @@ export function initApps() {
setupShutdownHandlers()
discoverApps()
runApps()
watchAppsDir()
}
export function onChange(cb: () => void) {
@ -86,6 +85,20 @@ export function removeApp(dir: string) {
update()
}
export function registerApp(dir: string) {
if (_apps.has(dir)) return // Already registered
const { pkg, error } = loadApp(dir)
const state: AppState = error ? 'invalid' : 'stopped'
const icon = pkg.toes?.icon ?? DEFAULT_EMOJI
const tool = pkg.toes?.tool
_apps.set(dir, { name: dir, state, icon, error, tool })
update()
if (!error) {
runApp(dir, getPort(dir))
}
}
export function renameApp(oldName: string, newName: string): { ok: boolean, error?: string } {
const app = _apps.get(oldName)
if (!app) return { ok: false, error: 'App not found' }
@ -359,14 +372,6 @@ function initPortPool() {
}
}
function isDir(path: string): boolean {
try {
return statSync(path).isDirectory()
} catch {
return false
}
}
function loadApp(dir: string): LoadResult {
try {
const pkgPath = join(APPS_DIR, dir, 'current', 'package.json')
@ -599,84 +604,3 @@ function startShutdownTimeout(app: App) {
}, SHUTDOWN_TIMEOUT)
}
function watchAppsDir() {
watch(APPS_DIR, { recursive: true }, (_event, filename) => {
if (!filename) return
const parts = filename.split('/')
const dir = parts[0]!
// Ignore changes inside old timestamp dirs (but allow current/)
if (parts.length > 2 && parts[1] !== 'current') return
// For versioned apps, only care about changes to "current" directory
if (parts.length === 2 && parts[1] !== 'current' && parts[1] !== 'package.json') return
// Handle new directory appearing
if (!_apps.has(dir)) {
// Make sure the directory actually exists (avoids race with rename)
if (!isDir(join(APPS_DIR, dir))) return
const { pkg, error } = loadApp(dir)
const state: AppState = error ? 'invalid' : 'stopped'
const icon = pkg.toes?.icon
const tool = pkg.toes?.tool
_apps.set(dir, { name: dir, state, icon, error, tool })
update()
if (!error) {
runApp(dir, getPort(dir))
}
return
}
const app = _apps.get(dir)!
// check if app was deleted
if (!isDir(join(APPS_DIR, dir))) {
clearTimers(app)
if (app.port) releasePort(app.port)
_apps.delete(dir)
update()
return
}
// Only care about package.json changes for existing apps
if (!filename.endsWith('package.json')) return
const { pkg, error } = loadApp(dir)
// Update icon, tool, and error from package.json
const iconChanged = app.icon !== pkg.toes?.icon
const toolChanged = app.tool !== pkg.toes?.tool
app.icon = pkg.toes?.icon
app.tool = pkg.toes?.tool
app.error = error
// Broadcast if icon or tool changed
if (iconChanged || toolChanged) update()
// App became valid - start it if stopped
if (!error && app.state === 'invalid') {
app.state = 'stopped'
runApp(dir, getPort(dir))
}
// App became invalid - stop it if running
if (error && app.state === 'running') {
app.state = 'invalid'
clearTimers(app)
app.proc?.kill()
}
// Update state if already stopped/invalid
if (error && app.state === 'stopped') {
app.state = 'invalid'
update()
}
if (!error && app.state === 'invalid') {
app.state = 'stopped'
update()
}
})
}