Skip to content

Commit a4d701a

Browse files
MendixMauclaude
andauthored
Exec speed: mx check gate by default, doctor performance check, Windows overhead fixes (#587)
mx check per exec by default (Gate mode: full opts out); full build on the first exec, Java/theme changes, real drops/renames with Java actions, --patch, every 10 check-only passes, before push (new pre-push hook, installed by sync) and at module close; stamp records kind; doctor performance section and process-start check; per-run memos, background build-plan refresh, cold-build notice, MXTK_PROFILE, lint-baseline message. For #584/#585. Review: MERGE-AFTER-FIX — state/module-close gating, refresh pipe, bash 4.1 guard and memo name re-reviewed MERGE; default-on change MERGE-AFTER-FIX (sync did not install the pre-push hook), fix re-reviewed MERGE. Field: Windows (103 ms/start) exec 138 s -> 90-103 s; Linux gate 20 s -> 9-10 s, 23-script replay with seeded defects all caught. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013jQvaonym9V1wKKfAEPmRr
1 parent 6648935 commit a4d701a

14 files changed

Lines changed: 667 additions & 10 deletions

‎CHANGELOG.md‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ Sections dated before 2026-09-19 predate the cycle and stay as they are. Named m
1919

2020
## Unreleased
2121

22+
- new(doctor, init): **`bin/doctor.sh <project>` measures performance, not just presence**: a "Performance (measured)" section reads the last 20 `steps:` rows of the project's BUILD-LOG and reports the median exec time, the median gate and the slowest step against a normal machine (17-34 s per exec, gate 9-20 s), warning above 60 s per exec or 45 s gate with the lever to pull; on Windows with slow process starts it recommends (never requires) WSL2 or a dev container (Docker Engine in WSL2, Rancher Desktop, Podman Desktop). `Gate mode: check` is the default for every project, existing ones included after sync (`Gate mode: full` in PROJECT.md keeps a full build per exec). Windows field run (ExpenseTracker, Windows 11, Git Bash, 103 ms per process start): check-only execs took 90-103 s against 138-139 s with the full build (gate 60 s → 12-14 s); the 60-70 s of non-gate overhead is the next target. Reported by mkr87. — mkr87 (#584, #585)
23+
- new(exec): **check-only gate by default (`Gate mode: full` in PROJECT.md, or `MXTK_GATE_MODE=full`, opts out)**: each exec runs `mx check` (beside mxbuild, same version) instead of the full deploy build, converting its JSON into mxbuild's errors shape so the delta gate, rollback and report are unchanged. The full build still runs when Java/JavaScript/theme/widgets changed since the last full build, when the project has Java actions and the script drops or renames something (`drop … if exists`, the re-create idiom, does not count), on `--patch`, before any full build is recorded, and after 10 check-only passes in a row (`MXTK_CHECK_MAX`). The other full builds sit at the work's natural boundaries, as mxcli itself advises (`mx check` after every exec, mxbuild when packaging): a new pre-push hook and `module-close.sh` run `bin/verify-model.sh` when the stamp says the model was only `mx check`ed (the stamp now records `kind: check|full`; `model-stamp.sh check --full`), and `bin/verify-model.sh` closes a batch on demand. Commits stay allowed on a check stamp. Field run (purchase-requests app, Linux, Mendix 11.13.0): gate 20 s → 9-10 s; a push after check-only execs ran the full build (20 s) and re-stamped the model as full; a dropped attribute caught as 6 × CE1613 by `mx check` on the broken model. Golden input: captured `mx check -j` output in `tests/wave2/fixtures/mx-check/`. For #584/#585 (Windows, mkr87). — mkr87 (#584, #585)
24+
- perf(exec, doctor): **Windows exec overhead (#584/#585)**: exec.sh resolves mxbuild and the model version once per run and exports them (`MXBUILD_PATH`, `MXTK_MODEL_VERSION`) instead of re-probing per call; the build-plan page refresh runs in the background after the verdict (`MXTK_SYNC_REFRESH=1` waits); the first full build after an hour says a cold start can take 2-3 min; `MXTK_PROFILE=1` traces every command with a timestamp and prints the 15 slowest lines plus the step times; `bin/doctor.sh` measures process-start cost and above 30 ms names the antivirus exclusions to ask for (or WSL2/dev container); a missing lint baseline now says `bin/lint-gate.sh --update-baseline` instead of "lint could not run (exit 2)". Reported by mkr87. — mkr87 (#584, #585)
2225
- fix(page-audit, design-audit, monkey, ui-loop): **no more leaked Mendix sessions from the toolkit's own browser helpers**: all three logged in and closed the browser without the xas logout, so each run burned a seat of a trial licence's cap and later journeys failed login as "Sign in failed"; they now log out first, and `ui-loop.md` says a hand-written screenshot script must too. Field: System.Session count +1 per run on master (page-audit 0 -> 1, design-audit 1 -> 2, monkey 2 -> 3), +0 with the fix — speed experiment E2
2326
- fix(page-audit, design-audit): **the audits produce their own `.claude/loop/page-scope.json`**: when it is missing or older than the model they run `bin/page-scope.sh` (`scope-ensure.js`, 2 s) instead of FAULTing on 0 pages; a failed regeneration never blanks an existing file and names the failure. Field: scope deleted -> page-audit ran on 9 pages, `touch` of the .mpr -> regenerated once — speed experiment E2
2427
- new(look-ledger, journey-runner, module-review): **a journey run's screenshots are LOOK evidence**: `bin/look-ledger.sh shots` (run by journey-runner after every run) maps each reached step's PNG to the one page whose widgets (`DESCRIBE PAGE`) it landed on, a URL pattern breaking ties, and lists every owed page as `OPEN` (open that PNG; the Read hook then marks the page SEEN) or `UNREACHED` (short gap-fill journey, or `page-audit.js` as before) with the line `owed N = A OPEN + B UNREACHED`; a shot older than the page's last build is not evidence and nothing is claimed when unsure. Field: 8 owed pages = 5 OPEN + 3 UNREACHED from two journey runs, no separate screenshot pass; baseline words 64,004 -> 64,193 — speed experiment E2

‎bin/doctor.sh‎

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -439,6 +439,76 @@ case "${BASH_VERSION:-0}" in
439439
3.*) note "bash 3.2 (the macOS system bash). Scripts here are written for it; nothing to do." ;;
440440
esac
441441

442+
# Process-start cost (#584/#585). Every toolkit script is many small process starts; exec.sh
443+
# alone makes hundreds. On a Windows machine whose antivirus scans each start, one start cost
444+
# 58-94 ms (vs ~1-3 ms on Linux/macOS), about 69 s of bookkeeping per exec before any build.
445+
# 20 starts of /bin/true (or `true` on PATH) measured with one clock read each side.
446+
_spawn_ms=""
447+
if command -v date >/dev/null 2>&1 && _t0=$(date +%s%N 2>/dev/null) && [ "${_t0%N}" = "$_t0" ]; then
448+
_i=0; _tbin="$(command -v true 2>/dev/null)"; case "$_tbin" in /*) ;; *) _tbin=/bin/true ;; esac
449+
while [ $_i -lt 20 ]; do "$_tbin"; _i=$((_i + 1)); done
450+
_t1=$(date +%s%N); _spawn_ms=$(( (_t1 - _t0) / 20000000 ))
451+
elif [ -n "${EPOCHREALTIME:-}" ]; then
452+
_t0=${EPOCHREALTIME/[.,]/}; _i=0
453+
while [ $_i -lt 20 ]; do /bin/true 2>/dev/null || true; _i=$((_i + 1)); done
454+
_t1=${EPOCHREALTIME/[.,]/}; _spawn_ms=$(( (_t1 - _t0) / 20000 ))
455+
fi
456+
if [ -z "$_spawn_ms" ]; then
457+
note "process start cost: not measured (no sub-second clock in this shell)"
458+
elif [ "$_spawn_ms" -gt "${MXTK_SPAWN_WARN_MS:-30}" ]; then
459+
warn "process start cost: ${_spawn_ms} ms each (fast machines: under 5 ms). Toolkit scripts start many"
460+
note "processes, so each exec spends roughly a minute just starting them. Usual cause on Windows:"
461+
note "antivirus scanning every start. Ask IT for Microsoft Defender exclusions (processes or folders) for:"
462+
note " this project folder, Git for Windows' usr\\bin, the JDK, and ~/.mxcli; or run the toolkit in"
463+
note " WSL2 / a dev container, where starts are cheap."
464+
else
465+
ok "process start cost: ${_spawn_ms} ms each"
466+
fi
467+
468+
# --- performance (measured, not just "is it installed") ----------------------------------------------
469+
# Doctor used to answer only "is it there". A machine where everything is installed can still spend
470+
# 64-234 s on a 1 s model write (#584/#585), and nobody saw it until a user profiled it by hand.
471+
# So, from the project's own docs/BUILD-LOG.md (exec.sh writes `steps: guards 0s check 1s … gate 20s
472+
# lint 3s score 3s` on every row): the median exec time and its slowest step over the last 20 execs,
473+
# against what a normal machine shows (measured 2026-10-28 on Linux: 17-34 s per exec, gate 9-20 s).
474+
# Thresholds: MXTK_PERF_EXEC_WARN (default 60 s per exec), MXTK_PERF_GATE_WARN (default 45 s gate).
475+
head_ "Performance (measured)"
476+
_perf_log="${PROJECT_DIR:+$PROJECT_DIR/docs/BUILD-LOG.md}"
477+
if [ -n "$_perf_log" ] && [ -f "$_perf_log" ] && grep -q 'steps: ' "$_perf_log" 2>/dev/null; then
478+
_perf=$(grep 'steps: ' "$_perf_log" | tail -20 | awk '
479+
{ line=$0; sub(/.*steps: /, "", line); sub(/ *\|.*/, "", line)
480+
n=split(line, w, " "); tot=0
481+
for (i=1; i+1<=n; i+=2) { v=w[i+1]; sub(/s$/, "", v); if (v ~ /^[0-9]+$/) { tot+=v; st[w[i]]=st[w[i]] " " v } }
482+
T[++rows]=tot }
483+
function med(str, a, k, m, i, j, t) { m=split(str, a, " "); for (i=1;i<=m;i++) for (j=i+1;j<=m;j++) if (a[j]+0<a[i]+0) { t=a[i]; a[i]=a[j]; a[j]=t }
484+
return m ? a[int((m+1)/2)] : 0 }
485+
END { all=""; for (i=1;i<=rows;i++) all=all " " T[i]
486+
top=""; tv=-1; for (k in st) { v=med(st[k]); if (v+0>tv) { tv=v+0; top=k } }
487+
printf "%d %d %s %d %d\n", rows, med(all), top, tv, med(st["gate"]) }')
488+
set -- $_perf
489+
_pn=${1:-0}; _pmed=${2:-0}; _ptop=${3:-?}; _ptopv=${4:-0}; _pgate=${5:-0}
490+
_pmsg="last $_pn execs: median ${_pmed} s per exec (gate ${_pgate} s; slowest step: $_ptop ${_ptopv} s). Normal: 17-34 s, gate 9-20 s."
491+
if [ "$_pmed" -gt "${MXTK_PERF_EXEC_WARN:-60}" ] || [ "$_pgate" -gt "${MXTK_PERF_GATE_WARN:-45}" ]; then
492+
warn "slow execs — $_pmsg"
493+
[ "$_pgate" -gt "${MXTK_PERF_GATE_WARN:-45}" ] && note "The gate (mxbuild) dominates: 'Gate mode: check' in PROJECT.md runs mx check per exec and the full build before push."
494+
[ -n "$_spawn_ms" ] && [ "$_spawn_ms" -gt "${MXTK_SPAWN_WARN_MS:-30}" ] && note "Process starts are slow too (above): that is the machine, not the model — see the advice there."
495+
note "Per-command profile of one exec: MXTK_PROFILE=1 ./bin/exec.sh <script> (prints the 15 slowest lines)."
496+
else
497+
ok "$_pmsg"
498+
fi
499+
elif [ -n "$PROJECT_DIR" ]; then
500+
note "no exec timings yet (docs/BUILD-LOG.md has no 'steps:' rows) — re-run doctor after a few execs"
501+
else
502+
note "exec timings: pass a project dir (bin/doctor.sh <project>) to compare its execs with a normal machine"
503+
fi
504+
# Windows advice: the dev container / WSL2 lane is RECOMMENDED where process starts are slow, never
505+
# required — a machine without it keeps working, just slower.
506+
if [ "${PLATFORM:-}" = gitbash ] && [ -n "$_spawn_ms" ] && [ "$_spawn_ms" -gt "${MXTK_SPAWN_WARN_MS:-30}" ]; then
507+
note "Recommended on this Windows machine (optional): run the toolkit in WSL2 or a dev container"
508+
note " (Docker Engine in WSL2, Rancher Desktop or Podman Desktop). Without one, the antivirus exclusions"
509+
note " above are the fix; everything keeps working either way."
510+
fi
511+
442512
# --- environment lane -------------------------------------------------------------------------
443513
# Where this session runs decides the write modes available (CONVERSION-RUNBOOK.md → "Where you
444514
# run this"). It is DETECTED, never asked: the agent records the answer in PROJECT.md and moves on.

‎bin/init-project.sh‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,11 @@ ${TOOLKIT_COMMIT_LINE}
196196
Exec approval: auto
197197
Permission profile: project
198198
Unit tests default: no
199+
Gate mode: check
200+
201+
<!-- Gate mode: check (the default, also without this line) = bin/exec.sh runs \`mx check\` per
202+
script and the full mxbuild before every push, at module close and when Java/theme files
203+
change (mxcli's own advice). \`Gate mode: full\` = a full mxbuild on every exec. -->
199204
200205
<!-- The bold line above is a READOUT: bin/gate-check.sh rewrites it on every full run from the
201206
verdicts it just produced (first stage neither PASS nor WAIVED). Do not edit it by hand.

‎project-bin/_common.sh‎

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -482,6 +482,10 @@ print('file:'+u.pathname2url(os.path.abspath(sys.argv[1]))+'?mode=ro')" "$1" | t
482482
# ---------------------------------------------------------------------------
483483
mxtk_model_version() {
484484
local mpr="${1:-}" v="" py
485+
# Memo: exec.sh exports MXTK_MODEL_VERSION once per run, so the many find_mxbuild calls
486+
# in one exec (and the scripts it starts) do not each spawn sqlite3/python (#584: a
487+
# process start costs 58-94 ms on a Defender-scanned Windows machine).
488+
if [ -z "$mpr" ] && [ -n "${MXTK_MODEL_VERSION:-}" ]; then printf '%s\n' "$MXTK_MODEL_VERSION"; return 0; fi
485489
[ -n "$mpr" ] || mpr="$(find_mpr 2>/dev/null)" || return 1
486490
[ -f "$mpr" ] || return 1
487491
if command -v sqlite3 >/dev/null 2>&1; then
@@ -555,6 +559,9 @@ EOF_SP
555559
# is loud in mxtk_ensure_mxbuild (this function is always called with stderr discarded).
556560
find_mxbuild() {
557561
if [ -n "${MXBUILD_PATH:-}" ]; then echo "$MXBUILD_PATH"; return 0; fi
562+
# Per-run memo exec.sh exports (#584) — its own name, so the explicit-override semantics of
563+
# MXBUILD_PATH (no version match, mxtk_ensure_mxbuild's warning) never reach child scripts.
564+
if [ -n "${MXTK_MXBUILD_MEMO:-}" ] && [ -x "$MXTK_MXBUILD_MEMO" ]; then echo "$MXTK_MXBUILD_MEMO"; return 0; fi
558565
local ver m
559566
if ver=$(mxtk_model_version 2>/dev/null) && m=$(mxtk_mxbuild_for_version "$ver"); then
560567
echo "$m"; return 0
@@ -809,6 +816,12 @@ sys.exit(0 if m and all(ok.match(x) and not re.search(r"CE[0-9]{4}", x) for x in
809816
mxtk_gate_mxbuild() {
810817
local _mpr="$1" _timeout="${2:-0}" _ef="$3" _rc=0
811818
MXTK_GATE_RETRY_NOTE=""; MXTK_GATE_FIRST_OUT=""; MXTK_GATE_FIRST_EXIT=""
819+
# MXTK_GATE_KIND=check (set by exec.sh's gate-kind decision): `mx check` instead of a
820+
# deploy build. No infra retry: mx check has no deploy stage to collide on.
821+
if [ "${MXTK_GATE_KIND:-full}" = "check" ]; then
822+
mxtk_mx_check_error_count "$_mpr" "$_timeout" "$_ef" >/dev/null || _rc=$?
823+
return "$_rc"
824+
fi
812825
mxtk_pause_watchers "${PROJECT_ROOT:-$(dirname "$_mpr")}"
813826
mxtk_mxbuild_error_count "$_mpr" "$_timeout" "$_ef" >/dev/null || _rc=$?
814827
case "${MXTK_MXBUILD_EXIT:-}" in
@@ -971,6 +984,99 @@ def one(e):
971984
print('; '.join(one(e) for e in d.get('errors',[]) or [])[:300])" 2>/dev/null || true
972985
}
973986

987+
# ---------------------------------------------------------------------------
988+
# Check-only gate (opt-in: `Gate mode: check` in PROJECT.md; exec.sh decides per exec).
989+
#
990+
# `mx check` ships next to mxbuild and runs the same model consistency checks (CE codes) in
991+
# ~12 s where a deploy build takes 18-165 s, because it skips the Java compile and the deploy
992+
# packaging. What it cannot see is exactly that: Java compile errors, including a model change
993+
# that breaks a generated proxy a Java action uses. exec.sh therefore still runs the full build
994+
# when those can change (mxtk_gate_kind_decide in exec.sh) — this is the cheap path only.
995+
# WHY (#584/#585, Windows field reports): mxbuild was the largest single cost per exec there.
996+
#
997+
# mxtk_java_fp <project-root> — content fingerprint of the files only a full build compiles
998+
# (javasource/ javascriptsource/ theme/ userlib/ widgets/). Content, not git state: committing an
999+
# unchanged file must not read as a change. javasource/*/proxies/ is excluded: mxbuild regenerates
1000+
# it from the model on every full build. exec.sh (gate kind) and verify-model.sh record it.
1001+
mxtk_java_fp() {
1002+
( cd "$1" 2>/dev/null || exit 0
1003+
_l="$(git ls-files -co --exclude-standard -- javasource javascriptsource theme userlib widgets 2>/dev/null \
1004+
| grep -v '^javasource/[^/]*/proxies/' || true)"
1005+
{ printf '%s\n' "$_l"; [ -z "$_l" ] || printf '%s\n' "$_l" | git hash-object --stdin-paths 2>/dev/null; } \
1006+
| cksum | awk '{print $1}' )
1007+
}
1008+
1009+
# mxtk_find_mx — the `mx` tool beside the resolved mxbuild (same version by construction).
1010+
mxtk_find_mx() {
1011+
local _mb _d _c
1012+
_mb="${MXBUILD_PATH:-$(find_mxbuild 2>/dev/null || true)}"
1013+
[ -n "$_mb" ] || return 1
1014+
_d="$(dirname "$_mb")"
1015+
for _c in "$_d/mx" "$_d/mx.exe"; do [ -x "$_c" ] && { echo "$_c"; return 0; }; done
1016+
return 1
1017+
}
1018+
1019+
# mxtk_mx_check_to_mxbuild <mx-check.json> <out.json> [python] — rewrite `mx check -j` output
1020+
# into the mxbuild --write-errors shape exec.sh's err_set/err_report/err_codes read:
1021+
# {"errors":[...],"problems":[{"severity":"Error","errorCode","message","locations":[{module,document,element}]}]}
1022+
# Golden input: tests/wave2/fixtures/mx-check/mx-check-11.13.0-CE1613.json (captured, mx 11.13.0).
1023+
mxtk_mx_check_to_mxbuild() {
1024+
local _py="${3:-${PY:-$(resolve_py 2>/dev/null || true)}}"
1025+
[ -n "$_py" ] || return 1
1026+
"$_py" -c "import json,sys
1027+
d=json.load(open(sys.argv[1]))
1028+
def loc(l): return {'module':l.get('module-name',''),'document':l.get('document-name',''),'element':l.get('element-name','')}
1029+
probs=[{'severity':'Error','errorCode':e.get('code',''),'message':e.get('message',''),
1030+
'locations':[loc(l) for l in e.get('locations') or []]} for e in d.get('errors') or []]
1031+
json.dump({'errors':[],'problems':probs,'source':'mx check'},open(sys.argv[2],'w'))
1032+
print(len(probs))" "$(native_path "$1")" "$(native_path "$2")" 2>/dev/null
1033+
}
1034+
1035+
# mxtk_mx_check_error_count <mpr> <timeout-seconds> [errors-file] — mxtk_mxbuild_error_count's
1036+
# contract (same globals, same return codes, same printed count), backed by `mx check -j`.
1037+
# The errors file it leaves is in mxbuild's shape (mxtk_mx_check_to_mxbuild), so every reader
1038+
# downstream is unchanged. A run that produced no JSON is "?" with the output tail in
1039+
# MXTK_MXBUILD_WHY — never 0.
1040+
mxtk_mx_check_error_count() {
1041+
local _mpr="$1" _timeout="${2:-300}" _ef="${3:-}" _own_ef=0
1042+
local _mx _out _js _pid _waited=0 _exit=0 _count
1043+
MXTK_MXBUILD_OUT=""; MXTK_MXBUILD_EXIT=""; MXTK_MXBUILD_WHY=""
1044+
_mx="$(mxtk_find_mx)" || { echo "?"; return 2; }
1045+
if [ -z "$_ef" ]; then _ef=$(mktemp /tmp/mxcheck-errors.XXXXXX) || { echo "?"; return 2; }; _own_ef=1; fi
1046+
_out=$(mktemp /tmp/mxcheck-out.XXXXXX) || { echo "?"; return 2; }
1047+
_js=$(mktemp /tmp/mxcheck-json.XXXXXX) || { rm -f "$_out"; echo "?"; return 2; }
1048+
rm -f "$_js" # mx writes the file itself; an empty pre-existing one would read as "no output"
1049+
"$_mx" check "$_mpr" -j "$(native_path "$_js")" > "$_out" 2>&1 < /dev/null &
1050+
_pid=$!
1051+
while kill -0 "$_pid" 2>/dev/null; do
1052+
if [ "$_timeout" -gt 0 ] && [ "$_waited" -ge "$_timeout" ]; then
1053+
kill "$_pid" 2>/dev/null; wait "$_pid" 2>/dev/null
1054+
MXTK_MXBUILD_OUT=$(cat "$_out" 2>/dev/null); rm -f "$_out" "$_js"
1055+
MXTK_MXBUILD_EXIT=124; [ "$_own_ef" -eq 1 ] && rm -f "$_ef"
1056+
echo "?"; return 3
1057+
fi
1058+
sleep 1; _waited=$((_waited + 1))
1059+
done
1060+
wait "$_pid" || _exit=$?
1061+
MXTK_MXBUILD_OUT=$(cat "$_out" 2>/dev/null); rm -f "$_out"
1062+
MXTK_MXBUILD_EXIT="$_exit"
1063+
if [ -s "$_js" ]; then _count=$(mxtk_mx_check_to_mxbuild "$_js" "$_ef"); fi
1064+
rm -f "$_js"
1065+
[ "$_own_ef" -eq 1 ] && rm -f "$_ef"
1066+
case "${_count:-}" in
1067+
''|*[!0-9]*)
1068+
MXTK_MXBUILD_WHY="mx check wrote no readable JSON (exit $_exit): $(printf '%s' "$MXTK_MXBUILD_OUT" | tail -3 | tr '\n' ' ' | cut -c1-240)"
1069+
echo "?"; return 1 ;;
1070+
esac
1071+
# exit 1 with errors is the normal "model has errors" answer; any other non-zero exit
1072+
# with 0 errors means mx did not check the model.
1073+
if [ "$_count" = "0" ] && [ "$_exit" -ne 0 ]; then
1074+
MXTK_MXBUILD_WHY="mx check exited $_exit with no errors listed: $(printf '%s' "$MXTK_MXBUILD_OUT" | tail -3 | tr '\n' ' ' | cut -c1-240)"
1075+
echo "?"; return 1
1076+
fi
1077+
echo "$_count"; return 0
1078+
}
1079+
9741080
# project_name — the .mpr basename without extension, for user-facing messages.
9751081
project_name() {
9761082
local mpr

0 commit comments

Comments
 (0)