This commit is contained in:
Chris Wanstrath 2026-01-27 14:40:43 -08:00
parent 7e0cde9ef5
commit 1ec012338f
16 changed files with 455 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
node_modules/

25
TODO.txt Normal file
View File

@ -0,0 +1,25 @@
# toes
## server
[x] start toes server
[x] scans for apps/**/package.json, scripts.toes
[x] runs that for each, giving it a PORT
[x] has GET / page that shows all the running apps/status/port
[ ] watch each app and restart it on update
## cli
[ ] `toes --help`
[ ] `toes --version`
[ ] `toes list`
[ ] `toes new`
[ ] `toes pull`
[ ] `toes push`
[ ] `toes sync`
## webui
[ ] list projects
[ ] todo.txt
[ ] ...

37
apps/basic/bun.lock Normal file
View File

@ -0,0 +1,37 @@
{
"lockfileVersion": 1,
"workspaces": {
"": {
"name": "toes-app",
"dependencies": {
"forge": "git+https://git.nose.space/defunkt/forge",
"hype": "git+https://git.nose.space/defunkt/hype",
},
"devDependencies": {
"@types/bun": "latest",
},
"peerDependencies": {
"typescript": "^5.9.2",
},
},
},
"packages": {
"@types/bun": ["@types/bun@1.3.6", "", { "dependencies": { "bun-types": "1.3.6" } }, "sha512-uWCv6FO/8LcpREhenN1d1b6fcspAB+cefwD7uti8C8VffIv0Um08TKMn98FynpTiU38+y2dUO55T11NgDt8VAA=="],
"@types/node": ["@types/node@25.0.10", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-zWW5KPngR/yvakJgGOmZ5vTBemDoSqF3AcV/LrO5u5wTWyEAVVh+IT39G4gtyAkh3CtTZs8aX/yRM82OfzHJRg=="],
"bun-types": ["bun-types@1.3.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-OlFwHcnNV99r//9v5IIOgQ9Uk37gZqrNMCcqEaExdkVq3Avwqok1bJFmvGMCkCE0FqzdY8VMOZpfpR3lwI+CsQ=="],
"forge": ["forge@git+https://git.nose.space/defunkt/forge#9b6e1e91ec77d7e03589cac256d97fb9cd942184", { "peerDependencies": { "typescript": "^5" } }, "9b6e1e91ec77d7e03589cac256d97fb9cd942184"],
"hono": ["hono@4.11.7", "", {}, "sha512-l7qMiNee7t82bH3SeyUCt9UF15EVmaBvsppY2zQtrbIhl/yzBTny+YUxsVjSjQ6gaqaeVtZmGocom8TzBlA4Yw=="],
"hype": ["hype@git+https://git.nose.space/defunkt/hype#52086f4eb94cc36ecd9470d9a101ff01002687c8", { "dependencies": { "hono": "^4.10.4", "kleur": "^4.1.5" }, "peerDependencies": { "typescript": "^5" } }, "52086f4eb94cc36ecd9470d9a101ff01002687c8"],
"kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="],
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
}
}

10
apps/basic/index.tsx Normal file
View File

@ -0,0 +1,10 @@
import { Hype } from 'hype'
const app = new Hype
app.get('/', c => c.html(<h1>Hi there!</h1>))
const apps = () => {
}
export default app.defaults

21
apps/basic/package.json Normal file
View File

@ -0,0 +1,21 @@
{
"name": "toes-app",
"module": "src/index.ts",
"type": "module",
"private": true,
"scripts": {
"toes": "bun start",
"start": "bun run index.tsx",
"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"
}
}

30
apps/basic/tsconfig.json Normal file
View File

@ -0,0 +1,30 @@
{
"compilerOptions": {
// Environment setup & latest features
"lib": ["ESNext"],
"target": "ESNext",
"module": "Preserve",
"moduleDetection": "force",
"jsx": "react-jsx",
"jsxImportSource": "hono/jsx",
"allowJs": true,
// Bundler mode
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"noEmit": true,
// Best practices
"strict": true,
"skipLibCheck": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
// Some stricter flags (disabled by default)
"noUnusedLocals": false,
"noUnusedParameters": false,
"noPropertyAccessFromIndexSignature": false
}
}

1
apps/file.txt Normal file
View File

@ -0,0 +1 @@
hi

37
apps/profile/bun.lock Normal file
View File

@ -0,0 +1,37 @@
{
"lockfileVersion": 1,
"workspaces": {
"": {
"name": "toes-app",
"dependencies": {
"forge": "git+https://git.nose.space/defunkt/forge",
"hype": "git+https://git.nose.space/defunkt/hype",
},
"devDependencies": {
"@types/bun": "latest",
},
"peerDependencies": {
"typescript": "^5.9.2",
},
},
},
"packages": {
"@types/bun": ["@types/bun@1.3.6", "", { "dependencies": { "bun-types": "1.3.6" } }, "sha512-uWCv6FO/8LcpREhenN1d1b6fcspAB+cefwD7uti8C8VffIv0Um08TKMn98FynpTiU38+y2dUO55T11NgDt8VAA=="],
"@types/node": ["@types/node@25.0.10", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-zWW5KPngR/yvakJgGOmZ5vTBemDoSqF3AcV/LrO5u5wTWyEAVVh+IT39G4gtyAkh3CtTZs8aX/yRM82OfzHJRg=="],
"bun-types": ["bun-types@1.3.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-OlFwHcnNV99r//9v5IIOgQ9Uk37gZqrNMCcqEaExdkVq3Avwqok1bJFmvGMCkCE0FqzdY8VMOZpfpR3lwI+CsQ=="],
"forge": ["forge@git+https://git.nose.space/defunkt/forge#9b6e1e91ec77d7e03589cac256d97fb9cd942184", { "peerDependencies": { "typescript": "^5" } }, "9b6e1e91ec77d7e03589cac256d97fb9cd942184"],
"hono": ["hono@4.11.7", "", {}, "sha512-l7qMiNee7t82bH3SeyUCt9UF15EVmaBvsppY2zQtrbIhl/yzBTny+YUxsVjSjQ6gaqaeVtZmGocom8TzBlA4Yw=="],
"hype": ["hype@git+https://git.nose.space/defunkt/hype#52086f4eb94cc36ecd9470d9a101ff01002687c8", { "dependencies": { "hono": "^4.10.4", "kleur": "^4.1.5" }, "peerDependencies": { "typescript": "^5" } }, "52086f4eb94cc36ecd9470d9a101ff01002687c8"],
"kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="],
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
}
}

10
apps/profile/index.tsx Normal file
View File

@ -0,0 +1,10 @@
import { Hype } from 'hype'
const app = new Hype
app.get('/', c => c.html(<h1>Hi there!</h1>))
const apps = () => {
}
export default app.defaults

21
apps/profile/package.json Normal file
View File

@ -0,0 +1,21 @@
{
"name": "toes-app",
"module": "src/index.ts",
"type": "module",
"private": true,
"scripts": {
"toes": "bun start",
"start": "bun run index.tsx",
"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"
}
}

View File

@ -0,0 +1,30 @@
{
"compilerOptions": {
// Environment setup & latest features
"lib": ["ESNext"],
"target": "ESNext",
"module": "Preserve",
"moduleDetection": "force",
"jsx": "react-jsx",
"jsxImportSource": "hono/jsx",
"allowJs": true,
// Bundler mode
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"noEmit": true,
// Best practices
"strict": true,
"skipLibCheck": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
// Some stricter flags (disabled by default)
"noUnusedLocals": false,
"noUnusedParameters": false,
"noPropertyAccessFromIndexSignature": false
}
}

1
apps/risk/package.json Normal file
View File

@ -0,0 +1 @@
{}

37
bun.lock Normal file
View File

@ -0,0 +1,37 @@
{
"lockfileVersion": 1,
"workspaces": {
"": {
"name": "toes",
"dependencies": {
"forge": "git+https://git.nose.space/defunkt/forge",
"hype": "git+https://git.nose.space/defunkt/hype",
},
"devDependencies": {
"@types/bun": "latest",
},
"peerDependencies": {
"typescript": "^5.9.2",
},
},
},
"packages": {
"@types/bun": ["@types/bun@1.3.6", "", { "dependencies": { "bun-types": "1.3.6" } }, "sha512-uWCv6FO/8LcpREhenN1d1b6fcspAB+cefwD7uti8C8VffIv0Um08TKMn98FynpTiU38+y2dUO55T11NgDt8VAA=="],
"@types/node": ["@types/node@25.0.10", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-zWW5KPngR/yvakJgGOmZ5vTBemDoSqF3AcV/LrO5u5wTWyEAVVh+IT39G4gtyAkh3CtTZs8aX/yRM82OfzHJRg=="],
"bun-types": ["bun-types@1.3.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-OlFwHcnNV99r//9v5IIOgQ9Uk37gZqrNMCcqEaExdkVq3Avwqok1bJFmvGMCkCE0FqzdY8VMOZpfpR3lwI+CsQ=="],
"forge": ["forge@git+https://git.nose.space/defunkt/forge#9b6e1e91ec77d7e03589cac256d97fb9cd942184", { "peerDependencies": { "typescript": "^5" } }, "9b6e1e91ec77d7e03589cac256d97fb9cd942184"],
"hono": ["hono@4.11.7", "", {}, "sha512-l7qMiNee7t82bH3SeyUCt9UF15EVmaBvsppY2zQtrbIhl/yzBTny+YUxsVjSjQ6gaqaeVtZmGocom8TzBlA4Yw=="],
"hype": ["hype@git+https://git.nose.space/defunkt/hype#52086f4eb94cc36ecd9470d9a101ff01002687c8", { "dependencies": { "hono": "^4.10.4", "kleur": "^4.1.5" }, "peerDependencies": { "typescript": "^5" } }, "52086f4eb94cc36ecd9470d9a101ff01002687c8"],
"kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="],
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
}
}

20
package.json Normal file
View File

@ -0,0 +1,20 @@
{
"name": "toes",
"module": "src/index.ts",
"type": "module",
"private": true,
"scripts": {
"start": "bun run src/server/index.tsx",
"dev": "bun run --hot src/server/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"
}
}

143
src/server/index.tsx Normal file
View File

@ -0,0 +1,143 @@
import { Subprocess } from 'bun'
import { Hype } from 'hype'
import { readdirSync, readFileSync } from 'fs'
import { join } from 'path'
const APPS_DIR = join(process.env.DATA_DIR ?? '.', 'apps')
type RunningApp = {
name: string
port: number
proc: Subprocess
}
const runningApps = new Map<string, RunningApp>()
const err = (app: string, ...msg: string[]) =>
console.error('🐾', `${app}:`, ...msg)
const info = (app: string, ...msg: string[]) =>
console.log('🐾', `${app}:`, ...msg)
const log = (app: string, ...msg: string[]) =>
console.log('🐾', `${app}»`, ...msg)
const appNames = () => {
return readdirSync(APPS_DIR, { withFileTypes: true })
.filter(e => e.isDirectory())
.map(e => e.name)
.sort()
}
let NEXT_PORT = 3001
const getPort = () => NEXT_PORT++
const runApps = () => {
for (const dir of appNames()) {
if (!isApp(dir)) continue
const port = getPort()
runApp(dir, port)
}
}
const isApp = (dir: string): boolean => {
try {
const file = readFileSync(join(APPS_DIR, dir, 'package.json'), 'utf-8')
const json = JSON.parse(file)
return !!json.scripts?.toes
} catch (e) {
return false
}
}
const loadApp = (dir: string) => {
try {
const file = readFileSync(join(APPS_DIR, dir, 'package.json'), 'utf-8')
const json = JSON.parse(file)
if (json.scripts?.toes) {
return json
} else {
err(dir, 'No `bun toes` script in package.json')
return {}
}
} catch (e) {
err(dir, 'No package.json')
return {}
}
}
const runApp = (dir: string, port: number) => {
const pkg = loadApp(dir)
if (!pkg.scripts?.toes) return
const cwd = join(APPS_DIR, dir)
const cmd = ['bun', 'run', 'toes']
info(dir, `Starting on port ${port}...`)
const proc = Bun.spawn(cmd, {
cwd,
env: { ...process.env, PORT: String(port) },
stdout: 'pipe',
stderr: 'pipe',
})
runningApps.set(dir, { name: dir, port, proc })
const streamOutput = async (stream: ReadableStream<Uint8Array> | null, isErr: boolean) => {
if (!stream) return
const reader = stream.getReader()
const decoder = new TextDecoder()
while (true) {
const { done, value } = await reader.read()
if (done) break
const text = decoder.decode(value).trimEnd()
if (text) {
//isErr ? err(dir, text) : info(dir, text)
log(dir, text)
}
}
}
streamOutput(proc.stdout, false)
streamOutput(proc.stderr, true)
// Handle process exit
proc.exited.then(code => {
if (code !== 0)
err(dir, `Exited with code ${code}`)
else
info(dir, 'Stopped')
runningApps.delete(dir)
})
}
const getRunningApps = () =>
Array.from(runningApps.values()).map(({ name, port }) => ({ name, port }))
const stopApp = (dir: string) => {
const app = runningApps.get(dir)
if (app) {
info(dir, 'Stopping...')
app.proc.kill()
}
}
console.log('🐾 Toes!')
runApps()
const app = new Hype()
app.get('/', c => {
return c.html(
<>
<h1>🐾 Running Apps</h1>
{getRunningApps().map(app => <h2><a href={`http://localhost:${app.port}`}>{app.port}: {app.name}</a></h2>)}
</>
)
})
export { getRunningApps, stopApp }
export default app.defaults

30
tsconfig.json Normal file
View File

@ -0,0 +1,30 @@
{
"compilerOptions": {
// Environment setup & latest features
"lib": ["ESNext"],
"target": "ESNext",
"module": "Preserve",
"moduleDetection": "force",
"jsx": "react-jsx",
"jsxImportSource": "hono/jsx",
"allowJs": true,
// Bundler mode
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"noEmit": true,
// Best practices
"strict": true,
"skipLibCheck": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
// Some stricter flags (disabled by default)
"noUnusedLocals": false,
"noUnusedParameters": false,
"noPropertyAccessFromIndexSignature": false
}
}