← Files ConductorARCHIVED FILE
install.ps1
39.4 KB · Oct 3, 2026 · 06:32 UTC
# ─────────────────────────────────────────────────────────────────────────────
# Conductor Skills Installer (PowerShell)
# Installs Conductor workflow orchestration skills for your AI coding agent.
# https://github.com/conductor-oss/conductor-skills
# ─────────────────────────────────────────────────────────────────────────────
param(
[string]$Agent,
[string]$ProjectDir = ".",
[switch]$Global,
[switch]$All,
[switch]$Upgrade,
[switch]$Check,
[switch]$Force,
[switch]$Uninstall,
[switch]$Version
)
$ErrorActionPreference = "Stop"
$SCRIPT_VERSION = "1.6.6"
# Per-file fetches and the upgrade-check both read from `main`. Releases are
# rolled by bumping VERSION on main, not by tagging.
$REPO_BASE = "https://raw.githubusercontent.com/conductor-oss/conductor-skills/main"
# When set, skip the network fetch and copy from this directory instead.
# The npm package sets this so the bundled files are used.
$LOCAL_DIR = $env:CONDUCTOR_SKILLS_LOCAL_DIR
$SKILL_FILES = @(
"skills/conductor/SKILL.md"
"skills/conductor/references/setup.md"
"skills/conductor/references/cli-index.md"
"skills/conductor/references/fallback-cli.md"
"skills/conductor/references/workflow-definition.md"
"skills/conductor/references/workers.md"
"skills/conductor/references/api-reference.md"
"skills/conductor/references/visualization.md"
"skills/conductor/references/schedules.md"
"skills/conductor/references/orkes.md"
"skills/conductor/references/optimization.md"
"skills/conductor/references/troubleshooting.md"
"skills/conductor/examples/create-and-run-workflow.md"
"skills/conductor/examples/monitor-and-retry.md"
"skills/conductor/examples/signal-wait-task.md"
"skills/conductor/examples/fork-join.md"
"skills/conductor/examples/do-while-loop.md"
"skills/conductor/examples/sub-workflow.md"
"skills/conductor/examples/review-workflow.md"
"skills/conductor/examples/llm-chat.md"
"skills/conductor/examples/ai-agent-mcp.md"
"skills/conductor/examples/ai-agent-loop.md"
"skills/conductor/examples/llm-rag.md"
"skills/conductor/examples/workflows/weather-notification.json"
"skills/conductor/examples/workflows/fork-join.json"
"skills/conductor/examples/workflows/do-while-loop.json"
"skills/conductor/examples/workflows/child-normalize.json"
"skills/conductor/examples/workflows/parent-pipeline.json"
"skills/conductor/examples/workflows/llm-chat.json"
"skills/conductor/examples/workflows/ai-agent-mcp.json"
"skills/conductor/examples/workflows/ai-agent-loop.json"
"skills/conductor/examples/workflows/llm-rag.json"
"skills/conductor/scripts/conductor_api.py"
)
$VALID_AGENTS = @("claude","codex","gemini","cursor","windsurf","cline","aider","copilot","amazonq","opencode","roo","amp")
$GLOBAL_AGENTS = @("claude","codex","gemini","cursor","windsurf","roo","amp","aider","opencode")
# ─────────────────────────────────────────────────────────────────────────────
# Helpers
# ─────────────────────────────────────────────────────────────────────────────
function Write-Info { param([string]$Msg) Write-Host "[info] $Msg" -ForegroundColor Blue }
function Write-Ok { param([string]$Msg) Write-Host "[ok] $Msg" -ForegroundColor Green }
function Write-Warn { param([string]$Msg) Write-Host "[warn] $Msg" -ForegroundColor Yellow }
function Write-Err { param([string]$Msg) Write-Host "[error] $Msg" -ForegroundColor Red }
# ─────────────────────────────────────────────────────────────────────────────
# Agent detection
# ─────────────────────────────────────────────────────────────────────────────
function Detect-Agents {
$detected = @()
$installDir = $env:USERPROFILE
if ((Get-Command claude -ErrorAction SilentlyContinue) -or (Test-Path (Join-Path $installDir ".claude"))) { $detected += "claude" }
if ((Get-Command codex -ErrorAction SilentlyContinue) -or (Test-Path (Join-Path $installDir ".codex"))) { $detected += "codex" }
if ((Get-Command gemini -ErrorAction SilentlyContinue) -or (Test-Path (Join-Path $installDir ".gemini"))) { $detected += "gemini" }
if (Test-Path (Join-Path $installDir ".cursor")) { $detected += "cursor" }
if (Test-Path (Join-Path $installDir ".codeium")) { $detected += "windsurf" }
if (Test-Path (Join-Path $installDir ".cline")) { $detected += "cline" }
if (Get-Command aider -ErrorAction SilentlyContinue) { $detected += "aider" }
if (Test-Path (Join-Path $installDir ".config\github-copilot")) { $detected += "copilot" }
if ((Get-Command q -ErrorAction SilentlyContinue) -or (Test-Path (Join-Path $installDir ".amazonq"))) { $detected += "amazonq" }
if (Get-Command opencode -ErrorAction SilentlyContinue) { $detected += "opencode" }
if (Test-Path (Join-Path $installDir ".roo")) { $detected += "roo" }
if ((Get-Command amp -ErrorAction SilentlyContinue) -or (Test-Path (Join-Path $installDir ".config\amp"))) { $detected += "amp" }
return $detected
}
# ─────────────────────────────────────────────────────────────────────────────
# Manifest tracking
# ─────────────────────────────────────────────────────────────────────────────
$GLOBAL_MANIFEST = Join-Path $env:USERPROFILE ".conductor-skills\manifest.json"
function Get-ManifestPath {
param([bool]$IsGlobal, [string]$ProjDir = ".")
if ($IsGlobal) { return $GLOBAL_MANIFEST }
return Join-Path $ProjDir ".conductor-skills\manifest.json"
}
function Ensure-Manifest {
param([string]$ManifestPath)
$dir = Split-Path -Parent $ManifestPath
if ($dir -and !(Test-Path $dir)) { New-Item -ItemType Directory -Path $dir -Force | Out-Null }
if (!(Test-Path $ManifestPath)) {
'{"schema_version":1,"installations":{}}' | Set-Content -Path $ManifestPath
}
}
function Read-ManifestVersion {
param([string]$ManifestPath, [string]$AgentName)
if (!(Test-Path $ManifestPath)) { return "" }
try {
$m = Get-Content $ManifestPath -Raw | ConvertFrom-Json
$entry = $m.installations.PSObject.Properties | Where-Object { $_.Name -eq $AgentName }
if ($entry) { return $entry.Value.version }
} catch {}
return ""
}
function Write-ManifestEntry {
param([string]$ManifestPath, [string]$AgentName, [string]$Ver, [string]$Mode, [string]$TargetPath)
Ensure-Manifest -ManifestPath $ManifestPath
$m = Get-Content $ManifestPath -Raw | ConvertFrom-Json
$now = (Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ")
$existingEntry = $m.installations.PSObject.Properties | Where-Object { $_.Name -eq $AgentName }
$installedAt = $now
if ($existingEntry) { $installedAt = $existingEntry.Value.installed_at }
$entry = [PSCustomObject]@{
version = $Ver
installed_at = $installedAt
updated_at = $now
mode = $Mode
target_path = $TargetPath
}
if ($existingEntry) {
$m.installations.PSObject.Properties.Remove($AgentName)
}
$m.installations | Add-Member -NotePropertyName $AgentName -NotePropertyValue $entry
$m | ConvertTo-Json -Depth 10 | Set-Content -Path $ManifestPath
}
function Remove-ManifestEntry {
param([string]$ManifestPath, [string]$AgentName)
if (!(Test-Path $ManifestPath)) { return }
try {
$m = Get-Content $ManifestPath -Raw | ConvertFrom-Json
$m.installations.PSObject.Properties.Remove($AgentName)
$m | ConvertTo-Json -Depth 10 | Set-Content -Path $ManifestPath
} catch {}
}
function Get-ManifestAgents {
param([string]$ManifestPath)
if (!(Test-Path $ManifestPath)) { return @() }
try {
$m = Get-Content $ManifestPath -Raw | ConvertFrom-Json
return @($m.installations.PSObject.Properties.Name)
} catch { return @() }
}
# ─────────────────────────────────────────────────────────────────────────────
# Remote version check
# ─────────────────────────────────────────────────────────────────────────────
function Fetch-RemoteVersion {
if ($LOCAL_DIR -and (Test-Path (Join-Path $LOCAL_DIR "VERSION"))) {
return (Get-Content (Join-Path $LOCAL_DIR "VERSION") -Raw).Trim()
}
try {
$ver = (Invoke-WebRequest -Uri "$REPO_BASE/VERSION" -UseBasicParsing -ErrorAction Stop).Content.Trim()
return $ver
} catch { return "" }
}
# ─────────────────────────────────────────────────────────────────────────────
# Download & assembly
# ─────────────────────────────────────────────────────────────────────────────
function Download-Files {
param([string]$TmpDir)
if ($LOCAL_DIR) {
if (-not (Test-Path -PathType Container $LOCAL_DIR)) {
Write-Err "CONDUCTOR_SKILLS_LOCAL_DIR=$LOCAL_DIR is not a directory"
exit 1
}
Write-Info "Copying skill files from $LOCAL_DIR..."
foreach ($file in $SKILL_FILES) {
$src = Join-Path $LOCAL_DIR $file
$dest = Join-Path $TmpDir $file
if (-not (Test-Path $src)) {
Write-Err "Missing file in local source: $file"
Remove-Item -Recurse -Force $TmpDir -ErrorAction SilentlyContinue
exit 1
}
$dir = Split-Path -Parent $dest
if ($dir -and !(Test-Path $dir)) {
New-Item -ItemType Directory -Path $dir -Force | Out-Null
}
Copy-Item $src $dest
}
Write-Ok "Copied $($SKILL_FILES.Count) files"
return
}
Write-Info "Downloading skill files..."
foreach ($file in $SKILL_FILES) {
$dir = Split-Path -Parent (Join-Path $TmpDir $file)
if ($dir -and !(Test-Path $dir)) {
New-Item -ItemType Directory -Path $dir -Force | Out-Null
}
$url = "$REPO_BASE/$file"
$dest = Join-Path $TmpDir $file
try {
Invoke-WebRequest -Uri $url -OutFile $dest -UseBasicParsing -ErrorAction Stop | Out-Null
} catch {
Write-Err "Failed to download $file"
Write-Err "Check your internet connection and try again."
Remove-Item -Recurse -Force $TmpDir -ErrorAction SilentlyContinue
exit 1
}
}
Write-Ok "Downloaded $($SKILL_FILES.Count) files"
}
function Assemble-Content {
param([string]$TmpDir, [string]$Output)
$content = @()
$content += Get-Content (Join-Path $TmpDir "skills/conductor/SKILL.md") -Raw
$content += "`n---`n"
$content += "# References`n"
foreach ($f in Get-ChildItem (Join-Path $TmpDir "skills/conductor/references") -Filter "*.md") {
$content += Get-Content $f.FullName -Raw
$content += "`n---`n"
}
$content += "# Examples`n"
foreach ($f in Get-ChildItem (Join-Path $TmpDir "skills/conductor/examples") -Filter "*.md") {
$content += Get-Content $f.FullName -Raw
$content += "`n---`n"
}
$content -join "`n" | Set-Content -Path $Output -NoNewline
}
# ─────────────────────────────────────────────────────────────────────────────
# Safe file writing
# ─────────────────────────────────────────────────────────────────────────────
function Safe-Write {
param([string]$Target, [string]$Source, [bool]$ForceWrite)
if ((Test-Path $Target) -and !$ForceWrite) {
Write-Warn "File already exists: $Target"
$answer = Read-Host " Overwrite? [y/N]"
if ($answer -notmatch "^[Yy]") {
Write-Info "Skipped. Use -Force to overwrite."
return $false
}
}
$dir = Split-Path -Parent $Target
if ($dir -and !(Test-Path $dir)) {
New-Item -ItemType Directory -Path $dir -Force | Out-Null
}
Copy-Item -Path $Source -Destination $Target -Force
Write-Ok "Installed: $Target"
return $true
}
# ─────────────────────────────────────────────────────────────────────────────
# Global install paths
# ─────────────────────────────────────────────────────────────────────────────
function Supports-Global {
param([string]$AgentName)
return $GLOBAL_AGENTS -contains $AgentName
}
function Get-GlobalPath {
param([string]$AgentName)
$installDir = $env:USERPROFILE
switch ($AgentName) {
"claude" { return Join-Path $installDir ".claude\settings.json" }
"codex" { $codexHome = if ($env:CODEX_HOME) { $env:CODEX_HOME } else { Join-Path $installDir ".codex" }; return Join-Path $codexHome "AGENTS.md" }
"cursor" { return Join-Path $installDir ".cursor\skills\conductor\SKILL.md" }
"gemini" { return Join-Path $installDir ".gemini\GEMINI.md" }
"windsurf" { return Join-Path $installDir ".codeium\windsurf\memories\global_rules.md" }
"roo" { return Join-Path $installDir ".roo\rules\conductor.md" }
"amp" { return Join-Path $installDir ".config\AGENTS.md" }
"aider" { return Join-Path $installDir ".aider.conf.yml" }
"opencode" { return Join-Path $installDir ".config\opencode\skills\conductor\SKILL.md" }
}
}
function Get-TargetPath {
param([string]$AgentName, [string]$ProjDir)
switch ($AgentName) {
"claude" { return Join-Path $ProjDir ".claude\settings.json" }
"codex" { return Join-Path $ProjDir "AGENTS.md" }
"gemini" { return Join-Path $ProjDir "GEMINI.md" }
"cursor" { return Join-Path $ProjDir ".cursor\rules\conductor.mdc" }
"windsurf" { return Join-Path $ProjDir ".windsurfrules" }
"cline" { return Join-Path $ProjDir ".clinerules" }
"aider" { return Join-Path $ProjDir ".conductor-skills" }
"copilot" { return Join-Path $ProjDir ".github\copilot-instructions.md" }
"amazonq" { return Join-Path $ProjDir ".amazonq\rules\conductor.md" }
"opencode" { return Join-Path $ProjDir "AGENTS.md" }
"roo" { return Join-Path $ProjDir ".roo\rules\conductor.md" }
"amp" { return Join-Path $ProjDir ".amp\instructions.md" }
}
}
# ─────────────────────────────────────────────────────────────────────────────
# Per-agent install logic
# ─────────────────────────────────────────────────────────────────────────────
# Bust caches so existing Claude installs actually pick up new content.
# Without this, a user with an old plugin cache will see a silent no-op:
# settings.json keys are already present, the old version cache keeps
# loading, and nothing changes. Always called on install and uninstall.
function Clear-ClaudeLegacyAndCaches {
# We do NOT delete ~/.claude/skills/conductor — that's the user-skill
# install location, populated by Install-ClaudeSkill. Re-installs
# *overwrite* it instead.
$cache = Join-Path $env:USERPROFILE ".claude\plugins\cache\conductor-skills"
$market = Join-Path $env:USERPROFILE ".claude\plugins\marketplaces\conductor-skills"
$registry = Join-Path $env:USERPROFILE ".claude\plugins\installed_plugins.json"
if (Test-Path $cache) {
Remove-Item -Recurse -Force $cache
Write-Ok "Cleared plugin cache: $cache (will re-fetch on next session)"
}
if (Test-Path $market) {
Remove-Item -Recurse -Force $market
Write-Ok "Cleared marketplace clone: $market (will re-clone on next session)"
}
# Surgically remove our entry from Claude's plugin registry — a stale
# "installed at v1.0.0" record pointing to a deleted cache dir would
# make Claude Code skip re-fetching on next session start.
if (Test-Path $registry) {
try {
$raw = Get-Content $registry -Raw
$r = $raw | ConvertFrom-Json
if ($r.plugins.PSObject.Properties.Name -contains 'conductor@conductor-skills') {
$r.plugins.PSObject.Properties.Remove('conductor@conductor-skills')
($r | ConvertTo-Json -Depth 10) | Set-Content -Path $registry -Encoding UTF8
Write-Ok "Cleared stale registry entry in $registry"
}
} catch {
# Non-fatal: registry could be missing keys or in a format we don't recognize.
}
}
}
function Install-ClaudeSkill {
param([string]$TmpDir)
$skillDest = Join-Path $env:USERPROFILE ".claude\skills\conductor"
if ($LOCAL_DIR) {
$srcDir = Join-Path $LOCAL_DIR "skills\conductor"
} else {
$srcDir = Join-Path $TmpDir "skills\conductor"
}
if (-not (Test-Path $srcDir)) {
Write-Err "Skill source not found at $srcDir"
return $false
}
# Mirror — replace, don't merge
if (Test-Path $skillDest) {
Remove-Item -Recurse -Force $skillDest
}
New-Item -ItemType Directory -Path $skillDest -Force | Out-Null
Copy-Item -Recurse -Force "$srcDir\*" $skillDest
Write-Ok "Installed skill files: $skillDest"
return $true
}
function Install-Claude {
param([bool]$IsGlobal, [string]$ProjDir, [string]$TmpDir)
# Claude Code reads `extraKnownMarketplaces` and `enabledPlugins` from
# settings.json on session start — writing both there is equivalent to
# running the in-session `/plugin marketplace add` + `/plugin install`
# commands. There is no `claude plugin install` subcommand.
if ($IsGlobal) {
$settingsPath = Join-Path $env:USERPROFILE ".claude\settings.json"
} else {
$settingsPath = Join-Path $ProjDir ".claude\settings.json"
}
$settingsDir = Split-Path -Parent $settingsPath
New-Item -ItemType Directory -Path $settingsDir -Force | Out-Null
Clear-ClaudeLegacyAndCaches
Install-ClaudeSkill -TmpDir $TmpDir | Out-Null
Write-Info "Enabling Conductor plugin in $settingsPath ..."
if (Test-Path $settingsPath) {
try {
$raw = Get-Content $settingsPath -Raw
if ([string]::IsNullOrWhiteSpace($raw)) {
$settings = [PSCustomObject]@{}
} else {
$settings = $raw | ConvertFrom-Json
}
} catch {
Write-Err "Failed to parse $settingsPath as JSON: $_"
return $false
}
} else {
$settings = [PSCustomObject]@{}
}
$marketplaceEntry = [PSCustomObject]@{
source = [PSCustomObject]@{
source = 'github'
repo = 'conductor-oss/conductor-skills'
}
}
if (-not ($settings.PSObject.Properties.Name -contains 'extraKnownMarketplaces')) {
$settings | Add-Member -NotePropertyName 'extraKnownMarketplaces' -NotePropertyValue ([PSCustomObject]@{})
}
if ($settings.extraKnownMarketplaces.PSObject.Properties.Name -contains 'conductor-skills') {
$settings.extraKnownMarketplaces.PSObject.Properties.Remove('conductor-skills')
}
$settings.extraKnownMarketplaces | Add-Member -NotePropertyName 'conductor-skills' -NotePropertyValue $marketplaceEntry
if (-not ($settings.PSObject.Properties.Name -contains 'enabledPlugins')) {
$settings | Add-Member -NotePropertyName 'enabledPlugins' -NotePropertyValue ([PSCustomObject]@{})
}
if ($settings.enabledPlugins.PSObject.Properties.Name -contains 'conductor@conductor-skills') {
$settings.enabledPlugins.PSObject.Properties.Remove('conductor@conductor-skills')
}
$settings.enabledPlugins | Add-Member -NotePropertyName 'conductor@conductor-skills' -NotePropertyValue $true
try {
($settings | ConvertTo-Json -Depth 10) | Set-Content -Path $settingsPath -Encoding UTF8
} catch {
Write-Err "Failed to write $settingsPath : $_"
return $false
}
Write-Ok "Configured: $settingsPath"
Write-Host ""
Write-Host "Conductor installed two ways:" -ForegroundColor Green
Write-Host ""
Write-Host " 1. Skill at ~\.claude\skills\conductor\"
Write-Host " Files are there NOW. Skill auto-loads on next session start."
Write-Host ""
Write-Host " 2. Plugin via settings.json (extraKnownMarketplaces + enabledPlugins)"
Write-Host " Claude Code fetches plugin v$SCRIPT_VERSION on next session start."
Write-Host " Provides slash commands: /conductor, /conductor-setup, /conductor-optimize, /conductor-scaffold-worker"
Write-Host ""
Write-Host "⚠ Restart Claude Code to activate." -ForegroundColor Yellow
Write-Host " * claude CLI: exit (Ctrl+D) and start a new session"
Write-Host " * VS Code extension: Ctrl+Shift+P -> ""Reload Window"""
Write-Host " * Desktop app: quit and reopen"
return $true
}
function Install-ToFile {
param([string]$Target, [string]$Assembled, [bool]$ForceWrite, [string]$Prefix = "")
if ($Prefix) {
$tmpFile = [System.IO.Path]::GetTempFileName()
($Prefix + (Get-Content $Assembled -Raw)) | Set-Content -Path $tmpFile -NoNewline
$result = Safe-Write -Target $Target -Source $tmpFile -ForceWrite $ForceWrite
Remove-Item $tmpFile -ErrorAction SilentlyContinue
return $result
} else {
return Safe-Write -Target $Target -Source $Assembled -ForceWrite $ForceWrite
}
}
function Install-AiderToDir {
param([string]$SkillDir, [string]$TmpDir, [string]$Config, [string]$ReadPrefix)
New-Item -ItemType Directory -Path (Join-Path $SkillDir "references") -Force | Out-Null
New-Item -ItemType Directory -Path (Join-Path $SkillDir "examples") -Force | Out-Null
New-Item -ItemType Directory -Path (Join-Path $SkillDir "scripts") -Force | Out-Null
Write-Info "Copying skill files to $SkillDir ..."
Copy-Item (Join-Path $TmpDir "skills/conductor/SKILL.md") $SkillDir -Force
foreach ($f in Get-ChildItem (Join-Path $TmpDir "skills/conductor/references") -Filter "*.md") {
Copy-Item $f.FullName (Join-Path $SkillDir "references") -Force
}
foreach ($f in Get-ChildItem (Join-Path $TmpDir "skills/conductor/examples") -Filter "*.md") {
Copy-Item $f.FullName (Join-Path $SkillDir "examples") -Force
}
$scriptsDir = Join-Path $TmpDir "skills/conductor/scripts"
if (Test-Path $scriptsDir) {
foreach ($f in Get-ChildItem $scriptsDir -Filter "*.py") {
Copy-Item $f.FullName (Join-Path $SkillDir "scripts") -Force
}
}
Write-Ok "Files copied to $SkillDir"
if ((Test-Path $Config) -and (Select-String -Path $Config -Pattern "conductor-skills" -Quiet)) {
Write-Info "Aider config already references conductor-skills, skipping."
} else {
Write-Info "Adding read entries to $Config ..."
$entries = @("", "# Conductor Skills", "read:")
foreach ($file in $SKILL_FILES) {
$relative = $file -replace "^skills/conductor/", ""
$entries += " - ${ReadPrefix}${relative}"
}
$entries -join "`n" | Add-Content -Path $Config
Write-Ok "Updated: $Config"
}
}
function Install-ForAgent {
param([string]$AgentName, [string]$ProjDir, [bool]$IsGlobal, [bool]$ForceWrite, [string]$TmpDir, [string]$Assembled)
if ($AgentName -eq "claude") {
return Install-Claude -IsGlobal $IsGlobal -ProjDir $ProjDir -TmpDir $TmpDir
}
if ($IsGlobal) {
$installDir = $env:USERPROFILE
if ($AgentName -eq "aider") {
Install-AiderToDir -SkillDir (Join-Path $installDir ".conductor-skills") -TmpDir $TmpDir -Config (Join-Path $installDir ".aider.conf.yml") -ReadPrefix "$installDir/.conductor-skills/"
return $true
} else {
$targetPath = Get-GlobalPath -AgentName $AgentName
return Install-ToFile -Target $targetPath -Assembled $Assembled -ForceWrite $ForceWrite
}
} else {
switch ($AgentName) {
"codex" { return Install-ToFile -Target (Join-Path $ProjDir "AGENTS.md") -Assembled $Assembled -ForceWrite $ForceWrite }
"gemini" { return Install-ToFile -Target (Join-Path $ProjDir "GEMINI.md") -Assembled $Assembled -ForceWrite $ForceWrite }
"cursor" {
$frontmatter = "---`ndescription: Conductor workflow orchestration - create, run, monitor, and manage workflows`nglobs: `"**/*`"`nalwaysApply: true`n---`n`n"
return Install-ToFile -Target (Join-Path $ProjDir ".cursor\rules\conductor.mdc") -Assembled $Assembled -ForceWrite $ForceWrite -Prefix $frontmatter
}
"windsurf" { return Install-ToFile -Target (Join-Path $ProjDir ".windsurfrules") -Assembled $Assembled -ForceWrite $ForceWrite }
"cline" { return Install-ToFile -Target (Join-Path $ProjDir ".clinerules") -Assembled $Assembled -ForceWrite $ForceWrite }
"aider" {
Install-AiderToDir -SkillDir (Join-Path $ProjDir ".conductor-skills") -TmpDir $TmpDir -Config (Join-Path $ProjDir ".aider.conf.yml") -ReadPrefix ".conductor-skills/"
return $true
}
"copilot" { return Install-ToFile -Target (Join-Path $ProjDir ".github\copilot-instructions.md") -Assembled $Assembled -ForceWrite $ForceWrite }
"amazonq" { return Install-ToFile -Target (Join-Path $ProjDir ".amazonq\rules\conductor.md") -Assembled $Assembled -ForceWrite $ForceWrite }
"opencode" { return Install-ToFile -Target (Join-Path $ProjDir "AGENTS.md") -Assembled $Assembled -ForceWrite $ForceWrite }
"roo" { return Install-ToFile -Target (Join-Path $ProjDir ".roo\rules\conductor.md") -Assembled $Assembled -ForceWrite $ForceWrite }
"amp" { return Install-ToFile -Target (Join-Path $ProjDir ".amp\instructions.md") -Assembled $Assembled -ForceWrite $ForceWrite }
}
}
return $false
}
# ─────────────────────────────────────────────────────────────────────────────
# Uninstall
# ─────────────────────────────────────────────────────────────────────────────
function Uninstall-Agent {
param([string]$AgentName, [string]$ProjDir, [bool]$IsGlobal)
if ($AgentName -eq "claude") {
if ($IsGlobal) {
$settingsPath = Join-Path $env:USERPROFILE ".claude\settings.json"
} else {
$settingsPath = Join-Path $ProjDir ".claude\settings.json"
}
if (-not (Test-Path $settingsPath)) {
Write-Warn "Nothing to uninstall: $settingsPath not found"
return
}
try {
$raw = Get-Content $settingsPath -Raw
$settings = $raw | ConvertFrom-Json
} catch {
Write-Err "Failed to parse $settingsPath : $_"
return
}
$changed = $false
if (($settings.PSObject.Properties.Name -contains 'extraKnownMarketplaces') -and
($settings.extraKnownMarketplaces.PSObject.Properties.Name -contains 'conductor-skills')) {
$settings.extraKnownMarketplaces.PSObject.Properties.Remove('conductor-skills')
if (-not $settings.extraKnownMarketplaces.PSObject.Properties.Name) {
$settings.PSObject.Properties.Remove('extraKnownMarketplaces')
}
$changed = $true
}
if (($settings.PSObject.Properties.Name -contains 'enabledPlugins') -and
($settings.enabledPlugins.PSObject.Properties.Name -contains 'conductor@conductor-skills')) {
$settings.enabledPlugins.PSObject.Properties.Remove('conductor@conductor-skills')
if (-not $settings.enabledPlugins.PSObject.Properties.Name) {
$settings.PSObject.Properties.Remove('enabledPlugins')
}
$changed = $true
}
if ($changed) {
($settings | ConvertTo-Json -Depth 10) | Set-Content -Path $settingsPath -Encoding UTF8
Write-Ok "Removed Conductor entries from $settingsPath"
} else {
Write-Warn "No Conductor entries found in $settingsPath"
}
$skillDir = Join-Path $env:USERPROFILE ".claude\skills\conductor"
if (Test-Path $skillDir) {
Remove-Item -Recurse -Force $skillDir
Write-Ok "Removed skill dir: $skillDir"
}
Clear-ClaudeLegacyAndCaches
return
}
if ($IsGlobal) {
if ($AgentName -eq "aider") {
$skillDir = Join-Path $env:USERPROFILE ".conductor-skills"
if (Test-Path $skillDir) {
Remove-Item -Recurse -Force $skillDir
Write-Ok "Removed: $skillDir"
Write-Info "Note: You may also want to remove the 'read:' entries from ~/.aider.conf.yml"
} else {
Write-Warn "Nothing to uninstall: $skillDir not found"
}
return
}
$target = Get-GlobalPath -AgentName $AgentName
} else {
$target = Get-TargetPath -AgentName $AgentName -ProjDir $ProjDir
if ($AgentName -eq "aider") {
if (Test-Path $target) {
Remove-Item -Recurse -Force $target
Write-Ok "Removed: $target"
Write-Info "Note: You may also want to remove the 'read:' entries from .aider.conf.yml"
} else {
Write-Warn "Nothing to uninstall: $target not found"
}
return
}
}
if (Test-Path $target) {
Remove-Item -Force $target
Write-Ok "Removed: $target"
} else {
Write-Warn "Nothing to uninstall: $target not found"
}
}
# ─────────────────────────────────────────────────────────────────────────────
# Check mode (dry run)
# ─────────────────────────────────────────────────────────────────────────────
function Do-Check {
param([string[]]$Agents)
$manifest = Get-ManifestPath -IsGlobal $true
Write-Host ""
Write-Host "Detected agents:" -ForegroundColor White
if ($Agents.Count -eq 0) {
Write-Warn "No AI coding agents detected on this system."
return
}
foreach ($a in $Agents) {
$installedVer = Read-ManifestVersion -ManifestPath $manifest -AgentName $a
$globalSupport = if (Supports-Global -AgentName $a) { "yes" } else { "no" }
if ($installedVer) {
if ($installedVer -eq $SCRIPT_VERSION) {
Write-Host " * $a v$installedVer (up to date)" -ForegroundColor Green
} else {
Write-Host " * $a v$installedVer -> v$SCRIPT_VERSION (upgrade available)" -ForegroundColor Yellow
}
} else {
Write-Host " * $a (not installed) global: $globalSupport" -ForegroundColor Blue
}
}
Write-Host ""
}
# ─────────────────────────────────────────────────────────────────────────────
# Main
# ─────────────────────────────────────────────────────────────────────────────
if ($Version) {
Write-Host "v$SCRIPT_VERSION"
exit 0
}
# Require -Agent or -All
if (-not $Agent -and -not $All) {
Write-Err "Missing required -Agent parameter (or use -All)"
exit 1
}
# Resolve project dir
$ProjectDir = (Resolve-Path $ProjectDir).Path
Write-Host ""
Write-Host "Conductor Skills Installer v$SCRIPT_VERSION" -ForegroundColor White
Write-Host ""
# Build agent list
$agentList = @()
if ($All) {
$agentList = @(Detect-Agents)
if ($agentList.Count -eq 0) {
Write-Warn "No AI coding agents detected on this system."
Write-Host ""
Write-Info "Supported agents: claude, codex, gemini, cursor, windsurf, cline,"
Write-Info " aider, copilot, amazonq, opencode, roo, amp"
Write-Host ""
Write-Info "Install one of the above, then re-run this command."
exit 0
}
Write-Info "Detected agents: $($agentList -join ', ')"
Write-Host ""
} else {
$Agent = $Agent.ToLower()
if ($VALID_AGENTS -notcontains $Agent) {
Write-Err "Unknown agent: $Agent"
exit 1
}
$agentList = @($Agent)
}
# Check mode
if ($Check) {
Do-Check -Agents $agentList
exit 0
}
# Determine manifest path
$useGlobalManifest = $Global -or $All
$manifest = Get-ManifestPath -IsGlobal $useGlobalManifest -ProjDir $ProjectDir
# Handle uninstall
if ($Uninstall) {
foreach ($a in $agentList) {
$useGlobal = [bool]$Global
if ($All -and (Supports-Global -AgentName $a)) { $useGlobal = $true }
Write-Info "Uninstalling $a ..."
Uninstall-Agent -AgentName $a -ProjDir $ProjectDir -IsGlobal $useGlobal
Remove-ManifestEntry -ManifestPath $manifest -AgentName $a
}
Write-Host ""
Write-Ok "Done!"
exit 0
}
# Check for upgrade
$targetVersion = $SCRIPT_VERSION
if ($Upgrade) {
Write-Info "Checking for updates..."
$remoteVer = Fetch-RemoteVersion
if (-not $remoteVer) {
Write-Warn "Could not check for updates (offline?). Using bundled v$SCRIPT_VERSION."
} elseif ($remoteVer -ne $SCRIPT_VERSION) {
Write-Info "Update available: v$SCRIPT_VERSION -> v$remoteVer"
$targetVersion = $remoteVer
} else {
Write-Info "Already at latest version (v$SCRIPT_VERSION)."
}
Write-Host ""
}
# Download files to temp dir
$tmpDir = Join-Path ([System.IO.Path]::GetTempPath()) "conductor-skills-$(Get-Random)"
New-Item -ItemType Directory -Path $tmpDir -Force | Out-Null
try {
Download-Files -TmpDir $tmpDir
$assembled = Join-Path $tmpDir "_assembled.md"
Assemble-Content -TmpDir $tmpDir -Output $assembled
$size = (Get-Item $assembled).Length
Write-Ok "Assembled skill content ($size bytes)"
$installedCount = 0
$skippedCount = 0
foreach ($a in $agentList) {
Write-Host ""
# Determine if global for this agent
$useGlobal = [bool]$Global
if ($All) {
if (Supports-Global -AgentName $a) {
$useGlobal = $true
} else {
Write-Info "${a}: skipped (project-only install). Use -ProjectDir <path> to include."
$skippedCount++
continue
}
}
# Validate global support for single-agent mode
if ($useGlobal -and !(Supports-Global -AgentName $a)) {
Write-Err "Global install is not supported for $a. Run from your project directory instead."
continue
}
# Idempotency check. -Force re-installs; -Upgrade does not (matches -Check).
# Exception: claude always re-runs. Its "installed state" lives in two
# places — our manifest AND Claude Code's plugin cache. The cache can
# drift independently (user installed v1.0.0 via /plugin install, then
# we publish v1.6.0 — manifest matches but cache is stale). Always
# re-running keeps upgrades honest.
$installedVer = Read-ManifestVersion -ManifestPath $manifest -AgentName $a
if ($a -ne 'claude' -and $installedVer -and ($installedVer -eq $targetVersion) -and !$Force) {
Write-Ok "$a already at v$installedVer, skipping."
$skippedCount++
continue
}
if ($installedVer -and ($installedVer -ne $targetVersion)) {
Write-Info "Upgrading $a from v$installedVer to v$targetVersion ..."
} else {
Write-Info "Installing for $a ..."
}
# Write-time force semantics:
# -Force / -Upgrade → always
# manifest shows older version → force (clear upgrade; file is OURS)
# Mirrors install.sh — the 1.5.0->1.6.x upgrade no-op happened because
# safe_write's Read-Host prompt got an empty answer on non-interactive
# stdin and silently skipped every file.
$installForce = [bool]$Force
if ($Upgrade) { $installForce = $true }
if ($installedVer -and ($installedVer -ne $targetVersion)) { $installForce = $true }
# Perform install
$result = Install-ForAgent -AgentName $a -ProjDir $ProjectDir -IsGlobal $useGlobal -ForceWrite $installForce -TmpDir $tmpDir -Assembled $assembled
if ($result -ne $false) {
$targetPath = if ($useGlobal) { Get-GlobalPath -AgentName $a } else { Get-TargetPath -AgentName $a -ProjDir $ProjectDir }
$mode = if ($useGlobal) { "global" } else { "project" }
Write-ManifestEntry -ManifestPath $manifest -AgentName $a -Ver $targetVersion -Mode $mode -TargetPath $targetPath
$installedCount++
}
}
Write-Host ""
Write-Host "Done! Installed: $installedCount, Skipped: $skippedCount" -ForegroundColor Green
Write-Host ""
Write-Host "Next steps:"
Write-Host ' Ask your agent to connect to your Conductor server, e.g.:'
Write-Host ""
Write-Host ' "Connect to my Conductor server at http://localhost:8080/api"'
Write-Host ""
Write-Host " Docs: https://github.com/conductor-oss/conductor-skills" -ForegroundColor Blue
Write-Host ""
} finally {
Remove-Item -Recurse -Force $tmpDir -ErrorAction SilentlyContinue
}
SHA-256: a05a40a5d4f68bdd19e14cb81fc6fa52c3592a70672297e0cf139249784dcf49