diff --git a/src/client/components/DashboardLanding.tsx b/src/client/components/DashboardLanding.tsx
index a771f83..6899adc 100644
--- a/src/client/components/DashboardLanding.tsx
+++ b/src/client/components/DashboardLanding.tsx
@@ -5,6 +5,7 @@ import {
AppSelectorChevron,
DashboardContainer,
DashboardHeader,
+ DashboardInstallCmd,
DashboardTitle,
StatusDot,
StatusDotLink,
@@ -35,7 +36,9 @@ export function DashboardLanding({ render }: { render: () => void }) {
)}
- {/*Your personal web appliance*/}
+
+ curl -fsSL {location.origin}/install | bash
+
diff --git a/src/client/styles/index.ts b/src/client/styles/index.ts
index f3e933d..3275071 100644
--- a/src/client/styles/index.ts
+++ b/src/client/styles/index.ts
@@ -27,6 +27,7 @@ export {
ClickableAppName,
DashboardContainer,
DashboardHeader,
+ DashboardInstallCmd,
DashboardSubtitle,
DashboardTitle,
HamburgerButton,
diff --git a/src/client/styles/layout.ts b/src/client/styles/layout.ts
index 6492cde..a223607 100644
--- a/src/client/styles/layout.ts
+++ b/src/client/styles/layout.ts
@@ -229,6 +229,14 @@ export const DashboardTitle = define('DashboardTitle', {
},
})
+export const DashboardInstallCmd = define('DashboardInstallCmd', {
+ base: 'code',
+ fontSize: 13,
+ opacity: 0.6,
+ userSelect: 'all',
+ cursor: 'text',
+})
+
export const DashboardSubtitle = define('DashboardSubtitle', {
fontSize: 16,
color: theme('colors-textMuted'),
diff --git a/src/server/index.tsx b/src/server/index.tsx
index 072a2ea..f8f40a2 100644
--- a/src/server/index.tsx
+++ b/src/server/index.tsx
@@ -6,9 +6,9 @@ import syncRouter from './api/sync'
import systemRouter from './api/system'
import { Hype } from '@because/hype'
import { cleanupStalePublishers } from './mdns'
+import { extractSubdomain, proxySubdomain, proxyWebSocket, websocket } from './proxy'
import type { Server } from 'bun'
import type { WsData } from './proxy'
-import { extractSubdomain, proxySubdomain, proxyWebSocket, websocket } from './proxy'
const app = new Hype({ layout: false, logging: !!process.env.DEBUG })
@@ -59,6 +59,31 @@ app.all('/api/tools/:tool/:path{.+}', async c => {
})
})
+const DIST_DIR = import.meta.dir + '/../../dist'
+const INSTALL_SCRIPT = await Bun.file(import.meta.dir + '/install.sh').text()
+
+// Install script: curl -fsSL http://toes.local/install | bash
+app.get('/install', c => {
+ if (!TOES_URL) return c.text('TOES_URL is not configured', 500)
+ const script = INSTALL_SCRIPT.replace('__TOES_URL__', TOES_URL)
+ return c.text(script, 200, { 'content-type': 'text/plain' })
+})
+
+// Serve built CLI binaries from dist/
+app.get('/dist/:file', async c => {
+ const file = c.req.param('file')
+ if (!file || file.includes('/') || file.includes('..')) {
+ return c.text('Not found', 404)
+ }
+ const bunFile = Bun.file(`${DIST_DIR}/${file}`)
+ if (!(await bunFile.exists())) {
+ return c.text(`Binary "${file}" not found — run cli:build:all on the server`, 404)
+ }
+ return new Response(bunFile, {
+ headers: { 'content-type': 'application/octet-stream' },
+ })
+})
+
cleanupStalePublishers()
await initApps()
diff --git a/src/server/install.sh b/src/server/install.sh
new file mode 100644
index 0000000..eb10d76
--- /dev/null
+++ b/src/server/install.sh
@@ -0,0 +1,38 @@
+#!/usr/bin/env bash
+set -e
+
+OS=$(uname -s | tr '[:upper:]' '[:lower:]')
+ARCH=$(uname -m)
+
+case "$OS" in
+ darwin) OS=macos ;;
+ linux) OS=linux ;;
+ *) echo "Unsupported OS: $OS" >&2; exit 1 ;;
+esac
+
+case "$ARCH" in
+ x86_64) ARCH=x64 ;;
+ arm64|aarch64) ARCH=arm64 ;;
+ *) echo "Unsupported arch: $ARCH" >&2; exit 1 ;;
+esac
+
+BINARY="toes-${OS}-${ARCH}"
+URL="__TOES_URL__/dist/${BINARY}"
+
+if [ -n "$TOES_INSTALL_DIR" ]; then
+ INSTALL_DIR="$TOES_INSTALL_DIR"
+elif [ -d /usr/local/bin ] && [ -w /usr/local/bin ]; then
+ INSTALL_DIR=/usr/local/bin
+else
+ INSTALL_DIR="$HOME/.local/bin"
+fi
+mkdir -p "$INSTALL_DIR"
+
+echo "Downloading toes CLI (${OS}/${ARCH})..."
+curl -fsSL "$URL" -o "$INSTALL_DIR/toes"
+chmod +x "$INSTALL_DIR/toes"
+echo "Installed toes to $INSTALL_DIR/toes"
+
+if ! echo "$PATH" | tr ':' '\n' | grep -qx "$INSTALL_DIR"; then
+ echo "Add $INSTALL_DIR to your PATH to use toes globally"
+fi