From 1ec012338fc85a750f6303c0d6d51309324c6f7e Mon Sep 17 00:00:00 2001
From: Chris Wanstrath <2+defunkt@users.noreply.github.com>
Date: Tue, 27 Jan 2026 14:40:43 -0800
Subject: [PATCH] TOES
---
.gitignore | 2 +
TODO.txt | 25 +++++++
apps/basic/bun.lock | 37 ++++++++++
apps/basic/index.tsx | 10 +++
apps/basic/package.json | 21 ++++++
apps/basic/tsconfig.json | 30 ++++++++
apps/file.txt | 1 +
apps/profile/bun.lock | 37 ++++++++++
apps/profile/index.tsx | 10 +++
apps/profile/package.json | 21 ++++++
apps/profile/tsconfig.json | 30 ++++++++
apps/risk/package.json | 1 +
bun.lock | 37 ++++++++++
package.json | 20 ++++++
src/server/index.tsx | 143 +++++++++++++++++++++++++++++++++++++
tsconfig.json | 30 ++++++++
16 files changed, 455 insertions(+)
create mode 100644 .gitignore
create mode 100644 TODO.txt
create mode 100644 apps/basic/bun.lock
create mode 100644 apps/basic/index.tsx
create mode 100644 apps/basic/package.json
create mode 100644 apps/basic/tsconfig.json
create mode 100644 apps/file.txt
create mode 100644 apps/profile/bun.lock
create mode 100644 apps/profile/index.tsx
create mode 100644 apps/profile/package.json
create mode 100644 apps/profile/tsconfig.json
create mode 100644 apps/risk/package.json
create mode 100644 bun.lock
create mode 100644 package.json
create mode 100644 src/server/index.tsx
create mode 100644 tsconfig.json
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..d570088
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+node_modules/
+
diff --git a/TODO.txt b/TODO.txt
new file mode 100644
index 0000000..24cef74
--- /dev/null
+++ b/TODO.txt
@@ -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
+[ ] ...
diff --git a/apps/basic/bun.lock b/apps/basic/bun.lock
new file mode 100644
index 0000000..ca19213
--- /dev/null
+++ b/apps/basic/bun.lock
@@ -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=="],
+ }
+}
diff --git a/apps/basic/index.tsx b/apps/basic/index.tsx
new file mode 100644
index 0000000..a7920b0
--- /dev/null
+++ b/apps/basic/index.tsx
@@ -0,0 +1,10 @@
+import { Hype } from 'hype'
+
+const app = new Hype
+
+app.get('/', c => c.html(
Hi there!
))
+
+const apps = () => {
+}
+
+export default app.defaults
diff --git a/apps/basic/package.json b/apps/basic/package.json
new file mode 100644
index 0000000..6fe680b
--- /dev/null
+++ b/apps/basic/package.json
@@ -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"
+ }
+}
diff --git a/apps/basic/tsconfig.json b/apps/basic/tsconfig.json
new file mode 100644
index 0000000..545396c
--- /dev/null
+++ b/apps/basic/tsconfig.json
@@ -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
+ }
+}
diff --git a/apps/file.txt b/apps/file.txt
new file mode 100644
index 0000000..45b983b
--- /dev/null
+++ b/apps/file.txt
@@ -0,0 +1 @@
+hi
diff --git a/apps/profile/bun.lock b/apps/profile/bun.lock
new file mode 100644
index 0000000..ca19213
--- /dev/null
+++ b/apps/profile/bun.lock
@@ -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=="],
+ }
+}
diff --git a/apps/profile/index.tsx b/apps/profile/index.tsx
new file mode 100644
index 0000000..a7920b0
--- /dev/null
+++ b/apps/profile/index.tsx
@@ -0,0 +1,10 @@
+import { Hype } from 'hype'
+
+const app = new Hype
+
+app.get('/', c => c.html(Hi there!
))
+
+const apps = () => {
+}
+
+export default app.defaults
diff --git a/apps/profile/package.json b/apps/profile/package.json
new file mode 100644
index 0000000..6fe680b
--- /dev/null
+++ b/apps/profile/package.json
@@ -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"
+ }
+}
diff --git a/apps/profile/tsconfig.json b/apps/profile/tsconfig.json
new file mode 100644
index 0000000..545396c
--- /dev/null
+++ b/apps/profile/tsconfig.json
@@ -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
+ }
+}
diff --git a/apps/risk/package.json b/apps/risk/package.json
new file mode 100644
index 0000000..0967ef4
--- /dev/null
+++ b/apps/risk/package.json
@@ -0,0 +1 @@
+{}
diff --git a/bun.lock b/bun.lock
new file mode 100644
index 0000000..5d6f287
--- /dev/null
+++ b/bun.lock
@@ -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=="],
+ }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..308648e
--- /dev/null
+++ b/package.json
@@ -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"
+ }
+}
diff --git a/src/server/index.tsx b/src/server/index.tsx
new file mode 100644
index 0000000..5d04e6e
--- /dev/null
+++ b/src/server/index.tsx
@@ -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()
+
+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 | 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(
+ <>
+ ๐พ Running Apps
+ {getRunningApps().map(app => )}
+ >
+ )
+})
+
+export { getRunningApps, stopApp }
+export default app.defaults
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..545396c
--- /dev/null
+++ b/tsconfig.json
@@ -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
+ }
+}