Compare commits

..

No commits in common. "21c6c27c9238c2940593d0dbf826aa664f6a4f3c" and "926e57e34e68570a8ef4f136be0d3ffc1dd35e49" have entirely different histories.

3 changed files with 18 additions and 55 deletions

View File

@ -34,18 +34,6 @@ Once complete, visit `http://<hostname>.local` on your local network.
- https://toes.local web UI for managing your projects. - https://toes.local web UI for managing your projects.
- `toes` CLI for managing your projects. - `toes` CLI for managing your projects.
## ssh cli
You can manage your toes server from any machine on your network over SSH — no install required.
```bash
ssh cli@toes.local # interactive shell with tab completion
ssh cli@toes.local list # run a single command
ssh cli@toes.local logs fog # stream logs for an app
```
The `cli` user's login shell is the `toes` binary itself. No password is needed. With no arguments, you get an interactive REPL. With arguments, it runs the command and exits.
## cli configuration ## cli configuration
by default, the CLI connects to `localhost:3000` in dev and `toes.local:80` in production. by default, the CLI connects to `localhost:3000` in dev and `toes.local:80` in production.

View File

@ -112,15 +112,6 @@ for app_dir in "$DEST"/apps/*/; do
rm -rf "$APPS_DIR/$app/.git" rm -rf "$APPS_DIR/$app/.git"
done done
# ── CLI + SSH ────────────────────────────────────────────
info "Installing CLI"
quiet bun run cli:build
sudo cp dist/toes /usr/local/bin/toes
info "Setting up SSH access"
quiet sudo bash "$DEST/scripts/setup-ssh.sh"
# ── Systemd ────────────────────────────────────────────── # ── Systemd ──────────────────────────────────────────────
info "Installing toes service" info "Installing toes service"
@ -140,7 +131,6 @@ echo " ${b}${g}🐾 toes $VERSION is up!${r}"
echo " ${d}─────────────────────────────${r}" echo " ${d}─────────────────────────────${r}"
echo "" echo ""
echo " Dashboard: ${c}http://$(hostname).local${r}" echo " Dashboard: ${c}http://$(hostname).local${r}"
echo " SSH CLI: ${c}ssh cli@$(hostname).local${r}"
echo "" echo ""
echo " ${d}Grab the CLI:${r}" echo " ${d}Grab the CLI:${r}"
echo " ${c}curl -fsSL http://$(hostname).local/install | bash${r}" echo " ${c}curl -fsSL http://$(hostname).local/install | bash${r}"

View File

@ -103,7 +103,7 @@ let _appDiskCache: Record<string, number> = {}
let _appDiskLastUpdate = 0 let _appDiskLastUpdate = 0
const DISK_CACHE_TTL = 30000 const DISK_CACHE_TTL = 30000
async function getAppMetrics(): Promise<Record<string, AppMetrics>> { function getAppMetrics(): Record<string, AppMetrics> {
const apps = allApps() const apps = allApps()
const running = apps.filter(a => a.proc?.pid) const running = apps.filter(a => a.proc?.pid)
const result: Record<string, AppMetrics> = {} const result: Record<string, AppMetrics> = {}
@ -117,10 +117,8 @@ async function getAppMetrics(): Promise<Record<string, AppMetrics>> {
if (pidToName.size > 0) { if (pidToName.size > 0) {
try { try {
const pids = [...pidToName.keys()].join(',') const pids = [...pidToName.keys()].join(',')
const proc = Bun.spawn(['ps', '-o', 'pid=,%cpu=,rss=', '-p', pids], { stdout: 'pipe', stderr: 'ignore' }) const ps = Bun.spawnSync(['ps', '-o', 'pid=,%cpu=,rss=', '-p', pids])
const output = await new Response(proc.stdout).text() for (const line of ps.stdout.toString().split('\n')) {
await proc.exited
for (const line of output.split('\n')) {
const parts = line.trim().split(/\s+/) const parts = line.trim().split(/\s+/)
if (parts.length < 3) continue if (parts.length < 3) continue
const pid = parseInt(parts[0]!, 10) const pid = parseInt(parts[0]!, 10)
@ -137,21 +135,12 @@ async function getAppMetrics(): Promise<Record<string, AppMetrics>> {
if (now - _appDiskLastUpdate > DISK_CACHE_TTL) { if (now - _appDiskLastUpdate > DISK_CACHE_TTL) {
_appDiskLastUpdate = now _appDiskLastUpdate = now
_appDiskCache = {} _appDiskCache = {}
const duResults = await Promise.all( for (const app of apps) {
apps.map(async app => { try {
try { const du = Bun.spawnSync(['du', '-sk', join(APPS_DIR, app.name)])
const proc = Bun.spawn(['du', '-sk', join(APPS_DIR, app.name)], { stdout: 'pipe', stderr: 'ignore' }) const kb = parseInt(du.stdout.toString().trim().split('\t')[0]!, 10)
const output = await new Response(proc.stdout).text() if (kb) _appDiskCache[app.name] = kb * 1024
await proc.exited } catch {}
const kb = parseInt(output.trim().split('\t')[0]!, 10)
return { name: app.name, bytes: kb ? kb * 1024 : 0 }
} catch {
return { name: app.name, bytes: 0 }
}
})
)
for (const { name, bytes } of duResults) {
if (bytes) _appDiskCache[name] = bytes
} }
} }
@ -165,30 +154,26 @@ async function getAppMetrics(): Promise<Record<string, AppMetrics>> {
} }
// Get current system metrics // Get current system metrics
router.get('/metrics', async c => { router.get('/metrics', c => {
const metrics: SystemMetrics = { const metrics: SystemMetrics = {
cpu: getCpuUsage(), cpu: getCpuUsage(),
ram: getMemoryUsage(), ram: getMemoryUsage(),
disk: getDiskUsage(), disk: getDiskUsage(),
apps: await getAppMetrics(), apps: getAppMetrics(),
} }
return c.json(metrics) return c.json(metrics)
}) })
// SSE stream for real-time metrics (updates every 2s) // SSE stream for real-time metrics (updates every 2s)
router.sse('/metrics/stream', (send) => { router.sse('/metrics/stream', (send) => {
let queue = Promise.resolve()
const sendMetrics = () => { const sendMetrics = () => {
queue = queue.then(async () => { const metrics: SystemMetrics = {
const metrics: SystemMetrics = { cpu: getCpuUsage(),
cpu: getCpuUsage(), ram: getMemoryUsage(),
ram: getMemoryUsage(), disk: getDiskUsage(),
disk: getDiskUsage(), apps: getAppMetrics(),
apps: await getAppMetrics(), }
} send(metrics)
await send(metrics)
})
} }
// Initial send // Initial send