#!/usr/bin/env bash set -euo pipefail MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}" PROJECTS_FILE="$MOSAIC_HOME/projects.txt" usage() { cat < [options] Commands: init Create projects registry file at ~/.config/mosaic/projects.txt add [repo-path...] Add one or more repos to the registry remove [repo-path...] Remove one or more repos from the registry list Show registered repos bootstrap [--all|repo-path...] [--force] [--quality-template ] Bootstrap registered repos or explicit repo paths orchestrate [--all|repo-path...] [--poll-sec N] [--no-sync] [--worker-cmd "cmd"] Run orchestrator actions across repos from one command Examples: mosaic-projects init mosaic-projects add ~/src/syncagent ~/src/inventory-stickers mosaic-projects bootstrap --all mosaic-projects orchestrate drain --all --worker-cmd "codex -p" mosaic-projects orchestrate start ~/src/syncagent --worker-cmd "opencode -p" USAGE } ensure_registry() { mkdir -p "$MOSAIC_HOME" if [[ ! -f "$PROJECTS_FILE" ]]; then cat > "$PROJECTS_FILE" <&2; return 1; } if grep -Fxq "$np" "$PROJECTS_FILE"; then echo "[mosaic-projects] already registered: $np" return 0 fi echo "$np" >> "$PROJECTS_FILE" echo "[mosaic-projects] added: $np" } remove_repo() { local p="$1" ensure_registry local np np="$(norm_path "$p" 2>/dev/null || echo "$p")" tmp="$(mktemp)" grep -vFx "$np" "$PROJECTS_FILE" > "$tmp" || true mv "$tmp" "$PROJECTS_FILE" echo "[mosaic-projects] removed: $np" } resolve_targets() { local use_all="$1" shift if [[ "$use_all" == "1" ]]; then read_registry return 0 fi if [[ $# -gt 0 ]]; then for p in "$@"; do norm_path "$p" || { echo "[mosaic-projects] missing target: $p" >&2; exit 1; } done return 0 fi if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then git rev-parse --show-toplevel return 0 fi echo "[mosaic-projects] no targets provided. Use --all or pass repo paths." >&2 exit 1 } cmd="${1:-}" if [[ -z "$cmd" ]]; then usage exit 1 fi shift || true case "$cmd" in init) ensure_registry echo "[mosaic-projects] registry ready: $PROJECTS_FILE" ;; add) [[ $# -gt 0 ]] || { echo "[mosaic-projects] add requires repo path(s)" >&2; exit 1; } for p in "$@"; do add_repo "$p"; done ;; remove) [[ $# -gt 0 ]] || { echo "[mosaic-projects] remove requires repo path(s)" >&2; exit 1; } for p in "$@"; do remove_repo "$p"; done ;; list) read_registry ;; bootstrap) use_all=0 force=0 quality_template="" targets=() while [[ $# -gt 0 ]]; do case "$1" in --all) use_all=1; shift ;; --force) force=1; shift ;; --quality-template) quality_template="${2:-}"; shift 2 ;; *) targets+=("$1"); shift ;; esac done mapfile -t repos < <(resolve_targets "$use_all" "${targets[@]}") [[ ${#repos[@]} -gt 0 ]] || { echo "[mosaic-projects] no repos resolved"; exit 1; } for repo in "${repos[@]}"; do args=() [[ $force -eq 1 ]] && args+=(--force) [[ -n "$quality_template" ]] && args+=(--quality-template "$quality_template") args+=("$repo") echo "[mosaic-projects] bootstrap: $repo" "$MOSAIC_HOME/bin/mosaic-bootstrap-repo" "${args[@]}" add_repo "$repo" || true done ;; orchestrate) action="${1:-}" [[ -n "$action" ]] || { echo "[mosaic-projects] orchestrate requires action: drain|start|status|stop" >&2; exit 1; } shift || true use_all=0 poll_sec=15 no_sync=0 worker_cmd="" targets=() while [[ $# -gt 0 ]]; do case "$1" in --all) use_all=1; shift ;; --poll-sec) poll_sec="${2:-15}"; shift 2 ;; --no-sync) no_sync=1; shift ;; --worker-cmd) worker_cmd="${2:-}"; shift 2 ;; *) targets+=("$1"); shift ;; esac done mapfile -t repos < <(resolve_targets "$use_all" "${targets[@]}") [[ ${#repos[@]} -gt 0 ]] || { echo "[mosaic-projects] no repos resolved"; exit 1; } for repo in "${repos[@]}"; do echo "[mosaic-projects] orchestrate:$action -> $repo" ( cd "$repo" if [[ -n "$worker_cmd" ]]; then export MOSAIC_WORKER_EXEC="$worker_cmd" fi if [[ -x "scripts/agent/orchestrator-daemon.sh" ]]; then args=() [[ "$action" == "start" || "$action" == "drain" ]] && args+=(--poll-sec "$poll_sec") [[ $no_sync -eq 1 ]] && args+=(--no-sync) bash scripts/agent/orchestrator-daemon.sh "$action" "${args[@]}" else case "$action" in drain) args=(--poll-sec "$poll_sec") [[ $no_sync -eq 1 ]] && args+=(--no-sync) "$MOSAIC_HOME/bin/mosaic-orchestrator-drain" "${args[@]}" ;; status) echo "[mosaic-projects] no daemon script in repo; run from bootstrapped repo or re-bootstrap" ;; start|stop) echo "[mosaic-projects] action '$action' requires scripts/agent/orchestrator-daemon.sh (run bootstrap first)" >&2 exit 1 ;; *) echo "[mosaic-projects] unsupported action: $action" >&2 exit 1 ;; esac fi ) done ;; *) usage exit 1 ;; esac