From d6cf4a1acc8d00e34aea28057d00add496465fab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Flor=C3=AAncio?= Date: Sun, 14 Jun 2026 22:05:49 +0100 Subject: [PATCH] chore: add provider-agnostic bootstrap (#1377) --- Makefile | 10 ++ README.md | 22 +-- docs/remote-cloud-environment.md | 23 ++- mise.toml | 15 ++ scripts/bootstrap-dev-env | 190 +++++++++++++++++++++++++ scripts/install-playwright-browsers.sh | 33 ++++- 6 files changed, 273 insertions(+), 20 deletions(-) create mode 100644 mise.toml create mode 100755 scripts/bootstrap-dev-env diff --git a/Makefile b/Makefile index 864c488070..05315453ab 100644 --- a/Makefile +++ b/Makefile @@ -62,6 +62,8 @@ help: @echo "Kandev - AI Agent Kanban Board" @echo "" @echo "Development Commands:" + @echo " bootstrap Install mise tools, workspace deps, and git hooks" + @echo " bootstrap-e2e Bootstrap plus Playwright browser/system deps" @echo " dev Run backend + web via CLI (auto ports)" @echo " dev-prod-db Run dev mode against the production db at ~/.kandev" @echo " dev-backend Run backend in development mode (port 38429)" @@ -129,6 +131,14 @@ help: # Development # +.PHONY: bootstrap +bootstrap: + @scripts/bootstrap-dev-env + +.PHONY: bootstrap-e2e +bootstrap-e2e: + @scripts/bootstrap-dev-env --with-e2e + # Build the linux/amd64 agentctl on every host except Linux/x86_64 (where the # host build IS already linux/amd64). Computed up here, OS-conditionally, so # the recipe doesn't shell out to `uname` — that fails on Windows under cmd diff --git a/README.md b/README.md index fca8f5096d..4acad64ddc 100644 --- a/README.md +++ b/README.md @@ -188,11 +188,20 @@ apps/ ### Prerequisites -- Go 1.26+ -- Node.js 18+ -- pnpm +- [mise](https://mise.jdx.dev/) for the pinned toolchain in `mise.toml` (`make bootstrap` installs it when missing) +- Go 1.26, Node.js 24, pnpm 9.15.9 (installed by `make bootstrap`) - Docker (optional) +### Environment Setup + +```bash +# Install mise tools, workspace dependencies, and git hooks +make bootstrap + +# Also install Playwright Chromium browser/deps for E2E +make bootstrap-e2e +``` + ### Running Dev Servers ```bash @@ -216,11 +225,8 @@ make fmt # Format all code ### Pre-commit Hooks ```bash -# Install pre-commit (https://pre-commit.com/#install) -pipx install pre-commit - -# Install git hooks -pre-commit install +# Installed and wired by make bootstrap +make doctor ``` diff --git a/docs/remote-cloud-environment.md b/docs/remote-cloud-environment.md index 1821cfebe0..24fb9a0cbe 100644 --- a/docs/remote-cloud-environment.md +++ b/docs/remote-cloud-environment.md @@ -4,11 +4,8 @@ Setup notes and caveats for developing Kandev in ephemeral cloud VMs (Cursor Clo ## Runtime requirements -- **Go 1.26** — install to `/usr/local/go` and ensure `PATH` includes `/usr/local/go/bin`. -- **Node.js 24** — use `nvm install 24 && nvm use 24`. -- **pnpm 9.15.9** — matches `packageManager` in `apps/package.json`. Install with `npm install -g pnpm@9.15.9`. -- **golangci-lint v2** — required for `make lint-backend`. Install with `go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest`. -- **gcc** — required for CGO (SQLite FTS5). Pre-installed on most Ubuntu-based cloud VMs. +- **mise** — `mise.toml` at the repo root pins the developer toolchain (Go 1.26, Node.js 24, pnpm 9.15.9, golangci-lint 2.9.0, go-licenses 1.6.0, pre-commit, etc.). +- **gcc / sqlite headers** — required for CGO (SQLite FTS5). `scripts/bootstrap-dev-env` installs these on common Linux/macOS package managers when it has permission. - **Azure Repos PR creation (optional)** — only needed when testing `worktree.create_pr` against Azure remotes outside the published Docker image: - [Azure CLI](https://learn.microsoft.com/en-us/cli/azure/install-azure-cli) (`az`) on `PATH` - DevOps extension: `az extension add --name azure-devops` @@ -16,6 +13,18 @@ Setup notes and caveats for developing Kandev in ephemeral cloud VMs (Cursor Clo - The `ghcr.io/kdlbs/kandev` image ships `gh`, `az`, and the `azure-devops` extension preinstalled (see root `Dockerfile`). - Host routing table: `apps/backend/internal/agentctl/AGENTS.md`. +Provider prepare scripts should do auth + clone first, then run the shared bootstrap from the repo root: + +```bash +scripts/bootstrap-dev-env +``` + +For E2E-capable environments: + +```bash +scripts/bootstrap-dev-env --with-e2e +``` + ## Generated files before typecheck Before running `make typecheck`, you must generate two files that are gitignored: @@ -45,7 +54,7 @@ All documented in the root `Makefile`: These apply to Cursor Cloud, Codex, and similar Firecracker-backed sandboxes: -- **golangci-lint PATH**: `go install` places the binary in `~/go/bin/`. Ensure `PATH` includes it (the update script appends to `~/.bashrc`, but in-session shells may need `export PATH="$PATH:$HOME/go/bin"`). +- **tool PATH**: `scripts/bootstrap-dev-env` activates mise for its own process. New shells should run `eval "$(mise activate bash)"` or use a shell profile that activates mise so `node`, `pnpm`, `go`, `golangci-lint`, and `pre-commit` resolve from the pinned toolchain. - **First page load**: `make dev` compiles pages on first visit via Turbopack — expect ~25 s for the initial load. - **CLI test flake**: `src/ports.test.ts > isPortInUse > returns false within the timeout when the host black-holes packets` fails because `192.0.2.1` behaves differently inside Firecracker. This is a known environment-specific flake, not a code issue. -- **Playwright browser installation**: `playwright install chromium` hangs during zip extraction (Node.js io_uring incompatibility with the Firecracker kernel). Workaround: `scripts/install-playwright-browsers.sh` runs the install with a timeout, then falls back to extracting the already-downloaded zips with `unzip`. The update script calls this automatically. System deps can be installed via `playwright install-deps chromium`. +- **Playwright browser installation**: `playwright install chromium` hangs during zip extraction (Node.js io_uring incompatibility with the Firecracker kernel). Workaround: `scripts/install-playwright-browsers.sh` runs the install with a timeout, then falls back to extracting the already-downloaded zips with `unzip`. `scripts/bootstrap-dev-env --with-e2e` calls this automatically. System deps can be installed via `playwright install-deps chromium`. diff --git a/mise.toml b/mise.toml new file mode 100644 index 0000000000..8d28ece46d --- /dev/null +++ b/mise.toml @@ -0,0 +1,15 @@ +[tools] +node = "24" +pnpm = "9.15.9" +go = "1.26.0" +jq = "1.8.1" +ripgrep = "15.1.1" +uv = "0.11.21" +golangci-lint = "2.9.0" +git-cliff = "2.13.1" +"go:github.com/google/go-licenses" = { version = "v1.6.0", depends = "go" } +"pipx:pre-commit" = { version = "4.6.0", depends = "uv" } + +[env] +NODE_OPTIONS = "--dns-result-order=ipv4first" +_.path = ["./apps/node_modules/.bin"] diff --git a/scripts/bootstrap-dev-env b/scripts/bootstrap-dev-env new file mode 100755 index 0000000000..c08e9af6d5 --- /dev/null +++ b/scripts/bootstrap-dev-env @@ -0,0 +1,190 @@ +#!/usr/bin/env bash +# bootstrap-dev-env - provider-agnostic Kandev development environment setup. +# +# Cloud executor prepare scripts should handle provider concerns first: +# authentication, clone/checkout, workspace placement, and agentctl startup. +# After cloning, call this script from the repo root to install the Kandev +# toolchain and dependencies consistently across Sprites, Daytona, Modal, +# Codespaces, local VMs, and similar environments. +# +# Usage: +# scripts/bootstrap-dev-env [options] +# +# Options: +# --with-e2e Install Playwright browser/system deps for E2E +# --without-hooks Skip pre-commit hook wiring +# --without-node-modules Skip pnpm install +# --without-os-packages Skip best-effort OS package installation +# --without-mise-install Require mise to already be installed +# --provider NAME Record caller/provider name in logs only +# -h, --help Show this help + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" + +WITH_E2E=0 +WITH_HOOKS=1 +WITH_NODE_MODULES=1 +WITH_OS_PACKAGES=1 +WITH_MISE_INSTALL=1 +PROVIDER="generic" + +usage() { + awk 'NR > 1 { if ($0 !~ /^#/) exit; sub(/^# ?/, ""); print }' "$0" + exit "${1:-2}" +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --with-e2e) WITH_E2E=1; shift ;; + --without-hooks) WITH_HOOKS=0; shift ;; + --without-node-modules) WITH_NODE_MODULES=0; shift ;; + --without-os-packages) WITH_OS_PACKAGES=0; shift ;; + --without-mise-install) WITH_MISE_INSTALL=0; shift ;; + --provider) PROVIDER="${2:?--provider requires a value}"; shift 2 ;; + -h|--help) usage 0 ;; + *) echo "bootstrap-dev-env: unknown argument '$1'" >&2; usage 2 ;; + esac +done + +log() { printf '[bootstrap:%s] %s\n' "$PROVIDER" "$*" >&2; } +warn() { printf '[bootstrap:%s] warning: %s\n' "$PROVIDER" "$*" >&2; } +die() { printf '[bootstrap:%s] error: %s\n' "$PROVIDER" "$*" >&2; exit 1; } + +run_privileged() { + if [[ "${EUID:-$(id -u)}" -eq 0 ]]; then + "$@" + elif command -v sudo >/dev/null 2>&1; then + sudo "$@" + else + return 127 + fi +} + +install_os_packages() { + [[ "$WITH_OS_PACKAGES" -eq 1 ]] || { log "skipping OS packages"; return 0; } + + if command -v apt-get >/dev/null 2>&1; then + log "installing OS packages with apt-get" + if ! run_privileged apt-get update; then + warn "apt-get requires root/sudo; skipping OS package installation" + return 0 + fi + run_privileged env DEBIAN_FRONTEND=noninteractive apt-get install -y \ + bash build-essential ca-certificates curl file git gzip lsof make openssh-client \ + pipx pkg-config python3 python3-pip rsync sqlite3 tar unzip xz-utils zip \ + libsqlite3-dev || warn "apt-get package installation failed; continuing because packages may already be present" + return 0 + fi + + if command -v dnf >/dev/null 2>&1; then + log "installing OS packages with dnf" + run_privileged dnf install -y \ + bash ca-certificates curl file gcc gcc-c++ git gzip lsof make openssh-clients \ + pipx pkgconf-pkg-config python3 python3-pip rsync sqlite sqlite-devel tar unzip xz zip \ + || warn "dnf package installation failed; continuing because packages may already be present" + return 0 + fi + + if command -v apk >/dev/null 2>&1; then + log "installing OS packages with apk" + run_privileged apk add --no-cache \ + bash build-base ca-certificates curl file git gzip lsof make openssh-client \ + pipx pkgconf python3 py3-pip rsync sqlite sqlite-dev tar unzip xz zip \ + || warn "apk package installation failed; continuing because packages may already be present" + return 0 + fi + + if command -v brew >/dev/null 2>&1; then + log "installing OS packages with Homebrew" + brew install bash ca-certificates curl file git lsof make openssh pipx pkg-config \ + python rsync sqlite unzip xz zip \ + || warn "Homebrew package installation failed; continuing because packages may already be present" + return 0 + fi + + warn "no supported package manager found; install build tools, sqlite headers, unzip, pipx, and OpenSSH manually if needed" +} + +install_mise() { + if command -v mise >/dev/null 2>&1; then + log "mise already installed: $(mise --version 2>/dev/null || true)" + persist_mise_activation + return 0 + fi + + [[ "$WITH_MISE_INSTALL" -eq 1 ]] || die "mise is not installed and --without-mise-install was passed" + command -v curl >/dev/null 2>&1 || die "curl is required to install mise" + + log "installing mise" + tmp_mise_script="$(mktemp)" + trap 'rm -f "$tmp_mise_script"' EXIT + curl -fsSL https://mise.run -o "$tmp_mise_script" + sh "$tmp_mise_script" + export PATH="$HOME/.local/bin:$PATH" + command -v mise >/dev/null 2>&1 || die "mise install finished but mise is not on PATH" + persist_mise_activation +} + +persist_mise_activation() { + [[ -n "${HOME:-}" ]] || return 0 + local bashrc="$HOME/.bashrc" + + touch "$bashrc" + grep -Fqx 'export PATH="$HOME/.local/bin:$PATH"' "$bashrc" 2>/dev/null \ + || printf '\nexport PATH="$HOME/.local/bin:$PATH"\n' >> "$bashrc" + grep -Fqx 'eval "$(mise activate bash)"' "$bashrc" 2>/dev/null \ + || printf 'eval "$(mise activate bash)"\n' >> "$bashrc" +} + +activate_mise() { + export PATH="$HOME/.local/bin:$PATH" + cd "$REPO_ROOT" + + [[ -f "$REPO_ROOT/mise.toml" ]] || die "missing mise.toml at repo root" + + log "trusting mise.toml" + mise trust "$REPO_ROOT/mise.toml" >/dev/null 2>&1 || mise trust >/dev/null + + log "installing mise tools" + mise install + + log "activating mise environment for bootstrap" + eval "$(mise activate bash)" +} + +install_node_modules() { + [[ "$WITH_NODE_MODULES" -eq 1 ]] || { log "skipping pnpm install"; return 0; } + log "installing pnpm workspace dependencies" + cd "$REPO_ROOT/apps" + pnpm install --frozen-lockfile +} + +install_hooks() { + [[ "$WITH_HOOKS" -eq 1 ]] || { log "skipping git hook setup"; return 0; } + log "wiring pre-commit hooks" + cd "$REPO_ROOT" + make doctor +} + +install_e2e() { + [[ "$WITH_E2E" -eq 1 ]] || { log "skipping Playwright E2E browser setup"; return 0; } + log "installing Playwright Chromium browser/deps" + "$REPO_ROOT/scripts/install-playwright-browsers.sh" +} + +main() { + cd "$REPO_ROOT" + log "bootstrapping Kandev development environment at $REPO_ROOT" + install_os_packages + install_mise + activate_mise + install_node_modules + install_hooks + install_e2e + log "bootstrap complete" +} + +main diff --git a/scripts/install-playwright-browsers.sh b/scripts/install-playwright-browsers.sh index d8ba823864..ed0501d757 100755 --- a/scripts/install-playwright-browsers.sh +++ b/scripts/install-playwright-browsers.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# install-playwright-browsers.sh — install Playwright chromium browser +# install-playwright-browsers.sh — install Playwright chromium browser artifacts # with a workaround for Firecracker VMs where Node.js io_uring-based # zip extraction hangs indefinitely. # @@ -14,8 +14,30 @@ TIMEOUT_SECS=90 cd "$(dirname "$0")/../apps" -# Check if browsers are already fully installed -if pnpm --filter @kandev/web exec playwright install chromium --dry-run 2>&1 | grep -q "already installed"; then +PW_ARGS=(chromium chromium-headless-shell) + +missing=1 +found=0 + +if dry_run="$(pnpm --filter @kandev/web exec playwright install --dry-run "${PW_ARGS[@]}" 2>&1)"; then + missing=0 + while IFS= read -r loc; do + [ -n "$loc" ] || continue + found=1 + if [ ! -f "$loc/INSTALLATION_COMPLETE" ]; then + missing=1 + break + fi + done < <(printf '%s\n' "$dry_run" | awk -F'Install location:[[:space:]]*' 'NF > 1 {print $2}' | sort -u) + + if [ "$found" -eq 0 ]; then + missing=1 + fi +else + echo "[playwright] dry-run failed; continuing with browser installation" +fi + +if [ "$missing" -eq 0 ]; then echo "[playwright] browsers already installed" exit 0 fi @@ -25,7 +47,7 @@ pnpm --filter @kandev/web exec playwright install-deps chromium 2>/dev/null || t # Attempt normal install with timeout rm -rf "$PW_CACHE/__dirlock" -if timeout "$TIMEOUT_SECS" pnpm --filter @kandev/web exec playwright install chromium 2>&1; then +if timeout "$TIMEOUT_SECS" pnpm --filter @kandev/web exec playwright install "${PW_ARGS[@]}" 2>&1; then echo "[playwright] install completed normally" exit 0 fi @@ -42,7 +64,8 @@ for comp_dir in "$PW_CACHE"/chromium-* "$PW_CACHE"/chromium_headless_shell-* "$P [ -f "$comp_dir/INSTALLATION_COMPLETE" ] && continue comp_name=$(basename "$comp_dir" | sed 's/-[0-9]*$//') - zipfile=$(find /tmp -name "playwright-download-${comp_name}-*" -type f 2>/dev/null | sort | tail -1) + comp_name_alt=${comp_name//_/-} + zipfile=$(find /tmp \( -name "playwright-download-${comp_name}-*" -o -name "playwright-download-${comp_name_alt}-*" \) -type f 2>/dev/null | sort | tail -1) if [ -z "$zipfile" ]; then echo "[playwright] no zip found for $comp_name — skipping"