← Files ConductorARCHIVED FILE
install.sh
36 KB · Oct 3, 2026 · 06:32 UTC
#!/usr/bin/env bash
set -euo pipefail
# ─────────────────────────────────────────────────────────────────────────────
# Conductor Skills Installer
# Installs Conductor workflow orchestration skills for your AI coding agent.
# https://github.com/conductor-oss/conductor-skills
# ─────────────────────────────────────────────────────────────────────────────
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 — the install scripts ride
# along with whatever main is serving.
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="${CONDUCTOR_SKILLS_LOCAL_DIR:-}"
# Files to ship to non-Claude agents (Claude uses the marketplace flow).
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"
)
# Colors (if terminal supports them)
if [ -t 1 ]; then
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
BOLD='\033[1m'
NC='\033[0m'
else
RED='' GREEN='' YELLOW='' BLUE='' BOLD='' NC=''
fi
# ─────────────────────────────────────────────────────────────────────────────
# Helpers
# ─────────────────────────────────────────────────────────────────────────────
info() { echo -e "${BLUE}[info]${NC} $*"; }
ok() { echo -e "${GREEN}[ok]${NC} $*"; }
warn() { echo -e "${YELLOW}[warn]${NC} $*"; }
error() { echo -e "${RED}[error]${NC} $*" >&2; }
usage() {
cat <<EOF
${BOLD}Conductor Skills Installer v${VERSION}${NC}
Usage:
install.sh [--agent <name> | --all] [--global] [--project-dir <path>]
[--upgrade] [--check] [--force] [--uninstall]
Options:
--agent <name> Install for a specific agent
--all Auto-detect all agents and install for each
--global Install globally (available in all projects)
--project-dir <path> Target project directory (default: current directory)
--upgrade Check for newer version and upgrade installed agents
--check Dry run — show what would be installed, no changes
--force Overwrite existing files without prompting
--uninstall Remove installed skill files
--version Print version and exit
--help Show this help message
Supported agents:
claude Claude Code (Anthropic)
codex Codex CLI (OpenAI)
gemini Gemini CLI (Google)
cursor Cursor
windsurf Windsurf (Codeium)
cline Cline
aider Aider
copilot GitHub Copilot
amazonq Amazon Q Developer
opencode OpenCode
roo Roo Code
amp Amp
Examples:
# Auto-detect and install for all agents
install.sh --all
# Install globally for Codex CLI
install.sh --agent codex --global
# Check what would be installed
install.sh --all --check
# Upgrade all installed agents to latest
install.sh --all --upgrade
# Uninstall from all agents
install.sh --all --uninstall
EOF
exit 0
}
# ─────────────────────────────────────────────────────────────────────────────
# Agent detection
# ─────────────────────────────────────────────────────────────────────────────
detect_agents() {
local detected=()
{ command -v claude &>/dev/null || [ -d "$HOME/.claude" ]; } && detected+=(claude)
{ command -v codex &>/dev/null || [ -d "$HOME/.codex" ]; } && detected+=(codex)
{ command -v gemini &>/dev/null || [ -d "$HOME/.gemini" ]; } && detected+=(gemini)
[ -d "$HOME/.cursor" ] && detected+=(cursor)
[ -d "$HOME/.codeium" ] && detected+=(windsurf)
[ -d "$HOME/.cline" ] && detected+=(cline)
command -v aider &>/dev/null && detected+=(aider)
[ -d "$HOME/.config/github-copilot" ] && detected+=(copilot)
{ command -v q &>/dev/null || [ -d "$HOME/.amazonq" ]; } && detected+=(amazonq)
command -v opencode &>/dev/null && detected+=(opencode)
[ -d "$HOME/.roo" ] && detected+=(roo)
{ command -v amp &>/dev/null || [ -d "$HOME/.config/amp" ]; } && detected+=(amp)
echo "${detected[@]:-}"
}
# ─────────────────────────────────────────────────────────────────────────────
# Manifest tracking
# ─────────────────────────────────────────────────────────────────────────────
GLOBAL_MANIFEST="$HOME/.conductor-skills/manifest.json"
get_manifest_path() {
local is_global="$1"
local project_dir="${2:-.}"
if [ "$is_global" = "true" ]; then
echo "$GLOBAL_MANIFEST"
else
echo "$project_dir/.conductor-skills/manifest.json"
fi
}
ensure_manifest() {
local manifest="$1"
local dir
dir=$(dirname "$manifest")
mkdir -p "$dir"
if [ ! -f "$manifest" ]; then
echo '{"schema_version":1,"installations":{}}' > "$manifest"
fi
}
read_manifest_version() {
local manifest="$1"
local agent="$2"
if [ ! -f "$manifest" ]; then
echo ""
return
fi
python3 -c "
import json, sys
try:
m = json.load(open('$manifest'))
print(m.get('installations',{}).get('$agent',{}).get('version',''))
except: pass
" 2>/dev/null || echo ""
}
write_manifest_entry() {
local manifest="$1"
local agent="$2"
local ver="$3"
local mode="$4"
local target_path="$5"
ensure_manifest "$manifest"
python3 -c "
import json, datetime
m = json.load(open('$manifest'))
now = datetime.datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%SZ')
entry = m.setdefault('installations',{}).get('$agent',{})
m['installations']['$agent'] = {
'version': '$ver',
'installed_at': entry.get('installed_at', now),
'updated_at': now,
'mode': '$mode',
'target_path': '$target_path'
}
json.dump(m, open('$manifest','w'), indent=2)
" 2>/dev/null
}
remove_manifest_entry() {
local manifest="$1"
local agent="$2"
if [ ! -f "$manifest" ]; then
return
fi
python3 -c "
import json
m = json.load(open('$manifest'))
m.get('installations',{}).pop('$agent', None)
json.dump(m, open('$manifest','w'), indent=2)
" 2>/dev/null
}
list_manifest_agents() {
local manifest="$1"
if [ ! -f "$manifest" ]; then
echo ""
return
fi
python3 -c "
import json
try:
m = json.load(open('$manifest'))
agents = list(m.get('installations',{}).keys())
print(' '.join(agents))
except: pass
" 2>/dev/null || echo ""
}
# ─────────────────────────────────────────────────────────────────────────────
# Remote version check
# ─────────────────────────────────────────────────────────────────────────────
fetch_remote_version() {
if [ -n "$LOCAL_DIR" ] && [ -f "$LOCAL_DIR/VERSION" ]; then
cat "$LOCAL_DIR/VERSION" | tr -d '[:space:]'
return
fi
local remote_ver
remote_ver=$(curl -sSfL "$REPO_BASE/VERSION" 2>/dev/null | tr -d '[:space:]') || true
echo "$remote_ver"
}
# ─────────────────────────────────────────────────────────────────────────────
# Download & assembly
# ─────────────────────────────────────────────────────────────────────────────
download_files() {
local tmp_dir="$1"
if [ -n "$LOCAL_DIR" ]; then
if [ ! -d "$LOCAL_DIR" ]; then
error "CONDUCTOR_SKILLS_LOCAL_DIR=$LOCAL_DIR is not a directory"
exit 1
fi
info "Copying skill files from $LOCAL_DIR..."
for file in "${SKILL_FILES[@]}"; do
local src="$LOCAL_DIR/$file"
local dest="$tmp_dir/$file"
if [ ! -f "$src" ]; then
error "Missing file in local source: $file"
# tmp_dir cleanup handled by EXIT trap
exit 1
fi
mkdir -p "$(dirname "$dest")"
cp "$src" "$dest"
done
ok "Copied ${#SKILL_FILES[@]} files"
return
fi
info "Downloading skill files..."
for file in "${SKILL_FILES[@]}"; do
local dir
dir=$(dirname "$file")
mkdir -p "$tmp_dir/$dir"
if ! curl -sSfL "$REPO_BASE/$file" -o "$tmp_dir/$file" 2>/dev/null; then
local http_code
http_code=$(curl -sS -o /dev/null -w "%{http_code}" "$REPO_BASE/$file" 2>/dev/null || echo "?")
error "Failed to download $file (HTTP $http_code from $REPO_BASE)"
if [ "$http_code" = "404" ]; then
error "File is missing upstream — likely a release packaging bug."
error "Please report: https://github.com/conductor-oss/conductor-skills/issues"
else
error "Check your internet connection and try again."
fi
# tmp_dir cleanup handled by EXIT trap
exit 1
fi
done
ok "Downloaded ${#SKILL_FILES[@]} files"
}
assemble_content() {
local tmp_dir="$1"
local output="$2"
{
cat "$tmp_dir/skills/conductor/SKILL.md"
echo ""
echo "---"
echo ""
echo "# References"
echo ""
for f in "$tmp_dir"/skills/conductor/references/*.md; do
cat "$f"
echo ""
echo "---"
echo ""
done
echo "# Examples"
echo ""
for f in "$tmp_dir"/skills/conductor/examples/*.md; do
cat "$f"
echo ""
echo "---"
echo ""
done
} > "$output"
}
# ─────────────────────────────────────────────────────────────────────────────
# Safe file writing (respects --force)
# ─────────────────────────────────────────────────────────────────────────────
safe_write() {
local target="$1"
local source="$2"
local force="$3"
if [ -f "$target" ] && [ "$force" != "true" ]; then
warn "File already exists: $target"
printf " Overwrite? [y/N] "
read -r answer
if [[ ! "$answer" =~ ^[Yy] ]]; then
info "Skipped. Use --force to overwrite."
return 1
fi
fi
local dir
dir=$(dirname "$target")
mkdir -p "$dir"
cp "$source" "$target"
ok "Installed: $target"
}
# ─────────────────────────────────────────────────────────────────────────────
# Global install paths
# ─────────────────────────────────────────────────────────────────────────────
supports_global() {
local agent="$1"
case "$agent" in
claude|codex|gemini|cursor|windsurf|roo|amp|aider|opencode) return 0 ;;
*) return 1 ;;
esac
}
get_global_path() {
local agent="$1"
case "$agent" in
claude) echo "$HOME/.claude/settings.json" ;;
codex) echo "${CODEX_HOME:-$HOME/.codex}/AGENTS.md" ;;
cursor) echo "$HOME/.cursor/skills/conductor/SKILL.md" ;;
gemini) echo "$HOME/.gemini/GEMINI.md" ;;
windsurf) echo "$HOME/.codeium/windsurf/memories/global_rules.md" ;;
roo) echo "$HOME/.roo/rules/conductor.md" ;;
amp) echo "$HOME/.config/AGENTS.md" ;;
aider) echo "$HOME/.aider.conf.yml" ;;
opencode) echo "$HOME/.config/opencode/skills/conductor/SKILL.md" ;;
esac
}
get_target_path() {
local agent="$1"
local project_dir="$2"
case "$agent" in
claude) echo "$project_dir/.claude/settings.json" ;;
codex) echo "$project_dir/AGENTS.md" ;;
gemini) echo "$project_dir/GEMINI.md" ;;
cursor) echo "$project_dir/.cursor/rules/conductor.mdc" ;;
windsurf) echo "$project_dir/.windsurfrules" ;;
cline) echo "$project_dir/.clinerules" ;;
aider) echo "$project_dir/.conductor-skills" ;;
copilot) echo "$project_dir/.github/copilot-instructions.md" ;;
amazonq) echo "$project_dir/.amazonq/rules/conductor.md" ;;
opencode) echo "$project_dir/AGENTS.md" ;;
roo) echo "$project_dir/.roo/rules/conductor.md" ;;
amp) echo "$project_dir/.amp/instructions.md" ;;
esac
}
# ─────────────────────────────────────────────────────────────────────────────
# Per-agent install logic
# ─────────────────────────────────────────────────────────────────────────────
# Bust plugin caches so existing Claude installs actually pick up new content.
# Note: we do NOT delete ~/.claude/skills/conductor — that's the user-skill
# install location, populated by install_claude_skill below. We *overwrite*
# it with fresh content on each install instead.
clean_claude_legacy_and_caches() {
local cache="$HOME/.claude/plugins/cache/conductor-skills"
local market="$HOME/.claude/plugins/marketplaces/conductor-skills"
local registry="$HOME/.claude/plugins/installed_plugins.json"
if [ -d "$cache" ]; then
rm -rf "$cache"
ok "Cleared plugin cache: $cache (will re-fetch on next session)"
fi
if [ -d "$market" ]; then
rm -rf "$market"
ok "Cleared marketplace clone: $market (will re-clone on next session)"
fi
# Surgically remove our entry from Claude's plugin registry — if we leave
# a stale "installed at v1.0.0" record pointing to a now-deleted cache dir,
# Claude Code may skip re-fetching on next session start.
if [ -f "$registry" ]; then
if python3 - "$registry" <<'PY' 2>/dev/null
import json, os, sys
path = sys.argv[1]
with open(path) as f:
r = json.load(f)
plugins = r.get('plugins') or {}
if 'conductor@conductor-skills' not in plugins:
sys.exit(2) # nothing to do
plugins.pop('conductor@conductor-skills')
tmp = path + '.tmp'
with open(tmp, 'w') as f:
json.dump(r, f, indent=2)
f.write('\n')
os.replace(tmp, path)
PY
then
ok "Cleared stale registry entry in $registry"
fi
fi
}
# Install Conductor as a user-skill at ~/.claude/skills/conductor — the
# "good old" skill location that's visible to the user immediately and
# auto-loaded by Claude Code at session start (no marketplace fetch needed).
# Source files come from $tmp_dir (downloaded from GitHub) or $LOCAL_DIR
# (bundled with the npm package).
install_claude_skill() {
local tmp_dir="$1"
local skill_dest="$HOME/.claude/skills/conductor"
local src_dir
if [ -n "$LOCAL_DIR" ]; then
src_dir="$LOCAL_DIR/skills/conductor"
else
src_dir="$tmp_dir/skills/conductor"
fi
if [ ! -d "$src_dir" ]; then
error "Skill source not found at $src_dir"
return 1
fi
mkdir -p "$skill_dest"
# Mirror the skill contents — replace, don't merge, so removed files vanish.
rm -rf "$skill_dest"
mkdir -p "$skill_dest"
cp -R "$src_dir/." "$skill_dest/"
ok "Installed skill files: $skill_dest"
}
install_claude() {
# Dual install for Claude Code:
# 1. Plugin via settings.json (slash commands, auto-updates from marketplace)
# 2. User skill at ~/.claude/skills/conductor (visible files, immediate)
# Both coexist — Claude Code dedupes on skill name at load time.
local is_global="$1"
local project_dir="$2"
local tmp_dir="$3"
local settings_path
if [ "$is_global" = "true" ]; then
settings_path="$HOME/.claude/settings.json"
else
settings_path="$project_dir/.claude/settings.json"
fi
mkdir -p "$(dirname "$settings_path")"
clean_claude_legacy_and_caches
install_claude_skill "$tmp_dir"
info "Enabling Conductor plugin in $settings_path ..."
if ! python3 - "$settings_path" <<'PY'
import json, os, sys
path = sys.argv[1]
try:
with open(path) as f:
s = json.load(f)
except FileNotFoundError:
s = {}
except json.JSONDecodeError as e:
print(f"error: {path} is not valid JSON: {e}", file=sys.stderr)
sys.exit(1)
mkts = s.setdefault('extraKnownMarketplaces', {})
mkts['conductor-skills'] = {
'source': {'source': 'github', 'repo': 'conductor-oss/conductor-skills'}
}
enabled = s.setdefault('enabledPlugins', {})
enabled['conductor@conductor-skills'] = True
tmp = path + '.tmp'
with open(tmp, 'w') as f:
json.dump(s, f, indent=2)
f.write('\n')
os.replace(tmp, path)
PY
then
error "Failed to update $settings_path"
return 1
fi
ok "Configured: $settings_path"
echo ""
echo -e "${GREEN}${BOLD}Conductor installed two ways:${NC}"
echo ""
echo " 1. ${BOLD}Skill${NC} at ~/.claude/skills/conductor/"
echo " Files are there NOW. Skill auto-loads on next session start."
echo ""
echo " 2. ${BOLD}Plugin${NC} via settings.json (extraKnownMarketplaces + enabledPlugins)"
echo " Claude Code fetches plugin v${VERSION} on next session start."
echo " Provides slash commands: /conductor, /conductor-setup, /conductor-optimize, /conductor-scaffold-worker"
echo ""
echo -e "${YELLOW}${BOLD}⚠ Restart Claude Code to activate.${NC}"
echo " • claude CLI: exit (Ctrl+D) and start a new session"
echo " • VS Code extension: Cmd+Shift+P → \"Reload Window\""
echo " • Desktop app: quit and reopen"
echo ""
echo " Verify after restart:"
echo " ls ~/.claude/skills/conductor/ # skill files"
echo " ls ~/.claude/plugins/cache/conductor-skills/conductor/${VERSION}/ # plugin files"
}
install_to_file() {
local target="$1"
local assembled="$2"
local force="$3"
local prefix="${4:-}"
if [ -n "$prefix" ]; then
local tmp_with_prefix
tmp_with_prefix=$(mktemp)
{
echo "$prefix"
cat "$assembled"
} > "$tmp_with_prefix"
local rv=0
safe_write "$target" "$tmp_with_prefix" "$force" || rv=$?
rm -f "$tmp_with_prefix"
return $rv
else
safe_write "$target" "$assembled" "$force"
fi
}
install_aider_to_dir() {
local skill_dir="$1"
local tmp_dir="$2"
local config="$3"
local read_prefix="$4"
mkdir -p "$skill_dir/references" "$skill_dir/examples" "$skill_dir/scripts"
info "Copying skill files to $skill_dir ..."
cp "$tmp_dir/skills/conductor/SKILL.md" "$skill_dir/"
for f in "$tmp_dir"/skills/conductor/references/*.md; do
cp "$f" "$skill_dir/references/"
done
for f in "$tmp_dir"/skills/conductor/examples/*.md; do
cp "$f" "$skill_dir/examples/"
done
for f in "$tmp_dir"/skills/conductor/scripts/*.py; do
[ -f "$f" ] && cp "$f" "$skill_dir/scripts/"
done
ok "Files copied to $skill_dir"
if [ -f "$config" ] && grep -q "conductor-skills" "$config" 2>/dev/null; then
info "Aider config already references conductor-skills, skipping."
else
info "Adding read entries to $config ..."
{
echo ""
echo "# Conductor Skills"
echo "read:"
for file in "${SKILL_FILES[@]}"; do
echo " - ${read_prefix}${file#skills/conductor/}"
done
} >> "$config"
ok "Updated: $config"
fi
}
# Install a single agent. Returns 0 on success, 1 on skip/failure.
install_for_agent() {
local agent="$1"
local project_dir="$2"
local is_global="$3"
local force="$4"
local tmp_dir="$5"
local assembled="$6"
# Claude has its own install path (writes to settings.json + skill dir)
if [ "$agent" = "claude" ]; then
install_claude "$is_global" "$project_dir" "$tmp_dir"
return $?
fi
if [ "$is_global" = "true" ]; then
local target_path
target_path=$(get_global_path "$agent")
if [ "$agent" = "aider" ]; then
install_aider_to_dir "$HOME/.conductor-skills" "$tmp_dir" "$HOME/.aider.conf.yml" "$HOME/.conductor-skills/"
else
install_to_file "$target_path" "$assembled" "$force"
fi
else
case "$agent" in
codex)
install_to_file "$project_dir/AGENTS.md" "$assembled" "$force"
;;
gemini)
install_to_file "$project_dir/GEMINI.md" "$assembled" "$force"
;;
cursor)
local frontmatter
frontmatter=$(cat <<'FRONT'
---
description: Conductor workflow orchestration - create, run, monitor, and manage workflows
globs: "**/*"
alwaysApply: true
---
FRONT
)
install_to_file "$project_dir/.cursor/rules/conductor.mdc" "$assembled" "$force" "$frontmatter"
;;
windsurf)
install_to_file "$project_dir/.windsurfrules" "$assembled" "$force"
;;
cline)
install_to_file "$project_dir/.clinerules" "$assembled" "$force"
;;
aider)
install_aider_to_dir "$project_dir/.conductor-skills" "$tmp_dir" "$project_dir/.aider.conf.yml" ".conductor-skills/"
;;
copilot)
install_to_file "$project_dir/.github/copilot-instructions.md" "$assembled" "$force"
;;
amazonq)
install_to_file "$project_dir/.amazonq/rules/conductor.md" "$assembled" "$force"
;;
opencode)
install_to_file "$project_dir/AGENTS.md" "$assembled" "$force"
;;
roo)
install_to_file "$project_dir/.roo/rules/conductor.md" "$assembled" "$force"
;;
amp)
install_to_file "$project_dir/.amp/instructions.md" "$assembled" "$force"
;;
esac
fi
}
# ─────────────────────────────────────────────────────────────────────────────
# Uninstall
# ─────────────────────────────────────────────────────────────────────────────
uninstall_agent() {
local agent="$1"
local project_dir="$2"
local is_global="$3"
if [ "$agent" = "claude" ]; then
local settings_path
if [ "$is_global" = "true" ]; then
settings_path="$HOME/.claude/settings.json"
else
settings_path="$project_dir/.claude/settings.json"
fi
if [ ! -f "$settings_path" ]; then
warn "Nothing to uninstall: $settings_path not found"
return
fi
if python3 - "$settings_path" <<'PY'
import json, os, sys
path = sys.argv[1]
with open(path) as f:
s = json.load(f)
changed = False
mkts = s.get('extraKnownMarketplaces') or {}
if 'conductor-skills' in mkts:
mkts.pop('conductor-skills')
if not mkts:
s.pop('extraKnownMarketplaces', None)
changed = True
enabled = s.get('enabledPlugins') or {}
if 'conductor@conductor-skills' in enabled:
enabled.pop('conductor@conductor-skills')
if not enabled:
s.pop('enabledPlugins', None)
changed = True
if changed:
tmp = path + '.tmp'
with open(tmp, 'w') as f:
json.dump(s, f, indent=2)
f.write('\n')
os.replace(tmp, path)
PY
then
ok "Removed Conductor entries from $settings_path"
# Also remove the user-skill copy
if [ -d "$HOME/.claude/skills/conductor" ]; then
rm -rf "$HOME/.claude/skills/conductor"
ok "Removed skill dir: $HOME/.claude/skills/conductor"
fi
clean_claude_legacy_and_caches
else
error "Failed to update $settings_path"
fi
return
fi
local target
if [ "$is_global" = "true" ]; then
if [ "$agent" = "aider" ]; then
local skill_dir="$HOME/.conductor-skills"
if [ -d "$skill_dir" ]; then
rm -rf "$skill_dir"
ok "Removed: $skill_dir"
info "Note: You may also want to remove the 'read:' entries from ~/.aider.conf.yml"
else
warn "Nothing to uninstall: $skill_dir not found"
fi
return
fi
target=$(get_global_path "$agent")
else
target=$(get_target_path "$agent" "$project_dir")
if [ "$agent" = "aider" ]; then
if [ -d "$target" ]; then
rm -rf "$target"
ok "Removed: $target"
info "Note: You may also want to remove the 'read:' entries from .aider.conf.yml"
else
warn "Nothing to uninstall: $target not found"
fi
return
fi
fi
if [ -f "$target" ]; then
rm -f "$target"
ok "Removed: $target"
else
warn "Nothing to uninstall: $target not found"
fi
}
# ─────────────────────────────────────────────────────────────────────────────
# Check mode (dry run)
# ─────────────────────────────────────────────────────────────────────────────
do_check() {
local agents=("$@")
local manifest
manifest=$(get_manifest_path "true" "")
echo ""
echo -e "${BOLD}Detected agents:${NC}"
if [ ${#agents[@]} -eq 0 ]; then
warn "No AI coding agents detected on this system."
return
fi
for agent in "${agents[@]}"; do
local installed_ver
installed_ver=$(read_manifest_version "$manifest" "$agent")
local global_support="yes"
supports_global "$agent" || global_support="no"
if [ -n "$installed_ver" ]; then
if [ "$installed_ver" = "$VERSION" ]; then
echo -e " ${GREEN}●${NC} $agent v${installed_ver} (up to date)"
else
echo -e " ${YELLOW}●${NC} $agent v${installed_ver} → v${VERSION} (upgrade available)"
fi
else
echo -e " ${BLUE}●${NC} $agent (not installed) global: $global_support"
fi
done
echo ""
}
# ─────────────────────────────────────────────────────────────────────────────
# Main
# ─────────────────────────────────────────────────────────────────────────────
main() {
local agent=""
local project_dir="."
local force="false"
local uninstall="false"
local global="false"
local all="false"
local upgrade="false"
local check="false"
while [[ $# -gt 0 ]]; do
case "$1" in
--agent) agent="$2"; shift 2 ;;
--project-dir) project_dir="$2"; shift 2 ;;
--global) global="true"; shift ;;
--all) all="true"; shift ;;
--upgrade) upgrade="true"; shift ;;
--check) check="true"; shift ;;
--force) force="true"; shift ;;
--uninstall) uninstall="true"; shift ;;
--version) echo "v${VERSION}"; exit 0 ;;
--help|-h) usage ;;
*) error "Unknown option: $1"; usage ;;
esac
done
# Require --agent or --all
if [ -z "$agent" ] && [ "$all" != "true" ]; then
error "Missing required --agent flag (or use --all)"
echo ""
usage
fi
# Resolve project dir to absolute path
project_dir=$(cd "$project_dir" && pwd)
echo ""
echo -e "${BOLD}Conductor Skills Installer v${VERSION}${NC}"
echo ""
# Build agent list
local agents=()
if [ "$all" = "true" ]; then
local detected
detected=$(detect_agents)
if [ -z "$detected" ]; then
warn "No AI coding agents detected on this system."
echo ""
info "Supported agents: claude, codex, gemini, cursor, windsurf, cline,"
info " aider, copilot, amazonq, opencode, roo, amp"
echo ""
info "Install one of the above, then re-run this command."
exit 0
fi
# shellcheck disable=SC2206
agents=($detected)
info "Detected agents: ${agents[*]}"
echo ""
else
agent=$(echo "$agent" | tr '[:upper:]' '[:lower:]')
case "$agent" in
claude|codex|gemini|cursor|windsurf|cline|aider|copilot|amazonq|opencode|roo|amp) ;;
*) error "Unknown agent: $agent"; echo ""; usage ;;
esac
agents=("$agent")
fi
# Check mode — dry run
if [ "$check" = "true" ]; then
do_check "${agents[@]}"
exit 0
fi
# Determine manifest path
local manifest
if [ "$global" = "true" ] || [ "$all" = "true" ]; then
manifest=$(get_manifest_path "true" "")
else
manifest=$(get_manifest_path "false" "$project_dir")
fi
# Handle uninstall
if [ "$uninstall" = "true" ]; then
for a in "${agents[@]}"; do
local use_global="$global"
if [ "$all" = "true" ] && supports_global "$a"; then
use_global="true"
fi
info "Uninstalling ${BOLD}${a}${NC} ..."
uninstall_agent "$a" "$project_dir" "$use_global"
remove_manifest_entry "$manifest" "$a"
done
echo ""
ok "Done!"
return
fi
# Check for upgrade
local target_version="$VERSION"
if [ "$upgrade" = "true" ]; then
info "Checking for updates..."
local remote_ver
remote_ver=$(fetch_remote_version)
if [ -z "$remote_ver" ]; then
warn "Could not check for updates (offline?). Using bundled v${VERSION}."
elif [ "$remote_ver" != "$VERSION" ]; then
info "Update available: v${VERSION} → v${remote_ver}"
target_version="$remote_ver"
else
info "Already at latest version (v${VERSION})."
fi
echo ""
fi
# Download files to temp dir (not local — the EXIT trap must access it after main returns)
tmp_dir=$(mktemp -d)
trap 'rm -rf "$tmp_dir"' EXIT
download_files "$tmp_dir"
# Assemble into single file
local assembled="$tmp_dir/_assembled.md"
assemble_content "$tmp_dir" "$assembled"
ok "Assembled skill content ($(wc -c < "$assembled" | tr -d ' ') bytes)"
# Install for each agent
local installed_count=0
local skipped_count=0
for a in "${agents[@]}"; do
echo ""
# Determine if global for this agent
local use_global="$global"
if [ "$all" = "true" ]; then
if supports_global "$a"; then
use_global="true"
else
info "${BOLD}${a}${NC}: skipped (project-only install). Use --project-dir <path> to include."
skipped_count=$((skipped_count + 1))
continue
fi
fi
# Validate global support for single-agent mode
if [ "$use_global" = "true" ] && ! supports_global "$a"; then
error "Global install is not supported for $a. Run from your project directory instead."
continue
fi
# Idempotency check. --force re-installs anyway; --upgrade does not (matches
# --check semantics: "already at latest" should be a no-op).
#
# Exception: claude always re-runs. Its "installed state" lives in two
# places — our manifest AND Claude Code's plugin cache. The manifest alone
# isn't authoritative; the cache can drift (e.g. user installed v1.0.0 via
# /plugin install, then we publish v1.6.0 — manifest matches but cache is
# stale). Always re-running the cache bust + settings.json upsert keeps
# upgrades honest. Cost: ~3 seconds added to next session start.
local installed_ver
installed_ver=$(read_manifest_version "$manifest" "$a")
if [ "$a" != "claude" ] && [ -n "$installed_ver" ] && [ "$installed_ver" = "$target_version" ] && [ "$force" != "true" ]; then
ok "${a} already at v${installed_ver}, skipping."
skipped_count=$((skipped_count + 1))
continue
fi
if [ -n "$installed_ver" ] && [ "$installed_ver" != "$target_version" ]; then
info "Upgrading ${BOLD}${a}${NC} from v${installed_ver} to v${target_version} ..."
else
info "Installing for ${BOLD}${a}${NC} ..."
fi
# Write-time force semantics:
# --force / --upgrade → always force
# manifest shows older → force (clear upgrade; file is OURS, user
# already consented to this install path on the original install).
# Without the third condition: `bash install.sh --all` against a v1.5.0
# install silently skipped every file because safe_write's `Overwrite? [y/N]`
# prompt got an empty answer from non-interactive stdin. That's the
# 1.5.0→1.6.x upgrade-no-op bug.
local install_force="$force"
[ "$upgrade" = "true" ] && install_force="true"
if [ -n "$installed_ver" ] && [ "$installed_ver" != "$target_version" ]; then
install_force="true"
fi
# Perform install
if install_for_agent "$a" "$project_dir" "$use_global" "$install_force" "$tmp_dir" "$assembled"; then
# Determine target path for manifest
local target_path
if [ "$use_global" = "true" ]; then
target_path=$(get_global_path "$a")
else
target_path=$(get_target_path "$a" "$project_dir")
fi
local mode
if [ "$use_global" = "true" ]; then mode="global"; else mode="project"; fi
write_manifest_entry "$manifest" "$a" "$target_version" "$mode" "$target_path"
installed_count=$((installed_count + 1))
fi
done
echo ""
echo -e "${GREEN}${BOLD}Done!${NC} ${installed_count} configured, ${skipped_count} skipped (already up to date)."
echo ""
echo "Next steps:"
# If claude was among the agents, the in-place messaging from install_claude
# already covered the restart instructions. For non-claude agents, just point
# the user at connecting to their server.
local agents_str="${agents[*]}"
if [[ " $agents_str " == *" claude "* ]]; then
echo " 1. Restart Claude Code to load the plugin (see ACTION REQUIRED above)"
echo " 2. In the new session, ask your agent to connect to your Conductor server, e.g.:"
else
echo " Ask your agent to connect to your Conductor server, e.g.:"
fi
echo ""
echo ' "Connect to my Conductor server at http://localhost:8080/api"'
echo ""
echo -e " Docs: ${BLUE}https://github.com/conductor-oss/conductor-skills${NC}"
echo ""
}
main "$@"
SHA-256: 7dd3104e7b567fa7d9404c3a330f74bec88b4498380a2ffbb8df93915e279522