Compare commits
No commits in common. "b7bda052e96f4fa4e992287bbbe59b57e925a155" and "e195b53275f60cabf7f1f8f4685a7c8d0658371d" have entirely different histories.
b7bda052e9
...
e195b53275
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"name": "basic",
|
"name": "toes-app",
|
||||||
"module": "src/index.ts",
|
"module": "src/index.ts",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
|
@ -15,7 +15,7 @@
|
||||||
"typescript": "^5.9.2"
|
"typescript": "^5.9.2"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@because/forge": "^0.0.1",
|
"hype": "git+https://git.nose.space/defunkt/hype",
|
||||||
"@because/hype": "^0.0.1"
|
"forge": "git+https://git.nose.space/defunkt/forge"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { Hype } from 'hype'
|
import { Hype } from 'hype'
|
||||||
import { define, stylesToCSS } from '@because/forge'
|
import { define, stylesToCSS } from 'forge'
|
||||||
|
|
||||||
const app = new Hype()
|
const app = new Hype()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"name": "clock",
|
"name": "toes-clock",
|
||||||
"module": "src/index.ts",
|
"module": "src/index.ts",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
|
@ -18,7 +18,7 @@
|
||||||
"typescript": "^5.9.2"
|
"typescript": "^5.9.2"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@because/forge": "^0.0.1",
|
"hype": "git+https://git.nose.space/defunkt/hype",
|
||||||
"@because/hype": "^0.0.1"
|
"forge": "git+https://git.nose.space/defunkt/forge"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,38 +0,0 @@
|
||||||
{
|
|
||||||
"lockfileVersion": 1,
|
|
||||||
"configVersion": 1,
|
|
||||||
"workspaces": {
|
|
||||||
"": {
|
|
||||||
"name": "clock2",
|
|
||||||
"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.8", "", { "dependencies": { "bun-types": "1.3.8" } }, "sha512-3LvWJ2q5GerAXYxO2mffLTqOzEu5qnhEAlh48Vnu8WQfnmSwbgagjGZV6BoHKJztENYEDn6QmVd949W4uESRJA=="],
|
|
||||||
|
|
||||||
"@types/node": ["@types/node@25.1.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-t7frlewr6+cbx+9Ohpl0NOTKXZNV9xHRmNOvql47BFJKcEG1CxtxlPEEe+gR9uhVWM4DwhnvTF110mIL4yP9RA=="],
|
|
||||||
|
|
||||||
"bun-types": ["bun-types@1.3.8", "", { "dependencies": { "@types/node": "*" } }, "sha512-fL99nxdOWvV4LqjmC+8Q9kW3M4QTtTR1eePs94v5ctGqU8OeceWrSUaRw3JYb7tU3FkMIAjkueehrHPPPGKi5Q=="],
|
|
||||||
|
|
||||||
"forge": ["@because/forge@git+https://git.nose.space/defunkt/forge#67180bb4f3f13b6eacba020e60592d4a76e2deda", { "peerDependencies": { "typescript": "^5" } }, "67180bb4f3f13b6eacba020e60592d4a76e2deda"],
|
|
||||||
|
|
||||||
"hono": ["hono@4.11.7", "", {}, "sha512-l7qMiNee7t82bH3SeyUCt9UF15EVmaBvsppY2zQtrbIhl/yzBTny+YUxsVjSjQ6gaqaeVtZmGocom8TzBlA4Yw=="],
|
|
||||||
|
|
||||||
"hype": ["@because/hype@git+https://git.nose.space/defunkt/hype#5ed8673274187958e3bee569aba0d33baf605bb1", { "dependencies": { "hono": "^4.10.4", "kleur": "^4.1.5" }, "peerDependencies": { "typescript": "^5" } }, "5ed8673274187958e3bee569aba0d33baf605bb1"],
|
|
||||||
|
|
||||||
"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=="],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"name": "profile",
|
"name": "toes-app",
|
||||||
"module": "src/index.ts",
|
"module": "src/index.ts",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
|
@ -18,7 +18,7 @@
|
||||||
"typescript": "^5.9.2"
|
"typescript": "^5.9.2"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@because/forge": "^0.0.1",
|
"forge": "git+https://git.nose.space/defunkt/forge",
|
||||||
"@because/hype": "^0.0.1"
|
"hype": "git+https://git.nose.space/defunkt/hype"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"name": "truisms",
|
"name": "toes-app",
|
||||||
"module": "src/index.ts",
|
"module": "src/index.ts",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
|
@ -17,7 +17,7 @@
|
||||||
"typescript": "^5.9.2"
|
"typescript": "^5.9.2"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@because/forge": "^0.0.1",
|
"hype": "git+https://git.nose.space/defunkt/hype",
|
||||||
"@because/hype": "^0.0.1"
|
"forge": "git+https://git.nose.space/defunkt/forge"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
8
bun.lock
8
bun.lock
|
|
@ -5,8 +5,8 @@
|
||||||
"": {
|
"": {
|
||||||
"name": "toes",
|
"name": "toes",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@because/forge": "^0.0.1.",
|
"@_because/forge": "*",
|
||||||
"@because/hype": "^0.0.1",
|
"@_because/hype": "*",
|
||||||
"commander": "^14.0.2",
|
"commander": "^14.0.2",
|
||||||
"kleur": "^4.1.5",
|
"kleur": "^4.1.5",
|
||||||
},
|
},
|
||||||
|
|
@ -19,9 +19,9 @@
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"packages": {
|
"packages": {
|
||||||
"@because/forge": ["@because/forge@0.0.1", "https://npm.nose.space/@because/forge/-/forge-0.0.1.tgz", { "peerDependencies": { "typescript": "^5" } }, "sha512-QS5CK51gcWma91i4uECWe4HPJeNHcE+Af4SQHOcfEovyzOEa7VOTAjei+jIWr2i+abGWqQCEC9wIuFgPgyr2Bg=="],
|
"@_because/forge": ["@_because/forge@0.1.0", "https://npm.nose.space/@_because/forge/-/forge-0.1.0.tgz", { "peerDependencies": { "typescript": "^5" } }, "sha512-kut50WMLDUb088SHnCPENJKI6rcXLzswSlGcKqsl3d8F40X8uRXTX/CdZDK5Q9Z1CDpFzGZHQ9nireqme3IvPQ=="],
|
||||||
|
|
||||||
"@because/hype": ["@because/hype@0.0.1", "https://npm.nose.space/@because/hype/-/hype-0.0.1.tgz", { "dependencies": { "hono": "^4.10.4", "kleur": "^4.1.5" }, "peerDependencies": { "typescript": "^5" } }, "sha512-i92DNUXJOwt3J8dN1x8sh7i86blelcTCk8XDpwD839Ic8oe710lkDSVXJ7xYZb/i8YtzGhRg+L6eXDhaRiU2Pw=="],
|
"@_because/hype": ["@_because/hype@0.1.0", "https://npm.nose.space/@_because/hype/-/hype-0.1.0.tgz", { "dependencies": { "hono": "^4.10.4", "kleur": "^4.1.5" }, "peerDependencies": { "typescript": "^5" } }, "sha512-cw3Ms2jOIv1159VeNZFAEcH/fzVKOuqul9ggcemkZQHFAX1uGJD9wEvdJd2PNbbR5GG62J5rAPpqHHNmWrb8qA=="],
|
||||||
|
|
||||||
"@types/bun": ["@types/bun@1.3.8", "https://npm.nose.space/@types/bun/-/bun-1.3.8.tgz", { "dependencies": { "bun-types": "1.3.8" } }, "sha512-3LvWJ2q5GerAXYxO2mffLTqOzEu5qnhEAlh48Vnu8WQfnmSwbgagjGZV6BoHKJztENYEDn6QmVd949W4uESRJA=="],
|
"@types/bun": ["@types/bun@1.3.8", "https://npm.nose.space/@types/bun/-/bun-1.3.8.tgz", { "dependencies": { "bun-types": "1.3.8" } }, "sha512-3LvWJ2q5GerAXYxO2mffLTqOzEu5qnhEAlh48Vnu8WQfnmSwbgagjGZV6BoHKJztENYEDn6QmVd949W4uESRJA=="],
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,7 @@
|
||||||
"test": "bun test",
|
"test": "bun test",
|
||||||
"cli:build": "bun run scripts/build.ts",
|
"cli:build": "bun run scripts/build.ts",
|
||||||
"cli:build:all": "bun run scripts/build.ts --all",
|
"cli:build:all": "bun run scripts/build.ts --all",
|
||||||
"cli:install": "bun cli:build && sudo cp dist/toes /usr/local/bin",
|
"cli:install": "bun cli:build && sudo cp dist/toes /usr/local/bin"
|
||||||
"cli:uninstall": "sudo rm /usr/local/bin"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bun": "latest"
|
"@types/bun": "latest"
|
||||||
|
|
@ -23,8 +22,8 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"commander": "^14.0.2",
|
"commander": "^14.0.2",
|
||||||
"@because/forge": "^0.0.1",
|
"@_because/forge": "*",
|
||||||
"@because/hype": "^0.0.1",
|
"@_because/hype": "*",
|
||||||
"kleur": "^4.1.5"
|
"kleur": "^4.1.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
168
src/cli/index.ts
168
src/cli/index.ts
|
|
@ -5,8 +5,7 @@ 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'
|
||||||
import color from 'kleur'
|
import color from 'kleur'
|
||||||
import { basename, dirname, join, relative } from 'path'
|
import { dirname, join, relative } from 'path'
|
||||||
import * as readline from 'readline'
|
|
||||||
|
|
||||||
const HOST = `http://localhost:${process.env.PORT ?? 3000}`
|
const HOST = `http://localhost:${process.env.PORT ?? 3000}`
|
||||||
|
|
||||||
|
|
@ -32,33 +31,13 @@ function makeUrl(path: string): string {
|
||||||
return `${HOST}${path}`
|
return `${HOST}${path}`
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleError(error: unknown): void {
|
|
||||||
if (error instanceof Error && 'code' in error && error.code === 'ConnectionRefused') {
|
|
||||||
console.error(`🐾 Can't connect to toes server at ${HOST}`)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
console.error(error)
|
|
||||||
}
|
|
||||||
|
|
||||||
async function get<T>(url: string): Promise<T | undefined> {
|
async function get<T>(url: string): Promise<T | undefined> {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(makeUrl(url))
|
const res = await fetch(makeUrl(url))
|
||||||
if (!res.ok) throw new Error(`${res.status} ${res.statusText}`)
|
if (!res.ok) throw new Error(`${res.status} ${res.statusText}`)
|
||||||
return await res.json()
|
return await res.json()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error)
|
console.error(error)
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getManifest(appName: string): Promise<{ exists: boolean; manifest?: Manifest } | null> {
|
|
||||||
try {
|
|
||||||
const res = await fetch(makeUrl(`/api/sync/apps/${appName}/manifest`))
|
|
||||||
if (res.status === 404) return { exists: false }
|
|
||||||
if (!res.ok) throw new Error(`${res.status} ${res.statusText}`)
|
|
||||||
return { exists: true, manifest: await res.json() }
|
|
||||||
} catch (error) {
|
|
||||||
handleError(error)
|
|
||||||
return null
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -72,7 +51,7 @@ async function post<T, B = unknown>(url: string, body?: B): Promise<T | undefine
|
||||||
if (!res.ok) throw new Error(`${res.status} ${res.statusText}`)
|
if (!res.ok) throw new Error(`${res.status} ${res.statusText}`)
|
||||||
return await res.json()
|
return await res.json()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error)
|
console.error(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -85,7 +64,7 @@ async function put(url: string, body: Buffer | Uint8Array): Promise<boolean> {
|
||||||
if (!res.ok) throw new Error(`${res.status} ${res.statusText}`)
|
if (!res.ok) throw new Error(`${res.status} ${res.statusText}`)
|
||||||
return true
|
return true
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error)
|
console.error(error)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -97,7 +76,7 @@ async function download(url: string): Promise<Buffer | undefined> {
|
||||||
if (!res.ok) throw new Error(`${res.status} ${res.statusText}`)
|
if (!res.ok) throw new Error(`${res.status} ${res.statusText}`)
|
||||||
return Buffer.from(await res.arrayBuffer())
|
return Buffer.from(await res.arrayBuffer())
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error)
|
console.error(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -109,7 +88,7 @@ async function del(url: string): Promise<boolean> {
|
||||||
if (!res.ok) throw new Error(`${res.status} ${res.statusText}`)
|
if (!res.ok) throw new Error(`${res.status} ${res.statusText}`)
|
||||||
return true
|
return true
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error)
|
console.error(error)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -334,29 +313,24 @@ async function pushApp() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const localManifest = generateLocalManifest(process.cwd(), appName)
|
console.log(`Pushing ${color.bold(appName)} to server...`)
|
||||||
const result = await getManifest(appName)
|
|
||||||
|
|
||||||
if (result === null) {
|
const localManifest = generateLocalManifest(process.cwd(), appName)
|
||||||
// Connection error - already printed
|
const remoteManifest: Manifest | undefined = await get(`/api/sync/apps/${appName}/manifest`)
|
||||||
|
|
||||||
|
if (!remoteManifest) {
|
||||||
|
console.error('App not found on server')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!result.exists) {
|
|
||||||
const ok = await confirm(`App ${color.bold(appName)} doesn't exist on server. Create it?`)
|
|
||||||
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(remoteManifest.files))
|
||||||
|
|
||||||
// Files to upload (new or changed)
|
// Files to upload (new or changed)
|
||||||
const toUpload: string[] = []
|
const toUpload: string[] = []
|
||||||
for (const file of localFiles) {
|
for (const file of localFiles) {
|
||||||
const local = localManifest.files[file]!
|
const local = localManifest.files[file]!
|
||||||
const remote = result.manifest?.files[file]
|
const remote = remoteManifest.files[file]
|
||||||
if (!remote || local.hash !== remote.hash) {
|
if (!remote || local.hash !== remote.hash) {
|
||||||
toUpload.push(file)
|
toUpload.push(file)
|
||||||
}
|
}
|
||||||
|
|
@ -403,114 +377,6 @@ async function pushApp() {
|
||||||
console.log(color.green('✓ Push complete'))
|
console.log(color.green('✓ Push complete'))
|
||||||
}
|
}
|
||||||
|
|
||||||
async function confirm(message: string): Promise<boolean> {
|
|
||||||
const rl = readline.createInterface({
|
|
||||||
input: process.stdin,
|
|
||||||
output: process.stdout,
|
|
||||||
})
|
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
rl.question(`${message} [y/N] `, (answer) => {
|
|
||||||
rl.close()
|
|
||||||
resolve(answer.toLowerCase() === 'y')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async function newApp(name?: string) {
|
|
||||||
const appPath = name ? join(process.cwd(), name) : process.cwd()
|
|
||||||
const appName = name ?? basename(process.cwd())
|
|
||||||
|
|
||||||
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 ok = await confirm(`Create ${color.bold(appName)} in ${appPath}?`)
|
|
||||||
if (!ok) return
|
|
||||||
|
|
||||||
if (name) {
|
|
||||||
mkdirSync(appPath, { recursive: true })
|
|
||||||
}
|
|
||||||
|
|
||||||
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()
|
|
||||||
console.log('Next steps:')
|
|
||||||
if (name) {
|
|
||||||
console.log(` cd ${name}`)
|
|
||||||
}
|
|
||||||
console.log(' bun install')
|
|
||||||
console.log(' bun dev')
|
|
||||||
}
|
|
||||||
|
|
||||||
async function pullApp() {
|
async function pullApp() {
|
||||||
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.')
|
||||||
|
|
@ -650,12 +516,6 @@ program
|
||||||
.argument('<name>', 'app name')
|
.argument('<name>', 'app name')
|
||||||
.action(getApp)
|
.action(getApp)
|
||||||
|
|
||||||
program
|
|
||||||
.command('new')
|
|
||||||
.description('Create a new toes app')
|
|
||||||
.argument('[name]', 'app name (uses current directory if omitted)')
|
|
||||||
.action(newApp)
|
|
||||||
|
|
||||||
program
|
program
|
||||||
.command('push')
|
.command('push')
|
||||||
.description('Push local changes to server')
|
.description('Push local changes to server')
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
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 'forge'
|
||||||
import type { App, AppState } from '../shared/types'
|
import type { App, AppState } from '../shared/types'
|
||||||
import { theme } from './themes'
|
import { theme } from './themes'
|
||||||
import { Modal, initModal } from './tags/modal'
|
import { Modal, initModal } from './tags/modal'
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { define } from '@because/forge'
|
import { define } from 'forge'
|
||||||
import { theme } from '../themes'
|
import { theme } from '../themes'
|
||||||
import { openModal, closeModal } from './modal'
|
import { openModal, closeModal } from './modal'
|
||||||
import { update } from '../update'
|
import { update } from '../update'
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import type { Child } from 'hono/jsx'
|
import type { Child } from 'hono/jsx'
|
||||||
import { define } from '@because/forge'
|
import { define } from 'forge'
|
||||||
import { theme } from '../themes'
|
import { theme } from '../themes'
|
||||||
|
|
||||||
let modalTitle: string | null = null
|
let modalTitle: string | null = null
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { createThemes } from '@because/forge'
|
import { createThemes } from 'forge'
|
||||||
import dark from './dark'
|
import dark from './dark'
|
||||||
import light from './light'
|
import light from './light'
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,121 +0,0 @@
|
||||||
import { allApps, onChange, startApp, stopApp, updateAppIcon } from '$apps'
|
|
||||||
import type { App as BackendApp } from '$apps'
|
|
||||||
import type { App as SharedApp } from '@types'
|
|
||||||
import { Hype } from '@because/hype'
|
|
||||||
|
|
||||||
const router = Hype.router()
|
|
||||||
|
|
||||||
// BackendApp -> SharedApp
|
|
||||||
function convert(app: BackendApp): SharedApp {
|
|
||||||
const clone = { ...app }
|
|
||||||
delete clone.proc
|
|
||||||
delete clone.logs
|
|
||||||
return clone
|
|
||||||
}
|
|
||||||
|
|
||||||
// SSE endpoint for real-time app state updates
|
|
||||||
router.sse('/stream', (send) => {
|
|
||||||
const broadcast = () => {
|
|
||||||
const apps: SharedApp[] = allApps().map(({ name, state, icon, error, port, started, logs }) => ({
|
|
||||||
name,
|
|
||||||
state,
|
|
||||||
icon,
|
|
||||||
error,
|
|
||||||
port,
|
|
||||||
started,
|
|
||||||
logs,
|
|
||||||
}))
|
|
||||||
send(apps)
|
|
||||||
}
|
|
||||||
|
|
||||||
broadcast()
|
|
||||||
const unsub = onChange(broadcast)
|
|
||||||
return () => unsub()
|
|
||||||
})
|
|
||||||
|
|
||||||
router.get('/', c => c.json(allApps().map(convert)))
|
|
||||||
|
|
||||||
router.get('/:app', c => {
|
|
||||||
const appName = c.req.param('app')
|
|
||||||
if (!appName) return c.json({ error: 'App not found' }, 404)
|
|
||||||
|
|
||||||
const app = allApps().find(a => a.name === appName)
|
|
||||||
if (!app) return c.json({ error: 'App not found' }, 404)
|
|
||||||
|
|
||||||
return c.json(convert(app))
|
|
||||||
})
|
|
||||||
|
|
||||||
router.get('/:app/logs', c => {
|
|
||||||
const appName = c.req.param('app')
|
|
||||||
if (!appName) return c.json({ error: 'App not found' }, 404)
|
|
||||||
|
|
||||||
const app = allApps().find(a => a.name === appName)
|
|
||||||
if (!app) return c.json({ error: 'App not found' }, 404)
|
|
||||||
|
|
||||||
return c.json(app.logs ?? [])
|
|
||||||
})
|
|
||||||
|
|
||||||
router.sse('/:app/logs/stream', (send, c) => {
|
|
||||||
const appName = c.req.param('app')
|
|
||||||
const targetApp = allApps().find(a => a.name === appName)
|
|
||||||
if (!targetApp) return
|
|
||||||
|
|
||||||
let lastLogCount = 0
|
|
||||||
|
|
||||||
const sendNewLogs = () => {
|
|
||||||
const currentApp = allApps().find(a => a.name === appName)
|
|
||||||
if (!currentApp) return
|
|
||||||
|
|
||||||
const logs = currentApp.logs ?? []
|
|
||||||
const newLogs = logs.slice(lastLogCount)
|
|
||||||
lastLogCount = logs.length
|
|
||||||
|
|
||||||
for (const line of newLogs) {
|
|
||||||
send(line)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sendNewLogs()
|
|
||||||
const unsub = onChange(sendNewLogs)
|
|
||||||
return () => unsub()
|
|
||||||
})
|
|
||||||
|
|
||||||
router.post('/:app/start', c => {
|
|
||||||
const appName = c.req.param('app')
|
|
||||||
if (!appName) return c.json({ error: 'App not found' }, 404)
|
|
||||||
|
|
||||||
startApp(appName)
|
|
||||||
return c.json({ ok: true })
|
|
||||||
})
|
|
||||||
|
|
||||||
router.post('/:app/restart', c => {
|
|
||||||
const appName = c.req.param('app')
|
|
||||||
if (!appName) return c.json({ error: 'App not found' }, 404)
|
|
||||||
|
|
||||||
stopApp(appName)
|
|
||||||
startApp(appName)
|
|
||||||
return c.json({ ok: true })
|
|
||||||
})
|
|
||||||
|
|
||||||
router.post('/:app/stop', c => {
|
|
||||||
const appName = c.req.param('app')
|
|
||||||
if (!appName) return c.json({ error: 'App not found' }, 404)
|
|
||||||
|
|
||||||
stopApp(appName)
|
|
||||||
return c.json({ ok: true })
|
|
||||||
})
|
|
||||||
|
|
||||||
router.post('/:app/icon', c => {
|
|
||||||
const appName = c.req.param('app')
|
|
||||||
const icon = c.req.query('icon') ?? ''
|
|
||||||
if (!icon) return c.json({ error: 'No icon query param provided' })
|
|
||||||
|
|
||||||
try {
|
|
||||||
updateAppIcon(appName, icon)
|
|
||||||
return c.json({ ok: true })
|
|
||||||
} catch (error) {
|
|
||||||
return c.json({ error })
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
export default router
|
|
||||||
|
|
@ -1,68 +0,0 @@
|
||||||
import { APPS_DIR, allApps } from '$apps'
|
|
||||||
import { generateManifest } from '../sync'
|
|
||||||
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'fs'
|
|
||||||
import { dirname, join } from 'path'
|
|
||||||
import { Hype } from '@because/hype'
|
|
||||||
|
|
||||||
const router = Hype.router()
|
|
||||||
|
|
||||||
router.get('/apps', c => c.json(allApps().map(a => a.name)))
|
|
||||||
|
|
||||||
router.get('/apps/:app/manifest', c => {
|
|
||||||
const appName = c.req.param('app')
|
|
||||||
if (!appName) return c.json({ error: 'App not found' }, 404)
|
|
||||||
|
|
||||||
const appPath = join(APPS_DIR, appName)
|
|
||||||
if (!existsSync(appPath)) return c.json({ error: 'App not found' }, 404)
|
|
||||||
|
|
||||||
const manifest = generateManifest(appPath, appName)
|
|
||||||
return c.json(manifest)
|
|
||||||
})
|
|
||||||
|
|
||||||
router.get('/apps/:app/files/:path{.+}', c => {
|
|
||||||
const appName = c.req.param('app')
|
|
||||||
const filePath = c.req.param('path')
|
|
||||||
|
|
||||||
if (!appName || !filePath) return c.json({ error: 'Invalid path' }, 400)
|
|
||||||
|
|
||||||
const fullPath = join(APPS_DIR, appName, filePath)
|
|
||||||
if (!existsSync(fullPath)) return c.json({ error: 'File not found' }, 404)
|
|
||||||
|
|
||||||
const content = readFileSync(fullPath)
|
|
||||||
return new Response(content)
|
|
||||||
})
|
|
||||||
|
|
||||||
router.put('/apps/:app/files/:path{.+}', async c => {
|
|
||||||
const appName = c.req.param('app')
|
|
||||||
const filePath = c.req.param('path')
|
|
||||||
|
|
||||||
if (!appName || !filePath) return c.json({ error: 'Invalid path' }, 400)
|
|
||||||
|
|
||||||
const fullPath = join(APPS_DIR, appName, filePath)
|
|
||||||
const dir = dirname(fullPath)
|
|
||||||
|
|
||||||
// Ensure directory exists
|
|
||||||
if (!existsSync(dir)) {
|
|
||||||
mkdirSync(dir, { recursive: true })
|
|
||||||
}
|
|
||||||
|
|
||||||
const body = await c.req.arrayBuffer()
|
|
||||||
writeFileSync(fullPath, new Uint8Array(body))
|
|
||||||
|
|
||||||
return c.json({ ok: true })
|
|
||||||
})
|
|
||||||
|
|
||||||
router.delete('/apps/:app/files/:path{.+}', c => {
|
|
||||||
const appName = c.req.param('app')
|
|
||||||
const filePath = c.req.param('path')
|
|
||||||
|
|
||||||
if (!appName || !filePath) return c.json({ error: 'Invalid path' }, 400)
|
|
||||||
|
|
||||||
const fullPath = join(APPS_DIR, appName, filePath)
|
|
||||||
if (!existsSync(fullPath)) return c.json({ error: 'File not found' }, 404)
|
|
||||||
|
|
||||||
unlinkSync(fullPath)
|
|
||||||
return c.json({ ok: true })
|
|
||||||
})
|
|
||||||
|
|
||||||
export default router
|
|
||||||
|
|
@ -1,12 +1,212 @@
|
||||||
import { initApps } from '$apps'
|
import { APPS_DIR, allApps, initApps, onChange, startApp, stopApp, updateAppIcon } from '$apps'
|
||||||
import appsRouter from './api/apps'
|
import type { App as SharedApp } from '@types'
|
||||||
import syncRouter from './api/sync'
|
import type { App as BackendApp } from '$apps'
|
||||||
import { Hype } from '@because/hype'
|
import { generateManifest } from './sync'
|
||||||
|
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'fs'
|
||||||
|
import { dirname, join } from 'path'
|
||||||
|
import { Hype } from 'hype'
|
||||||
|
|
||||||
|
// BackendApp -> SharedApp
|
||||||
|
function convert(app: BackendApp): SharedApp {
|
||||||
|
const clone = { ...app }
|
||||||
|
delete clone.proc
|
||||||
|
delete clone.logs
|
||||||
|
return clone
|
||||||
|
}
|
||||||
|
|
||||||
const app = new Hype({ layout: false })
|
const app = new Hype({ layout: false })
|
||||||
|
|
||||||
app.route('/api/apps', appsRouter)
|
// SSE endpoint for real-time app state updates
|
||||||
app.route('/api/sync', syncRouter)
|
app.get('/api/apps/stream', c => {
|
||||||
|
const encoder = new TextEncoder()
|
||||||
|
|
||||||
|
const stream = new ReadableStream({
|
||||||
|
start(controller) {
|
||||||
|
const send = () => {
|
||||||
|
// Strip proc field from apps before sending
|
||||||
|
const apps: SharedApp[] = allApps().map(({ name, state, icon, error, port, started, logs }) => ({
|
||||||
|
name,
|
||||||
|
state,
|
||||||
|
icon,
|
||||||
|
error,
|
||||||
|
port,
|
||||||
|
started,
|
||||||
|
logs,
|
||||||
|
}))
|
||||||
|
const data = JSON.stringify(apps)
|
||||||
|
controller.enqueue(encoder.encode(`data: ${data}\n\n`))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send initial state
|
||||||
|
send()
|
||||||
|
|
||||||
|
// Subscribe to changes
|
||||||
|
const unsub = onChange(send)
|
||||||
|
|
||||||
|
// Handle client disconnect via abort signal
|
||||||
|
c.req.raw.signal.addEventListener('abort', () => {
|
||||||
|
unsub()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return new Response(stream, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'text/event-stream',
|
||||||
|
'Cache-Control': 'no-cache',
|
||||||
|
Connection: 'keep-alive',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
app.get('/api/apps', c =>
|
||||||
|
c.json(allApps().map(convert))
|
||||||
|
)
|
||||||
|
|
||||||
|
app.get('/api/apps/:app', c => {
|
||||||
|
const appName = c.req.param('app')
|
||||||
|
if (!appName) return c.json({ error: 'App not found' }, 404)
|
||||||
|
|
||||||
|
const app = allApps().find(a => a.name === appName)
|
||||||
|
if (!app) return c.json({ error: 'App not found' }, 404)
|
||||||
|
|
||||||
|
return c.json(convert(app))
|
||||||
|
})
|
||||||
|
|
||||||
|
app.get('/api/apps/:app/logs', c => {
|
||||||
|
const appName = c.req.param('app')
|
||||||
|
if (!appName) return c.json({ error: 'App not found' }, 404)
|
||||||
|
|
||||||
|
const app = allApps().find(a => a.name === appName)
|
||||||
|
if (!app) return c.json({ error: 'App not found' }, 404)
|
||||||
|
|
||||||
|
return c.json(app.logs ?? [])
|
||||||
|
})
|
||||||
|
|
||||||
|
app.sse('/api/apps/:app/logs/stream', (send, c) => {
|
||||||
|
const appName = c.req.param('app')
|
||||||
|
const targetApp = allApps().find(a => a.name === appName)
|
||||||
|
if (!targetApp) return
|
||||||
|
|
||||||
|
let lastLogCount = 0
|
||||||
|
|
||||||
|
const sendNewLogs = () => {
|
||||||
|
const currentApp = allApps().find(a => a.name === appName)
|
||||||
|
if (!currentApp) return
|
||||||
|
|
||||||
|
const logs = currentApp.logs ?? []
|
||||||
|
const newLogs = logs.slice(lastLogCount)
|
||||||
|
lastLogCount = logs.length
|
||||||
|
|
||||||
|
for (const line of newLogs) {
|
||||||
|
send(line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sendNewLogs()
|
||||||
|
const unsub = onChange(sendNewLogs)
|
||||||
|
return () => unsub()
|
||||||
|
})
|
||||||
|
|
||||||
|
app.post('/api/apps/:app/start', c => {
|
||||||
|
const appName = c.req.param('app')
|
||||||
|
if (!appName) return c.json({ error: 'App not found' }, 404)
|
||||||
|
|
||||||
|
startApp(appName)
|
||||||
|
return c.json({ ok: true })
|
||||||
|
})
|
||||||
|
|
||||||
|
app.post('/api/apps/:app/restart', c => {
|
||||||
|
const appName = c.req.param('app')
|
||||||
|
if (!appName) return c.json({ error: 'App not found' }, 404)
|
||||||
|
|
||||||
|
stopApp(appName)
|
||||||
|
startApp(appName)
|
||||||
|
return c.json({ ok: true })
|
||||||
|
})
|
||||||
|
|
||||||
|
app.post('/api/apps/:app/stop', c => {
|
||||||
|
const appName = c.req.param('app')
|
||||||
|
if (!appName) return c.json({ error: 'App not found' }, 404)
|
||||||
|
|
||||||
|
stopApp(appName)
|
||||||
|
return c.json({ ok: true })
|
||||||
|
})
|
||||||
|
|
||||||
|
app.post('/api/apps/:app/icon', c => {
|
||||||
|
const appName = c.req.param('app')
|
||||||
|
const icon = c.req.query('icon') ?? ''
|
||||||
|
if (!icon) return c.json({ error: 'No icon query param provided' })
|
||||||
|
|
||||||
|
try {
|
||||||
|
updateAppIcon(appName, icon)
|
||||||
|
return c.json({ ok: true })
|
||||||
|
} catch (error) {
|
||||||
|
return c.json({ error })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Sync API
|
||||||
|
app.get('/api/sync/apps', c =>
|
||||||
|
c.json(allApps().map(a => a.name))
|
||||||
|
)
|
||||||
|
|
||||||
|
app.get('/api/sync/apps/:app/manifest', c => {
|
||||||
|
const appName = c.req.param('app')
|
||||||
|
if (!appName) return c.json({ error: 'App not found' }, 404)
|
||||||
|
|
||||||
|
const appPath = join(APPS_DIR, appName)
|
||||||
|
if (!existsSync(appPath)) return c.json({ error: 'App not found' }, 404)
|
||||||
|
|
||||||
|
const manifest = generateManifest(appPath, appName)
|
||||||
|
return c.json(manifest)
|
||||||
|
})
|
||||||
|
|
||||||
|
app.get('/api/sync/apps/:app/files/:path{.+}', c => {
|
||||||
|
const appName = c.req.param('app')
|
||||||
|
const filePath = c.req.param('path')
|
||||||
|
|
||||||
|
if (!appName || !filePath) return c.json({ error: 'Invalid path' }, 400)
|
||||||
|
|
||||||
|
const fullPath = join(APPS_DIR, appName, filePath)
|
||||||
|
if (!existsSync(fullPath)) return c.json({ error: 'File not found' }, 404)
|
||||||
|
|
||||||
|
const content = readFileSync(fullPath)
|
||||||
|
return new Response(content)
|
||||||
|
})
|
||||||
|
|
||||||
|
app.put('/api/sync/apps/:app/files/:path{.+}', async c => {
|
||||||
|
const appName = c.req.param('app')
|
||||||
|
const filePath = c.req.param('path')
|
||||||
|
|
||||||
|
if (!appName || !filePath) return c.json({ error: 'Invalid path' }, 400)
|
||||||
|
|
||||||
|
const fullPath = join(APPS_DIR, appName, filePath)
|
||||||
|
const dir = dirname(fullPath)
|
||||||
|
|
||||||
|
// Ensure directory exists
|
||||||
|
if (!existsSync(dir)) {
|
||||||
|
mkdirSync(dir, { recursive: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = await c.req.arrayBuffer()
|
||||||
|
writeFileSync(fullPath, new Uint8Array(body))
|
||||||
|
|
||||||
|
return c.json({ ok: true })
|
||||||
|
})
|
||||||
|
|
||||||
|
app.delete('/api/sync/apps/:app/files/:path{.+}', c => {
|
||||||
|
const appName = c.req.param('app')
|
||||||
|
const filePath = c.req.param('path')
|
||||||
|
|
||||||
|
if (!appName || !filePath) return c.json({ error: 'Invalid path' }, 400)
|
||||||
|
|
||||||
|
const fullPath = join(APPS_DIR, appName, filePath)
|
||||||
|
if (!existsSync(fullPath)) return c.json({ error: 'File not found' }, 404)
|
||||||
|
|
||||||
|
unlinkSync(fullPath)
|
||||||
|
return c.json({ ok: true })
|
||||||
|
})
|
||||||
|
|
||||||
console.log('🐾 Toes!')
|
console.log('🐾 Toes!')
|
||||||
initApps()
|
initApps()
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user