Created
April 22, 2026 22:22
-
-
Save therebelrobot/ac9620d918eee3a64eaa4d4f2b39c286 to your computer and use it in GitHub Desktop.
Rough and dirty migrate to kilo from roo
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| # ============================================================================= | |
| # migrate-roo-to-kilo.sh | |
| # Migrates Roo Code configuration to Kilo Code — global and/or project-level. | |
| # | |
| # USAGE: | |
| # ./migrate-roo-to-kilo.sh [OPTIONS] | |
| # | |
| # OPTIONS: | |
| # --global-only Migrate only global config (~/.roo/, VSCode globalStorage) | |
| # --project-only Migrate only project-level config (.roo/, .roomodes) | |
| # --dry-run Show what would happen without writing anything | |
| # --no-backup Skip creating backups of existing Kilo Code config files | |
| # --skip-existing Skip files that already exist in the Kilo Code destination | |
| # --help Show this help message | |
| # | |
| # WHAT GETS MIGRATED: | |
| # | |
| # Global: | |
| # ~/.roo/rules/ -> ~/.config/kilo/rules/ | |
| # + registers glob in ~/.config/kilo/kilo.jsonc | |
| # ~/.roo/commands/ -> ~/.config/kilo/commands/ (slash-command workflows) | |
| # ~/.roo/skills/ -> ~/.kilo/skills/ | |
| # [VSCode globalStorage] | |
| # mcp_settings.json -> Kilo Code globalStorage mcp_settings.json | |
| # custom_modes.yaml/json -> ~/.config/kilo/agents/<slug>.md (one per mode) | |
| # | |
| # Project (run from project root): | |
| # .roo/mcp.json -> .kilo/mcp.json | |
| # .roo/rules/ -> .kilo/rules/ | |
| # + registers glob in kilo.jsonc | |
| # .roo/commands/ -> .kilo/commands/ (slash-command workflows) | |
| # .roo/skills/ -> .kilo/skills/ | |
| # .roomodes -> .kilo/agents/<slug>.md (one per mode) | |
| # | |
| # KILO CODE PATH REFERENCE (VSCode extension): | |
| # Global agents: ~/.config/kilo/agents/<slug>.md | |
| # Global commands: ~/.config/kilo/commands/<name>.md | |
| # Global rules: ~/.config/kilo/rules/<name>.md (+ kilo.jsonc instructions) | |
| # Global skills: ~/.kilo/skills/<name>/SKILL.md | |
| # Global MCP: [VSCode globalStorage]/kilocode.kilo-code/settings/mcp_settings.json | |
| # Project agents: .kilo/agents/<slug>.md | |
| # Project commands: .kilo/commands/<name>.md | |
| # Project rules: .kilo/rules/<name>.md (+ kilo.jsonc instructions) | |
| # Project skills: .kilo/skills/<name>/SKILL.md | |
| # Project MCP: .kilo/mcp.json | |
| # | |
| # REQUIREMENTS: | |
| # - bash 4+ | |
| # - python3 with PyYAML (`pip3 install pyyaml`) for .roomodes / custom_modes.yaml | |
| # Falls back to JSON parsing if PyYAML is absent (works for custom_modes.json) | |
| # ============================================================================= | |
| set -euo pipefail | |
| # ── colours ────────────────────────────────────────────────────────────────── | |
| RED='\033[0;31m' | |
| GREEN='\033[0;32m' | |
| YELLOW='\033[1;33m' | |
| BLUE='\033[0;34m' | |
| CYAN='\033[0;36m' | |
| BOLD='\033[1m' | |
| RESET='\033[0m' | |
| # ── flags ───────────────────────────────────────────────────────────────────── | |
| DO_GLOBAL=true | |
| DO_PROJECT=true | |
| DRY_RUN=false | |
| BACKUP=true | |
| SKIP_EXISTING=false | |
| # ── counters ────────────────────────────────────────────────────────────────── | |
| COPIED=0 | |
| SKIPPED=0 | |
| WARNINGS=0 | |
| # ── helpers ─────────────────────────────────────────────────────────────────── | |
| log_info() { echo -e "${BLUE}[INFO]${RESET} $*"; } | |
| log_ok() { echo -e "${GREEN}[OK]${RESET} $*"; } | |
| log_skip() { echo -e "${YELLOW}[SKIP]${RESET} $*"; } | |
| log_warn() { echo -e "${YELLOW}[WARN]${RESET} $*"; WARNINGS=$((WARNINGS+1)); } | |
| log_dry() { echo -e "${CYAN}[DRY]${RESET} $*"; } | |
| log_section() { echo -e "\n${BOLD}${BLUE}== $* ==${RESET}"; } | |
| die() { echo -e "${RED}[ERROR]${RESET} $*" >&2; exit 1; } | |
| usage() { | |
| sed -n '/^# USAGE/,/^# ====/p' "$0" | grep -v '^# ====' | sed 's/^# \?//' | |
| exit 0 | |
| } | |
| # ── arg parsing ─────────────────────────────────────────────────────────────── | |
| for arg in "$@"; do | |
| case "$arg" in | |
| --global-only) DO_PROJECT=false ;; | |
| --project-only) DO_GLOBAL=false ;; | |
| --dry-run) DRY_RUN=true ;; | |
| --no-backup) BACKUP=false ;; | |
| --skip-existing) SKIP_EXISTING=true ;; | |
| --help|-h) usage ;; | |
| *) die "Unknown option: $arg. Use --help for usage." ;; | |
| esac | |
| done | |
| # ── platform detection ──────────────────────────────────────────────────────── | |
| OS="$(uname -s)" | |
| case "$OS" in | |
| Darwin) | |
| VSCODE_GLOBAL_STORAGE="$HOME/Library/Application Support/Code/User/globalStorage" | |
| ;; | |
| Linux) | |
| VSCODE_GLOBAL_STORAGE="${XDG_DATA_HOME:-$HOME/.local/share}/Code/User/globalStorage" | |
| ;; | |
| MINGW*|MSYS*|CYGWIN*) | |
| VSCODE_GLOBAL_STORAGE="${APPDATA}/Code/User/globalStorage" | |
| ;; | |
| *) | |
| log_warn "Unknown OS '$OS'; assuming Linux-style paths for VSCode storage" | |
| VSCODE_GLOBAL_STORAGE="$HOME/.local/share/Code/User/globalStorage" | |
| ;; | |
| esac | |
| # Roo Code paths | |
| ROO_VSCODE_DIR="$VSCODE_GLOBAL_STORAGE/rooveterinaryinc.roo-cline/settings" | |
| ROO_GLOBAL_DIR="$HOME/.roo" | |
| # Kilo Code paths | |
| KILO_VSCODE_DIR="$VSCODE_GLOBAL_STORAGE/kilocode.kilo-code/settings" | |
| KILO_CONFIG_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/kilo" # rules, commands, agents, kilo.jsonc | |
| KILO_HOME_DIR="$HOME/.kilo" # skills | |
| PROJECT_ROO_DIR=".roo" | |
| PROJECT_KILO_DIR=".kilo" | |
| # Built-in Kilo agent slugs to skip during modes migration | |
| BUILTIN_SLUGS=("code" "build" "architect" "plan" "ask" "debug" "orchestrator" "explore" "general") | |
| # Temp dir for Python helper script (cleaned up on exit) | |
| TMPDIR_SCRIPT="$(mktemp -d)" | |
| PYTHON_HELPER="$TMPDIR_SCRIPT/modes_converter.py" | |
| cleanup() { rm -rf "$TMPDIR_SCRIPT"; } | |
| trap cleanup EXIT | |
| # ── write Python helper to tempfile ────────────────────────────────────────── | |
| write_python_helper() { | |
| cat > "$PYTHON_HELPER" << 'PYEOF' | |
| #!/usr/bin/env python3 | |
| """ | |
| Roo modes -> Kilo agents converter. | |
| Usage: python3 modes_converter.py <yaml_file> <output_dir> <dry_run> <skip_existing> [builtin_slug ...] | |
| Writes one <slug>.md file per custom mode into output_dir. | |
| Prints __RESULT__ copied=N skipped=N to stdout on completion. | |
| All other messages go to stderr. | |
| """ | |
| import sys | |
| import os | |
| import re | |
| yaml_file = sys.argv[1] | |
| output_dir = sys.argv[2] | |
| dry_run = sys.argv[3].lower() == "true" | |
| skip_existing = sys.argv[4].lower() == "true" | |
| builtin_slugs = set(sys.argv[5:]) | |
| # ── YAML loading ────────────────────────────────────────────────────────────── | |
| try: | |
| import yaml | |
| def load_file(path): | |
| with open(path, "r", encoding="utf-8") as f: | |
| return yaml.safe_load(f) | |
| except ImportError: | |
| import json | |
| def load_file(path): | |
| with open(path, "r", encoding="utf-8") as f: | |
| raw = f.read() | |
| for attempt in [raw, re.sub(r"//[^\n]*", "", raw)]: | |
| try: | |
| return json.loads(attempt) | |
| except json.JSONDecodeError: | |
| pass | |
| print( | |
| f"[WARN] PyYAML not installed and {path} is not valid JSON. " | |
| "Install PyYAML: pip3 install pyyaml", | |
| file=sys.stderr, | |
| ) | |
| return None | |
| data = load_file(yaml_file) | |
| if data is None: | |
| print("__RESULT__ copied=0 skipped=0") | |
| sys.exit(0) | |
| modes = [] | |
| if isinstance(data, dict): | |
| modes = data.get("customModes", []) | |
| elif isinstance(data, list): | |
| modes = data | |
| if not modes: | |
| print(f"[INFO] No customModes found in {yaml_file}", file=sys.stderr) | |
| print("__RESULT__ copied=0 skipped=0") | |
| sys.exit(0) | |
| copied = 0 | |
| skipped = 0 | |
| def yaml_str(s): | |
| """Wrap value in YAML double-quotes, escaping internal quotes.""" | |
| if not s: | |
| return '""' | |
| escaped = s.replace("\\", "\\\\").replace('"', '\\"') | |
| return f'"{escaped}"' | |
| for mode in modes: | |
| if not isinstance(mode, dict): | |
| continue | |
| slug = (mode.get("slug") or "").strip() | |
| if not slug: | |
| print(f"[WARN] Mode missing slug, skipping: {mode}", file=sys.stderr) | |
| continue | |
| if slug in builtin_slugs: | |
| print(f"[SKIP] Built-in slug '{slug}' -> maps to Kilo built-in, skipping", file=sys.stderr) | |
| skipped += 1 | |
| continue | |
| description = (mode.get("description") or "").strip() | |
| when_to_use = (mode.get("whenToUse") or "").strip() | |
| role_definition = (mode.get("roleDefinition") or "").strip() | |
| custom_instruct = (mode.get("customInstructions") or "").strip() | |
| # Combine description + whenToUse | |
| combined = description | |
| if when_to_use and when_to_use != description: | |
| combined = (combined + " " + when_to_use).strip() if combined else when_to_use | |
| # Build frontmatter | |
| fm = ["---", f"description: {yaml_str(combined)}", "mode: primary"] | |
| # Convert groups -> permission (best-effort) | |
| groups = mode.get("groups", []) | |
| perm_lines = [] | |
| for g in groups: | |
| if isinstance(g, str): | |
| g_name, g_cfg = g, None | |
| elif isinstance(g, list) and g: | |
| g_name = g[0] | |
| g_cfg = g[1] if len(g) > 1 else None | |
| else: | |
| continue | |
| if g_name == "read": | |
| perm_lines.append(" read: allow") | |
| elif g_name == "edit": | |
| if isinstance(g_cfg, dict) and g_cfg.get("fileRegex"): | |
| regex = g_cfg["fileRegex"] | |
| # Rough regex -> glob: strip anchors, unescape dots | |
| glob = regex.replace("\\.", ".").rstrip("$").lstrip("^") | |
| perm_lines.append(" edit:") | |
| perm_lines.append(f' "{glob}": allow') | |
| perm_lines.append(' "*": deny') | |
| else: | |
| perm_lines.append(" edit: allow") | |
| elif g_name == "command": | |
| perm_lines.append(" bash: allow") | |
| # mcp, browser -> no direct Kilo permission mapping needed | |
| if perm_lines: | |
| fm.append("permission:") | |
| fm.extend(perm_lines) | |
| fm.append("---") | |
| frontmatter = "\n".join(fm) | |
| # Build markdown body | |
| body_parts = [] | |
| if role_definition: | |
| body_parts.append(role_definition) | |
| if custom_instruct: | |
| body_parts.append("\n## Custom Instructions\n") | |
| body_parts.append(custom_instruct) | |
| body = "\n".join(body_parts).strip() | |
| content = frontmatter + "\n" + body + "\n" | |
| dest = os.path.join(output_dir, f"{slug}.md") | |
| if dry_run: | |
| print(f"[DRY] would write -> {dest}", file=sys.stderr) | |
| copied += 1 | |
| continue | |
| if os.path.exists(dest) and skip_existing: | |
| print(f"[SKIP] Exists: {dest}", file=sys.stderr) | |
| skipped += 1 | |
| continue | |
| os.makedirs(output_dir, exist_ok=True) | |
| with open(dest, "w", encoding="utf-8") as f: | |
| f.write(content) | |
| print(f"[OK] wrote -> {dest}", file=sys.stderr) | |
| copied += 1 | |
| print(f"__RESULT__ copied={copied} skipped={skipped}") | |
| PYEOF | |
| } | |
| # ── dependency checks ───────────────────────────────────────────────────────── | |
| check_python() { | |
| if ! command -v python3 &>/dev/null; then | |
| die "python3 is required for YAML parsing of .roomodes / custom_modes.yaml." | |
| fi | |
| } | |
| # ── core copy helpers ───────────────────────────────────────────────────────── | |
| copy_file() { | |
| local src="$1" | |
| local dest="$2" | |
| if [[ ! -f "$src" ]]; then | |
| log_warn "Source not found, skipping: $src" | |
| return | |
| fi | |
| if [[ "$DRY_RUN" == true ]]; then | |
| log_dry "cp $src -> $dest" | |
| COPIED=$((COPIED+1)) | |
| return | |
| fi | |
| mkdir -p "$(dirname "$dest")" | |
| if [[ -f "$dest" ]]; then | |
| if [[ "$SKIP_EXISTING" == true ]]; then | |
| log_skip "Exists: $dest" | |
| SKIPPED=$((SKIPPED+1)) | |
| return | |
| fi | |
| if [[ "$BACKUP" == true ]]; then | |
| local bak="${dest}.roo-backup-$(date +%Y%m%d-%H%M%S)" | |
| cp "$dest" "$bak" | |
| log_info " backed up -> $bak" | |
| fi | |
| fi | |
| cp "$src" "$dest" | |
| log_ok "$src -> $dest" | |
| COPIED=$((COPIED+1)) | |
| } | |
| copy_dir() { | |
| local src_dir="$1" | |
| local dest_dir="$2" | |
| if [[ ! -d "$src_dir" ]]; then | |
| log_warn "Source dir not found, skipping: $src_dir" | |
| return | |
| fi | |
| while IFS= read -r -d '' src_file; do | |
| [[ "$(basename "$src_file")" == ".DS_Store" ]] && continue | |
| local rel="${src_file#$src_dir/}" | |
| copy_file "$src_file" "$dest_dir/$rel" | |
| done < <(find "$src_dir" -type f -print0 2>/dev/null) | |
| } | |
| # ── kilo.jsonc helpers ──────────────────────────────────────────────────────── | |
| # Generates or merges the instructions array in a kilo.jsonc file. | |
| # upsert_kilo_jsonc <jsonc_file> <glob_pattern> | |
| upsert_kilo_jsonc() { | |
| local jsonc_file="$1" | |
| local glob_pattern="$2" | |
| if [[ "$DRY_RUN" == true ]]; then | |
| log_dry "upsert instructions: \"$glob_pattern\" in $jsonc_file" | |
| return | |
| fi | |
| mkdir -p "$(dirname "$jsonc_file")" | |
| if [[ ! -f "$jsonc_file" ]]; then | |
| printf '{\n "instructions": ["%s"]\n}\n' "$glob_pattern" > "$jsonc_file" | |
| log_ok "created $jsonc_file with instructions: [\"$glob_pattern\"]" | |
| return | |
| fi | |
| # File exists — check if pattern is already present | |
| if grep -qF "\"$glob_pattern\"" "$jsonc_file" 2>/dev/null; then | |
| log_info " $jsonc_file already contains \"$glob_pattern\"" | |
| return | |
| fi | |
| # Inject into existing instructions array using python3 | |
| python3 - "$jsonc_file" "$glob_pattern" << 'PYJSONC' | |
| import sys, re, json | |
| path = sys.argv[1] | |
| pattern = sys.argv[2] | |
| with open(path, "r", encoding="utf-8") as f: | |
| raw = f.read() | |
| # Strip single-line comments for parsing (JSONC) | |
| stripped = re.sub(r"//[^\n]*", "", raw) | |
| try: | |
| data = json.loads(stripped) | |
| except json.JSONDecodeError as e: | |
| print(f"[WARN] Could not parse {path} as JSONC: {e}", file=sys.stderr) | |
| print(f"[WARN] Please manually add \"{pattern}\" to the instructions array.", file=sys.stderr) | |
| sys.exit(0) | |
| instructions = data.get("instructions", []) | |
| if pattern not in instructions: | |
| instructions.append(pattern) | |
| data["instructions"] = instructions | |
| with open(path, "w", encoding="utf-8") as f: | |
| json.dump(data, f, indent=2) | |
| f.write("\n") | |
| print(f"[OK] updated {path}: added \"{pattern}\" to instructions") | |
| else: | |
| print(f"[INFO] {path} already contains \"{pattern}\"") | |
| PYJSONC | |
| } | |
| # ── modes -> agents conversion ──────────────────────────────────────────────── | |
| # run_modes_conversion <yaml_file> <output_dir> <label> | |
| run_modes_conversion() { | |
| local yaml_file="$1" | |
| local output_dir="$2" | |
| local label="$3" | |
| if [[ ! -f "$yaml_file" ]]; then | |
| log_warn "Modes file not found, skipping: $yaml_file" | |
| return | |
| fi | |
| check_python | |
| log_info "$label" | |
| # Write Python helper to tempfile if not already done | |
| if [[ ! -f "$PYTHON_HELPER" ]]; then | |
| write_python_helper | |
| fi | |
| # Run the helper; capture its stdout (result line), let stderr flow to terminal | |
| local result_line | |
| result_line="$(python3 "$PYTHON_HELPER" \ | |
| "$yaml_file" "$output_dir" "$DRY_RUN" "$SKIP_EXISTING" \ | |
| "${BUILTIN_SLUGS[@]}" 2>&1 1>/tmp/kilo_modes_result.txt | cat >&2; cat /tmp/kilo_modes_result.txt)" | |
| if [[ -n "$result_line" ]]; then | |
| local c s | |
| c="$(echo "$result_line" | grep -oE 'copied=[0-9]+' | grep -oE '[0-9]+' || echo 0)" | |
| s="$(echo "$result_line" | grep -oE 'skipped=[0-9]+' | grep -oE '[0-9]+' || echo 0)" | |
| COPIED=$((COPIED + ${c:-0})) | |
| SKIPPED=$((SKIPPED + ${s:-0})) | |
| fi | |
| } | |
| # ── section: global migration ───────────────────────────────────────────────── | |
| migrate_global() { | |
| log_section "GLOBAL MIGRATION" | |
| if [[ ! -d "$ROO_GLOBAL_DIR" ]]; then | |
| log_warn "Global Roo directory not found: $ROO_GLOBAL_DIR" | |
| else | |
| # rules -> ~/.config/kilo/rules/ + register in global kilo.jsonc | |
| if [[ -d "$ROO_GLOBAL_DIR/rules" ]]; then | |
| log_info "Global rules: $ROO_GLOBAL_DIR/rules/ -> $KILO_CONFIG_DIR/rules/" | |
| copy_dir "$ROO_GLOBAL_DIR/rules" "$KILO_CONFIG_DIR/rules" | |
| upsert_kilo_jsonc "$KILO_CONFIG_DIR/kilo.jsonc" "~/.config/kilo/rules/*.md" | |
| fi | |
| # rules-*/ (mode-specific rule dirs) | |
| while IFS= read -r -d '' rules_dir; do | |
| local dir_name | |
| dir_name="$(basename "$rules_dir")" | |
| log_info "Mode-specific global rules: $rules_dir -> $KILO_CONFIG_DIR/$dir_name/" | |
| copy_dir "$rules_dir" "$KILO_CONFIG_DIR/$dir_name" | |
| done < <(find "$ROO_GLOBAL_DIR" -maxdepth 1 -type d -name 'rules-*' -print0 2>/dev/null) | |
| # commands -> ~/.config/kilo/commands/ (Kilo "slash-command workflows") | |
| if [[ -d "$ROO_GLOBAL_DIR/commands" ]]; then | |
| log_info "Global commands: $ROO_GLOBAL_DIR/commands/ -> $KILO_CONFIG_DIR/commands/" | |
| copy_dir "$ROO_GLOBAL_DIR/commands" "$KILO_CONFIG_DIR/commands" | |
| fi | |
| # skills -> ~/.kilo/skills/ | |
| if [[ -d "$ROO_GLOBAL_DIR/skills" ]]; then | |
| log_info "Global skills: $ROO_GLOBAL_DIR/skills/ -> $KILO_HOME_DIR/skills/" | |
| copy_dir "$ROO_GLOBAL_DIR/skills" "$KILO_HOME_DIR/skills" | |
| fi | |
| fi | |
| # VSCode globalStorage: MCP settings | |
| if [[ -f "$ROO_VSCODE_DIR/mcp_settings.json" ]]; then | |
| log_info "Global MCP settings -> $KILO_VSCODE_DIR/mcp_settings.json" | |
| [[ "$DRY_RUN" == false ]] && mkdir -p "$KILO_VSCODE_DIR" | |
| copy_file "$ROO_VSCODE_DIR/mcp_settings.json" "$KILO_VSCODE_DIR/mcp_settings.json" | |
| else | |
| log_warn "Global MCP settings not found: $ROO_VSCODE_DIR/mcp_settings.json" | |
| fi | |
| # VSCode globalStorage: custom modes -> ~/.config/kilo/agents/<slug>.md | |
| local kilo_global_agents="$KILO_CONFIG_DIR/agents" | |
| for modes_file in custom_modes.yaml custom_modes.json; do | |
| local src="$ROO_VSCODE_DIR/$modes_file" | |
| if [[ -f "$src" ]]; then | |
| run_modes_conversion "$src" "$kilo_global_agents" \ | |
| "Global modes ($modes_file) -> $kilo_global_agents/<slug>.md" | |
| fi | |
| done | |
| # Also process exported mode YAML files from ~/.roo/modes/ | |
| if [[ -d "$ROO_GLOBAL_DIR/modes" ]]; then | |
| while IFS= read -r -d '' yaml_file; do | |
| local fname | |
| fname="$(basename "$yaml_file")" | |
| run_modes_conversion "$yaml_file" "$kilo_global_agents" \ | |
| "Global exported mode ($fname) -> $kilo_global_agents/<slug>.md" | |
| done < <(find "$ROO_GLOBAL_DIR/modes" -type f \( -name "*.yaml" -o -name "*.yml" -o -name "*.json" \) -print0 2>/dev/null) | |
| fi | |
| } | |
| # ── section: project migration ──────────────────────────────────────────────── | |
| migrate_project() { | |
| log_section "PROJECT MIGRATION ($(pwd))" | |
| if [[ ! -d "$PROJECT_ROO_DIR" ]] && [[ ! -f ".roomodes" ]]; then | |
| log_warn "No .roo/ directory or .roomodes file found in current directory." | |
| log_warn "Run this script from the root of a Roo Code project." | |
| return | |
| fi | |
| local project_jsonc="kilo.jsonc" | |
| # .roo/mcp.json -> .kilo/mcp.json | |
| if [[ -f "$PROJECT_ROO_DIR/mcp.json" ]]; then | |
| log_info "Project MCP: .roo/mcp.json -> .kilo/mcp.json" | |
| copy_file "$PROJECT_ROO_DIR/mcp.json" "$PROJECT_KILO_DIR/mcp.json" | |
| fi | |
| # .roo/rules/ -> .kilo/rules/ + register in kilo.jsonc | |
| if [[ -d "$PROJECT_ROO_DIR/rules" ]]; then | |
| log_info "Project rules: .roo/rules/ -> .kilo/rules/" | |
| copy_dir "$PROJECT_ROO_DIR/rules" "$PROJECT_KILO_DIR/rules" | |
| upsert_kilo_jsonc "$project_jsonc" ".kilo/rules/*.md" | |
| fi | |
| # .roo/rules-*/ -> .kilo/rules-*/ | |
| while IFS= read -r -d '' rules_dir; do | |
| local dir_name | |
| dir_name="$(basename "$rules_dir")" | |
| log_info "Mode-specific project rules: $rules_dir -> $PROJECT_KILO_DIR/$dir_name/" | |
| copy_dir "$rules_dir" "$PROJECT_KILO_DIR/$dir_name" | |
| done < <(find "$PROJECT_ROO_DIR" -maxdepth 1 -type d -name 'rules-*' -print0 2>/dev/null) | |
| # .roo/commands/ -> .kilo/commands/ (Kilo slash-command workflows) | |
| if [[ -d "$PROJECT_ROO_DIR/commands" ]]; then | |
| log_info "Project commands: .roo/commands/ -> .kilo/commands/" | |
| copy_dir "$PROJECT_ROO_DIR/commands" "$PROJECT_KILO_DIR/commands" | |
| fi | |
| # .roo/skills/ -> .kilo/skills/ | |
| if [[ -d "$PROJECT_ROO_DIR/skills" ]]; then | |
| log_info "Project skills: .roo/skills/ -> .kilo/skills/" | |
| copy_dir "$PROJECT_ROO_DIR/skills" "$PROJECT_KILO_DIR/skills" | |
| fi | |
| # .roomodes -> .kilo/agents/<slug>.md (one per mode) | |
| if [[ -f ".roomodes" ]]; then | |
| run_modes_conversion ".roomodes" "$PROJECT_KILO_DIR/agents" \ | |
| ".roomodes -> .kilo/agents/<slug>.md" | |
| fi | |
| } | |
| # ── summary ─────────────────────────────────────────────────────────────────── | |
| print_summary() { | |
| echo "" | |
| echo -e "${BOLD}== MIGRATION SUMMARY ==${RESET}" | |
| [[ "$DRY_RUN" == true ]] && echo -e " ${CYAN}Mode:${RESET} DRY RUN (no files written)" | |
| echo -e " ${GREEN}Copied:${RESET} $COPIED file(s)" | |
| echo -e " ${YELLOW}Skipped:${RESET} $SKIPPED file(s)" | |
| [[ $WARNINGS -gt 0 ]] && echo -e " ${YELLOW}Warnings:${RESET} $WARNINGS (see above)" | |
| echo "" | |
| if [[ "$DRY_RUN" == false && $COPIED -gt 0 ]]; then | |
| echo -e "${GREEN}Migration complete!${RESET}" | |
| echo "" | |
| echo -e "${BOLD}Kilo Code config locations:${RESET}" | |
| if [[ "$DO_GLOBAL" == true ]]; then | |
| echo " Global rules: $KILO_CONFIG_DIR/rules/" | |
| echo " Global commands: $KILO_CONFIG_DIR/commands/" | |
| echo " Global agents: $KILO_CONFIG_DIR/agents/" | |
| echo " Global skills: $KILO_HOME_DIR/skills/" | |
| echo " Global config: $KILO_CONFIG_DIR/kilo.jsonc" | |
| echo " Global MCP: $KILO_VSCODE_DIR/mcp_settings.json" | |
| fi | |
| if [[ "$DO_PROJECT" == true ]]; then | |
| echo " Project rules: .kilo/rules/" | |
| echo " Project commands: .kilo/commands/" | |
| echo " Project agents: .kilo/agents/" | |
| echo " Project skills: .kilo/skills/" | |
| echo " Project MCP: .kilo/mcp.json" | |
| echo " Project config: kilo.jsonc" | |
| fi | |
| echo "" | |
| echo -e "${BOLD}Next steps:${RESET}" | |
| echo " 1. Reload VS Code (Cmd+Shift+P -> 'Developer: Reload Window')." | |
| echo " 2. Open Kilo Code and verify agents appear in the agent picker." | |
| echo " 3. Check kilo.jsonc 'instructions' array lists your rule files." | |
| echo " 4. Review .kilo/agents/*.md — permissions are best-effort" | |
| echo " conversions from Roo groups; adjust as needed." | |
| echo " 5. Invoke commands with /command-name in Kilo Code chat." | |
| echo " 6. Your original Roo Code config is untouched." | |
| [[ "$BACKUP" == true ]] && echo " 7. Backup files (*.roo-backup-*) were created for pre-existing files." | |
| echo "" | |
| echo -e "${BOLD}Skipped built-in slugs:${RESET} ${BUILTIN_SLUGS[*]}" | |
| echo " These map to Kilo's built-in agents. Override via kilo.jsonc if needed." | |
| fi | |
| } | |
| # ── main ────────────────────────────────────────────────────────────────────── | |
| main() { | |
| echo -e "${BOLD}Roo Code -> Kilo Code Migration Script${RESET}" | |
| echo "OS: $OS" | |
| echo "VSCode storage: $VSCODE_GLOBAL_STORAGE" | |
| echo "Kilo config dir: $KILO_CONFIG_DIR" | |
| echo "Kilo home dir: $KILO_HOME_DIR (skills)" | |
| [[ "$DRY_RUN" == true ]] && echo -e "${CYAN}[DRY RUN MODE - no files will be written]${RESET}" | |
| echo "" | |
| [[ "$DO_GLOBAL" == true ]] && migrate_global | |
| [[ "$DO_PROJECT" == true ]] && migrate_project | |
| print_summary | |
| } | |
| main |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment