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:
Jason Woltje
2026-02-19 11:49:36 -06:00
parent 449e47bedc
commit 7316870c81
7 changed files with 670 additions and 0 deletions

206
bin/mosaic-doctor.ps1 Normal file
View 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
}

View 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"

View 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
View 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
View 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
View 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
View 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