diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..ef79e5d --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,26 @@ +# Changelog + +## [0.1.1] - 2026-02-19 + +### Changed +- Install script now downloads from GitHub Releases instead of raw.githubusercontent.com +- README overhaul with demo GIF, badges, and restructured content + +### Added +- CONTRIBUTING.md with development and architecture documentation +- CHANGELOG.md +- VHS demo tape for recording demo GIFs +- Launch materials + +## [0.1.0] - 2026-02-16 + +### Added +- Workspace initialization (`mars init`) +- Repository management (`add`, `clone`, `list`) +- Git operations (`status`, `branch`, `checkout`, `sync`) +- Cross-repo command execution (`mars exec`) +- Tag-based filtering for all operations +- Parallel cloning (4 concurrent jobs) +- Shared Claude configuration (`claude.md`, `.claude/`) +- Clack-style terminal UI with Unicode/ASCII fallback +- Distribution via npm, Homebrew, and curl installer diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..554fd86 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,130 @@ +# Contributing to Mars + +## Development Setup + +Clone the repo and run directly in development mode: + +```bash +git clone https://github.com/dean0x/mars.git +cd mars +./mars init # Sources from lib/ directory +``` + +## Project Structure + +``` +mars/ +├── mars # Main CLI entry point +├── lib/ +│ ├── ui.sh # Terminal UI (colors, spinners, prompts, tables) +│ ├── yaml.sh # mars.yaml parser +│ ├── config.sh # Workspace detection and config loading +│ ├── git.sh # Git wrapper with output capture +│ └── commands/ # Command implementations +│ ├── init.sh +│ ├── clone.sh +│ ├── status.sh +│ ├── branch.sh +│ ├── checkout.sh +│ ├── sync.sh +│ ├── exec.sh +│ ├── add.sh +│ └── list.sh +├── build.sh # Build bundled distribution +├── install.sh # Curl installer +├── dist/ # Bundled distribution (committed) +└── test/ # Test suite +``` + +## Architecture + +### Two Operating Modes + +- **Development**: `./mars` sources files from `lib/` subdirectories +- **Distribution**: `dist/mars` is a single bundled file with all code inlined + +### Key Patterns + +**Output Capture** — Git operations capture output in globals: + +```bash +GIT_OUTPUT="" +GIT_ERROR="" +if git_clone "$url" "$path"; then + # success: use GIT_OUTPUT +else + # failure: use GIT_ERROR +fi +``` + +**Bash 3.2 Compatibility** — No associative arrays; uses parallel indexed arrays: + +```bash +YAML_REPO_URLS=() +YAML_REPO_PATHS=() +YAML_REPO_TAGS=() +``` + +**Tag Filtering** — Comma-separated tags with string matching: + +```bash +[[ ",$tags," == *",$filter_tag,"* ]] +``` + +**Command Pattern** — Each command is `cmd_()` in its own file under `lib/commands/`. + +### Implementation Constraints + +- Bash 3.2+ (macOS default) — no associative arrays, no `readarray` +- Return exit codes, never throw (bash has no exceptions) +- Avoid subshells where possible (breaks global variable updates) +- Check return codes explicitly and propagate errors +- Use `ui_step_error()`/`ui_step_done()` for user feedback +- Parallel operations limited to 4 concurrent jobs (`CLONE_PARALLEL_LIMIT`) + +## Running Tests + +```bash +bash test/test_yaml.sh +bash test/test_config.sh +bash test/test_integration.sh +``` + +Tests use `/tmp/claude/` for temporary files. + +## Building + +```bash +./build.sh # Output: dist/mars +``` + +**Important:** `dist/mars` is committed to the repo for easy installation. Always run `./build.sh` before committing changes to source files. + +## Pull Requests + +- Run all tests before submitting +- Run `./build.sh` and include the updated `dist/mars` +- Maintain bash 3.2 compatibility (test on macOS if possible) +- Follow existing code patterns (output capture, parallel arrays, command pattern) + +## Release Process + +1. Update version in `package.json` +2. Commit: `git commit -am "Bump version to X.Y.Z"` +3. Tag: `git tag vX.Y.Z` +4. Push: `git push origin main --tags` + +CI automatically handles: + +- Run tests +- Verify tag version matches `package.json` +- Create GitHub Release with `dist/mars` binary attached +- Publish to npm (`@dean0x/mars`) +- Update Homebrew formula (`dean0x/tap/mars`) + +### Required Secrets (Maintainers) + +| Secret | Repository | Purpose | +|--------|------------|---------| +| `NPM_TOKEN` | mars | npm publish access token | +| `HOMEBREW_TAP_TOKEN` | mars | PAT with repo scope for homebrew-tap workflow | diff --git a/README.md b/README.md index 50c44e6..9adbbb1 100644 --- a/README.md +++ b/README.md @@ -1,83 +1,46 @@ -# Mars CLI +# Mars -Multi Agentic Repo workspace manager for git repositories with shared Claude configuration. +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) +[![npm](https://img.shields.io/npm/v/@dean0x/mars)](https://www.npmjs.com/package/@dean0x/mars) +[![CI](https://github.com/dean0x/mars/actions/workflows/ci.yml/badge.svg)](https://github.com/dean0x/mars/actions/workflows/ci.yml) -## Features +Manage multiple Git repositories as one workspace. -- Manage multiple git repos as a unified workspace -- Shared `claude.md` and `.claude/` config across repos -- Tag-based repo filtering for targeted operations -- Parallel cloning with rate limiting -- Works with bash 3.2+ (macOS compatible) +Tag-based filtering, parallel operations, shared Claude configuration. -## Installation - -### npm (recommended) - -```bash -npm install -g @dean0x/mars -``` - -Or run without installing: - -```bash -npx @dean0x/mars --help -``` +

+ Mars CLI demo +

-### Homebrew (macOS/Linux) +## Why Mars? -```bash -brew install dean0x/tap/mars -``` +- **Polyrepo without the pain** — one CLI for status, branching, syncing across all repos +- **Tag-based filtering** — target subsets of repos (`--tag frontend`, `--tag backend`) +- **Shared Claude config** — `claude.md` and `.claude/` directory shared across repos +- **Zero dependencies** — pure bash 3.2+, works on macOS out of the box -### Shell Script +## Quick Install ```bash -curl -fsSL https://raw.githubusercontent.com/dean0x/mars/main/install.sh | bash +npm install -g @dean0x/mars ``` -### Manual - -```bash -git clone https://github.com/dean0x/mars.git -cd mars -./build.sh -cp dist/mars ~/.local/bin/ # or anywhere in PATH -``` +See [all installation methods](#installation) for Homebrew, curl, and manual options. ## Quick Start ```bash -# Create a new workspace mkdir my-project && cd my-project mars init -# Add repositories -mars add git@github.com:org/frontend.git --tags frontend,web -mars add git@github.com:org/backend.git --tags backend,api -mars add git@github.com:org/shared.git --tags shared +mars add https://github.com/dean0x/mars-example-frontend.git --tags frontend,web +mars add https://github.com/dean0x/mars-example-backend.git --tags backend,api +mars add https://github.com/dean0x/mars-example-shared.git --tags shared -# Clone all repos mars clone - -# Check status mars status ``` -## Workspace Structure - -``` -my-project/ -├── mars.yaml # Workspace configuration -├── claude.md # Shared Claude config (optional) -├── .claude/ # Shared Claude folder (optional) -├── .gitignore # Contains 'repos/' -└── repos/ # Cloned repositories (gitignored) - ├── frontend/ - ├── backend/ - └── shared/ -``` - ## Commands | Command | Description | @@ -128,62 +91,64 @@ defaults: branch: main ``` -## Development - -### Project Structure +## Workspace Structure ``` -mars/ -├── mars # Main CLI entry point -├── lib/ -│ ├── ui.sh # Terminal UI components -│ ├── yaml.sh # YAML parser -│ ├── config.sh # Config management -│ ├── git.sh # Git operations -│ └── commands/ # Command implementations -├── build.sh # Build distribution -├── install.sh # Installer script -└── test/ # Test suite +my-project/ +├── mars.yaml # Workspace configuration +├── claude.md # Shared Claude config (optional) +├── .claude/ # Shared Claude folder (optional) +├── .gitignore # Contains 'repos/' +└── repos/ # Cloned repositories (gitignored) + ├── frontend/ + ├── backend/ + └── shared/ +``` + +## Installation + +### npm (recommended) + +```bash +npm install -g @dean0x/mars ``` -### Running Tests +Or run without installing: ```bash -# Run all tests -bash test/test_yaml.sh -bash test/test_config.sh -bash test/test_integration.sh +npx @dean0x/mars --help ``` -### Building +### Homebrew (macOS/Linux) ```bash -./build.sh # Output: dist/mars +brew install dean0x/tap/mars ``` -**Important:** `dist/mars` is committed for easy installation. Always run `./build.sh` before committing changes to source files. +### Shell Script + +```bash +curl -fsSL https://raw.githubusercontent.com/dean0x/mars/main/install.sh | bash +``` -## Releasing +Install a specific version: -Releases are automated via GitHub Actions. To create a release: +```bash +MARS_VERSION=0.1.1 curl -fsSL https://raw.githubusercontent.com/dean0x/mars/main/install.sh | bash +``` -1. Update version in `package.json` -2. Commit the change -3. Create and push a tag: `git tag v0.1.1 && git push origin v0.1.1` +### Manual -The CI will automatically: -- Run tests -- Verify the tag version matches `package.json` -- Create a GitHub Release with auto-generated notes -- Publish to npm -- Update the Homebrew formula +```bash +git clone https://github.com/dean0x/mars.git +cd mars +./build.sh +cp dist/mars ~/.local/bin/ # or anywhere in PATH +``` -### Required Secrets (for maintainers) +## Contributing -| Secret | Repository | Purpose | -|--------|------------|---------| -| `NPM_TOKEN` | mars | npm publish access token | -| `HOMEBREW_TAP_TOKEN` | mars | PAT with repo scope to trigger homebrew-tap workflow | +See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup, architecture, and release process. ## License diff --git a/build.sh b/build.sh index d89f586..312c417 100755 --- a/build.sh +++ b/build.sh @@ -19,7 +19,7 @@ cat > "$OUTPUT_FILE" << 'HEADER' set -euo pipefail -MARS_VERSION="0.1.0" +MARS_VERSION="0.1.1" HEADER diff --git a/demo.tape b/demo.tape new file mode 100644 index 0000000..da3b83b --- /dev/null +++ b/demo.tape @@ -0,0 +1,62 @@ +Output demo.gif + +Set FontSize 18 +Set Width 1200 +Set Height 650 +Set Padding 24 +Set Theme "Catppuccin Mocha" +Set WindowBar Colorful +Set TypingSpeed 40ms +Set Framerate 30 + +# Clean start +Hide +Type "cd $(mktemp -d) && clear" +Enter +Sleep 500ms +Show + +# Step 1: Initialize workspace +Type "mars init" +Enter +Sleep 1.5s + +# Type workspace name when prompted +Type "my-project" +Enter +Sleep 1s + +# Confirm Claude config (press enter for yes) +Enter +Sleep 1s + +# Step 2: Add repos with tags +Type "mars add https://github.com/dean0x/mars-example-frontend.git --tags frontend,web" +Enter +Sleep 1.5s + +Type "mars add https://github.com/dean0x/mars-example-backend.git --tags backend,api" +Enter +Sleep 1.5s + +Type "mars add https://github.com/dean0x/mars-example-shared.git --tags shared" +Enter +Sleep 1.5s + +# Step 3: Clone all repos +Type "mars clone" +Enter +Sleep 4s + +# Step 4: Check status +Type "mars status" +Enter +Sleep 2s + +# Step 5: Tag filtering with exec +Type 'mars exec "git log --oneline -3" --tag frontend' +Enter +Sleep 2s + +# Final pause +Sleep 2s diff --git a/dist/mars b/dist/mars index 1c11d88..da42e95 100755 --- a/dist/mars +++ b/dist/mars @@ -5,7 +5,7 @@ set -euo pipefail -MARS_VERSION="0.1.0" +MARS_VERSION="0.1.1" # === lib/ui.sh === @@ -255,14 +255,14 @@ ui_select() { ((selected > 0)) && ((selected--)) ;; '[B') # Down - ((selected < count - 1)) && ((selected++)) + ((selected < count - 1)) && selected=$((selected + 1)) ;; esac elif [[ "$key" == "" ]]; then # Enter pressed break elif [[ "$key" == "j" ]]; then - ((selected < count - 1)) && ((selected++)) + ((selected < count - 1)) && selected=$((selected + 1)) elif [[ "$key" == "k" ]]; then ((selected > 0)) && ((selected--)) fi @@ -738,7 +738,7 @@ config_repo_count() { repos=$(config_get_repos "$tag") while IFS= read -r repo; do - [[ -n "$repo" ]] && ((count++)) + [[ -n "$repo" ]] && count=$((count + 1)) done <<< "$repos" printf '%d' "$count" @@ -1191,7 +1191,7 @@ cmd_clone() { while IFS= read -r repo; do [[ -z "$repo" ]] && continue - ((total++)) + total=$((total + 1)) local path path=$(yaml_get_path "$repo") @@ -1300,10 +1300,10 @@ _clone_wait_one() { if [[ $exit_code -eq 0 ]]; then ui_step_done "Cloned:" "$path" - ((success_count++)) + success_count=$((success_count + 1)) else ui_step_error "Failed to clone: $path" - ((fail_count++)) + fail_count=$((fail_count + 1)) failed_repos+=("$repo") fi @@ -1368,7 +1368,7 @@ cmd_status() { if [[ ! -d "$full_path" ]]; then ui_table_row "$path" "$(ui_dim "not cloned")" "-" "-" - ((not_cloned++)) + not_cloned=$((not_cloned + 1)) continue fi @@ -1474,7 +1474,7 @@ cmd_branch() { if [[ ! -d "$full_path" ]]; then ui_step_done "Skipped (not cloned):" "$path" - ((skip_count++)) + skip_count=$((skip_count + 1)) continue fi @@ -1483,10 +1483,10 @@ cmd_branch() { # Try to checkout instead if git_checkout "$full_path" "$branch_name"; then ui_step_done "Checked out existing:" "$path → $branch_name" - ((success_count++)) + success_count=$((success_count + 1)) else ui_step_error "Failed to checkout existing branch: $path" - ((fail_count++)) + fail_count=$((fail_count + 1)) fi continue fi @@ -1494,10 +1494,10 @@ cmd_branch() { # Create new branch if git_branch "$full_path" "$branch_name"; then ui_step_done "Created:" "$path → $branch_name" - ((success_count++)) + success_count=$((success_count + 1)) else ui_step_error "Failed: $path - $GIT_ERROR" - ((fail_count++)) + fail_count=$((fail_count + 1)) fi done <<< "$repos" @@ -1583,7 +1583,7 @@ cmd_checkout() { if [[ ! -d "$full_path" ]]; then ui_step_done "Skipped (not cloned):" "$path" - ((skip_count++)) + skip_count=$((skip_count + 1)) continue fi @@ -1591,24 +1591,24 @@ cmd_checkout() { if git_is_dirty "$full_path" && [[ $force -eq 0 ]]; then ui_step_error "Uncommitted changes: $path" dirty_repos+=("$path") - ((fail_count++)) + fail_count=$((fail_count + 1)) continue fi # Check if branch exists if ! git_branch_exists "$full_path" "$branch_name"; then ui_step_error "Branch not found: $path" - ((fail_count++)) + fail_count=$((fail_count + 1)) continue fi # Checkout if git_checkout "$full_path" "$branch_name"; then ui_step_done "Checked out:" "$path → $branch_name" - ((success_count++)) + success_count=$((success_count + 1)) else ui_step_error "Failed: $path - $GIT_ERROR" - ((fail_count++)) + fail_count=$((fail_count + 1)) fi done <<< "$repos" @@ -1684,7 +1684,7 @@ cmd_sync() { if [[ ! -d "$full_path" ]]; then ui_step_done "Skipped (not cloned):" "$path" - ((skip_count++)) + skip_count=$((skip_count + 1)) continue fi @@ -1702,7 +1702,7 @@ cmd_sync() { else ui_step_done "Updated:" "$path" fi - ((success_count++)) + success_count=$((success_count + 1)) else # Check for conflicts if [[ "$GIT_ERROR" == *"conflict"* ]] || [[ "$GIT_ERROR" == *"CONFLICT"* ]]; then @@ -1711,7 +1711,7 @@ cmd_sync() { else ui_step_error "Failed: $path" fi - ((fail_count++)) + fail_count=$((fail_count + 1)) fi done <<< "$repos" @@ -1805,7 +1805,7 @@ cmd_exec() { if [[ ! -d "$full_path" ]]; then ui_step_done "Skipped (not cloned):" "$path" - ((skip_count++)) + skip_count=$((skip_count + 1)) continue fi @@ -1831,10 +1831,10 @@ cmd_exec() { if [[ $exit_code -eq 0 ]]; then ui_step_done "Success:" "$path" - ((success_count++)) + success_count=$((success_count + 1)) else ui_step_error "Failed (exit $exit_code): $path" - ((fail_count++)) + fail_count=$((fail_count + 1)) fi ui_bar_line @@ -1988,7 +1988,7 @@ cmd_list() { while IFS= read -r repo; do [[ -z "$repo" ]] && continue - ((total++)) + total=$((total + 1)) local path path=$(yaml_get_path "$repo") @@ -1999,7 +1999,7 @@ cmd_list() { local cloned_text if [[ -d "$full_path" ]]; then cloned_text="$(ui_green "yes")" - ((cloned++)) + cloned=$((cloned + 1)) else cloned_text="$(ui_dim "no")" fi diff --git a/install.sh b/install.sh index 30910c4..d5b282c 100755 --- a/install.sh +++ b/install.sh @@ -1,15 +1,15 @@ #!/usr/bin/env bash # Mars CLI - Installer # Usage: curl -fsSL https://raw.githubusercontent.com/dean0x/mars/main/install.sh | bash +# Override version: MARS_VERSION=0.1.1 curl -fsSL ... | bash set -euo pipefail # Configuration -MARS_VERSION="0.1.0" +MARS_VERSION="${MARS_VERSION:-latest}" MARS_INSTALL_DIR="${MARS_INSTALL_DIR:-$HOME/.mars}" MARS_BIN_DIR="$MARS_INSTALL_DIR/bin" MARS_REPO="dean0x/mars" -MARS_DOWNLOAD_URL="https://raw.githubusercontent.com/$MARS_REPO/main/dist/mars" # Colors (if terminal supports it) if [[ -t 1 ]] && [[ "${TERM:-}" != "dumb" ]]; then @@ -98,6 +98,15 @@ download() { fi } +# Build download URL based on version +get_download_url() { + if [[ "$MARS_VERSION" == "latest" ]]; then + echo "https://github.com/$MARS_REPO/releases/latest/download/mars" + else + echo "https://github.com/$MARS_REPO/releases/download/v$MARS_VERSION/mars" + fi +} + # Add to PATH in shell config add_to_path() { local rc_file="$1" @@ -121,7 +130,7 @@ add_to_path() { # Main installation main() { printf "\n" - printf "${CYAN}┌${RESET} Mars - Multi Agentic Repo Workspace Manager Installer v%s\n" "$MARS_VERSION" + printf "${CYAN}┌${RESET} Mars - Multi Agentic Repo Workspace Manager Installer\n" printf "${DIM}│${RESET}\n" # Check requirements @@ -135,16 +144,32 @@ main() { success "Created $MARS_BIN_DIR" # Download mars - info "Downloading Mars CLI..." local mars_path="$MARS_BIN_DIR/mars" + local release_url + release_url="$(get_download_url)" - if download "$MARS_DOWNLOAD_URL" "$mars_path"; then + if [[ "$MARS_VERSION" == "latest" ]]; then + info "Downloading Mars CLI (latest release)..." + else + info "Downloading Mars CLI v${MARS_VERSION}..." + fi + + if download "$release_url" "$mars_path"; then chmod +x "$mars_path" success "Downloaded mars executable" else - error "Failed to download Mars CLI" - error "URL: $MARS_DOWNLOAD_URL" - exit 1 + # Fallback to raw.githubusercontent.com + local fallback_url="https://raw.githubusercontent.com/$MARS_REPO/main/dist/mars" + warn "Release download failed, trying fallback..." + if download "$fallback_url" "$mars_path"; then + chmod +x "$mars_path" + warn "Downloaded from fallback (unversioned). Consider installing a tagged release." + else + error "Failed to download Mars CLI" + error "Tried: $release_url" + error "Fallback: $fallback_url" + exit 1 + fi fi # Add to PATH @@ -162,8 +187,9 @@ main() { # Verify installation printf "${DIM}│${RESET}\n" - if "$mars_path" --version &> /dev/null; then - success "Installation verified" + local installed_version + if installed_version=$("$mars_path" --version 2>/dev/null); then + success "Installed $installed_version" else warn "Installation may have issues - please check $mars_path" fi diff --git a/launch/checklist.md b/launch/checklist.md new file mode 100644 index 0000000..e2aab88 --- /dev/null +++ b/launch/checklist.md @@ -0,0 +1,32 @@ +# Launch Checklist + +## Pre-Launch + +- [ ] Demo GIF recorded and committed +- [ ] README renders correctly on GitHub +- [ ] Badges show correct status +- [ ] install.sh works against v0.1.1 release +- [ ] npm install works (`npm i -g @dean0x/mars`) +- [ ] Homebrew install works (`brew install dean0x/tap/mars`) +- [ ] Example repos exist and are public (mars-example-frontend, mars-example-backend, mars-example-shared) +- [ ] GitHub repo description and topics set +- [ ] v0.1.1 tag pushed and CI passes all 5 jobs + +## Rollout Schedule + +| Day | Action | Reference | +|-----|--------|-----------| +| Day 1 | Polish README, record demo GIF, set GitHub metadata | `demo.tape` | +| Day 2 | LinkedIn post (morning, professional audience) | `linkedin.md` | +| Day 3 | Show HN (Tue-Thu, 8-10 AM ET) + X thread | `hackernews.md`, `twitter.md` | +| Day 4 | r/ClaudeAI + r/commandline | `reddit.md` | +| Day 5 | r/programming + r/git | `reddit.md` | +| Week 2 | Dev.to blog post | `devto-outline.md` | + +## Post-Launch + +- [ ] Monitor HN comments and respond within 1 hour +- [ ] Track GitHub stars and npm installs +- [ ] Respond to issues and PRs promptly +- [ ] Cross-link posts (e.g., link HN discussion from Reddit) +- [ ] Update README if common questions emerge diff --git a/launch/devto-outline.md b/launch/devto-outline.md new file mode 100644 index 0000000..de536f1 --- /dev/null +++ b/launch/devto-outline.md @@ -0,0 +1,79 @@ +# Dev.to Blog Post + +**Title:** How to Manage a Polyrepo Workspace with Mars CLI + +**Tags:** git, bash, cli, productivity + +**Target publish:** Week 2 after launch + +--- + +## Outline + +### 1. Introduction — The Polyrepo Problem + +- Not every project fits in a monorepo +- Independent repos mean independent CI, access controls, release cycles +- But daily operations across repos are painful: status, branching, syncing +- The typical solution: a growing collection of shell scripts + +### 2. Existing Tools and Their Tradeoffs + +| Tool | Language | Config | Approach | +|------|----------|--------|----------| +| git submodules | git-native | .gitmodules | Couples repos at git level, tracks specific commits | +| gita | Python | CLI-based | Group and manage repos, Python dependency | +| myrepos | Perl | .mrconfig | Config-file driven, runs arbitrary commands | +| meta | Node | meta.json | JSON config, plugin system | +| **Mars** | Bash | mars.yaml | Tag-based filtering, Claude-aware, zero deps | + +Key differentiators for Mars: +- Pure bash 3.2+ — no runtime dependencies +- Tag-based filtering as a first-class concept +- Shared Claude Code configuration + +### 3. Getting Started with Mars + +- Installation (all 4 methods: npm, Homebrew, curl, manual) +- `mars init` walkthrough — what it creates +- `mars add` — registering repos with tags +- `mars clone` — parallel cloning + +Include code examples and expected output. + +### 4. Daily Workflow + +- Morning routine: `mars sync` then `mars status` +- Starting a feature: `mars branch feature-x --tag backend` +- Running tests: `mars exec "npm test" --tag frontend` +- Checking what changed: `mars status` +- End of day: `mars sync --rebase` + +### 5. Tag-Based Workflows + +- Organizing repos: frontend, backend, shared, infrastructure +- Operations on subsets: `--tag frontend` +- Multiple tags per repo: `--tags frontend,web` +- Real-world example: deploying frontend repos vs backend repos + +### 6. Claude Configuration Sharing + +- What `claude.md` and `.claude/` do in Claude Code +- How Mars shares them at the workspace level +- Workspace structure that enables this +- Use case: consistent AI context across a polyrepo + +### 7. Under the Hood + +- Why bash 3.2+ (macOS default, zero friction) +- Clack-style UI implementation in bash (Unicode/ASCII fallback) +- Parallel operations with bash job control +- Single-file bundling for distribution +- No associative arrays — parallel indexed arrays pattern + +### 8. Conclusion + +- When to use Mars vs monorepo vs submodules +- Mars fits when: independent repos, unified operations, tag-based organization +- Links: GitHub, npm, install command +- Call for feedback and contributions diff --git a/launch/hackernews.md b/launch/hackernews.md new file mode 100644 index 0000000..b261382 --- /dev/null +++ b/launch/hackernews.md @@ -0,0 +1,77 @@ +# Show HN Post + +## Submission + +**Title:** Show HN: Mars – manage multiple Git repos as one workspace + +**URL:** https://github.com/dean0x/mars + +**Timing:** Tuesday-Thursday, 8-10 AM ET + +--- + +## First Comment + +Post immediately after submission: + +--- + +Hi HN, I built Mars because managing 10+ repos with shell scripts was getting painful. Every project had its own `status-all.sh`, `branch-all.sh`, `sync-all.sh` — and they all did the same thing slightly differently. + +Mars gives you a single CLI for the common operations: init a workspace, add repos (with optional tags), clone them in parallel, check status across all of them, create branches, sync, and run arbitrary commands. + +**What it does:** + +- `mars init` creates a workspace with a `mars.yaml` config +- `mars add --tags frontend,web` adds repos with tags +- `mars clone` clones everything (4 parallel jobs) +- `mars status` shows branch, dirty state, and sync status in a table +- `mars exec "npm install" --tag frontend` runs commands on tagged subsets +- `mars branch/checkout/sync` for cross-repo git operations + +**Technical decisions:** + +- Pure bash 3.2+ — ships with every Mac, zero install friction, no runtime dependencies +- Clack-style terminal UI (Unicode spinners, box drawing characters, color) with ASCII fallback +- No associative arrays (bash 3.2 compat) — uses parallel indexed arrays instead +- Parallel operations with job control, capped at 4 concurrent + +**One extra feature:** if you use Claude Code, Mars can share a `claude.md` and `.claude/` directory across all repos in your workspace. Useful if you want consistent AI context across a polyrepo. + +**What it doesn't do (yet):** + +- No Windows support (bash-only) +- No repo removal command +- No workspace-level git hooks +- No dependency graph between repos + +Install: `npm i -g @dean0x/mars` or `brew install dean0x/tap/mars` + +Happy to answer questions about the implementation, bash 3.2 constraints, or polyrepo workflows. + +--- + +## Anticipated Q&A + +**"Why not git submodules?"** + +Different model. Submodules couple repos at the git level — they track specific commits and require coordinated updates. Mars keeps repos fully independent but lets you orchestrate operations across them. You can add/remove repos from a workspace without affecting the repos themselves. + +**"How is this different from gita/myrepos/meta?"** + +- gita (Python): Similar concept, different implementation. Mars is pure bash with zero deps, has tag-based filtering, and Claude config sharing. +- myrepos (Perl): Config-file driven, more complex configuration. Mars uses a simple YAML format. +- meta (Node): JSON-based, heavier runtime. Mars is a single bash script. +- All of them are good tools. Mars trades extensibility for simplicity and zero-friction installation. + +**"Bash in 2026?"** + +bash 3.2 ships with every Mac. `curl | bash` installs it in 5 seconds. No Node, no Python, no Go binary to download. The tradeoff is development speed (bash is painful to write) for deployment simplicity (bash is everywhere). For a CLI that orchestrates git commands, bash is a natural fit. + +**"Why not a monorepo?"** + +Monorepos are great when they work for your team. Mars is for the cases where you need or want independent repos — different CI pipelines, different access controls, different release cycles — but still want unified operations across them. + +**"How does the Claude config sharing work?"** + +When you run `mars init`, it optionally creates a `claude.md` and `.claude/` directory at the workspace root. When you open any repo in the workspace with Claude Code, it walks up the directory tree and finds these shared config files. No symlinks, no copying — just directory structure. diff --git a/launch/linkedin.md b/launch/linkedin.md new file mode 100644 index 0000000..59ea32a --- /dev/null +++ b/launch/linkedin.md @@ -0,0 +1,35 @@ +# LinkedIn Post + +--- + +**How I manage 10+ repositories as a single workspace** + +If you've worked on a project spread across multiple Git repos, you know the pain: checking status means opening each repo. Creating a feature branch means running `git checkout -b` five times. Syncing means pulling in each directory. + +I used to manage this with a collection of shell scripts. Every project had its own `status-all.sh` and `sync-all.sh`. They all did roughly the same thing, slightly differently, and broke in slightly different ways. + +So I built **Mars** — a CLI that manages multiple Git repos as one workspace. + +**What it does:** + +• `mars init` — creates a workspace config +• `mars add --tags frontend,api` — registers repos with tags +• `mars clone` — clones everything in parallel +• `mars status` — one table showing branch, state, and sync status for every repo +• `mars branch feature-x --tag backend` — create branches on specific repo groups +• `mars sync` — pull latest across all repos +• `mars exec "npm test" --tag frontend` — run commands on tagged subsets + +The tag system is the key workflow enabler. Tag repos by team, by function, by whatever makes sense — then target operations at specific groups. Run frontend tests without touching backend repos. Create a branch only where you need it. + +For teams using Claude Code for AI-assisted development, Mars also shares `claude.md` configuration across all repos in the workspace — consistent AI context without manual syncing. + +**Technical choices:** + +• Pure bash 3.2+ — works on every Mac out of the box +• Zero dependencies beyond git +• Single-file distribution + +Install: `npm i -g @dean0x/mars` or `brew install dean0x/tap/mars` + +If you work with polyrepo setups, I'd love your feedback: https://github.com/dean0x/mars diff --git a/launch/reddit.md b/launch/reddit.md new file mode 100644 index 0000000..82ea5b2 --- /dev/null +++ b/launch/reddit.md @@ -0,0 +1,149 @@ +# Reddit Posts + +--- + +## r/ClaudeAI + +**Title:** Built a CLI tool for managing shared Claude config across multiple repos + +**Body:** + +I work with 10+ repos and wanted a way to share `claude.md` and `.claude/` configuration across all of them when using Claude Code. + +So I built **Mars** — a polyrepo workspace manager that, among other things, lets you maintain shared Claude configuration at the workspace level. When you open any repo in the workspace, Claude Code picks up the shared context automatically. + +**How it works:** + +1. `mars init` — creates a workspace with optional `claude.md` and `.claude/` directory +2. `mars add --tags backend,api` — add repos with tags +3. `mars clone` — clones everything into `repos/` subdirectory +4. Open any repo — Claude Code walks up and finds the shared config + +Beyond the Claude integration, it handles the usual multi-repo operations: parallel clone, cross-repo status/branching/syncing, tag-based filtering, and running arbitrary commands across repos. + +Pure bash 3.2+, zero dependencies. + +**Install:** +```bash +npm i -g @dean0x/mars +``` + +GitHub: https://github.com/dean0x/mars + +--- + +## r/commandline + +**Title:** Mars — a polyrepo workspace manager written in pure bash 3.2+ + +**Body:** + +I've been working on a CLI tool for managing multiple Git repos as a unified workspace. It's written entirely in bash 3.2+ (macOS default) with zero external dependencies beyond git. + +**What it does:** + +- Initialize workspaces with `mars init` (interactive prompts) +- Add repos with tags: `mars add --tags frontend,web` +- Parallel clone (4 concurrent jobs) with progress spinners +- Cross-repo status table showing branch, dirty state, sync status +- Tag-based filtering: `mars status --tag backend` +- Run arbitrary commands: `mars exec "npm test" --tag frontend` +- Branch/checkout/sync across repos + +**Technical details:** + +- Clack-style terminal UI — Unicode spinners (◒◐◓◑), box drawing (┌│└), colored output with ASCII fallback for dumb terminals +- No associative arrays (bash 3.2 compat) — parallel indexed arrays instead +- No subshells where possible to preserve global state +- Output capture pattern for git operations (stdout/stderr in globals) +- Single-file distribution: `build.sh` concatenates all modules into one executable + +The whole thing is ~1200 lines across 13 source files, bundled into a single `dist/mars` for distribution. + +**Install:** `npm i -g @dean0x/mars` or `brew install dean0x/tap/mars` or `curl -fsSL https://raw.githubusercontent.com/dean0x/mars/main/install.sh | bash` + +GitHub: https://github.com/dean0x/mars + +--- + +## r/programming + +**Title:** Mars: Manage multiple Git repos as one workspace (polyrepo tooling) + +**Body:** + +If you work with multiple independent repos that belong to the same project, you've probably written shell scripts to check status across all of them, create branches, sync changes, etc. + +**Mars** is a CLI that replaces those scripts. You define a workspace with `mars.yaml`, tag your repos by function (frontend, backend, shared), and use a single CLI for operations across all of them: + +```bash +mars init +mars add git@github.com:org/frontend.git --tags frontend +mars add git@github.com:org/backend.git --tags backend +mars clone # parallel, 4 jobs +mars status # table view across all repos +mars branch feature-auth --tag backend # branch on backend repos only +mars exec "npm test" --tag frontend # run tests on frontend repos +mars sync --rebase # pull latest on all repos +``` + +It's written in bash 3.2+ with no dependencies beyond git. Installs via npm, Homebrew, or curl. + +**Compared to alternatives:** + +- **git submodules** — couples repos at git level; Mars keeps them independent +- **gita** (Python) — similar concept, different tradeoffs (Mars is zero-dep bash) +- **myrepos** (Perl) — more complex config format +- **meta** (Node) — JSON config, heavier runtime + +GitHub: https://github.com/dean0x/mars + +--- + +## r/git + +**Title:** Tool for managing branches, status, and sync across multiple repos + +**Body:** + +Built a CLI for orchestrating git operations across multiple repos. If you work with a polyrepo setup, this replaces the collection of shell scripts you've accumulated: + +**Operations:** + +- `mars clone` — parallel clone of all configured repos (4 concurrent jobs) +- `mars status` — table showing branch, dirty/clean, ahead/behind for every repo +- `mars branch feature-x` — create a branch on all (or tagged) repos +- `mars checkout main` — checkout across repos +- `mars sync --rebase` — pull latest on all repos +- `mars exec "git log --oneline -5"` — run arbitrary commands + +**Tag filtering:** + +Repos can be tagged (`frontend`, `backend`, `shared`, etc.) and every command supports `--tag` to target subsets: + +```bash +mars branch feature-auth --tag backend +mars exec "npm install" --tag frontend +mars status --tag shared +``` + +**Config:** + +Simple `mars.yaml`: + +```yaml +version: 1 +workspace: + name: "my-project" +repos: + - url: git@github.com:org/frontend.git + tags: [frontend, web] + - url: git@github.com:org/backend.git + tags: [backend, api] +defaults: + branch: main +``` + +Pure bash 3.2+, no dependencies beyond git. + +GitHub: https://github.com/dean0x/mars diff --git a/launch/twitter.md b/launch/twitter.md new file mode 100644 index 0000000..53c83af --- /dev/null +++ b/launch/twitter.md @@ -0,0 +1,55 @@ +# X/Twitter Thread + +--- + +## Tweet 1 (Hook) + +Managing 10+ Git repos is painful. + +Status across all? Shell scripts. +Branch on all? More scripts. +Sync all? You get the idea. + +I built something better. + +🧵 + +--- + +## Tweet 2 (Solution) + +Introducing Mars — a polyrepo workspace manager. + +One CLI for: +→ init/clone/status across all repos +→ Tag-based filtering (--tag frontend) +→ Parallel operations (4 concurrent jobs) +→ Cross-repo branching & sync + +Pure bash 3.2+, zero dependencies. + +--- + +## Tweet 3 (Differentiator) + +The killer feature: shared Claude configuration. + +Your `claude.md` and `.claude/` directory work across all repos in the workspace. + +Multi-repo Claude Code workflows, finally. + +--- + +## Tweet 4 (CTA) + +Install in 10 seconds: + +``` +npm i -g @dean0x/mars +``` + +or `brew install dean0x/tap/mars` + +GitHub: github.com/dean0x/mars + +[Attach demo.gif] diff --git a/lib/commands/branch.sh b/lib/commands/branch.sh index 679296d..77a2359 100644 --- a/lib/commands/branch.sh +++ b/lib/commands/branch.sh @@ -60,7 +60,7 @@ cmd_branch() { if [[ ! -d "$full_path" ]]; then ui_step_done "Skipped (not cloned):" "$path" - ((skip_count++)) + skip_count=$((skip_count + 1)) continue fi @@ -69,10 +69,10 @@ cmd_branch() { # Try to checkout instead if git_checkout "$full_path" "$branch_name"; then ui_step_done "Checked out existing:" "$path → $branch_name" - ((success_count++)) + success_count=$((success_count + 1)) else ui_step_error "Failed to checkout existing branch: $path" - ((fail_count++)) + fail_count=$((fail_count + 1)) fi continue fi @@ -80,10 +80,10 @@ cmd_branch() { # Create new branch if git_branch "$full_path" "$branch_name"; then ui_step_done "Created:" "$path → $branch_name" - ((success_count++)) + success_count=$((success_count + 1)) else ui_step_error "Failed: $path - $GIT_ERROR" - ((fail_count++)) + fail_count=$((fail_count + 1)) fi done <<< "$repos" diff --git a/lib/commands/checkout.sh b/lib/commands/checkout.sh index 426453e..a6d65a0 100644 --- a/lib/commands/checkout.sh +++ b/lib/commands/checkout.sh @@ -66,7 +66,7 @@ cmd_checkout() { if [[ ! -d "$full_path" ]]; then ui_step_done "Skipped (not cloned):" "$path" - ((skip_count++)) + skip_count=$((skip_count + 1)) continue fi @@ -74,24 +74,24 @@ cmd_checkout() { if git_is_dirty "$full_path" && [[ $force -eq 0 ]]; then ui_step_error "Uncommitted changes: $path" dirty_repos+=("$path") - ((fail_count++)) + fail_count=$((fail_count + 1)) continue fi # Check if branch exists if ! git_branch_exists "$full_path" "$branch_name"; then ui_step_error "Branch not found: $path" - ((fail_count++)) + fail_count=$((fail_count + 1)) continue fi # Checkout if git_checkout "$full_path" "$branch_name"; then ui_step_done "Checked out:" "$path → $branch_name" - ((success_count++)) + success_count=$((success_count + 1)) else ui_step_error "Failed: $path - $GIT_ERROR" - ((fail_count++)) + fail_count=$((fail_count + 1)) fi done <<< "$repos" diff --git a/lib/commands/clone.sh b/lib/commands/clone.sh index b836351..911ef17 100644 --- a/lib/commands/clone.sh +++ b/lib/commands/clone.sh @@ -51,7 +51,7 @@ cmd_clone() { while IFS= read -r repo; do [[ -z "$repo" ]] && continue - ((total++)) + total=$((total + 1)) local path path=$(yaml_get_path "$repo") @@ -160,10 +160,10 @@ _clone_wait_one() { if [[ $exit_code -eq 0 ]]; then ui_step_done "Cloned:" "$path" - ((success_count++)) + success_count=$((success_count + 1)) else ui_step_error "Failed to clone: $path" - ((fail_count++)) + fail_count=$((fail_count + 1)) failed_repos+=("$repo") fi diff --git a/lib/commands/exec.sh b/lib/commands/exec.sh index d297230..5b50fe6 100644 --- a/lib/commands/exec.sh +++ b/lib/commands/exec.sh @@ -66,7 +66,7 @@ cmd_exec() { if [[ ! -d "$full_path" ]]; then ui_step_done "Skipped (not cloned):" "$path" - ((skip_count++)) + skip_count=$((skip_count + 1)) continue fi @@ -92,10 +92,10 @@ cmd_exec() { if [[ $exit_code -eq 0 ]]; then ui_step_done "Success:" "$path" - ((success_count++)) + success_count=$((success_count + 1)) else ui_step_error "Failed (exit $exit_code): $path" - ((fail_count++)) + fail_count=$((fail_count + 1)) fi ui_bar_line diff --git a/lib/commands/list.sh b/lib/commands/list.sh index bc5ab93..7a6cf4f 100644 --- a/lib/commands/list.sh +++ b/lib/commands/list.sh @@ -46,7 +46,7 @@ cmd_list() { while IFS= read -r repo; do [[ -z "$repo" ]] && continue - ((total++)) + total=$((total + 1)) local path path=$(yaml_get_path "$repo") @@ -57,7 +57,7 @@ cmd_list() { local cloned_text if [[ -d "$full_path" ]]; then cloned_text="$(ui_green "yes")" - ((cloned++)) + cloned=$((cloned + 1)) else cloned_text="$(ui_dim "no")" fi diff --git a/lib/commands/status.sh b/lib/commands/status.sh index 983992f..b506fa8 100644 --- a/lib/commands/status.sh +++ b/lib/commands/status.sh @@ -46,7 +46,7 @@ cmd_status() { if [[ ! -d "$full_path" ]]; then ui_table_row "$path" "$(ui_dim "not cloned")" "-" "-" - ((not_cloned++)) + not_cloned=$((not_cloned + 1)) continue fi diff --git a/lib/commands/sync.sh b/lib/commands/sync.sh index c8c53f1..e298dc3 100644 --- a/lib/commands/sync.sh +++ b/lib/commands/sync.sh @@ -51,7 +51,7 @@ cmd_sync() { if [[ ! -d "$full_path" ]]; then ui_step_done "Skipped (not cloned):" "$path" - ((skip_count++)) + skip_count=$((skip_count + 1)) continue fi @@ -69,7 +69,7 @@ cmd_sync() { else ui_step_done "Updated:" "$path" fi - ((success_count++)) + success_count=$((success_count + 1)) else # Check for conflicts if [[ "$GIT_ERROR" == *"conflict"* ]] || [[ "$GIT_ERROR" == *"CONFLICT"* ]]; then @@ -78,7 +78,7 @@ cmd_sync() { else ui_step_error "Failed: $path" fi - ((fail_count++)) + fail_count=$((fail_count + 1)) fi done <<< "$repos" diff --git a/lib/config.sh b/lib/config.sh index f9dfe59..f9ff016 100644 --- a/lib/config.sh +++ b/lib/config.sh @@ -98,7 +98,7 @@ config_repo_count() { repos=$(config_get_repos "$tag") while IFS= read -r repo; do - [[ -n "$repo" ]] && ((count++)) + [[ -n "$repo" ]] && count=$((count + 1)) done <<< "$repos" printf '%d' "$count" diff --git a/lib/ui.sh b/lib/ui.sh index abee131..f0ee090 100644 --- a/lib/ui.sh +++ b/lib/ui.sh @@ -245,14 +245,14 @@ ui_select() { ((selected > 0)) && ((selected--)) ;; '[B') # Down - ((selected < count - 1)) && ((selected++)) + ((selected < count - 1)) && selected=$((selected + 1)) ;; esac elif [[ "$key" == "" ]]; then # Enter pressed break elif [[ "$key" == "j" ]]; then - ((selected < count - 1)) && ((selected++)) + ((selected < count - 1)) && selected=$((selected + 1)) elif [[ "$key" == "k" ]]; then ((selected > 0)) && ((selected--)) fi diff --git a/mars b/mars index c1dc329..3e40f68 100755 --- a/mars +++ b/mars @@ -4,7 +4,7 @@ set -euo pipefail -MARS_VERSION="0.1.0" +MARS_VERSION="0.1.1" # Determine script directory for sourcing libs MARS_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" diff --git a/package.json b/package.json index bb2a9e4..7882e6c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@dean0x/mars", - "version": "0.1.0", + "version": "0.1.1", "description": "Multi Agentic Repo workspace manager for Git repositories", "bin": { "mars": "./dist/mars"