bots-garden/mini-mepublic Fork
⑂ b080361
Commits
⬇ Clone ▾
git clone https://git.rickub.com/bots-garden/mini-me.git
git clone ssh://git@rickub.com/bots-garden/mini-me.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

Download ZIP

🛟 Updated.

k33g committed 2026-09-17T07:19:45+02:00 Browse files
b080361 parent: 9de06ac
added .github/workflows/release.yml +99 -0
new file mode 100644
@@ -0,0 +1,99 @@
1+name: Release
2+
3+# Publishes a release with the cross-compiled binaries whenever a tag v* is
4+# pushed — what ./01-release.tag.sh does at its last line. The binaries are
5+# built by ./02-build-releases.sh, the same script one runs on a laptop, so a
6+# local build and a published one are byte-for-byte the same pipeline.
7+#
8+# Rickub runs this as an ordinary GitHub Actions workflow. Two platform facts
9+# matter here: the job's GITHUB_TOKEN is the ONLY credential the release API
10+# (the /gh shim behind $GITHUB_API_URL) accepts — a personal token is refused —
11+# and it is read-only unless the workflow asks for `contents: write` below.
12+#
13+# No workflow_dispatch on purpose: Rickub's dispatch API fires EVERY
14+# dispatchable workflow of a ref, so a repository should declare at most one.
15+on:
16+ push:
17+ tags:
18+ - "v*"
19+
20+permissions:
21+ contents: write
22+
23+concurrency:
24+ group: release-${{ github.ref_name }}
25+ cancel-in-progress: false
26+
27+jobs:
28+ release:
29+ name: build and publish ${{ github.ref_name }}
30+ runs-on: ubuntu-latest
31+ steps:
32+ - name: Checkout
33+ uses: actions/checkout@v4
34+ with:
35+ # The whole history and the tags: the release notes below are read
36+ # from the annotated tag's message.
37+ fetch-depth: 0
38+
39+ - name: Set up Go
40+ uses: actions/setup-go@v5
41+ with:
42+ go-version-file: go.mod
43+ cache: true
44+
45+ - name: go test
46+ run: go test ./... -count=1
47+
48+ - name: Build the binaries
49+ # release.env is git-ignored, so the tag is passed explicitly and the
50+ # script falls back to "Mini Me <tag>" for the description.
51+ run: bash ./02-build-releases.sh "${GITHUB_REF_NAME}"
52+
53+ - name: Release notes
54+ id: notes
55+ # The message ./01-release.tag.sh put on the annotated tag (ABOUT in
56+ # release.env), then the download table the build wrote. A lightweight
57+ # tag has no message: the tag name stands in.
58+ run: |
59+ set -euo pipefail
60+ message="$(git for-each-ref "refs/tags/${GITHUB_REF_NAME}" --format='%(contents)' | sed '/^-----BEGIN PGP SIGNATURE-----/,$d')"
61+ if [ -z "$(printf '%s' "${message}" | tr -d '[:space:]')" ]; then
62+ message="Mini Me ${GITHUB_REF_NAME}"
63+ fi
64+ {
65+ printf '%s\n\n' "${message}"
66+ echo "- Commit: \`${GITHUB_SHA}\`"
67+ echo "- Built by the Release workflow, run #${GITHUB_RUN_NUMBER}, with $(go env GOVERSION)"
68+ echo
69+ echo '## Checksums'
70+ echo
71+ echo '```'
72+ cat "release/${GITHUB_REF_NAME}/SHA256SUMS"
73+ echo '```'
74+ } > "${RUNNER_TEMP}/notes.md"
75+ echo "path=${RUNNER_TEMP}/notes.md" >> "$GITHUB_OUTPUT"
76+
77+ - name: Keep the binaries as a run artifact
78+ # Downloadable from the run page even if the publish step below fails
79+ # (an old CI node that does not forward /gh answers 403 there).
80+ uses: actions/upload-artifact@v4
81+ with:
82+ name: mm-${{ github.ref_name }}
83+ path: release/${{ github.ref_name }}/
84+ if-no-files-found: error
85+ retention-days: 14
86+
87+ - name: Publish the release
88+ uses: softprops/action-gh-release@v2
89+ with:
90+ tag_name: ${{ github.ref_name }}
91+ name: ${{ github.ref_name }}
92+ body_path: ${{ steps.notes.outputs.path }}
93+ draft: false
94+ prerelease: ${{ contains(github.ref_name, '-') }}
95+ files: |
96+ release/${{ github.ref_name }}/mm-*
97+ release/${{ github.ref_name }}/SHA256SUMS
98+ release/${{ github.ref_name }}/README.md
99+ fail_on_unmatched_files: true
new file mode 100644
@@ -0,0 +1,99 @@
1+name: Release
2+
3+# Publishes a release with the cross-compiled binaries whenever a tag v* is
4+# pushed — what ./01-release.tag.sh does at its last line. The binaries are
5+# built by ./02-build-releases.sh, the same script one runs on a laptop, so a
6+# local build and a published one are byte-for-byte the same pipeline.
7+#
8+# Rickub runs this as an ordinary GitHub Actions workflow. Two platform facts
9+# matter here: the job's GITHUB_TOKEN is the ONLY credential the release API
10+# (the /gh shim behind $GITHUB_API_URL) accepts — a personal token is refused —
11+# and it is read-only unless the workflow asks for `contents: write` below.
12+#
13+# No workflow_dispatch on purpose: Rickub's dispatch API fires EVERY
14+# dispatchable workflow of a ref, so a repository should declare at most one.
15+on:
16+ push:
17+ tags:
18+ - "v*"
19+
20+permissions:
21+ contents: write
22+
23+concurrency:
24+ group: release-${{ github.ref_name }}
25+ cancel-in-progress: false
26+
27+jobs:
28+ release:
29+ name: build and publish ${{ github.ref_name }}
30+ runs-on: ubuntu-latest
31+ steps:
32+ - name: Checkout
33+ uses: actions/checkout@v4
34+ with:
35+ # The whole history and the tags: the release notes below are read
36+ # from the annotated tag's message.
37+ fetch-depth: 0
38+
39+ - name: Set up Go
40+ uses: actions/setup-go@v5
41+ with:
42+ go-version-file: go.mod
43+ cache: true
44+
45+ - name: go test
46+ run: go test ./... -count=1
47+
48+ - name: Build the binaries
49+ # release.env is git-ignored, so the tag is passed explicitly and the
50+ # script falls back to "Mini Me <tag>" for the description.
51+ run: bash ./02-build-releases.sh "${GITHUB_REF_NAME}"
52+
53+ - name: Release notes
54+ id: notes
55+ # The message ./01-release.tag.sh put on the annotated tag (ABOUT in
56+ # release.env), then the download table the build wrote. A lightweight
57+ # tag has no message: the tag name stands in.
58+ run: |
59+ set -euo pipefail
60+ message="$(git for-each-ref "refs/tags/${GITHUB_REF_NAME}" --format='%(contents)' | sed '/^-----BEGIN PGP SIGNATURE-----/,$d')"
61+ if [ -z "$(printf '%s' "${message}" | tr -d '[:space:]')" ]; then
62+ message="Mini Me ${GITHUB_REF_NAME}"
63+ fi
64+ {
65+ printf '%s\n\n' "${message}"
66+ echo "- Commit: \`${GITHUB_SHA}\`"
67+ echo "- Built by the Release workflow, run #${GITHUB_RUN_NUMBER}, with $(go env GOVERSION)"
68+ echo
69+ echo '## Checksums'
70+ echo
71+ echo '```'
72+ cat "release/${GITHUB_REF_NAME}/SHA256SUMS"
73+ echo '```'
74+ } > "${RUNNER_TEMP}/notes.md"
75+ echo "path=${RUNNER_TEMP}/notes.md" >> "$GITHUB_OUTPUT"
76+
77+ - name: Keep the binaries as a run artifact
78+ # Downloadable from the run page even if the publish step below fails
79+ # (an old CI node that does not forward /gh answers 403 there).
80+ uses: actions/upload-artifact@v4
81+ with:
82+ name: mm-${{ github.ref_name }}
83+ path: release/${{ github.ref_name }}/
84+ if-no-files-found: error
85+ retention-days: 14
86+
87+ - name: Publish the release
88+ uses: softprops/action-gh-release@v2
89+ with:
90+ tag_name: ${{ github.ref_name }}
91+ name: ${{ github.ref_name }}
92+ body_path: ${{ steps.notes.outputs.path }}
93+ draft: false
94+ prerelease: ${{ contains(github.ref_name, '-') }}
95+ files: |
96+ release/${{ github.ref_name }}/mm-*
97+ release/${{ github.ref_name }}/SHA256SUMS
98+ release/${{ github.ref_name }}/README.md
99+ fail_on_unmatched_files: true
modified .gitignore +2 -0
@@ -10,3 +10,5 @@ sandboxes
1010 .qlty
1111 .quality
1212 .DS_Store
13+*.env
14+release
@@ -10,3 +10,5 @@ sandboxes
10 .qlty10 .qlty
11 .quality11 .quality
12 .DS_Store12 .DS_Store
13+*.env
14+release
added 01-release.tag.sh +68 -0
new file mode 100644
@@ -0,0 +1,68 @@
1+#!/bin/bash
2+: <<'COMMENT'
3+1. Update TAG and ABOUT in release.env
4+2. Run this script: ./01-release.tag.sh (commit, push, tag, push the tag)
5+3. Watch the "Release" workflow on Rickub (Actions tab): the tag push starts
6+ it; it builds the binaries with ./02-build-releases.sh and publishes the
7+ release with them, using the job's own token. No personal token is needed.
8+COMMENT
9+
10+# Without this, a failing step is ignored and the next one runs anyway. That is
11+# not theoretical: `git tag` refusing a tag that already existed was skipped in
12+# silence, and the `git push` below then pushed the OLD tag — so a release was
13+# cut from a commit nobody meant, and 03 was left to notice.
14+set -euo pipefail
15+
16+set -o allexport
17+source release.env
18+set +o allexport
19+
20+echo "Generating release: ${TAG} ${ABOUT}"
21+
22+# tagExists reports whether TAG is already taken, here or on the remote. The
23+# remote matters on its own: a tag deleted locally after a failed attempt still
24+# exists there, and pushing a new one at a different commit is rejected.
25+tagExists() {
26+ if git rev-parse -q --verify "refs/tags/${TAG}" >/dev/null; then
27+ printf 'locally, on %s\n' "$(git rev-parse --short "${TAG}^{commit}")"
28+ return 0
29+ fi
30+ if ! remote="$(git ls-remote --tags origin "refs/tags/${TAG}" 2>/dev/null)"; then
31+ return 1 # the remote is unreachable; the push below will say so
32+ fi
33+ if [ -n "${remote}" ]; then
34+ # No commit is named here on purpose: for an annotated tag ls-remote
35+ # gives the tag object, not the commit, and printing that as if it were
36+ # the commit sends the reader looking for a SHA they will never find.
37+ printf 'on origin\n'
38+ return 0
39+ fi
40+ return 1
41+}
42+
43+if where="$(tagExists)"; then
44+ echo "❌ ${TAG} already exists ${where}"
45+ echo "💡 Bump TAG in release.env, or move the tag onto this commit:"
46+ echo " git tag -f -a ${TAG} -m \"${ABOUT}\""
47+ echo " git push --force origin ${TAG}"
48+ exit 1
49+fi
50+
51+find . -name '.DS_Store' -type f -delete
52+
53+git add .
54+
55+# Nothing to commit is not a failure — the work may already be committed — but
56+# under `set -e` a plain `git commit` would stop the release right here.
57+if git diff --cached --quiet; then
58+ echo "Nothing to commit; releasing what is already on HEAD"
59+else
60+ git commit -m "📦 ${ABOUT}"
61+fi
62+
63+git push origin main
64+
65+# The tag goes on after the push, so a rejected push never leaves a tag behind
66+# pointing at a commit the remote has never seen.
67+git tag -a "${TAG}" -m "${ABOUT}"
68+git push origin "${TAG}"
new file mode 100644
@@ -0,0 +1,68 @@
1+#!/bin/bash
2+: <<'COMMENT'
3+1. Update TAG and ABOUT in release.env
4+2. Run this script: ./01-release.tag.sh (commit, push, tag, push the tag)
5+3. Watch the "Release" workflow on Rickub (Actions tab): the tag push starts
6+ it; it builds the binaries with ./02-build-releases.sh and publishes the
7+ release with them, using the job's own token. No personal token is needed.
8+COMMENT
9+
10+# Without this, a failing step is ignored and the next one runs anyway. That is
11+# not theoretical: `git tag` refusing a tag that already existed was skipped in
12+# silence, and the `git push` below then pushed the OLD tag — so a release was
13+# cut from a commit nobody meant, and 03 was left to notice.
14+set -euo pipefail
15+
16+set -o allexport
17+source release.env
18+set +o allexport
19+
20+echo "Generating release: ${TAG} ${ABOUT}"
21+
22+# tagExists reports whether TAG is already taken, here or on the remote. The
23+# remote matters on its own: a tag deleted locally after a failed attempt still
24+# exists there, and pushing a new one at a different commit is rejected.
25+tagExists() {
26+ if git rev-parse -q --verify "refs/tags/${TAG}" >/dev/null; then
27+ printf 'locally, on %s\n' "$(git rev-parse --short "${TAG}^{commit}")"
28+ return 0
29+ fi
30+ if ! remote="$(git ls-remote --tags origin "refs/tags/${TAG}" 2>/dev/null)"; then
31+ return 1 # the remote is unreachable; the push below will say so
32+ fi
33+ if [ -n "${remote}" ]; then
34+ # No commit is named here on purpose: for an annotated tag ls-remote
35+ # gives the tag object, not the commit, and printing that as if it were
36+ # the commit sends the reader looking for a SHA they will never find.
37+ printf 'on origin\n'
38+ return 0
39+ fi
40+ return 1
41+}
42+
43+if where="$(tagExists)"; then
44+ echo "❌ ${TAG} already exists ${where}"
45+ echo "💡 Bump TAG in release.env, or move the tag onto this commit:"
46+ echo " git tag -f -a ${TAG} -m \"${ABOUT}\""
47+ echo " git push --force origin ${TAG}"
48+ exit 1
49+fi
50+
51+find . -name '.DS_Store' -type f -delete
52+
53+git add .
54+
55+# Nothing to commit is not a failure — the work may already be committed — but
56+# under `set -e` a plain `git commit` would stop the release right here.
57+if git diff --cached --quiet; then
58+ echo "Nothing to commit; releasing what is already on HEAD"
59+else
60+ git commit -m "📦 ${ABOUT}"
61+fi
62+
63+git push origin main
64+
65+# The tag goes on after the push, so a rejected push never leaves a tag behind
66+# pointing at a commit the remote has never seen.
67+git tag -a "${TAG}" -m "${ABOUT}"
68+git push origin "${TAG}"
added 02-build-releases.sh +162 -0
new file mode 100644
@@ -0,0 +1,162 @@
1+#!/bin/bash
2+: <<'COMMENT'
3+Build the release binaries of mm and stage them under release/${TAG}/
4+
5+Usage:
6+ ./02-build-releases.sh # TAG and ABOUT come from release.env
7+ ./02-build-releases.sh v0.2.0 # override the tag for this run (what CI does)
8+
9+Only the Go toolchain is needed: every binary is a plain `go build`, there is
10+no Makefile and no helper script. The same command works on a laptop and in a
11+Rickub CI job (runners are linux/amd64; the other targets are cross-compiled).
12+
13+What ends up in release/${TAG}/:
14+ mm-<version>-<os>-<arch>[.exe] one binary per platform
15+ SHA256SUMS checksums of the binaries
16+ README.md download table + how to run
17+COMMENT
18+
19+set -euo pipefail
20+
21+# release.env carries TAG ("v0.2.0") and ABOUT (the one-line description). It
22+# is git-ignored (*.env), so a CI job does not have it: there the tag comes
23+# from the command line and ABOUT from the environment, or defaults to the
24+# tag. A tag given on the command line always wins, so a test build never
25+# edits the file.
26+if [ -f release.env ]; then
27+ source release.env
28+fi
29+TAG="${1:-${TAG:-}}"
30+ABOUT="${ABOUT:-Mini Me ${TAG}}"
31+
32+# A tag that is not vMAJOR.MINOR.PATCH[-prerelease] is a typo — measured: a
33+# release.env with "v0.o.0" (letter o) would otherwise build and stage
34+# binaries nobody can name.
35+if ! [[ "${TAG}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$ ]]; then
36+ echo "❌ TAG must look like v1.2.3 or v1.2.3-rc.1, got '${TAG}' (check release.env)"
37+ exit 1
38+fi
39+
40+# The platforms a release is built for. Add or remove a line and everything
41+# below follows: the builds, the checksums and the README.
42+PLATFORMS=(
43+ "darwin/arm64"
44+ "linux/amd64"
45+ "linux/arm64"
46+ "windows/amd64"
47+ "windows/arm64"
48+)
49+
50+# The tag is "v0.2.0"; the assets carry the bare version, "0.2.0".
51+VERSION="${TAG#v}"
52+RELEASES_DIR="release/${TAG}"
53+
54+# -trimpath keeps this machine's paths out of binaries that go to strangers;
55+# -s -w drops the symbol table and DWARF, about a third of the size. mm has no
56+# version variable to stamp, so there is no -X here: the file name and the
57+# SHA256SUMS are what tie a binary to its release.
58+BUILD_FLAGS=(-trimpath -ldflags "-s -w")
59+
60+echo "🚀 Building Mini Me ${TAG} — ${ABOUT}"
61+echo "🐹 $(go version)"
62+
63+rm -rf "${RELEASES_DIR}"
64+mkdir -p "${RELEASES_DIR}"
65+
66+# assetName is what the binary for a platform is called once staged. Windows
67+# executables carry .exe, or Windows will not run them.
68+assetName() {
69+ local goos=$1 goarch=$2
70+ local name="mm-${VERSION}-${goos}-${goarch}"
71+ if [ "${goos}" = "windows" ]; then
72+ name="${name}.exe"
73+ fi
74+ printf '%s\n' "${name}"
75+}
76+
77+echo ""
78+echo "🔨 Cross-compiling for ${#PLATFORMS[@]} platforms..."
79+
80+for platform in "${PLATFORMS[@]}"; do
81+ goos="${platform%/*}"
82+ goarch="${platform#*/}"
83+ asset="$(assetName "${goos}" "${goarch}")"
84+
85+ # CGO_ENABLED=0: nothing to link against on the other side of a
86+ # cross-compile, and mm needs no C — genkit, tcell and the ACP SDK are pure
87+ # Go. Without it, a darwin build from a Linux box fails at link time.
88+ if ! CGO_ENABLED=0 GOOS="${goos}" GOARCH="${goarch}" \
89+ go build "${BUILD_FLAGS[@]}" -o "${RELEASES_DIR}/${asset}" .; then
90+ echo " ❌ ${platform}"
91+ exit 1
92+ fi
93+ echo " ✅ ${asset}"
94+done
95+
96+# The binary for this machine is the only one that can run here, and running
97+# it is the one proof that what ships starts at all: -h prints the usage and
98+# exits 0 before any config or model server is looked for.
99+HOST_ASSET="$(assetName "$(go env GOOS)" "$(go env GOARCH)")"
100+if [ -x "${RELEASES_DIR}/${HOST_ASSET}" ]; then
101+ if ! "${RELEASES_DIR}/${HOST_ASSET}" -h >/dev/null 2>&1; then
102+ echo " ❌ ${HOST_ASSET} does not start (mm -h failed)"
103+ exit 1
104+ fi
105+ echo " ✅ ${HOST_ASSET} starts"
106+fi
107+
108+# checksum runs whichever of the two tools this machine has: sha256sum on
109+# Linux, shasum on macOS.
110+checksum() {
111+ if command -v sha256sum >/dev/null 2>&1; then
112+ sha256sum "$@"
113+ else
114+ shasum -a 256 "$@"
115+ fi
116+}
117+
118+# One SHA256SUMS for every platform, names only (no directory), which is what
119+# `sha256sum -c` expects to read next to the downloaded files.
120+(cd "${RELEASES_DIR}" && checksum mm-"${VERSION}"-* >SHA256SUMS)
121+echo " ✅ SHA256SUMS"
122+
123+# downloadTable lists the platforms as a Markdown table, so the README grows
124+# and shrinks with PLATFORMS rather than repeating it by hand.
125+downloadTable() {
126+ printf '| Platform | Download |\n|---|---|\n'
127+ for platform in "${PLATFORMS[@]}"; do
128+ local goos="${platform%/*}" goarch="${platform#*/}"
129+ printf '| %s | `%s` |\n' "${platform}" "$(assetName "${goos}" "${goarch}")"
130+ done
131+}
132+
133+cat >"${RELEASES_DIR}/README.md" <<EOM
134+# Mini Me ${TAG}
135+
136+${ABOUT}
137+
138+Built with $(go env GOVERSION), CGO disabled. No runtime dependency besides a
139+model server (Docker Model Runner or llama-server).
140+
141+$(downloadTable)
142+
143+## Running it
144+
145+ chmod +x mm-${VERSION}-<platform>
146+ ./mm-${VERSION}-<platform> agent.yaml # line mode
147+ ./mm-${VERSION}-<platform> -tui agent.yaml # window mode
148+ ./mm-${VERSION}-<platform> -acp # from an editor (Zed, …)
149+
150+On macOS, an unsigned download is quarantined until you say otherwise:
151+
152+ xattr -d com.apple.quarantine mm-${VERSION}-darwin-arm64
153+
154+## Verifying the download
155+
156+ sha256sum -c SHA256SUMS --ignore-missing # shasum -a 256 -c on macOS
157+EOM
158+echo " ✅ README.md"
159+
160+echo ""
161+echo "✨ Build complete!"
162+ls -lh "${RELEASES_DIR}"
new file mode 100644
@@ -0,0 +1,162 @@
1+#!/bin/bash
2+: <<'COMMENT'
3+Build the release binaries of mm and stage them under release/${TAG}/
4+
5+Usage:
6+ ./02-build-releases.sh # TAG and ABOUT come from release.env
7+ ./02-build-releases.sh v0.2.0 # override the tag for this run (what CI does)
8+
9+Only the Go toolchain is needed: every binary is a plain `go build`, there is
10+no Makefile and no helper script. The same command works on a laptop and in a
11+Rickub CI job (runners are linux/amd64; the other targets are cross-compiled).
12+
13+What ends up in release/${TAG}/:
14+ mm-<version>-<os>-<arch>[.exe] one binary per platform
15+ SHA256SUMS checksums of the binaries
16+ README.md download table + how to run
17+COMMENT
18+
19+set -euo pipefail
20+
21+# release.env carries TAG ("v0.2.0") and ABOUT (the one-line description). It
22+# is git-ignored (*.env), so a CI job does not have it: there the tag comes
23+# from the command line and ABOUT from the environment, or defaults to the
24+# tag. A tag given on the command line always wins, so a test build never
25+# edits the file.
26+if [ -f release.env ]; then
27+ source release.env
28+fi
29+TAG="${1:-${TAG:-}}"
30+ABOUT="${ABOUT:-Mini Me ${TAG}}"
31+
32+# A tag that is not vMAJOR.MINOR.PATCH[-prerelease] is a typo — measured: a
33+# release.env with "v0.o.0" (letter o) would otherwise build and stage
34+# binaries nobody can name.
35+if ! [[ "${TAG}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$ ]]; then
36+ echo "❌ TAG must look like v1.2.3 or v1.2.3-rc.1, got '${TAG}' (check release.env)"
37+ exit 1
38+fi
39+
40+# The platforms a release is built for. Add or remove a line and everything
41+# below follows: the builds, the checksums and the README.
42+PLATFORMS=(
43+ "darwin/arm64"
44+ "linux/amd64"
45+ "linux/arm64"
46+ "windows/amd64"
47+ "windows/arm64"
48+)
49+
50+# The tag is "v0.2.0"; the assets carry the bare version, "0.2.0".
51+VERSION="${TAG#v}"
52+RELEASES_DIR="release/${TAG}"
53+
54+# -trimpath keeps this machine's paths out of binaries that go to strangers;
55+# -s -w drops the symbol table and DWARF, about a third of the size. mm has no
56+# version variable to stamp, so there is no -X here: the file name and the
57+# SHA256SUMS are what tie a binary to its release.
58+BUILD_FLAGS=(-trimpath -ldflags "-s -w")
59+
60+echo "🚀 Building Mini Me ${TAG} — ${ABOUT}"
61+echo "🐹 $(go version)"
62+
63+rm -rf "${RELEASES_DIR}"
64+mkdir -p "${RELEASES_DIR}"
65+
66+# assetName is what the binary for a platform is called once staged. Windows
67+# executables carry .exe, or Windows will not run them.
68+assetName() {
69+ local goos=$1 goarch=$2
70+ local name="mm-${VERSION}-${goos}-${goarch}"
71+ if [ "${goos}" = "windows" ]; then
72+ name="${name}.exe"
73+ fi
74+ printf '%s\n' "${name}"
75+}
76+
77+echo ""
78+echo "🔨 Cross-compiling for ${#PLATFORMS[@]} platforms..."
79+
80+for platform in "${PLATFORMS[@]}"; do
81+ goos="${platform%/*}"
82+ goarch="${platform#*/}"
83+ asset="$(assetName "${goos}" "${goarch}")"
84+
85+ # CGO_ENABLED=0: nothing to link against on the other side of a
86+ # cross-compile, and mm needs no C — genkit, tcell and the ACP SDK are pure
87+ # Go. Without it, a darwin build from a Linux box fails at link time.
88+ if ! CGO_ENABLED=0 GOOS="${goos}" GOARCH="${goarch}" \
89+ go build "${BUILD_FLAGS[@]}" -o "${RELEASES_DIR}/${asset}" .; then
90+ echo " ❌ ${platform}"
91+ exit 1
92+ fi
93+ echo " ✅ ${asset}"
94+done
95+
96+# The binary for this machine is the only one that can run here, and running
97+# it is the one proof that what ships starts at all: -h prints the usage and
98+# exits 0 before any config or model server is looked for.
99+HOST_ASSET="$(assetName "$(go env GOOS)" "$(go env GOARCH)")"
100+if [ -x "${RELEASES_DIR}/${HOST_ASSET}" ]; then
101+ if ! "${RELEASES_DIR}/${HOST_ASSET}" -h >/dev/null 2>&1; then
102+ echo " ❌ ${HOST_ASSET} does not start (mm -h failed)"
103+ exit 1
104+ fi
105+ echo " ✅ ${HOST_ASSET} starts"
106+fi
107+
108+# checksum runs whichever of the two tools this machine has: sha256sum on
109+# Linux, shasum on macOS.
110+checksum() {
111+ if command -v sha256sum >/dev/null 2>&1; then
112+ sha256sum "$@"
113+ else
114+ shasum -a 256 "$@"
115+ fi
116+}
117+
118+# One SHA256SUMS for every platform, names only (no directory), which is what
119+# `sha256sum -c` expects to read next to the downloaded files.
120+(cd "${RELEASES_DIR}" && checksum mm-"${VERSION}"-* >SHA256SUMS)
121+echo " ✅ SHA256SUMS"
122+
123+# downloadTable lists the platforms as a Markdown table, so the README grows
124+# and shrinks with PLATFORMS rather than repeating it by hand.
125+downloadTable() {
126+ printf '| Platform | Download |\n|---|---|\n'
127+ for platform in "${PLATFORMS[@]}"; do
128+ local goos="${platform%/*}" goarch="${platform#*/}"
129+ printf '| %s | `%s` |\n' "${platform}" "$(assetName "${goos}" "${goarch}")"
130+ done
131+}
132+
133+cat >"${RELEASES_DIR}/README.md" <<EOM
134+# Mini Me ${TAG}
135+
136+${ABOUT}
137+
138+Built with $(go env GOVERSION), CGO disabled. No runtime dependency besides a
139+model server (Docker Model Runner or llama-server).
140+
141+$(downloadTable)
142+
143+## Running it
144+
145+ chmod +x mm-${VERSION}-<platform>
146+ ./mm-${VERSION}-<platform> agent.yaml # line mode
147+ ./mm-${VERSION}-<platform> -tui agent.yaml # window mode
148+ ./mm-${VERSION}-<platform> -acp # from an editor (Zed, …)
149+
150+On macOS, an unsigned download is quarantined until you say otherwise:
151+
152+ xattr -d com.apple.quarantine mm-${VERSION}-darwin-arm64
153+
154+## Verifying the download
155+
156+ sha256sum -c SHA256SUMS --ignore-missing # shasum -a 256 -c on macOS
157+EOM
158+echo " ✅ README.md"
159+
160+echo ""
161+echo "✨ Build complete!"
162+ls -lh "${RELEASES_DIR}"