Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
86 changes: 86 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
name: Release

on:
push:
tags:
- "v*"

permissions:
contents: write

jobs:
build:
name: Build (${{ matrix.goos }}/${{ matrix.goarch }})
runs-on: ubuntu-latest
strategy:
matrix:
include:
- goos: linux
goarch: amd64
- goos: linux
goarch: arm64
- goos: darwin
goarch: amd64
- goos: darwin
goarch: arm64

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: "1.24"
cache: true

- name: Get version from tag
id: version
run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT

- name: Build binaries
env:
GOOS: ${{ matrix.goos }}
GOARCH: ${{ matrix.goarch }}
CGO_ENABLED: 0
run: |
VERSION=${{ steps.version.outputs.VERSION }}
LDFLAGS="-X github.com/pmarsceill/mapcli/internal/cli.Version=${VERSION}"

mkdir -p dist
go build -ldflags "${LDFLAGS}" -o dist/map ./cmd/map
go build -ldflags "${LDFLAGS}" -o dist/mapd ./cmd/mapd

- name: Create tarball
run: |
PLATFORM="${{ matrix.goos }}-${{ matrix.goarch }}"
tar -czvf "map-${PLATFORM}.tar.gz" -C dist map mapd

- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: map-${{ matrix.goos }}-${{ matrix.goarch }}
path: map-${{ matrix.goos }}-${{ matrix.goarch }}.tar.gz

release:
name: Create Release
needs: build
runs-on: ubuntu-latest

steps:
- name: Download all artifacts
uses: actions/download-artifact@v4
with:
path: artifacts

- name: Collect tarballs
run: |
mkdir -p release
find artifacts -name "*.tar.gz" -exec mv {} release/ \;
ls -la release/

- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
files: release/*.tar.gz
generate_release_notes: true
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,29 @@ A tool for spawning and managing multiple AI coding agents (Claude Code and Open

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

## Installation

**Quick install (macOS and Linux):**

```bash
curl -fsSL https://raw.githubusercontent.com/pmarsceill/mapcli/main/install.sh | bash
```

This installs both `map` and `mapd` to `~/.local/bin`. Make sure this directory is in your PATH.

**Manual installation:**

Download the latest release from the [releases page](https://github.com/pmarsceill/mapcli/releases) and extract the binaries to a directory in your PATH.

**Build from source:**

```bash
git clone https://github.com/pmarsceill/mapcli.git
cd mapcli
make build
# Binaries are in bin/
```

## Overview

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:
Expand Down
136 changes: 136 additions & 0 deletions install.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
#!/bin/bash
#
# MAP CLI Installer
# Usage: curl -fsSL https://raw.githubusercontent.com/pmarsceill/mapcli/main/install.sh | bash
#

set -e

REPO="pmarsceill/mapcli"
INSTALL_DIR="${MAP_INSTALL_DIR:-$HOME/.local/bin}"

# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color

info() {
echo -e "${GREEN}[INFO]${NC} $1"
}

warn() {
echo -e "${YELLOW}[WARN]${NC} $1"
}

error() {
echo -e "${RED}[ERROR]${NC} $1"
exit 1
}

# Detect OS
detect_os() {
local os
os=$(uname -s)
case "$os" in
Linux)
echo "linux"
;;
Darwin)
echo "darwin"
;;
*)
error "Unsupported operating system: $os"
;;
esac
}

# Detect architecture
detect_arch() {
local arch
arch=$(uname -m)
case "$arch" in
x86_64|amd64)
echo "amd64"
;;
arm64|aarch64)
echo "arm64"
;;
*)
error "Unsupported architecture: $arch"
;;
esac
}

# Get latest release version from GitHub API
get_latest_version() {
local version
version=$(curl -fsSL "https://api.github.com/repos/${REPO}/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/')
if [ -z "$version" ]; then
error "Failed to fetch latest version"
fi
echo "$version"
}

main() {
info "Installing MAP CLI..."

# Detect platform
local os arch platform
os=$(detect_os)
arch=$(detect_arch)
platform="${os}-${arch}"
info "Detected platform: $platform"

# Get latest version
local version
version=$(get_latest_version)
info "Latest version: $version"

# Construct download URL
local download_url="https://github.com/${REPO}/releases/download/${version}/map-${platform}.tar.gz"
info "Downloading from: $download_url"

# Create install directory
mkdir -p "$INSTALL_DIR"

# Download and extract
local tmp_dir
tmp_dir=$(mktemp -d)
trap 'rm -rf "$tmp_dir"' EXIT

if ! curl -fsSL "$download_url" -o "$tmp_dir/map.tar.gz"; then
error "Failed to download release"
fi

if ! tar -xzf "$tmp_dir/map.tar.gz" -C "$tmp_dir"; then
error "Failed to extract archive"
fi

# Install binaries
mv "$tmp_dir/map" "$INSTALL_DIR/map"
mv "$tmp_dir/mapd" "$INSTALL_DIR/mapd"
chmod +x "$INSTALL_DIR/map" "$INSTALL_DIR/mapd"

info "Installed map and mapd to $INSTALL_DIR"

# Verify installation
if "$INSTALL_DIR/map" --version > /dev/null 2>&1; then
info "Installation verified: $("$INSTALL_DIR"/map --version)"
else
warn "Installation completed but verification failed"
fi

# Check if install dir is in PATH
if [[ ":$PATH:" != *":$INSTALL_DIR:"* ]]; then
warn "$INSTALL_DIR is not in your PATH"
echo ""
echo "Add it to your shell profile:"
echo " export PATH=\"\$PATH:$INSTALL_DIR\""
echo ""
fi

info "Installation complete!"
}

main "$@"
87 changes: 87 additions & 0 deletions install_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package mapcli

import (
"os"
"os/exec"
"strings"
"testing"
)

func TestInstallScriptExists(t *testing.T) {
_, err := os.Stat("install.sh")
if os.IsNotExist(err) {
t.Fatal("install.sh does not exist")
}
if err != nil {
t.Fatalf("failed to stat install.sh: %v", err)
}
}

func TestInstallScriptExecutable(t *testing.T) {
info, err := os.Stat("install.sh")
if err != nil {
t.Fatalf("failed to stat install.sh: %v", err)
}

// Check if executable bit is set for owner
if info.Mode()&0100 == 0 {
t.Error("install.sh should be executable")
}
}

func TestInstallScriptSyntax(t *testing.T) {
cmd := exec.Command("bash", "-n", "install.sh")
output, err := cmd.CombinedOutput()
if err != nil {
t.Fatalf("install.sh has syntax errors: %v\n%s", err, output)
}
}

func TestInstallScriptContent(t *testing.T) {
content, err := os.ReadFile("install.sh")
if err != nil {
t.Fatalf("failed to read install.sh: %v", err)
}

script := string(content)

// Check for required elements
checks := []struct {
name string
contains string
}{
{"shebang", "#!/bin/bash"},
{"repo reference", "pmarsceill/mapcli"},
{"OS detection", "uname -s"},
{"arch detection", "uname -m"},
{"GitHub API", "api.github.com"},
{"install dir", "INSTALL_DIR"},
{"error handling", "set -e"},
{"linux support", "linux"},
{"darwin support", "darwin"},
{"amd64 support", "amd64"},
{"arm64 support", "arm64"},
}

for _, check := range checks {
t.Run(check.name, func(t *testing.T) {
if !strings.Contains(script, check.contains) {
t.Errorf("install.sh should contain %q", check.contains)
}
})
}
}

func TestInstallScriptShellcheck(t *testing.T) {
// Skip if shellcheck is not installed
_, err := exec.LookPath("shellcheck")
if err != nil {
t.Skip("shellcheck not installed")
}

cmd := exec.Command("shellcheck", "install.sh")
output, err := cmd.CombinedOutput()
if err != nil {
t.Fatalf("shellcheck found issues:\n%s", output)
}
}
10 changes: 7 additions & 3 deletions internal/cli/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,17 @@ import (
"github.com/spf13/cobra"
)

// Version is set via -ldflags at build time
var Version = "dev"

var socketPath string

// rootCmd is the base command
var rootCmd = &cobra.Command{
Use: "map",
Short: "Multi-agent coordination CLI",
Long: `map is a CLI for coordinating multiple agents through the mapd daemon.`,
Use: "map",
Short: "Multi-agent coordination CLI",
Long: `map is a CLI for coordinating multiple agents through the mapd daemon.`,
Version: Version,
}

// Execute runs the CLI
Expand Down
Loading