267 lines
7.3 KiB
TypeScript
267 lines
7.3 KiB
TypeScript
import type { App } from '@types'
|
|
import { generateTemplates, type TemplateType } from '%templates'
|
|
import color from 'kleur'
|
|
import { existsSync, mkdirSync, writeFileSync } from 'fs'
|
|
import { basename, join } from 'path'
|
|
import { del, get, getManifest, HOST, post } from '../http'
|
|
import { confirm, prompt } from '../prompts'
|
|
import { resolveAppName } from '../name'
|
|
import { pushApp } from './sync'
|
|
|
|
export const STATE_ICONS: Record<string, string> = {
|
|
running: color.green('●'),
|
|
starting: color.yellow('◎'),
|
|
stopped: color.gray('◯'),
|
|
invalid: color.red('◌'),
|
|
}
|
|
|
|
export async function configShow() {
|
|
console.log(`Host: ${color.bold(HOST)}`)
|
|
|
|
const source = process.env.TOES_URL
|
|
? 'TOES_URL'
|
|
: process.env.TOES_HOST
|
|
? 'TOES_HOST' + (process.env.PORT ? ' + PORT' : '')
|
|
: process.env.NODE_ENV === 'production'
|
|
? 'default (production)'
|
|
: 'default (development)'
|
|
|
|
console.log(`Source: ${color.gray(source)}`)
|
|
|
|
if (process.env.TOES_URL) {
|
|
console.log(` TOES_URL=${process.env.TOES_URL}`)
|
|
}
|
|
if (process.env.TOES_HOST) {
|
|
console.log(` TOES_HOST=${process.env.TOES_HOST}`)
|
|
}
|
|
if (process.env.PORT) {
|
|
console.log(` PORT=${process.env.PORT}`)
|
|
}
|
|
if (process.env.NODE_ENV) {
|
|
console.log(` NODE_ENV=${process.env.NODE_ENV}`)
|
|
}
|
|
}
|
|
|
|
export async function infoApp(arg?: string) {
|
|
const name = resolveAppName(arg)
|
|
if (!name) return
|
|
|
|
const app: App | undefined = await get(`/api/apps/${name}`)
|
|
if (!app) {
|
|
console.error(`App not found: ${name}`)
|
|
return
|
|
}
|
|
|
|
const icon = STATE_ICONS[app.state] ?? '◯'
|
|
console.log(`${icon} ${color.bold(app.name)} ${app.tool ? '[tool]' : ''}`)
|
|
console.log(` State: ${app.state}`)
|
|
if (app.port) {
|
|
console.log(` Port: ${app.port}`)
|
|
console.log(` URL: http://localhost:${app.port}`)
|
|
}
|
|
if (app.started) {
|
|
const uptime = Date.now() - app.started
|
|
const seconds = Math.floor(uptime / 1000) % 60
|
|
const minutes = Math.floor(uptime / 60000) % 60
|
|
const hours = Math.floor(uptime / 3600000)
|
|
const parts = []
|
|
if (hours) parts.push(`${hours}h`)
|
|
if (minutes) parts.push(`${minutes}m`)
|
|
parts.push(`${seconds}s`)
|
|
console.log(` Uptime: ${parts.join(' ')}`)
|
|
}
|
|
if (app.error) console.log(` Error: ${color.red(app.error)}`)
|
|
}
|
|
|
|
interface ListAppsOptions {
|
|
apps?: boolean
|
|
tools?: boolean
|
|
}
|
|
|
|
export async function listApps(options: ListAppsOptions) {
|
|
const allApps: App[] | undefined = await get('/api/apps')
|
|
if (!allApps) return
|
|
|
|
if (options.apps || options.tools) {
|
|
const filtered = allApps.filter((app) => {
|
|
if (options.tools) return app.tool
|
|
return !app.tool
|
|
})
|
|
|
|
for (const app of filtered) {
|
|
console.log(`${STATE_ICONS[app.state] ?? '◯'} ${app.name}`)
|
|
}
|
|
} else {
|
|
const apps = allApps.filter((app) => !app.tool)
|
|
const tools = allApps.filter((app) => app.tool)
|
|
|
|
if (tools.length === 0) {
|
|
// No tools, just list apps without header/indent
|
|
for (const app of apps) {
|
|
console.log(`${STATE_ICONS[app.state] ?? '◯'} ${app.name}`)
|
|
}
|
|
} else {
|
|
if (apps.length > 0) {
|
|
console.log('apps:')
|
|
for (const app of apps) {
|
|
console.log(` ${STATE_ICONS[app.state] ?? '◯'} ${app.name}`)
|
|
}
|
|
console.log()
|
|
}
|
|
console.log('tools:')
|
|
for (const tool of tools) {
|
|
console.log(` ${STATE_ICONS[tool.state] ?? '◯'} ${tool.name}`)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
interface NewAppOptions {
|
|
ssr?: boolean
|
|
bare?: boolean
|
|
spa?: boolean
|
|
}
|
|
|
|
export async function newApp(name: string | undefined, options: NewAppOptions) {
|
|
const appPath = name ? join(process.cwd(), name) : process.cwd()
|
|
const appName = name ?? basename(process.cwd())
|
|
|
|
// Determine template type from flags
|
|
let template: TemplateType = 'ssr'
|
|
if (options.bare) template = 'bare'
|
|
else if (options.spa) template = 'spa'
|
|
|
|
if (name && existsSync(appPath)) {
|
|
console.error(`Directory already exists: ${name}`)
|
|
return
|
|
}
|
|
|
|
const filesToCheck = ['index.tsx', 'package.json', 'tsconfig.json']
|
|
const existing = filesToCheck.filter((f) => existsSync(join(appPath, f)))
|
|
if (existing.length > 0) {
|
|
console.error(`Files already exist: ${existing.join(', ')}`)
|
|
return
|
|
}
|
|
|
|
const templateLabel = template === 'ssr' ? '' : ` (${template})`
|
|
const ok = await confirm(`Create ${color.bold(appName)}${templateLabel} in ${appPath}?`)
|
|
if (!ok) return
|
|
|
|
const templates = generateTemplates(appName, template)
|
|
|
|
// Create directories for all template files
|
|
for (const filename of Object.keys(templates)) {
|
|
const dir = join(appPath, filename, '..')
|
|
mkdirSync(dir, { recursive: true })
|
|
}
|
|
|
|
for (const [filename, content] of Object.entries(templates)) {
|
|
writeFileSync(join(appPath, filename), content)
|
|
}
|
|
|
|
process.chdir(appPath)
|
|
await pushApp()
|
|
|
|
console.log(color.green(`✓ Created ${appName}`))
|
|
console.log()
|
|
console.log('Next steps:')
|
|
if (name) {
|
|
console.log(` cd ${name}`)
|
|
}
|
|
console.log(' bun install')
|
|
console.log(' bun dev')
|
|
}
|
|
|
|
export async function openApp(arg?: string) {
|
|
const name = resolveAppName(arg)
|
|
if (!name) return
|
|
|
|
const app: App | undefined = await get(`/api/apps/${name}`)
|
|
if (!app) {
|
|
console.error(`App not found: ${name}`)
|
|
return
|
|
}
|
|
if (app.state !== 'running') {
|
|
console.error(`App is not running: ${name}`)
|
|
return
|
|
}
|
|
const url = `http://localhost:${app.port}`
|
|
console.log(`Opening ${url}`)
|
|
Bun.spawn(['open', url])
|
|
}
|
|
|
|
export async function renameApp(arg: string | undefined, newName: string) {
|
|
const name = resolveAppName(arg)
|
|
if (!name) return
|
|
|
|
const result = await getManifest(name)
|
|
if (result === null) return
|
|
if (!result.exists) {
|
|
console.error(`App not found on server: ${name}`)
|
|
return
|
|
}
|
|
|
|
const expected = `sudo rename ${name} ${newName}`
|
|
console.log(`This will rename ${color.bold(name)} to ${color.bold(newName)}.`)
|
|
const answer = await prompt(`Type "${expected}" to confirm: `)
|
|
|
|
if (answer !== expected) {
|
|
console.log('Aborted.')
|
|
return
|
|
}
|
|
|
|
const response = await post<{ ok: boolean, error?: string, name?: string }>(`/api/apps/${name}/rename`, { name: newName })
|
|
if (!response) return
|
|
|
|
if (!response.ok) {
|
|
console.error(color.red(`Error: ${response.error}`))
|
|
return
|
|
}
|
|
|
|
console.log(color.green(`✓ Renamed ${name} to ${response.name}`))
|
|
}
|
|
|
|
export async function restartApp(arg?: string) {
|
|
const name = resolveAppName(arg)
|
|
if (!name) return
|
|
await post(`/api/apps/${name}/restart`)
|
|
}
|
|
|
|
export async function rmApp(arg?: string) {
|
|
const name = resolveAppName(arg)
|
|
if (!name) return
|
|
|
|
const result = await getManifest(name)
|
|
if (result === null) return
|
|
if (!result.exists) {
|
|
console.error(`App not found on server: ${name}`)
|
|
return
|
|
}
|
|
|
|
const expected = `sudo rm ${name}`
|
|
console.log(`This will ${color.red('permanently delete')} ${color.bold(name)} from the server.`)
|
|
const answer = await prompt(`Type "${expected}" to confirm: `)
|
|
|
|
if (answer !== expected) {
|
|
console.log('Aborted.')
|
|
return
|
|
}
|
|
|
|
const success = await del(`/api/sync/apps/${name}`)
|
|
if (success) {
|
|
console.log(color.green(`✓ Removed ${name}`))
|
|
}
|
|
}
|
|
|
|
export async function startApp(arg?: string) {
|
|
const name = resolveAppName(arg)
|
|
if (!name) return
|
|
await post(`/api/apps/${name}/start`)
|
|
}
|
|
|
|
export async function stopApp(arg?: string) {
|
|
const name = resolveAppName(arg)
|
|
if (!name) return
|
|
await post(`/api/apps/${name}/stop`)
|
|
}
|