219 lines
5.8 KiB
Bash
Executable File
219 lines
5.8 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}"
|
|
PROJECTS_FILE="$MOSAIC_HOME/projects.txt"
|
|
|
|
usage() {
|
|
cat <<USAGE
|
|
Usage: $(basename "$0") <command> [options]
|
|
|
|
Commands:
|
|
init
|
|
Create projects registry file at ~/.config/mosaic/projects.txt
|
|
|
|
add <repo-path> [repo-path...]
|
|
Add one or more repos to the registry
|
|
|
|
remove <repo-path> [repo-path...]
|
|
Remove one or more repos from the registry
|
|
|
|
list
|
|
Show registered repos
|
|
|
|
bootstrap [--all|repo-path...] [--force] [--quality-template <name>]
|
|
Bootstrap registered repos or explicit repo paths
|
|
|
|
orchestrate <drain|start|status|stop> [--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" <<EOF
|
|
# Mosaic managed projects (one absolute path per line)
|
|
# Lines starting with # are ignored.
|
|
EOF
|
|
fi
|
|
}
|
|
|
|
norm_path() {
|
|
local p="$1"
|
|
if [[ -d "$p" ]]; then
|
|
(cd "$p" && pwd)
|
|
else
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
read_registry() {
|
|
ensure_registry
|
|
grep -vE '^\s*#|^\s*$' "$PROJECTS_FILE" | while read -r p; do
|
|
[[ -d "$p" ]] && echo "$p"
|
|
done
|
|
}
|
|
|
|
add_repo() {
|
|
local p="$1"
|
|
ensure_registry
|
|
local np
|
|
np="$(norm_path "$p")" || { echo "[mosaic-projects] skip missing dir: $p" >&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
|