Skip to content

Latest commit

 

History

History
351 lines (274 loc) · 15.8 KB

File metadata and controls

351 lines (274 loc) · 15.8 KB

Devner

Local dev environment orchestrator for WordPress, Laravel, Node, Next.js and Astro projects.

  • Single cross-platform Go binary (macOS, Linux, Windows) — no bash, no Makefile for users
  • Three interfaces sharing the same backend: CLI (Cobra) · TUI (Bubble Tea) · native GUI (Wails + React + Tailwind)
  • macOS menubar daemon with Start / Stop / Open GUI and a live stack N/M running indicator
  • Built-in AI agent: ask in natural language, it calls tools (create projects, databases, hosts, logs, exec)
  • Multi-provider LLM: Infomaniak AI Tools (open-source models), Anthropic, local Ollama
  • Docker stack: FrankenPHP (PHP+Caddy+Node), MySQL 8.4, PostgreSQL + PostGIS, Redis, Mailpit, Adminer

Install

macOS / Linux

# From release binary (when published)
curl -L https://github.com/MarJC5/devner/releases/latest/download/devner_darwin_arm64.tar.gz | tar -xz
sudo mv devner /usr/local/bin/

# Or build from source
git clone -b go https://github.com/MarJC5/devner.git
cd devner
make install             # builds and installs to /usr/local/bin

Windows (PowerShell)

# Download the .zip from Releases and extract devner.exe into PATH.
iwr -useb https://github.com/MarJC5/devner/releases/latest/download/devner_windows_amd64.zip -o devner.zip
Expand-Archive devner.zip

Requirements

  • Docker Desktop (macOS, Windows) or Docker Engine + Docker Compose v2 (Linux)
  • Go 1.22+ (to build from source)
  • (optional) mkcert for system-wide HTTPS trust outside browsers

Quickstart

# 1. Start the shared stack (pulls images the first time)
devner up

# 2. Create a Laravel project with MySQL
devner new laravel myapp --db=mysql
# → scaffolded, DB created, Caddy configured
# → https://myapp.localhost

# 3. Interactive TUI
devner tui

# 4. Ask the agent (Infomaniak by default)
#    First-run: edit ~/Library/Application Support/devner/config.toml,
#    set product_id and api_key under [llm.providers.infomaniak].
devner models                                             # sanity check auth
devner agent "create a Next.js project called web and show me logs"

Migrate from an existing projects directory

devner import --source=~/path/to/old/projects --dry-run
# review what would be imported
devner import --source=~/path/to/old/projects

Detection (priority order — first match wins for Type):

  • wp-config.php / wp-settings.php → WordPress (sniffs DB_NAME, DB_USER from wp-config.php)
  • artisan → Laravel (sniffs DB_DATABASE from .env)
  • index.php / public/index.php / composer.json / .htaccess with PHP rewrite → PHP (generic, import-only)
  • astro.config.* → Astro
  • next.config.* → Next.js
  • nuxt.config.* → Nuxt
  • svelte.config.* → SvelteKit
  • vite.config.* → Vite
  • package.json (fallback) → Node

Dev mode is detected in a second pass by reading package.json scripts:

  • server — Node-app types with a framework dev script (vite, next dev, astro dev, nuxi dev, nodemon, …). Gets a reverse-proxy port allocated in [3100, 3999].
  • watch — PHP-typed projects (WordPress, Laravel, generic PHP) with a watch or dev script. Runs alongside FrankenPHP; no port, no Caddy rerouting.
  • none — libraries (package.json with main/module/exports/files and no dev script), plain PHP, static sites. devner dev start refuses with a clear message.

Commands

devner up              # start stack
devner down            # stop stack
devner ps              # container status
devner logs <svc>      # tail container logs
devner restart [svc]   # restart stack / service
devner rebuild         # force rebuild images
devner delete --force  # destroy stack + volumes (DESTRUCTIVE)

devner new <type> <name> [--db=mysql|postgres] [--template=...]
# types: wordpress | laravel | node | nextjs | nuxt | astro | sveltekit | vite
# --template covers Vue / Svelte / Solid / Qwik / Preact / Lit / vanilla via Vite
devner remove <name>
devner list

devner dev start <name> [--command "..."]           # launch dev server (Node apps) or asset watcher (PHP+Vite/Mix)
devner dev stop <name>
devner dev status [name]                            # shows mode=server|watch + running/stopped
devner dev logs <name> [--tail N]

devner db create <mysql|postgres> <name>
devner db drop   <mysql|postgres> <name>

devner hosts add <domain> [target]     # no-op for *.localhost
devner hosts remove <domain>

devner import --source=<path>          # migrate projects from a directory
devner reconcile [--apply]             # fix drift: store ↔ FS ↔ Caddy

devner certs status
devner certs install                   # mkcert -install

devner models                          # list models of the active LLM provider
devner agent "<prompt>"                # one-shot agent call
devner tui                             # interactive TUI
devner gui                             # launch the desktop app (Wails)
devner menubar                         # launch the macOS menubar daemon
devner menubar stop                    # kill it
devner menubar status                  # check whether it's running

Configuration

The config file is created automatically on first run at the OS-standard location:

OS Path
macOS ~/Library/Application Support/devner/config.toml
Linux ~/.config/devner/config.toml
Windows %AppData%\devner\config.toml

Template (generated on first run, safe to edit):

[stack]
data_dir     = "~/.devner"
projects_dir = "~/devner/projects"

[llm]
active_provider = "infomaniak"

# Infomaniak AI Tools — OpenAI-compatible (v2 endpoint).
# Get your product_id from https://manager.infomaniak.com (AI Tools)
# or GET /1/ai with your token.
[llm.providers.infomaniak]
base_url    = "https://api.infomaniak.com/2/ai/{product_id}/openai/v1"
# Use either api_key (token in the file) OR api_key_env (env var name).
# api_key wins if both are set.
api_key     = ""
api_key_env = "INFOMANIAK_API_KEY"
product_id  = ""
model       = "qwen3"
kind        = "openai_compat"

[llm.providers.anthropic]
api_key_env = "ANTHROPIC_API_KEY"
model       = "claude-opus-4-7"
kind        = "anthropic"

# Local Ollama — no API key needed.
[llm.providers.ollama]
base_url    = "http://localhost:11434/v1"
model       = "qwen2.5-coder:14b"
kind        = "openai_compat"

Infomaniak setup

  1. Get your product ID: curl -H "Authorization: Bearer $TOKEN" https://api.infomaniak.com/1/ai
  2. Put the product ID and the token in config.toml under [llm.providers.infomaniak].
  3. Verify connectivity and list available models:
    devner models
  4. Run the agent:
    devner agent "list my projects"

Infomaniak v2 endpoint: POST /2/ai/{product_id}/openai/v1/chat/completions — see docs.

Node / Next / Nuxt / Astro / SvelteKit / Vite projects

Framework coverage:

Type Scaffold Dev command Notes
nextjs pnpm create next-app pnpm dev (PORT env) App Router + TS + Tailwind by default
nuxt pnpm dlx nuxi init pnpm dev --port N
astro pnpm create astro pnpm dev --port N --template minimal|basics|blog|portfolio|starlight
sveltekit pnpm create svelte pnpm dev --port N --template skeleton|minimal|demo
vite pnpm create vite pnpm dev --port N --template react-ts|vue-ts|svelte-ts|solid-ts|preact-ts|qwik-ts|lit-ts|vanilla-ts
node npm init -y npm run start User controls the server code

For Vue / Svelte / Solid / Qwik / Preact / Lit / vanilla-JS, use devner new vite myapp --template <tpl>-ts — no framework-specific devner type needed.

PHP projects (WordPress, Laravel, generic PHP) serve directly through FrankenPHP's PHP handler. Node-app projects use a different path:

  1. At creation time, devner allocates an internal port in the range 3100–3999 and persists it on the project row (projects.dev_port in SQLite).
  2. The rendered Caddyfile emits reverse_proxy localhost:<port> for that site instead of php_server + file_server.
  3. Nothing listens on the port until you run devner dev start. Caddy returns 502 meanwhile — that's the "up but dev not running" signal.
  4. devner dev start <name> spawns pnpm dev (or npm run start for plain Node) via docker exec -d inside the frankenphp container, under a new session so devner dev stop can terminate the whole process group (pnpm → vite/next/astro → …). A PID file lives at /tmp/devner/dev-<name>.pid; logs at /tmp/devner/dev-<name>.log.
  5. The URL is the same as PHP projects: https://<name>.localhost. HTTPS, HMR via WebSocket, everything transparent.

Port handling per framework

Vite and Astro don't read PORT from env by default — devner invokes them as pnpm dev --port <N>. Next.js reads PORT from env so devner sets it directly. Plain Node projects run npm run start and rely on the user's package.json honouring the PORT env var (conventional). Pass --command "..." to devner dev start to override.

Hot reload on macOS + Docker Desktop

Bind-mount file events don't propagate reliably from the host into the container on macOS. Devner sets CHOKIDAR_USEPOLLING=1, CHOKIDAR_INTERVAL=300, and WATCHPACK_POLLING=1 when spawning dev servers so chokidar/webpack-style watchers fall back to polling. Costs ~1–3 % of one core for instant HMR reliability. Enable Docker Desktop's VirtioFS file-sharing (Settings > General) if you prefer native fsevents without the polling overhead — the env vars become redundant but harmless.

Stateless lifecycle

Dev servers die when the frankenphp container restarts (which happens whenever devner applies a new Caddy config — i.e. every devner new / devner remove). This is intentional: devner does not auto-restart them. Run devner dev start <name> again when you want it back.

Quickstart

devner new vite myapp          # allocates port, scaffolds pnpm create vite react-ts
devner dev start myapp         # launches pnpm dev --port <allocated>
open https://myapp.localhost   # HTTP 200, Vite dev server with HMR
devner dev logs myapp          # tail the dev server output
devner dev stop myapp          # kill the process group, Caddy → 502

PHP projects with an asset pipeline (watch mode)

Many PHP projects (custom CMSes, WordPress themes, Laravel apps) carry a package.json for asset compilation (Vite, Tailwind, Laravel Mix). Devner classifies these as dev_mode=watch at import time and devner dev start launches the asset watcher alongside FrankenPHP — the PHP backend keeps serving https://<name>.localhost, no reverse-proxy switcheroo.

Detection rule: PHP type + a watch script in package.json → mode=watch, command=pnpm watch. If only dev is present, that becomes the command (Vite's dev script on a PHP project is an asset watcher in disguise).

devner import --source=~/path/to/projects   # classifies each folder
devner dev status                           # shows mode=watch / server / none
devner dev start my-cms                     # runs `pnpm watch` inside frankenphp
devner dev logs my-cms                      # tails vite build --watch output

Libraries (pure JS packages with main/exports/files and no dev script) get dev_mode=nonedevner dev start refuses with a clear message instead of launching a non-existent server.

Agent tools

The LLM sees these tools. Destructive ones (marked ⚠) require confirmation in the TUI or --yes on the CLI.

Tool Purpose
list_projects list managed projects
project_status details for one project
create_project scaffold wordpress / laravel / node / nextjs / astro / vite
delete_project remove files + DB + Caddy entry
create_database mysql or postgres DB + user
start_dev_server launch pnpm dev / npm run start for a Node-like project
stop_dev_server stop the dev server
dev_server_status running/stopped + PID + port
drop_database drop DB + user
start_stack / stop_stack lifecycle
rebuild_stack rebuild images + recreate
tail_logs container logs
composer run composer install / require / update in the project
npm run npm / pnpm / yarn install / build / add in the project
wp_cli run WP-CLI: plugin install, option get, user create, etc.
artisan run php artisan on a Laravel project
exec_in_project arbitrary shell escape hatch (prefer the typed tools above)

Every tool call is audited in ~/.devner/store.db (history table).

Architecture

cmd/devner/          # main (Cobra + Bubble Tea entrypoint)
internal/
  cli/               # Cobra subcommands
  tui/               # Bubble Tea scenes (projects, stack, chat, logs, config)
  agent/             # tool-use loop (max 10 iterations, destructive confirm)
  llm/               # Provider interface + OpenAI-compat + Anthropic native
  tools/             # Registry + JSON schemas
  project/           # WP/Laravel/Node/Next/Astro detect + scaffold
  database/          # MySQL + Postgres ops (validated identifiers, random passwords)
  runtime/           # docker compose wrapper + docker exec/logs
  network/           # Caddyfile rendering + docker restart reload
  store/             # SQLite metadata (modernc — pure Go, no CGO)
  app/               # Deps bundle (shared CLI + TUI wiring)
  config/            # Viper TOML
  platform/          # OS detection, hosts path, elevation check
assets/              # embed.FS: compose.yaml, Dockerfile, php.ini, Caddyfile
migrations/          # SQLite schema (.sql + embed.FS)

Desktop app (GUI)

Cross-platform desktop app built with Wails v2 (Go backend + React + Tailwind frontend + native OS WebView). Shares internal/app.Deps with the CLI / TUI so every action goes through the same code path — no HTTP layer, no divergence.

  • Dark + light themes (Sun / Moon / System — follows macOS preference)
  • EN / FR UI (persisted in localStorage, selectable in Settings)
  • Frosted-glass panels on macOS (vibrancy) and Windows (Mica); flat clean look on Linux
  • Keyboard shortcuts: ⌘1..7 jump between Projects / Stack / Chat / Logs / Databases / Hosts / Settings
make gui-dev        # hot-reload Vite + auto Go rebuild on save
make gui-build      # produces gui/build/bin/devner-gui(.app|.exe|binary)
make gui-clean

Requirements: Go 1.22, Node 20, pnpm 9, and the Wails CLI:

go install github.com/wailsapp/wails/v2/cmd/wails@latest

Launched from the CLI with devner gui — the CLI searches /Applications/Devner.app, gui/build/bin/Devner.app, devner-gui on $PATH, or the $DEVNER_GUI env var.

Menubar daemon (macOS / Windows)

A tiny standalone process (cmd/devner-menubar) owns a system-tray icon with Start stack / Stop stack / Open GUI entries and a live stack N/M running indicator. It runs as a separate binary because Wails and fyne.io/systray both want ownership of the main run loop.

devner menubar          # launch (backgrounded)
devner menubar stop
devner menubar status

Development

make build              # ./bin/devner
make build-menubar      # ./bin/devner-menubar
make build-all-bins     # CLI + menubar
make test               # go test ./...
make install            # copies devner to /usr/local/bin (may need sudo)
make clean              # removes ./bin and ./dist

# Without Make:
go build -o bin/devner ./cmd/devner
go test ./...

Cross-compile the CLI:

make build-all      # bin/devner-{darwin,linux,windows}-{amd64,arm64}

Release (local dry-run):

make release-snapshot    # goreleaser --snapshot --clean

Release CI (GitHub Actions) attaches GUI archives — Devner_darwin_{arm64,amd64}.zip, Devner_linux_amd64.tar.gz, Devner_windows_amd64.zip — alongside the GoReleaser-produced CLI archives on every v* tag push.