Compare commits
2 Commits
b7bda052e9
...
dd56dc0df6
| Author | SHA1 | Date | |
|---|---|---|---|
| dd56dc0df6 | |||
| 9d22008f32 |
2
TODO.txt
2
TODO.txt
|
|
@ -28,7 +28,7 @@
|
||||||
[x] `toes logs <app>`
|
[x] `toes logs <app>`
|
||||||
[x] `toes logs -f <app>`
|
[x] `toes logs -f <app>`
|
||||||
[x] `toes info <app>`
|
[x] `toes info <app>`
|
||||||
[ ] `toes new`
|
[x] `toes new`
|
||||||
[x] `toes pull`
|
[x] `toes pull`
|
||||||
[x] `toes push`
|
[x] `toes push`
|
||||||
[ ] `toes sync`
|
[ ] `toes sync`
|
||||||
|
|
|
||||||
132
src/cli/index.ts
132
src/cli/index.ts
|
|
@ -1,6 +1,7 @@
|
||||||
#!/usr/bin/env bun
|
#!/usr/bin/env bun
|
||||||
import type { App, LogLine } from '@types'
|
import type { App, LogLine } from '@types'
|
||||||
import { loadGitignore } from '@gitignore'
|
import { loadGitignore } from '@gitignore'
|
||||||
|
import { generateTemplates } from '@templates'
|
||||||
import { program } from 'commander'
|
import { program } from 'commander'
|
||||||
import { createHash } from 'crypto'
|
import { createHash } from 'crypto'
|
||||||
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, unlinkSync, writeFileSync } from 'fs'
|
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, unlinkSync, writeFileSync } from 'fs'
|
||||||
|
|
@ -156,7 +157,10 @@ function generateLocalManifest(appPath: string, appName: string): Manifest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function infoApp(name: string) {
|
async function infoApp(arg?: string) {
|
||||||
|
const name = resolveAppName(arg)
|
||||||
|
if (!name) return
|
||||||
|
|
||||||
const app: App | undefined = await get(`/api/apps/${name}`)
|
const app: App | undefined = await get(`/api/apps/${name}`)
|
||||||
if (!app) {
|
if (!app) {
|
||||||
console.error(`App not found: ${name}`)
|
console.error(`App not found: ${name}`)
|
||||||
|
|
@ -193,22 +197,31 @@ async function listApps() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const startApp = async (app: string) => {
|
async function startApp(arg?: string) {
|
||||||
await post(`/api/apps/${app}/start`)
|
const name = resolveAppName(arg)
|
||||||
|
if (!name) return
|
||||||
|
await post(`/api/apps/${name}/start`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const stopApp = async (app: string) => {
|
async function stopApp(arg?: string) {
|
||||||
await post(`/api/apps/${app}/stop`)
|
const name = resolveAppName(arg)
|
||||||
|
if (!name) return
|
||||||
|
await post(`/api/apps/${name}/stop`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const restartApp = async (app: string) => {
|
async function restartApp(arg?: string) {
|
||||||
await post(`/api/apps/${app}/restart`)
|
const name = resolveAppName(arg)
|
||||||
|
if (!name) return
|
||||||
|
await post(`/api/apps/${name}/restart`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const printLog = (line: LogLine) =>
|
const printLog = (line: LogLine) =>
|
||||||
console.log(`${new Date(line.time).toLocaleTimeString()} ${line.text}`)
|
console.log(`${new Date(line.time).toLocaleTimeString()} ${line.text}`)
|
||||||
|
|
||||||
async function logApp(name: string, options: { follow?: boolean }) {
|
async function logApp(arg: string | undefined, options: { follow?: boolean }) {
|
||||||
|
const name = resolveAppName(arg)
|
||||||
|
if (!name) return
|
||||||
|
|
||||||
if (options.follow) {
|
if (options.follow) {
|
||||||
await tailLogs(name)
|
await tailLogs(name)
|
||||||
return
|
return
|
||||||
|
|
@ -258,7 +271,10 @@ async function tailLogs(name: string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function openApp(name: string) {
|
async function openApp(arg?: string) {
|
||||||
|
const name = resolveAppName(arg)
|
||||||
|
if (!name) return
|
||||||
|
|
||||||
const app: App | undefined = await get(`/api/apps/${name}`)
|
const app: App | undefined = await get(`/api/apps/${name}`)
|
||||||
if (!app) {
|
if (!app) {
|
||||||
console.error(`App not found: ${name}`)
|
console.error(`App not found: ${name}`)
|
||||||
|
|
@ -322,6 +338,13 @@ function isApp(): boolean {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveAppName(name?: string): string | undefined {
|
||||||
|
if (name) return name
|
||||||
|
if (isApp()) return basename(process.cwd())
|
||||||
|
console.error('No app specified and current directory is not a toes app')
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
async function pushApp() {
|
async function pushApp() {
|
||||||
if (!isApp()) {
|
if (!isApp()) {
|
||||||
console.error('Not a toes app. Use `toes get <app>` to grab one.')
|
console.error('Not a toes app. Use `toes get <app>` to grab one.')
|
||||||
|
|
@ -347,8 +370,6 @@ async function pushApp() {
|
||||||
if (!ok) return
|
if (!ok) return
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`Pushing ${color.bold(appName)} to server...`)
|
|
||||||
|
|
||||||
const localFiles = new Set(Object.keys(localManifest.files))
|
const localFiles = new Set(Object.keys(localManifest.files))
|
||||||
const remoteFiles = new Set(Object.keys(result.manifest?.files ?? {}))
|
const remoteFiles = new Set(Object.keys(result.manifest?.files ?? {}))
|
||||||
|
|
||||||
|
|
@ -375,6 +396,8 @@ async function pushApp() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log(`Pushing ${color.bold(appName)} to server...`)
|
||||||
|
|
||||||
if (toUpload.length > 0) {
|
if (toUpload.length > 0) {
|
||||||
console.log(`Uploading ${toUpload.length} files...`)
|
console.log(`Uploading ${toUpload.length} files...`)
|
||||||
for (const file of toUpload) {
|
for (const file of toUpload) {
|
||||||
|
|
@ -436,77 +459,20 @@ async function newApp(name?: string) {
|
||||||
const ok = await confirm(`Create ${color.bold(appName)} in ${appPath}?`)
|
const ok = await confirm(`Create ${color.bold(appName)} in ${appPath}?`)
|
||||||
if (!ok) return
|
if (!ok) return
|
||||||
|
|
||||||
if (name) {
|
mkdirSync(join(appPath, 'src', 'pages'), { recursive: true })
|
||||||
mkdirSync(appPath, { recursive: true })
|
|
||||||
|
const templates = generateTemplates(appName)
|
||||||
|
for (const [filename, content] of Object.entries(templates)) {
|
||||||
|
writeFileSync(join(appPath, filename), content)
|
||||||
}
|
}
|
||||||
|
|
||||||
const packageJson = `{
|
|
||||||
"name": "${appName}",
|
|
||||||
"module": "index.tsx",
|
|
||||||
"type": "module",
|
|
||||||
"private": true,
|
|
||||||
"scripts": {
|
|
||||||
"toes": "bun run --watch index.tsx",
|
|
||||||
"start": "bun toes",
|
|
||||||
"dev": "bun run --hot index.tsx"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@types/bun": "latest"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"typescript": "^5.9.2"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"hype": "git+https://git.nose.space/defunkt/hype",
|
|
||||||
"forge": "git+https://git.nose.space/defunkt/forge"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
const indexTsx = `import { Hype } from 'hype'
|
|
||||||
|
|
||||||
const app = new Hype()
|
|
||||||
|
|
||||||
app.get('/', (c) => c.html(<h1>${appName}</h1>))
|
|
||||||
|
|
||||||
export default app.defaults
|
|
||||||
`
|
|
||||||
|
|
||||||
const tsconfig = `{
|
|
||||||
"compilerOptions": {
|
|
||||||
"lib": ["ESNext"],
|
|
||||||
"target": "ESNext",
|
|
||||||
"module": "Preserve",
|
|
||||||
"moduleDetection": "force",
|
|
||||||
"jsx": "react-jsx",
|
|
||||||
"jsxImportSource": "hono/jsx",
|
|
||||||
"allowJs": true,
|
|
||||||
"moduleResolution": "bundler",
|
|
||||||
"allowImportingTsExtensions": true,
|
|
||||||
"verbatimModuleSyntax": true,
|
|
||||||
"noEmit": true,
|
|
||||||
"strict": true,
|
|
||||||
"skipLibCheck": true,
|
|
||||||
"noFallthroughCasesInSwitch": true,
|
|
||||||
"noUncheckedIndexedAccess": true,
|
|
||||||
"noImplicitOverride": true,
|
|
||||||
"noUnusedLocals": false,
|
|
||||||
"noUnusedParameters": false,
|
|
||||||
"noPropertyAccessFromIndexSignature": false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
writeFileSync(join(appPath, 'package.json'), packageJson)
|
|
||||||
writeFileSync(join(appPath, 'index.tsx'), indexTsx)
|
|
||||||
writeFileSync(join(appPath, 'tsconfig.json'), tsconfig)
|
|
||||||
|
|
||||||
console.log(color.green(`✓ Created ${appName}`))
|
console.log(color.green(`✓ Created ${appName}`))
|
||||||
console.log()
|
console.log()
|
||||||
console.log('Next steps:')
|
console.log('Next steps:')
|
||||||
if (name) {
|
if (name) {
|
||||||
console.log(` cd ${name}`)
|
console.log(` cd ${name}`)
|
||||||
}
|
}
|
||||||
|
console.log(' toes push')
|
||||||
console.log(' bun install')
|
console.log(' bun install')
|
||||||
console.log(' bun dev')
|
console.log(' bun dev')
|
||||||
}
|
}
|
||||||
|
|
@ -523,8 +489,6 @@ async function pullApp() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`Pulling ${color.bold(appName)} from server...`)
|
|
||||||
|
|
||||||
const remoteManifest: Manifest | undefined = await get(`/api/sync/apps/${appName}/manifest`)
|
const remoteManifest: Manifest | undefined = await get(`/api/sync/apps/${appName}/manifest`)
|
||||||
if (!remoteManifest) {
|
if (!remoteManifest) {
|
||||||
console.error('App not found on server')
|
console.error('App not found on server')
|
||||||
|
|
@ -559,6 +523,8 @@ async function pullApp() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log(`Pulling ${color.bold(appName)} from server...`)
|
||||||
|
|
||||||
if (toDownload.length > 0) {
|
if (toDownload.length > 0) {
|
||||||
console.log(`Downloading ${toDownload.length} files...`)
|
console.log(`Downloading ${toDownload.length} files...`)
|
||||||
for (const file of toDownload) {
|
for (const file of toDownload) {
|
||||||
|
|
@ -599,7 +565,7 @@ program
|
||||||
program
|
program
|
||||||
.command('info')
|
.command('info')
|
||||||
.description('Show info for an app')
|
.description('Show info for an app')
|
||||||
.argument('<name>', 'app name')
|
.argument('[name]', 'app name (uses current directory if omitted)')
|
||||||
.action(infoApp)
|
.action(infoApp)
|
||||||
|
|
||||||
program
|
program
|
||||||
|
|
@ -610,38 +576,38 @@ program
|
||||||
program
|
program
|
||||||
.command('start')
|
.command('start')
|
||||||
.description('Start an app')
|
.description('Start an app')
|
||||||
.argument('<name>', 'app name')
|
.argument('[name]', 'app name (uses current directory if omitted)')
|
||||||
.action(startApp)
|
.action(startApp)
|
||||||
|
|
||||||
program
|
program
|
||||||
.command('stop')
|
.command('stop')
|
||||||
.description('Stop an app')
|
.description('Stop an app')
|
||||||
.argument('<name>', 'app name')
|
.argument('[name]', 'app name (uses current directory if omitted)')
|
||||||
.action(stopApp)
|
.action(stopApp)
|
||||||
|
|
||||||
program
|
program
|
||||||
.command('restart')
|
.command('restart')
|
||||||
.description('Restart an app')
|
.description('Restart an app')
|
||||||
.argument('<name>', 'app name')
|
.argument('[name]', 'app name (uses current directory if omitted)')
|
||||||
.action(restartApp)
|
.action(restartApp)
|
||||||
|
|
||||||
program
|
program
|
||||||
.command('logs')
|
.command('logs')
|
||||||
.description('Show logs for an app')
|
.description('Show logs for an app')
|
||||||
.argument('<name>', 'app name')
|
.argument('[name]', 'app name (uses current directory if omitted)')
|
||||||
.option('-f, --follow', 'follow log output')
|
.option('-f, --follow', 'follow log output')
|
||||||
.action(logApp)
|
.action(logApp)
|
||||||
|
|
||||||
program
|
program
|
||||||
.command('log', { hidden: true })
|
.command('log', { hidden: true })
|
||||||
.argument('<name>', 'app name')
|
.argument('[name]', 'app name (uses current directory if omitted)')
|
||||||
.option('-f, --follow', 'follow log output')
|
.option('-f, --follow', 'follow log output')
|
||||||
.action(logApp)
|
.action(logApp)
|
||||||
|
|
||||||
program
|
program
|
||||||
.command('open')
|
.command('open')
|
||||||
.description('Open an app in browser')
|
.description('Open an app in browser')
|
||||||
.argument('<name>', 'app name')
|
.argument('[name]', 'app name (uses current directory if omitted)')
|
||||||
.action(openApp)
|
.action(openApp)
|
||||||
|
|
||||||
program
|
program
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
import { render as renderApp } from 'hono/jsx/dom'
|
import { render as renderApp } from 'hono/jsx/dom'
|
||||||
import { define, Styles } from '@because/forge'
|
import { define, Styles } from '@because/forge'
|
||||||
import type { App, AppState } from '../shared/types'
|
import type { App, AppState } from '../shared/types'
|
||||||
|
import { generateTemplates } from '../shared/templates'
|
||||||
import { theme } from './themes'
|
import { theme } from './themes'
|
||||||
import { Modal, initModal } from './tags/modal'
|
import { closeModal, initModal, Modal, openModal, rerenderModal } from './tags/modal'
|
||||||
import { initUpdate } from './update'
|
import { initUpdate } from './update'
|
||||||
import { openEmojiPicker } from './tags/emoji-picker'
|
import { openEmojiPicker } from './tags/emoji-picker'
|
||||||
|
|
||||||
|
|
@ -346,6 +347,143 @@ const stateLabels: Record<AppState, string> = {
|
||||||
stopping: 'Stopping',
|
stopping: 'Stopping',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Form styles for modal
|
||||||
|
const Form = define('Form', {
|
||||||
|
base: 'form',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: 16,
|
||||||
|
})
|
||||||
|
|
||||||
|
const FormField = define('FormField', {
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: 6,
|
||||||
|
})
|
||||||
|
|
||||||
|
const FormLabel = define('FormLabel', {
|
||||||
|
base: 'label',
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: 500,
|
||||||
|
color: theme('colors-text'),
|
||||||
|
})
|
||||||
|
|
||||||
|
const FormInput = define('FormInput', {
|
||||||
|
base: 'input',
|
||||||
|
padding: '8px 12px',
|
||||||
|
background: theme('colors-bgSubtle'),
|
||||||
|
border: `1px solid ${theme('colors-border')}`,
|
||||||
|
borderRadius: theme('radius-md'),
|
||||||
|
color: theme('colors-text'),
|
||||||
|
fontSize: 14,
|
||||||
|
selectors: {
|
||||||
|
'&:focus': {
|
||||||
|
outline: 'none',
|
||||||
|
borderColor: theme('colors-primary'),
|
||||||
|
},
|
||||||
|
'&::placeholder': {
|
||||||
|
color: theme('colors-textFaint'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const FormError = define('FormError', {
|
||||||
|
fontSize: 13,
|
||||||
|
color: theme('colors-error'),
|
||||||
|
})
|
||||||
|
|
||||||
|
const FormActions = define('FormActions', {
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'flex-end',
|
||||||
|
gap: 8,
|
||||||
|
marginTop: 8,
|
||||||
|
})
|
||||||
|
|
||||||
|
// New App creation
|
||||||
|
let newAppError = ''
|
||||||
|
let newAppCreating = false
|
||||||
|
|
||||||
|
async function createNewApp(input: HTMLInputElement) {
|
||||||
|
const name = input.value.trim().toLowerCase().replace(/\s+/g, '-')
|
||||||
|
|
||||||
|
if (!name) {
|
||||||
|
newAppError = 'App name is required'
|
||||||
|
rerenderModal()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!/^[a-z][a-z0-9-]*$/.test(name)) {
|
||||||
|
newAppError = 'Name must start with a letter and contain only lowercase letters, numbers, and hyphens'
|
||||||
|
rerenderModal()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (apps.some(a => a.name === name)) {
|
||||||
|
newAppError = 'An app with this name already exists'
|
||||||
|
rerenderModal()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
newAppCreating = true
|
||||||
|
newAppError = ''
|
||||||
|
rerenderModal()
|
||||||
|
|
||||||
|
try {
|
||||||
|
const templates = generateTemplates(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}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Success - close modal and select the new app
|
||||||
|
selectedApp = name
|
||||||
|
localStorage.setItem('selectedApp', name)
|
||||||
|
closeModal()
|
||||||
|
} catch (err) {
|
||||||
|
newAppError = err instanceof Error ? err.message : 'Failed to create app'
|
||||||
|
newAppCreating = false
|
||||||
|
rerenderModal()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openNewAppModal() {
|
||||||
|
newAppError = ''
|
||||||
|
newAppCreating = false
|
||||||
|
|
||||||
|
openModal('New App', () => (
|
||||||
|
<Form onSubmit={(e: Event) => {
|
||||||
|
e.preventDefault()
|
||||||
|
const input = (e.target as HTMLFormElement).querySelector('input') as HTMLInputElement
|
||||||
|
createNewApp(input)
|
||||||
|
}}>
|
||||||
|
<FormField>
|
||||||
|
<FormLabel for="app-name">App Name</FormLabel>
|
||||||
|
<FormInput
|
||||||
|
id="app-name"
|
||||||
|
type="text"
|
||||||
|
placeholder="my-app"
|
||||||
|
autofocus
|
||||||
|
/>
|
||||||
|
{newAppError && <FormError>{newAppError}</FormError>}
|
||||||
|
</FormField>
|
||||||
|
<FormActions>
|
||||||
|
<Button type="button" onClick={closeModal} disabled={newAppCreating}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" variant="primary" disabled={newAppCreating}>
|
||||||
|
{newAppCreating ? 'Creating...' : 'Create App'}
|
||||||
|
</Button>
|
||||||
|
</FormActions>
|
||||||
|
</Form>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
// Actions - call API then let SSE update the state
|
// Actions - call API then let SSE update the state
|
||||||
const startApp = (name: string) => fetch(`/api/apps/${name}/start`, { method: 'POST' })
|
const startApp = (name: string) => fetch(`/api/apps/${name}/start`, { method: 'POST' })
|
||||||
const stopApp = (name: string) => fetch(`/api/apps/${name}/stop`, { method: 'POST' })
|
const stopApp = (name: string) => fetch(`/api/apps/${name}/stop`, { method: 'POST' })
|
||||||
|
|
@ -522,7 +660,7 @@ const Dashboard = () => {
|
||||||
</AppList>
|
</AppList>
|
||||||
{!sidebarCollapsed && (
|
{!sidebarCollapsed && (
|
||||||
<SidebarFooter>
|
<SidebarFooter>
|
||||||
<NewAppButton>+ New App</NewAppButton>
|
<NewAppButton onClick={openNewAppModal}>+ New App</NewAppButton>
|
||||||
</SidebarFooter>
|
</SidebarFooter>
|
||||||
)}
|
)}
|
||||||
</Sidebar>
|
</Sidebar>
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,12 @@
|
||||||
import type { App as SharedApp, AppState, LogLine } from '@types'
|
import type { App as SharedApp, AppState, LogLine } from '@types'
|
||||||
import type { Subprocess } from 'bun'
|
import type { Subprocess } from 'bun'
|
||||||
|
import { DEFAULT_EMOJI } from '@types'
|
||||||
import { existsSync, readdirSync, readFileSync, statSync, watch, writeFileSync } from 'fs'
|
import { existsSync, readdirSync, readFileSync, statSync, watch, writeFileSync } from 'fs'
|
||||||
import { join } from 'path'
|
import { join } from 'path'
|
||||||
|
|
||||||
export type { AppState } from '@types'
|
export type { AppState } from '@types'
|
||||||
|
|
||||||
export const APPS_DIR = join(process.env.DATA_DIR ?? '.', 'apps')
|
export const APPS_DIR = join(process.env.DATA_DIR ?? '.', 'apps')
|
||||||
|
|
||||||
const DEFAULT_EMOJI = '🖥️'
|
|
||||||
const MAX_LOGS = 100
|
const MAX_LOGS = 100
|
||||||
const _apps = new Map<string, App>()
|
const _apps = new Map<string, App>()
|
||||||
const _listeners = new Set<() => void>()
|
const _listeners = new Set<() => void>()
|
||||||
|
|
|
||||||
69
src/shared/templates.ts
Normal file
69
src/shared/templates.ts
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
import { DEFAULT_EMOJI } from './types'
|
||||||
|
|
||||||
|
export interface AppTemplates {
|
||||||
|
'index.tsx': string
|
||||||
|
'package.json': string
|
||||||
|
'tsconfig.json': string
|
||||||
|
'.npmrc': string
|
||||||
|
'src/pages/index.tsx': string
|
||||||
|
}
|
||||||
|
|
||||||
|
const tsconfig = {
|
||||||
|
compilerOptions: {
|
||||||
|
lib: ['ESNext'],
|
||||||
|
target: 'ESNext',
|
||||||
|
module: 'Preserve',
|
||||||
|
moduleDetection: 'force',
|
||||||
|
jsx: 'react-jsx',
|
||||||
|
jsxImportSource: 'hono/jsx',
|
||||||
|
allowJs: true,
|
||||||
|
moduleResolution: 'bundler',
|
||||||
|
allowImportingTsExtensions: true,
|
||||||
|
verbatimModuleSyntax: true,
|
||||||
|
noEmit: true,
|
||||||
|
strict: true,
|
||||||
|
skipLibCheck: true,
|
||||||
|
noFallthroughCasesInSwitch: true,
|
||||||
|
noUncheckedIndexedAccess: true,
|
||||||
|
noImplicitOverride: true,
|
||||||
|
noUnusedLocals: false,
|
||||||
|
noUnusedParameters: false,
|
||||||
|
noPropertyAccessFromIndexSignature: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export function generateTemplates(appName: string): AppTemplates {
|
||||||
|
const packageJson = {
|
||||||
|
name: appName,
|
||||||
|
module: 'index.tsx',
|
||||||
|
type: 'module',
|
||||||
|
private: true,
|
||||||
|
scripts: {
|
||||||
|
toes: 'bun run --watch index.tsx',
|
||||||
|
start: 'bun toes',
|
||||||
|
dev: 'bun run --hot index.tsx',
|
||||||
|
},
|
||||||
|
toes: {
|
||||||
|
icon: DEFAULT_EMOJI,
|
||||||
|
},
|
||||||
|
devDependencies: {
|
||||||
|
'@types/bun': 'latest',
|
||||||
|
},
|
||||||
|
peerDependencies: {
|
||||||
|
typescript: '^5.9.2',
|
||||||
|
},
|
||||||
|
dependencies: {
|
||||||
|
'@because/hype': '*',
|
||||||
|
'@because/forge': '*',
|
||||||
|
'@because/howl': '*',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
'.npmrc': 'registry=https://npm.nose.space',
|
||||||
|
'package.json': JSON.stringify(packageJson, null, 2) + '\n',
|
||||||
|
'src/pages/index.tsx': `export default () => <h1>${appName}</h1>`,
|
||||||
|
'index.tsx': `import { Hype } from '@because/hype'\nconst app = new Hype\nexport default app.defaults`,
|
||||||
|
'tsconfig.json': JSON.stringify(tsconfig, null, 2) + '\n',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
export const DEFAULT_EMOJI = '🖥️'
|
||||||
|
|
||||||
export type AppState = 'invalid' | 'stopped' | 'starting' | 'running' | 'stopping'
|
export type AppState = 'invalid' | 'stopped' | 'starting' | 'running' | 'stopping'
|
||||||
|
|
||||||
export type LogLine = {
|
export type LogLine = {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user