diff --git a/.claude/CHANGELOG.md b/.claude/CHANGELOG.md index bd71ec1..7f7f96b 100644 --- a/.claude/CHANGELOG.md +++ b/.claude/CHANGELOG.md @@ -5,6 +5,45 @@ Each entry: **what** changed, **why** it was needed, **files** touched. --- +## 2026-05-25 — v0.5.0 — OpenCode CLI + plugin marketplaces (GSD/gstack/superpowers) + +**What** +- `profiles/cli-bundle/05b-opencode.sh` — installs OpenCode via upstream `curl -fsSL https://opencode.ai/install | bash`. Adds `~/.opencode/bin` and `~/.local/bin` to PATH via `~/.bashrc`. Toggle: `INSTALL_OPENCODE`. +- `lib/base-packages.sh` — new `install_bun()` (required by gstack). +- `lib/plugins.sh` — `claude_headless`, `install_claude_plugin`, `install_gstack_for`, `print_manual_install_hint`. +- `profiles/cli-bundle/09-plugins.sh` — orchestrates the three plugins across the bundle. +- `tests/test_plugins.bats` — 4 new unit tests (stubbed `claude` binary). +- `.env.example` — toggles + per-plugin per-CLI subtoggles. + +**Plugin coverage matrix** +| Plugin | Claude | Codex | Antigravity | Cursor | OpenCode | +|--------------|----------|------------------|------------------------------|----------------------|----------------------| +| GSD | headless | — | — | — | — | +| gstack | headless | — | — | — | headless (`--host opencode`) | +| superpowers | headless | manual `/plugins`| manual (NOT documented) | manual `/add-plugin` | manual fetch URL | + +**Decisions** +- **Antigravity superpowers**: option (b) from prior planning — no blind attempt. Prints manual hint with a Gemini-CLI-pattern guess + warning that upstream docs don't cover Antigravity. +- **gstack runtime**: requires Bun. `install_bun()` lives in `base-packages.sh` (reusable) rather than `plugins.sh`; only invoked when `INSTALL_GSTACK=true`. +- **Headless plugin install for Claude**: uses `claude -p "/plugin install ..." --dangerously-skip-permissions`. Other CLIs lack equivalent flags; we deliberately do not script keystrokes against interactive prompts. +- **gstack targets**: list-based (`GSTACK_TARGETS="claude opencode"`) instead of per-target booleans. Extensible to other supported hosts (Cursor, Codex, Hermes, etc.) without env-var explosion. + +**Why** +- The three plugins shape how every CLI session behaves (slash commands, agent roles, workflows). Manual install on every fresh VPS is the worst kind of toil. +- OpenCode joins the bundle because gstack and superpowers both support it and the install path is the same shape as the other CLIs (single curl|bash). + +**Files** +- `profiles/cli-bundle/05b-opencode.sh` (new) +- `profiles/cli-bundle/09-plugins.sh` (new) +- `profiles/cli-bundle/install.sh` (calls 05b + 09) +- `profiles/cli-bundle/.env.example` (`INSTALL_OPENCODE`, plugin toggles) +- `profiles/cli-bundle/README.md` (Plugins section) +- `lib/plugins.sh` (new) +- `lib/base-packages.sh` (`install_bun`) +- `tests/test_plugins.bats` (new, 4 tests) + +--- + ## 2026-05-25 — v0.4.0 — Obsidian vault for cli-bundle agents **What** diff --git a/.claude/TODO.md b/.claude/TODO.md index 1e20e33..0f03bcd 100644 --- a/.claude/TODO.md +++ b/.claude/TODO.md @@ -30,6 +30,14 @@ - [ ] Per-CLI auth verification (`claude --version`, `codex --version`, etc.) gated by toggle. - [ ] Document tmux session naming convention when running multiple CLIs. +## Plugins (v0.5.0+) + +- [ ] **Antigravity + superpowers**: confirm whether `antigravity extensions install ` is a real command. Replace manual hint with headless install if confirmed. +- [ ] gstack `./setup` — confirm non-interactive behaviour on fresh box (`--yes`-style flag?). Currently we assume it runs cleanly without prompts. +- [ ] GSD: legacy npm package conflict check (`get-shit-done-cc`, `get-shit-done-redux`) — auto-uninstall in 09-plugins.sh if found. +- [ ] Superpowers OpenCode install is "fetch instructions" — investigate whether OpenCode has a headless prompt API to drive it programmatically. +- [ ] Plugin verification: after install, run a sanity check (e.g. `claude -p "/plugin list"` and grep for the installed names). + ## Obsidian vault (v0.4.0+) - [ ] Workstation-side companion: doc snippet for installing Obsidian app + git pull of the same vault. diff --git a/lib/base-packages.sh b/lib/base-packages.sh index 0fcc1f1..5aae7a8 100644 --- a/lib/base-packages.sh +++ b/lib/base-packages.sh @@ -110,6 +110,25 @@ install_pnpm() { pnpm -v } +# Install Bun (JS runtime; required by gstack plugin). +install_bun() { + if command -v bun >/dev/null 2>&1; then + echo "==> bun $(bun --version) already installed" + return 0 + fi + echo "==> Installing bun (bun.sh)" + curl -fsSL https://bun.sh/install | bash + export PATH="$HOME/.bun/bin:$PATH" + if ! grep -q 'BUN_INSTALL' "$HOME/.bashrc" 2>/dev/null; then + cat >> "$HOME/.bashrc" <<'EOF' + +# Bun +export BUN_INSTALL="$HOME/.bun" +case ":$PATH:" in *":$BUN_INSTALL/bin:"*) ;; *) export PATH="$BUN_INSTALL/bin:$PATH";; esac +EOF + fi +} + # Install uv (Python package manager used by Hermes). install_uv() { if command -v uv >/dev/null 2>&1; then diff --git a/lib/plugins.sh b/lib/plugins.sh new file mode 100644 index 0000000..fa782eb --- /dev/null +++ b/lib/plugins.sh @@ -0,0 +1,80 @@ +#!/usr/bin/env bash +# ============================================================ +# lib/plugins.sh — install Claude Code plugins (and friends) headlessly. +# +# Most plugin markets are interactive (`/plugin install …` typed inside a +# CLI session). Where headless is supported, we drive it via `claude -p` +# with --dangerously-skip-permissions. Elsewhere we print a manual hint +# the operator pastes into the relevant CLI. +# ============================================================ + +# Run a Claude Code slash command headless and return its exit code. +# $1 = prompt (typically a slash command like `/plugin install foo`) +claude_headless() { + local prompt="$1" + if ! command -v claude >/dev/null 2>&1; then + echo "ERROR: 'claude' not on PATH — install Claude Code first." + return 1 + fi + claude -p "$prompt" --dangerously-skip-permissions +} + +# Install a Claude Code plugin from a marketplace. +# $1 = marketplace spec (e.g. "jnuyens/gsd-plugin" or "obra/superpowers-marketplace") +# $2 = plugin spec (e.g. "gsd@gsd-plugin" or "superpowers@claude-plugins-official") +install_claude_plugin() { + local marketplace="$1" + local plugin="$2" + echo "==> Claude plugin: marketplace=$marketplace plugin=$plugin" + # Marketplace add is idempotent in Claude Code; install is too (re-runs fine). + claude_headless "/plugin marketplace add $marketplace" || true + claude_headless "/plugin install $plugin" + claude_headless "/reload-plugins" || true +} + +# Install gstack into a target CLI's skills dir. +# $1 = host name (claude, opencode, etc.) +install_gstack_for() { + local host="$1" + local target_dir="$HOME/.${host}/skills/gstack" + echo "==> Installing gstack for $host → $target_dir" + + if [[ -d "$target_dir/.git" ]]; then + git -C "$target_dir" fetch --depth 1 origin + git -C "$target_dir" reset --hard origin/HEAD + else + git clone --single-branch --depth 1 \ + https://github.com/garrytan/gstack.git "$target_dir" + fi + + pushd "$target_dir" >/dev/null || return 1 + if [[ -x ./setup ]]; then + if [[ "$host" == "claude" ]]; then + ./setup + else + ./setup --host "$host" + fi + else + echo "WARN: gstack/setup not found or not executable" + fi + popd >/dev/null || return 1 +} + +# Tell the operator exactly what to type into a non-headless CLI. +print_manual_install_hint() { + local cli="$1" + local instruction="$2" + cat < OpenCode install disabled (INSTALL_OPENCODE != true). Skipping." + exit 0 +fi + +echo "==> Installing OpenCode CLI (upstream installer)" +curl -fsSL https://opencode.ai/install | bash + +# Installer typically drops binary in ~/.opencode/bin or ~/.local/bin. +if ! grep -q 'OPENCODE_BIN' "$HOME/.bashrc" 2>/dev/null; then + cat >> "$HOME/.bashrc" <<'EOF' + +# OpenCode CLI +export OPENCODE_BIN="$HOME/.opencode/bin" +case ":$PATH:" in *":$OPENCODE_BIN:"*) ;; *) export PATH="$OPENCODE_BIN:$HOME/.local/bin:$PATH";; esac +EOF +fi +export PATH="$HOME/.opencode/bin:$HOME/.local/bin:$PATH" + +opencode --version 2>/dev/null || echo " (opencode binary not yet on PATH — run \`source ~/.bashrc\`)" + +echo "==> OpenCode CLI installed. Configure with: opencode auth" diff --git a/profiles/cli-bundle/09-plugins.sh b/profiles/cli-bundle/09-plugins.sh new file mode 100644 index 0000000..616fd0d --- /dev/null +++ b/profiles/cli-bundle/09-plugins.sh @@ -0,0 +1,98 @@ +#!/usr/bin/env bash +# ============================================================ +# 09-plugins.sh — install GSD, gstack, superpowers across the CLIs. +# +# Coverage matrix: +# GSD → Claude only +# gstack → Claude, OpenCode (and any host listed in GSTACK_TARGETS) +# superpowers → Claude (headless), Codex/Cursor/OpenCode (manual hints), +# Antigravity (option (b) per RECOMMENDATIONS: no attempt, +# just print hint with the warning that it is not officially +# documented). +# ============================================================ +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" +ENV_FILE="$SCRIPT_DIR/.env" + +if [[ ! -f "$ENV_FILE" ]]; then + echo "ERROR: $ENV_FILE not found." + exit 1 +fi + +# shellcheck disable=SC1090 +set -a; source "$ENV_FILE"; set +a + +# shellcheck source=../../lib/plugins.sh +source "$REPO_ROOT/lib/plugins.sh" +# shellcheck source=../../lib/base-packages.sh +source "$REPO_ROOT/lib/base-packages.sh" + +export PATH="$HOME/.npm-global/bin:$HOME/.local/bin:$HOME/.opencode/bin:$HOME/.bun/bin:$PATH" + +# --- GSD (Claude only) ---------------------------------------------------- +if [[ "${INSTALL_GSD:-false}" == "true" ]]; then + if [[ "${INSTALL_CLAUDE:-true}" != "true" ]]; then + echo "WARN: INSTALL_GSD=true but Claude not installed — skipping." + else + install_claude_plugin "jnuyens/gsd-plugin" "gsd@gsd-plugin" + fi +fi + +# --- gstack (Claude + any host in GSTACK_TARGETS) ------------------------- +if [[ "${INSTALL_GSTACK:-false}" == "true" ]]; then + install_bun + TARGETS="${GSTACK_TARGETS:-claude}" + for host in $TARGETS; do + case "$host" in + claude) + if [[ "${INSTALL_CLAUDE:-true}" != "true" ]]; then + echo "WARN: gstack target 'claude' requested but INSTALL_CLAUDE != true. Skipping." + continue + fi + ;; + opencode) + if [[ "${INSTALL_OPENCODE:-false}" != "true" ]]; then + echo "WARN: gstack target 'opencode' requested but INSTALL_OPENCODE != true. Skipping." + continue + fi + ;; + esac + install_gstack_for "$host" + done +fi + +# --- superpowers (multi-CLI) ---------------------------------------------- +if [[ "${INSTALL_SUPERPOWERS:-false}" == "true" ]]; then + + if [[ "${SUPERPOWERS_CLAUDE:-true}" == "true" && "${INSTALL_CLAUDE:-true}" == "true" ]]; then + install_claude_plugin "obra/superpowers-marketplace" "superpowers@superpowers-marketplace" + fi + + if [[ "${SUPERPOWERS_CODEX:-true}" == "true" && "${INSTALL_CODEX:-false}" == "true" ]]; then + print_manual_install_hint "Codex CLI" \ + "/plugins → search 'superpowers' → Install Plugin" + fi + + if [[ "${SUPERPOWERS_CURSOR:-true}" == "true" && "${INSTALL_CURSOR:-false}" == "true" ]]; then + print_manual_install_hint "Cursor agent CLI" \ + "/add-plugin superpowers" + fi + + if [[ "${SUPERPOWERS_OPENCODE:-true}" == "true" && "${INSTALL_OPENCODE:-false}" == "true" ]]; then + print_manual_install_hint "OpenCode" \ + "Tell OpenCode: Fetch and follow instructions from https://raw.githubusercontent.com/obra/superpowers/refs/heads/main/.opencode/INSTALL.md" + fi + + if [[ "${SUPERPOWERS_ANTIGRAVITY:-false}" == "true" && "${INSTALL_ANTIGRAVITY:-false}" == "true" ]]; then + # Option (b) from .claude/RECOMMENDATIONS-style decision: no blind attempt. + print_manual_install_hint "Google Antigravity CLI" \ + "(NOT OFFICIALLY DOCUMENTED) Try the Gemini-CLI pattern manually: antigravity extensions install https://github.com/obra/superpowers" + echo "WARN: Antigravity superpowers integration is not officially documented." + echo " The hint above mirrors the Gemini CLI pattern — it may or may not work." + fi +fi + +echo +echo "==> Plugins step complete." diff --git a/profiles/cli-bundle/README.md b/profiles/cli-bundle/README.md index 92d057e..41233e1 100644 --- a/profiles/cli-bundle/README.md +++ b/profiles/cli-bundle/README.md @@ -166,6 +166,44 @@ crontab -l | grep -v obsidian-vault-sync | crontab - rm ~/.local/bin/obsidian-vault-sync ``` +## Plugins (GSD, gstack, superpowers) + +Opt-in plugin install handled by `09-plugins.sh`. Headless onde possível; +hint manual onde não. + +| Plugin | Claude | Codex | Antigravity | Cursor | OpenCode | +|--------------|-------------------|-------------------|-----------------------------|-------------------|-------------------| +| GSD | headless | — | — | — | — | +| gstack | headless (`./setup`) | — | — | — | headless (`./setup --host opencode`) | +| superpowers | headless | manual `/plugins` | manual (não documentado) | manual `/add-plugin` | manual fetch URL | + +Toggles no `.env`: +```env +INSTALL_GSD=true # Claude only +INSTALL_GSTACK=true +GSTACK_TARGETS="claude opencode" # ou só "claude" +INSTALL_SUPERPOWERS=true +SUPERPOWERS_CLAUDE=true +SUPERPOWERS_CODEX=true +SUPERPOWERS_CURSOR=true +SUPERPOWERS_OPENCODE=true +SUPERPOWERS_ANTIGRAVITY=false # não documentado oficialmente +``` + +Rodar isolado: +```bash +bash 09-plugins.sh +``` + +**Bun**: gstack precisa de Bun. `09-plugins.sh` instala via `install_bun()` se +`INSTALL_GSTACK=true`. + +**Antigravity superpowers**: docs upstream não cobrem. Toggle imprime hint +manual com palpite (padrão Gemini CLI). Sem tentativa automática. + +**Manual hints**: Codex/Cursor/OpenCode imprimem mensagem com comando exato +pra colar dentro da sessão. Sem CLI flag headless documentada. + ## Manutenção ```bash claude mcp list # ver registrados diff --git a/profiles/cli-bundle/install.sh b/profiles/cli-bundle/install.sh index 45c3215..77fa137 100755 --- a/profiles/cli-bundle/install.sh +++ b/profiles/cli-bundle/install.sh @@ -28,9 +28,11 @@ bash "$SCRIPT_DIR/02-claude.sh" bash "$SCRIPT_DIR/03-codex.sh" bash "$SCRIPT_DIR/04-antigravity.sh" bash "$SCRIPT_DIR/05-cursor.sh" +bash "$SCRIPT_DIR/05b-opencode.sh" bash "$SCRIPT_DIR/08-obsidian.sh" # vault skeleton first; MCP step below registers it bash "$SCRIPT_DIR/06-mcp.sh" bash "$SCRIPT_DIR/07-dream.sh" +bash "$SCRIPT_DIR/09-plugins.sh" # plugin marketplaces (Claude headless; others manual hint) mutex_set "cli-bundle" diff --git a/tests/test_plugins.bats b/tests/test_plugins.bats new file mode 100644 index 0000000..9e1fa04 --- /dev/null +++ b/tests/test_plugins.bats @@ -0,0 +1,68 @@ +#!/usr/bin/env bats +# tests/test_plugins.bats — unit tests for lib/plugins.sh. +# Heavy integration with the real `claude` binary is out of scope here; +# we stub it so we can verify the helpers dispatch correctly. + +setup() { + REPO_ROOT="$(cd "${BATS_TEST_DIRNAME}/.." && pwd)" + TEST_HOME="$(mktemp -d)" + STUB_DIR="$TEST_HOME/bin" + mkdir -p "$STUB_DIR" + export HOME="$TEST_HOME" + export PATH="$STUB_DIR:$PATH" + # shellcheck source=../lib/plugins.sh + source "$REPO_ROOT/lib/plugins.sh" +} + +teardown() { + rm -rf "$TEST_HOME" +} + +# ---------- claude_headless ---------- + +@test "claude_headless: errors when claude missing" { + # Isolate PATH to a dir with no `claude` binary. + PATH="$STUB_DIR" run claude_headless "/help" + [ "$status" -ne 0 ] + [[ "$output" == *"not on PATH"* ]] +} + +@test "claude_headless: dispatches prompt + flag to claude binary" { + cat > "$STUB_DIR/claude" <<'EOF' +#!/usr/bin/env bash +echo "ARGS: $*" +EOF + chmod +x "$STUB_DIR/claude" + run claude_headless "/plugin install foo@bar" + [ "$status" -eq 0 ] + [[ "$output" == *"-p"* ]] + [[ "$output" == *"/plugin install foo@bar"* ]] + [[ "$output" == *"--dangerously-skip-permissions"* ]] +} + +# ---------- install_claude_plugin ---------- + +@test "install_claude_plugin: runs marketplace add then install then reload" { + CALLS_FILE="$TEST_HOME/calls.txt" + cat > "$STUB_DIR/claude" <> "$CALLS_FILE" +EOF + chmod +x "$STUB_DIR/claude" + install_claude_plugin "owner/repo" "name@market" + [ -f "$CALLS_FILE" ] + grep -q "/plugin marketplace add owner/repo" "$CALLS_FILE" + grep -q "/plugin install name@market" "$CALLS_FILE" + grep -q "/reload-plugins" "$CALLS_FILE" +} + +# ---------- print_manual_install_hint ---------- + +@test "print_manual_install_hint: prints CLI name and instruction" { + run print_manual_install_hint "Codex CLI" "/plugins → search" + [ "$status" -eq 0 ] + [[ "$output" == *"Codex CLI"* ]] + [[ "$output" == *"/plugins → search"* ]] + [[ "$output" == *"MANUAL STEP REQUIRED"* ]] +}