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 =>

{app.port}: {app.name}

)} + + ) +}) + +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 + } +}