diff --git a/.gitignore b/.gitignore index 00a0e28..e234711 100644 --- a/.gitignore +++ b/.gitignore @@ -51,3 +51,5 @@ wip/ # rendered by bug-logs/pending-github-issues/render-paste-ready.sh — regenerate, never commit bug-logs/pending-github-issues/paste-ready/ +# Machine-local tool locations read by project-bin/_common.sh — never commit paths +.claude/toolkit.env diff --git a/CHANGELOG.md b/CHANGELOG.md index e95c038..efb8505 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -72,6 +72,7 @@ Credit the person or project that surfaced the change — the credit line is the - learn(bug-logs): BUG-115 addendum + its upstream draft (`bug115-file-manager-widget-not-authorable.md`) — a second project independently found that MDL cannot author a **File Manager** widget (six spellings all `mismatched input … expecting '}'`), and added the costly half: `DESCRIBE PAGE` of a page that HAS one emits it as a `-- NOT re-executable` comment, and that script **passes `check --references` clean** — so the documented describe → edit → `CREATE OR REPLACE PAGE` loop deletes the widget with no error at any gate. Verified against `AgentCommons.AgentImportExportFile_NewEdit`, which ships one. The comment is good behaviour and the draft says so; the ask is a grammar production, or failing that a `check` warning on the marker, since a pipeline reads the gate and not the comment — a sales-coaching build (logged on its branch as BUG-116; folded into master's BUG-115 at merge, the same defect) - learn(bug-logs): BUG-127 (`ALTER PAGE … REPLACE` of a **pluggable** widget silently drops properties you did write — a Combobox came back with no `Attribute`, quoted and unquoted alike, and still rendered a normal-looking dropdown bound to nothing; `check --references` and mxbuild both clean, only `DESCRIBE PAGE` shows it), BUG-128 (`CREATE MODULE ROLE` has no `OR MODIFY` / `IF NOT EXISTS`, so a re-run aborts on it and every statement *after* it silently does not run — keep non-idempotent statements in their own once-applied script), BUG-129 (`exec` logged `Created microflow:` ×7 plus a page, passed the mxbuild gate and exited 0, and the model afterwards held the role and all six grants from the same script and **none** of the documents; root cause not established, logged as observed rather than diagnosed). The practice BUG-129 forces is worth having regardless of the bug: **read the model back with `SHOW` after every exec — exit 0 plus a log of `Created …` lines is a claim about what the tool tried, not a fact about the model** — a sales-coaching build (renumbered from BUG-113/114/115 at merge; those numbers, and 107–109 before them, were taken on master while the branch was open) - new(guide/README): "Where you run this" visual — three lanes (cloud, devcontainer, local) on one shared project, the Windows Docker-on-WSL2 vs WSL-as-shell distinction, the build-headless/polish-in-Studio-Pro rhythm with the one-writer rule, and a side-by-side "why a Dev Container instead of local" table; SVG in `toolkit-guide.html` §5 (later sections renumbered), mermaid + prose in `README.md`; `doctor.sh`'s devcontainer note now says SP modes are unavailable *here*, not unavailable — same onboarding thread as the line below — Clinton Chee, via Maurits Visser +- fix(doctor/_common): four Windows onboarding defects from one field run (Git Bash, Studio Pro 10.24 + 11.8 + 11.11 side by side, project shared with a Dev Container). (1) `find_sp_app` glued two `ls` listings without a newline and listed the same Program Files root twice, producing ".../Mendix/gradle-8.5//c/Program Files/Mendix/10.24..." as the "highest" install — now newline-joined, deduped, and only version-shaped dirs count. (2) `check-scripts.sh` reported FAIL on a project without `bin/` — now a WARN pointing at init-project. (3) A project's `./mxcli` can be the Dev Container's Linux ELF binary, which Git Bash can never run and `chmod +x` cannot fix — new `find_project_mxcli` prefers `mxcli.exe` on Windows and names the ELF case; doctor `--install` fetches mxcli.exe beside it instead of refusing; exec.sh, lint-gate, review-module, conformance-check, fixture-manifest, page-scope use the resolver. (4) The macOS-only save-sp/restart-sp section is a note, not a WARN, off macOS. Plus `toolkit.env` — `/.claude/toolkit.env` or `~/.mxcli-toolkit.env`, KEY=VALUE, Windows paths pasted as-is — loaded by `_common.sh` for MENDIX_APP / JAVA_HOME / MXBUILD_PATH / MXCLI_VERSION / PYTHON; doctor reports which loaded, self-gitignores the project file, flags a JRE-without-javac, and its gate/verdict wording now guides instead of alarms. Fixture-proven here (glue, precedence, .exe-over-ELF); Windows field run still owed — the PR stays draft until that run is cited here (and a golden `ls -d` capture of the glued Studio Pro path sits beside a fixture); merge review 2026-09-08 also routed the pasted `.exe` probe in page-scope.sh and fixture-manifest.sh through `find_project_mxcli`, dropped a dead `MXCLI_LINUX_SEEN`, corrected the "gitignored by init-project" comment (doctor.sh appends it) and the WARN verdict wording — Clinton Chee, via Maurits Visser - new(guide/README): "Where you run this" visual — three lanes (cloud, devcontainer, local) on one shared project, the Windows Docker-on-WSL2 vs WSL-as-shell distinction, the build-headless/polish-in-Studio-Pro rhythm with the one-writer rule, and a side-by-side "why a Dev Container instead of local" table; SVG in `toolkit-guide.html` §5 (later sections renumbered), the same SVG exported to `docs/where-you-run-this.svg` and embedded in `README.md` (replaces the mermaid draft) + prose; `doctor.sh`'s devcontainer note now says SP modes are unavailable *here*, not unavailable — same onboarding thread as the line below — Clinton Chee, via Maurits Visser - fix(docs/README): the "Git Bash, not WSL" warning now says explicitly that Docker Desktop's WSL2 backend (the "install Docker on WSL" prompt from `mxcli new`'s Dev Container) is fine and is the `devcontainer` lane, and that the lanes mix freely on one project with one writer on the `.mpr` at a time — a colleague onboarding via `mxcli new` read the warning as "say No to Docker on WSL" — Clinton Chee, via Maurits Visser diff --git a/bin/check-scripts.sh b/bin/check-scripts.sh index e0c6b85..215e618 100755 --- a/bin/check-scripts.sh +++ b/bin/check-scripts.sh @@ -97,7 +97,13 @@ if [ -n "$PROJECT_DIR" ]; then # The installed copies. bin/ exists on every wired project; tests/e2e/ only after # install-tests.sh — its absence is install-manifest's finding, not a parse failure, # so probe it only when present. - check_sh "project bin/*.sh" "$PROJECT_DIR"/bin/*.sh + if [ -d "$PROJECT_DIR/bin" ]; then + check_sh "project bin/*.sh" "$PROJECT_DIR"/bin/*.sh + else + # Not wired yet is not a parse failure — bin/ arrives with init-project.sh. Reported as + # FAIL once (Windows onboarding, 2026-09-08) and read as "something is broken". + printf ' WARN project has no bin/ yet — run bin/init-project.sh %s first; nothing to parse-check until then\n' "$PROJECT_DIR" + fi if [ -d "$PROJECT_DIR/tests/e2e" ]; then check_js "project tests/e2e/*.js" "$PROJECT_DIR"/tests/e2e/*.js fi diff --git a/bin/doctor.sh b/bin/doctor.sh index 4250363..f9cebf0 100755 --- a/bin/doctor.sh +++ b/bin/doctor.sh @@ -307,10 +307,12 @@ case "$PLATFORM" in fi ;; gitbash|wsl|linux|unknown) - warn "The Studio Pro scripts (save-sp.sh, restart-sp.sh, and the SP handling in exec.sh)" - note "are macOS-only: they are built on osascript, lsof and 'open -a', and Studio Pro is" - note "discovered by looking in /Applications. There is no Windows port yet." - note "Everything else in the toolkit works here. Drive Studio Pro by hand on this platform." + # Informational only. Nobody needs these scripts to use the toolkit — Studio Pro is + # opened and saved by hand at the points where they would run — so on the platforms + # they do not exist for, this is not a warning. (It was one; a Windows onboarding + # counted it among "6 warnings" and asked what to do about it. Nothing.) + note "save-sp.sh / restart-sp.sh (macOS conveniences) are not on this platform — you open" + note "and save Studio Pro yourself at those points. Nothing else depends on them." ;; esac @@ -342,6 +344,18 @@ MXBUILD="$(find_mxbuild 2>/dev/null || true)" JAVA_HOME_FOUND="$(find_java 2>/dev/null || true)" JAVA_EXE="$(find_java_exe 2>/dev/null || true)" +# toolkit.env — the human-editable answer to "doctor guessed the wrong Studio Pro / Java". +# _common.sh already loaded it (project file, then ~/.mxcli-toolkit.env; a variable set in +# the shell wins over both). Say what was read, so a stale value is findable. +if [ -n "${MXTK_ENV_LOADED:-}" ]; then + ok "toolkit.env loaded: $MXTK_ENV_LOADED" +else + note "No toolkit.env — discovery guesses below. To pin tool locations for this machine, create" + note " ${PROJECT_DIR:-}/.claude/toolkit.env (this project) or ~/.mxcli-toolkit.env (every project)" + note " with lines like: MENDIX_APP=C:\\Program Files\\Mendix\\11.11.0 JAVA_HOME=C:\\Software\\Java\\jdk-21" + note " (Windows paths can be pasted as-is; MXBUILD_PATH, MXCLI_VERSION and PYTHON work too.)" +fi + # --install: when no runnable mxbuild was discovered, download the standalone toolchain # through the project's own mxcli — `./mxcli setup mxbuild -p .mpr`, the exact download # the headless container build runs, cached at ~/.mxcli/mxbuild// and shared across @@ -357,15 +371,15 @@ install_toolchain() { bad "--install: no .mpr in $PROJECT_DIR — setup mxbuild needs the model to pick a version." return fi - note "downloading the mxbuild toolchain (./mxcli setup mxbuild — same as the container build)..." - if (cd "$PROJECT_DIR" && ./mxcli setup mxbuild -p "$(basename "$INSTALL_MPR")"); then + note "downloading the mxbuild toolchain (mxcli setup mxbuild — same as the container build)..." + if (cd "$PROJECT_DIR" && "$PMXCLI" setup mxbuild -p "$(basename "$INSTALL_MPR")"); then SP_APP="$(find_sp_app 2>/dev/null || true)" MXBUILD="$(find_mxbuild 2>/dev/null || true)" JAVA_HOME_FOUND="$(find_java 2>/dev/null || true)" JAVA_EXE="$(find_java_exe 2>/dev/null || true)" ok "toolchain downloaded to ~/.mxcli/mxbuild/ (shared cache, reused by every project)" else - bad "'./mxcli setup mxbuild' failed — see its output above." + bad "'mxcli setup mxbuild' failed — see its output above." note "A blocked network/proxy is the usual cause; the download comes from the Mendix CDN." fi } @@ -376,7 +390,8 @@ if [ "$INSTALL" -eq 1 ]; then NEED_TOOLCHAIN=1 if [ -n "$MXBUILD" ] && [ -x "$MXBUILD" ] && probe_runs "$MXBUILD"; then NEED_TOOLCHAIN=0; fi NEED_MXCLI=0 - if [ -n "$PROJECT_DIR" ] && [ ! -x "$PROJECT_DIR/mxcli" ] && [ ! -f "$PROJECT_DIR/mxcli" ]; then + PMXCLI="$(find_project_mxcli 2>/dev/null || true)" + if [ -n "$PROJECT_DIR" ] && [ -z "$PMXCLI" ]; then NEED_MXCLI=1 fi @@ -386,7 +401,14 @@ if [ "$INSTALL" -eq 1 ]; then bad "--install needs a project directory: bin/doctor.sh --install " note "The download runs through that project's own ./mxcli, which reads the model's" note "Mendix version and fetches the matching toolchain." - elif [ -f "$PROJECT_DIR/mxcli" ] && [ ! -x "$PROJECT_DIR/mxcli" ]; then + elif [ "$NEED_MXCLI" -eq 1 ] && [ "$PLATFORM" = gitbash ] && mxtk_is_elf "$PROJECT_DIR/mxcli"; then + # Not a permissions problem, and chmod cannot fix it (Git Bash derives the executable bit + # from the extension/header, so `chmod +x` looks like it "reverts"). The file is the Linux + # build the Dev Container uses on this same folder. Windows needs mxcli.exe beside it. + note "--install: $PROJECT_DIR/mxcli is the Linux build (the Dev Container's) — fine, leave it." + note "Git Bash needs mxcli.exe next to it; fetching that now, the two coexist." + NEED_MXCLI=1; PMXCLI="" + elif [ -f "$PROJECT_DIR/mxcli" ] && [ ! -x "$PROJECT_DIR/mxcli" ] && [ "$PLATFORM" != gitbash ]; then bad "--install: mxcli in $PROJECT_DIR is present but not executable — chmod +x mxcli, re-run." else # The release assets follow one naming scheme (verified against the published releases, @@ -445,9 +467,9 @@ if [ "$INSTALL" -eq 1 ]; then note " $_STEP. mxbuild toolchain (~800 MB, one-time) -> ~/.mxcli/mxbuild//" note " verifies every model write (the exec.sh gate); from cdn.mendix.com, exact" note " version read from the project's .mpr — the same download the container build uses." - if [ "$NEED_MXCLI" -eq 0 ] && [ -x "$PROJECT_DIR/mxcli" ]; then + if [ "$NEED_MXCLI" -eq 0 ] && [ -n "$PMXCLI" ]; then _PLAN_MPR="$(ls "$PROJECT_DIR"/*.mpr 2>/dev/null | head -1)" - [ -n "$_PLAN_MPR" ] && (cd "$PROJECT_DIR" && ./mxcli setup mxbuild -p "$(basename "$_PLAN_MPR")" --dry-run 2>/dev/null) \ + [ -n "$_PLAN_MPR" ] && (cd "$PROJECT_DIR" && "$PMXCLI" setup mxbuild -p "$(basename "$_PLAN_MPR")" --dry-run 2>/dev/null) \ | grep -E 'Version:|URL:' | while IFS= read -r l; do note " $l"; done fi fi @@ -478,6 +500,7 @@ if [ "$INSTALL" -eq 1 ]; then if $MXCLI_FETCH "$PROJECT_DIR/$MXCLI_DEST" "$MXCLI_URL" && chmod +x "$PROJECT_DIR/$MXCLI_DEST" \ && probe_runs "$PROJECT_DIR/$MXCLI_DEST"; then ok "mxcli fetched into the project root: $(probe_line)" + PMXCLI="$PROJECT_DIR/$MXCLI_DEST" else rm -f "$PROJECT_DIR/$MXCLI_DEST" 2>/dev/null MXCLI_READY=0 @@ -542,8 +565,9 @@ else else bad "mxbuild not found/executable: ${MXBUILD:-}" fi - note "Fix without Studio Pro: bin/doctor.sh --install (downloads it locally," - note "like the container build). Or set MXBUILD_PATH=/path/to/mxbuild." + note "Have Studio Pro? Point at it: MENDIX_APP= in toolkit.env (see above)." + note "No Studio Pro: bin/doctor.sh --install downloads a standalone mxbuild," + note "like the container build. MXBUILD_PATH= also works." fi # Java: mxbuild is invoked with an explicit --java-exe-path; if none resolves, the gate skips. @@ -553,6 +577,15 @@ if [ -n "$JAVA_EXE" ] && [ -x "$JAVA_EXE" ]; then JV="$("$JAVA_EXE" -version 2>&1 | grep -i 'version' | head -1)" || JV="" if [ -n "$JV" ]; then ok "java runs: $JV ($JAVA_EXE)" + # mxbuild compiles the model's Java actions, which takes javac — a JRE has none. Studio + # Pro's bundled runtime is a JRE that mxbuild is paired with, so only flag a system Java. + case "$JAVA_HOME_FOUND" in + "${SP_APP:-}"/*) ;; + *) if [ ! -x "$(dirname "$JAVA_EXE")/javac" ] && [ ! -x "$(dirname "$JAVA_EXE")/javac.exe" ]; then + note "this Java is a JRE (no javac). Java actions in the model need a JDK to compile:" + note "set JAVA_HOME= in toolkit.env if a build complains about javac." + fi ;; + esac else GATE_OK=0 bad "java exists but produced no version output: $JAVA_EXE" @@ -580,14 +613,15 @@ else if [ "$PLATFORM" = linux ] && [ -z "$SP_APP" ] && [ -z "${MXBUILD_PATH:-}" ]; then warn "exec.sh mxbuild gate WILL BE SKIPPED on this machine." else - bad "exec.sh mxbuild gate WILL BE SKIPPED on this machine. Fix the FAIL lines above" - note "before writing to any model from this machine." + bad "the mxbuild gate would be skipped on this machine — sort out the mxbuild/java lines" + note "above before the first model write, so every write gets verified." fi - note "Every MDL exec here will report gate=skipped and go through UNVERIFIED: consistency" - note "errors (CE) are never captured, and BSON corruption — which mxbuild is the only" - note "reliable detector for — reaches Studio Pro undetected." - note "Fastest fix on ANY platform: bin/doctor.sh --install — downloads the" - note "toolchain locally through the project's ./mxcli, exactly like the container build." + note "Why it matters: without mxbuild, MDL execs report gate=skipped and consistency errors" + note "go uncaught until Studio Pro opens the model. Usually one of these fixes it:" + note " - Studio Pro is installed: put its folder in toolkit.env, e.g." + note " MENDIX_APP=C:\\Program Files\\Mendix\\11.11.0 (pick the version your .mpr uses)" + note " - No Studio Pro here: bin/doctor.sh --install downloads a standalone" + note " mxbuild (the same one the container build uses)." fi if [ "$QUICK" != 1 ]; then @@ -706,18 +740,26 @@ if [ -n "$PROJECT_DIR" ]; then warn "no .mpr found in the project root" note "If the model lives elsewhere, point doctor at the folder that contains it;" note "a brand-new project gets its .mpr from 'mxcli init' or a Studio Pro export."; } - if [ -x "$PROJECT_DIR/mxcli" ]; then + PMXCLI_PROBE="$(find_project_mxcli 2>/dev/null || true)" + if [ -n "$PMXCLI_PROBE" ]; then # Present is not enough — a wrong-platform binary is present, executable, and exits 126. MXCLI_EXIT=0 - probe_runs "$PROJECT_DIR/mxcli" || MXCLI_EXIT=$? + probe_runs "$PMXCLI_PROBE" || MXCLI_EXIT=$? if [ "$MXCLI_EXIT" -eq 0 ]; then ok "mxcli runs ($PROBE_HOW): $(probe_line)" + note "at: $PMXCLI_PROBE" else - bad "mxcli exists but cannot run (exit $MXCLI_EXIT)" + bad "mxcli exists but cannot run (exit $MXCLI_EXIT): $PMXCLI_PROBE" printf '%s\n' "$PROBE_OUT" | grep -v '^[[:space:]]*$' | head -3 | while IFS= read -r l; do note " $l"; done note "Exit 126 usually means a binary built for another platform/architecture —" note "re-download the mxcli build for this OS." fi + elif [ "$PLATFORM" = gitbash ] && mxtk_is_elf "$PROJECT_DIR/mxcli"; then + warn "the project's mxcli is the Linux build (the Dev Container's) — Git Bash needs mxcli.exe beside it." + note "Not a permissions problem: chmod cannot make a Linux binary run here, which is why it" + note "looked like it 'reverted'. Leave the file for the container and add the Windows build:" + note " bin/doctor.sh --install $PROJECT_DIR (fetches mxcli.exe; the two coexist)" + note " or copy the mxcli.exe you already have on PATH into the project root." elif [ -f "$PROJECT_DIR/mxcli" ]; then bad "mxcli is present but not executable — chmod +x mxcli" else warn "no mxcli in the project root" @@ -755,14 +797,20 @@ if [ -n "$PROJECT_DIR" ] && [ -d "$PROJECT_DIR" ]; then >> "$PROJECT_DIR/.gitignore" 2>/dev/null || true note "(added /.claude/.doctor-receipt to the project's .gitignore — the receipt is machine-local)" fi + # toolkit.env holds this machine's paths — never something to commit. + if [ -d "$PROJECT_DIR/.git" ] && ! git -C "$PROJECT_DIR" check-ignore -q .claude/toolkit.env 2>/dev/null; then + printf '\n# Machine-local tool locations (bin/doctor.sh / project-bin/_common.sh)\n/.claude/toolkit.env\n' \ + >> "$PROJECT_DIR/.gitignore" 2>/dev/null || true + note "(added /.claude/toolkit.env to the project's .gitignore — it holds this machine's paths)" + fi fi if [ "$FAIL" -gt 0 ]; then - printf ' %s problem(s) that will break a pipeline stage, %s warning(s).\n' "$FAIL" "$WARN" - printf ' Fix the FAIL lines above before running anything else.\n' + printf ' %s thing(s) to sort out before the first model write (FAIL lines), %s note(s) worth reading (WARN).\n' "$FAIL" "$WARN" + printf ' Analysis and planning stages run fine meanwhile; each FAIL line says what fixes it.\n' exit 2 elif [ "$WARN" -gt 0 ]; then - printf ' Ready, with %s warning(s). Read them — each one names something that will not work.\n' "$WARN" + printf ' Ready. %s warning(s) above — read each one: some are optional, a missing .mpr or CLAUDE.local.md is not.\n' "$WARN" exit 1 else printf ' Ready.\n' diff --git a/project-bin/_common.sh b/project-bin/_common.sh index 637448d..5fe3dbe 100755 --- a/project-bin/_common.sh +++ b/project-bin/_common.sh @@ -44,6 +44,59 @@ _mxtk_resolve_root() { } PROJECT_ROOT="${PROJECT_ROOT:-$(_mxtk_resolve_root)}" +# mxtk_posix_path — a Windows-form path (C:\Program Files\Mendix) as Git Bash addresses it +# (/c/Program Files/Mendix). Anything already POSIX passes through untouched. Used for every +# path that arrives from the environment or toolkit.env, because people copy paths out of +# Explorer, and `[ -d "C:\..." ]` is false in bash even when the folder exists. +mxtk_posix_path() { + local p="$1" + case "$p" in + [A-Za-z]:*) p="/$(printf '%s' "${p%%:*}" | tr '[:upper:]' '[:lower:]')${p#*:}" + p=$(printf '%s' "$p" | tr '\\' '/') ;; + esac + printf '%s\n' "$p" +} + +# --- toolkit.env: where the tools live on THIS machine ------------------------------------- +# Discovery below guesses (Program Files\Mendix, /Applications, JAVA_HOME, PATH). When the +# guess is wrong the fix used to be "export MENDIX_APP=... in every shell" — which nobody +# remembers between sessions, and which an agent's subshell never sees. So the same overrides +# can live in a file, KEY=VALUE, one per line, # comments: +# +# /.claude/toolkit.env this project on this machine (doctor.sh appends it to .gitignore on its first run — commit that line) +# ~/.mxcli-toolkit.env every project on this machine +# +# Precedence: a variable already set in the environment wins; then the project file; then +# the user file. Windows paths may be pasted as-is (C:\Program Files\Mendix\11.11.0). +# Keys the toolkit reads: MENDIX_APP, MXBUILD_PATH, JAVA_HOME, MXCLI_VERSION, MXCLI_HOME, +# PYTHON. Unknown keys are exported too, harmlessly. bin/doctor.sh prints which files loaded. +# Field origin: a Windows onboarding (2026-09-08) with Studio Pro 10.24 + 11.8 + 11.11 side by +# side and a JDK that was not the JRE on PATH — four overrides, none of them discoverable. +MXTK_ENV_LOADED="" +mxtk_load_env() { + local f line key val + for f in "$PROJECT_ROOT/.claude/toolkit.env" "${HOME:-/nonexistent}/.mxcli-toolkit.env"; do + [ -f "$f" ] || continue + MXTK_ENV_LOADED="${MXTK_ENV_LOADED:+$MXTK_ENV_LOADED }$f" + while IFS= read -r line || [ -n "$line" ]; do + line=${line%$'\r'} + case "$line" in ''|'#'*) continue ;; esac + case "$line" in *=*) ;; *) continue ;; esac + key=$(printf '%s' "${line%%=*}" | tr -d '[:space:]'); val=${line#*=} + case "$key" in [A-Za-z_]*) ;; *) continue ;; esac + case "$key" in *[!A-Za-z0-9_]*) continue ;; esac + # export KEY=value and "KEY = value" both work; surrounding quotes are stripped. + case "$val" in \"*\") val=${val#\"}; val=${val%\"} ;; \'*\') val=${val#\'}; val=${val%\'} ;; esac + val=$(printf '%s' "$val" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//') + val=$(mxtk_posix_path "$val") + # Already set (by the shell, or by the project file on the previous pass) — keep it. + eval "[ -n \"\${$key:-}\" ]" && continue + export "$key=$val" + done < "$f" + done +} +mxtk_load_env + # --------------------------------------------------------------------------- # find_mpr — echo the project's single .mpr, or fail loudly. # @@ -159,8 +212,8 @@ mxtk_platform() { # than quietly return a wrong answer. $MENDIX_APP overrides. # --------------------------------------------------------------------------- find_sp_app() { - if [ -n "${MENDIX_APP:-}" ]; then echo "$MENDIX_APP"; return 0; fi - local list="" root + if [ -n "${MENDIX_APP:-}" ]; then mxtk_posix_path "$MENDIX_APP"; return 0; fi + local list="" root seen="" case "$(mxtk_platform)" in macos) list=$(ls -d /Applications/Mendix\ Studio\ Pro*.app 2>/dev/null) || true @@ -180,15 +233,22 @@ find_sp_app() { "/d/Program Files" "/d/Mendix" "/c/Mendix"; do [ -n "$root" ] || continue # Env vars arrive in Windows form (C:\Program Files); make them POSIX. - case "$root" in - [A-Za-z]:*) root="/$(printf '%s' "${root%%:*}" | tr '[:upper:]' '[:lower:]')${root#*:}" - root=$(printf '%s' "$root" | tr '\\' '/') ;; - esac + root=$(mxtk_posix_path "$root") [ -d "$root/Mendix" ] && root="$root/Mendix" [ -d "$root" ] || continue - list="$list$(ls -d "$root"/*/ 2>/dev/null)" + # ProgramW6432, PROGRAMFILES and the literal /c/Program Files are usually the SAME + # folder — list it once. And join with a newline: `$(ls)` strips the trailing one, so + # appending two listings glued the last entry of one onto the first of the next. + # Real output (Windows, 2026-09-08): ".../Mendix/gradle-8.5//c/Program Files/Mendix/ + # 10.24.14.90436" — a path that exists nowhere, ranked highest by sort -V. + case "$seen" in *"|$root|"*) continue ;; esac + seen="$seen|$root|" + list="$list$(ls -d "$root"/*/ 2>/dev/null) +" done - list=$(printf '%s\n' "$list" | sed 's:/*$::' | grep -v '^$') || true + # Only version-shaped directories are Studio Pro installs. Mendix also drops gradle-8.5, + # a shared-tools folder, and the like under the same root, and none of them has a modeler/. + list=$(printf '%s\n' "$list" | sed 's:/*$::' | grep -v '^$' | grep -E '/[0-9]+\.[0-9]+(\.[0-9]+)*(\.[0-9]+)?$') || true [ -z "$list" ] && { echo "ERROR: no Mendix Studio Pro install found under Program Files\\Mendix." >&2 echo " Set MENDIX_APP= or MXBUILD_PATH=." >&2 @@ -210,6 +270,33 @@ find_sp_app() { } # --------------------------------------------------------------------------- +# mxtk_is_elf — true when the file is a Linux (ELF) binary. On Git Bash that is the +# one thing a project's ./mxcli can be that no chmod will ever fix. +mxtk_is_elf() { [ -f "$1" ] && [ "$(head -c 4 "$1" 2>/dev/null | tr -d '\177')" = "ELF" ]; } + +# find_project_mxcli — the project's own mxcli binary, or fail. +# +# A project folder is shared between lanes (a Dev Container and Git Bash on the same disk), +# and each lane needs its own build of mxcli: the container's is a Linux ELF binary named +# `mxcli`, Windows' is `mxcli.exe`. Git Bash maps `mxcli` -> `mxcli.exe` transparently ONLY +# when no file called `mxcli` exists; once the container has put its binary there, every +# `./mxcli` and `[ -x mxcli ]` on the Windows side hits the ELF file instead and fails with +# exit 126 — and `chmod +x` appears to "revert", because MSYS derives the executable bit +# from the extension/header, not from mode bits. Real report, Windows, 2026-09-08. +# So on Windows, mxcli.exe is looked for first, explicitly. +find_project_mxcli() { + local c + if [ "$(mxtk_platform)" = windows ]; then + for c in "$PROJECT_ROOT/mxcli.exe" "$PROJECT_ROOT/mxcli"; do + [ -x "$c" ] && ! mxtk_is_elf "$c" && { echo "$c"; return 0; } + done + return 1 + fi + c="$PROJECT_ROOT/mxcli" + [ -x "$c" ] && { echo "$c"; return 0; } + return 1 +} + # find_mxcli_cache — newest version dir in the mxcli download cache # (~/.mxcli/mxbuild//), or fail. # diff --git a/project-bin/conformance-check.sh b/project-bin/conformance-check.sh index 0d43358..e916b35 100755 --- a/project-bin/conformance-check.sh +++ b/project-bin/conformance-check.sh @@ -46,7 +46,7 @@ set -uo pipefail cd "$PROJECT_ROOT" || exit 2 MPR="$(find_mpr)" || exit 2 -MXCLI="./mxcli" +MXCLI="$(find_project_mxcli 2>/dev/null || echo ./mxcli)" OUTDIR="docs/conformance" BASELINE="$OUTDIR/baseline.tsv" MODULE="" diff --git a/project-bin/exec.sh b/project-bin/exec.sh index 207e1ba..06dc498 100755 --- a/project-bin/exec.sh +++ b/project-bin/exec.sh @@ -251,9 +251,10 @@ HDR2 # enforced nowhere — the pipeline went snapshot → exec → mxbuild. Costs ~2s on # a passing build. SKIP_CHECK=1 for the rare script mxcli's parser rejects but # the model accepts (log why, in the script). -if [ "${SKIP_CHECK:-0}" != "1" ] && [ -x "$PROJECT_ROOT/mxcli" ]; then +PMXCLI="$(find_project_mxcli 2>/dev/null || true)" +if [ "${SKIP_CHECK:-0}" != "1" ] && [ -n "$PMXCLI" ]; then echo "→ Pre-exec check: mxcli check (grammar + references)..." - if ! ./mxcli check "$SCRIPT" -p "$MPR" --references; then + if ! "$PMXCLI" check "$SCRIPT" -p "$MPR" --references; then echo "" echo " ✗ mxcli check failed — refusing to exec. NOTHING was written to the model." echo " Fix the script, or re-run with SKIP_CHECK=1 if you know why the parser is wrong." @@ -407,7 +408,7 @@ fi # it. A failed exec is precisely when the gate matters most. echo "→ Executing $SCRIPT..." EXEC_STATUS=0 -./mxcli exec "$SCRIPT" -p "$MPR" || EXEC_STATUS=$? +"${PMXCLI:-./mxcli}" exec "$SCRIPT" -p "$MPR" || EXEC_STATUS=$? if [ "$EXEC_STATUS" -ne 0 ]; then echo "" diff --git a/project-bin/fixture-manifest.sh b/project-bin/fixture-manifest.sh index d5dad26..1f7cd6c 100755 --- a/project-bin/fixture-manifest.sh +++ b/project-bin/fixture-manifest.sh @@ -46,7 +46,10 @@ while [ $# -gt 0 ]; do done # ── Resolve the project and the admin API ──────────────────────────────────── -MXCLI="${MXCLI:-./mxcli}" +# Resolve the binary the same way every other script does (mxcli.exe on Git Bash, never an +# ELF mxcli left by a Dev Container) — one resolver in _common.sh, not a pasted uname case. +_mxcli_default="$(find_project_mxcli 2>/dev/null || echo ./mxcli)" +MXCLI="${MXCLI:-$_mxcli_default}" [ -x "$MXCLI" ] || { echo "FAULT: $MXCLI not executable — run from the project root" >&2; exit 2; } if [ -z "${PROJECT_MPR:-}" ]; then diff --git a/project-bin/lint-gate.sh b/project-bin/lint-gate.sh index 133103f..95b711a 100755 --- a/project-bin/lint-gate.sh +++ b/project-bin/lint-gate.sh @@ -79,7 +79,12 @@ for a in "$@"; do esac done -[ -x ./mxcli ] || { echo "lint-gate: ./mxcli not found in $ROOT" >&2; exit 2; } +# This script does not source _common.sh (see header); resolve the binary the same way it +# does — mxcli.exe first on Git Bash, since the Dev Container may have left a Linux `mxcli`. +MXCLI="" +case "$(uname -s 2>/dev/null)" in MINGW*|MSYS*|CYGWIN*) [ -x ./mxcli.exe ] && MXCLI=./mxcli.exe ;; esac +[ -n "$MXCLI" ] || { [ -x ./mxcli ] && MXCLI=./mxcli; } +[ -n "$MXCLI" ] || { echo "lint-gate: no runnable mxcli in $ROOT (Windows: mxcli.exe) — bin/doctor.sh --install" >&2; exit 2; } [ -e "$MPR" ] || { echo "lint-gate: $MPR not found" >&2; exit 2; } # Build the --exclude list from the vendor file (comments and blanks stripped). @@ -100,9 +105,9 @@ else # First run on a cold catalog can take MINUTES (measured: 717s on PROJECT-A, 2s warm) -- # the catalog rebuild dominates, not the lint. Do not treat a slow first run as a hang. if [ -n "$EXCLUDE" ]; then - ./mxcli lint -p "$MPR" -e "$EXCLUDE" --format json > "$OUT" 2>/dev/null + "$MXCLI" lint -p "$MPR" -e "$EXCLUDE" --format json > "$OUT" 2>/dev/null else - ./mxcli lint -p "$MPR" --format json > "$OUT" 2>/dev/null + "$MXCLI" lint -p "$MPR" --format json > "$OUT" 2>/dev/null fi fi [ -s "$OUT" ] || { echo "lint-gate: lint produced no output" >&2; exit 2; } diff --git a/project-bin/page-scope.sh b/project-bin/page-scope.sh index 5d1ce12..c3b2458 100755 --- a/project-bin/page-scope.sh +++ b/project-bin/page-scope.sh @@ -61,7 +61,10 @@ while [ $# -gt 0 ]; do done MPR="$(find_mpr)" || exit 2 -MXCLI="${MXCLI_BIN:-./mxcli}" +# Resolve the binary the same way every other script does (mxcli.exe on Git Bash, never an +# ELF mxcli left by a Dev Container) — one resolver in _common.sh, not a pasted uname case. +_mxcli_default="$(find_project_mxcli 2>/dev/null || echo ./mxcli)" +MXCLI="${MXCLI_BIN:-$_mxcli_default}" [ -x "$MXCLI" ] || { echo "FAULT: $MXCLI not found or not executable — the scope cannot be read from the model, and guessing it would be worse than not writing it" >&2; exit 2; } # JSON assembly and table parsing are done in Python; require_py (exit 2 with a fix per OS) diff --git a/project-bin/review-module.sh b/project-bin/review-module.sh index d07af4a..547300b 100755 --- a/project-bin/review-module.sh +++ b/project-bin/review-module.sh @@ -261,7 +261,7 @@ if [ -f "$ROOT/.mxcli/catalog.db" ] && command -v sqlite3 >/dev/null 2>&1; then [ "${n:-0}" -gt 0 ] 2>/dev/null && MODULE_KNOWN=1 fi if [ "$MODULE_KNOWN" -eq 0 ]; then - MODLIST="$(with_timeout 60 ./mxcli -p "$MPR" -c "SHOW MODULES" 2>/dev/null)" + MODLIST="$(with_timeout 60 "$(find_project_mxcli 2>/dev/null || echo ./mxcli)" -p "$MPR" -c "SHOW MODULES" 2>/dev/null)" printf '%s\n' "$MODLIST" | grep -qE "^\| *$MODULE +\|" && MODULE_KNOWN=1 fi