Compare commits

..

No commits in common. "52bfa783e17561613bf41a464281ab5a74e8e06e" and "d99f80cd0e8ed629bc204bf0fa905149ac0dd79e" have entirely different histories.

18 changed files with 48 additions and 110 deletions

3
.gitignore vendored
View File

@ -33,6 +33,3 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
# Finder (MacOS) folder config
.DS_Store
# app symlinks (created on boot)
apps/*/current

View File

@ -23,8 +23,9 @@ by default, the CLI connects to `localhost:3000` in dev and `toes.local:80` in p
```bash
toes config # show current host
TOES_URL=http://192.168.1.50:3000 toes list # connect to IP
TOES_URL=http://mypi.local toes list # connect to hostname
TOES_URL=http://192.168.1.50:3000 toes list # full URL
TOES_HOST=mypi.local toes list # hostname (port 80)
TOES_HOST=mypi.local PORT=3000 toes list # hostname + port
```
set `NODE_ENV=production` to default to `toes.local:80`.

1
apps/basic/current Symbolic link
View File

@ -0,0 +1 @@
20260130-000000

1
apps/clock/current Symbolic link
View File

@ -0,0 +1 @@
20260130-000000

1
apps/code/current Symbolic link
View File

@ -0,0 +1 @@
20260130-000000

1
apps/cron/current Symbolic link
View File

@ -0,0 +1 @@
20260201-000000

1
apps/env/current vendored Symbolic link
View File

@ -0,0 +1 @@
20260130-000000

1
apps/profile/current Symbolic link
View File

@ -0,0 +1 @@
20260130-000000

1
apps/risk/current Symbolic link
View File

@ -0,0 +1 @@
20260130-000000

1
apps/todo/current Symbolic link
View File

@ -0,0 +1 @@
20260130-181927

1
apps/truisms/current Symbolic link
View File

@ -0,0 +1 @@
20260130-000000

1
apps/versions/current Symbolic link
View File

@ -0,0 +1 @@
20260130-000000

View File

@ -3,7 +3,7 @@ import { generateTemplates, type TemplateType } from '%templates'
import color from 'kleur'
import { existsSync, mkdirSync, writeFileSync } from 'fs'
import { basename, join } from 'path'
import { del, get, getManifest, HOST, makeAppUrl, post } from '../http'
import { del, get, getManifest, HOST, post } from '../http'
import { confirm, prompt } from '../prompts'
import { resolveAppName } from '../name'
import { pushApp } from './sync'
@ -20,15 +20,23 @@ export async function configShow() {
const source = process.env.TOES_URL
? 'TOES_URL'
: process.env.TOES_HOST
? 'TOES_HOST' + (process.env.PORT ? ' + PORT' : '')
: process.env.NODE_ENV === 'production'
? '(production)'
: '(development)'
? 'default (production)'
: 'default (development)'
console.log(`Source: ${color.gray(source)}`)
if (process.env.TOES_URL) {
console.log(` TOES_URL=${process.env.TOES_URL}`)
}
if (process.env.TOES_HOST) {
console.log(` TOES_HOST=${process.env.TOES_HOST}`)
}
if (process.env.PORT) {
console.log(` PORT=${process.env.PORT}`)
}
if (process.env.NODE_ENV) {
console.log(` NODE_ENV=${process.env.NODE_ENV}`)
}
@ -49,7 +57,7 @@ export async function infoApp(arg?: string) {
console.log(` State: ${app.state}`)
if (app.port) {
console.log(` Port: ${app.port}`)
console.log(` URL: ${makeAppUrl(app.port)}`)
console.log(` URL: http://localhost:${app.port}`)
}
if (app.started) {
const uptime = Date.now() - app.started
@ -177,7 +185,7 @@ export async function openApp(arg?: string) {
console.error(`App is not running: ${name}`)
return
}
const url = makeAppUrl(app.port!)
const url = `http://localhost:${app.port}`
console.log(`Opening ${url}`)
Bun.spawn(['open', url])
}

View File

@ -7,40 +7,20 @@ function getDefaultHost(): string {
return `http://localhost:${process.env.PORT ?? 3000}`
}
const normalizeUrl = (url: string) =>
url.startsWith('http://') || url.startsWith('https://') ? url : `http://${url}`
function tryParseError(text: string): string | undefined {
try {
const json = JSON.parse(text)
return json.error
} catch {
return undefined
}
}
const defaultPort = process.env.NODE_ENV === 'production' ? 80 : 3000
export const HOST = process.env.TOES_URL
? normalizeUrl(process.env.TOES_URL)
: getDefaultHost()
?? (process.env.TOES_HOST ? `http://${process.env.TOES_HOST}:${process.env.PORT ?? defaultPort}` : undefined)
?? getDefaultHost()
export function makeUrl(path: string): string {
return `${HOST}${path}`
}
export function makeAppUrl(port: number): string {
const url = new URL(HOST)
url.port = String(port)
return url.toString().replace(/\/$/, '')
}
export 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}`)
console.error(` Set TOES_URL to connect to a different host`)
return
}
if (error instanceof Error) {
console.error(`Error: ${error.message}`)
console.error(` Set TOES_URL or TOES_HOST to connect to a different host`)
return
}
console.error(error)
@ -49,11 +29,7 @@ export function handleError(error: unknown): void {
export async function get<T>(url: string): Promise<T | undefined> {
try {
const res = await fetch(makeUrl(url))
if (!res.ok) {
const text = await res.text()
const msg = tryParseError(text) ?? `${res.status} ${res.statusText}`
throw new Error(msg)
}
if (!res.ok) throw new Error(`${res.status} ${res.statusText}`)
return await res.json()
} catch (error) {
handleError(error)
@ -79,11 +55,7 @@ export async function post<T, B = unknown>(url: string, body?: B): Promise<T | u
headers: body !== undefined ? { 'Content-Type': 'application/json' } : undefined,
body: body !== undefined ? JSON.stringify(body) : undefined,
})
if (!res.ok) {
const text = await res.text()
const msg = tryParseError(text) ?? `${res.status} ${res.statusText}`
throw new Error(msg)
}
if (!res.ok) throw new Error(`${res.status} ${res.statusText}`)
return await res.json()
} catch (error) {
handleError(error)
@ -96,11 +68,7 @@ export async function put(url: string, body: Buffer | Uint8Array): Promise<boole
method: 'PUT',
body: body as BodyInit,
})
if (!res.ok) {
const text = await res.text()
const msg = tryParseError(text) ?? `${res.status} ${res.statusText}`
throw new Error(msg)
}
if (!res.ok) throw new Error(`${res.status} ${res.statusText}`)
return true
} catch (error) {
handleError(error)
@ -112,11 +80,7 @@ export async function download(url: string): Promise<Buffer | undefined> {
try {
const fullUrl = makeUrl(url)
const res = await fetch(fullUrl)
if (!res.ok) {
const text = await res.text()
const msg = tryParseError(text) ?? `${res.status} ${res.statusText}`
throw new Error(msg)
}
if (!res.ok) throw new Error(`${res.status} ${res.statusText}`)
return Buffer.from(await res.arrayBuffer())
} catch (error) {
handleError(error)
@ -128,11 +92,7 @@ export async function del(url: string): Promise<boolean> {
const res = await fetch(makeUrl(url), {
method: 'DELETE',
})
if (!res.ok) {
const text = await res.text()
const msg = tryParseError(text) ?? `${res.status} ${res.statusText}`
throw new Error(msg)
}
if (!res.ok) throw new Error(`${res.status} ${res.statusText}`)
return true
} catch (error) {
handleError(error)

View File

@ -74,8 +74,8 @@ export function AppDetail({ app, render }: { app: App, render: () => void }) {
<InfoRow>
<InfoLabel>URL</InfoLabel>
<InfoValue>
<Link href={`${location.protocol}//${location.hostname}:${app.port}`} target="_blank">
{location.protocol}//{location.hostname}:{app.port}
<Link href={`http://localhost:${app.port}`} target="_blank">
http://localhost:{app.port}
</Link>
</InfoValue>
</InfoRow>

View File

@ -47,17 +47,15 @@ export function initToolIframes() {
if (iframes.size === 0) {
const existingIframes = container.querySelectorAll('iframe')
existingIframes.forEach(iframe => {
try {
const url = new URL(iframe.src)
const port = parseInt(url.port, 10)
const match = iframe.src.match(/localhost:(\d+)/)
if (match && match[1]) {
const port = parseInt(match[1], 10)
const toolName = iframe.dataset.toolName
const appName = iframe.dataset.appName
if (port && toolName) {
if (toolName) {
const cacheKey = appName ? `${toolName}:${appName}` : toolName
iframes.set(cacheKey, { iframe, port })
}
} catch {
// Invalid URL, skip
}
})
}
@ -70,13 +68,11 @@ export function initToolIframes() {
`
}
// Build URL with params using TOES_URL base
// Build URL with params
function buildToolUrl(port: number, params: Record<string, string>): string {
const base = new URL(window.location.origin)
base.port = String(port)
const searchParams = new URLSearchParams(params)
const query = searchParams.toString()
return query ? `${base.origin}?${query}` : base.origin
return query ? `http://localhost:${port}?${query}` : `http://localhost:${port}`
}
// Build cache key from tool name and params

View File

@ -1,7 +1,7 @@
import type { App as SharedApp, AppState } from '@types'
import type { Subprocess } from 'bun'
import { DEFAULT_EMOJI } from '@types'
import { appendFileSync, existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, renameSync, symlinkSync, unlinkSync, writeFileSync } from 'fs'
import { appendFileSync, existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, renameSync, unlinkSync, writeFileSync } from 'fs'
import { join, resolve } from 'path'
import { appLog, hostLog, setApps } from './tui'
@ -84,7 +84,6 @@ export function initApps() {
initPortPool()
setupShutdownHandlers()
rotateLogs()
createAppSymlinks()
discoverApps()
runApps()
}
@ -293,37 +292,6 @@ function allAppDirs() {
.sort()
}
function createAppSymlinks() {
for (const app of readdirSync(APPS_DIR, { withFileTypes: true })) {
if (!app.isDirectory()) continue
const appDir = join(APPS_DIR, app.name)
const currentPath = join(appDir, 'current')
if (existsSync(currentPath)) continue
// Find valid version directories
const versions = readdirSync(appDir, { withFileTypes: true })
.filter(e => {
if (!e.isDirectory()) return false
const pkgPath = join(appDir, e.name, 'package.json')
if (!existsSync(pkgPath)) return false
try {
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'))
return !!pkg.scripts?.toes
} catch {
return false
}
})
.map(e => e.name)
.sort()
.reverse()
const latest = versions[0]
if (latest) {
symlinkSync(latest, currentPath)
}
}
}
function discoverApps() {
for (const dir of allAppDirs()) {
const { pkg, error } = loadApp(dir)

View File

@ -1,4 +1,4 @@
import { allApps, initApps, TOES_URL } from '$apps'
import { allApps, initApps } from '$apps'
import appsRouter from './api/apps'
import syncRouter from './api/sync'
import { Hype } from '@because/hype'
@ -16,9 +16,7 @@ app.get('/tool/:tool', c => {
return c.text(`Tool "${toolName}" not found or not running`, 404)
}
const params = new URLSearchParams(c.req.query()).toString()
const base = new URL(TOES_URL)
base.port = String(tool.port)
const url = params ? `${base.origin}?${params}` : base.origin
const url = params ? `http://localhost:${tool.port}?${params}` : `http://localhost:${tool.port}`
return c.redirect(url)
})