fix toes new templates
This commit is contained in:
parent
664861a0fb
commit
557ea669fb
|
|
@ -35,7 +35,14 @@ Personal web appliance that auto-discovers and runs multiple web apps on your ho
|
|||
- `commands/logs.ts` - log viewing with tail support
|
||||
|
||||
### Shared (`src/shared/`)
|
||||
- Code shared between frontend (browser) and backend (server)
|
||||
- `types.ts` - App, AppState, Manifest interfaces
|
||||
- IMPORTANT: Cannot use filesystem or Node APIs (runs in browser)
|
||||
|
||||
### Lib (`src/lib/`)
|
||||
- Code shared between CLI and server (server-side only)
|
||||
- `templates.ts` - Template generation for new apps
|
||||
- Can use filesystem and Node APIs (never runs in browser)
|
||||
|
||||
### Other
|
||||
- `apps/*/package.json` - Must have `"toes": "bun run --watch index.tsx"` script
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import type { App } from '@types'
|
||||
import { generateTemplates, type TemplateType } from '@templates'
|
||||
import { generateTemplates, type TemplateType } from '%templates'
|
||||
import color from 'kleur'
|
||||
import { existsSync, mkdirSync, writeFileSync } from 'fs'
|
||||
import { basename, join } from 'path'
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import { generateTemplates } from '../../shared/templates'
|
||||
import { closeModal, openModal, rerenderModal } from '../components/modal'
|
||||
import { apps, setSelectedApp } from '../state'
|
||||
import { Button, Form, FormActions, FormError, FormField, FormInput, FormLabel } from '../styles'
|
||||
|
|
@ -32,16 +31,16 @@ async function createNewApp(input: HTMLInputElement) {
|
|||
rerenderModal()
|
||||
|
||||
try {
|
||||
const templates = generateTemplates(name)
|
||||
const res = await fetch('/api/apps', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name }),
|
||||
})
|
||||
|
||||
for (const [filename, content] of Object.entries(templates)) {
|
||||
const res = await fetch(`/api/sync/apps/${name}/files/${filename}`, {
|
||||
method: 'PUT',
|
||||
body: content,
|
||||
})
|
||||
if (!res.ok) {
|
||||
throw new Error(`Failed to create ${filename}`)
|
||||
}
|
||||
const data = await res.json()
|
||||
|
||||
if (!res.ok || !data.ok) {
|
||||
throw new Error(data.error || 'Failed to create app')
|
||||
}
|
||||
|
||||
// Success - close modal and select the new app
|
||||
|
|
|
|||
|
|
@ -1,17 +1,17 @@
|
|||
import { DEFAULT_EMOJI } from '@types'
|
||||
import { readdirSync, readFileSync, statSync } from 'fs'
|
||||
import { join, relative } from 'path'
|
||||
import { DEFAULT_EMOJI } from './types'
|
||||
|
||||
export type TemplateType = 'ssr' | 'bare' | 'spa'
|
||||
|
||||
export type AppTemplates = Record<string, string>
|
||||
|
||||
interface TemplateVars {
|
||||
APP_NAME: string
|
||||
APP_EMOJI: string
|
||||
APP_NAME: string
|
||||
}
|
||||
|
||||
const TEMPLATES_DIR = join(import.meta.dirname, '../../templates')
|
||||
const TEMPLATES_DIR = join(import.meta.dir, '../../templates')
|
||||
|
||||
function readDir(dir: string): string[] {
|
||||
const files: string[] = []
|
||||
|
|
@ -34,8 +34,8 @@ function replaceVars(content: string, vars: TemplateVars): string {
|
|||
|
||||
export function generateTemplates(appName: string, template: TemplateType = 'ssr'): AppTemplates {
|
||||
const vars: TemplateVars = {
|
||||
APP_NAME: appName,
|
||||
APP_EMOJI: DEFAULT_EMOJI,
|
||||
APP_NAME: appName,
|
||||
}
|
||||
|
||||
const result: AppTemplates = {}
|
||||
|
|
@ -1,7 +1,10 @@
|
|||
import { allApps, onChange, renameApp, startApp, stopApp, updateAppIcon } from '$apps'
|
||||
import { APPS_DIR, allApps, onChange, 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'
|
||||
import { Hype } from '@because/hype'
|
||||
import { existsSync, mkdirSync, writeFileSync } from 'fs'
|
||||
import { dirname, join } from 'path'
|
||||
|
||||
const router = Hype.router()
|
||||
|
||||
|
|
@ -55,6 +58,42 @@ router.get('/:app/logs', c => {
|
|||
return c.json(app.logs ?? [])
|
||||
})
|
||||
|
||||
router.post('/', async c => {
|
||||
let body: { name?: string, template?: TemplateType }
|
||||
try {
|
||||
body = await c.req.json()
|
||||
} catch {
|
||||
return c.json({ ok: false, error: 'Invalid JSON body' }, 400)
|
||||
}
|
||||
|
||||
const name = body.name?.trim().toLowerCase().replace(/\s+/g, '-')
|
||||
if (!name) return c.json({ ok: false, error: 'App name is required' }, 400)
|
||||
|
||||
if (!/^[a-z][a-z0-9-]*$/.test(name)) {
|
||||
return c.json({ ok: false, error: 'Name must start with a letter and contain only lowercase letters, numbers, and hyphens' }, 400)
|
||||
}
|
||||
|
||||
const appPath = join(APPS_DIR, name)
|
||||
if (existsSync(appPath)) {
|
||||
return c.json({ ok: false, error: 'An app with this name already exists' }, 400)
|
||||
}
|
||||
|
||||
const template = body.template ?? 'ssr'
|
||||
const templates = generateTemplates(name, template)
|
||||
|
||||
// Create directories and write files
|
||||
for (const [filename, content] of Object.entries(templates)) {
|
||||
const fullPath = join(appPath, filename)
|
||||
const dir = dirname(fullPath)
|
||||
if (!existsSync(dir)) {
|
||||
mkdirSync(dir, { recursive: true })
|
||||
}
|
||||
writeFileSync(fullPath, content)
|
||||
}
|
||||
|
||||
return c.json({ ok: true, name })
|
||||
})
|
||||
|
||||
router.sse('/:app/logs/stream', (send, c) => {
|
||||
const appName = c.req.param('app')
|
||||
const targetApp = allApps().find(a => a.name === appName)
|
||||
|
|
|
|||
|
|
@ -2,11 +2,11 @@ import type { App as SharedApp, AppState } from '@types'
|
|||
import type { Subprocess } from 'bun'
|
||||
import { DEFAULT_EMOJI } from '@types'
|
||||
import { existsSync, readdirSync, readFileSync, renameSync, statSync, watch, writeFileSync } from 'fs'
|
||||
import { join } from 'path'
|
||||
import { join, resolve } from 'path'
|
||||
|
||||
export type { AppState } from '@types'
|
||||
|
||||
export const APPS_DIR = join(process.env.DATA_DIR ?? '.', 'apps')
|
||||
export const APPS_DIR = process.env.APPS_DIR ?? resolve(join(process.env.DATA_DIR ?? '.', 'apps'))
|
||||
|
||||
const HEALTH_CHECK_FAILURES_BEFORE_RESTART = 3
|
||||
const HEALTH_CHECK_INTERVAL = 30000
|
||||
|
|
@ -402,7 +402,7 @@ async function runApp(dir: string, port: number) {
|
|||
|
||||
const proc = Bun.spawn(['bun', 'run', 'toes'], {
|
||||
cwd,
|
||||
env: { ...process.env, PORT: String(port), NO_AUTOPORT: 'true' },
|
||||
env: { ...process.env, PORT: String(port), NO_AUTOPORT: 'true', APPS_DIR },
|
||||
stdout: 'pipe',
|
||||
stderr: 'pipe',
|
||||
})
|
||||
|
|
|
|||
|
|
@ -5,6 +5,18 @@ import { Hype } from '@because/hype'
|
|||
|
||||
const app = new Hype({ layout: false })
|
||||
|
||||
// Serve pre-bundled client in production
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
app.get('/client/index.js', async (c) => {
|
||||
const file = Bun.file('./dist/client/index.js')
|
||||
if (!(await file.exists())) {
|
||||
console.error('⚠️ Bundled client not found. Run `bun run build` first.')
|
||||
return c.text('Client bundle not found', 500)
|
||||
}
|
||||
return new Response(file, { headers: { 'Content-Type': 'text/javascript' } })
|
||||
})
|
||||
}
|
||||
|
||||
app.route('/api/apps', appsRouter)
|
||||
app.route('/api/sync', syncRouter)
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user