#!/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 } 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_tree_links() { local src_root="$1" local dst_root="$2" [[ -d "$src_root" ]] || return while IFS= read -r -d '' src; do local rel dst rel="${src#$src_root/}" dst="$dst_root/$rel" if [[ ! -L "$dst" ]]; then warn "Not symlinked: $dst (expected -> $src)" continue fi local dst_real src_real dst_real="$(readlink -f "$dst" 2>/dev/null || true)" src_real="$(readlink -f "$src" 2>/dev/null || true)" if [[ -z "$dst_real" || -z "$src_real" || "$dst_real" != "$src_real" ]]; then warn "Drifted link: $dst (expected -> $src)" else pass "Linked: $dst" fi done < <(find "$src_root" -type f -print0) } 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/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" # Claude runtime checks check_tree_links "$MOSAIC_HOME/guides" "$HOME/.claude/agent-guides" check_tree_links "$MOSAIC_HOME/rails/git" "$HOME/.claude/scripts/git" check_tree_links "$MOSAIC_HOME/rails/codex" "$HOME/.claude/scripts/codex" check_tree_links "$MOSAIC_HOME/rails/bootstrap" "$HOME/.claude/scripts/bootstrap" check_tree_links "$MOSAIC_HOME/rails/cicd" "$HOME/.claude/scripts/cicd" check_tree_links "$MOSAIC_HOME/rails/portainer" "$HOME/.claude/scripts/portainer" check_tree_links "$MOSAIC_HOME/templates/agent" "$HOME/.claude/templates" check_tree_links "$MOSAIC_HOME/profiles/domains" "$HOME/.claude/presets/domains" check_tree_links "$MOSAIC_HOME/profiles/tech-stacks" "$HOME/.claude/presets/tech-stacks" check_tree_links "$MOSAIC_HOME/profiles/workflows" "$HOME/.claude/presets/workflows" check_tree_links "$MOSAIC_HOME/runtime/claude/settings-overlays" "$HOME/.claude/presets" for rf in CLAUDE.md settings.json hooks-config.json context7-integration.md; do src="$MOSAIC_HOME/runtime/claude/$rf" dst="$HOME/.claude/$rf" [[ -f "$src" ]] || continue if [[ ! -L "$dst" ]]; then warn "Not symlinked: $dst (expected -> $src)" continue fi dst_real="$(readlink -f "$dst" 2>/dev/null || true)" src_real="$(readlink -f "$src" 2>/dev/null || true)" if [[ -z "$dst_real" || -z "$src_real" || "$dst_real" != "$src_real" ]]; then warn "Drifted link: $dst (expected -> $src)" else pass "Linked: $dst" fi done # Skills runtime checks 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 # Runtime-specific local skills are allowed only for hidden/system entries. 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 link_roots=( "$HOME/.claude/agent-guides" "$HOME/.claude/scripts" "$HOME/.claude/templates" "$HOME/.claude/presets" "$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 symlinks detected across runtimes: $broken_links" fi echo "[mosaic-doctor] warnings=$warn_count" if [[ $FAIL_ON_WARN -eq 1 && $warn_count -gt 0 ]]; then exit 1 fi