#!/usr/bin/env bash set -euo pipefail MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.mosaic}" FAIL_ON_WARN=0 VERBOSE=0 usage() { cat <&2 usage >&2 exit 1 ;; esac done warn_count=0 warn() { warn_count=$((warn_count + 1)); echo "[WARN] $*"; } pass() { if [[ $VERBOSE -eq 1 ]]; then echo "[OK] $*" fi return 0 } expect_dir() { local d="$1" if [[ ! -d "$d" ]]; then warn "Missing directory: $d" else pass "Directory present: $d" fi } expect_file() { local f="$1" if [[ ! -f "$f" ]]; then warn "Missing file: $f" else pass "File present: $f" fi } check_runtime_file_copy() { local src="$1" local dst="$2" [[ -f "$src" ]] || return 0 if [[ ! -e "$dst" ]]; then warn "Missing runtime file: $dst" return fi if [[ -L "$dst" ]]; then warn "Runtime file should not be symlinked: $dst" return fi if ! cmp -s "$src" "$dst"; then warn "Runtime file drift: $dst (does not match $src)" else pass "Runtime file synced: $dst" fi } warn_if_symlink_tree_present() { local p="$1" [[ -e "$p" ]] || return 0 if [[ -L "$p" ]]; then warn "Legacy symlink path still present: $p" return fi if [[ -d "$p" ]]; then symlink_count=$(find "$p" -type l 2>/dev/null | wc -l | tr -d ' ') if [[ "$symlink_count" != "0" ]]; then warn "Legacy symlink entries still present under $p: $symlink_count" else pass "No symlinks under legacy path: $p" fi fi } echo "[mosaic-doctor] Mosaic home: $MOSAIC_HOME" # Canonical Mosaic checks expect_file "$MOSAIC_HOME/STANDARDS.md" expect_dir "$MOSAIC_HOME/guides" expect_dir "$MOSAIC_HOME/rails" expect_dir "$MOSAIC_HOME/rails/quality" expect_dir "$MOSAIC_HOME/rails/orchestrator-matrix" expect_dir "$MOSAIC_HOME/profiles" expect_dir "$MOSAIC_HOME/templates/agent" 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-quality-apply" expect_file "$MOSAIC_HOME/bin/mosaic-quality-verify" expect_file "$MOSAIC_HOME/bin/mosaic-orchestrator-run" # Claude runtime file checks (copied, non-symlink). for rf in CLAUDE.md settings.json hooks-config.json context7-integration.md; do check_runtime_file_copy "$MOSAIC_HOME/runtime/claude/$rf" "$HOME/.claude/$rf" done # Legacy migration surfaces should no longer contain symlink trees. legacy_paths=( "$HOME/.claude/agent-guides" "$HOME/.claude/scripts/git" "$HOME/.claude/scripts/codex" "$HOME/.claude/scripts/bootstrap" "$HOME/.claude/scripts/cicd" "$HOME/.claude/scripts/portainer" "$HOME/.claude/templates" "$HOME/.claude/presets/domains" "$HOME/.claude/presets/tech-stacks" "$HOME/.claude/presets/workflows" ) for p in "${legacy_paths[@]}"; do warn_if_symlink_tree_present "$p" done # Skills runtime checks (still symlinked into runtime-specific skills dirs). for runtime_skills in "$HOME/.claude/skills" "$HOME/.codex/skills" "$HOME/.config/opencode/skills"; do [[ -d "$runtime_skills" ]] || continue while IFS= read -r -d '' skill; do name="$(basename "$skill")" [[ "$name" == .* ]] && continue target="$runtime_skills/$name" if [[ ! -e "$target" ]]; then warn "Missing skill link: $target" continue fi if [[ ! -L "$target" ]]; then warn "Non-symlink skill entry: $target" continue fi target_real="$(readlink -f "$target" 2>/dev/null || true)" skill_real="$(readlink -f "$skill" 2>/dev/null || true)" if [[ -z "$target_real" || -z "$skill_real" || "$target_real" != "$skill_real" ]]; then warn "Drifted skill link: $target (expected -> $skill)" else pass "Linked skill: $target" fi done < <(find "$MOSAIC_HOME/skills" "$MOSAIC_HOME/skills-local" -mindepth 1 -maxdepth 1 -type d -print0) done # Broken links only in managed runtime skill dirs. link_roots=( "$HOME/.claude/skills" "$HOME/.codex/skills" "$HOME/.config/opencode/skills" ) existing_link_roots=() for d in "${link_roots[@]}"; do [[ -e "$d" ]] && existing_link_roots+=("$d") done broken_links=0 if [[ ${#existing_link_roots[@]} -gt 0 ]]; then broken_links=$(find "${existing_link_roots[@]}" -xtype l 2>/dev/null | wc -l | tr -d ' ') fi if [[ "$broken_links" != "0" ]]; then warn "Broken skill symlinks detected: $broken_links" fi echo "[mosaic-doctor] warnings=$warn_count" if [[ $FAIL_ON_WARN -eq 1 && $warn_count -gt 0 ]]; then exit 1 fi