feat: remove nextjs production runtime (#1389)

This commit is contained in:
Carlos Florêncio
2026-06-18 18:36:48 +01:00
committed by GitHub
parent 2781556ead
commit aee1846cd5
368 changed files with 10228 additions and 3306 deletions
+3 -3
View File
@@ -28,7 +28,7 @@
# scripts/dev-isolated [options]
#
# Options:
# --web Also start the Next.js dev frontend (default: backend only)
# --web Also start the Vite dev frontend (default: backend only)
# --backend-port N Force the backend port (default: auto from base 48429)
# --web-port N Force the web port (default: auto from base 47429)
# --agentctl-port N Force the agentctl base port (default: auto from base 49429)
@@ -281,10 +281,10 @@ if [[ "$healthy" -ne 1 ]]; then
exit 1
fi
# --- Optionally launch the web frontend (Next.js dev) ---
# --- Optionally launch the web frontend (Vite dev) ---
WEB_STARTED=0
if [[ "$WANT_WEB" -eq 1 ]]; then
echo "dev-isolated: starting web (next dev) on :$WEB_PORT ..."
echo "dev-isolated: starting web (vite dev) on :$WEB_PORT ..."
env \
NEXT_PUBLIC_KANDEV_API_PORT="$BACKEND_PORT" \
KANDEV_API_BASE_URL="$BACKEND_URL" \
+16 -70
View File
@@ -7,10 +7,9 @@
#
# Sources:
# - backend PID + listening port: `ss -tlnp` (falls back to `lsof`)
# - web port: the separate `next-server` listener pointing at this backend
# (matched via its KANDEV_API_BASE_URL / NEXT_PUBLIC_KANDEV_API_PORT, port
# read from `ss`); falls back to the backend's KANDEV_WEB_INTERNAL_URL
# (else `?`). The backend's own port is never the web port.
# - web port: a separate Vite dev listener from KANDEV_WEB_INTERNAL_URL when
# configured and reachable; otherwise the backend port, because production
# serves the SPA from the Go backend.
# - agentctl port: KANDEV_AGENT_STANDALONE_PORT / AGENTCTL_PORT in environ,
# else the `-port=` flag of a child `agentctl` process (else `?`)
# - HOME_DIR: KANDEV_HOME_DIR in environ, else HOME, else `?`
@@ -97,75 +96,22 @@ port_has_listener() {
# Resolve the web port for a backend.
#
# The web UI is served by a SEPARATE `next-server` process, not the backend, so
# the backend's own listening port is never the web port. We resolve in two
# steps, preferring the authoritative source:
#
# 1. The next-server process that points back at THIS backend. Each web
# process is launched with KANDEV_API_BASE_URL=http://host:<backend_port>
# (and/or NEXT_PUBLIC_KANDEV_API_PORT=<backend_port>) and its own
# PORT=<web_port>. We find the next-server whose backend-pointing env
# matches this backend's port, then read its ACTUAL listening port from ss
# (authoritative — the real socket, not an env hint).
# 2. Fallback: the backend's KANDEV_WEB_INTERNAL_URL=http://host:<web_port>,
# which the backend uses to reach the web server. Parse the trailing
# :<port> (the host:port pair, never the scheme's colon).
#
# If neither resolves, print `?` rather than guessing a wrong port (e.g. the
# backend port from PORT / NEXT_PUBLIC_KANDEV_API_PORT, which are the backend's).
# Production serves the SPA from the backend port. In development, a separate
# Vite server may still be configured via KANDEV_WEB_INTERNAL_URL; report that
# port only when something is actually listening there.
resolve_web_port() {
local backend_pid="$1" backend_port="${2:-}" port
port="$(resolve_web_port_via_next_server "$backend_port")"
if [[ -z "$port" ]]; then
local url cand=""
url="$(read_environ_var "$backend_pid" KANDEV_WEB_INTERNAL_URL)"
# Match the LAST :<port> so an IPv6 host or path can't fool us; the trailing
# host:port is what we want, not the scheme separator (http:).
if [[ "$url" =~ :([0-9]+)([/?#].*)?$ ]]; then
cand="${BASH_REMATCH[1]}"
fi
# KANDEV_WEB_INTERNAL_URL is the web URL the backend was CONFIGURED with — it
# may name a web port that never started (e.g. a backend-only dev-isolated
# run). Only trust it if something is actually listening there; otherwise the
# web UI isn't reachable, so report `?` rather than a dead port.
if [[ -n "$cand" ]] && port_has_listener "$cand"; then
port="$cand"
fi
local url cand=""
url="$(read_environ_var "$backend_pid" KANDEV_WEB_INTERNAL_URL)"
# Match the LAST :<port> so an IPv6 host or path can't fool us; the trailing
# host:port is what we want, not the scheme separator (http:).
if [[ "$url" =~ :([0-9]+)([/?#].*)?$ ]]; then
cand="${BASH_REMATCH[1]}"
fi
printf '%s\n' "${port:-?}"
}
# Find the next-server process bound to this backend and return its listening
# port (the web port). Empty output means "not found".
resolve_web_port_via_next_server() {
local backend_port="$1"
[[ -n "$backend_port" ]] || return 0
command -v ss >/dev/null 2>&1 || return 0
# Enumerate listening next-server sockets as "pid port" pairs.
local pid web_port api_port api_url
while read -r _ _ _ local_addr _ procinfo; do
case "$procinfo" in
*'next-server'*) ;;
*) continue ;;
esac
# sed (not grep) so a no-match yields empty + exit 0 instead of tripping
# `set -o pipefail` when a socket line carries no pid= field.
pid="$(printf '%s\n' "$procinfo" | sed -nE 's/.*pid=([0-9]+).*/\1/p' | head -1)"
web_port="${local_addr##*:}"
[[ -n "$pid" && -n "$web_port" ]] || continue
# Does this web process point back at OUR backend port?
api_port="$(read_environ_var "$pid" NEXT_PUBLIC_KANDEV_API_PORT)"
if [[ -z "$api_port" ]]; then
api_url="$(read_environ_var "$pid" KANDEV_API_BASE_URL)"
[[ "$api_url" =~ :([0-9]+)([/?#].*)?$ ]] && api_port="${BASH_REMATCH[1]}"
fi
if [[ "$api_port" == "$backend_port" ]]; then
printf '%s\n' "$web_port"
return 0
fi
done < <(ss -tlnpH 2>/dev/null)
return 0
if [[ -n "$cand" ]] && port_has_listener "$cand"; then
port="$cand"
fi
printf '%s\n' "${port:-$backend_port}"
}
# Emit "PID PORT" pairs for every listening kandev backend socket.
+3 -3
View File
@@ -1,6 +1,6 @@
#!/usr/bin/env bash
# kandev-kill — terminate exactly ONE kandev instance: the backend listening on
# a given port, plus its agentctl child(ren) and its matching web/next server.
# a given port, plus its agentctl child(ren) and matching Vite dev server.
#
# This NEVER does a blanket `pkill kandev`. It resolves the backend PID from the
# port, confirms the process is actually a kandev backend, computes the exact
@@ -210,8 +210,8 @@ collect_descendants() {
mapfile -t TREE < <(collect_descendants "$BACKEND_PID")
# The web/next server is typically a sibling (spawned by the launcher, not a
# child of the backend). If a pidfile sidecar lists it, include it.
# The optional Vite dev server is typically a sibling (spawned by the launcher,
# not a child of the backend). If a pidfile sidecar lists it, include it.
WEB_PID=""
if [[ -n "$PIDFILE" && -f "${PIDFILE%.pid}.web.pid" ]]; then
WEB_PID="$(tr -d '[:space:]' <"${PIDFILE%.pid}.web.pid" 2>/dev/null || true)"
+1 -1
View File
@@ -6,7 +6,7 @@ class Kandev < Formula
license "AGPL-3.0-only"
version "__VERSION__"
# Node is required: the CLI launcher and Next.js standalone server both need it.
# Node is required for the CLI launcher.
depends_on "node"
on_macos do
+3 -12
View File
@@ -1,24 +1,15 @@
#!/usr/bin/env bash
# Finalize the dist/kandev/ release layout from already-built pieces.
# Caller must have run, in this order:
# - scripts/release/package-web.sh (produces dist/web/)
# - scripts/release/package-cli.sh (produces dist/kandev/cli/)
# - go build ./cmd/{kandev,agentctl} -o dist/kandev/bin/...
# After this: dist/kandev/{bin,web,cli} is ready to install or tar.
# - go build ./cmd/{kandev,agentctl} -o dist/kandev/bin/... with Vite
# assets synced into the backend embed package before building kandev.
# After this: dist/kandev/{bin,cli} is ready to install or tar.
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
WEB_SRC="$ROOT_DIR/dist/web"
BUNDLE="$ROOT_DIR/dist/kandev"
if [ ! -d "$WEB_SRC" ]; then
echo "Missing $WEB_SRC; run scripts/release/package-web.sh first" >&2
exit 1
fi
mkdir -p "$BUNDLE/web"
cp -R "$WEB_SRC/." "$BUNDLE/web/"
if [ ! -f "$BUNDLE/cli/bin/cli.js" ]; then
echo "Missing $BUNDLE/cli/bin/cli.js; run scripts/release/package-cli.sh first" >&2
exit 1
+8 -46
View File
@@ -1,62 +1,24 @@
#!/usr/bin/env bash
# Used by .github/workflows/release.yml to package the Next.js standalone output.
# Used by .github/workflows/release.yml to package the Vite SPA output.
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
WEB_DIR="$ROOT_DIR/apps/web"
OUT_DIR="$ROOT_DIR/dist/web"
echo "Packaging Next.js standalone output for release..."
echo "Packaging Vite web output for release..."
if [ ! -d "$WEB_DIR/.next/standalone" ]; then
echo "Missing standalone output at $WEB_DIR/.next/standalone"
echo "Run: pnpm -C apps --filter @kandev/web build"
if [ ! -f "$WEB_DIR/dist/index.html" ]; then
echo "Missing Vite output at $WEB_DIR/dist/index.html"
echo "Run: pnpm -C apps --filter @kandev/web build:vite"
exit 1
fi
"$ROOT_DIR/scripts/release/validate-web-bundle.sh" "$WEB_DIR/dist"
rm -rf "$OUT_DIR"
mkdir -p "$OUT_DIR"
# Copy with symlinks dereferenced so pnpm's .pnpm/ structure survives
# archiving (symlinks cause issues on Windows even inside tar). The
# standalone output may contain broken symlinks from partial tracing —
# --safe-links skips those and rsync exits with code 23 (partial transfer),
# which is expected.
rsync -a --copy-links --safe-links "$WEB_DIR/.next/standalone/" "$OUT_DIR/" || {
rc=$?
if [ "$rc" -eq 23 ]; then
echo "Warning: some broken symlinks were skipped (expected for standalone output)"
else
exit "$rc"
fi
}
# pnpm's .pnpm virtual store keeps hoisted packages in
# node_modules/.pnpm/node_modules/ which is unreachable via normal Node
# module resolution from web/node_modules/next/. Copy everything into
# web/node_modules/ so Next.js (and its deps) can find them at runtime.
PNPM_HOISTED="$OUT_DIR/node_modules/.pnpm/node_modules"
NEXT_MODULES="$OUT_DIR/web/node_modules"
if [ -d "$PNPM_HOISTED" ] && [ -d "$NEXT_MODULES" ]; then
for pkg in "$PNPM_HOISTED"/*; do
name="$(basename "$pkg")"
if [ ! -e "$NEXT_MODULES/$name" ]; then
cp -R "$pkg" "$NEXT_MODULES/$name"
fi
done
echo "Hoisted pnpm packages into web/node_modules/"
# Remove the .pnpm virtual store — everything needed is now in
# web/node_modules/ and the root node_modules/ is unreachable from
# the server. Removing it avoids 3-4x duplication in the archive.
rm -rf "$OUT_DIR/node_modules"
fi
# Static assets and public/ must be alongside server.js (inside web/)
# so Next.js can serve them. The standalone trace puts build artifacts
# at web/.next/ but doesn't include static/ — copy it there.
cp -R "$WEB_DIR/.next/static" "$OUT_DIR/web/.next/"
if [ -d "$WEB_DIR/public" ]; then
cp -R "$WEB_DIR/public" "$OUT_DIR/web/public"
fi
cp -R "$WEB_DIR/dist/." "$OUT_DIR/"
"$ROOT_DIR/scripts/release/validate-web-bundle.sh" "$OUT_DIR"
echo "Web bundle packaged at $OUT_DIR"
+25
View File
@@ -0,0 +1,25 @@
#!/usr/bin/env bash
# Validate that a directory contains a complete Vite SPA bundle.
set -euo pipefail
if [ "$#" -ne 1 ]; then
echo "usage: $0 <web-bundle-dir>" >&2
exit 2
fi
WEB_DIR="$1"
if [ ! -f "$WEB_DIR/index.html" ]; then
echo "Missing Vite SPA index: $WEB_DIR/index.html" >&2
exit 1
fi
if [ ! -d "$WEB_DIR/assets" ]; then
echo "Missing Vite SPA assets directory: $WEB_DIR/assets" >&2
exit 1
fi
if ! find "$WEB_DIR/assets" -type f -name '*.js' -print -quit | grep -q .; then
echo "Missing Vite SPA JavaScript assets under: $WEB_DIR/assets" >&2
exit 1
fi