diff --git a/CLAUDE.md b/CLAUDE.md index 270a61a..6b8a7d7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,7 +6,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co This is the Ralph for Claude Code repository - an autonomous AI development loop system that enables continuous development cycles with intelligent exit detection and rate limiting. -**Version**: v0.10.1 | **Tests**: 321 passing (100% pass rate) | **CI/CD**: GitHub Actions +**Version**: v0.11.0 | **Tests**: 396 passing (100% pass rate) | **CI/CD**: GitHub Actions ## Core Architecture @@ -22,6 +22,14 @@ The system consists of four main bash scripts and a modular library system: - Uses modern Claude Code CLI with `--output-format json` for structured responses - Implements `detect_response_format()` and `parse_conversion_response()` for JSON parsing - Backward compatible with older CLI versions (automatic text fallback) +6. **ralph_enable.sh** - Interactive wizard for enabling Ralph in existing projects + - Multi-step wizard with environment detection, task source selection, configuration + - Imports tasks from beads, GitHub Issues, or PRD documents + - Generates `.ralphrc` project configuration file +7. **ralph_enable_ci.sh** - Non-interactive version for CI/automation + - Same functionality as interactive version with CLI flags + - JSON output mode for machine parsing + - Exit codes: 0 (success), 1 (error), 2 (already enabled) ### Library Components (lib/) @@ -59,6 +67,23 @@ The system uses a modular architecture with reusable components in the `lib/` di - `portable_timeout()` function for seamless cross-platform execution - Automatic detection with caching for performance +5. **lib/enable_core.sh** - Shared logic for ralph enable commands + - Idempotency checks: `check_existing_ralph()`, `is_ralph_enabled()` + - Safe file operations: `safe_create_file()`, `safe_create_dir()` + - Project detection: `detect_project_context()`, `detect_git_info()`, `detect_task_sources()` + - Template generation: `generate_prompt_md()`, `generate_agent_md()`, `generate_fix_plan_md()`, `generate_ralphrc()` + +6. **lib/wizard_utils.sh** - Interactive prompt utilities for enable wizard + - User prompts: `confirm()`, `prompt_text()`, `prompt_number()` + - Selection utilities: `select_option()`, `select_multiple()`, `select_with_default()` + - Output formatting: `print_header()`, `print_bullet()`, `print_success/warning/error/info()` + +7. **lib/task_sources.sh** - Task import from external sources + - Beads integration: `check_beads_available()`, `fetch_beads_tasks()`, `get_beads_count()` + - GitHub integration: `check_github_available()`, `fetch_github_tasks()`, `get_github_issue_count()` + - PRD extraction: `extract_prd_tasks()`, supports checkbox and numbered list formats + - Task normalization: `normalize_tasks()`, `prioritize_tasks()`, `import_tasks_from_sources()` + ## Key Commands ### Installation @@ -84,6 +109,27 @@ cd existing-project ralph-migrate ``` +### Enabling Ralph in Existing Projects +```bash +# Interactive wizard (recommended for humans) +cd existing-project +ralph-enable + +# With specific task source +ralph-enable --from beads +ralph-enable --from github --label "sprint-1" +ralph-enable --from prd ./docs/requirements.md + +# Force overwrite existing .ralph/ +ralph-enable --force + +# Non-interactive for CI/scripts +ralph-enable-ci # Sensible defaults +ralph-enable-ci --from github # With task source +ralph-enable-ci --project-type typescript # Override detection +ralph-enable-ci --json # Machine-readable output +``` + ### Running the Ralph Loop ```bash # Start with integrated tmux monitoring (recommended) @@ -121,7 +167,7 @@ tmux attach -t ### Running Tests ```bash -# Run all tests (165 tests) +# Run all tests (396 tests) npm test # Run specific test suites @@ -132,6 +178,9 @@ npm run test:integration bats tests/unit/test_cli_parsing.bats bats tests/unit/test_json_parsing.bats bats tests/unit/test_cli_modern.bats +bats tests/unit/test_enable_core.bats +bats tests/unit/test_task_sources.bats +bats tests/unit/test_ralph_enable.bats ``` ## Ralph Loop Configuration @@ -264,10 +313,10 @@ Templates in `templates/` provide starting points for new projects: ## Global Installation Ralph installs to: -- **Commands**: `~/.local/bin/` (ralph, ralph-monitor, ralph-setup, ralph-import, ralph-migrate) +- **Commands**: `~/.local/bin/` (ralph, ralph-monitor, ralph-setup, ralph-import, ralph-migrate, ralph-enable, ralph-enable-ci) - **Templates**: `~/.ralph/templates/` -- **Scripts**: `~/.ralph/` (ralph_loop.sh, ralph_monitor.sh, setup.sh, ralph_import.sh, migrate_to_ralph_folder.sh) -- **Libraries**: `~/.ralph/lib/` (circuit_breaker.sh, response_analyzer.sh, date_utils.sh, timeout_utils.sh) +- **Scripts**: `~/.ralph/` (ralph_loop.sh, ralph_monitor.sh, setup.sh, ralph_import.sh, migrate_to_ralph_folder.sh, ralph_enable.sh, ralph_enable_ci.sh) +- **Libraries**: `~/.ralph/lib/` (circuit_breaker.sh, response_analyzer.sh, date_utils.sh, timeout_utils.sh, enable_core.sh, wizard_utils.sh, task_sources.sh) After installation, the following global commands are available: - `ralph` - Start the autonomous development loop @@ -275,6 +324,8 @@ After installation, the following global commands are available: - `ralph-setup` - Create a new Ralph-managed project - `ralph-import` - Import PRD/specification documents to Ralph format - `ralph-migrate` - Migrate existing projects from flat structure to `.ralph/` subfolder +- `ralph-enable` - Interactive wizard to enable Ralph in existing projects +- `ralph-enable-ci` - Non-interactive version for CI/automation ## Integration Points @@ -351,7 +402,7 @@ Ralph uses advanced error detection with two-stage filtering to eliminate false ## Test Suite -### Test Files (265 tests total) +### Test Files (396 tests total) | File | Tests | Description | |------|-------|-------------| @@ -366,6 +417,9 @@ Ralph uses advanced error detection with two-stage filtering to eliminate false | `test_installation.bats` | 14 | Global installation/uninstall workflows | | `test_project_setup.bats` | 36 | Project setup (setup.sh) validation | | `test_prd_import.bats` | 33 | PRD import (ralph_import.sh) workflows + modern CLI tests | +| `test_enable_core.bats` | 30 | Enable core library (idempotency, project detection, template generation) | +| `test_task_sources.bats` | 23 | Task sources (beads, GitHub, PRD extraction, normalization) | +| `test_ralph_enable.bats` | 22 | Ralph enable integration tests (wizard, CI version, JSON output) | ### Running Tests ```bash @@ -381,6 +435,25 @@ bats tests/unit/test_cli_parsing.bats ## Recent Improvements +### Ralph Enable Command (v0.11.0) +- Added `ralph-enable` interactive wizard for enabling Ralph in existing projects + - 5-phase wizard: Environment Detection → Task Source Selection → Configuration → File Generation → Verification + - Auto-detects project type (TypeScript, Python, Rust, Go) and framework (Next.js, FastAPI, Django) + - Imports tasks from beads, GitHub Issues, or PRD documents + - Generates `.ralphrc` project configuration file +- Added `ralph-enable-ci` non-interactive version for CI/automation + - JSON output mode (`--json`) for machine parsing + - Exit codes: 0 (success), 1 (error), 2 (already enabled) + - Override flags: `--project-name`, `--project-type`, `--from`, `--force` +- New library components: + - `lib/enable_core.sh` - Shared enable logic with idempotency checks + - `lib/wizard_utils.sh` - Interactive prompt utilities + - `lib/task_sources.sh` - Task import from beads/GitHub/PRD +- Updated `ralph_loop.sh` to load `.ralphrc` configuration at startup +- Added 75 new tests (30 enable_core + 23 task_sources + 22 integration) +- Test count: 396 (up from 321) +- Related issues: #85, #121, #64, #87, #99 + ### Stale Completion Indicators Fix (v0.10.1) - Issue #91 - Fixed premature exit caused by stale completion indicators persisting across sessions - Root cause: `.exit_signals` and `.response_analysis` files retained old completion counts diff --git a/install.sh b/install.sh index a5dc65a..3977f86 100755 --- a/install.sh +++ b/install.sh @@ -165,6 +165,28 @@ EOF RALPH_HOME="$HOME/.ralph" exec "$RALPH_HOME/migrate_to_ralph_folder.sh" "$@" +EOF + + # Create ralph-enable command (interactive wizard) + cat > "$INSTALL_DIR/ralph-enable" << 'EOF' +#!/bin/bash +# Ralph Enable - Interactive Wizard for Existing Projects +# Adds Ralph configuration to an existing codebase + +RALPH_HOME="$HOME/.ralph" + +exec "$RALPH_HOME/ralph_enable.sh" "$@" +EOF + + # Create ralph-enable-ci command (non-interactive) + cat > "$INSTALL_DIR/ralph-enable-ci" << 'EOF' +#!/bin/bash +# Ralph Enable CI - Non-Interactive Version for Automation +# Adds Ralph configuration with sensible defaults + +RALPH_HOME="$HOME/.ralph" + +exec "$RALPH_HOME/ralph_enable_ci.sh" "$@" EOF # Copy actual script files to Ralph home with modifications for global operation @@ -176,15 +198,23 @@ EOF # Copy migration script to Ralph home cp "$SCRIPT_DIR/migrate_to_ralph_folder.sh" "$RALPH_HOME/" + # Copy enable scripts to Ralph home + cp "$SCRIPT_DIR/ralph_enable.sh" "$RALPH_HOME/" + cp "$SCRIPT_DIR/ralph_enable_ci.sh" "$RALPH_HOME/" + # Make all commands executable chmod +x "$INSTALL_DIR/ralph" chmod +x "$INSTALL_DIR/ralph-monitor" chmod +x "$INSTALL_DIR/ralph-setup" chmod +x "$INSTALL_DIR/ralph-import" chmod +x "$INSTALL_DIR/ralph-migrate" + chmod +x "$INSTALL_DIR/ralph-enable" + chmod +x "$INSTALL_DIR/ralph-enable-ci" chmod +x "$RALPH_HOME/ralph_monitor.sh" chmod +x "$RALPH_HOME/ralph_import.sh" chmod +x "$RALPH_HOME/migrate_to_ralph_folder.sh" + chmod +x "$RALPH_HOME/ralph_enable.sh" + chmod +x "$RALPH_HOME/ralph_enable_ci.sh" chmod +x "$RALPH_HOME/lib/"*.sh log "SUCCESS" "Ralph scripts installed to $INSTALL_DIR" @@ -291,6 +321,8 @@ main() { echo " ralph --monitor # Start Ralph with integrated monitoring" echo " ralph --help # Show Ralph options" echo " ralph-setup my-project # Create new Ralph project" + echo " ralph-enable # Enable Ralph in existing project (interactive)" + echo " ralph-enable-ci # Enable Ralph in existing project (non-interactive)" echo " ralph-import prd.md # Convert PRD to Ralph project" echo " ralph-migrate # Migrate existing project to .ralph/ structure" echo " ralph-monitor # Manual monitoring dashboard" @@ -314,7 +346,7 @@ case "${1:-install}" in ;; uninstall) log "INFO" "Uninstalling Ralph for Claude Code..." - rm -f "$INSTALL_DIR/ralph" "$INSTALL_DIR/ralph-monitor" "$INSTALL_DIR/ralph-setup" "$INSTALL_DIR/ralph-import" "$INSTALL_DIR/ralph-migrate" + rm -f "$INSTALL_DIR/ralph" "$INSTALL_DIR/ralph-monitor" "$INSTALL_DIR/ralph-setup" "$INSTALL_DIR/ralph-import" "$INSTALL_DIR/ralph-migrate" "$INSTALL_DIR/ralph-enable" "$INSTALL_DIR/ralph-enable-ci" rm -rf "$RALPH_HOME" log "SUCCESS" "Ralph for Claude Code uninstalled" ;; diff --git a/lib/enable_core.sh b/lib/enable_core.sh new file mode 100755 index 0000000..0dc278d --- /dev/null +++ b/lib/enable_core.sh @@ -0,0 +1,815 @@ +#!/usr/bin/env bash + +# enable_core.sh - Shared logic for ralph enable commands +# Provides idempotency checks, safe file creation, and project detection +# +# Used by: +# - ralph_enable.sh (interactive wizard) +# - ralph_enable_ci.sh (non-interactive CI version) + +# Exit codes - specific codes for different failure types +export ENABLE_SUCCESS=0 # Successful completion +export ENABLE_ERROR=1 # General error +export ENABLE_ALREADY_ENABLED=2 # Ralph already enabled (use --force) +export ENABLE_INVALID_ARGS=3 # Invalid command line arguments +export ENABLE_FILE_NOT_FOUND=4 # Required file not found (e.g., PRD file) +export ENABLE_DEPENDENCY_MISSING=5 # Required dependency missing (e.g., jq for --json) +export ENABLE_PERMISSION_DENIED=6 # Cannot create files/directories + +# Colors (can be disabled for non-interactive mode) +export ENABLE_USE_COLORS="${ENABLE_USE_COLORS:-true}" + +_color() { + if [[ "$ENABLE_USE_COLORS" == "true" ]]; then + echo -e "$1" + else + echo -e "$2" + fi +} + +# Color codes +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +CYAN='\033[0;36m' +NC='\033[0m' + +# Logging function +enable_log() { + local level=$1 + local message=$2 + local color="" + + case $level in + "INFO") color=$BLUE ;; + "WARN") color=$YELLOW ;; + "ERROR") color=$RED ;; + "SUCCESS") color=$GREEN ;; + "SKIP") color=$CYAN ;; + esac + + if [[ "$ENABLE_USE_COLORS" == "true" ]]; then + echo -e "${color}[$level]${NC} $message" + else + echo "[$level] $message" + fi +} + +# ============================================================================= +# IDEMPOTENCY CHECKS +# ============================================================================= + +# check_existing_ralph - Check if .ralph directory exists and its state +# +# Returns: +# 0 - No .ralph directory, safe to proceed +# 1 - .ralph exists but incomplete (partial setup) +# 2 - .ralph exists and fully initialized +# +# Outputs: +# Sets global RALPH_STATE: "none" | "partial" | "complete" +# Sets global RALPH_MISSING_FILES: array of missing files if partial +# +check_existing_ralph() { + RALPH_STATE="none" + RALPH_MISSING_FILES=() + + if [[ ! -d ".ralph" ]]; then + RALPH_STATE="none" + return 0 + fi + + # Check for required files + local required_files=( + ".ralph/PROMPT.md" + ".ralph/@fix_plan.md" + ".ralph/@AGENT.md" + ) + + local missing=() + local found=0 + + for file in "${required_files[@]}"; do + if [[ -f "$file" ]]; then + found=$((found + 1)) + else + missing+=("$file") + fi + done + + RALPH_MISSING_FILES=("${missing[@]}") + + if [[ $found -eq 0 ]]; then + RALPH_STATE="none" + return 0 + elif [[ ${#missing[@]} -gt 0 ]]; then + RALPH_STATE="partial" + return 1 + else + RALPH_STATE="complete" + return 2 + fi +} + +# is_ralph_enabled - Simple check if Ralph is fully enabled +# +# Returns: +# 0 - Ralph is fully enabled +# 1 - Ralph is not enabled or only partially +# +is_ralph_enabled() { + check_existing_ralph || true + [[ "$RALPH_STATE" == "complete" ]] +} + +# ============================================================================= +# SAFE FILE OPERATIONS +# ============================================================================= + +# safe_create_file - Create a file only if it doesn't exist (or force overwrite) +# +# Parameters: +# $1 (target) - Target file path +# $2 (content) - Content to write (can be empty string) +# +# Environment: +# ENABLE_FORCE - If "true", overwrites existing files instead of skipping +# +# Returns: +# 0 - File created/overwritten successfully +# 1 - File already exists (skipped, only when ENABLE_FORCE is not true) +# 2 - Error creating file +# +# Side effects: +# Logs [CREATE], [OVERWRITE], or [SKIP] message +# +safe_create_file() { + local target=$1 + local content=$2 + local force="${ENABLE_FORCE:-false}" + + if [[ -f "$target" ]]; then + if [[ "$force" == "true" ]]; then + # Force mode: overwrite existing file + enable_log "INFO" "Overwriting $target (--force)" + else + # Normal mode: skip existing file + enable_log "SKIP" "$target already exists" + return 1 + fi + fi + + # Create parent directory if needed + local parent_dir + parent_dir=$(dirname "$target") + if [[ ! -d "$parent_dir" ]]; then + if ! mkdir -p "$parent_dir" 2>/dev/null; then + enable_log "ERROR" "Failed to create directory: $parent_dir" + return 2 + fi + fi + + # Write content to file using printf to avoid shell injection + # printf '%s\n' is safer than echo for arbitrary content (handles backslashes, -n, etc.) + if printf '%s\n' "$content" > "$target" 2>/dev/null; then + if [[ -f "$target" ]] && [[ "$force" == "true" ]]; then + enable_log "SUCCESS" "Overwrote $target" + else + enable_log "SUCCESS" "Created $target" + fi + return 0 + else + enable_log "ERROR" "Failed to create: $target" + return 2 + fi +} + +# safe_create_dir - Create a directory only if it doesn't exist +# +# Parameters: +# $1 (target) - Target directory path +# +# Returns: +# 0 - Directory created or already exists +# 1 - Error creating directory +# +safe_create_dir() { + local target=$1 + + if [[ -d "$target" ]]; then + return 0 + fi + + if mkdir -p "$target" 2>/dev/null; then + enable_log "SUCCESS" "Created directory: $target" + return 0 + else + enable_log "ERROR" "Failed to create directory: $target" + return 1 + fi +} + +# ============================================================================= +# DIRECTORY STRUCTURE +# ============================================================================= + +# create_ralph_structure - Create the .ralph/ directory structure +# +# Creates: +# .ralph/ +# .ralph/specs/ +# .ralph/examples/ +# .ralph/logs/ +# .ralph/docs/generated/ +# +# Returns: +# 0 - Structure created successfully +# 1 - Error creating structure +# +create_ralph_structure() { + local dirs=( + ".ralph" + ".ralph/specs" + ".ralph/examples" + ".ralph/logs" + ".ralph/docs/generated" + ) + + for dir in "${dirs[@]}"; do + if ! safe_create_dir "$dir"; then + return 1 + fi + done + + return 0 +} + +# ============================================================================= +# PROJECT DETECTION +# ============================================================================= + +# Exported detection results +export DETECTED_PROJECT_NAME="" +export DETECTED_PROJECT_TYPE="" +export DETECTED_FRAMEWORK="" +export DETECTED_BUILD_CMD="" +export DETECTED_TEST_CMD="" +export DETECTED_RUN_CMD="" + +# detect_project_context - Detect project type, name, and build commands +# +# Detects: +# - Project type: javascript, typescript, python, rust, go, unknown +# - Framework: nextjs, fastapi, express, etc. +# - Build/test/run commands based on detected tooling +# +# Sets globals: +# DETECTED_PROJECT_NAME - Project name (from package.json, folder, etc.) +# DETECTED_PROJECT_TYPE - Language/type +# DETECTED_FRAMEWORK - Framework if detected +# DETECTED_BUILD_CMD - Build command +# DETECTED_TEST_CMD - Test command +# DETECTED_RUN_CMD - Run/start command +# +detect_project_context() { + # Reset detection results + DETECTED_PROJECT_NAME="" + DETECTED_PROJECT_TYPE="unknown" + DETECTED_FRAMEWORK="" + DETECTED_BUILD_CMD="" + DETECTED_TEST_CMD="" + DETECTED_RUN_CMD="" + + # Detect from package.json (JavaScript/TypeScript) + if [[ -f "package.json" ]]; then + DETECTED_PROJECT_TYPE="javascript" + + # Check for TypeScript + if grep -q '"typescript"' package.json 2>/dev/null || \ + [[ -f "tsconfig.json" ]]; then + DETECTED_PROJECT_TYPE="typescript" + fi + + # Extract project name + if command -v jq &>/dev/null; then + DETECTED_PROJECT_NAME=$(jq -r '.name // empty' package.json 2>/dev/null) + else + # Fallback: grep for name field + DETECTED_PROJECT_NAME=$(grep -m1 '"name"' package.json | sed 's/.*: *"\([^"]*\)".*/\1/' 2>/dev/null) + fi + + # Detect framework + if grep -q '"next"' package.json 2>/dev/null; then + DETECTED_FRAMEWORK="nextjs" + elif grep -q '"express"' package.json 2>/dev/null; then + DETECTED_FRAMEWORK="express" + elif grep -q '"react"' package.json 2>/dev/null; then + DETECTED_FRAMEWORK="react" + elif grep -q '"vue"' package.json 2>/dev/null; then + DETECTED_FRAMEWORK="vue" + fi + + # Set build commands + DETECTED_BUILD_CMD="npm run build" + DETECTED_TEST_CMD="npm test" + DETECTED_RUN_CMD="npm start" + + # Check for yarn + if [[ -f "yarn.lock" ]]; then + DETECTED_BUILD_CMD="yarn build" + DETECTED_TEST_CMD="yarn test" + DETECTED_RUN_CMD="yarn start" + fi + + # Check for pnpm + if [[ -f "pnpm-lock.yaml" ]]; then + DETECTED_BUILD_CMD="pnpm build" + DETECTED_TEST_CMD="pnpm test" + DETECTED_RUN_CMD="pnpm start" + fi + fi + + # Detect from pyproject.toml or setup.py (Python) + if [[ -f "pyproject.toml" ]] || [[ -f "setup.py" ]]; then + DETECTED_PROJECT_TYPE="python" + + # Extract project name from pyproject.toml + if [[ -f "pyproject.toml" ]]; then + DETECTED_PROJECT_NAME=$(grep -m1 '^name' pyproject.toml | sed 's/.*= *"\([^"]*\)".*/\1/' 2>/dev/null) + + # Detect framework + if grep -q 'fastapi' pyproject.toml 2>/dev/null; then + DETECTED_FRAMEWORK="fastapi" + elif grep -q 'django' pyproject.toml 2>/dev/null; then + DETECTED_FRAMEWORK="django" + elif grep -q 'flask' pyproject.toml 2>/dev/null; then + DETECTED_FRAMEWORK="flask" + fi + fi + + # Set build commands (prefer uv if detected) + if [[ -f "uv.lock" ]] || command -v uv &>/dev/null; then + DETECTED_BUILD_CMD="uv sync" + DETECTED_TEST_CMD="uv run pytest" + DETECTED_RUN_CMD="uv run python -m ${DETECTED_PROJECT_NAME:-main}" + else + DETECTED_BUILD_CMD="pip install -e ." + DETECTED_TEST_CMD="pytest" + DETECTED_RUN_CMD="python -m ${DETECTED_PROJECT_NAME:-main}" + fi + fi + + # Detect from Cargo.toml (Rust) + if [[ -f "Cargo.toml" ]]; then + DETECTED_PROJECT_TYPE="rust" + DETECTED_PROJECT_NAME=$(grep -m1 '^name' Cargo.toml | sed 's/.*= *"\([^"]*\)".*/\1/' 2>/dev/null) + DETECTED_BUILD_CMD="cargo build" + DETECTED_TEST_CMD="cargo test" + DETECTED_RUN_CMD="cargo run" + fi + + # Detect from go.mod (Go) + if [[ -f "go.mod" ]]; then + DETECTED_PROJECT_TYPE="go" + DETECTED_PROJECT_NAME=$(head -1 go.mod | sed 's/module //' 2>/dev/null) + DETECTED_BUILD_CMD="go build" + DETECTED_TEST_CMD="go test ./..." + DETECTED_RUN_CMD="go run ." + fi + + # Fallback project name to folder name + if [[ -z "$DETECTED_PROJECT_NAME" ]]; then + DETECTED_PROJECT_NAME=$(basename "$(pwd)") + fi +} + +# detect_git_info - Detect git repository information +# +# Sets globals: +# DETECTED_GIT_REPO - true if in git repo +# DETECTED_GIT_REMOTE - Remote URL (origin) +# DETECTED_GIT_GITHUB - true if GitHub remote +# +export DETECTED_GIT_REPO="false" +export DETECTED_GIT_REMOTE="" +export DETECTED_GIT_GITHUB="false" + +detect_git_info() { + DETECTED_GIT_REPO="false" + DETECTED_GIT_REMOTE="" + DETECTED_GIT_GITHUB="false" + + # Check if in git repo + if git rev-parse --git-dir &>/dev/null; then + DETECTED_GIT_REPO="true" + + # Get remote URL + DETECTED_GIT_REMOTE=$(git remote get-url origin 2>/dev/null || echo "") + + # Check if GitHub + if [[ "$DETECTED_GIT_REMOTE" == *"github.com"* ]]; then + DETECTED_GIT_GITHUB="true" + fi + fi +} + +# detect_task_sources - Detect available task sources +# +# Sets globals: +# DETECTED_BEADS_AVAILABLE - true if .beads directory exists +# DETECTED_GITHUB_AVAILABLE - true if GitHub remote detected +# DETECTED_PRD_FILES - Array of potential PRD files found +# +export DETECTED_BEADS_AVAILABLE="false" +export DETECTED_GITHUB_AVAILABLE="false" +declare -a DETECTED_PRD_FILES=() + +detect_task_sources() { + DETECTED_BEADS_AVAILABLE="false" + DETECTED_GITHUB_AVAILABLE="false" + DETECTED_PRD_FILES=() + + # Check for beads + if [[ -d ".beads" ]]; then + DETECTED_BEADS_AVAILABLE="true" + fi + + # Check for GitHub (reuse git detection) + detect_git_info + DETECTED_GITHUB_AVAILABLE="$DETECTED_GIT_GITHUB" + + # Search for PRD/spec files + local search_dirs=("docs" "specs" "." "requirements") + local prd_patterns=("*prd*.md" "*PRD*.md" "*requirements*.md" "*spec*.md" "*specification*.md") + + for dir in "${search_dirs[@]}"; do + if [[ -d "$dir" ]]; then + for pattern in "${prd_patterns[@]}"; do + while IFS= read -r -d '' file; do + DETECTED_PRD_FILES+=("$file") + done < <(find "$dir" -maxdepth 2 -name "$pattern" -print0 2>/dev/null) + done + fi + done +} + +# ============================================================================= +# TEMPLATE GENERATION +# ============================================================================= + +# get_templates_dir - Get the templates directory path +# +# Returns: +# Echoes the path to templates directory +# Returns 1 if not found +# +get_templates_dir() { + # Check global installation first + if [[ -d "$HOME/.ralph/templates" ]]; then + echo "$HOME/.ralph/templates" + return 0 + fi + + # Check local installation (development) + local script_dir + script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + if [[ -d "$script_dir/../templates" ]]; then + echo "$script_dir/../templates" + return 0 + fi + + return 1 +} + +# generate_prompt_md - Generate PROMPT.md with project context +# +# Parameters: +# $1 (project_name) - Project name +# $2 (project_type) - Project type (typescript, python, etc.) +# $3 (framework) - Framework if any (optional) +# $4 (objectives) - Custom objectives (optional, newline-separated) +# +# Outputs to stdout +# +generate_prompt_md() { + local project_name="${1:-$(basename "$(pwd)")}" + local project_type="${2:-unknown}" + local framework="${3:-}" + local objectives="${4:-}" + + local framework_line="" + if [[ -n "$framework" ]]; then + framework_line="**Framework:** $framework" + fi + + local objectives_section="" + if [[ -n "$objectives" ]]; then + objectives_section="$objectives" + else + objectives_section="- Review the codebase and understand the current state +- Follow tasks in @fix_plan.md +- Implement one task per loop +- Write tests for new functionality +- Update documentation as needed" + fi + + cat << PROMPTEOF +# Ralph Development Instructions + +## Context +You are Ralph, an autonomous AI development agent working on the **${project_name}** project. + +**Project Type:** ${project_type} +${framework_line} + +## Current Objectives +${objectives_section} + +## Key Principles +- ONE task per loop - focus on the most important thing +- Search the codebase before assuming something isn't implemented +- Write comprehensive tests with clear documentation +- Update @fix_plan.md with your learnings +- Commit working changes with descriptive messages + +## Testing Guidelines +- LIMIT testing to ~20% of your total effort per loop +- PRIORITIZE: Implementation > Documentation > Tests +- Only write tests for NEW functionality you implement + +## Build & Run +See @AGENT.md for build and run instructions. + +## Status Reporting (CRITICAL) + +At the end of your response, ALWAYS include this status block: + +\`\`\` +---RALPH_STATUS--- +STATUS: IN_PROGRESS | COMPLETE | BLOCKED +TASKS_COMPLETED_THIS_LOOP: +FILES_MODIFIED: +TESTS_STATUS: PASSING | FAILING | NOT_RUN +WORK_TYPE: IMPLEMENTATION | TESTING | DOCUMENTATION | REFACTORING +EXIT_SIGNAL: false | true +RECOMMENDATION: +---END_RALPH_STATUS--- +\`\`\` + +## Current Task +Follow @fix_plan.md and choose the most important item to implement next. +PROMPTEOF +} + +# generate_agent_md - Generate @AGENT.md with detected build commands +# +# Parameters: +# $1 (build_cmd) - Build command +# $2 (test_cmd) - Test command +# $3 (run_cmd) - Run command +# +# Outputs to stdout +# +generate_agent_md() { + local build_cmd="${1:-echo 'No build command configured'}" + local test_cmd="${2:-echo 'No test command configured'}" + local run_cmd="${3:-echo 'No run command configured'}" + + cat << AGENTEOF +# Ralph Agent Configuration + +## Build Instructions + +\`\`\`bash +# Build the project +${build_cmd} +\`\`\` + +## Test Instructions + +\`\`\`bash +# Run tests +${test_cmd} +\`\`\` + +## Run Instructions + +\`\`\`bash +# Start/run the project +${run_cmd} +\`\`\` + +## Notes +- Update this file when build process changes +- Add environment setup instructions as needed +- Include any pre-requisites or dependencies +AGENTEOF +} + +# generate_fix_plan_md - Generate @fix_plan.md with imported tasks +# +# Parameters: +# $1 (tasks) - Tasks to include (newline-separated, markdown checkbox format) +# +# Outputs to stdout +# +generate_fix_plan_md() { + local tasks="${1:-}" + + local high_priority="" + local medium_priority="" + local low_priority="" + + if [[ -n "$tasks" ]]; then + high_priority="$tasks" + else + high_priority="- [ ] Review codebase and understand architecture +- [ ] Identify and document key components +- [ ] Set up development environment" + medium_priority="- [ ] Implement core features +- [ ] Add test coverage +- [ ] Update documentation" + low_priority="- [ ] Performance optimization +- [ ] Code cleanup and refactoring" + fi + + cat << FIXPLANEOF +# Ralph Fix Plan + +## High Priority +${high_priority} + +## Medium Priority +${medium_priority} + +## Low Priority +${low_priority} + +## Completed +- [x] Project enabled for Ralph + +## Notes +- Focus on MVP functionality first +- Ensure each feature is properly tested +- Update this file after each major milestone +FIXPLANEOF +} + +# generate_ralphrc - Generate .ralphrc configuration file +# +# Parameters: +# $1 (project_name) - Project name +# $2 (project_type) - Project type +# $3 (task_sources) - Task sources (local, beads, github) +# +# Outputs to stdout +# +generate_ralphrc() { + local project_name="${1:-$(basename "$(pwd)")}" + local project_type="${2:-unknown}" + local task_sources="${3:-local}" + + cat << RALPHRCEOF +# .ralphrc - Ralph project configuration +# Generated by: ralph enable +# Documentation: https://github.com/frankbria/ralph-claude-code + +# Project identification +PROJECT_NAME="${project_name}" +PROJECT_TYPE="${project_type}" + +# Loop settings +MAX_CALLS_PER_HOUR=100 +CLAUDE_TIMEOUT_MINUTES=15 +CLAUDE_OUTPUT_FORMAT="json" + +# Tool permissions +# Comma-separated list of allowed tools +ALLOWED_TOOLS="Write,Read,Edit,Bash(git *),Bash(npm *),Bash(pytest)" + +# Session management +SESSION_CONTINUITY=true +SESSION_EXPIRY_HOURS=24 + +# Task sources (for ralph enable --sync) +# Options: local, beads, github (comma-separated for multiple) +TASK_SOURCES="${task_sources}" +GITHUB_TASK_LABEL="ralph-task" +BEADS_FILTER="status:open" + +# Circuit breaker thresholds +CB_NO_PROGRESS_THRESHOLD=3 +CB_SAME_ERROR_THRESHOLD=5 +CB_OUTPUT_DECLINE_THRESHOLD=70 +RALPHRCEOF +} + +# ============================================================================= +# MAIN ENABLE LOGIC +# ============================================================================= + +# enable_ralph_in_directory - Main function to enable Ralph in current directory +# +# Parameters: +# $1 (options) - JSON-like options string or empty +# force: true/false - Force overwrite existing +# skip_tasks: true/false - Skip task import +# project_name: string - Override project name +# task_content: string - Pre-imported task content +# +# Returns: +# 0 - Success +# 1 - Error +# 2 - Already enabled (and no force flag) +# +enable_ralph_in_directory() { + local force="${ENABLE_FORCE:-false}" + local skip_tasks="${ENABLE_SKIP_TASKS:-false}" + local project_name="${ENABLE_PROJECT_NAME:-}" + local project_type="${ENABLE_PROJECT_TYPE:-}" + local task_content="${ENABLE_TASK_CONTENT:-}" + + # Check existing state (use || true to prevent set -e from exiting) + check_existing_ralph || true + + if [[ "$RALPH_STATE" == "complete" && "$force" != "true" ]]; then + enable_log "INFO" "Ralph is already enabled in this project" + enable_log "INFO" "Use --force to overwrite existing configuration" + return $ENABLE_ALREADY_ENABLED + fi + + # Detect project context + detect_project_context + + # Use detected or provided project name + if [[ -z "$project_name" ]]; then + project_name="$DETECTED_PROJECT_NAME" + fi + + # Use detected or provided project type + if [[ -n "$project_type" ]]; then + DETECTED_PROJECT_TYPE="$project_type" + fi + + enable_log "INFO" "Enabling Ralph for: $project_name" + enable_log "INFO" "Project type: $DETECTED_PROJECT_TYPE" + if [[ -n "$DETECTED_FRAMEWORK" ]]; then + enable_log "INFO" "Framework: $DETECTED_FRAMEWORK" + fi + + # Create directory structure + if ! create_ralph_structure; then + enable_log "ERROR" "Failed to create .ralph/ structure" + return $ENABLE_ERROR + fi + + # Generate and create files + local prompt_content + prompt_content=$(generate_prompt_md "$project_name" "$DETECTED_PROJECT_TYPE" "$DETECTED_FRAMEWORK") + safe_create_file ".ralph/PROMPT.md" "$prompt_content" + + local agent_content + agent_content=$(generate_agent_md "$DETECTED_BUILD_CMD" "$DETECTED_TEST_CMD" "$DETECTED_RUN_CMD") + safe_create_file ".ralph/@AGENT.md" "$agent_content" + + local fix_plan_content + fix_plan_content=$(generate_fix_plan_md "$task_content") + safe_create_file ".ralph/@fix_plan.md" "$fix_plan_content" + + # Detect task sources for .ralphrc + detect_task_sources + local task_sources="local" + if [[ "$DETECTED_BEADS_AVAILABLE" == "true" ]]; then + task_sources="beads,$task_sources" + fi + if [[ "$DETECTED_GITHUB_AVAILABLE" == "true" ]]; then + task_sources="github,$task_sources" + fi + + # Generate .ralphrc + local ralphrc_content + ralphrc_content=$(generate_ralphrc "$project_name" "$DETECTED_PROJECT_TYPE" "$task_sources") + safe_create_file ".ralphrc" "$ralphrc_content" + + enable_log "SUCCESS" "Ralph enabled successfully!" + + return $ENABLE_SUCCESS +} + +# Export functions for use in other scripts +export -f enable_log +export -f check_existing_ralph +export -f is_ralph_enabled +export -f safe_create_file +export -f safe_create_dir +export -f create_ralph_structure +export -f detect_project_context +export -f detect_git_info +export -f detect_task_sources +export -f get_templates_dir +export -f generate_prompt_md +export -f generate_agent_md +export -f generate_fix_plan_md +export -f generate_ralphrc +export -f enable_ralph_in_directory diff --git a/lib/task_sources.sh b/lib/task_sources.sh new file mode 100755 index 0000000..2824046 --- /dev/null +++ b/lib/task_sources.sh @@ -0,0 +1,550 @@ +#!/usr/bin/env bash + +# task_sources.sh - Task import utilities for Ralph enable +# Supports importing tasks from beads, GitHub Issues, and PRD files + +# ============================================================================= +# BEADS INTEGRATION +# ============================================================================= + +# check_beads_available - Check if beads (bd) is available and configured +# +# Returns: +# 0 - Beads available +# 1 - Beads not available or not configured +# +check_beads_available() { + # Check for .beads directory + if [[ ! -d ".beads" ]]; then + return 1 + fi + + # Check if bd command exists + if ! command -v bd &>/dev/null; then + return 1 + fi + + return 0 +} + +# fetch_beads_tasks - Fetch tasks from beads issue tracker +# +# Parameters: +# $1 (filter) - Filter string (optional, e.g., "status:open") +# +# Outputs: +# Tasks in markdown checkbox format, one per line +# e.g., "- [ ] [issue-001] Fix authentication bug" +# +# Returns: +# 0 - Success (may output empty if no tasks) +# 1 - Error fetching tasks +# +fetch_beads_tasks() { + local filter="${1:-status:open}" + local tasks="" + + # Check if beads is available + if ! check_beads_available; then + return 1 + fi + + # Try to get tasks as JSON (pass filter if provided) + local json_output + if json_output=$(bd list --json --filter "$filter" 2>/dev/null); then + # Parse JSON and format as markdown tasks + if command -v jq &>/dev/null; then + tasks=$(echo "$json_output" | jq -r ' + .[] | + select(.status != "closed") | + "- [ ] [\(.id)] \(.title)" + ' 2>/dev/null) + fi + else + # Fallback: try plain text output + tasks=$(bd list 2>/dev/null | while IFS= read -r line; do + # Extract ID and title from bd list output + local id title + id=$(echo "$line" | grep -oE '^[a-z]+-[0-9]+' || echo "") + title=$(echo "$line" | sed 's/^[a-z]+-[0-9]* *//' || echo "$line") + if [[ -n "$id" ]]; then + echo "- [ ] [$id] $title" + fi + done) + fi + + if [[ -n "$tasks" ]]; then + echo "$tasks" + return 0 + else + return 0 # Empty is not an error + fi +} + +# get_beads_count - Get count of open beads issues +# +# Returns: +# 0 and echoes the count +# 1 if beads unavailable +# +get_beads_count() { + if ! check_beads_available; then + echo "0" + return 1 + fi + + local count + if command -v jq &>/dev/null; then + count=$(bd list --json 2>/dev/null | jq '[.[] | select(.status != "closed")] | length' 2>/dev/null || echo "0") + else + count=$(bd list 2>/dev/null | wc -l | tr -d ' ') + fi + + echo "${count:-0}" + return 0 +} + +# ============================================================================= +# GITHUB ISSUES INTEGRATION +# ============================================================================= + +# check_github_available - Check if GitHub CLI (gh) is available and authenticated +# +# Returns: +# 0 - GitHub available and authenticated +# 1 - Not available +# +check_github_available() { + # Check for gh command + if ! command -v gh &>/dev/null; then + return 1 + fi + + # Check if authenticated + if ! gh auth status &>/dev/null; then + return 1 + fi + + # Check if in a git repo with GitHub remote + if ! git remote get-url origin 2>/dev/null | grep -q "github.com"; then + return 1 + fi + + return 0 +} + +# fetch_github_tasks - Fetch issues from GitHub +# +# Parameters: +# $1 (label) - Label to filter by (optional, default: "ralph-task") +# $2 (limit) - Maximum number of issues (optional, default: 50) +# +# Outputs: +# Tasks in markdown checkbox format +# e.g., "- [ ] [#123] Implement user authentication" +# +# Returns: +# 0 - Success +# 1 - Error +# +fetch_github_tasks() { + local label="${1:-}" + local limit="${2:-50}" + local tasks="" + + # Check if GitHub is available + if ! check_github_available; then + return 1 + fi + + # Build gh command + local gh_args=("issue" "list" "--state" "open" "--limit" "$limit" "--json" "number,title,labels") + if [[ -n "$label" ]]; then + gh_args+=("--label" "$label") + fi + + # Fetch issues + local json_output + if ! json_output=$(gh "${gh_args[@]}" 2>/dev/null); then + return 1 + fi + + # Parse JSON and format as markdown tasks + if command -v jq &>/dev/null; then + tasks=$(echo "$json_output" | jq -r ' + .[] | + "- [ ] [#\(.number)] \(.title)" + ' 2>/dev/null) + fi + + if [[ -n "$tasks" ]]; then + echo "$tasks" + fi + + return 0 +} + +# get_github_issue_count - Get count of open GitHub issues +# +# Parameters: +# $1 (label) - Label to filter by (optional) +# +# Returns: +# 0 and echoes the count +# 1 if GitHub unavailable +# +get_github_issue_count() { + local label="${1:-}" + + if ! check_github_available; then + echo "0" + return 1 + fi + + local gh_args=("issue" "list" "--state" "open" "--json" "number") + if [[ -n "$label" ]]; then + gh_args+=("--label" "$label") + fi + + local count + if command -v jq &>/dev/null; then + count=$(gh "${gh_args[@]}" 2>/dev/null | jq 'length' 2>/dev/null || echo "0") + else + count=$(gh issue list --state open 2>/dev/null | wc -l | tr -d ' ') + fi + + echo "${count:-0}" + return 0 +} + +# get_github_labels - Get available labels from GitHub repo +# +# Outputs: +# Newline-separated list of label names +# +get_github_labels() { + if ! check_github_available; then + return 1 + fi + + gh label list --json name --jq '.[].name' 2>/dev/null +} + +# ============================================================================= +# PRD CONVERSION +# ============================================================================= + +# extract_prd_tasks - Extract tasks from a PRD/specification document +# +# Parameters: +# $1 (prd_file) - Path to the PRD file +# +# Outputs: +# Tasks in markdown checkbox format +# +# Returns: +# 0 - Success +# 1 - Error +# +# Note: For full PRD conversion with Claude, use ralph-import +# This function does basic extraction without AI assistance +# +extract_prd_tasks() { + local prd_file=$1 + + if [[ ! -f "$prd_file" ]]; then + return 1 + fi + + local tasks="" + + # Look for existing checkbox items + local checkbox_tasks + checkbox_tasks=$(grep -E '^[[:space:]]*[-*][[:space:]]*\[[[:space:]]*[xX ]?[[:space:]]*\]' "$prd_file" 2>/dev/null) + if [[ -n "$checkbox_tasks" ]]; then + # Normalize to unchecked format + tasks=$(echo "$checkbox_tasks" | sed 's/\[x\]/[ ]/gi; s/\[X\]/[ ]/g') + fi + + # Look for numbered list items that look like tasks + local numbered_tasks + numbered_tasks=$(grep -E '^[[:space:]]*[0-9]+\.[[:space:]]+' "$prd_file" 2>/dev/null | head -20) + if [[ -n "$numbered_tasks" ]]; then + while IFS= read -r line; do + # Convert numbered item to checkbox + local task_text + task_text=$(echo "$line" | sed -E 's/^[[:space:]]*[0-9]*\.[[:space:]]*//') + if [[ -n "$task_text" ]]; then + tasks="${tasks} +- [ ] ${task_text}" + fi + done <<< "$numbered_tasks" + fi + + # Look for headings that might be task sections + local headings + headings=$(grep -E '^#{1,3}[[:space:]]+(TODO|Tasks|Requirements|Features|Backlog|Sprint)' "$prd_file" 2>/dev/null) + if [[ -n "$headings" ]]; then + # Extract content after these headings as potential tasks + while IFS= read -r heading; do + local section_name + section_name=$(echo "$heading" | sed -E 's/^#*[[:space:]]*//') + # This is informational - actual task extraction would need more context + done <<< "$headings" + fi + + # Clean up and output + if [[ -n "$tasks" ]]; then + echo "$tasks" | grep -v '^$' | head -30 # Limit to 30 tasks + return 0 + fi + + return 0 # Empty is not an error +} + +# convert_prd_with_claude - Full PRD conversion using Claude (calls ralph-import logic) +# +# Parameters: +# $1 (prd_file) - Path to the PRD file +# $2 (output_dir) - Directory to output converted files (optional, defaults to .ralph/) +# +# Outputs: +# Sets CONVERTED_PROMPT_FILE, CONVERTED_FIX_PLAN_FILE, CONVERTED_SPECS_FILE +# +# Returns: +# 0 - Success +# 1 - Error +# +convert_prd_with_claude() { + local prd_file=$1 + local output_dir="${2:-.ralph}" + + # This would call into ralph_import.sh's convert_prd function + # For now, we do basic extraction + # Full Claude-based conversion requires the import script + + if [[ ! -f "$prd_file" ]]; then + return 1 + fi + + # Check if ralph-import is available for full conversion + if command -v ralph-import &>/dev/null; then + # Use ralph-import for full conversion + # Note: ralph-import creates a new project, so we need to adapt + echo "Full PRD conversion available via: ralph-import $prd_file" + return 1 # Return error to indicate basic extraction should be used + fi + + # Fall back to basic extraction + extract_prd_tasks "$prd_file" +} + +# ============================================================================= +# TASK NORMALIZATION +# ============================================================================= + +# normalize_tasks - Normalize tasks to consistent markdown format +# +# Parameters: +# $1 (tasks) - Raw task text (multi-line) +# $2 (source) - Source identifier (beads, github, prd) +# +# Outputs: +# Normalized tasks in markdown checkbox format +# +normalize_tasks() { + local tasks=$1 + local source="${2:-unknown}" + + if [[ -z "$tasks" ]]; then + return 0 + fi + + # Process each line + echo "$tasks" | while IFS= read -r line; do + # Skip empty lines + [[ -z "$line" ]] && continue + + # Already in checkbox format + if echo "$line" | grep -qE '^[[:space:]]*-[[:space:]]*\[[[:space:]]*[xX ]?[[:space:]]*\]'; then + # Normalize the checkbox + echo "$line" | sed 's/\[x\]/[ ]/gi; s/\[X\]/[ ]/g' + continue + fi + + # Bullet point without checkbox + if echo "$line" | grep -qE '^[[:space:]]*[-*][[:space:]]+'; then + local text + text=$(echo "$line" | sed -E 's/^[[:space:]]*[-*][[:space:]]*//') + echo "- [ ] $text" + continue + fi + + # Numbered item + if echo "$line" | grep -qE '^[[:space:]]*[0-9]+\.?[[:space:]]+'; then + local text + text=$(echo "$line" | sed -E 's/^[[:space:]]*[0-9]*\.?[[:space:]]*//') + echo "- [ ] $text" + continue + fi + + # Plain text line - make it a task + echo "- [ ] $line" + done +} + +# prioritize_tasks - Sort tasks by priority heuristics +# +# Parameters: +# $1 (tasks) - Tasks in markdown format +# +# Outputs: +# Tasks sorted with priority indicators +# +# Heuristics: +# - "critical", "urgent", "blocker" -> High priority +# - "important", "should", "must" -> High priority +# - "nice to have", "optional", "future" -> Low priority +# +prioritize_tasks() { + local tasks=$1 + + if [[ -z "$tasks" ]]; then + return 0 + fi + + # Separate into priority buckets + local high_priority="" + local medium_priority="" + local low_priority="" + + while IFS= read -r line; do + [[ -z "$line" ]] && continue + + local lower_line + lower_line=$(echo "$line" | tr '[:upper:]' '[:lower:]') + + # Check for priority indicators + if echo "$lower_line" | grep -qE '(critical|urgent|blocker|breaking|security|p0|p1)'; then + high_priority="${high_priority}${line} +" + elif echo "$lower_line" | grep -qE '(nice.to.have|optional|future|later|p3|p4|low.priority)'; then + low_priority="${low_priority}${line} +" + elif echo "$lower_line" | grep -qE '(important|should|must|needed|required|p2)'; then + high_priority="${high_priority}${line} +" + else + medium_priority="${medium_priority}${line} +" + fi + done <<< "$tasks" + + # Output in priority order + echo "## High Priority" + [[ -n "$high_priority" ]] && echo "$high_priority" + + echo "" + echo "## Medium Priority" + [[ -n "$medium_priority" ]] && echo "$medium_priority" + + echo "" + echo "## Low Priority" + [[ -n "$low_priority" ]] && echo "$low_priority" +} + +# ============================================================================= +# COMBINED IMPORT +# ============================================================================= + +# import_tasks_from_sources - Import tasks from multiple sources +# +# Parameters: +# $1 (sources) - Space-separated list of sources: beads, github, prd +# $2 (prd_file) - Path to PRD file (required if prd in sources) +# $3 (github_label) - GitHub label filter (optional) +# +# Outputs: +# Combined tasks in markdown format +# +# Returns: +# 0 - Success +# 1 - No tasks imported +# +import_tasks_from_sources() { + local sources=$1 + local prd_file="${2:-}" + local github_label="${3:-}" + + local all_tasks="" + local source_count=0 + + # Import from beads + if echo "$sources" | grep -qw "beads"; then + local beads_tasks + if beads_tasks=$(fetch_beads_tasks); then + if [[ -n "$beads_tasks" ]]; then + all_tasks="${all_tasks} +# Tasks from beads +${beads_tasks} +" + ((source_count++)) + fi + fi + fi + + # Import from GitHub + if echo "$sources" | grep -qw "github"; then + local github_tasks + if github_tasks=$(fetch_github_tasks "$github_label"); then + if [[ -n "$github_tasks" ]]; then + all_tasks="${all_tasks} +# Tasks from GitHub +${github_tasks} +" + ((source_count++)) + fi + fi + fi + + # Import from PRD + if echo "$sources" | grep -qw "prd"; then + if [[ -n "$prd_file" && -f "$prd_file" ]]; then + local prd_tasks + if prd_tasks=$(extract_prd_tasks "$prd_file"); then + if [[ -n "$prd_tasks" ]]; then + all_tasks="${all_tasks} +# Tasks from PRD +${prd_tasks} +" + ((source_count++)) + fi + fi + fi + fi + + if [[ -z "$all_tasks" ]]; then + return 1 + fi + + # Normalize and output + normalize_tasks "$all_tasks" "combined" + return 0 +} + +# ============================================================================= +# EXPORTS +# ============================================================================= + +export -f check_beads_available +export -f fetch_beads_tasks +export -f get_beads_count +export -f check_github_available +export -f fetch_github_tasks +export -f get_github_issue_count +export -f get_github_labels +export -f extract_prd_tasks +export -f convert_prd_with_claude +export -f normalize_tasks +export -f prioritize_tasks +export -f import_tasks_from_sources diff --git a/lib/wizard_utils.sh b/lib/wizard_utils.sh new file mode 100755 index 0000000..ca95240 --- /dev/null +++ b/lib/wizard_utils.sh @@ -0,0 +1,542 @@ +#!/usr/bin/env bash + +# wizard_utils.sh - Interactive prompt utilities for Ralph enable wizard +# Provides consistent, user-friendly prompts for configuration + +# Colors (exported for subshells) +export WIZARD_CYAN='\033[0;36m' +export WIZARD_GREEN='\033[0;32m' +export WIZARD_YELLOW='\033[1;33m' +export WIZARD_RED='\033[0;31m' +export WIZARD_BOLD='\033[1m' +export WIZARD_NC='\033[0m' + +# ============================================================================= +# BASIC PROMPTS +# ============================================================================= + +# confirm - Ask a yes/no question +# +# Parameters: +# $1 (prompt) - The question to ask +# $2 (default) - Default answer: "y" or "n" (optional, defaults to "n") +# +# Returns: +# 0 - User answered yes +# 1 - User answered no +# +# Example: +# if confirm "Continue with installation?" "y"; then +# echo "Installing..." +# fi +# +confirm() { + local prompt=$1 + local default="${2:-n}" + local response + + local yn_hint="[y/N]" + if [[ "${default,,}" == "y" ]]; then + yn_hint="[Y/n]" + fi + + while true; do + echo -en "${WIZARD_CYAN}${prompt}${WIZARD_NC} ${yn_hint}: " + read -r response + + # Handle empty response (use default) + if [[ -z "$response" ]]; then + response="$default" + fi + + case "${response,,}" in + y|yes) + return 0 + ;; + n|no) + return 1 + ;; + *) + echo -e "${WIZARD_YELLOW}Please answer yes (y) or no (n)${WIZARD_NC}" + ;; + esac + done +} + +# prompt_text - Ask for text input with optional default +# +# Parameters: +# $1 (prompt) - The prompt text +# $2 (default) - Default value (optional) +# +# Outputs: +# Echoes the user's input (or default if empty) +# +# Example: +# project_name=$(prompt_text "Project name" "my-project") +# +prompt_text() { + local prompt=$1 + local default="${2:-}" + local response + + if [[ -n "$default" ]]; then + echo -en "${WIZARD_CYAN}${prompt}${WIZARD_NC} [${default}]: " + else + echo -en "${WIZARD_CYAN}${prompt}${WIZARD_NC}: " + fi + + read -r response + + if [[ -z "$response" ]]; then + echo "$default" + else + echo "$response" + fi +} + +# prompt_number - Ask for numeric input with optional default and range +# +# Parameters: +# $1 (prompt) - The prompt text +# $2 (default) - Default value (optional) +# $3 (min) - Minimum value (optional) +# $4 (max) - Maximum value (optional) +# +# Outputs: +# Echoes the validated number +# +prompt_number() { + local prompt=$1 + local default="${2:-}" + local min="${3:-}" + local max="${4:-}" + local response + + while true; do + if [[ -n "$default" ]]; then + echo -en "${WIZARD_CYAN}${prompt}${WIZARD_NC} [${default}]: " + else + echo -en "${WIZARD_CYAN}${prompt}${WIZARD_NC}: " + fi + + read -r response + + # Use default if empty + if [[ -z "$response" ]]; then + if [[ -n "$default" ]]; then + echo "$default" + return 0 + else + echo -e "${WIZARD_YELLOW}Please enter a number${WIZARD_NC}" + continue + fi + fi + + # Validate it's a number + if ! [[ "$response" =~ ^[0-9]+$ ]]; then + echo -e "${WIZARD_YELLOW}Please enter a valid number${WIZARD_NC}" + continue + fi + + # Check range if specified + if [[ -n "$min" && "$response" -lt "$min" ]]; then + echo -e "${WIZARD_YELLOW}Value must be at least ${min}${WIZARD_NC}" + continue + fi + + if [[ -n "$max" && "$response" -gt "$max" ]]; then + echo -e "${WIZARD_YELLOW}Value must be at most ${max}${WIZARD_NC}" + continue + fi + + echo "$response" + return 0 + done +} + +# ============================================================================= +# SELECTION PROMPTS +# ============================================================================= + +# select_option - Present a list of options for single selection +# +# Parameters: +# $1 (prompt) - The question/prompt text +# $@ (options) - Remaining arguments are the options +# +# Outputs: +# Echoes the selected option (the text, not the number) +# +# Example: +# choice=$(select_option "Select package manager" "npm" "yarn" "pnpm") +# echo "Selected: $choice" +# +select_option() { + local prompt=$1 + shift + local options=("$@") + local num_options=${#options[@]} + + # Guard against empty options array + if [[ $num_options -eq 0 ]]; then + echo "" + return 1 + fi + + echo -e "\n${WIZARD_BOLD}${prompt}${WIZARD_NC}" + echo "" + + # Display options + local i=1 + for opt in "${options[@]}"; do + echo -e " ${WIZARD_CYAN}${i})${WIZARD_NC} ${opt}" + ((i++)) + done + + echo "" + + while true; do + echo -en "Select option [1-${num_options}]: " + read -r response + + # Validate it's a number in range + if [[ "$response" =~ ^[0-9]+$ ]] && \ + [[ "$response" -ge 1 ]] && \ + [[ "$response" -le "$num_options" ]]; then + # Return the option text (0-indexed array) + echo "${options[$((response - 1))]}" + return 0 + else + echo -e "${WIZARD_YELLOW}Please enter a number between 1 and ${num_options}${WIZARD_NC}" + fi + done +} + +# select_multiple - Present checkboxes for multi-selection +# +# Parameters: +# $1 (prompt) - The question/prompt text +# $@ (options) - Remaining arguments are the options +# +# Outputs: +# Echoes comma-separated list of selected indices (0-based) +# Returns empty string if nothing selected +# +# Example: +# selected=$(select_multiple "Select task sources" "beads" "github" "prd") +# # If user selects first and third: selected="0,2" +# IFS=',' read -ra indices <<< "$selected" +# for idx in "${indices[@]}"; do +# echo "Selected: ${options[$idx]}" +# done +# +select_multiple() { + local prompt=$1 + shift + local options=("$@") + local num_options=${#options[@]} + + # Track selected state (0 = not selected, 1 = selected) + declare -a selected + for ((i = 0; i < num_options; i++)); do + selected[$i]=0 + done + + # Display instructions (redirect to stderr to avoid corrupting return value) + echo -e "\n${WIZARD_BOLD}${prompt}${WIZARD_NC}" >&2 + echo -e "${WIZARD_CYAN}(Enter numbers to toggle, press Enter when done)${WIZARD_NC}" >&2 + echo "" >&2 + + while true; do + # Display options with checkboxes + local i=1 + for opt in "${options[@]}"; do + local checkbox="[ ]" + if [[ "${selected[$((i - 1))]}" == "1" ]]; then + checkbox="[${WIZARD_GREEN}x${WIZARD_NC}]" + fi + echo -e " ${WIZARD_CYAN}${i})${WIZARD_NC} ${checkbox} ${opt}" >&2 + ((i++)) || true + done + + echo "" >&2 + echo -en "Toggle [1-${num_options}] or Enter to confirm: " >&2 + read -r response + + # Empty input = done + if [[ -z "$response" ]]; then + break + fi + + # Validate it's a number in range + if [[ "$response" =~ ^[0-9]+$ ]] && \ + [[ "$response" -ge 1 ]] && \ + [[ "$response" -le "$num_options" ]]; then + # Toggle the selection + local idx=$((response - 1)) + if [[ "${selected[$idx]}" == "0" ]]; then + selected[$idx]=1 + else + selected[$idx]=0 + fi + else + echo -e "${WIZARD_YELLOW}Please enter a number between 1 and ${num_options}${WIZARD_NC}" >&2 + fi + + # Clear previous display (move cursor up) + # Number of lines to clear: options + 2 (prompt line + input line) + for ((j = 0; j < num_options + 2; j++)); do + echo -en "\033[A\033[K" >&2 + done + done + + # Build result string (comma-separated indices) + local result="" + for ((i = 0; i < num_options; i++)); do + if [[ "${selected[$i]}" == "1" ]]; then + if [[ -n "$result" ]]; then + result="$result,$i" + else + result="$i" + fi + fi + done + + echo "$result" +} + +# select_with_default - Present options with a recommended default +# +# Parameters: +# $1 (prompt) - The question/prompt text +# $2 (default_index) - 1-based index of default option +# $@ (options) - Remaining arguments are the options +# +# Outputs: +# Echoes the selected option +# +select_with_default() { + local prompt=$1 + local default_index=$2 + shift 2 + local options=("$@") + local num_options=${#options[@]} + + echo -e "\n${WIZARD_BOLD}${prompt}${WIZARD_NC}" + echo "" + + # Display options with default marked + local i=1 + for opt in "${options[@]}"; do + if [[ $i -eq $default_index ]]; then + echo -e " ${WIZARD_GREEN}${i})${WIZARD_NC} ${opt} ${WIZARD_GREEN}(recommended)${WIZARD_NC}" + else + echo -e " ${WIZARD_CYAN}${i})${WIZARD_NC} ${opt}" + fi + ((i++)) + done + + echo "" + + while true; do + echo -en "Select option [1-${num_options}] (default: ${default_index}): " + read -r response + + # Use default if empty + if [[ -z "$response" ]]; then + echo "${options[$((default_index - 1))]}" + return 0 + fi + + # Validate it's a number in range + if [[ "$response" =~ ^[0-9]+$ ]] && \ + [[ "$response" -ge 1 ]] && \ + [[ "$response" -le "$num_options" ]]; then + echo "${options[$((response - 1))]}" + return 0 + else + echo -e "${WIZARD_YELLOW}Please enter a number between 1 and ${num_options}${WIZARD_NC}" + fi + done +} + +# ============================================================================= +# DISPLAY UTILITIES +# ============================================================================= + +# print_header - Print a section header +# +# Parameters: +# $1 (title) - The header title +# $2 (phase) - Optional phase number (e.g., "1 of 5") +# +print_header() { + local title=$1 + local phase="${2:-}" + + echo "" + echo -e "${WIZARD_BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${WIZARD_NC}" + if [[ -n "$phase" ]]; then + echo -e "${WIZARD_BOLD} ${title}${WIZARD_NC} ${WIZARD_CYAN}(${phase})${WIZARD_NC}" + else + echo -e "${WIZARD_BOLD} ${title}${WIZARD_NC}" + fi + echo -e "${WIZARD_BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${WIZARD_NC}" + echo "" +} + +# print_bullet - Print a bullet point item +# +# Parameters: +# $1 (text) - The text to display +# $2 (symbol) - Optional symbol (defaults to "•") +# +print_bullet() { + local text=$1 + local symbol="${2:-•}" + + echo -e " ${WIZARD_CYAN}${symbol}${WIZARD_NC} ${text}" +} + +# print_success - Print a success message +# +# Parameters: +# $1 (message) - The message to display +# +print_success() { + echo -e "${WIZARD_GREEN}✓${WIZARD_NC} $1" +} + +# print_warning - Print a warning message +# +# Parameters: +# $1 (message) - The message to display +# +print_warning() { + echo -e "${WIZARD_YELLOW}⚠${WIZARD_NC} $1" +} + +# print_error - Print an error message +# +# Parameters: +# $1 (message) - The message to display +# +print_error() { + echo -e "${WIZARD_RED}✗${WIZARD_NC} $1" +} + +# print_info - Print an info message +# +# Parameters: +# $1 (message) - The message to display +# +print_info() { + echo -e "${WIZARD_CYAN}ℹ${WIZARD_NC} $1" +} + +# print_detection_result - Print a detection result with status +# +# Parameters: +# $1 (label) - What was detected +# $2 (value) - The detected value +# $3 (available) - "true" or "false" +# +print_detection_result() { + local label=$1 + local value=$2 + local available="${3:-true}" + + if [[ "$available" == "true" ]]; then + echo -e " ${WIZARD_GREEN}✓${WIZARD_NC} ${label}: ${WIZARD_BOLD}${value}${WIZARD_NC}" + else + echo -e " ${WIZARD_YELLOW}○${WIZARD_NC} ${label}: ${value}" + fi +} + +# ============================================================================= +# PROGRESS DISPLAY +# ============================================================================= + +# show_progress - Display a simple progress indicator +# +# Parameters: +# $1 (current) - Current step number +# $2 (total) - Total steps +# $3 (message) - Current step message +# +show_progress() { + local current=$1 + local total=$2 + local message=$3 + + local bar_width=30 + local filled=$((current * bar_width / total)) + local empty=$((bar_width - filled)) + + local bar="" + for ((i = 0; i < filled; i++)); do bar+="█"; done + for ((i = 0; i < empty; i++)); do bar+="░"; done + + echo -en "\r${WIZARD_CYAN}[${bar}]${WIZARD_NC} ${current}/${total} ${message}" +} + +# clear_line - Clear the current line +# +clear_line() { + echo -en "\r\033[K" +} + +# ============================================================================= +# SUMMARY DISPLAY +# ============================================================================= + +# print_summary - Print a summary box +# +# Parameters: +# $1 (title) - Summary title +# $@ (items) - Key=value pairs to display +# +# Example: +# print_summary "Configuration" "Project=my-app" "Type=typescript" "Tasks=15" +# +print_summary() { + local title=$1 + shift + local items=("$@") + + echo "" + echo -e "${WIZARD_BOLD}┌─ ${title} ───────────────────────────────────────┐${WIZARD_NC}" + echo "│" + + for item in "${items[@]}"; do + local key="${item%%=*}" + local value="${item#*=}" + printf "│ ${WIZARD_CYAN}%-20s${WIZARD_NC} %s\n" "${key}:" "$value" + done + + echo "│" + echo -e "${WIZARD_BOLD}└────────────────────────────────────────────────────┘${WIZARD_NC}" + echo "" +} + +# ============================================================================= +# EXPORTS +# ============================================================================= + +export -f confirm +export -f prompt_text +export -f prompt_number +export -f select_option +export -f select_multiple +export -f select_with_default +export -f print_header +export -f print_bullet +export -f print_success +export -f print_warning +export -f print_error +export -f print_info +export -f print_detection_result +export -f show_progress +export -f clear_line +export -f print_summary diff --git a/ralph_enable.sh b/ralph_enable.sh new file mode 100755 index 0000000..f8e82ae --- /dev/null +++ b/ralph_enable.sh @@ -0,0 +1,571 @@ +#!/bin/bash + +# Ralph Enable - Interactive Wizard for Existing Projects +# Adds Ralph configuration to an existing codebase +# +# Usage: +# ralph enable # Interactive wizard +# ralph enable --from beads # With specific task source +# ralph enable --force # Overwrite existing .ralph/ +# ralph enable --skip-tasks # Skip task import +# +# Version: 0.11.0 + +set -e + +# Get script directory for library loading +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# Try to load libraries from global installation first, then local +RALPH_HOME="${RALPH_HOME:-$HOME/.ralph}" +if [[ -f "$RALPH_HOME/lib/enable_core.sh" ]]; then + LIB_DIR="$RALPH_HOME/lib" +elif [[ -f "$SCRIPT_DIR/lib/enable_core.sh" ]]; then + LIB_DIR="$SCRIPT_DIR/lib" +else + echo "Error: Cannot find Ralph libraries" + echo "Please run ./install.sh first or ensure RALPH_HOME is set correctly" + exit 1 +fi + +# Source libraries +source "$LIB_DIR/enable_core.sh" +source "$LIB_DIR/wizard_utils.sh" +source "$LIB_DIR/task_sources.sh" + +# ============================================================================= +# CONFIGURATION +# ============================================================================= + +# Command line options +FORCE_OVERWRITE=false +SKIP_TASKS=false +TASK_SOURCE="" +PRD_FILE="" +GITHUB_LABEL="" +NON_INTERACTIVE=false +SHOW_HELP=false + +# Version +VERSION="0.11.0" + +# ============================================================================= +# HELP +# ============================================================================= + +show_help() { + cat << EOF +Ralph Enable - Add Ralph to Existing Projects + +Usage: ralph enable [OPTIONS] + +Options: + --from Import tasks from: beads, github, prd + --prd PRD file to convert (when --from prd) + --label