diff --git a/src/server/api/apps.ts b/src/server/api/apps.ts index ac544a5..148b12a 100644 --- a/src/server/api/apps.ts +++ b/src/server/api/apps.ts @@ -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 }) }) diff --git a/src/server/api/sync.ts b/src/server/api/sync.ts index cdc1f37..656e415 100644 --- a/src/server/api/sync.ts +++ b/src/server/api/sync.ts @@ -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) { diff --git a/src/server/apps.ts b/src/server/apps.ts index 191386e..de40dd9 100644 --- a/src/server/apps.ts +++ b/src/server/apps.ts @@ -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() - } - }) -}