From 449e47bedcf67e8ac13e0f1204b01344c0cac316 Mon Sep 17 00:00:00 2001 From: Jason Woltje Date: Tue, 17 Feb 2026 14:50:02 -0600 Subject: [PATCH] add central multi-repo project orchestration command --- README.md | 19 ++++ bin/mosaic-doctor | 1 + bin/mosaic-projects | 218 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 238 insertions(+) create mode 100755 bin/mosaic-projects diff --git a/README.md b/README.md index 38a672b..2b0d407 100644 --- a/README.md +++ b/README.md @@ -104,6 +104,25 @@ Inside any compatible repository: Wrapper commands call local repo scripts under `scripts/agent/`. +## Central Project Control + +Manage multiple repos from one place: + +```bash +~/.config/mosaic/bin/mosaic-projects init +~/.config/mosaic/bin/mosaic-projects add ~/src/syncagent ~/src/uscllc-website +~/.config/mosaic/bin/mosaic-projects list +~/.config/mosaic/bin/mosaic-projects bootstrap --all +``` + +Run orchestration across registered repos: + +```bash +~/.config/mosaic/bin/mosaic-projects orchestrate drain --all --worker-cmd "codex -p" +~/.config/mosaic/bin/mosaic-projects orchestrate start --all --worker-cmd "opencode -p" +~/.config/mosaic/bin/mosaic-projects orchestrate status --all +``` + ## Quality Rails (Generalized) Mosaic includes vendored quality-rails templates and scripts at: diff --git a/bin/mosaic-doctor b/bin/mosaic-doctor index 41beefd..8c7fd18 100755 --- a/bin/mosaic-doctor +++ b/bin/mosaic-doctor @@ -123,6 +123,7 @@ expect_dir "$MOSAIC_HOME/skills" expect_dir "$MOSAIC_HOME/skills-local" expect_file "$MOSAIC_HOME/bin/mosaic-link-runtime-assets" expect_file "$MOSAIC_HOME/bin/mosaic-sync-skills" +expect_file "$MOSAIC_HOME/bin/mosaic-projects" expect_file "$MOSAIC_HOME/bin/mosaic-quality-apply" expect_file "$MOSAIC_HOME/bin/mosaic-quality-verify" expect_file "$MOSAIC_HOME/bin/mosaic-orchestrator-run" diff --git a/bin/mosaic-projects b/bin/mosaic-projects new file mode 100755 index 0000000..1c4f12c --- /dev/null +++ b/bin/mosaic-projects @@ -0,0 +1,218 @@ +#!/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