Skip to content

Commit 44f1809

Browse files
authored
Merge pull request #13 from pmarsceill/feat/automated-binary-distribution
feat(release): add automated binary distribution via GitHub
2 parents f51f211 + eca882d commit 44f1809

6 files changed

Lines changed: 411 additions & 3 deletions

File tree

.github/workflows/release.yml

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
name: Release
2+
3+
on:
4+
push:
5+
tags:
6+
- "v*"
7+
8+
permissions:
9+
contents: write
10+
11+
jobs:
12+
build:
13+
name: Build (${{ matrix.goos }}/${{ matrix.goarch }})
14+
runs-on: ubuntu-latest
15+
strategy:
16+
matrix:
17+
include:
18+
- goos: linux
19+
goarch: amd64
20+
- goos: linux
21+
goarch: arm64
22+
- goos: darwin
23+
goarch: amd64
24+
- goos: darwin
25+
goarch: arm64
26+
27+
steps:
28+
- name: Checkout code
29+
uses: actions/checkout@v4
30+
31+
- name: Set up Go
32+
uses: actions/setup-go@v5
33+
with:
34+
go-version: "1.24"
35+
cache: true
36+
37+
- name: Get version from tag
38+
id: version
39+
run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
40+
41+
- name: Build binaries
42+
env:
43+
GOOS: ${{ matrix.goos }}
44+
GOARCH: ${{ matrix.goarch }}
45+
CGO_ENABLED: 0
46+
run: |
47+
VERSION=${{ steps.version.outputs.VERSION }}
48+
LDFLAGS="-X github.com/pmarsceill/mapcli/internal/cli.Version=${VERSION}"
49+
50+
mkdir -p dist
51+
go build -ldflags "${LDFLAGS}" -o dist/map ./cmd/map
52+
go build -ldflags "${LDFLAGS}" -o dist/mapd ./cmd/mapd
53+
54+
- name: Create tarball
55+
run: |
56+
PLATFORM="${{ matrix.goos }}-${{ matrix.goarch }}"
57+
tar -czvf "map-${PLATFORM}.tar.gz" -C dist map mapd
58+
59+
- name: Upload artifact
60+
uses: actions/upload-artifact@v4
61+
with:
62+
name: map-${{ matrix.goos }}-${{ matrix.goarch }}
63+
path: map-${{ matrix.goos }}-${{ matrix.goarch }}.tar.gz
64+
65+
release:
66+
name: Create Release
67+
needs: build
68+
runs-on: ubuntu-latest
69+
70+
steps:
71+
- name: Download all artifacts
72+
uses: actions/download-artifact@v4
73+
with:
74+
path: artifacts
75+
76+
- name: Collect tarballs
77+
run: |
78+
mkdir -p release
79+
find artifacts -name "*.tar.gz" -exec mv {} release/ \;
80+
ls -la release/
81+
82+
- name: Create GitHub Release
83+
uses: softprops/action-gh-release@v2
84+
with:
85+
files: release/*.tar.gz
86+
generate_release_notes: true

README.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,29 @@ A tool for spawning and managing multiple AI coding agents (Claude Code and Open
66

77
https://github.com/user-attachments/assets/1e0f02fe-fdbb-4cf7-bff4-a2161662b7a2
88

9+
## Installation
10+
11+
**Quick install (macOS and Linux):**
12+
13+
```bash
14+
curl -fsSL https://raw.githubusercontent.com/pmarsceill/mapcli/main/install.sh | bash
15+
```
16+
17+
This installs both `map` and `mapd` to `~/.local/bin`. Make sure this directory is in your PATH.
18+
19+
**Manual installation:**
20+
21+
Download the latest release from the [releases page](https://github.com/pmarsceill/mapcli/releases) and extract the binaries to a directory in your PATH.
22+
23+
**Build from source:**
24+
25+
```bash
26+
git clone https://github.com/pmarsceill/mapcli.git
27+
cd mapcli
28+
make build
29+
# Binaries are in bin/
30+
```
31+
932
## Overview
1033

1134
MAP (Multi-Agent Platform) provides infrastructure for spawning and coordinating multiple AI coding agents. It supports both **Claude Code** and **OpenAI Codex** agents. The architecture separates concerns:

install.sh

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
#!/bin/bash
2+
#
3+
# MAP CLI Installer
4+
# Usage: curl -fsSL https://raw.githubusercontent.com/pmarsceill/mapcli/main/install.sh | bash
5+
#
6+
7+
set -e
8+
9+
REPO="pmarsceill/mapcli"
10+
INSTALL_DIR="${MAP_INSTALL_DIR:-$HOME/.local/bin}"
11+
12+
# Colors for output
13+
RED='\033[0;31m'
14+
GREEN='\033[0;32m'
15+
YELLOW='\033[1;33m'
16+
NC='\033[0m' # No Color
17+
18+
info() {
19+
echo -e "${GREEN}[INFO]${NC} $1"
20+
}
21+
22+
warn() {
23+
echo -e "${YELLOW}[WARN]${NC} $1"
24+
}
25+
26+
error() {
27+
echo -e "${RED}[ERROR]${NC} $1"
28+
exit 1
29+
}
30+
31+
# Detect OS
32+
detect_os() {
33+
local os
34+
os=$(uname -s)
35+
case "$os" in
36+
Linux)
37+
echo "linux"
38+
;;
39+
Darwin)
40+
echo "darwin"
41+
;;
42+
*)
43+
error "Unsupported operating system: $os"
44+
;;
45+
esac
46+
}
47+
48+
# Detect architecture
49+
detect_arch() {
50+
local arch
51+
arch=$(uname -m)
52+
case "$arch" in
53+
x86_64|amd64)
54+
echo "amd64"
55+
;;
56+
arm64|aarch64)
57+
echo "arm64"
58+
;;
59+
*)
60+
error "Unsupported architecture: $arch"
61+
;;
62+
esac
63+
}
64+
65+
# Get latest release version from GitHub API
66+
get_latest_version() {
67+
local version
68+
version=$(curl -fsSL "https://api.github.com/repos/${REPO}/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/')
69+
if [ -z "$version" ]; then
70+
error "Failed to fetch latest version"
71+
fi
72+
echo "$version"
73+
}
74+
75+
main() {
76+
info "Installing MAP CLI..."
77+
78+
# Detect platform
79+
local os arch platform
80+
os=$(detect_os)
81+
arch=$(detect_arch)
82+
platform="${os}-${arch}"
83+
info "Detected platform: $platform"
84+
85+
# Get latest version
86+
local version
87+
version=$(get_latest_version)
88+
info "Latest version: $version"
89+
90+
# Construct download URL
91+
local download_url="https://github.com/${REPO}/releases/download/${version}/map-${platform}.tar.gz"
92+
info "Downloading from: $download_url"
93+
94+
# Create install directory
95+
mkdir -p "$INSTALL_DIR"
96+
97+
# Download and extract
98+
local tmp_dir
99+
tmp_dir=$(mktemp -d)
100+
trap 'rm -rf "$tmp_dir"' EXIT
101+
102+
if ! curl -fsSL "$download_url" -o "$tmp_dir/map.tar.gz"; then
103+
error "Failed to download release"
104+
fi
105+
106+
if ! tar -xzf "$tmp_dir/map.tar.gz" -C "$tmp_dir"; then
107+
error "Failed to extract archive"
108+
fi
109+
110+
# Install binaries
111+
mv "$tmp_dir/map" "$INSTALL_DIR/map"
112+
mv "$tmp_dir/mapd" "$INSTALL_DIR/mapd"
113+
chmod +x "$INSTALL_DIR/map" "$INSTALL_DIR/mapd"
114+
115+
info "Installed map and mapd to $INSTALL_DIR"
116+
117+
# Verify installation
118+
if "$INSTALL_DIR/map" --version > /dev/null 2>&1; then
119+
info "Installation verified: $("$INSTALL_DIR"/map --version)"
120+
else
121+
warn "Installation completed but verification failed"
122+
fi
123+
124+
# Check if install dir is in PATH
125+
if [[ ":$PATH:" != *":$INSTALL_DIR:"* ]]; then
126+
warn "$INSTALL_DIR is not in your PATH"
127+
echo ""
128+
echo "Add it to your shell profile:"
129+
echo " export PATH=\"\$PATH:$INSTALL_DIR\""
130+
echo ""
131+
fi
132+
133+
info "Installation complete!"
134+
}
135+
136+
main "$@"

install_test.go

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
package mapcli
2+
3+
import (
4+
"os"
5+
"os/exec"
6+
"strings"
7+
"testing"
8+
)
9+
10+
func TestInstallScriptExists(t *testing.T) {
11+
_, err := os.Stat("install.sh")
12+
if os.IsNotExist(err) {
13+
t.Fatal("install.sh does not exist")
14+
}
15+
if err != nil {
16+
t.Fatalf("failed to stat install.sh: %v", err)
17+
}
18+
}
19+
20+
func TestInstallScriptExecutable(t *testing.T) {
21+
info, err := os.Stat("install.sh")
22+
if err != nil {
23+
t.Fatalf("failed to stat install.sh: %v", err)
24+
}
25+
26+
// Check if executable bit is set for owner
27+
if info.Mode()&0100 == 0 {
28+
t.Error("install.sh should be executable")
29+
}
30+
}
31+
32+
func TestInstallScriptSyntax(t *testing.T) {
33+
cmd := exec.Command("bash", "-n", "install.sh")
34+
output, err := cmd.CombinedOutput()
35+
if err != nil {
36+
t.Fatalf("install.sh has syntax errors: %v\n%s", err, output)
37+
}
38+
}
39+
40+
func TestInstallScriptContent(t *testing.T) {
41+
content, err := os.ReadFile("install.sh")
42+
if err != nil {
43+
t.Fatalf("failed to read install.sh: %v", err)
44+
}
45+
46+
script := string(content)
47+
48+
// Check for required elements
49+
checks := []struct {
50+
name string
51+
contains string
52+
}{
53+
{"shebang", "#!/bin/bash"},
54+
{"repo reference", "pmarsceill/mapcli"},
55+
{"OS detection", "uname -s"},
56+
{"arch detection", "uname -m"},
57+
{"GitHub API", "api.github.com"},
58+
{"install dir", "INSTALL_DIR"},
59+
{"error handling", "set -e"},
60+
{"linux support", "linux"},
61+
{"darwin support", "darwin"},
62+
{"amd64 support", "amd64"},
63+
{"arm64 support", "arm64"},
64+
}
65+
66+
for _, check := range checks {
67+
t.Run(check.name, func(t *testing.T) {
68+
if !strings.Contains(script, check.contains) {
69+
t.Errorf("install.sh should contain %q", check.contains)
70+
}
71+
})
72+
}
73+
}
74+
75+
func TestInstallScriptShellcheck(t *testing.T) {
76+
// Skip if shellcheck is not installed
77+
_, err := exec.LookPath("shellcheck")
78+
if err != nil {
79+
t.Skip("shellcheck not installed")
80+
}
81+
82+
cmd := exec.Command("shellcheck", "install.sh")
83+
output, err := cmd.CombinedOutput()
84+
if err != nil {
85+
t.Fatalf("shellcheck found issues:\n%s", output)
86+
}
87+
}

internal/cli/root.go

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,17 @@ import (
77
"github.com/spf13/cobra"
88
)
99

10+
// Version is set via -ldflags at build time
11+
var Version = "dev"
12+
1013
var socketPath string
1114

1215
// rootCmd is the base command
1316
var rootCmd = &cobra.Command{
14-
Use: "map",
15-
Short: "Multi-agent coordination CLI",
16-
Long: `map is a CLI for coordinating multiple agents through the mapd daemon.`,
17+
Use: "map",
18+
Short: "Multi-agent coordination CLI",
19+
Long: `map is a CLI for coordinating multiple agents through the mapd daemon.`,
20+
Version: Version,
1721
}
1822

1923
// Execute runs the CLI

0 commit comments

Comments
 (0)