Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 8 additions & 29 deletions platform-integrations/INSTALL_SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
## Overview

`install.sh` is a single-file bash/Python hybrid installer that sets up Evolve integrations
into a user's project directory for one or more supported platforms: **Bob**, **Roo**, **Claude**, and **Codex**.
into a user's project directory for one or more supported platforms: **Bob**, **Claude**, and **Codex**.

It is designed to be run:
- Locally from within the evolve repo: `./install.sh install`
Expand Down Expand Up @@ -48,13 +48,13 @@ Commands:
status Show what is currently installed

install options:
--platform {bob,roo,claude,codex,all} Platform to install (default: auto-detect + prompt)
--platform {bob,claude,codex,all} Platform to install (default: auto-detect + prompt)
--mode {lite,full} Installation mode for bob (default: lite)
--dir DIR Target project directory (default: current working dir)
--dry-run Preview changes without modifying files

uninstall options:
--platform {bob,roo,claude,codex,all} Platform to uninstall (default: prompt)
--platform {bob,claude,codex,all} Platform to uninstall (default: prompt)
--dir DIR Target project directory (default: current working dir)
--dry-run Preview changes without modifying files
```
Expand All @@ -68,7 +68,6 @@ Detection checks in order (any match = platform considered available):
| Platform | Detection signals |
|----------|-------------------|
| bob | `.bob/` dir exists in target dir, OR `bob` on PATH |
| roo | `.roomodes` file exists in target dir, OR `roo` or `roo-code` on PATH |
| claude | `.claude/` dir exists in target dir, OR `claude` on PATH |
| codex | `.codex/` dir exists in target dir, OR `.agents/plugins/marketplace.json` exists, OR `codex` on PATH |

Expand Down Expand Up @@ -96,18 +95,6 @@ All of lite mode, plus:
5. Read `platform-integrations/bob/evolve-full/mcp.json`
6. Upsert key `mcpServers.evolve` into `.bob/mcp.json` (JSON key upsert, see JSON Strategy)

### Roo — Lite Mode

Source: `platform-integrations/roo/evolve-lite/`
Target: project directory

1. Copy `skills/evolve-learn/` → `.roo/skills/evolve-learn/` (merge, idempotent)
2. Copy `skills/evolve-recall/` → `.roo/skills/evolve-recall/` (merge, idempotent)
3. Merge mode entry from `skills/.roomodes` → `.roomodes` in project dir
- Target `.roomodes` may be JSON or YAML; detected by trying `json.loads` first
- Upsert by `slug: evolve-lite` (JSON: array upsert; YAML: sentinel block)
- If target does not exist, create as YAML

### Claude — Lite Mode

Source: `platform-integrations/claude/plugins/evolve-lite/`
Expand Down Expand Up @@ -148,11 +135,6 @@ Codex is currently implemented only in lite mode. Full mode is reserved for futu
4. Remove sentinel block for `evolve-lite` from `.bob/custom_modes.yaml`
5. (Full mode) Remove `mcpServers.evolve` key from `.bob/mcp.json`

### Roo
1. Remove `.roo/skills/evolve-learn/`
2. Remove `.roo/skills/evolve-recall/`
3. Remove `evolve-lite` entry from `.roomodes` (JSON array filter or YAML sentinel strip)

### Claude
1. Attempt `claude plugin uninstall evolve-lite` via subprocess
2. If that fails, print manual instructions
Expand All @@ -166,7 +148,7 @@ Codex is currently implemented only in lite mode. Full mode is reserved for futu

## File Operation Strategies

### JSON Strategy (mcp.json, .roomodes, marketplace.json, hooks.json)
### JSON Strategy (mcp.json, marketplace.json, hooks.json)

All JSON writes use atomic read-modify-write:
1. Read existing file (or start with `{}` if not found)
Expand All @@ -176,12 +158,12 @@ All JSON writes use atomic read-modify-write:

**Key upsert** (`mcpServers.evolve`, `hooks.UserPromptSubmit` scaffolding): navigate nested keys via `dict.setdefault`, merge matching dict values in place, and only replace scalar/list leaves.

**Array upsert** (`.roomodes` `customModes`, `marketplace.json` `plugins`): iterate array, find item where the identity key matches,
**Array upsert** (`marketplace.json` `plugins`): iterate array, find item where the identity key matches,
merge matching dict items in place; append if not found.

**Array remove**: filter array by `item["slug"] != target_slug`, write back.

### YAML Strategy (custom_modes.yaml, .roomodes when YAML)
### YAML Strategy (custom_modes.yaml)

YAML files use sentinel comment blocks:

Expand All @@ -201,9 +183,6 @@ between sentinels. If no, append sentinel block to end of file.

**Uninstall**: find sentinel start and end lines, remove all lines between them (inclusive).

**Source parsing**: the source `.roomodes` from `platform-integrations/roo/` is YAML format.
The mode data is extracted via regex from the YAML source and converted to a Python dict
for JSON insertion. No third-party YAML library is required.

---

Expand Down Expand Up @@ -260,7 +239,7 @@ curl -fsSL https://raw.githubusercontent.com/AgentToolkit/altk-evolve/v1.2.0/pla

# Non-interactive, specific platform
curl -fsSL https://raw.githubusercontent.com/AgentToolkit/altk-evolve/main/platform-integrations/install.sh | \
bash -s -- install --platform roo
bash -s -- install --platform bob
```

## Local Install Example
Expand All @@ -271,5 +250,5 @@ curl -fsSL https://raw.githubusercontent.com/AgentToolkit/altk-evolve/main/platf
./platform-integrations/install.sh install --platform bob --mode full
./platform-integrations/install.sh install --platform all
./platform-integrations/install.sh status
./platform-integrations/install.sh uninstall --platform roo
./platform-integrations/install.sh uninstall --platform bob
```
151 changes: 9 additions & 142 deletions platform-integrations/install.sh
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
#!/usr/bin/env bash
# Evolve Platform Installer
# Installs Evolve Lite (and optionally Full) integrations for Bob, Roo, Claude Code, and Codex.
# Installs Evolve Lite (and optionally Full) integrations for Bob, Claude Code, and Codex.
#
# Usage:
# ./install.sh install [--platform bob|roo|claude|codex|all] [--mode lite|full] [--dir DIR] [--dry-run]
# ./install.sh uninstall [--platform bob|roo|claude|codex|all] [--dir DIR] [--dry-run]
# ./install.sh install [--platform bob|claude|codex|all] [--mode lite|full] [--dir DIR] [--dry-run]
# ./install.sh uninstall [--platform bob|claude|codex|all] [--dir DIR] [--dry-run]
# ./install.sh status [--dir DIR]
#
# Remote:
# curl -fsSL https://raw.githubusercontent.com/AgentToolkit/altk-evolve/main/platform-integrations/install.sh | bash
# curl -fsSL https://raw.githubusercontent.com/AgentToolkit/altk-evolve/main/platform-integrations/install.sh | bash -s -- install --platform roo
# curl -fsSL https://raw.githubusercontent.com/AgentToolkit/altk-evolve/main/platform-integrations/install.sh | bash -s -- install --platform bob
#
# Pinned version (SCRIPT_VERSION is substituted by the release process, so the
# script fetched from a tag already knows its own version — no env var needed):
Expand Down Expand Up @@ -142,7 +142,6 @@ EVOLVE_DEBUG = os.environ.get("EVOLVE_DEBUG", "0") == "1"
DRY_RUN = False # set to True by --dry-run flag; checked in all write primitives

BOB_SLUG = "evolve-lite"
ROO_SLUG = "evolve-lite"
CLAUDE_PLUGIN = "evolve-lite"
CODEX_PLUGIN = "evolve-lite"

Expand Down Expand Up @@ -587,57 +586,6 @@ def remove_yaml_custom_mode(target_yaml_path, slug):
atomic_write_text(target_yaml_path, pattern.sub("", text))


def load_roo_mode_from_yaml(source_path):
"""
Extract the evolve-lite mode dict from the Roo source .roomodes YAML file
using regex, returning a dict suitable for JSON insertion.
"""
with open(str(source_path)) as f:
text = f.read()

def extract(field):
# Match simple single-line values at mode definition level (2-4 spaces, optionally with list dash)
# First try to match with list dash (for slug field: " - slug: value")
m = re.search(rf"^ - {field}:\s+(.+)$", text, re.MULTILINE)
if m:
return m.group(1).strip().strip('"')
# Then try without dash (for other fields: " name: value")
m = re.search(rf"^ {field}:\s+(.+)$", text, re.MULTILINE)
if m:
return m.group(1).strip().strip('"')
return ""

def extract_block(field):
# Match block scalar (|- or |) at mode definition level (4 spaces, no list dash)
m = re.search(rf"^ {field}:\s*\|-?\s*\n((?:[ \t]+.+\n?)*)", text, re.MULTILINE)
if m:
lines = m.group(1).splitlines()
# Find minimum indent
min_indent = min((len(l) - len(l.lstrip()) for l in lines if l.strip()), default=0)
return "\n".join(l[min_indent:] for l in lines)
return extract(field)

def extract_list(field):
# Match list field at mode definition level (4 spaces, no list dash before field name)
m = re.search(rf"^ {field}:\s*\n((?: - .+\n?)*)", text, re.MULTILINE)
if m:
return [re.sub(r"^\s+- ", "", l) for l in m.group(1).splitlines() if l.strip()]
return []

slug = extract("slug")
if not slug:
return None

return {
"slug": slug,
"name": extract("name"),
"firstMessage": extract("firstMessage"),
"roleDefinition": extract_block("roleDefinition"),
"customInstructions": extract_block("customInstructions"),
"groups": extract_list("groups"),
"description": extract("description"),
}


# ── Platform detection ─────────────────────────────────────────────────────────

Expand All @@ -648,12 +596,6 @@ def detect_platforms(target_dir):
shutil.which("bob") is not None or
(target / ".bob").is_dir()
),
"roo": (
shutil.which("roo") is not None or
shutil.which("roo-code") is not None or
(target / ".roomodes").is_file() or
(target / ".roo").is_dir()
),
"claude": (
shutil.which("claude") is not None or
(target / ".claude").is_dir()
Expand Down Expand Up @@ -793,75 +735,6 @@ def status_bob(target_dir):
print(f" mcp.json (full mode) : {'✓' if has_mcp else '✗'}")


# ── Roo installer ─────────────────────────────────────────────────────────────

def install_roo(source_dir, target_dir):
roo_source = Path(source_dir) / "platform-integrations" / "roo" / "evolve-lite"
roo_skills_source = roo_source / "skills"

info(f"Installing Roo → {target_dir}")

# Skill directories (exclude .roomodes from copy)
copy_tree(roo_skills_source / "evolve-learn", Path(target_dir) / ".roo" / "skills" / "evolve-learn")
copy_tree(roo_skills_source / "evolve-recall", Path(target_dir) / ".roo" / "skills" / "evolve-recall")
success("Copied Roo skills")

# .roomodes — detect target format
roomodes_source = roo_skills_source / ".roomodes" # YAML format in source
roomodes_target = Path(target_dir) / ".roomodes"

if not roomodes_target.is_file():
# Target doesn't exist — create as YAML (roo-code preferred format)
merge_yaml_custom_mode(roomodes_source, roomodes_target, ROO_SLUG)
success(f"Created {roomodes_target} with mode '{ROO_SLUG}' (YAML)")
elif is_json_file(roomodes_target):
# Target exists and is JSON — load mode from YAML source and upsert into JSON
mode_dict = load_roo_mode_from_yaml(roomodes_source)
if mode_dict is None:
warn(f"Could not extract mode '{ROO_SLUG}' from {roomodes_source}")
else:
upsert_json_array_item(roomodes_target, "customModes", mode_dict, "slug")
success(f"Upserted mode '{ROO_SLUG}' into {roomodes_target} (JSON)")
else:
# Target exists and is YAML — use sentinel merge
merge_yaml_custom_mode(roomodes_source, roomodes_target, ROO_SLUG)
success(f"Merged mode '{ROO_SLUG}' into {roomodes_target} (YAML)")

success("Roo installation complete")


def uninstall_roo(target_dir):
info(f"Uninstalling Roo from {target_dir}")
remove_dir(Path(target_dir) / ".roo" / "skills" / "evolve-learn")
remove_dir(Path(target_dir) / ".roo" / "skills" / "evolve-recall")

roomodes_target = Path(target_dir) / ".roomodes"
if roomodes_target.is_file():
if is_json_file(roomodes_target):
remove_json_array_item(roomodes_target, "customModes", "slug", ROO_SLUG)
else:
remove_yaml_custom_mode(roomodes_target, ROO_SLUG)

success("Roo uninstall complete")


def status_roo(target_dir):
roo_skills = Path(target_dir) / ".roo" / "skills"
roomodes = Path(target_dir) / ".roomodes"
print(f" Roo (.roo/ + .roomodes):")
print(f" .roo/skills/evolve-learn : {'✓' if (roo_skills / 'evolve-learn').is_dir() else '✗'}")
print(f" .roo/skills/evolve-recall : {'✓' if (roo_skills / 'evolve-recall').is_dir() else '✗'}")

mode_present = False
if roomodes.is_file():
if is_json_file(roomodes):
data = read_json(roomodes)
mode_present = any(m.get("slug") == ROO_SLUG for m in data.get("customModes", []))
else:
with open(roomodes) as f:
mode_present = f"slug: {ROO_SLUG}" in f.read()
print(f" .roomodes (evolve-lite) : {'✓' if mode_present else '✗'}")


# ── Claude installer ──────────────────────────────────────────────────────────

Expand Down Expand Up @@ -1033,7 +906,7 @@ def cmd_install(args):

# Resolve platforms
if args.platform == "all":
platforms = ["bob", "roo", "claude", "codex"]
platforms = ["bob", "claude", "codex"]
elif args.platform:
platforms = [args.platform]
else:
Expand All @@ -1054,8 +927,6 @@ def cmd_install(args):
try:
if platform == "bob":
install_bob(SOURCE_DIR, target_dir, mode=args.mode)
elif platform == "roo":
install_roo(SOURCE_DIR, target_dir)
elif platform == "claude":
install_claude(SOURCE_DIR, target_dir)
elif platform == "codex":
Expand Down Expand Up @@ -1087,7 +958,7 @@ def cmd_uninstall(args):
info(_c("35", "DRY RUN — no files will be written or deleted"))

if args.platform == "all":
platforms = ["bob", "roo", "claude", "codex"]
platforms = ["bob", "claude", "codex"]
elif args.platform:
platforms = [args.platform]
else:
Expand All @@ -1100,8 +971,6 @@ def cmd_uninstall(args):
try:
if platform == "bob":
uninstall_bob(target_dir)
elif platform == "roo":
uninstall_roo(target_dir)
elif platform == "claude":
uninstall_claude(target_dir)
elif platform == "codex":
Expand All @@ -1128,8 +997,6 @@ def cmd_status(args):
print()
status_bob(target_dir)
print()
status_roo(target_dir)
print()
status_claude(target_dir)
print()
status_codex(target_dir)
Expand All @@ -1141,14 +1008,14 @@ def cmd_status(args):
def main():
parser = argparse.ArgumentParser(
prog="install.sh",
description="Install Evolve integrations for Bob, Roo, Claude Code, and Codex.",
description="Install Evolve integrations for Bob, Claude Code, and Codex.",
)
sub = parser.add_subparsers(dest="command", required=True)

# install
p_install = sub.add_parser("install", help="Install Evolve into the current project")
p_install.add_argument(
"--platform", choices=["bob", "roo", "claude", "codex", "all"], default=None,
"--platform", choices=["bob", "claude", "codex", "all"], default=None,
Comment thread
coderabbitai[bot] marked this conversation as resolved.
help="Platform to install (default: auto-detect and prompt)",
)
p_install.add_argument(
Expand All @@ -1167,7 +1034,7 @@ def main():
# uninstall
p_uninstall = sub.add_parser("uninstall", help="Remove Evolve from the current project")
p_uninstall.add_argument(
"--platform", choices=["bob", "roo", "claude", "codex", "all"], default=None,
"--platform", choices=["bob", "claude", "codex", "all"], default=None,
help="Platform to uninstall (default: prompt)",
)
p_uninstall.add_argument(
Expand Down
Loading
Loading