mirror of
https://github.com/jhd3197/ServerKit.git
synced 2026-10-23 03:27:43 +09:00
Add tunnel mode and enhance servers UI
Add ngrok 'tunnel' mode to dev scripts and documentation, plus a large UI overhaul for the Servers pages. - Add .gitattributes and new scripts/setup-wsl.sh helper. - Extend dev.ps1 and dev.sh with a new 'tunnel' mode that starts backend+frontend and launches an ngrok tunnel (supports NGROK_DOMAIN, NGROK_AUTHTOKEN, and sets sensible CORS_ORIGINS). Includes polling of ngrok API, process cleanup, and helpful error messages. - Update docs/LOCAL_DEVELOPMENT.md with an "Exposing Your Local Control Plane (ngrok)" guide and WSL setup notes. - Revamp frontend pages: ServerDetail.jsx and Servers.jsx refactored with new header layouts, status/avatars, status pills, telemetry rows, selectable table rows, bulk actions, confirm dialogs, improved add-server modal, new icons (FolderTinyIcon, MoreIcon), utilities (formatLastSeen, clamp), and accessibility/interaction improvements (menus, selection, copy install command). - Update servers SCSS to support the new layouts and loading states. These changes add a developer-friendly tunnel mode for remote testing and significantly improve the server management UX in the frontend.
This commit is contained in:
1 parent
e9d8af241a
commit
e25cf6f83e
8 files changed
+2504
-345
No files matched your search
@@ -0,0 +1,13 @@
|
||||
* text=auto eol=lf
|
||||
|
||||
*.sh text eol=lf
|
||||
*.ps1 text eol=crlf
|
||||
*.bat text eol=crlf
|
||||
*.cmd text eol=crlf
|
||||
|
||||
*.png binary
|
||||
*.jpg binary
|
||||
*.jpeg binary
|
||||
*.gif binary
|
||||
*.ico binary
|
||||
*.pdf binary
|
||||
@@ -3,19 +3,25 @@
|
||||
.SYNOPSIS
|
||||
ServerKit development launcher and validation tool.
|
||||
.DESCRIPTION
|
||||
Start backend, frontend, or both. Run validation checks.
|
||||
Start backend, frontend, both, or expose backend via an ngrok tunnel.
|
||||
Run validation checks.
|
||||
.PARAMETER Mode
|
||||
Operation mode: start (default), backend, frontend, validate
|
||||
Operation mode: start (default), backend, frontend, tunnel, validate
|
||||
.EXAMPLE
|
||||
.\dev.ps1 # Start backend + frontend
|
||||
.\dev.ps1 backend # Backend only
|
||||
.\dev.ps1 frontend # Frontend only
|
||||
.\dev.ps1 tunnel # Backend + frontend + ngrok tunnel for the backend
|
||||
.\dev.ps1 validate # Run all linters/checks
|
||||
.NOTES
|
||||
Optional environment variables for tunnel mode:
|
||||
NGROK_DOMAIN - reserved ngrok domain (e.g. my-app.ngrok-free.app)
|
||||
NGROK_AUTHTOKEN - ngrok authtoken (alternatively run `ngrok config add-authtoken`)
|
||||
#>
|
||||
|
||||
param(
|
||||
[Parameter(Position = 0)]
|
||||
[ValidateSet('start', 'backend', 'frontend', 'validate')]
|
||||
[ValidateSet('start', 'backend', 'frontend', 'tunnel', 'validate')]
|
||||
[string]$Mode = 'start'
|
||||
)
|
||||
|
||||
@@ -120,6 +126,120 @@ function Start-Both {
|
||||
}
|
||||
}
|
||||
|
||||
function Start-Tunnel {
|
||||
if (-not (Get-Command ngrok -ErrorAction SilentlyContinue)) {
|
||||
Write-Host "Error: ngrok is not installed or not in PATH." -ForegroundColor Red
|
||||
Write-Host ""
|
||||
Write-Host "Install ngrok:"
|
||||
Write-Host " - https://ngrok.com/download"
|
||||
Write-Host " - Windows: choco install ngrok (or scoop install ngrok)"
|
||||
Write-Host " - WSL: snap install ngrok (or download the .tgz)"
|
||||
Write-Host ""
|
||||
Write-Host "Then authenticate once: ngrok config add-authtoken <YOUR_TOKEN>"
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "ServerKit Dev Server (with ngrok tunnel)" -ForegroundColor Cyan
|
||||
Write-Host " Backend: http://localhost:5000"
|
||||
Write-Host " Frontend: http://localhost:5173"
|
||||
Write-Host " Tunnel: exposing backend (port 5000) via ngrok"
|
||||
Write-Host ""
|
||||
Write-Host "NOTE: " -ForegroundColor Yellow -NoNewline
|
||||
Write-Host "Agents and remote callers should use the public ngrok URL"
|
||||
Write-Host " printed below as their --server / control plane URL."
|
||||
Write-Host ""
|
||||
|
||||
if (-not $env:CORS_ORIGINS) {
|
||||
$env:CORS_ORIGINS = 'http://localhost:5173,http://localhost:5000,https://*.ngrok-free.app,https://*.ngrok.app,https://*.ngrok.io'
|
||||
}
|
||||
|
||||
$envSnapshot = @{
|
||||
CORS_ORIGINS = $env:CORS_ORIGINS
|
||||
}
|
||||
|
||||
$backendJob = Start-Job -ScriptBlock {
|
||||
param($dir, $envVars)
|
||||
foreach ($k in $envVars.Keys) { Set-Item -Path "Env:$k" -Value $envVars[$k] }
|
||||
Set-Location $dir
|
||||
if (Test-Path 'venv\Scripts\Activate.ps1') {
|
||||
& 'venv\Scripts\Activate.ps1'
|
||||
}
|
||||
python run.py
|
||||
} -ArgumentList $BackendDir, $envSnapshot
|
||||
|
||||
Start-Sleep -Seconds 2
|
||||
|
||||
$frontendJob = Start-Job -ScriptBlock {
|
||||
param($dir)
|
||||
Set-Location $dir
|
||||
npm run dev
|
||||
} -ArgumentList $FrontendDir
|
||||
|
||||
Start-Sleep -Seconds 1
|
||||
|
||||
$ngrokArgs = @('http', '5000', '--log=stdout')
|
||||
if ($env:NGROK_DOMAIN) { $ngrokArgs += "--domain=$($env:NGROK_DOMAIN)" }
|
||||
if ($env:NGROK_AUTHTOKEN) { $ngrokArgs += "--authtoken=$($env:NGROK_AUTHTOKEN)" }
|
||||
|
||||
$ngrokProc = Start-Process -FilePath 'ngrok' -ArgumentList $ngrokArgs -PassThru -WindowStyle Hidden
|
||||
|
||||
$urlPrinted = $false
|
||||
try {
|
||||
Write-Host "Press Ctrl+C to stop..." -ForegroundColor DarkGray
|
||||
$polls = 0
|
||||
while ($true) {
|
||||
Receive-Job $backendJob -ErrorAction SilentlyContinue
|
||||
Receive-Job $frontendJob -ErrorAction SilentlyContinue
|
||||
|
||||
if (-not $urlPrinted -and $polls -lt 30) {
|
||||
try {
|
||||
$resp = Invoke-RestMethod -Uri 'http://127.0.0.1:4040/api/tunnels' -TimeoutSec 1 -ErrorAction Stop
|
||||
$publicUrl = ($resp.tunnels | Where-Object { $_.public_url -like 'https://*' } | Select-Object -First 1).public_url
|
||||
if ($publicUrl) {
|
||||
Write-Host ""
|
||||
Write-Host "=========================================================" -ForegroundColor Green
|
||||
Write-Host " Public tunnel URL: " -ForegroundColor Green -NoNewline
|
||||
Write-Host $publicUrl -ForegroundColor Cyan
|
||||
Write-Host " Use this as your agent --server / control plane URL." -ForegroundColor Green
|
||||
Write-Host "=========================================================" -ForegroundColor Green
|
||||
Write-Host ""
|
||||
$urlPrinted = $true
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
$polls++
|
||||
}
|
||||
|
||||
if ($backendJob.State -eq 'Failed') {
|
||||
Write-Host "Backend crashed!" -ForegroundColor Red
|
||||
Receive-Job $backendJob
|
||||
break
|
||||
}
|
||||
if ($frontendJob.State -eq 'Failed') {
|
||||
Write-Host "Frontend crashed!" -ForegroundColor Red
|
||||
Receive-Job $frontendJob
|
||||
break
|
||||
}
|
||||
if ($ngrokProc.HasExited) {
|
||||
Write-Host "ngrok exited." -ForegroundColor Red
|
||||
break
|
||||
}
|
||||
Start-Sleep -Seconds 1
|
||||
}
|
||||
}
|
||||
finally {
|
||||
Stop-Job $backendJob -ErrorAction SilentlyContinue
|
||||
Stop-Job $frontendJob -ErrorAction SilentlyContinue
|
||||
Remove-Job $backendJob -Force -ErrorAction SilentlyContinue
|
||||
Remove-Job $frontendJob -Force -ErrorAction SilentlyContinue
|
||||
if ($ngrokProc -and -not $ngrokProc.HasExited) {
|
||||
Stop-Process -Id $ngrokProc.Id -ErrorAction SilentlyContinue
|
||||
}
|
||||
Write-Host "`nStopped." -ForegroundColor Yellow
|
||||
}
|
||||
}
|
||||
|
||||
function Invoke-Check {
|
||||
param(
|
||||
[string]$Name,
|
||||
@@ -234,6 +354,7 @@ function Run-ValidateWatch {
|
||||
switch ($Mode) {
|
||||
'backend' { Start-Backend }
|
||||
'frontend' { Start-Frontend }
|
||||
'tunnel' { Start-Tunnel }
|
||||
'validate' { Run-ValidateWatch }
|
||||
default { Start-Both }
|
||||
}
|
||||
@@ -5,7 +5,12 @@
|
||||
# ./dev.sh Start backend + frontend (default)
|
||||
# ./dev.sh backend Backend only
|
||||
# ./dev.sh frontend Frontend only
|
||||
# ./dev.sh tunnel Start backend + frontend + expose backend via ngrok
|
||||
# ./dev.sh validate Run all linters/checks
|
||||
#
|
||||
# Environment variables:
|
||||
# NGROK_DOMAIN Optional reserved ngrok domain (e.g. my-app.ngrok-free.app)
|
||||
# NGROK_AUTHTOKEN Optional ngrok authtoken (alternatively run `ngrok config add-authtoken`)
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
@@ -83,6 +88,94 @@ start_both() {
|
||||
wait
|
||||
}
|
||||
|
||||
start_tunnel() {
|
||||
if ! command -v ngrok &>/dev/null; then
|
||||
echo -e "${RED}Error:${NC} ngrok is not installed or not in PATH."
|
||||
echo ""
|
||||
echo "Install ngrok:"
|
||||
echo " - https://ngrok.com/download"
|
||||
echo " - macOS: brew install ngrok/ngrok/ngrok"
|
||||
echo " - Linux: snap install ngrok (or download the .tgz)"
|
||||
echo " - Windows: choco install ngrok (or scoop install ngrok)"
|
||||
echo ""
|
||||
echo "Then authenticate once: ngrok config add-authtoken <YOUR_TOKEN>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo -e "${CYAN}ServerKit Dev Server (with ngrok tunnel)${NC}"
|
||||
echo " Backend: http://localhost:5000"
|
||||
echo " Frontend: http://localhost:5173"
|
||||
echo " Tunnel: exposing backend (port 5000) via ngrok"
|
||||
echo ""
|
||||
echo -e "${YELLOW}NOTE:${NC} Agents and remote callers should use the public ngrok URL"
|
||||
echo " printed below as their --server / control plane URL."
|
||||
echo ""
|
||||
|
||||
# Allow CORS from any ngrok domain by default unless caller already set it.
|
||||
export CORS_ORIGINS="${CORS_ORIGINS:-http://localhost:5173,http://localhost:5000,https://*.ngrok-free.app,https://*.ngrok.app,https://*.ngrok.io}"
|
||||
|
||||
cd "$BACKEND_DIR"
|
||||
if [ -f venv/bin/activate ]; then
|
||||
source venv/bin/activate
|
||||
fi
|
||||
python run.py &
|
||||
BACKEND_PID=$!
|
||||
|
||||
sleep 2
|
||||
|
||||
cd "$FRONTEND_DIR"
|
||||
npm run dev &
|
||||
FRONTEND_PID=$!
|
||||
|
||||
sleep 1
|
||||
|
||||
NGROK_ARGS=(http 5000 --log=stdout)
|
||||
if [ -n "${NGROK_DOMAIN:-}" ]; then
|
||||
NGROK_ARGS+=("--domain=$NGROK_DOMAIN")
|
||||
fi
|
||||
if [ -n "${NGROK_AUTHTOKEN:-}" ]; then
|
||||
NGROK_ARGS+=("--authtoken=$NGROK_AUTHTOKEN")
|
||||
fi
|
||||
|
||||
ngrok "${NGROK_ARGS[@]}" &
|
||||
NGROK_PID=$!
|
||||
|
||||
# Poll the local ngrok API for the public URL and print it prominently.
|
||||
(
|
||||
for _ in $(seq 1 20); do
|
||||
sleep 1
|
||||
if command -v curl &>/dev/null; then
|
||||
URL=$(curl -fsS http://127.0.0.1:4040/api/tunnels 2>/dev/null \
|
||||
| grep -oE '"public_url":"https://[^"]+' \
|
||||
| head -n1 \
|
||||
| sed 's/"public_url":"//')
|
||||
if [ -n "$URL" ]; then
|
||||
echo ""
|
||||
echo -e "${GREEN}=========================================================${NC}"
|
||||
echo -e "${GREEN} Public tunnel URL: ${NC}${CYAN}$URL${NC}"
|
||||
echo -e "${GREEN} Use this as your agent --server / control plane URL.${NC}"
|
||||
echo -e "${GREEN}=========================================================${NC}"
|
||||
echo ""
|
||||
break
|
||||
fi
|
||||
fi
|
||||
done
|
||||
) &
|
||||
|
||||
cleanup_tunnel() {
|
||||
echo ""
|
||||
echo -e "${YELLOW}Stopping...${NC}"
|
||||
kill "$BACKEND_PID" "$FRONTEND_PID" "$NGROK_PID" 2>/dev/null || true
|
||||
wait "$BACKEND_PID" "$FRONTEND_PID" "$NGROK_PID" 2>/dev/null || true
|
||||
echo "Stopped."
|
||||
}
|
||||
trap cleanup_tunnel INT TERM
|
||||
|
||||
echo -e "${DIM}Press Ctrl+C to stop...${NC}"
|
||||
wait
|
||||
}
|
||||
|
||||
run_validate() {
|
||||
header "ServerKit Validation Suite"
|
||||
local failed=0
|
||||
@@ -173,6 +266,7 @@ MODE="${1:-start}"
|
||||
case "$MODE" in
|
||||
backend) start_backend ;;
|
||||
frontend) start_frontend ;;
|
||||
tunnel) start_tunnel ;;
|
||||
validate) run_validate_watch ;;
|
||||
start|*) start_both ;;
|
||||
esac
|
||||
@@ -252,3 +252,90 @@ sudo usermod -aG docker $USER
|
||||
# Remove the lock file
|
||||
rm backend/instance/serverkit.db-journal
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Exposing Your Local Control Plane (ngrok)
|
||||
|
||||
When developing locally on Windows/WSL you may want a remote ServerKit agent (or
|
||||
a Docker host on another machine) to connect back to your machine. The `dev.sh`
|
||||
and `dev.ps1` scripts include a `tunnel` mode that starts the backend, frontend,
|
||||
and an ngrok tunnel pointed at the backend (port 5000).
|
||||
|
||||
### Prerequisites
|
||||
|
||||
1. Install ngrok and authenticate once:
|
||||
|
||||
```bash
|
||||
# macOS / Linux / WSL
|
||||
brew install ngrok/ngrok/ngrok # or: snap install ngrok
|
||||
ngrok config add-authtoken <YOUR_TOKEN>
|
||||
```
|
||||
|
||||
```powershell
|
||||
# Windows
|
||||
choco install ngrok # or: scoop install ngrok
|
||||
ngrok config add-authtoken <YOUR_TOKEN>
|
||||
```
|
||||
|
||||
2. (Optional) Reserve a domain in the ngrok dashboard so the URL stays stable
|
||||
across restarts.
|
||||
|
||||
### Start with a tunnel
|
||||
|
||||
```bash
|
||||
# Linux / macOS / WSL
|
||||
./dev.sh tunnel
|
||||
```
|
||||
|
||||
```powershell
|
||||
# Windows
|
||||
.\dev.ps1 tunnel
|
||||
```
|
||||
|
||||
The public ngrok URL is printed in green once the tunnel is up. Use it as the
|
||||
`--server` argument when registering an agent:
|
||||
|
||||
```bash
|
||||
serverkit-agent register \
|
||||
--token "sk_reg_xxx" \
|
||||
--server "https://<your-tunnel>.ngrok-free.app"
|
||||
```
|
||||
|
||||
### Optional environment variables
|
||||
|
||||
| Variable | Purpose |
|
||||
|----------|---------|
|
||||
| `NGROK_DOMAIN` | Use a reserved ngrok domain (e.g. `my-app.ngrok-free.app`). |
|
||||
| `NGROK_AUTHTOKEN` | Override the authtoken (otherwise the one stored by `ngrok config` is used). |
|
||||
| `CORS_ORIGINS` | Override CORS allow-list. The script defaults include `https://*.ngrok-free.app`, `https://*.ngrok.app`, `https://*.ngrok.io`. |
|
||||
|
||||
---
|
||||
|
||||
## Quick Start with WSL2 (Helper Script)
|
||||
|
||||
If you're on Windows and using WSL2 Ubuntu/Debian, you can skip the manual
|
||||
steps above and use the bundled setup helper:
|
||||
|
||||
```bash
|
||||
# from inside WSL, in the cloned repo
|
||||
bash scripts/setup-wsl.sh # full setup
|
||||
bash scripts/setup-wsl.sh --check # dry-run / diagnostics only
|
||||
```
|
||||
|
||||
The script will:
|
||||
|
||||
- Detect WSL and warn if the repo lives under `/mnt/c` (slow file I/O for hot-reload).
|
||||
- Verify Ubuntu/Debian and install missing apt packages
|
||||
(`python3`, `python3-venv`, `python3-pip`, `nodejs`, `npm`, `git`, `curl`).
|
||||
- Create `backend/venv` and install Python requirements.
|
||||
- Run `npm install` in `frontend/`.
|
||||
- Copy `backend/.env.example` to `backend/.env` if missing.
|
||||
- Print WSL-specific tips (localhost forwarding, polling reloader, ngrok).
|
||||
|
||||
Once it finishes:
|
||||
|
||||
```bash
|
||||
./dev.sh # start backend + frontend
|
||||
./dev.sh tunnel # also expose backend via ngrok (see section above)
|
||||
```
|
||||
@@ -152,13 +152,6 @@ const ServerDetail = () => {
|
||||
);
|
||||
}
|
||||
|
||||
const statusColors = {
|
||||
online: '#10B981',
|
||||
offline: '#EF4444',
|
||||
connecting: '#F59E0B',
|
||||
pending: '#6B7280'
|
||||
};
|
||||
|
||||
const tabs = [
|
||||
{ id: 'overview', label: 'Overview' },
|
||||
{ id: 'docker', label: 'Docker' },
|
||||
@@ -168,35 +161,55 @@ const ServerDetail = () => {
|
||||
|
||||
return (
|
||||
<div className="server-detail-page">
|
||||
<div className="page-header">
|
||||
<div className="page-breadcrumb">
|
||||
<Link to="/servers">Servers</Link>
|
||||
<span className="breadcrumb-separator">/</span>
|
||||
<span>{server.name}</span>
|
||||
</div>
|
||||
<div className="page-header-content">
|
||||
<div className="server-title">
|
||||
<div
|
||||
className="status-dot large"
|
||||
style={{ backgroundColor: statusColors[server.status] }}
|
||||
title={server.status}
|
||||
/>
|
||||
<h1>{server.name}</h1>
|
||||
<div className="page-breadcrumb">
|
||||
<Link to="/servers">Servers</Link>
|
||||
<span className="breadcrumb-separator">/</span>
|
||||
<span>{server.name}</span>
|
||||
</div>
|
||||
|
||||
<header className="server-detail-header">
|
||||
<div className="server-detail-header__main">
|
||||
<div className={`server-detail-header__avatar server-detail-header__avatar--${server.status || 'pending'}`}>
|
||||
{(server.name || '?').charAt(0).toUpperCase()}
|
||||
</div>
|
||||
<div className="server-detail-header__identity">
|
||||
<div className="server-detail-header__title-row">
|
||||
<h1>{server.name}</h1>
|
||||
<span className={`status-pill status-pill--${server.status || 'pending'}`}>
|
||||
<span className="status-pill__dot" />
|
||||
{server.status || 'pending'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="server-detail-header__meta">
|
||||
<span className="mono">{server.hostname || server.ip_address || 'No endpoint configured'}</span>
|
||||
{server.group_name && (
|
||||
<span className="server-detail-header__chip"><FolderTinyIcon /> {server.group_name}</span>
|
||||
)}
|
||||
{server.os_type && <span className="server-detail-header__chip">{server.os_type}</span>}
|
||||
{server.agent_version && <span className="server-detail-header__chip">agent {server.agent_version}</span>}
|
||||
{server.last_seen && (
|
||||
<span className="server-detail-header__chip">
|
||||
last seen {new Date(server.last_seen).toLocaleString()}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{server.description && (
|
||||
<p className="server-detail-header__description">{server.description}</p>
|
||||
)}
|
||||
</div>
|
||||
<p className="page-description">
|
||||
{server.hostname || server.ip_address}
|
||||
{server.description && ` - ${server.description}`}
|
||||
</p>
|
||||
</div>
|
||||
<div className="page-header-actions">
|
||||
<div className="server-detail-header__actions">
|
||||
<button className="btn btn-secondary" onClick={handlePingServer}>
|
||||
<RefreshIcon /> Ping
|
||||
</button>
|
||||
<Link className="btn btn-secondary" to={`/servers/${id}/docker`}>
|
||||
Docker
|
||||
</Link>
|
||||
<button className="btn btn-primary" onClick={handleGenerateToken}>
|
||||
<KeyIcon /> Generate Token
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="server-detail-tabs">
|
||||
{tabs.map(t => (
|
||||
@@ -1181,6 +1194,12 @@ Install-ServerKitAgent -Server "${window.location.origin}" -Token "${token}"` :
|
||||
};
|
||||
|
||||
// Icons
|
||||
const FolderTinyIcon = () => (
|
||||
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
|
||||
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const RefreshIcon = () => (
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<polyline points="23 4 23 10 17 10"/>
|
||||
|
||||
+587
-277
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,241 @@
|
||||
#!/usr/bin/env bash
|
||||
# ServerKit WSL setup helper.
|
||||
#
|
||||
# One-shot installer for Windows users running ServerKit dev inside
|
||||
# WSL2 (Ubuntu/Debian). Installs system packages, creates the Python
|
||||
# venv, installs npm dependencies, prepares .env files, and verifies
|
||||
# common WSL pitfalls (filesystem location, localhost binding,
|
||||
# polling-based reloader).
|
||||
#
|
||||
# Usage:
|
||||
# bash scripts/setup-wsl.sh # full setup
|
||||
# bash scripts/setup-wsl.sh --check # diagnostics only, no install
|
||||
#
|
||||
# After this finishes:
|
||||
# ./dev.sh # start backend + frontend
|
||||
# ./dev.sh tunnel # also expose backend through ngrok
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
CYAN='\033[0;36m'
|
||||
GREEN='\033[0;32m'
|
||||
RED='\033[0;31m'
|
||||
YELLOW='\033[1;33m'
|
||||
DIM='\033[2m'
|
||||
NC='\033[0m'
|
||||
|
||||
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
BACKEND_DIR="$PROJECT_ROOT/backend"
|
||||
FRONTEND_DIR="$PROJECT_ROOT/frontend"
|
||||
CHECK_ONLY=0
|
||||
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--check|-c) CHECK_ONLY=1 ;;
|
||||
--help|-h)
|
||||
sed -n '2,16p' "$0" | sed 's/^# \{0,1\}//'
|
||||
exit 0
|
||||
;;
|
||||
*) echo -e "${RED}Unknown argument:${NC} $arg"; exit 2 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
header() { echo ""; echo -e "${CYAN}=== $1 ===${NC}"; }
|
||||
ok() { echo -e " ${GREEN}OK${NC} $1"; }
|
||||
warn() { echo -e " ${YELLOW}WARN${NC} $1"; }
|
||||
fail() { echo -e " ${RED}FAIL${NC} $1"; }
|
||||
info() { echo -e " ${DIM}$1${NC}"; }
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Diagnostics
|
||||
# -----------------------------------------------------------------------------
|
||||
header "Environment diagnostics"
|
||||
|
||||
IN_WSL=0
|
||||
if grep -qiE 'microsoft|wsl' /proc/version 2>/dev/null; then
|
||||
IN_WSL=1
|
||||
ok "Running inside WSL ($(uname -r))"
|
||||
else
|
||||
warn "Not detected as WSL. This script is intended for WSL2 but will still try to run."
|
||||
fi
|
||||
|
||||
if [ "$IN_WSL" -eq 1 ]; then
|
||||
case "$PROJECT_ROOT" in
|
||||
/mnt/*)
|
||||
warn "Repo is on the Windows filesystem ($PROJECT_ROOT)."
|
||||
info "File I/O and Vite/Flask reloaders are noticeably slower here."
|
||||
info "For best performance clone the repo into the Linux home, e.g.:"
|
||||
info " cd ~ && git clone https://github.com/jhd3197/ServerKit.git"
|
||||
;;
|
||||
*)
|
||||
ok "Repo is on the Linux filesystem ($PROJECT_ROOT)."
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# Distro detection
|
||||
DISTRO_ID="unknown"
|
||||
if [ -r /etc/os-release ]; then
|
||||
# shellcheck disable=SC1091
|
||||
. /etc/os-release
|
||||
DISTRO_ID="$ID"
|
||||
ok "Distro: $PRETTY_NAME"
|
||||
fi
|
||||
|
||||
if [ "$DISTRO_ID" != "ubuntu" ] && [ "$DISTRO_ID" != "debian" ]; then
|
||||
warn "This script targets Ubuntu/Debian. Other distros may need manual adjustments."
|
||||
fi
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Required tools
|
||||
# -----------------------------------------------------------------------------
|
||||
header "Required tooling"
|
||||
|
||||
NEED_APT=()
|
||||
need() {
|
||||
local cmd="$1" pkg="$2"
|
||||
if command -v "$cmd" &>/dev/null; then
|
||||
ok "$cmd ($(command -v "$cmd"))"
|
||||
else
|
||||
warn "$cmd not found (needs apt package: $pkg)"
|
||||
NEED_APT+=("$pkg")
|
||||
fi
|
||||
}
|
||||
|
||||
need python3 python3
|
||||
need pip3 python3-pip
|
||||
need node nodejs
|
||||
need npm npm
|
||||
need git git
|
||||
need curl curl
|
||||
|
||||
# venv module
|
||||
if python3 -c 'import venv' 2>/dev/null; then
|
||||
ok "python3-venv module available"
|
||||
else
|
||||
warn "python3-venv module missing"
|
||||
NEED_APT+=("python3-venv")
|
||||
fi
|
||||
|
||||
# Node version sanity
|
||||
if command -v node &>/dev/null; then
|
||||
NODE_MAJOR="$(node -v 2>/dev/null | sed -E 's/^v([0-9]+).*/\1/')"
|
||||
if [ -n "$NODE_MAJOR" ] && [ "$NODE_MAJOR" -lt 18 ]; then
|
||||
warn "Node.js version is $(node -v); ServerKit expects 18+ (20+ recommended)."
|
||||
info "Upgrade with NodeSource: https://github.com/nodesource/distributions"
|
||||
fi
|
||||
fi
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Install missing apt packages
|
||||
# -----------------------------------------------------------------------------
|
||||
if [ "${#NEED_APT[@]}" -gt 0 ]; then
|
||||
if [ "$CHECK_ONLY" -eq 1 ]; then
|
||||
warn "Missing packages (would install): ${NEED_APT[*]}"
|
||||
else
|
||||
header "Installing missing apt packages"
|
||||
info "Running: sudo apt update && sudo apt install -y ${NEED_APT[*]}"
|
||||
sudo apt update
|
||||
sudo apt install -y "${NEED_APT[@]}"
|
||||
fi
|
||||
fi
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Backend venv + requirements
|
||||
# -----------------------------------------------------------------------------
|
||||
header "Backend (Python venv)"
|
||||
|
||||
if [ ! -d "$BACKEND_DIR" ]; then
|
||||
fail "backend directory not found at $BACKEND_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$CHECK_ONLY" -eq 1 ]; then
|
||||
if [ -d "$BACKEND_DIR/venv" ]; then
|
||||
ok "venv exists at backend/venv"
|
||||
else
|
||||
warn "venv not yet created (run without --check to create it)"
|
||||
fi
|
||||
else
|
||||
if [ ! -d "$BACKEND_DIR/venv" ]; then
|
||||
info "Creating venv in backend/venv"
|
||||
python3 -m venv "$BACKEND_DIR/venv"
|
||||
fi
|
||||
# shellcheck disable=SC1091
|
||||
source "$BACKEND_DIR/venv/bin/activate"
|
||||
info "Upgrading pip"
|
||||
pip install --quiet --upgrade pip
|
||||
info "Installing backend requirements"
|
||||
pip install --quiet -r "$BACKEND_DIR/requirements.txt"
|
||||
deactivate
|
||||
ok "Backend dependencies installed"
|
||||
fi
|
||||
|
||||
# .env
|
||||
if [ ! -f "$BACKEND_DIR/.env" ] && [ -f "$BACKEND_DIR/.env.example" ]; then
|
||||
if [ "$CHECK_ONLY" -eq 1 ]; then
|
||||
warn "backend/.env missing (would copy from .env.example)"
|
||||
else
|
||||
cp "$BACKEND_DIR/.env.example" "$BACKEND_DIR/.env"
|
||||
ok "Created backend/.env from .env.example"
|
||||
info "Edit backend/.env to set SECRET_KEY / JWT_SECRET_KEY before any production-like use."
|
||||
fi
|
||||
else
|
||||
[ -f "$BACKEND_DIR/.env" ] && ok "backend/.env present"
|
||||
fi
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Frontend npm install
|
||||
# -----------------------------------------------------------------------------
|
||||
header "Frontend (npm)"
|
||||
|
||||
if [ ! -d "$FRONTEND_DIR" ]; then
|
||||
fail "frontend directory not found at $FRONTEND_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$CHECK_ONLY" -eq 1 ]; then
|
||||
if [ -d "$FRONTEND_DIR/node_modules" ]; then
|
||||
ok "node_modules present"
|
||||
else
|
||||
warn "node_modules missing (would run npm install)"
|
||||
fi
|
||||
else
|
||||
if [ ! -d "$FRONTEND_DIR/node_modules" ]; then
|
||||
info "Running npm install in frontend/"
|
||||
(cd "$FRONTEND_DIR" && npm install)
|
||||
else
|
||||
info "node_modules already present (skipping npm install)"
|
||||
fi
|
||||
ok "Frontend dependencies ready"
|
||||
fi
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# WSL-specific advice
|
||||
# -----------------------------------------------------------------------------
|
||||
if [ "$IN_WSL" -eq 1 ]; then
|
||||
header "WSL tips"
|
||||
info "Backend already binds to 0.0.0.0 and uses a stat-based reloader (good for WSL)."
|
||||
info "Vite dev server is reachable from Windows at http://localhost:5173 thanks to WSL2 localhost forwarding."
|
||||
info "If hot-reload feels stuck, ensure the repo lives under your Linux home (~) rather than /mnt/c."
|
||||
if command -v ngrok &>/dev/null; then
|
||||
ok "ngrok is installed (use ./dev.sh tunnel to expose the backend)"
|
||||
else
|
||||
info "Install ngrok and then ./dev.sh tunnel will expose the backend so remote agents can connect."
|
||||
fi
|
||||
fi
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Done
|
||||
# -----------------------------------------------------------------------------
|
||||
header "Done"
|
||||
if [ "$CHECK_ONLY" -eq 1 ]; then
|
||||
echo "Diagnostics complete. Re-run without --check to install anything missing."
|
||||
else
|
||||
echo -e " ${GREEN}Setup complete.${NC}"
|
||||
echo ""
|
||||
echo "Next steps:"
|
||||
echo " 1. (Optional) edit backend/.env to set unique SECRET_KEY / JWT_SECRET_KEY"
|
||||
echo " 2. ./dev.sh # start backend + frontend"
|
||||
echo " 3. ./dev.sh tunnel # also expose backend via ngrok"
|
||||
fi
|
||||
Reference in new issue
Block a user