- remote-install.sh: POSIX one-liner (curl | sh), downloads archive to tmpdir - remote-install.ps1: Windows one-liner (irm | iex), fully native PowerShell - install.ps1: Native Windows installer calling all .ps1 post-install scripts - bin/mosaic-link-runtime-assets.ps1: Syncs runtime config files - bin/mosaic-sync-skills.ps1: Clones skills, links via directory junctions - bin/mosaic-migrate-local-skills.ps1: Migrates local skills to junctions - bin/mosaic-doctor.ps1: Health audit for Windows environments Directory junctions used instead of symlinks (no elevation required). All junction operations fall back to file copy on failure. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
207 lines
7.3 KiB
PowerShell
207 lines
7.3 KiB
PowerShell
# mosaic-doctor.ps1
|
|
# Audits Mosaic runtime state and detects drift across agent runtimes.
|
|
# PowerShell equivalent of mosaic-doctor (bash).
|
|
$ErrorActionPreference = "Stop"
|
|
|
|
param(
|
|
[switch]$FailOnWarn,
|
|
[switch]$Verbose,
|
|
[switch]$Help
|
|
)
|
|
|
|
$MosaicHome = if ($env:MOSAIC_HOME) { $env:MOSAIC_HOME } else { Join-Path $env:USERPROFILE ".config\mosaic" }
|
|
|
|
if ($Help) {
|
|
Write-Host @"
|
|
Usage: mosaic-doctor.ps1 [-FailOnWarn] [-Verbose] [-Help]
|
|
|
|
Audit Mosaic runtime state and detect drift across agent runtimes.
|
|
"@
|
|
exit 0
|
|
}
|
|
|
|
$script:warnCount = 0
|
|
|
|
function Warn {
|
|
param([string]$Message)
|
|
$script:warnCount++
|
|
Write-Host "[WARN] $Message" -ForegroundColor Yellow
|
|
}
|
|
|
|
function Pass {
|
|
param([string]$Message)
|
|
if ($Verbose) { Write-Host "[OK] $Message" -ForegroundColor Green }
|
|
}
|
|
|
|
function Expect-Dir {
|
|
param([string]$Path)
|
|
if (-not (Test-Path $Path -PathType Container)) { Warn "Missing directory: $Path" }
|
|
else { Pass "Directory present: $Path" }
|
|
}
|
|
|
|
function Expect-File {
|
|
param([string]$Path)
|
|
if (-not (Test-Path $Path -PathType Leaf)) { Warn "Missing file: $Path" }
|
|
else { Pass "File present: $Path" }
|
|
}
|
|
|
|
function Check-RuntimeFileCopy {
|
|
param([string]$Src, [string]$Dst)
|
|
|
|
if (-not (Test-Path $Src)) { return }
|
|
|
|
if (-not (Test-Path $Dst)) {
|
|
Warn "Missing runtime file: $Dst"
|
|
return
|
|
}
|
|
|
|
$item = Get-Item $Dst -Force -ErrorAction SilentlyContinue
|
|
if ($item -and ($item.Attributes -band [System.IO.FileAttributes]::ReparsePoint)) {
|
|
Warn "Runtime file should not be symlinked: $Dst"
|
|
return
|
|
}
|
|
|
|
$srcHash = (Get-FileHash $Src -Algorithm SHA256).Hash
|
|
$dstHash = (Get-FileHash $Dst -Algorithm SHA256).Hash
|
|
if ($srcHash -ne $dstHash) {
|
|
Warn "Runtime file drift: $Dst (does not match $Src)"
|
|
}
|
|
else {
|
|
Pass "Runtime file synced: $Dst"
|
|
}
|
|
}
|
|
|
|
function Warn-IfReparsePresent {
|
|
param([string]$Path)
|
|
if (-not (Test-Path $Path)) { return }
|
|
|
|
$item = Get-Item $Path -Force -ErrorAction SilentlyContinue
|
|
if ($item -and ($item.Attributes -band [System.IO.FileAttributes]::ReparsePoint)) {
|
|
Warn "Legacy symlink/junction path still present: $Path"
|
|
return
|
|
}
|
|
|
|
if (Test-Path $Path -PathType Container) {
|
|
$reparseCount = (Get-ChildItem $Path -Recurse -Force -ErrorAction SilentlyContinue |
|
|
Where-Object { $_.Attributes -band [System.IO.FileAttributes]::ReparsePoint } |
|
|
Measure-Object).Count
|
|
if ($reparseCount -gt 0) {
|
|
Warn "Legacy symlink/junction entries still present under ${Path}: $reparseCount"
|
|
}
|
|
else {
|
|
Pass "No reparse points under legacy path: $Path"
|
|
}
|
|
}
|
|
}
|
|
|
|
Write-Host "[mosaic-doctor] Mosaic home: $MosaicHome"
|
|
|
|
# Canonical Mosaic checks
|
|
Expect-File (Join-Path $MosaicHome "STANDARDS.md")
|
|
Expect-Dir (Join-Path $MosaicHome "guides")
|
|
Expect-Dir (Join-Path $MosaicHome "rails")
|
|
Expect-Dir (Join-Path $MosaicHome "rails\quality")
|
|
Expect-Dir (Join-Path $MosaicHome "rails\orchestrator-matrix")
|
|
Expect-Dir (Join-Path $MosaicHome "profiles")
|
|
Expect-Dir (Join-Path $MosaicHome "templates\agent")
|
|
Expect-Dir (Join-Path $MosaicHome "skills")
|
|
Expect-Dir (Join-Path $MosaicHome "skills-local")
|
|
Expect-File (Join-Path $MosaicHome "bin\mosaic-link-runtime-assets")
|
|
Expect-File (Join-Path $MosaicHome "bin\mosaic-sync-skills")
|
|
Expect-File (Join-Path $MosaicHome "bin\mosaic-projects")
|
|
Expect-File (Join-Path $MosaicHome "bin\mosaic-quality-apply")
|
|
Expect-File (Join-Path $MosaicHome "bin\mosaic-quality-verify")
|
|
Expect-File (Join-Path $MosaicHome "bin\mosaic-orchestrator-run")
|
|
Expect-File (Join-Path $MosaicHome "bin\mosaic-orchestrator-sync-tasks")
|
|
Expect-File (Join-Path $MosaicHome "bin\mosaic-orchestrator-drain")
|
|
Expect-File (Join-Path $MosaicHome "bin\mosaic-orchestrator-matrix-publish")
|
|
Expect-File (Join-Path $MosaicHome "bin\mosaic-orchestrator-matrix-consume")
|
|
Expect-File (Join-Path $MosaicHome "bin\mosaic-orchestrator-matrix-cycle")
|
|
Expect-File (Join-Path $MosaicHome "rails\orchestrator-matrix\transport\matrix_transport.py")
|
|
Expect-File (Join-Path $MosaicHome "rails\orchestrator-matrix\controller\tasks_md_sync.py")
|
|
|
|
# Claude runtime file checks
|
|
$runtimeFiles = @("CLAUDE.md", "settings.json", "hooks-config.json", "context7-integration.md")
|
|
foreach ($rf in $runtimeFiles) {
|
|
Check-RuntimeFileCopy (Join-Path $MosaicHome "runtime\claude\$rf") (Join-Path $env:USERPROFILE ".claude\$rf")
|
|
}
|
|
|
|
# OpenCode runtime adapter
|
|
Check-RuntimeFileCopy (Join-Path $MosaicHome "runtime\opencode\AGENTS.md") (Join-Path $env:USERPROFILE ".config\opencode\AGENTS.md")
|
|
|
|
# Legacy migration surfaces
|
|
$legacyPaths = @(
|
|
(Join-Path $env:USERPROFILE ".claude\agent-guides"),
|
|
(Join-Path $env:USERPROFILE ".claude\scripts\git"),
|
|
(Join-Path $env:USERPROFILE ".claude\scripts\codex"),
|
|
(Join-Path $env:USERPROFILE ".claude\scripts\bootstrap"),
|
|
(Join-Path $env:USERPROFILE ".claude\scripts\cicd"),
|
|
(Join-Path $env:USERPROFILE ".claude\scripts\portainer"),
|
|
(Join-Path $env:USERPROFILE ".claude\templates"),
|
|
(Join-Path $env:USERPROFILE ".claude\presets\domains"),
|
|
(Join-Path $env:USERPROFILE ".claude\presets\tech-stacks"),
|
|
(Join-Path $env:USERPROFILE ".claude\presets\workflows")
|
|
)
|
|
foreach ($p in $legacyPaths) {
|
|
Warn-IfReparsePresent $p
|
|
}
|
|
|
|
# Skills runtime checks (junctions or symlinks into runtime-specific dirs)
|
|
$linkTargets = @(
|
|
(Join-Path $env:USERPROFILE ".claude\skills"),
|
|
(Join-Path $env:USERPROFILE ".codex\skills"),
|
|
(Join-Path $env:USERPROFILE ".config\opencode\skills")
|
|
)
|
|
|
|
$skillSources = @($MosaicHome + "\skills", $MosaicHome + "\skills-local")
|
|
|
|
foreach ($runtimeSkills in $linkTargets) {
|
|
if (-not (Test-Path $runtimeSkills)) { continue }
|
|
|
|
foreach ($sourceDir in $skillSources) {
|
|
if (-not (Test-Path $sourceDir)) { continue }
|
|
|
|
Get-ChildItem $sourceDir -Directory | Where-Object { -not $_.Name.StartsWith(".") } | ForEach-Object {
|
|
$name = $_.Name
|
|
$skillPath = $_.FullName
|
|
$target = Join-Path $runtimeSkills $name
|
|
|
|
if (-not (Test-Path $target)) {
|
|
Warn "Missing skill link: $target"
|
|
return
|
|
}
|
|
|
|
$item = Get-Item $target -Force -ErrorAction SilentlyContinue
|
|
if (-not ($item.Attributes -band [System.IO.FileAttributes]::ReparsePoint)) {
|
|
Warn "Non-junction skill entry: $target"
|
|
return
|
|
}
|
|
|
|
$targetResolved = $item.Target
|
|
if (-not $targetResolved -or (Resolve-Path $targetResolved -ErrorAction SilentlyContinue).Path -ne (Resolve-Path $skillPath -ErrorAction SilentlyContinue).Path) {
|
|
Warn "Drifted skill link: $target (expected -> $skillPath)"
|
|
}
|
|
else {
|
|
Pass "Linked skill: $target"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
# Broken junctions/symlinks in managed runtime skill dirs
|
|
$brokenLinks = 0
|
|
foreach ($d in $linkTargets) {
|
|
if (-not (Test-Path $d)) { continue }
|
|
Get-ChildItem $d -Force -ErrorAction SilentlyContinue | Where-Object {
|
|
($_.Attributes -band [System.IO.FileAttributes]::ReparsePoint) -and -not (Test-Path $_.FullName)
|
|
} | ForEach-Object { $brokenLinks++ }
|
|
}
|
|
if ($brokenLinks -gt 0) {
|
|
Warn "Broken skill junctions/symlinks detected: $brokenLinks"
|
|
}
|
|
|
|
Write-Host "[mosaic-doctor] warnings=$($script:warnCount)"
|
|
if ($FailOnWarn -and $script:warnCount -gt 0) {
|
|
exit 1
|
|
}
|