feat: add Windows PowerShell support and remote install one-liners
- 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>
This commit is contained in:
206
bin/mosaic-doctor.ps1
Normal file
206
bin/mosaic-doctor.ps1
Normal file
@@ -0,0 +1,206 @@
|
||||
# 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
|
||||
}
|
||||
97
bin/mosaic-link-runtime-assets.ps1
Normal file
97
bin/mosaic-link-runtime-assets.ps1
Normal file
@@ -0,0 +1,97 @@
|
||||
# mosaic-link-runtime-assets.ps1
|
||||
# Syncs Mosaic runtime config files into agent runtime directories.
|
||||
# PowerShell equivalent of mosaic-link-runtime-assets (bash).
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$MosaicHome = if ($env:MOSAIC_HOME) { $env:MOSAIC_HOME } else { Join-Path $env:USERPROFILE ".config\mosaic" }
|
||||
$BackupStamp = Get-Date -Format "yyyyMMddHHmmss"
|
||||
|
||||
function Copy-FileManaged {
|
||||
param([string]$Src, [string]$Dst)
|
||||
|
||||
$parent = Split-Path $Dst -Parent
|
||||
if (-not (Test-Path $parent)) { New-Item -ItemType Directory -Path $parent -Force | Out-Null }
|
||||
|
||||
# Remove existing symlink/junction
|
||||
$item = Get-Item $Dst -Force -ErrorAction SilentlyContinue
|
||||
if ($item -and $item.Attributes -band [System.IO.FileAttributes]::ReparsePoint) {
|
||||
Remove-Item $Dst -Force
|
||||
}
|
||||
|
||||
if (Test-Path $Dst) {
|
||||
$srcHash = (Get-FileHash $Src -Algorithm SHA256).Hash
|
||||
$dstHash = (Get-FileHash $Dst -Algorithm SHA256).Hash
|
||||
if ($srcHash -eq $dstHash) { return }
|
||||
Rename-Item $Dst "$Dst.mosaic-bak-$BackupStamp"
|
||||
}
|
||||
|
||||
Copy-Item $Src $Dst -Force
|
||||
}
|
||||
|
||||
function Remove-LegacyPath {
|
||||
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) {
|
||||
Remove-Item $Path -Force
|
||||
return
|
||||
}
|
||||
|
||||
if (Test-Path $Path -PathType Container) {
|
||||
# Remove symlinks/junctions inside, then empty dirs
|
||||
Get-ChildItem $Path -Recurse -Force | Where-Object {
|
||||
$_.Attributes -band [System.IO.FileAttributes]::ReparsePoint
|
||||
} | Remove-Item -Force
|
||||
|
||||
Get-ChildItem $Path -Recurse -Directory -Force |
|
||||
Sort-Object { $_.FullName.Length } -Descending |
|
||||
Where-Object { (Get-ChildItem $_.FullName -Force | Measure-Object).Count -eq 0 } |
|
||||
Remove-Item -Force
|
||||
}
|
||||
}
|
||||
|
||||
# Remove legacy compatibility paths
|
||||
$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\scripts\debug-hook.sh"),
|
||||
(Join-Path $env:USERPROFILE ".claude\scripts\qa-hook-handler.sh"),
|
||||
(Join-Path $env:USERPROFILE ".claude\scripts\qa-hook-stdin.sh"),
|
||||
(Join-Path $env:USERPROFILE ".claude\scripts\qa-hook-wrapper.sh"),
|
||||
(Join-Path $env:USERPROFILE ".claude\scripts\qa-queue-monitor.sh"),
|
||||
(Join-Path $env:USERPROFILE ".claude\scripts\remediation-hook-handler.sh"),
|
||||
(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"),
|
||||
(Join-Path $env:USERPROFILE ".claude\presets\jarvis-ralph.json")
|
||||
)
|
||||
|
||||
foreach ($p in $legacyPaths) {
|
||||
Remove-LegacyPath $p
|
||||
}
|
||||
|
||||
# Sync runtime config files (copy, not link)
|
||||
$runtimeFiles = @("CLAUDE.md", "settings.json", "hooks-config.json", "context7-integration.md")
|
||||
foreach ($rf in $runtimeFiles) {
|
||||
$src = Join-Path $MosaicHome "runtime\claude\$rf"
|
||||
if (-not (Test-Path $src)) { continue }
|
||||
$dst = Join-Path $env:USERPROFILE ".claude\$rf"
|
||||
Copy-FileManaged $src $dst
|
||||
}
|
||||
|
||||
# OpenCode runtime adapter
|
||||
$opencodeSrc = Join-Path $MosaicHome "runtime\opencode\AGENTS.md"
|
||||
if (Test-Path $opencodeSrc) {
|
||||
$opencodeDst = Join-Path $env:USERPROFILE ".config\opencode\AGENTS.md"
|
||||
Copy-FileManaged $opencodeSrc $opencodeDst
|
||||
}
|
||||
|
||||
Write-Host "[mosaic-link] Runtime assets synced (non-symlink mode)"
|
||||
Write-Host "[mosaic-link] Canonical source: $MosaicHome"
|
||||
90
bin/mosaic-migrate-local-skills.ps1
Normal file
90
bin/mosaic-migrate-local-skills.ps1
Normal file
@@ -0,0 +1,90 @@
|
||||
# mosaic-migrate-local-skills.ps1
|
||||
# Migrates runtime-local skill directories to Mosaic-managed junctions.
|
||||
# Uses directory junctions (no elevation required) with fallback to copies.
|
||||
# PowerShell equivalent of mosaic-migrate-local-skills (bash).
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
param(
|
||||
[switch]$Apply,
|
||||
[switch]$Help
|
||||
)
|
||||
|
||||
$MosaicHome = if ($env:MOSAIC_HOME) { $env:MOSAIC_HOME } else { Join-Path $env:USERPROFILE ".config\mosaic" }
|
||||
$LocalSkillsDir = Join-Path $MosaicHome "skills-local"
|
||||
|
||||
if ($Help) {
|
||||
Write-Host @"
|
||||
Usage: mosaic-migrate-local-skills.ps1 [-Apply] [-Help]
|
||||
|
||||
Migrate runtime-local skill directories (e.g. ~/.claude/skills/jarvis) to
|
||||
Mosaic-managed skills by replacing local directories with junctions to
|
||||
~/.config/mosaic/skills-local.
|
||||
|
||||
Default mode is dry-run.
|
||||
"@
|
||||
exit 0
|
||||
}
|
||||
|
||||
if (-not (Test-Path $LocalSkillsDir)) {
|
||||
Write-Host "[mosaic-local-skills] Missing local skills dir: $LocalSkillsDir" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
$skillRoots = @(
|
||||
(Join-Path $env:USERPROFILE ".claude\skills"),
|
||||
(Join-Path $env:USERPROFILE ".codex\skills"),
|
||||
(Join-Path $env:USERPROFILE ".config\opencode\skills")
|
||||
)
|
||||
|
||||
$count = 0
|
||||
|
||||
Get-ChildItem $LocalSkillsDir -Directory | ForEach-Object {
|
||||
$name = $_.Name
|
||||
$src = $_.FullName
|
||||
|
||||
foreach ($root in $skillRoots) {
|
||||
if (-not (Test-Path $root)) { continue }
|
||||
$target = Join-Path $root $name
|
||||
|
||||
# Already a junction/symlink — check if it points to the right place
|
||||
$existing = Get-Item $target -Force -ErrorAction SilentlyContinue
|
||||
if ($existing -and ($existing.Attributes -band [System.IO.FileAttributes]::ReparsePoint)) {
|
||||
$currentTarget = $existing.Target
|
||||
if ($currentTarget -and ($currentTarget -eq $src -or (Resolve-Path $currentTarget -ErrorAction SilentlyContinue).Path -eq (Resolve-Path $src -ErrorAction SilentlyContinue).Path)) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
# Only migrate local directories containing SKILL.md
|
||||
if ((Test-Path $target -PathType Container) -and
|
||||
(Test-Path (Join-Path $target "SKILL.md")) -and
|
||||
-not ($existing -and ($existing.Attributes -band [System.IO.FileAttributes]::ReparsePoint))) {
|
||||
|
||||
$count++
|
||||
if ($Apply) {
|
||||
$stamp = Get-Date -Format "yyyyMMddHHmmss"
|
||||
Rename-Item $target "$target.mosaic-bak-$stamp"
|
||||
try {
|
||||
New-Item -ItemType Junction -Path $target -Target $src -ErrorAction Stop | Out-Null
|
||||
Write-Host "[mosaic-local-skills] migrated: $target -> $src"
|
||||
}
|
||||
catch {
|
||||
Write-Host "[mosaic-local-skills] Junction failed for $name, falling back to copy"
|
||||
Copy-Item $src $target -Recurse -Force
|
||||
Write-Host "[mosaic-local-skills] copied: $target <- $src"
|
||||
}
|
||||
}
|
||||
else {
|
||||
Write-Host "[mosaic-local-skills] would migrate: $target -> $src"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($Apply) {
|
||||
Write-Host "[mosaic-local-skills] complete: migrated=$count"
|
||||
}
|
||||
else {
|
||||
Write-Host "[mosaic-local-skills] dry-run: migratable=$count"
|
||||
Write-Host "[mosaic-local-skills] re-run with -Apply to migrate"
|
||||
}
|
||||
126
bin/mosaic-sync-skills.ps1
Normal file
126
bin/mosaic-sync-skills.ps1
Normal file
@@ -0,0 +1,126 @@
|
||||
# mosaic-sync-skills.ps1
|
||||
# Syncs canonical skills and links them into agent runtime skill directories.
|
||||
# Uses directory junctions (no elevation required) with fallback to copies.
|
||||
# PowerShell equivalent of mosaic-sync-skills (bash).
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
param(
|
||||
[switch]$LinkOnly,
|
||||
[switch]$NoLink,
|
||||
[switch]$Help
|
||||
)
|
||||
|
||||
$MosaicHome = if ($env:MOSAIC_HOME) { $env:MOSAIC_HOME } else { Join-Path $env:USERPROFILE ".config\mosaic" }
|
||||
$SkillsRepoUrl = if ($env:MOSAIC_SKILLS_REPO_URL) { $env:MOSAIC_SKILLS_REPO_URL } else { "https://git.mosaicstack.dev/mosaic/agent-skills.git" }
|
||||
$SkillsRepoDir = if ($env:MOSAIC_SKILLS_REPO_DIR) { $env:MOSAIC_SKILLS_REPO_DIR } else { Join-Path $MosaicHome "sources\agent-skills" }
|
||||
$MosaicSkillsDir = Join-Path $MosaicHome "skills"
|
||||
$MosaicLocalSkillsDir = Join-Path $MosaicHome "skills-local"
|
||||
|
||||
if ($Help) {
|
||||
Write-Host @"
|
||||
Usage: mosaic-sync-skills.ps1 [-LinkOnly] [-NoLink] [-Help]
|
||||
|
||||
Sync canonical skills into ~/.config/mosaic/skills and link all Mosaic skills
|
||||
into runtime skill directories using directory junctions.
|
||||
|
||||
Options:
|
||||
-LinkOnly Skip git clone/pull and only relink
|
||||
-NoLink Sync canonical skills but do not update runtime links
|
||||
-Help Show help
|
||||
"@
|
||||
exit 0
|
||||
}
|
||||
|
||||
foreach ($d in @($MosaicHome, $MosaicSkillsDir, $MosaicLocalSkillsDir)) {
|
||||
if (-not (Test-Path $d)) { New-Item -ItemType Directory -Path $d -Force | Out-Null }
|
||||
}
|
||||
|
||||
# Fetch skills from git
|
||||
if (-not $LinkOnly) {
|
||||
if (Test-Path (Join-Path $SkillsRepoDir ".git")) {
|
||||
Write-Host "[mosaic-skills] Updating skills source: $SkillsRepoDir"
|
||||
git -C $SkillsRepoDir pull --rebase
|
||||
}
|
||||
else {
|
||||
Write-Host "[mosaic-skills] Cloning skills source to: $SkillsRepoDir"
|
||||
$parentDir = Split-Path $SkillsRepoDir -Parent
|
||||
if (-not (Test-Path $parentDir)) { New-Item -ItemType Directory -Path $parentDir -Force | Out-Null }
|
||||
git clone $SkillsRepoUrl $SkillsRepoDir
|
||||
}
|
||||
|
||||
$sourceSkillsDir = Join-Path $SkillsRepoDir "skills"
|
||||
if (-not (Test-Path $sourceSkillsDir)) {
|
||||
Write-Host "[mosaic-skills] Missing source skills dir: $sourceSkillsDir" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Sync: remove old, copy new
|
||||
if (Test-Path $MosaicSkillsDir) {
|
||||
Get-ChildItem $MosaicSkillsDir | Remove-Item -Recurse -Force
|
||||
}
|
||||
Copy-Item "$sourceSkillsDir\*" $MosaicSkillsDir -Recurse -Force
|
||||
}
|
||||
|
||||
if (-not (Test-Path $MosaicSkillsDir)) {
|
||||
Write-Host "[mosaic-skills] Canonical skills dir missing: $MosaicSkillsDir" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
if ($NoLink) {
|
||||
Write-Host "[mosaic-skills] Canonical sync completed (link update skipped)"
|
||||
exit 0
|
||||
}
|
||||
|
||||
function Link-SkillIntoTarget {
|
||||
param([string]$SkillPath, [string]$TargetDir)
|
||||
|
||||
$name = Split-Path $SkillPath -Leaf
|
||||
if ($name.StartsWith(".")) { return }
|
||||
|
||||
$linkPath = Join-Path $TargetDir $name
|
||||
|
||||
# Already a junction/symlink — recreate
|
||||
$existing = Get-Item $linkPath -Force -ErrorAction SilentlyContinue
|
||||
if ($existing -and ($existing.Attributes -band [System.IO.FileAttributes]::ReparsePoint)) {
|
||||
Remove-Item $linkPath -Force
|
||||
}
|
||||
elseif ($existing) {
|
||||
Write-Host "[mosaic-skills] Preserve existing runtime-specific entry: $linkPath"
|
||||
return
|
||||
}
|
||||
|
||||
# Try junction first, fall back to copy
|
||||
try {
|
||||
New-Item -ItemType Junction -Path $linkPath -Target $SkillPath -ErrorAction Stop | Out-Null
|
||||
}
|
||||
catch {
|
||||
Write-Host "[mosaic-skills] Junction failed for $name, falling back to copy"
|
||||
Copy-Item $SkillPath $linkPath -Recurse -Force
|
||||
}
|
||||
}
|
||||
|
||||
$linkTargets = @(
|
||||
(Join-Path $env:USERPROFILE ".claude\skills"),
|
||||
(Join-Path $env:USERPROFILE ".codex\skills"),
|
||||
(Join-Path $env:USERPROFILE ".config\opencode\skills")
|
||||
)
|
||||
|
||||
foreach ($target in $linkTargets) {
|
||||
if (-not (Test-Path $target)) { New-Item -ItemType Directory -Path $target -Force | Out-Null }
|
||||
|
||||
# Link canonical skills
|
||||
Get-ChildItem $MosaicSkillsDir -Directory | ForEach-Object {
|
||||
Link-SkillIntoTarget $_.FullName $target
|
||||
}
|
||||
|
||||
# Link local skills
|
||||
if (Test-Path $MosaicLocalSkillsDir) {
|
||||
Get-ChildItem $MosaicLocalSkillsDir -Directory | ForEach-Object {
|
||||
Link-SkillIntoTarget $_.FullName $target
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host "[mosaic-skills] Linked skills into: $target"
|
||||
}
|
||||
|
||||
Write-Host "[mosaic-skills] Complete"
|
||||
70
install.ps1
Normal file
70
install.ps1
Normal file
@@ -0,0 +1,70 @@
|
||||
# Mosaic Bootstrap — Windows Installer
|
||||
# PowerShell equivalent of install.sh
|
||||
#
|
||||
# Usage:
|
||||
# powershell -ExecutionPolicy Bypass -File install.ps1
|
||||
#
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$SourceDir = $PSScriptRoot
|
||||
$TargetDir = if ($env:MOSAIC_HOME) { $env:MOSAIC_HOME } else { Join-Path $env:USERPROFILE ".config\mosaic" }
|
||||
|
||||
if (-not (Test-Path $TargetDir)) { New-Item -ItemType Directory -Path $TargetDir -Force | Out-Null }
|
||||
|
||||
# Sync files: remove old content, copy new
|
||||
Write-Host "[mosaic-install] Copying framework to $TargetDir..."
|
||||
Get-ChildItem $TargetDir -Exclude ".git" | Remove-Item -Recurse -Force
|
||||
Get-ChildItem $SourceDir -Exclude ".git" | ForEach-Object {
|
||||
$dest = Join-Path $TargetDir $_.Name
|
||||
if ($_.PSIsContainer) {
|
||||
Copy-Item $_.FullName $dest -Recurse -Force
|
||||
}
|
||||
else {
|
||||
Copy-Item $_.FullName $dest -Force
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host "[mosaic-install] Installed framework to $TargetDir"
|
||||
|
||||
# Run post-install scripts (PowerShell versions)
|
||||
$binDir = Join-Path $TargetDir "bin"
|
||||
|
||||
Write-Host "[mosaic-install] Linking runtime compatibility assets"
|
||||
try {
|
||||
& "$binDir\mosaic-link-runtime-assets.ps1"
|
||||
}
|
||||
catch {
|
||||
Write-Host "[mosaic-install] WARNING: runtime asset linking failed (framework install still complete)" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
Write-Host "[mosaic-install] Syncing universal skills"
|
||||
if ($env:MOSAIC_SKIP_SKILLS_SYNC -eq "1") {
|
||||
Write-Host "[mosaic-install] Skipping skills sync (MOSAIC_SKIP_SKILLS_SYNC=1)"
|
||||
}
|
||||
else {
|
||||
try {
|
||||
& "$binDir\mosaic-sync-skills.ps1"
|
||||
}
|
||||
catch {
|
||||
Write-Host "[mosaic-install] WARNING: skills sync failed (framework install still complete)" -ForegroundColor Yellow
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host "[mosaic-install] Migrating runtime-local skills to Mosaic junctions"
|
||||
try {
|
||||
& "$binDir\mosaic-migrate-local-skills.ps1" -Apply
|
||||
}
|
||||
catch {
|
||||
Write-Host "[mosaic-install] WARNING: local skill migration failed (framework install still complete)" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
Write-Host "[mosaic-install] Running health audit"
|
||||
try {
|
||||
& "$binDir\mosaic-doctor.ps1"
|
||||
}
|
||||
catch {
|
||||
Write-Host "[mosaic-install] WARNING: doctor reported issues (run mosaic-doctor.ps1 -FailOnWarn)" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
Write-Host "[mosaic-install] Done."
|
||||
Write-Host "[mosaic-install] Add to PATH: `$env:USERPROFILE\.config\mosaic\bin"
|
||||
37
remote-install.ps1
Normal file
37
remote-install.ps1
Normal file
@@ -0,0 +1,37 @@
|
||||
# Mosaic Bootstrap — Remote Installer (Windows PowerShell)
|
||||
#
|
||||
# One-liner:
|
||||
# irm https://git.mosaicstack.dev/mosaic/bootstrap/raw/branch/main/remote-install.ps1 | iex
|
||||
#
|
||||
# Or explicit:
|
||||
# powershell -ExecutionPolicy Bypass -File remote-install.ps1
|
||||
#
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$ArchiveUrl = "https://git.mosaicstack.dev/mosaic/bootstrap/archive/main.zip"
|
||||
$WorkDir = Join-Path $env:TEMP "mosaic-bootstrap-$PID"
|
||||
$ZipPath = "$WorkDir.zip"
|
||||
|
||||
try {
|
||||
Write-Host "[mosaic] Downloading bootstrap archive..."
|
||||
New-Item -ItemType Directory -Path $WorkDir -Force | Out-Null
|
||||
Invoke-WebRequest -Uri $ArchiveUrl -OutFile $ZipPath -UseBasicParsing
|
||||
|
||||
Write-Host "[mosaic] Extracting..."
|
||||
Expand-Archive -Path $ZipPath -DestinationPath $WorkDir -Force
|
||||
|
||||
$InstallScript = Join-Path $WorkDir "bootstrap\install.ps1"
|
||||
if (-not (Test-Path $InstallScript)) {
|
||||
throw "install.ps1 not found in archive"
|
||||
}
|
||||
|
||||
Write-Host "[mosaic] Running install..."
|
||||
& $InstallScript
|
||||
|
||||
Write-Host "[mosaic] Done."
|
||||
}
|
||||
finally {
|
||||
Write-Host "[mosaic] Cleaning up temporary files..."
|
||||
Remove-Item -Path $ZipPath -Force -ErrorAction SilentlyContinue
|
||||
Remove-Item -Path $WorkDir -Recurse -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
44
remote-install.sh
Executable file
44
remote-install.sh
Executable file
@@ -0,0 +1,44 @@
|
||||
#!/usr/bin/env sh
|
||||
# Mosaic Bootstrap — Remote Installer (POSIX)
|
||||
#
|
||||
# One-liner:
|
||||
# curl -sL https://git.mosaicstack.dev/mosaic/bootstrap/raw/branch/main/remote-install.sh | sh
|
||||
#
|
||||
# Or with wget:
|
||||
# wget -qO- https://git.mosaicstack.dev/mosaic/bootstrap/raw/branch/main/remote-install.sh | sh
|
||||
#
|
||||
set -eu
|
||||
|
||||
ARCHIVE_URL="https://git.mosaicstack.dev/mosaic/bootstrap/archive/main.tar.gz"
|
||||
TMPDIR_BASE="${TMPDIR:-/tmp}"
|
||||
WORK_DIR="$TMPDIR_BASE/mosaic-bootstrap-$$"
|
||||
|
||||
cleanup() {
|
||||
rm -rf "$WORK_DIR"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
echo "[mosaic] Downloading bootstrap archive..."
|
||||
|
||||
mkdir -p "$WORK_DIR"
|
||||
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
curl -sL "$ARCHIVE_URL" | tar xz -C "$WORK_DIR"
|
||||
elif command -v wget >/dev/null 2>&1; then
|
||||
wget -qO- "$ARCHIVE_URL" | tar xz -C "$WORK_DIR"
|
||||
else
|
||||
echo "[mosaic] ERROR: curl or wget required" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -f "$WORK_DIR/bootstrap/install.sh" ]; then
|
||||
echo "[mosaic] ERROR: install.sh not found in archive" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "[mosaic] Running install..."
|
||||
cd "$WORK_DIR/bootstrap"
|
||||
bash install.sh
|
||||
|
||||
echo "[mosaic] Cleaning up temporary files..."
|
||||
# cleanup runs via trap
|
||||
Reference in New Issue
Block a user