Compare commits
No commits in common. "21c6c27c9238c2940593d0dbf826aa664f6a4f3c" and "926e57e34e68570a8ef4f136be0d3ffc1dd35e49" have entirely different histories.
21c6c27c92
...
926e57e34e
12
README.md
12
README.md
|
|
@ -34,18 +34,6 @@ Once complete, visit `http://<hostname>.local` on your local network.
|
|||
- https://toes.local web UI 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
|
||||
|
||||
by default, the CLI connects to `localhost:3000` in dev and `toes.local:80` in production.
|
||||
|
|
|
|||
|
|
@ -112,15 +112,6 @@ for app_dir in "$DEST"/apps/*/; do
|
|||
rm -rf "$APPS_DIR/$app/.git"
|
||||
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 ──────────────────────────────────────────────
|
||||
|
||||
info "Installing toes service"
|
||||
|
|
@ -140,7 +131,6 @@ echo " ${b}${g}🐾 toes $VERSION is up!${r}"
|
|||
echo " ${d}─────────────────────────────${r}"
|
||||
echo ""
|
||||
echo " Dashboard: ${c}http://$(hostname).local${r}"
|
||||
echo " SSH CLI: ${c}ssh cli@$(hostname).local${r}"
|
||||
echo ""
|
||||
echo " ${d}Grab the CLI:${r}"
|
||||
echo " ${c}curl -fsSL http://$(hostname).local/install | bash${r}"
|
||||
|
|
|
|||
|
|
@ -103,7 +103,7 @@ let _appDiskCache: Record<string, number> = {}
|
|||
let _appDiskLastUpdate = 0
|
||||
const DISK_CACHE_TTL = 30000
|
||||
|
||||
async function getAppMetrics(): Promise<Record<string, AppMetrics>> {
|
||||
function getAppMetrics(): Record<string, AppMetrics> {
|
||||
const apps = allApps()
|
||||
const running = apps.filter(a => a.proc?.pid)
|
||||
const result: Record<string, AppMetrics> = {}
|
||||
|
|
@ -117,10 +117,8 @@ async function getAppMetrics(): Promise<Record<string, AppMetrics>> {
|
|||
if (pidToName.size > 0) {
|
||||
try {
|
||||
const pids = [...pidToName.keys()].join(',')
|
||||
const proc = Bun.spawn(['ps', '-o', 'pid=,%cpu=,rss=', '-p', pids], { stdout: 'pipe', stderr: 'ignore' })
|
||||
const output = await new Response(proc.stdout).text()
|
||||
await proc.exited
|
||||
for (const line of output.split('\n')) {
|
||||
const ps = Bun.spawnSync(['ps', '-o', 'pid=,%cpu=,rss=', '-p', pids])
|
||||
for (const line of ps.stdout.toString().split('\n')) {
|
||||
const parts = line.trim().split(/\s+/)
|
||||
if (parts.length < 3) continue
|
||||
const pid = parseInt(parts[0]!, 10)
|
||||
|
|
@ -137,21 +135,12 @@ async function getAppMetrics(): Promise<Record<string, AppMetrics>> {
|
|||
if (now - _appDiskLastUpdate > DISK_CACHE_TTL) {
|
||||
_appDiskLastUpdate = now
|
||||
_appDiskCache = {}
|
||||
const duResults = await Promise.all(
|
||||
apps.map(async app => {
|
||||
try {
|
||||
const proc = Bun.spawn(['du', '-sk', join(APPS_DIR, app.name)], { stdout: 'pipe', stderr: 'ignore' })
|
||||
const output = await new Response(proc.stdout).text()
|
||||
await proc.exited
|
||||
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
|
||||
for (const app of apps) {
|
||||
try {
|
||||
const du = Bun.spawnSync(['du', '-sk', join(APPS_DIR, app.name)])
|
||||
const kb = parseInt(du.stdout.toString().trim().split('\t')[0]!, 10)
|
||||
if (kb) _appDiskCache[app.name] = kb * 1024
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -165,30 +154,26 @@ async function getAppMetrics(): Promise<Record<string, AppMetrics>> {
|
|||
}
|
||||
|
||||
// Get current system metrics
|
||||
router.get('/metrics', async c => {
|
||||
router.get('/metrics', c => {
|
||||
const metrics: SystemMetrics = {
|
||||
cpu: getCpuUsage(),
|
||||
ram: getMemoryUsage(),
|
||||
disk: getDiskUsage(),
|
||||
apps: await getAppMetrics(),
|
||||
apps: getAppMetrics(),
|
||||
}
|
||||
return c.json(metrics)
|
||||
})
|
||||
|
||||
// SSE stream for real-time metrics (updates every 2s)
|
||||
router.sse('/metrics/stream', (send) => {
|
||||
let queue = Promise.resolve()
|
||||
|
||||
const sendMetrics = () => {
|
||||
queue = queue.then(async () => {
|
||||
const metrics: SystemMetrics = {
|
||||
cpu: getCpuUsage(),
|
||||
ram: getMemoryUsage(),
|
||||
disk: getDiskUsage(),
|
||||
apps: await getAppMetrics(),
|
||||
}
|
||||
await send(metrics)
|
||||
})
|
||||
const metrics: SystemMetrics = {
|
||||
cpu: getCpuUsage(),
|
||||
ram: getMemoryUsage(),
|
||||
disk: getDiskUsage(),
|
||||
apps: getAppMetrics(),
|
||||
}
|
||||
send(metrics)
|
||||
}
|
||||
|
||||
// Initial send
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user