Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
aaee332
docs: add warm dev loop proposal (mxbuild --serve + reload_model)
claude Jul 17, 2026
673710c
docs: add prototype-to-production workflow to warm dev loop proposal
claude Jul 18, 2026
a8404b4
docs: add hot-reload scope + DB metamodel-catalog root cause to warm-…
claude Jul 18, 2026
4a799cd
docs: add tunnel-hub scaling (self-registration + admin overview) to …
claude Jul 18, 2026
2c4c1fc
docs: add provisioning (new project -> running app, SessionStart boot…
claude Jul 18, 2026
27aede7
docs: make prompt template the primary web/iPad provisioning path
claude Jul 18, 2026
a7f41e4
docs: add delivery-slices section to warm-loop proposal
claude Jul 18, 2026
88252c5
docs: regenerate proposals README index (add warm-loop + 4 others)
claude Jul 18, 2026
2bfb306
docs: normalise two non-canonical proposal statuses, reindex README
claude Jul 18, 2026
13f0168
docs: capture diagnostics-catalog research + commit extraction script
claude Jul 18, 2026
49ce895
Merge pull request #1 from ako/claude/mxbuild-diagnostics-spike-emta6h
ako Jul 18, 2026
0a67c47
feat(docker): mxbuild --serve client (warm incremental build) for run…
claude Jul 18, 2026
111bbba
feat(docker): preflight the mxbuild runtime/ dir in StartServe
claude Jul 18, 2026
e92de16
feat(docker): add RuntimeController for the local warm-dev loop
claude Jul 18, 2026
11b30fd
feat(docker): add standalone runtime boot for the local warm loop
claude Jul 18, 2026
cca93d0
feat(run): add `mxcli run --local` warm dev loop
claude Jul 18, 2026
fa94928
docs(run-local): document the warm local dev loop
claude Jul 18, 2026
ada5a4f
feat(run-local): bundle the browser client so pages render
claude Jul 18, 2026
cf6a0da
feat(run-local): incremental client bundler + Playwright screenshots
claude Jul 18, 2026
f186511
feat(run-local): authenticated + deep-link screenshots
claude Jul 18, 2026
9461e5a
feat(run-local): multi-page screenshot sets
claude Jul 18, 2026
5de6b54
feat(run-local): --ensure-db provisions the local database (slice 2)
claude Jul 18, 2026
1d3863a
feat(run-local,init): SessionStart hook + bootstrap prompt (slice 2 c…
claude Jul 18, 2026
7e36ef4
docs: promote the bootstrap prompt to the README + clarify install ve…
claude Jul 18, 2026
938e3b5
docs: correct install path — go install @latest doesn't work (uncommi…
claude Jul 18, 2026
a9bbbd9
docs(bootstrap): recommend the nightly build for install
claude Jul 18, 2026
82e1fc8
docs: sweep install guidance — correct asset format, go install, reco…
claude Jul 18, 2026
878a3bd
Merge pull request #2 from ako/claude/mxbuild-diagnostics-spike-emta6h
ako Jul 18, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 125 additions & 0 deletions .claude/skills/mendix/run-local.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
# Warm Local Dev Loop — `mxcli run --local`

## Overview

`mxcli run --local` runs a Mendix app **without Docker**, keeping a
`mxbuild --serve` process and a standalone runtime hot so model changes apply in
~1 s instead of a ~30–60 s rebuild-and-restart. Use it as the fast inner loop when
iterating on an app; use `mxcli docker run` when you need the fully-rendered browser
client (see the limitation below).

## When to Use This Skill

Use this when:
- You want the fastest edit → running-app loop for a Mendix 11.x project.
- You're driving the model programmatically (`mxcli exec`/MDL) and want each change
live immediately.
- You're iterating on **page design** (the app renders in a real browser) or doing
runtime/model/API/headless verification.

Prefer `mxcli docker run` when:
- The project is Mendix 9/10 (JDK 11/17 — not yet supported by `run --local`).
- You want a container-parity deployment rather than a standalone runtime.

## Usage

```bash
# boot once and keep serving (Ctrl-C to stop)
mxcli run --local -p app.mpr

# boot and hot-apply on every project change
mxcli run --local -p app.mpr --watch
```

## How apply is chosen

Every warm rebuild reports whether a restart is required; `run --local` applies the
cheapest action automatically:

| Change | Apply | Cost |
|--------|-------|------|
| page / microflow / nanoflow / text | hot `reload_model` (no restart) | ~1 s |
| entity / view entity / association | runtime restart + DDL | ~9 s |

Structural changes need a restart because the runtime reconciles its entity/
association catalog only at startup; behavioural changes are hot-reloaded.

## Prerequisites

- **Mendix 11.x** project (runtime launches under **JDK 21**).
- A **PostgreSQL** database (defaults: `127.0.0.1:5432`, user `mendix`, db derived
from the project name; override with `--db-host/--db-name/--db-user/--db-password`).
- **`--ensure-db`** provisions it for a fresh session: starts local Postgres if the
port is down and creates the role + database if missing (local superuser via
`sudo -u postgres`). Remote hosts are only checked, not provisioned.
- Without `--ensure-db`, create it once and the command errors if it's unreachable:

```bash
createdb -h 127.0.0.1 -U mendix "$(basename app.mpr .mpr | tr '[:upper:]' '[:lower:]')"
```

## The intended loop

```bash
# terminal 1: keep the app hot
mxcli run --local -p app.mpr --watch

# terminal 2 (or an agent): edit the model — the change hot-applies automatically
mxcli exec add-page.mdl -p app.mpr
```

`--watch` observes the project directory (MPR v1 and v2 layouts), ignoring
`deployment/`, `.git`, and `node_modules`.

## Flags

| Flag | Default | Purpose |
|------|---------|---------|
| `--local` | — | Required; run without Docker |
| `--watch` | off | Rebuild + hot-apply on each change |
| `--ensure-db` | off | Provision local Postgres + app database if missing |
| `--setup` | off | Cache MxBuild+runtime + ensure DB, then exit (SessionStart bring-up) |
| `--screenshot` | off | Playwright PNG after boot + each change |
| `--screenshot-path` / `--screenshot-url` | `.mxcli/run-local.png` / app root | Screenshot output / page (URL or `/path`) |
| `--screenshot-user` / `--screenshot-password` | — | Log in once, reuse session (pages behind login) |
| `--app-port` / `--admin-port` / `--serve-port` | 8080 / 8090 / 6543 | Ports |
| `--db-host` / `--db-name` / `--db-user` / `--db-password` | 127.0.0.1:5432 / derived / mendix / mendix | Database |

## Pages render in the browser

`run --local` bundles the browser client (`web/dist/`) with mxbuild's rollup tooling
after the deploy build, so the app renders in a real browser (verified with
Playwright + the devcontainer's Chromium).

- **`--watch`** keeps a long-lived incremental bundler hot (the client-side mirror of
`mxbuild --serve`): a page/widget edit re-bundles in ~3–4 s; a microflow/entity edit
skips the bundle and just hot-reloads. It uses `CHOKIDAR_USEPOLLING` because inotify
is silent on container filesystems.
- Without `--watch`, a single one-shot bundle (~7 s) runs before boot.

## Pixel-perfect page loop

`--screenshot` captures a PNG (default `<projectDir>/.mxcli/run-local.png`) after boot
and after each applied change, via Playwright's built-in `screenshot` command
(Chromium from `PLAYWRIGHT_BROWSERS_PATH`):

```bash
mxcli run --local -p app.mpr --watch --screenshot
# edit a page -> auto rebuild -> re-bundle -> reload -> fresh screenshot
```

- `--screenshot-url /p/customers` shoots a specific page (bare path resolved against
the app root; a full URL is used as-is). Repeat it for a multi-page set — each page
gets its own PNG (`run-local-p-customers.png`, `run-local-home.png`).
- `--screenshot-user`/`--screenshot-password` log in once (Mendix form auth) and
reuse the session, so pages behind login render authenticated. Best-effort: an
anonymous app with no login form proceeds unauthenticated.

## Validation checklist

- [ ] Project is Mendix 11.x.
- [ ] Postgres is running and the target database exists.
- [ ] `curl -s -o /dev/null -w '%{http_code}' http://localhost:8080/` returns `200`,
and `.../dist/index.js` also returns `200` (client bundle served).
- [ ] With `--watch`, editing a microflow logs `applied via reload`; adding an entity
logs `applied via restart` and creates the table in Postgres.
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -523,6 +523,7 @@ Full syntax tables for all MDL statements (microflows, pages, security, navigati
- LSP server with hover, go-to-definition, completion, diagnostics, symbols, folding
- VS Code extension (`vscode-mdl`) with context menu commands (Run/Check/Selection)
- Docker build integration (`mxcli docker build`) with PAD patching (Phase 1)
- Warm local dev loop (`mxcli run --local [--watch] [--screenshot]`): Docker-free `mxbuild --serve` + standalone runtime, hot `reload_model` for behavioural changes and restart+DDL for structural ones (chosen from the serve build's `restartRequired`). Bundles the browser client (`web/dist/` via mxbuild's rollup runner, which the serve Deploy target skips) so Mendix 11.x apps render in a browser. `--watch` keeps an incremental rollup bundler hot (CHOKIDAR_USEPOLLING for container fs; ~3-4s page re-bundle, skipped for model-only edits) and watches only model source (`.mpr`+`mprcontents/`). `--ensure-db` provisions the local Postgres + app database if missing; `--setup` does the non-blocking prerequisites (cache mxbuild+runtime, ensure DB) and exits — `mxcli init` wires it into a Claude Code SessionStart hook so a fresh/reaped web session self-bootstraps, and `docs-site/src/tools/bootstrap-prompt.md` is the empty-repo seed prompt. `--screenshot` captures a Playwright PNG each change (pixel-perfect page loop), with `--screenshot-url` deep links (repeatable for multi-page sets, one PNG per page) and `--screenshot-user`/`--screenshot-password` form login (session saved as Playwright storage state, reused via `screenshot --load-storage`). See `docs/11-proposals/PROPOSAL_mxcli_dev_warm_loop.md`
- OQL query execution against running runtime (`mxcli oql`)
- Business event services (SHOW/DESCRIBE/CREATE/DROP)
- Project settings (SHOW/DESCRIBE/ALTER)
Expand Down
27 changes: 26 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,20 @@ mxcli new MyApp --version 11.8.0

This downloads MxBuild, creates a blank Mendix project, sets up AI tooling and a Dev Container, and installs the correct mxcli binary. Open the resulting folder in VS Code and reopen in the Dev Container — you're ready to go.

### From the web or an iPad (empty repo, no local install)

No machine with a CLI? Open an **empty repo** in [Claude Code on the web](https://claude.ai/code) (works on an iPad) and paste the **bootstrap prompt** — the agent provisions the whole project (creates the app, wires the Dev Container + AI tooling, provisions the database) and commits it so future sessions self-bootstrap. This is the recommended web/iPad path; you don't need to pick a GitHub template.

> See **[Bootstrap Prompt](https://mendixlabs.github.io/mxcli/tools/bootstrap-prompt.html)** for the exact copy-paste text. In short: it runs `mxcli new` → `mxcli init` → commits the config → `mxcli run --local --setup --ensure-db` so the app comes up testable.

Then iterate with the **warm local dev loop** — a Docker-free ~1-second edit→test cycle:

```bash
mxcli run --local -p app.mpr --watch --screenshot # hot-reload + auto screenshots
```

`mxcli run --local` keeps `mxbuild --serve` and a standalone runtime hot: a page/microflow edit is hot-applied in seconds (a hot `reload_model`, or a restart + DDL for entity changes), and `--screenshot` captures each page with Playwright. See **[Local Dev Loop](https://mendixlabs.github.io/mxcli/tools/run-local.html)**.

### Existing project

For an existing Mendix project, use `mxcli init` to add AI tooling and a Dev Container:
Expand Down Expand Up @@ -166,7 +180,16 @@ mxcli add-tool cursor

## Installation

Download the latest release for your platform from the [releases page](https://github.com/mendixlabs/mxcli/releases), or build from source:
Download a pre-built binary from the [releases page](https://github.com/mendixlabs/mxcli/releases) — the assets are raw binaries named `mxcli-<os>-<arch>` (nothing to extract). While mxcli is fast-moving alpha, the rolling `nightly` build is recommended (new features land there first); pin a `vX.Y.Z` release for reproducibility:

```bash
# Linux/macOS — nightly
curl -fsSL -o mxcli \
https://github.com/mendixlabs/mxcli/releases/download/nightly/mxcli-linux-amd64
chmod +x mxcli && sudo mv mxcli /usr/local/bin/
```

Or build from source (Go + Make — `make build` runs the ANTLR parser generation that `go install` can't):

```bash
git clone https://github.com/mendixlabs/mxcli.git
Expand All @@ -175,6 +198,8 @@ make build
# binary is at ./bin/mxcli
```

> `go install …@latest` is not supported: the generated ANTLR parser isn't committed, so a module-source build fails. Use a pre-built binary or `make build`.

## Core Features

### Explore Project Structure
Expand Down
116 changes: 116 additions & 0 deletions cmd/mxcli/cmd_run.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
// SPDX-License-Identifier: Apache-2.0

package main

import (
"fmt"
"os"

"github.com/mendixlabs/mxcli/cmd/mxcli/docker"
"github.com/spf13/cobra"
)

var runCmd = &cobra.Command{
Use: "run",
Short: "Run a Mendix app locally in a warm dev loop",
Long: `Run a Mendix app with a warm, Docker-free dev loop (--local).

'mxcli run --local' keeps a mxbuild --serve process and a standalone Mendix
runtime hot. The first build is cold (~10-15s); after that a model change
rebuilds incrementally (~1s) and is applied without a full restart:

- page / microflow / text change -> hot reload_model (no restart)
- entity / view / association -> runtime restart (metamodel is
reconciled only at startup)

The serve build reports which is needed, so the right action is chosen
automatically. With --watch, mxcli rebuilds and hot-applies on every change.

Requirements:
- Mendix 11.x project (JDK 21; version-aware JDK selection is a follow-up)
- A reachable PostgreSQL (the devcontainer provides one); the database must
already exist. Defaults: 127.0.0.1:5432, user 'mendix', db from the project
name. Override with --db-host/--db-name/--db-user/--db-password.

Examples:
mxcli run --local -p app.mpr
mxcli run --local -p app.mpr --watch
mxcli run --local -p app.mpr --app-port 8081 --db-name myapp
`,
Run: func(cmd *cobra.Command, args []string) {
local, _ := cmd.Flags().GetBool("local")
if !local {
fmt.Fprintln(os.Stderr, "Error: only --local is supported for now (use 'mxcli docker run' for the container workflow)")
os.Exit(1)
}
projectPath, _ := cmd.Flags().GetString("project")
if projectPath == "" {
fmt.Fprintln(os.Stderr, "Error: --project (-p) is required")
os.Exit(1)
}

watch, _ := cmd.Flags().GetBool("watch")
ensureDB, _ := cmd.Flags().GetBool("ensure-db")
setupOnly, _ := cmd.Flags().GetBool("setup")
appPort, _ := cmd.Flags().GetInt("app-port")
adminPort, _ := cmd.Flags().GetInt("admin-port")
servePort, _ := cmd.Flags().GetInt("serve-port")
dbHost, _ := cmd.Flags().GetString("db-host")
dbName, _ := cmd.Flags().GetString("db-name")
dbUser, _ := cmd.Flags().GetString("db-user")
dbPassword, _ := cmd.Flags().GetString("db-password")
screenshot, _ := cmd.Flags().GetBool("screenshot")
screenshotPath, _ := cmd.Flags().GetString("screenshot-path")
screenshotURLs, _ := cmd.Flags().GetStringArray("screenshot-url")
screenshotUser, _ := cmd.Flags().GetString("screenshot-user")
screenshotPassword, _ := cmd.Flags().GetString("screenshot-password")

opts := docker.LocalRunOptions{
ProjectPath: projectPath,
AppPort: appPort,
AdminPort: adminPort,
ServePort: servePort,
Watch: watch,
EnsureDB: ensureDB,
SetupOnly: setupOnly,
Screenshot: screenshot,
ScreenshotPath: screenshotPath,
ScreenshotURLs: screenshotURLs,
ScreenshotUser: screenshotUser,
ScreenshotPassword: screenshotPassword,
DB: docker.DBConfig{
Host: dbHost,
Name: dbName,
User: dbUser,
Password: dbPassword,
},
Stdout: os.Stdout,
Stderr: os.Stderr,
}

if err := docker.RunLocal(opts); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
},
}

func init() {
runCmd.Flags().Bool("local", false, "Run locally without Docker (warm serve + standalone runtime)")
runCmd.Flags().Bool("watch", false, "Rebuild and hot-apply on every project change")
runCmd.Flags().Bool("ensure-db", false, "Provision the local Postgres + app database if missing (fresh-session bootstrap)")
runCmd.Flags().Bool("setup", false, "Prepare prerequisites (cache MxBuild+runtime, ensure DB) and exit without booting — for a SessionStart hook")
runCmd.Flags().Int("app-port", 0, "HTTP port for the app (default 8080)")
runCmd.Flags().Int("admin-port", 0, "M2EE admin API port (default 8090)")
runCmd.Flags().Int("serve-port", 0, "mxbuild --serve port (default 6543)")
runCmd.Flags().String("db-host", "", "Database host:port (default 127.0.0.1:5432)")
runCmd.Flags().String("db-name", "", "Database name (default derived from the project name)")
runCmd.Flags().String("db-user", "", "Database user (default mendix)")
runCmd.Flags().String("db-password", "", "Database password (default mendix)")
runCmd.Flags().Bool("screenshot", false, "Capture a Playwright screenshot after boot and each applied change")
runCmd.Flags().String("screenshot-path", "", "Screenshot output PNG (default <projectDir>/.mxcli/run-local.png)")
runCmd.Flags().StringArray("screenshot-url", nil, "Page to screenshot: a full URL or a path relative to the app root, e.g. /p/customers (default the app root). Repeat for a multi-page set.")
runCmd.Flags().String("screenshot-user", "", "Log in with this user before screenshotting (for pages behind login)")
runCmd.Flags().String("screenshot-password", "", "Password for --screenshot-user")
rootCmd.AddCommand(runCmd)
}
Loading
Loading