feat(enable): add ralph-enable wizard for existing projects (v0.11.0) (#124)
* feat(enable): add ralph-enable wizard for existing projects (v0.11.0) Add interactive wizard and CI version for enabling Ralph in existing projects. New commands: - ralph-enable: Interactive 5-phase wizard for humans - ralph-enable-ci: Non-interactive version with JSON output for CI/automation New library components: - lib/enable_core.sh: Shared logic for idempotency, project detection, templates - lib/wizard_utils.sh: Interactive prompt utilities - lib/task_sources.sh: Task import from beads, GitHub Issues, PRD documents Features: - Auto-detects project type (TypeScript, Python, Rust, Go) - Auto-detects framework (Next.js, FastAPI, Django, Express) - Imports tasks from beads, GitHub Issues, or PRD documents - Generates .ralphrc project configuration file - Idempotent: safe to run multiple times, respects existing files - Exit codes: 0 (success), 1 (error), 2 (already enabled) Updated: - install.sh: Added new commands to global installation - ralph_loop.sh: Loads .ralphrc configuration at startup Tests: 75 new tests (30 enable_core + 23 task_sources + 22 integration) Total: 396 tests passing (100% pass rate) Closes #85, #121, #64, #87, #99 * fix(enable): address code review feedback Fixes from PR #124 review: 1. sed -i portability (ralph_enable.sh:456) - Use portable sed + mv pattern instead of GNU-only sed -i 2. sed regex portability (lib/task_sources.sh) - Replace \s with POSIX [[:space:]] character class - Add sed -E flag for extended regex 3. jq availability check (ralph_enable_ci.sh:177) - Add check for jq when --json flag is used 4. Unused filter parameter (lib/task_sources.sh:44) - Pass filter to bd list --filter command 5. Word-splitting in select_multiple (ralph_enable.sh:322) - Return comma-separated indices instead of space-separated text - Update caller to parse indices correctly 6. Missing || true for check_existing_ralph (ralph_enable.sh:185) - Prevent set -e from exiting on non-zero return 7. select_multiple stdout corruption (lib/wizard_utils.sh) - Redirect interactive output to stderr - Only final result goes to stdout 8. Color variables not exported (lib/wizard_utils.sh:12) - Export WIZARD_* color variables for subshells 9. select_option infinite loop (lib/wizard_utils.sh:179) - Add guard for empty options array * fix(tests): add missing mocks and exports for new enable feature - Add RESPONSE_ANALYSIS_FILE export to test_session_continuity.bats setup - Add mock ralph_enable.sh and ralph_enable_ci.sh to test_installation.bats - Add mock lib files: enable_core.sh, wizard_utils.sh, task_sources.sh, timeout_utils.sh All 396 tests now pass. * fix(config): fix critical issues from PR review 1. .ralphrc Configuration Loading Fix: - Captured env var state BEFORE setting defaults with _env_* variables - load_ralphrc now only restores values explicitly set by environment - .ralphrc settings are now properly applied (not overwritten by defaults) 2. sed Command Injection Fix: - Replaced sed with awk for .ralphrc updates in ralph_enable.sh - awk -v pattern safely handles user input without shell injection risk 3. Shell Injection Fix in safe_create_file(): - Replaced echo with printf '%s\n' for safer content handling - Prevents issues with backslashes, -n, and special characters 4. Specific Error Codes: - Added ENABLE_INVALID_ARGS=3 for argument errors - Added ENABLE_FILE_NOT_FOUND=4 for missing files - Added ENABLE_DEPENDENCY_MISSING=5 for missing deps (e.g., jq) - Added ENABLE_PERMISSION_DENIED=6 for permission errors - Updated ralph_enable.sh and ralph_enable_ci.sh to use specific codes 5. Added tests for .ralphrc loading pattern verification Test count: 398 (up from 396) * fix(enable): make --force flag actually overwrite existing files The --force flag was accepted but safe_create_file() always skipped existing files regardless of ENABLE_FORCE value. Changes: - safe_create_file() now checks ENABLE_FORCE environment variable - When ENABLE_FORCE="true", overwrites existing files instead of skipping - Added proper logging for overwrite operations Added tests: - Verify enable_ralph_in_directory actually changes file contents with --force - Test safe_create_file overwrites when ENABLE_FORCE is true - Test safe_create_file skips when ENABLE_FORCE is false Test count: 400 (up from 398) --------- Co-authored-by: Test User <test@example.com>
This commit is contained in:
parent
019b8c738a
commit
910f794fcc
15 changed files with 4169 additions and 18 deletions
85
CLAUDE.md
85
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.
|
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
|
## 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
|
- Uses modern Claude Code CLI with `--output-format json` for structured responses
|
||||||
- Implements `detect_response_format()` and `parse_conversion_response()` for JSON parsing
|
- Implements `detect_response_format()` and `parse_conversion_response()` for JSON parsing
|
||||||
- Backward compatible with older CLI versions (automatic text fallback)
|
- 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/)
|
### 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
|
- `portable_timeout()` function for seamless cross-platform execution
|
||||||
- Automatic detection with caching for performance
|
- 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
|
## Key Commands
|
||||||
|
|
||||||
### Installation
|
### Installation
|
||||||
|
|
@ -84,6 +109,27 @@ cd existing-project
|
||||||
ralph-migrate
|
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
|
### Running the Ralph Loop
|
||||||
```bash
|
```bash
|
||||||
# Start with integrated tmux monitoring (recommended)
|
# Start with integrated tmux monitoring (recommended)
|
||||||
|
|
@ -121,7 +167,7 @@ tmux attach -t <session-name>
|
||||||
|
|
||||||
### Running Tests
|
### Running Tests
|
||||||
```bash
|
```bash
|
||||||
# Run all tests (165 tests)
|
# Run all tests (396 tests)
|
||||||
npm test
|
npm test
|
||||||
|
|
||||||
# Run specific test suites
|
# Run specific test suites
|
||||||
|
|
@ -132,6 +178,9 @@ npm run test:integration
|
||||||
bats tests/unit/test_cli_parsing.bats
|
bats tests/unit/test_cli_parsing.bats
|
||||||
bats tests/unit/test_json_parsing.bats
|
bats tests/unit/test_json_parsing.bats
|
||||||
bats tests/unit/test_cli_modern.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
|
## Ralph Loop Configuration
|
||||||
|
|
@ -264,10 +313,10 @@ Templates in `templates/` provide starting points for new projects:
|
||||||
## Global Installation
|
## Global Installation
|
||||||
|
|
||||||
Ralph installs to:
|
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/`
|
- **Templates**: `~/.ralph/templates/`
|
||||||
- **Scripts**: `~/.ralph/` (ralph_loop.sh, ralph_monitor.sh, setup.sh, ralph_import.sh, migrate_to_ralph_folder.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)
|
- **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:
|
After installation, the following global commands are available:
|
||||||
- `ralph` - Start the autonomous development loop
|
- `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-setup` - Create a new Ralph-managed project
|
||||||
- `ralph-import` - Import PRD/specification documents to Ralph format
|
- `ralph-import` - Import PRD/specification documents to Ralph format
|
||||||
- `ralph-migrate` - Migrate existing projects from flat structure to `.ralph/` subfolder
|
- `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
|
## Integration Points
|
||||||
|
|
||||||
|
|
@ -351,7 +402,7 @@ Ralph uses advanced error detection with two-stage filtering to eliminate false
|
||||||
|
|
||||||
## Test Suite
|
## Test Suite
|
||||||
|
|
||||||
### Test Files (265 tests total)
|
### Test Files (396 tests total)
|
||||||
|
|
||||||
| File | Tests | Description |
|
| 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_installation.bats` | 14 | Global installation/uninstall workflows |
|
||||||
| `test_project_setup.bats` | 36 | Project setup (setup.sh) validation |
|
| `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_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
|
### Running Tests
|
||||||
```bash
|
```bash
|
||||||
|
|
@ -381,6 +435,25 @@ bats tests/unit/test_cli_parsing.bats
|
||||||
|
|
||||||
## Recent Improvements
|
## 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
|
### Stale Completion Indicators Fix (v0.10.1) - Issue #91
|
||||||
- Fixed premature exit caused by stale completion indicators persisting across sessions
|
- Fixed premature exit caused by stale completion indicators persisting across sessions
|
||||||
- Root cause: `.exit_signals` and `.response_analysis` files retained old completion counts
|
- Root cause: `.exit_signals` and `.response_analysis` files retained old completion counts
|
||||||
|
|
|
||||||
34
install.sh
34
install.sh
|
|
@ -165,6 +165,28 @@ EOF
|
||||||
RALPH_HOME="$HOME/.ralph"
|
RALPH_HOME="$HOME/.ralph"
|
||||||
|
|
||||||
exec "$RALPH_HOME/migrate_to_ralph_folder.sh" "$@"
|
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
|
EOF
|
||||||
|
|
||||||
# Copy actual script files to Ralph home with modifications for global operation
|
# Copy actual script files to Ralph home with modifications for global operation
|
||||||
|
|
@ -176,15 +198,23 @@ EOF
|
||||||
# Copy migration script to Ralph home
|
# Copy migration script to Ralph home
|
||||||
cp "$SCRIPT_DIR/migrate_to_ralph_folder.sh" "$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
|
# Make all commands executable
|
||||||
chmod +x "$INSTALL_DIR/ralph"
|
chmod +x "$INSTALL_DIR/ralph"
|
||||||
chmod +x "$INSTALL_DIR/ralph-monitor"
|
chmod +x "$INSTALL_DIR/ralph-monitor"
|
||||||
chmod +x "$INSTALL_DIR/ralph-setup"
|
chmod +x "$INSTALL_DIR/ralph-setup"
|
||||||
chmod +x "$INSTALL_DIR/ralph-import"
|
chmod +x "$INSTALL_DIR/ralph-import"
|
||||||
chmod +x "$INSTALL_DIR/ralph-migrate"
|
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_monitor.sh"
|
||||||
chmod +x "$RALPH_HOME/ralph_import.sh"
|
chmod +x "$RALPH_HOME/ralph_import.sh"
|
||||||
chmod +x "$RALPH_HOME/migrate_to_ralph_folder.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
|
chmod +x "$RALPH_HOME/lib/"*.sh
|
||||||
|
|
||||||
log "SUCCESS" "Ralph scripts installed to $INSTALL_DIR"
|
log "SUCCESS" "Ralph scripts installed to $INSTALL_DIR"
|
||||||
|
|
@ -291,6 +321,8 @@ main() {
|
||||||
echo " ralph --monitor # Start Ralph with integrated monitoring"
|
echo " ralph --monitor # Start Ralph with integrated monitoring"
|
||||||
echo " ralph --help # Show Ralph options"
|
echo " ralph --help # Show Ralph options"
|
||||||
echo " ralph-setup my-project # Create new Ralph project"
|
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-import prd.md # Convert PRD to Ralph project"
|
||||||
echo " ralph-migrate # Migrate existing project to .ralph/ structure"
|
echo " ralph-migrate # Migrate existing project to .ralph/ structure"
|
||||||
echo " ralph-monitor # Manual monitoring dashboard"
|
echo " ralph-monitor # Manual monitoring dashboard"
|
||||||
|
|
@ -314,7 +346,7 @@ case "${1:-install}" in
|
||||||
;;
|
;;
|
||||||
uninstall)
|
uninstall)
|
||||||
log "INFO" "Uninstalling Ralph for Claude Code..."
|
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"
|
rm -rf "$RALPH_HOME"
|
||||||
log "SUCCESS" "Ralph for Claude Code uninstalled"
|
log "SUCCESS" "Ralph for Claude Code uninstalled"
|
||||||
;;
|
;;
|
||||||
|
|
|
||||||
815
lib/enable_core.sh
Executable file
815
lib/enable_core.sh
Executable file
|
|
@ -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: <number>
|
||||||
|
FILES_MODIFIED: <number>
|
||||||
|
TESTS_STATUS: PASSING | FAILING | NOT_RUN
|
||||||
|
WORK_TYPE: IMPLEMENTATION | TESTING | DOCUMENTATION | REFACTORING
|
||||||
|
EXIT_SIGNAL: false | true
|
||||||
|
RECOMMENDATION: <one line summary of what to do next>
|
||||||
|
---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
|
||||||
550
lib/task_sources.sh
Executable file
550
lib/task_sources.sh
Executable file
|
|
@ -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
|
||||||
542
lib/wizard_utils.sh
Executable file
542
lib/wizard_utils.sh
Executable file
|
|
@ -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
|
||||||
571
ralph_enable.sh
Executable file
571
ralph_enable.sh
Executable file
|
|
@ -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 <source> Import tasks from: beads, github, prd
|
||||||
|
--prd <file> PRD file to convert (when --from prd)
|
||||||
|
--label <label> GitHub label filter (when --from github)
|
||||||
|
--force Overwrite existing .ralph/ configuration
|
||||||
|
--skip-tasks Skip task import, use default templates
|
||||||
|
--non-interactive Run with defaults (no prompts)
|
||||||
|
-h, --help Show this help message
|
||||||
|
-v, --version Show version
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
# Interactive wizard (recommended)
|
||||||
|
cd my-existing-project
|
||||||
|
ralph enable
|
||||||
|
|
||||||
|
# Import tasks from beads
|
||||||
|
ralph enable --from beads
|
||||||
|
|
||||||
|
# Import from GitHub issues with label
|
||||||
|
ralph enable --from github --label "ralph-task"
|
||||||
|
|
||||||
|
# Convert a PRD document
|
||||||
|
ralph enable --from prd --prd ./docs/requirements.md
|
||||||
|
|
||||||
|
# Skip task import
|
||||||
|
ralph enable --skip-tasks
|
||||||
|
|
||||||
|
# Force overwrite existing configuration
|
||||||
|
ralph enable --force
|
||||||
|
|
||||||
|
What this command does:
|
||||||
|
1. Detects your project type (TypeScript, Python, etc.)
|
||||||
|
2. Identifies available task sources (beads, GitHub, PRDs)
|
||||||
|
3. Imports tasks from selected sources
|
||||||
|
4. Creates .ralph/ configuration directory
|
||||||
|
5. Generates PROMPT.md, @fix_plan.md, @AGENT.md
|
||||||
|
6. Creates .ralphrc for project-specific settings
|
||||||
|
|
||||||
|
This command is:
|
||||||
|
- Idempotent: Safe to run multiple times
|
||||||
|
- Non-destructive: Never overwrites existing files (unless --force)
|
||||||
|
- Project-aware: Detects your language, framework, and build tools
|
||||||
|
|
||||||
|
For new projects, use: ralph-setup <project-name>
|
||||||
|
For migrating old structure, use: ralph-migrate
|
||||||
|
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# ARGUMENT PARSING
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
parse_arguments() {
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--from)
|
||||||
|
if [[ -n "$2" && ! "$2" =~ ^-- ]]; then
|
||||||
|
TASK_SOURCE="$2"
|
||||||
|
shift 2
|
||||||
|
else
|
||||||
|
echo "Error: --from requires a source (beads, github, prd)" >&2
|
||||||
|
exit $ENABLE_INVALID_ARGS
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
--prd)
|
||||||
|
if [[ -n "$2" && ! "$2" =~ ^-- ]]; then
|
||||||
|
PRD_FILE="$2"
|
||||||
|
shift 2
|
||||||
|
else
|
||||||
|
echo "Error: --prd requires a file path" >&2
|
||||||
|
exit $ENABLE_INVALID_ARGS
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
--label)
|
||||||
|
if [[ -n "$2" && ! "$2" =~ ^-- ]]; then
|
||||||
|
GITHUB_LABEL="$2"
|
||||||
|
shift 2
|
||||||
|
else
|
||||||
|
echo "Error: --label requires a label name" >&2
|
||||||
|
exit $ENABLE_INVALID_ARGS
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
--force)
|
||||||
|
FORCE_OVERWRITE=true
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--skip-tasks)
|
||||||
|
SKIP_TASKS=true
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--non-interactive)
|
||||||
|
NON_INTERACTIVE=true
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
-h|--help)
|
||||||
|
SHOW_HELP=true
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
-v|--version)
|
||||||
|
echo "ralph enable version $VERSION"
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Unknown option: $1" >&2
|
||||||
|
echo "Use --help for usage information" >&2
|
||||||
|
exit $ENABLE_INVALID_ARGS
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# PHASE 1: ENVIRONMENT DETECTION
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
phase_environment_detection() {
|
||||||
|
print_header "Environment Detection" "Phase 1 of 5"
|
||||||
|
|
||||||
|
echo "Analyzing your project..."
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Check for existing Ralph setup (use || true to prevent set -e from exiting)
|
||||||
|
check_existing_ralph || true
|
||||||
|
case "$RALPH_STATE" in
|
||||||
|
"complete")
|
||||||
|
print_detection_result "Ralph status" "Already enabled" "true"
|
||||||
|
if [[ "$FORCE_OVERWRITE" != "true" ]]; then
|
||||||
|
echo ""
|
||||||
|
print_warning "Ralph is already enabled in this project."
|
||||||
|
echo ""
|
||||||
|
if [[ "$NON_INTERACTIVE" != "true" ]]; then
|
||||||
|
if ! confirm "Do you want to continue anyway?" "n"; then
|
||||||
|
echo "Exiting. Use --force to overwrite."
|
||||||
|
exit $ENABLE_ALREADY_ENABLED
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "Use --force to overwrite existing configuration."
|
||||||
|
exit $ENABLE_ALREADY_ENABLED
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
"partial")
|
||||||
|
print_detection_result "Ralph status" "Partially configured" "false"
|
||||||
|
echo ""
|
||||||
|
print_info "Missing files: ${RALPH_MISSING_FILES[*]}"
|
||||||
|
echo ""
|
||||||
|
;;
|
||||||
|
"none")
|
||||||
|
print_detection_result "Ralph status" "Not configured" "false"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Detect project context
|
||||||
|
detect_project_context
|
||||||
|
print_detection_result "Project name" "$DETECTED_PROJECT_NAME" "true"
|
||||||
|
print_detection_result "Project type" "$DETECTED_PROJECT_TYPE" "true"
|
||||||
|
if [[ -n "$DETECTED_FRAMEWORK" ]]; then
|
||||||
|
print_detection_result "Framework" "$DETECTED_FRAMEWORK" "true"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Detect git info
|
||||||
|
detect_git_info
|
||||||
|
if [[ "$DETECTED_GIT_REPO" == "true" ]]; then
|
||||||
|
print_detection_result "Git repository" "Yes" "true"
|
||||||
|
if [[ "$DETECTED_GIT_GITHUB" == "true" ]]; then
|
||||||
|
print_detection_result "GitHub remote" "Yes" "true"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
print_detection_result "Git repository" "No" "false"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Detect task sources
|
||||||
|
detect_task_sources
|
||||||
|
echo ""
|
||||||
|
echo "Available task sources:"
|
||||||
|
if [[ "$DETECTED_BEADS_AVAILABLE" == "true" ]]; then
|
||||||
|
local beads_count
|
||||||
|
beads_count=$(get_beads_count)
|
||||||
|
print_detection_result "beads" "$beads_count open issues" "true"
|
||||||
|
fi
|
||||||
|
if [[ "$DETECTED_GITHUB_AVAILABLE" == "true" ]]; then
|
||||||
|
local gh_count
|
||||||
|
gh_count=$(get_github_issue_count)
|
||||||
|
print_detection_result "GitHub Issues" "$gh_count open issues" "true"
|
||||||
|
fi
|
||||||
|
if [[ ${#DETECTED_PRD_FILES[@]} -gt 0 ]]; then
|
||||||
|
print_detection_result "PRD files" "${#DETECTED_PRD_FILES[@]} found" "true"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
}
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# PHASE 2: TASK SOURCE SELECTION
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
phase_task_source_selection() {
|
||||||
|
print_header "Task Source Selection" "Phase 2 of 5"
|
||||||
|
|
||||||
|
# If task source specified via CLI, use it
|
||||||
|
if [[ -n "$TASK_SOURCE" ]]; then
|
||||||
|
echo "Using task source from command line: $TASK_SOURCE"
|
||||||
|
SELECTED_SOURCES="$TASK_SOURCE"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# If skip tasks, use empty
|
||||||
|
if [[ "$SKIP_TASKS" == "true" ]]; then
|
||||||
|
echo "Skipping task import (--skip-tasks)"
|
||||||
|
SELECTED_SOURCES=""
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Non-interactive mode: auto-select available sources
|
||||||
|
if [[ "$NON_INTERACTIVE" == "true" ]]; then
|
||||||
|
local auto_sources=""
|
||||||
|
[[ "$DETECTED_BEADS_AVAILABLE" == "true" ]] && auto_sources="beads"
|
||||||
|
[[ "$DETECTED_GITHUB_AVAILABLE" == "true" ]] && auto_sources="${auto_sources:+$auto_sources }github"
|
||||||
|
SELECTED_SOURCES="$auto_sources"
|
||||||
|
echo "Auto-selected sources: ${SELECTED_SOURCES:-none}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Build options list
|
||||||
|
local options=()
|
||||||
|
local option_keys=()
|
||||||
|
|
||||||
|
if [[ "$DETECTED_BEADS_AVAILABLE" == "true" ]]; then
|
||||||
|
local beads_count
|
||||||
|
beads_count=$(get_beads_count)
|
||||||
|
options+=("Import from beads ($beads_count issues)")
|
||||||
|
option_keys+=("beads")
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$DETECTED_GITHUB_AVAILABLE" == "true" ]]; then
|
||||||
|
local gh_count
|
||||||
|
gh_count=$(get_github_issue_count)
|
||||||
|
options+=("Import from GitHub Issues ($gh_count issues)")
|
||||||
|
option_keys+=("github")
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ${#DETECTED_PRD_FILES[@]} -gt 0 ]]; then
|
||||||
|
options+=("Convert PRD/spec document (${#DETECTED_PRD_FILES[@]} found)")
|
||||||
|
option_keys+=("prd")
|
||||||
|
fi
|
||||||
|
|
||||||
|
options+=("Start with empty task list")
|
||||||
|
option_keys+=("none")
|
||||||
|
|
||||||
|
# Interactive selection
|
||||||
|
if [[ ${#options[@]} -gt 1 ]]; then
|
||||||
|
echo "Where would you like to import tasks from?"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
local selected_indices
|
||||||
|
selected_indices=$(select_multiple "Select task sources" "${options[@]}")
|
||||||
|
|
||||||
|
# Parse selected indices (comma-separated)
|
||||||
|
SELECTED_SOURCES=""
|
||||||
|
if [[ -n "$selected_indices" ]]; then
|
||||||
|
IFS=',' read -ra indices <<< "$selected_indices"
|
||||||
|
for idx in "${indices[@]}"; do
|
||||||
|
if [[ "${option_keys[$idx]}" != "none" ]]; then
|
||||||
|
SELECTED_SOURCES="${SELECTED_SOURCES:+$SELECTED_SOURCES }${option_keys[$idx]}"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
SELECTED_SOURCES=""
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Selected sources: ${SELECTED_SOURCES:-none}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# PHASE 3: CONFIGURATION
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
phase_configuration() {
|
||||||
|
print_header "Configuration" "Phase 3 of 5"
|
||||||
|
|
||||||
|
# Project name
|
||||||
|
if [[ "$NON_INTERACTIVE" != "true" ]]; then
|
||||||
|
CONFIG_PROJECT_NAME=$(prompt_text "Project name" "$DETECTED_PROJECT_NAME")
|
||||||
|
else
|
||||||
|
CONFIG_PROJECT_NAME="$DETECTED_PROJECT_NAME"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# API call limit
|
||||||
|
if [[ "$NON_INTERACTIVE" != "true" ]]; then
|
||||||
|
CONFIG_MAX_CALLS=$(prompt_number "Max API calls per hour" "100" "10" "500")
|
||||||
|
else
|
||||||
|
CONFIG_MAX_CALLS=100
|
||||||
|
fi
|
||||||
|
|
||||||
|
# GitHub label (if GitHub selected)
|
||||||
|
if echo "$SELECTED_SOURCES" | grep -qw "github"; then
|
||||||
|
if [[ -n "$GITHUB_LABEL" ]]; then
|
||||||
|
CONFIG_GITHUB_LABEL="$GITHUB_LABEL"
|
||||||
|
elif [[ "$NON_INTERACTIVE" != "true" ]]; then
|
||||||
|
CONFIG_GITHUB_LABEL=$(prompt_text "GitHub issue label filter" "ralph-task")
|
||||||
|
else
|
||||||
|
CONFIG_GITHUB_LABEL="ralph-task"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# PRD file selection (if PRD selected)
|
||||||
|
if echo "$SELECTED_SOURCES" | grep -qw "prd"; then
|
||||||
|
if [[ -n "$PRD_FILE" ]]; then
|
||||||
|
CONFIG_PRD_FILE="$PRD_FILE"
|
||||||
|
elif [[ "$NON_INTERACTIVE" != "true" && ${#DETECTED_PRD_FILES[@]} -gt 0 ]]; then
|
||||||
|
echo ""
|
||||||
|
echo "Found PRD files:"
|
||||||
|
CONFIG_PRD_FILE=$(select_option "Select PRD file to convert" "${DETECTED_PRD_FILES[@]}")
|
||||||
|
else
|
||||||
|
CONFIG_PRD_FILE="${DETECTED_PRD_FILES[0]:-}"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Show configuration summary
|
||||||
|
echo ""
|
||||||
|
print_summary "Configuration" \
|
||||||
|
"Project=$CONFIG_PROJECT_NAME" \
|
||||||
|
"Type=$DETECTED_PROJECT_TYPE" \
|
||||||
|
"Max calls/hour=$CONFIG_MAX_CALLS" \
|
||||||
|
"Task sources=${SELECTED_SOURCES:-none}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# PHASE 4: FILE GENERATION
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
phase_file_generation() {
|
||||||
|
print_header "File Generation" "Phase 4 of 5"
|
||||||
|
|
||||||
|
# Import tasks if sources selected
|
||||||
|
local imported_tasks=""
|
||||||
|
if [[ -n "$SELECTED_SOURCES" ]]; then
|
||||||
|
echo "Importing tasks..."
|
||||||
|
|
||||||
|
if echo "$SELECTED_SOURCES" | grep -qw "beads"; then
|
||||||
|
local beads_tasks
|
||||||
|
if beads_tasks=$(fetch_beads_tasks); then
|
||||||
|
imported_tasks="${imported_tasks}${beads_tasks}
|
||||||
|
"
|
||||||
|
print_success "Imported tasks from beads"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if echo "$SELECTED_SOURCES" | grep -qw "github"; then
|
||||||
|
local github_tasks
|
||||||
|
if github_tasks=$(fetch_github_tasks "$CONFIG_GITHUB_LABEL"); then
|
||||||
|
imported_tasks="${imported_tasks}${github_tasks}
|
||||||
|
"
|
||||||
|
print_success "Imported tasks from GitHub"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if echo "$SELECTED_SOURCES" | grep -qw "prd"; then
|
||||||
|
if [[ -n "$CONFIG_PRD_FILE" && -f "$CONFIG_PRD_FILE" ]]; then
|
||||||
|
local prd_tasks
|
||||||
|
if prd_tasks=$(extract_prd_tasks "$CONFIG_PRD_FILE"); then
|
||||||
|
imported_tasks="${imported_tasks}${prd_tasks}
|
||||||
|
"
|
||||||
|
print_success "Extracted tasks from PRD: $CONFIG_PRD_FILE"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Set up enable environment
|
||||||
|
export ENABLE_FORCE="$FORCE_OVERWRITE"
|
||||||
|
export ENABLE_SKIP_TASKS="$SKIP_TASKS"
|
||||||
|
export ENABLE_PROJECT_NAME="$CONFIG_PROJECT_NAME"
|
||||||
|
export ENABLE_TASK_CONTENT="$imported_tasks"
|
||||||
|
|
||||||
|
# Run core enable logic
|
||||||
|
echo "Creating Ralph configuration..."
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
if ! enable_ralph_in_directory; then
|
||||||
|
print_error "Failed to enable Ralph"
|
||||||
|
exit $ENABLE_ERROR
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Update .ralphrc with specific settings
|
||||||
|
# Using awk instead of sed to avoid command injection from user input
|
||||||
|
if [[ -f ".ralphrc" ]]; then
|
||||||
|
# Update max calls (awk safely handles the value without shell interpretation)
|
||||||
|
awk -v val="$CONFIG_MAX_CALLS" '/^MAX_CALLS_PER_HOUR=/{$0="MAX_CALLS_PER_HOUR="val}1' .ralphrc > .ralphrc.tmp && mv .ralphrc.tmp .ralphrc
|
||||||
|
|
||||||
|
# Update GitHub label if set
|
||||||
|
if [[ -n "$CONFIG_GITHUB_LABEL" ]]; then
|
||||||
|
awk -v val="$CONFIG_GITHUB_LABEL" '/^GITHUB_TASK_LABEL=/{$0="GITHUB_TASK_LABEL=\""val"\""}1' .ralphrc > .ralphrc.tmp && mv .ralphrc.tmp .ralphrc
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
}
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# PHASE 5: VERIFICATION
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
phase_verification() {
|
||||||
|
print_header "Verification" "Phase 5 of 5"
|
||||||
|
|
||||||
|
echo "Checking created files..."
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Verify required files
|
||||||
|
local all_good=true
|
||||||
|
|
||||||
|
if [[ -f ".ralph/PROMPT.md" ]]; then
|
||||||
|
print_success ".ralph/PROMPT.md"
|
||||||
|
else
|
||||||
|
print_error ".ralph/PROMPT.md - MISSING"
|
||||||
|
all_good=false
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -f ".ralph/@fix_plan.md" ]]; then
|
||||||
|
print_success ".ralph/@fix_plan.md"
|
||||||
|
else
|
||||||
|
print_error ".ralph/@fix_plan.md - MISSING"
|
||||||
|
all_good=false
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -f ".ralph/@AGENT.md" ]]; then
|
||||||
|
print_success ".ralph/@AGENT.md"
|
||||||
|
else
|
||||||
|
print_error ".ralph/@AGENT.md - MISSING"
|
||||||
|
all_good=false
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -f ".ralphrc" ]]; then
|
||||||
|
print_success ".ralphrc"
|
||||||
|
else
|
||||||
|
print_warning ".ralphrc - MISSING (optional)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -d ".ralph/specs" ]]; then
|
||||||
|
print_success ".ralph/specs/"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -d ".ralph/logs" ]]; then
|
||||||
|
print_success ".ralph/logs/"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
if [[ "$all_good" == "true" ]]; then
|
||||||
|
print_success "Ralph enabled successfully!"
|
||||||
|
echo ""
|
||||||
|
echo "Next steps:"
|
||||||
|
echo ""
|
||||||
|
print_bullet "Review and customize .ralph/PROMPT.md" "1."
|
||||||
|
print_bullet "Edit tasks in .ralph/@fix_plan.md" "2."
|
||||||
|
print_bullet "Update build commands in .ralph/@AGENT.md" "3."
|
||||||
|
print_bullet "Start Ralph: ralph --monitor" "4."
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
if [[ "$NON_INTERACTIVE" != "true" ]]; then
|
||||||
|
if confirm "Show current status?" "y"; then
|
||||||
|
echo ""
|
||||||
|
ralph --status 2>/dev/null || echo "(ralph --status not available)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
print_error "Some files were not created. Please check the errors above."
|
||||||
|
exit $ENABLE_ERROR
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# MAIN
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
main() {
|
||||||
|
# Parse arguments
|
||||||
|
parse_arguments "$@"
|
||||||
|
|
||||||
|
# Show help if requested
|
||||||
|
if [[ "$SHOW_HELP" == "true" ]]; then
|
||||||
|
show_help
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Welcome banner
|
||||||
|
echo ""
|
||||||
|
echo -e "\033[1m╔════════════════════════════════════════════════════════════╗\033[0m"
|
||||||
|
echo -e "\033[1m║ Ralph Enable - Existing Project Wizard ║\033[0m"
|
||||||
|
echo -e "\033[1m╚════════════════════════════════════════════════════════════╝\033[0m"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Run phases
|
||||||
|
phase_environment_detection
|
||||||
|
phase_task_source_selection
|
||||||
|
phase_configuration
|
||||||
|
phase_file_generation
|
||||||
|
phase_verification
|
||||||
|
|
||||||
|
exit $ENABLE_SUCCESS
|
||||||
|
}
|
||||||
|
|
||||||
|
# Run main
|
||||||
|
main "$@"
|
||||||
405
ralph_enable_ci.sh
Executable file
405
ralph_enable_ci.sh
Executable file
|
|
@ -0,0 +1,405 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Ralph Enable CI - Non-Interactive Version for Automation
|
||||||
|
# Adds Ralph configuration with sensible defaults
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# ralph enable-ci # Auto-detect and enable
|
||||||
|
# ralph enable-ci --from beads # With specific task source
|
||||||
|
# ralph enable-ci --json # Output JSON result
|
||||||
|
#
|
||||||
|
# Exit codes:
|
||||||
|
# 0 - Success: Ralph enabled
|
||||||
|
# 1 - Error: General error
|
||||||
|
# 2 - Already enabled (use --force to override)
|
||||||
|
# 3 - Invalid arguments
|
||||||
|
# 4 - File not found (e.g., PRD file)
|
||||||
|
# 5 - Dependency missing (e.g., jq for --json)
|
||||||
|
#
|
||||||
|
# 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", "code": 1}' >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Disable colors for CI
|
||||||
|
export ENABLE_USE_COLORS="false"
|
||||||
|
|
||||||
|
# Source libraries
|
||||||
|
source "$LIB_DIR/enable_core.sh"
|
||||||
|
source "$LIB_DIR/task_sources.sh"
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# CONFIGURATION
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# Command line options
|
||||||
|
FORCE_OVERWRITE=false
|
||||||
|
TASK_SOURCE=""
|
||||||
|
PRD_FILE=""
|
||||||
|
GITHUB_LABEL="ralph-task"
|
||||||
|
PROJECT_NAME=""
|
||||||
|
PROJECT_TYPE=""
|
||||||
|
OUTPUT_JSON=false
|
||||||
|
QUIET=false
|
||||||
|
SHOW_HELP=false
|
||||||
|
|
||||||
|
# Version
|
||||||
|
VERSION="0.11.0"
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# HELP
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
show_help() {
|
||||||
|
cat << EOF
|
||||||
|
Ralph Enable CI - Non-Interactive Version for Automation
|
||||||
|
|
||||||
|
Usage: ralph enable-ci [OPTIONS]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--from <source> Import tasks from: beads, github, prd, none
|
||||||
|
--prd <file> PRD file to convert (when --from prd)
|
||||||
|
--label <label> GitHub label filter (default: ralph-task)
|
||||||
|
--project-name <name> Override detected project name
|
||||||
|
--project-type <type> Override detected type (typescript, python, etc.)
|
||||||
|
--force Overwrite existing .ralph/ configuration
|
||||||
|
--json Output result as JSON
|
||||||
|
--quiet Suppress non-error output
|
||||||
|
-h, --help Show this help message
|
||||||
|
-v, --version Show version
|
||||||
|
|
||||||
|
Exit Codes:
|
||||||
|
0 - Success: Ralph enabled
|
||||||
|
1 - Error: General error
|
||||||
|
2 - Already enabled: Use --force to override
|
||||||
|
3 - Invalid arguments
|
||||||
|
4 - File not found (e.g., PRD file)
|
||||||
|
5 - Dependency missing (e.g., jq for --json)
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
# Auto-detect and enable with defaults
|
||||||
|
ralph enable-ci
|
||||||
|
|
||||||
|
# Enable with beads tasks
|
||||||
|
ralph enable-ci --from beads
|
||||||
|
|
||||||
|
# Enable with GitHub issues
|
||||||
|
ralph enable-ci --from github --label "sprint-1"
|
||||||
|
|
||||||
|
# Enable with PRD conversion
|
||||||
|
ralph enable-ci --from prd --prd docs/requirements.md
|
||||||
|
|
||||||
|
# Force overwrite and output JSON
|
||||||
|
ralph enable-ci --force --json
|
||||||
|
|
||||||
|
# Override project detection
|
||||||
|
ralph enable-ci --project-name my-app --project-type typescript
|
||||||
|
|
||||||
|
JSON Output Format:
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"project_name": "my-project",
|
||||||
|
"project_type": "typescript",
|
||||||
|
"files_created": [".ralph/PROMPT.md", ...],
|
||||||
|
"tasks_imported": 15,
|
||||||
|
"message": "Ralph enabled successfully"
|
||||||
|
}
|
||||||
|
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# ARGUMENT PARSING
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
parse_arguments() {
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--from)
|
||||||
|
if [[ -n "$2" && ! "$2" =~ ^-- ]]; then
|
||||||
|
TASK_SOURCE="$2"
|
||||||
|
shift 2
|
||||||
|
else
|
||||||
|
output_error "--from requires a source (beads, github, prd, none)"
|
||||||
|
exit $ENABLE_INVALID_ARGS
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
--prd)
|
||||||
|
if [[ -n "$2" && ! "$2" =~ ^-- ]]; then
|
||||||
|
PRD_FILE="$2"
|
||||||
|
shift 2
|
||||||
|
else
|
||||||
|
output_error "--prd requires a file path"
|
||||||
|
exit $ENABLE_INVALID_ARGS
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
--label)
|
||||||
|
if [[ -n "$2" && ! "$2" =~ ^-- ]]; then
|
||||||
|
GITHUB_LABEL="$2"
|
||||||
|
shift 2
|
||||||
|
else
|
||||||
|
output_error "--label requires a label name"
|
||||||
|
exit $ENABLE_INVALID_ARGS
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
--project-name)
|
||||||
|
if [[ -n "$2" && ! "$2" =~ ^-- ]]; then
|
||||||
|
PROJECT_NAME="$2"
|
||||||
|
shift 2
|
||||||
|
else
|
||||||
|
output_error "--project-name requires a name"
|
||||||
|
exit $ENABLE_INVALID_ARGS
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
--project-type)
|
||||||
|
if [[ -n "$2" && ! "$2" =~ ^-- ]]; then
|
||||||
|
PROJECT_TYPE="$2"
|
||||||
|
shift 2
|
||||||
|
else
|
||||||
|
output_error "--project-type requires a type"
|
||||||
|
exit $ENABLE_INVALID_ARGS
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
--force)
|
||||||
|
FORCE_OVERWRITE=true
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--json)
|
||||||
|
if ! command -v jq &>/dev/null; then
|
||||||
|
echo "Error: --json requires jq to be installed" >&2
|
||||||
|
exit $ENABLE_DEPENDENCY_MISSING
|
||||||
|
fi
|
||||||
|
OUTPUT_JSON=true
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--quiet)
|
||||||
|
QUIET=true
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
-h|--help)
|
||||||
|
SHOW_HELP=true
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
-v|--version)
|
||||||
|
if [[ "$OUTPUT_JSON" == "true" ]]; then
|
||||||
|
echo "{\"version\": \"$VERSION\"}"
|
||||||
|
else
|
||||||
|
echo "ralph enable-ci version $VERSION"
|
||||||
|
fi
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
output_error "Unknown option: $1"
|
||||||
|
exit $ENABLE_INVALID_ARGS
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# OUTPUT FUNCTIONS
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# Track created files for JSON output
|
||||||
|
declare -a CREATED_FILES=()
|
||||||
|
TASKS_IMPORTED=0
|
||||||
|
|
||||||
|
output_message() {
|
||||||
|
if [[ "$QUIET" != "true" && "$OUTPUT_JSON" != "true" ]]; then
|
||||||
|
echo "$1"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
output_error() {
|
||||||
|
if [[ "$OUTPUT_JSON" == "true" ]]; then
|
||||||
|
echo "{\"error\": \"$1\", \"code\": 1}" >&2
|
||||||
|
else
|
||||||
|
echo "Error: $1" >&2
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
output_success() {
|
||||||
|
local project_name="$1"
|
||||||
|
local project_type="$2"
|
||||||
|
|
||||||
|
if [[ "$OUTPUT_JSON" == "true" ]]; then
|
||||||
|
local files_json
|
||||||
|
files_json=$(printf '%s\n' "${CREATED_FILES[@]}" | jq -R . | jq -s .)
|
||||||
|
|
||||||
|
cat << EOF
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"project_name": "$project_name",
|
||||||
|
"project_type": "$project_type",
|
||||||
|
"files_created": $files_json,
|
||||||
|
"tasks_imported": $TASKS_IMPORTED,
|
||||||
|
"message": "Ralph enabled successfully"
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
else
|
||||||
|
echo "Ralph enabled successfully for: $project_name ($project_type)"
|
||||||
|
echo "Files created: ${#CREATED_FILES[@]}"
|
||||||
|
echo "Tasks imported: $TASKS_IMPORTED"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
output_already_enabled() {
|
||||||
|
if [[ "$OUTPUT_JSON" == "true" ]]; then
|
||||||
|
echo '{"success": false, "code": 2, "message": "Ralph already enabled. Use --force to override."}'
|
||||||
|
else
|
||||||
|
echo "Ralph is already enabled in this project. Use --force to override."
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# MAIN LOGIC
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
main() {
|
||||||
|
# Parse arguments
|
||||||
|
parse_arguments "$@"
|
||||||
|
|
||||||
|
# Show help if requested
|
||||||
|
if [[ "$SHOW_HELP" == "true" ]]; then
|
||||||
|
show_help
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
output_message "Ralph Enable CI - Non-Interactive Mode"
|
||||||
|
output_message ""
|
||||||
|
|
||||||
|
# Check existing state (use || true to prevent set -e from exiting)
|
||||||
|
check_existing_ralph || true
|
||||||
|
if [[ "$RALPH_STATE" == "complete" && "$FORCE_OVERWRITE" != "true" ]]; then
|
||||||
|
output_already_enabled
|
||||||
|
exit $ENABLE_ALREADY_ENABLED
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Detect project context
|
||||||
|
detect_project_context
|
||||||
|
output_message "Detected: $DETECTED_PROJECT_NAME ($DETECTED_PROJECT_TYPE)"
|
||||||
|
|
||||||
|
# Override with CLI options if provided
|
||||||
|
if [[ -n "$PROJECT_NAME" ]]; then
|
||||||
|
DETECTED_PROJECT_NAME="$PROJECT_NAME"
|
||||||
|
fi
|
||||||
|
if [[ -n "$PROJECT_TYPE" ]]; then
|
||||||
|
DETECTED_PROJECT_TYPE="$PROJECT_TYPE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Auto-detect task source if not specified
|
||||||
|
if [[ -z "$TASK_SOURCE" ]]; then
|
||||||
|
detect_task_sources
|
||||||
|
|
||||||
|
if [[ "$DETECTED_BEADS_AVAILABLE" == "true" ]]; then
|
||||||
|
TASK_SOURCE="beads"
|
||||||
|
output_message "Auto-detected task source: beads"
|
||||||
|
elif [[ "$DETECTED_GITHUB_AVAILABLE" == "true" ]]; then
|
||||||
|
TASK_SOURCE="github"
|
||||||
|
output_message "Auto-detected task source: github"
|
||||||
|
elif [[ ${#DETECTED_PRD_FILES[@]} -gt 0 ]]; then
|
||||||
|
TASK_SOURCE="prd"
|
||||||
|
PRD_FILE="${DETECTED_PRD_FILES[0]}"
|
||||||
|
output_message "Auto-detected task source: prd ($PRD_FILE)"
|
||||||
|
else
|
||||||
|
TASK_SOURCE="none"
|
||||||
|
output_message "No task sources detected, using defaults"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Import tasks
|
||||||
|
local imported_tasks=""
|
||||||
|
case "$TASK_SOURCE" in
|
||||||
|
beads)
|
||||||
|
if beads_tasks=$(fetch_beads_tasks 2>/dev/null); then
|
||||||
|
imported_tasks="$beads_tasks"
|
||||||
|
TASKS_IMPORTED=$(echo "$imported_tasks" | grep -c '^\- \[' || echo "0")
|
||||||
|
output_message "Imported $TASKS_IMPORTED tasks from beads"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
github)
|
||||||
|
if github_tasks=$(fetch_github_tasks "$GITHUB_LABEL" 2>/dev/null); then
|
||||||
|
imported_tasks="$github_tasks"
|
||||||
|
TASKS_IMPORTED=$(echo "$imported_tasks" | grep -c '^\- \[' || echo "0")
|
||||||
|
output_message "Imported $TASKS_IMPORTED tasks from GitHub"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
prd)
|
||||||
|
if [[ -n "$PRD_FILE" && -f "$PRD_FILE" ]]; then
|
||||||
|
if prd_tasks=$(extract_prd_tasks "$PRD_FILE" 2>/dev/null); then
|
||||||
|
imported_tasks="$prd_tasks"
|
||||||
|
TASKS_IMPORTED=$(echo "$imported_tasks" | grep -c '^\- \[' || echo "0")
|
||||||
|
output_message "Extracted $TASKS_IMPORTED tasks from PRD"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
output_error "PRD file not found: $PRD_FILE"
|
||||||
|
exit $ENABLE_FILE_NOT_FOUND
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
none|"")
|
||||||
|
output_message "Skipping task import"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
output_error "Unknown task source: $TASK_SOURCE"
|
||||||
|
exit $ENABLE_ERROR
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Set up enable environment
|
||||||
|
export ENABLE_FORCE="$FORCE_OVERWRITE"
|
||||||
|
export ENABLE_SKIP_TASKS="false"
|
||||||
|
export ENABLE_PROJECT_NAME="$DETECTED_PROJECT_NAME"
|
||||||
|
export ENABLE_PROJECT_TYPE="$DETECTED_PROJECT_TYPE"
|
||||||
|
export ENABLE_TASK_CONTENT="$imported_tasks"
|
||||||
|
|
||||||
|
# Run core enable logic
|
||||||
|
output_message ""
|
||||||
|
output_message "Creating Ralph configuration..."
|
||||||
|
|
||||||
|
# Suppress enable_ralph_in_directory output when in JSON mode
|
||||||
|
if [[ "$OUTPUT_JSON" == "true" ]]; then
|
||||||
|
if ! enable_ralph_in_directory >/dev/null 2>&1; then
|
||||||
|
output_error "Failed to enable Ralph"
|
||||||
|
exit $ENABLE_ERROR
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
if ! enable_ralph_in_directory; then
|
||||||
|
output_error "Failed to enable Ralph"
|
||||||
|
exit $ENABLE_ERROR
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Track created files
|
||||||
|
[[ -f ".ralph/PROMPT.md" ]] && CREATED_FILES+=(".ralph/PROMPT.md")
|
||||||
|
[[ -f ".ralph/@fix_plan.md" ]] && CREATED_FILES+=(".ralph/@fix_plan.md")
|
||||||
|
[[ -f ".ralph/@AGENT.md" ]] && CREATED_FILES+=(".ralph/@AGENT.md")
|
||||||
|
[[ -f ".ralphrc" ]] && CREATED_FILES+=(".ralphrc")
|
||||||
|
|
||||||
|
# Verify required files exist
|
||||||
|
if [[ ! -f ".ralph/PROMPT.md" ]] || [[ ! -f ".ralph/@fix_plan.md" ]]; then
|
||||||
|
output_error "Required files were not created"
|
||||||
|
exit $ENABLE_ERROR
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Output success
|
||||||
|
output_message ""
|
||||||
|
output_success "$DETECTED_PROJECT_NAME" "$DETECTED_PROJECT_TYPE"
|
||||||
|
|
||||||
|
exit $ENABLE_SUCCESS
|
||||||
|
}
|
||||||
|
|
||||||
|
# Run main
|
||||||
|
main "$@"
|
||||||
|
|
@ -21,18 +21,30 @@ DOCS_DIR="$RALPH_DIR/docs/generated"
|
||||||
STATUS_FILE="$RALPH_DIR/status.json"
|
STATUS_FILE="$RALPH_DIR/status.json"
|
||||||
PROGRESS_FILE="$RALPH_DIR/progress.json"
|
PROGRESS_FILE="$RALPH_DIR/progress.json"
|
||||||
CLAUDE_CODE_CMD="claude"
|
CLAUDE_CODE_CMD="claude"
|
||||||
MAX_CALLS_PER_HOUR=100 # Adjust based on your plan
|
|
||||||
VERBOSE_PROGRESS=false # Default: no verbose progress updates
|
|
||||||
CLAUDE_TIMEOUT_MINUTES=15 # Default: 15 minutes timeout for Claude Code execution
|
|
||||||
SLEEP_DURATION=3600 # 1 hour in seconds
|
SLEEP_DURATION=3600 # 1 hour in seconds
|
||||||
CALL_COUNT_FILE="$RALPH_DIR/.call_count"
|
CALL_COUNT_FILE="$RALPH_DIR/.call_count"
|
||||||
TIMESTAMP_FILE="$RALPH_DIR/.last_reset"
|
TIMESTAMP_FILE="$RALPH_DIR/.last_reset"
|
||||||
USE_TMUX=false
|
USE_TMUX=false
|
||||||
|
|
||||||
|
# Save environment variable state BEFORE setting defaults
|
||||||
|
# These are used by load_ralphrc() to determine which values came from environment
|
||||||
|
_env_MAX_CALLS_PER_HOUR="${MAX_CALLS_PER_HOUR:-}"
|
||||||
|
_env_CLAUDE_TIMEOUT_MINUTES="${CLAUDE_TIMEOUT_MINUTES:-}"
|
||||||
|
_env_CLAUDE_OUTPUT_FORMAT="${CLAUDE_OUTPUT_FORMAT:-}"
|
||||||
|
_env_CLAUDE_ALLOWED_TOOLS="${CLAUDE_ALLOWED_TOOLS:-}"
|
||||||
|
_env_CLAUDE_USE_CONTINUE="${CLAUDE_USE_CONTINUE:-}"
|
||||||
|
_env_CLAUDE_SESSION_EXPIRY_HOURS="${CLAUDE_SESSION_EXPIRY_HOURS:-}"
|
||||||
|
_env_VERBOSE_PROGRESS="${VERBOSE_PROGRESS:-}"
|
||||||
|
|
||||||
|
# Now set defaults (only if not already set by environment)
|
||||||
|
MAX_CALLS_PER_HOUR="${MAX_CALLS_PER_HOUR:-100}"
|
||||||
|
VERBOSE_PROGRESS="${VERBOSE_PROGRESS:-false}"
|
||||||
|
CLAUDE_TIMEOUT_MINUTES="${CLAUDE_TIMEOUT_MINUTES:-15}"
|
||||||
|
|
||||||
# Modern Claude CLI configuration (Phase 1.1)
|
# Modern Claude CLI configuration (Phase 1.1)
|
||||||
CLAUDE_OUTPUT_FORMAT="json" # Options: json, text
|
CLAUDE_OUTPUT_FORMAT="${CLAUDE_OUTPUT_FORMAT:-json}"
|
||||||
CLAUDE_ALLOWED_TOOLS="Write,Bash(git *),Read" # Comma-separated list of allowed tools
|
CLAUDE_ALLOWED_TOOLS="${CLAUDE_ALLOWED_TOOLS:-Write,Bash(git *),Read}"
|
||||||
CLAUDE_USE_CONTINUE=true # Enable session continuity
|
CLAUDE_USE_CONTINUE="${CLAUDE_USE_CONTINUE:-true}"
|
||||||
CLAUDE_SESSION_FILE="$RALPH_DIR/.claude_session_id" # Session ID persistence file
|
CLAUDE_SESSION_FILE="$RALPH_DIR/.claude_session_id" # Session ID persistence file
|
||||||
CLAUDE_MIN_VERSION="2.0.76" # Minimum required Claude CLI version
|
CLAUDE_MIN_VERSION="2.0.76" # Minimum required Claude CLI version
|
||||||
|
|
||||||
|
|
@ -73,6 +85,65 @@ MAX_CONSECUTIVE_TEST_LOOPS=3
|
||||||
MAX_CONSECUTIVE_DONE_SIGNALS=2
|
MAX_CONSECUTIVE_DONE_SIGNALS=2
|
||||||
TEST_PERCENTAGE_THRESHOLD=30 # If more than 30% of recent loops are test-only, flag it
|
TEST_PERCENTAGE_THRESHOLD=30 # If more than 30% of recent loops are test-only, flag it
|
||||||
|
|
||||||
|
# .ralphrc configuration file
|
||||||
|
RALPHRC_FILE=".ralphrc"
|
||||||
|
RALPHRC_LOADED=false
|
||||||
|
|
||||||
|
# load_ralphrc - Load project-specific configuration from .ralphrc
|
||||||
|
#
|
||||||
|
# This function sources .ralphrc if it exists, applying project-specific
|
||||||
|
# settings. Environment variables take precedence over .ralphrc values.
|
||||||
|
#
|
||||||
|
# Configuration values that can be overridden:
|
||||||
|
# - MAX_CALLS_PER_HOUR
|
||||||
|
# - CLAUDE_TIMEOUT_MINUTES
|
||||||
|
# - CLAUDE_OUTPUT_FORMAT
|
||||||
|
# - ALLOWED_TOOLS (mapped to CLAUDE_ALLOWED_TOOLS)
|
||||||
|
# - SESSION_CONTINUITY (mapped to CLAUDE_USE_CONTINUE)
|
||||||
|
# - SESSION_EXPIRY_HOURS (mapped to CLAUDE_SESSION_EXPIRY_HOURS)
|
||||||
|
# - CB_NO_PROGRESS_THRESHOLD
|
||||||
|
# - CB_SAME_ERROR_THRESHOLD
|
||||||
|
# - CB_OUTPUT_DECLINE_THRESHOLD
|
||||||
|
# - RALPH_VERBOSE
|
||||||
|
#
|
||||||
|
load_ralphrc() {
|
||||||
|
if [[ ! -f "$RALPHRC_FILE" ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Source .ralphrc (this may override default values)
|
||||||
|
# shellcheck source=/dev/null
|
||||||
|
source "$RALPHRC_FILE"
|
||||||
|
|
||||||
|
# Map .ralphrc variable names to internal names
|
||||||
|
if [[ -n "${ALLOWED_TOOLS:-}" ]]; then
|
||||||
|
CLAUDE_ALLOWED_TOOLS="$ALLOWED_TOOLS"
|
||||||
|
fi
|
||||||
|
if [[ -n "${SESSION_CONTINUITY:-}" ]]; then
|
||||||
|
CLAUDE_USE_CONTINUE="$SESSION_CONTINUITY"
|
||||||
|
fi
|
||||||
|
if [[ -n "${SESSION_EXPIRY_HOURS:-}" ]]; then
|
||||||
|
CLAUDE_SESSION_EXPIRY_HOURS="$SESSION_EXPIRY_HOURS"
|
||||||
|
fi
|
||||||
|
if [[ -n "${RALPH_VERBOSE:-}" ]]; then
|
||||||
|
VERBOSE_PROGRESS="$RALPH_VERBOSE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Restore ONLY values that were explicitly set via environment variables
|
||||||
|
# (not script defaults). The _env_* variables were captured BEFORE defaults were set.
|
||||||
|
# If _env_* is non-empty, the user explicitly set it in their environment.
|
||||||
|
[[ -n "$_env_MAX_CALLS_PER_HOUR" ]] && MAX_CALLS_PER_HOUR="$_env_MAX_CALLS_PER_HOUR"
|
||||||
|
[[ -n "$_env_CLAUDE_TIMEOUT_MINUTES" ]] && CLAUDE_TIMEOUT_MINUTES="$_env_CLAUDE_TIMEOUT_MINUTES"
|
||||||
|
[[ -n "$_env_CLAUDE_OUTPUT_FORMAT" ]] && CLAUDE_OUTPUT_FORMAT="$_env_CLAUDE_OUTPUT_FORMAT"
|
||||||
|
[[ -n "$_env_CLAUDE_ALLOWED_TOOLS" ]] && CLAUDE_ALLOWED_TOOLS="$_env_CLAUDE_ALLOWED_TOOLS"
|
||||||
|
[[ -n "$_env_CLAUDE_USE_CONTINUE" ]] && CLAUDE_USE_CONTINUE="$_env_CLAUDE_USE_CONTINUE"
|
||||||
|
[[ -n "$_env_CLAUDE_SESSION_EXPIRY_HOURS" ]] && CLAUDE_SESSION_EXPIRY_HOURS="$_env_CLAUDE_SESSION_EXPIRY_HOURS"
|
||||||
|
[[ -n "$_env_VERBOSE_PROGRESS" ]] && VERBOSE_PROGRESS="$_env_VERBOSE_PROGRESS"
|
||||||
|
|
||||||
|
RALPHRC_LOADED=true
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
# Colors for terminal output
|
# Colors for terminal output
|
||||||
RED='\033[0;31m'
|
RED='\033[0;31m'
|
||||||
GREEN='\033[0;32m'
|
GREEN='\033[0;32m'
|
||||||
|
|
@ -1053,6 +1124,12 @@ loop_count=0
|
||||||
|
|
||||||
# Main loop
|
# Main loop
|
||||||
main() {
|
main() {
|
||||||
|
# Load project-specific configuration from .ralphrc
|
||||||
|
if load_ralphrc; then
|
||||||
|
if [[ "$RALPHRC_LOADED" == "true" ]]; then
|
||||||
|
log_status "INFO" "Loaded configuration from .ralphrc"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
log_status "SUCCESS" "🚀 Ralph loop starting with Claude Code"
|
log_status "SUCCESS" "🚀 Ralph loop starting with Claude Code"
|
||||||
log_status "INFO" "Max calls per hour: $MAX_CALLS_PER_HOUR"
|
log_status "INFO" "Max calls per hour: $MAX_CALLS_PER_HOUR"
|
||||||
|
|
@ -1087,10 +1164,11 @@ main() {
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "To fix this:"
|
echo "To fix this:"
|
||||||
echo " 1. Create a new project: ralph-setup my-project"
|
echo " 1. Enable Ralph in existing project: ralph-enable"
|
||||||
echo " 2. Import existing requirements: ralph-import requirements.md"
|
echo " 2. Create a new project: ralph-setup my-project"
|
||||||
echo " 3. Navigate to an existing Ralph project directory"
|
echo " 3. Import existing requirements: ralph-import requirements.md"
|
||||||
echo " 4. Or create .ralph/PROMPT.md manually in this directory"
|
echo " 4. Navigate to an existing Ralph project directory"
|
||||||
|
echo " 5. Or create .ralph/PROMPT.md manually in this directory"
|
||||||
echo ""
|
echo ""
|
||||||
echo "Ralph projects should contain: .ralph/PROMPT.md, .ralph/@fix_plan.md, .ralph/specs/, src/, etc."
|
echo "Ralph projects should contain: .ralph/PROMPT.md, .ralph/@fix_plan.md, .ralph/specs/, src/, etc."
|
||||||
exit 1
|
exit 1
|
||||||
|
|
|
||||||
95
templates/ralphrc.template
Normal file
95
templates/ralphrc.template
Normal file
|
|
@ -0,0 +1,95 @@
|
||||||
|
# .ralphrc - Ralph project configuration
|
||||||
|
# Generated by: ralph enable
|
||||||
|
# Documentation: https://github.com/frankbria/ralph-claude-code
|
||||||
|
#
|
||||||
|
# This file configures Ralph's behavior for this specific project.
|
||||||
|
# Values here override global Ralph defaults.
|
||||||
|
# Environment variables override values in this file.
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# PROJECT IDENTIFICATION
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# Project name (used in prompts and logging)
|
||||||
|
PROJECT_NAME="${PROJECT_NAME:-my-project}"
|
||||||
|
|
||||||
|
# Project type: javascript, typescript, python, rust, go, unknown
|
||||||
|
PROJECT_TYPE="${PROJECT_TYPE:-unknown}"
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# LOOP SETTINGS
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# Maximum API calls per hour (rate limiting)
|
||||||
|
MAX_CALLS_PER_HOUR=100
|
||||||
|
|
||||||
|
# Timeout for each Claude Code invocation (in minutes)
|
||||||
|
CLAUDE_TIMEOUT_MINUTES=15
|
||||||
|
|
||||||
|
# Output format: json (structured) or text (legacy)
|
||||||
|
CLAUDE_OUTPUT_FORMAT="json"
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# TOOL PERMISSIONS
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# Comma-separated list of allowed tools for Claude
|
||||||
|
# Common tools: Write, Read, Edit, Grep, Glob
|
||||||
|
# Bash patterns: Bash(git *), Bash(npm *), Bash(pytest)
|
||||||
|
ALLOWED_TOOLS="Write,Read,Edit,Bash(git *),Bash(npm *),Bash(pytest)"
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# SESSION MANAGEMENT
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# Enable session continuity (maintain context across loops)
|
||||||
|
SESSION_CONTINUITY=true
|
||||||
|
|
||||||
|
# Session expiration time in hours (start fresh after this time)
|
||||||
|
SESSION_EXPIRY_HOURS=24
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# TASK SOURCES
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# Where to import tasks from (comma-separated)
|
||||||
|
# Options: local, beads, github
|
||||||
|
TASK_SOURCES="local"
|
||||||
|
|
||||||
|
# GitHub label for task filtering (when github is in TASK_SOURCES)
|
||||||
|
GITHUB_TASK_LABEL="ralph-task"
|
||||||
|
|
||||||
|
# Beads filter for task import (when beads is in TASK_SOURCES)
|
||||||
|
BEADS_FILTER="status:open"
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# CIRCUIT BREAKER THRESHOLDS
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# Open circuit after N loops with no file changes
|
||||||
|
CB_NO_PROGRESS_THRESHOLD=3
|
||||||
|
|
||||||
|
# Open circuit after N loops with the same error
|
||||||
|
CB_SAME_ERROR_THRESHOLD=5
|
||||||
|
|
||||||
|
# Open circuit if output declines by more than N percent
|
||||||
|
CB_OUTPUT_DECLINE_THRESHOLD=70
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# ADVANCED SETTINGS
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# Minimum Claude CLI version required
|
||||||
|
CLAUDE_MIN_VERSION="2.0.76"
|
||||||
|
|
||||||
|
# Enable verbose logging
|
||||||
|
RALPH_VERBOSE=false
|
||||||
|
|
||||||
|
# Custom prompt file (relative to .ralph/)
|
||||||
|
# PROMPT_FILE="PROMPT.md"
|
||||||
|
|
||||||
|
# Custom fix plan file (relative to .ralph/)
|
||||||
|
# FIX_PLAN_FILE="@fix_plan.md"
|
||||||
|
|
||||||
|
# Custom agent file (relative to .ralph/)
|
||||||
|
# AGENT_FILE="@AGENT.md"
|
||||||
|
|
@ -80,6 +80,43 @@ EOF
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# Mock migrate_to_ralph_folder.sh
|
# Mock migrate_to_ralph_folder.sh
|
||||||
echo "Migration running"
|
echo "Migration running"
|
||||||
|
EOF
|
||||||
|
|
||||||
|
cat > "$MOCK_SOURCE_DIR/ralph_enable.sh" << 'EOF'
|
||||||
|
#!/bin/bash
|
||||||
|
# Mock ralph_enable.sh
|
||||||
|
echo "Ralph enable running"
|
||||||
|
EOF
|
||||||
|
|
||||||
|
cat > "$MOCK_SOURCE_DIR/ralph_enable_ci.sh" << 'EOF'
|
||||||
|
#!/bin/bash
|
||||||
|
# Mock ralph_enable_ci.sh
|
||||||
|
echo "Ralph enable CI running"
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Create mock lib files for new enable functionality
|
||||||
|
cat > "$MOCK_SOURCE_DIR/lib/enable_core.sh" << 'EOF'
|
||||||
|
#!/bin/bash
|
||||||
|
# Mock enable_core.sh
|
||||||
|
check_existing_ralph() { :; }
|
||||||
|
EOF
|
||||||
|
|
||||||
|
cat > "$MOCK_SOURCE_DIR/lib/wizard_utils.sh" << 'EOF'
|
||||||
|
#!/bin/bash
|
||||||
|
# Mock wizard_utils.sh
|
||||||
|
confirm() { :; }
|
||||||
|
EOF
|
||||||
|
|
||||||
|
cat > "$MOCK_SOURCE_DIR/lib/task_sources.sh" << 'EOF'
|
||||||
|
#!/bin/bash
|
||||||
|
# Mock task_sources.sh
|
||||||
|
fetch_beads_tasks() { :; }
|
||||||
|
EOF
|
||||||
|
|
||||||
|
cat > "$MOCK_SOURCE_DIR/lib/timeout_utils.sh" << 'EOF'
|
||||||
|
#!/bin/bash
|
||||||
|
# Mock timeout_utils.sh
|
||||||
|
portable_timeout() { timeout "$@"; }
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
chmod +x "$MOCK_SOURCE_DIR"/*.sh
|
chmod +x "$MOCK_SOURCE_DIR"/*.sh
|
||||||
|
|
|
||||||
|
|
@ -157,8 +157,9 @@ teardown() {
|
||||||
|
|
||||||
@test "CLAUDE_OUTPUT_FORMAT defaults to json" {
|
@test "CLAUDE_OUTPUT_FORMAT defaults to json" {
|
||||||
# Verify by checking the default in ralph_loop.sh via grep
|
# Verify by checking the default in ralph_loop.sh via grep
|
||||||
|
# The default is set via ${CLAUDE_OUTPUT_FORMAT:-json} pattern
|
||||||
run grep 'CLAUDE_OUTPUT_FORMAT=' "${BATS_TEST_DIRNAME}/../../ralph_loop.sh"
|
run grep 'CLAUDE_OUTPUT_FORMAT=' "${BATS_TEST_DIRNAME}/../../ralph_loop.sh"
|
||||||
[[ "$output" == *'"json"'* ]]
|
[[ "$output" == *"json"* ]]
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "CLAUDE_ALLOWED_TOOLS has sensible defaults" {
|
@test "CLAUDE_ALLOWED_TOOLS has sensible defaults" {
|
||||||
|
|
@ -622,3 +623,29 @@ EOF
|
||||||
|
|
||||||
[[ "$found_prompt" == "true" ]]
|
[[ "$found_prompt" == "true" ]]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# .RALPHRC CONFIGURATION LOADING TESTS
|
||||||
|
# Tests for the environment variable precedence fix
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
@test "load_ralphrc uses env var capture pattern for precedence" {
|
||||||
|
# Verify the implementation pattern: _env_* variables capture state before defaults
|
||||||
|
# This test validates the pattern is correctly implemented in ralph_loop.sh
|
||||||
|
|
||||||
|
run grep '_env_MAX_CALLS_PER_HOUR=' "${BATS_TEST_DIRNAME}/../../ralph_loop.sh"
|
||||||
|
|
||||||
|
# Should capture env var state BEFORE setting defaults
|
||||||
|
[[ "$output" == *'${MAX_CALLS_PER_HOUR:-}'* ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "load_ralphrc restores only env var overrides, not defaults" {
|
||||||
|
# Verify that load_ralphrc uses _env_* pattern for restoration
|
||||||
|
# This ensures .ralphrc values are not overwritten by script defaults
|
||||||
|
|
||||||
|
run grep -A5 'Restore ONLY values' "${BATS_TEST_DIRNAME}/../../ralph_loop.sh"
|
||||||
|
|
||||||
|
# Should check _env_* variables (not saved_* which would always have values)
|
||||||
|
[[ "$output" == *'_env_MAX_CALLS_PER_HOUR'* ]]
|
||||||
|
[[ "$output" == *'_env_CLAUDE_TIMEOUT_MINUTES'* ]]
|
||||||
|
}
|
||||||
|
|
|
||||||
388
tests/unit/test_enable_core.bats
Normal file
388
tests/unit/test_enable_core.bats
Normal file
|
|
@ -0,0 +1,388 @@
|
||||||
|
#!/usr/bin/env bats
|
||||||
|
# Unit tests for lib/enable_core.sh
|
||||||
|
# Tests idempotency, safe file creation, project detection, and template generation
|
||||||
|
|
||||||
|
load '../helpers/test_helper'
|
||||||
|
load '../helpers/fixtures'
|
||||||
|
|
||||||
|
# Path to enable_core.sh
|
||||||
|
ENABLE_CORE="${BATS_TEST_DIRNAME}/../../lib/enable_core.sh"
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
# Create temporary test directory
|
||||||
|
TEST_DIR="$(mktemp -d)"
|
||||||
|
cd "$TEST_DIR"
|
||||||
|
|
||||||
|
# Source the library (disable set -e for testing)
|
||||||
|
set +e
|
||||||
|
source "$ENABLE_CORE"
|
||||||
|
set -e
|
||||||
|
}
|
||||||
|
|
||||||
|
teardown() {
|
||||||
|
if [[ -n "$TEST_DIR" ]] && [[ -d "$TEST_DIR" ]]; then
|
||||||
|
cd /
|
||||||
|
rm -rf "$TEST_DIR"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# IDEMPOTENCY CHECKS (5 tests)
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
@test "check_existing_ralph returns 'none' when no .ralph directory exists" {
|
||||||
|
check_existing_ralph || true
|
||||||
|
|
||||||
|
assert_equal "$RALPH_STATE" "none"
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "check_existing_ralph returns 'complete' when all required files exist" {
|
||||||
|
mkdir -p .ralph
|
||||||
|
echo "# PROMPT" > .ralph/PROMPT.md
|
||||||
|
echo "# Fix Plan" > .ralph/@fix_plan.md
|
||||||
|
echo "# Agent" > .ralph/@AGENT.md
|
||||||
|
|
||||||
|
check_existing_ralph || true
|
||||||
|
|
||||||
|
assert_equal "$RALPH_STATE" "complete"
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "check_existing_ralph returns 'partial' when some files are missing" {
|
||||||
|
mkdir -p .ralph
|
||||||
|
echo "# PROMPT" > .ralph/PROMPT.md
|
||||||
|
# Missing @fix_plan.md and @AGENT.md
|
||||||
|
|
||||||
|
check_existing_ralph || true
|
||||||
|
|
||||||
|
assert_equal "$RALPH_STATE" "partial"
|
||||||
|
[[ " ${RALPH_MISSING_FILES[*]} " =~ ".ralph/@fix_plan.md" ]]
|
||||||
|
[[ " ${RALPH_MISSING_FILES[*]} " =~ ".ralph/@AGENT.md" ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "is_ralph_enabled returns 0 when fully enabled" {
|
||||||
|
mkdir -p .ralph
|
||||||
|
echo "# PROMPT" > .ralph/PROMPT.md
|
||||||
|
echo "# Fix Plan" > .ralph/@fix_plan.md
|
||||||
|
echo "# Agent" > .ralph/@AGENT.md
|
||||||
|
|
||||||
|
run is_ralph_enabled
|
||||||
|
assert_success
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "is_ralph_enabled returns 1 when not enabled" {
|
||||||
|
run is_ralph_enabled
|
||||||
|
assert_failure
|
||||||
|
}
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# SAFE FILE OPERATIONS (5 tests)
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
@test "safe_create_file creates file that doesn't exist" {
|
||||||
|
run safe_create_file "test.txt" "test content"
|
||||||
|
|
||||||
|
assert_success
|
||||||
|
[[ -f "test.txt" ]]
|
||||||
|
[[ "$(cat test.txt)" == "test content" ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "safe_create_file skips existing file" {
|
||||||
|
echo "original content" > existing.txt
|
||||||
|
|
||||||
|
run safe_create_file "existing.txt" "new content"
|
||||||
|
|
||||||
|
assert_failure # Returns 1 for skip
|
||||||
|
assert_equal "$(cat existing.txt)" "original content"
|
||||||
|
[[ "$output" =~ "SKIP" ]] || [[ "$output" =~ "already exists" ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "safe_create_file creates parent directories" {
|
||||||
|
run safe_create_file "nested/dir/file.txt" "nested content"
|
||||||
|
|
||||||
|
assert_success
|
||||||
|
[[ -f "nested/dir/file.txt" ]]
|
||||||
|
[[ "$(cat nested/dir/file.txt)" == "nested content" ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "safe_create_dir creates directory that doesn't exist" {
|
||||||
|
run safe_create_dir "new_dir"
|
||||||
|
|
||||||
|
assert_success
|
||||||
|
[[ -d "new_dir" ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "safe_create_dir succeeds when directory already exists" {
|
||||||
|
mkdir existing_dir
|
||||||
|
|
||||||
|
run safe_create_dir "existing_dir"
|
||||||
|
|
||||||
|
assert_success
|
||||||
|
[[ -d "existing_dir" ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# DIRECTORY STRUCTURE (2 tests)
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
@test "create_ralph_structure creates all required directories" {
|
||||||
|
run create_ralph_structure
|
||||||
|
|
||||||
|
assert_success
|
||||||
|
[[ -d ".ralph" ]]
|
||||||
|
[[ -d ".ralph/specs" ]]
|
||||||
|
[[ -d ".ralph/examples" ]]
|
||||||
|
[[ -d ".ralph/logs" ]]
|
||||||
|
[[ -d ".ralph/docs/generated" ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "create_ralph_structure is idempotent" {
|
||||||
|
create_ralph_structure
|
||||||
|
echo "test" > .ralph/specs/test.txt
|
||||||
|
|
||||||
|
run create_ralph_structure
|
||||||
|
|
||||||
|
assert_success
|
||||||
|
[[ -f ".ralph/specs/test.txt" ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# PROJECT DETECTION (6 tests)
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
@test "detect_project_context identifies TypeScript from package.json" {
|
||||||
|
cat > package.json << 'EOF'
|
||||||
|
{
|
||||||
|
"name": "my-ts-project",
|
||||||
|
"devDependencies": {
|
||||||
|
"typescript": "^5.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
detect_project_context
|
||||||
|
|
||||||
|
assert_equal "$DETECTED_PROJECT_TYPE" "typescript"
|
||||||
|
assert_equal "$DETECTED_PROJECT_NAME" "my-ts-project"
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "detect_project_context identifies JavaScript from package.json" {
|
||||||
|
cat > package.json << 'EOF'
|
||||||
|
{
|
||||||
|
"name": "my-js-project"
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
detect_project_context
|
||||||
|
|
||||||
|
assert_equal "$DETECTED_PROJECT_TYPE" "javascript"
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "detect_project_context identifies Python from pyproject.toml" {
|
||||||
|
cat > pyproject.toml << 'EOF'
|
||||||
|
[project]
|
||||||
|
name = "my-python-project"
|
||||||
|
EOF
|
||||||
|
|
||||||
|
detect_project_context
|
||||||
|
|
||||||
|
assert_equal "$DETECTED_PROJECT_TYPE" "python"
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "detect_project_context identifies Next.js framework" {
|
||||||
|
cat > package.json << 'EOF'
|
||||||
|
{
|
||||||
|
"name": "nextjs-app",
|
||||||
|
"dependencies": {
|
||||||
|
"next": "^14.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
detect_project_context
|
||||||
|
|
||||||
|
assert_equal "$DETECTED_FRAMEWORK" "nextjs"
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "detect_project_context identifies FastAPI framework" {
|
||||||
|
cat > pyproject.toml << 'EOF'
|
||||||
|
[project]
|
||||||
|
name = "fastapi-app"
|
||||||
|
dependencies = ["fastapi>=0.100.0"]
|
||||||
|
EOF
|
||||||
|
|
||||||
|
detect_project_context
|
||||||
|
|
||||||
|
assert_equal "$DETECTED_FRAMEWORK" "fastapi"
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "detect_project_context falls back to folder name" {
|
||||||
|
detect_project_context
|
||||||
|
|
||||||
|
# Should use the temp directory name
|
||||||
|
[[ -n "$DETECTED_PROJECT_NAME" ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# GIT DETECTION (3 tests)
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
@test "detect_git_info detects git repository" {
|
||||||
|
git init >/dev/null 2>&1
|
||||||
|
|
||||||
|
detect_git_info
|
||||||
|
|
||||||
|
assert_equal "$DETECTED_GIT_REPO" "true"
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "detect_git_info detects non-git directory" {
|
||||||
|
detect_git_info
|
||||||
|
|
||||||
|
assert_equal "$DETECTED_GIT_REPO" "false"
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "detect_git_info detects GitHub remote" {
|
||||||
|
git init >/dev/null 2>&1
|
||||||
|
git remote add origin git@github.com:user/repo.git 2>/dev/null || true
|
||||||
|
|
||||||
|
detect_git_info
|
||||||
|
|
||||||
|
assert_equal "$DETECTED_GIT_GITHUB" "true"
|
||||||
|
}
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# TASK SOURCE DETECTION (2 tests)
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
@test "detect_task_sources detects .beads directory" {
|
||||||
|
mkdir -p .beads
|
||||||
|
|
||||||
|
detect_task_sources
|
||||||
|
|
||||||
|
assert_equal "$DETECTED_BEADS_AVAILABLE" "true"
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "detect_task_sources finds PRD files" {
|
||||||
|
mkdir -p docs
|
||||||
|
echo "# Requirements" > docs/requirements.md
|
||||||
|
|
||||||
|
detect_task_sources
|
||||||
|
|
||||||
|
[[ ${#DETECTED_PRD_FILES[@]} -gt 0 ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# TEMPLATE GENERATION (4 tests)
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
@test "generate_prompt_md includes project name" {
|
||||||
|
output=$(generate_prompt_md "my-project" "typescript")
|
||||||
|
|
||||||
|
[[ "$output" =~ "my-project" ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "generate_prompt_md includes project type" {
|
||||||
|
output=$(generate_prompt_md "my-project" "python")
|
||||||
|
|
||||||
|
[[ "$output" =~ "python" ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "generate_agent_md includes build command" {
|
||||||
|
output=$(generate_agent_md "npm run build" "npm test" "npm start")
|
||||||
|
|
||||||
|
[[ "$output" =~ "npm run build" ]]
|
||||||
|
[[ "$output" =~ "npm test" ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "generate_ralphrc includes project configuration" {
|
||||||
|
output=$(generate_ralphrc "my-project" "typescript" "local,beads")
|
||||||
|
|
||||||
|
[[ "$output" =~ "PROJECT_NAME=\"my-project\"" ]]
|
||||||
|
[[ "$output" =~ "PROJECT_TYPE=\"typescript\"" ]]
|
||||||
|
[[ "$output" =~ "TASK_SOURCES=\"local,beads\"" ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# FULL ENABLE FLOW (3 tests)
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
@test "enable_ralph_in_directory creates all required files" {
|
||||||
|
export ENABLE_FORCE="false"
|
||||||
|
export ENABLE_SKIP_TASKS="true"
|
||||||
|
export ENABLE_PROJECT_NAME="test-project"
|
||||||
|
|
||||||
|
run enable_ralph_in_directory
|
||||||
|
|
||||||
|
assert_success
|
||||||
|
[[ -f ".ralph/PROMPT.md" ]]
|
||||||
|
[[ -f ".ralph/@fix_plan.md" ]]
|
||||||
|
[[ -f ".ralph/@AGENT.md" ]]
|
||||||
|
[[ -f ".ralphrc" ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "enable_ralph_in_directory returns ALREADY_ENABLED when complete and no force" {
|
||||||
|
mkdir -p .ralph
|
||||||
|
echo "# PROMPT" > .ralph/PROMPT.md
|
||||||
|
echo "# Fix Plan" > .ralph/@fix_plan.md
|
||||||
|
echo "# Agent" > .ralph/@AGENT.md
|
||||||
|
|
||||||
|
export ENABLE_FORCE="false"
|
||||||
|
|
||||||
|
run enable_ralph_in_directory
|
||||||
|
|
||||||
|
assert_equal "$status" "$ENABLE_ALREADY_ENABLED"
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "enable_ralph_in_directory overwrites with force flag" {
|
||||||
|
mkdir -p .ralph
|
||||||
|
echo "old content" > .ralph/PROMPT.md
|
||||||
|
echo "old fix plan" > .ralph/@fix_plan.md
|
||||||
|
echo "old agent" > .ralph/@AGENT.md
|
||||||
|
|
||||||
|
export ENABLE_FORCE="true"
|
||||||
|
export ENABLE_PROJECT_NAME="new-project"
|
||||||
|
|
||||||
|
run enable_ralph_in_directory
|
||||||
|
|
||||||
|
assert_success
|
||||||
|
|
||||||
|
# Verify files were actually overwritten, not just skipped
|
||||||
|
local prompt_content
|
||||||
|
prompt_content=$(cat .ralph/PROMPT.md)
|
||||||
|
|
||||||
|
# Should contain new project name, not "old content"
|
||||||
|
[[ "$prompt_content" != "old content" ]]
|
||||||
|
[[ "$prompt_content" == *"new-project"* ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "safe_create_file overwrites existing file when ENABLE_FORCE is true" {
|
||||||
|
# Create existing file with old content
|
||||||
|
echo "original content" > test_file.txt
|
||||||
|
|
||||||
|
export ENABLE_FORCE="true"
|
||||||
|
|
||||||
|
run safe_create_file "test_file.txt" "new content"
|
||||||
|
|
||||||
|
assert_success
|
||||||
|
|
||||||
|
# Verify file was overwritten
|
||||||
|
local content
|
||||||
|
content=$(cat test_file.txt)
|
||||||
|
[[ "$content" == "new content" ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "safe_create_file skips existing file when ENABLE_FORCE is false" {
|
||||||
|
# Create existing file with old content
|
||||||
|
echo "original content" > test_file.txt
|
||||||
|
|
||||||
|
export ENABLE_FORCE="false"
|
||||||
|
|
||||||
|
run safe_create_file "test_file.txt" "new content"
|
||||||
|
|
||||||
|
# Should return 1 (skipped)
|
||||||
|
assert_failure
|
||||||
|
|
||||||
|
# Verify file was NOT overwritten
|
||||||
|
local content
|
||||||
|
content=$(cat test_file.txt)
|
||||||
|
[[ "$content" == "original content" ]]
|
||||||
|
}
|
||||||
268
tests/unit/test_ralph_enable.bats
Normal file
268
tests/unit/test_ralph_enable.bats
Normal file
|
|
@ -0,0 +1,268 @@
|
||||||
|
#!/usr/bin/env bats
|
||||||
|
# Integration tests for ralph_enable.sh and ralph_enable_ci.sh
|
||||||
|
# Tests the full enable wizard flow and CI version
|
||||||
|
|
||||||
|
load '../helpers/test_helper'
|
||||||
|
load '../helpers/fixtures'
|
||||||
|
|
||||||
|
# Paths to scripts
|
||||||
|
RALPH_ENABLE="${BATS_TEST_DIRNAME}/../../ralph_enable.sh"
|
||||||
|
RALPH_ENABLE_CI="${BATS_TEST_DIRNAME}/../../ralph_enable_ci.sh"
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
# Create temporary test directory
|
||||||
|
TEST_DIR="$(mktemp -d)"
|
||||||
|
cd "$TEST_DIR"
|
||||||
|
|
||||||
|
# Initialize git repo (required by some detection)
|
||||||
|
git init > /dev/null 2>&1
|
||||||
|
git config user.email "test@example.com"
|
||||||
|
git config user.name "Test User"
|
||||||
|
}
|
||||||
|
|
||||||
|
teardown() {
|
||||||
|
if [[ -n "$TEST_DIR" ]] && [[ -d "$TEST_DIR" ]]; then
|
||||||
|
cd /
|
||||||
|
rm -rf "$TEST_DIR"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# HELP AND VERSION (4 tests)
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
@test "ralph enable --help shows usage information" {
|
||||||
|
run bash "$RALPH_ENABLE" --help
|
||||||
|
|
||||||
|
assert_success
|
||||||
|
[[ "$output" =~ "Usage:" ]]
|
||||||
|
[[ "$output" =~ "--from" ]]
|
||||||
|
[[ "$output" =~ "--force" ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "ralph enable --version shows version" {
|
||||||
|
run bash "$RALPH_ENABLE" --version
|
||||||
|
|
||||||
|
assert_success
|
||||||
|
[[ "$output" =~ "version" ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "ralph enable-ci --help shows usage information" {
|
||||||
|
run bash "$RALPH_ENABLE_CI" --help
|
||||||
|
|
||||||
|
assert_success
|
||||||
|
[[ "$output" =~ "Usage:" ]]
|
||||||
|
[[ "$output" =~ "Exit Codes:" ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "ralph enable-ci --version shows version" {
|
||||||
|
run bash "$RALPH_ENABLE_CI" --version
|
||||||
|
|
||||||
|
assert_success
|
||||||
|
[[ "$output" =~ "version" ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# CI VERSION TESTS (8 tests)
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
@test "ralph enable-ci creates .ralph structure in empty directory" {
|
||||||
|
run bash "$RALPH_ENABLE_CI" --from none
|
||||||
|
|
||||||
|
assert_success
|
||||||
|
[[ -d ".ralph" ]]
|
||||||
|
[[ -f ".ralph/PROMPT.md" ]]
|
||||||
|
[[ -f ".ralph/@fix_plan.md" ]]
|
||||||
|
[[ -f ".ralph/@AGENT.md" ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "ralph enable-ci creates .ralphrc configuration" {
|
||||||
|
run bash "$RALPH_ENABLE_CI" --from none
|
||||||
|
|
||||||
|
assert_success
|
||||||
|
[[ -f ".ralphrc" ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "ralph enable-ci detects TypeScript project" {
|
||||||
|
cat > package.json << 'EOF'
|
||||||
|
{
|
||||||
|
"name": "test-ts-project",
|
||||||
|
"devDependencies": {
|
||||||
|
"typescript": "^5.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
run bash "$RALPH_ENABLE_CI" --from none
|
||||||
|
|
||||||
|
assert_success
|
||||||
|
grep -q "PROJECT_TYPE=\"typescript\"" .ralphrc
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "ralph enable-ci detects Python project" {
|
||||||
|
cat > pyproject.toml << 'EOF'
|
||||||
|
[project]
|
||||||
|
name = "test-python-project"
|
||||||
|
EOF
|
||||||
|
|
||||||
|
run bash "$RALPH_ENABLE_CI" --from none
|
||||||
|
|
||||||
|
assert_success
|
||||||
|
grep -q "PROJECT_TYPE=\"python\"" .ralphrc
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "ralph enable-ci respects --project-name override" {
|
||||||
|
run bash "$RALPH_ENABLE_CI" --from none --project-name "custom-name"
|
||||||
|
|
||||||
|
assert_success
|
||||||
|
grep -q "PROJECT_NAME=\"custom-name\"" .ralphrc
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "ralph enable-ci respects --project-type override" {
|
||||||
|
run bash "$RALPH_ENABLE_CI" --from none --project-type "rust"
|
||||||
|
|
||||||
|
assert_success
|
||||||
|
grep -q "PROJECT_TYPE=\"rust\"" .ralphrc
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "ralph enable-ci returns exit code 2 when already enabled" {
|
||||||
|
# First enable
|
||||||
|
bash "$RALPH_ENABLE_CI" --from none >/dev/null 2>&1
|
||||||
|
|
||||||
|
# Second enable without force
|
||||||
|
run bash "$RALPH_ENABLE_CI" --from none
|
||||||
|
|
||||||
|
assert_equal "$status" 2
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "ralph enable-ci --force overwrites existing configuration" {
|
||||||
|
# First enable
|
||||||
|
bash "$RALPH_ENABLE_CI" --from none --project-name "old-name" >/dev/null 2>&1
|
||||||
|
|
||||||
|
# Second enable with force
|
||||||
|
run bash "$RALPH_ENABLE_CI" --from none --force --project-name "new-name"
|
||||||
|
|
||||||
|
assert_success
|
||||||
|
}
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# JSON OUTPUT TESTS (3 tests)
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
@test "ralph enable-ci --json outputs valid JSON on success" {
|
||||||
|
run bash "$RALPH_ENABLE_CI" --from none --json
|
||||||
|
|
||||||
|
assert_success
|
||||||
|
# Validate JSON structure
|
||||||
|
echo "$output" | jq -e '.success == true'
|
||||||
|
echo "$output" | jq -e '.project_name'
|
||||||
|
echo "$output" | jq -e '.files_created'
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "ralph enable-ci --json includes project info" {
|
||||||
|
cat > package.json << 'EOF'
|
||||||
|
{"name": "json-test"}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
run bash "$RALPH_ENABLE_CI" --from none --json
|
||||||
|
|
||||||
|
assert_success
|
||||||
|
echo "$output" | jq -e '.project_name == "json-test"'
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "ralph enable-ci --json returns proper structure when already enabled" {
|
||||||
|
bash "$RALPH_ENABLE_CI" --from none >/dev/null 2>&1
|
||||||
|
|
||||||
|
run bash "$RALPH_ENABLE_CI" --from none --json
|
||||||
|
|
||||||
|
assert_equal "$status" 2
|
||||||
|
echo "$output" | jq -e '.code == 2'
|
||||||
|
}
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# PRD IMPORT TESTS (2 tests)
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
@test "ralph enable-ci imports tasks from PRD file" {
|
||||||
|
mkdir -p docs
|
||||||
|
cat > docs/requirements.md << 'EOF'
|
||||||
|
# Project Requirements
|
||||||
|
|
||||||
|
- [ ] Implement user authentication
|
||||||
|
- [ ] Add API endpoints
|
||||||
|
- [ ] Create database schema
|
||||||
|
EOF
|
||||||
|
|
||||||
|
run bash "$RALPH_ENABLE_CI" --from prd --prd docs/requirements.md
|
||||||
|
|
||||||
|
assert_success
|
||||||
|
# Check that tasks were imported
|
||||||
|
grep -q "authentication\|API\|database" .ralph/@fix_plan.md
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "ralph enable-ci fails gracefully with missing PRD file" {
|
||||||
|
run bash "$RALPH_ENABLE_CI" --from prd --prd nonexistent.md
|
||||||
|
|
||||||
|
assert_failure
|
||||||
|
}
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# IDEMPOTENCY TESTS (3 tests)
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
@test "ralph enable-ci is idempotent with force flag" {
|
||||||
|
bash "$RALPH_ENABLE_CI" --from none >/dev/null 2>&1
|
||||||
|
|
||||||
|
# Add a file to .ralph
|
||||||
|
echo "custom file" > .ralph/custom.txt
|
||||||
|
|
||||||
|
run bash "$RALPH_ENABLE_CI" --from none --force
|
||||||
|
|
||||||
|
assert_success
|
||||||
|
# Custom file should still exist (we don't delete extra files)
|
||||||
|
[[ -f ".ralph/custom.txt" ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "ralph enable-ci preserves existing .ralph subdirectories" {
|
||||||
|
bash "$RALPH_ENABLE_CI" --from none >/dev/null 2>&1
|
||||||
|
|
||||||
|
# Add custom content
|
||||||
|
echo "spec content" > .ralph/specs/custom_spec.md
|
||||||
|
|
||||||
|
run bash "$RALPH_ENABLE_CI" --from none --force
|
||||||
|
|
||||||
|
assert_success
|
||||||
|
[[ -f ".ralph/specs/custom_spec.md" ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "ralph enable-ci does not overwrite existing files without force" {
|
||||||
|
mkdir -p .ralph
|
||||||
|
echo "original prompt" > .ralph/PROMPT.md
|
||||||
|
echo "original fix plan" > .ralph/@fix_plan.md
|
||||||
|
echo "original agent" > .ralph/@AGENT.md
|
||||||
|
|
||||||
|
run bash "$RALPH_ENABLE_CI" --from none
|
||||||
|
|
||||||
|
assert_equal "$status" 2
|
||||||
|
# Verify original content preserved
|
||||||
|
assert_equal "$(cat .ralph/PROMPT.md)" "original prompt"
|
||||||
|
}
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# QUIET MODE TESTS (2 tests)
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
@test "ralph enable-ci --quiet suppresses output" {
|
||||||
|
run bash "$RALPH_ENABLE_CI" --from none --quiet
|
||||||
|
|
||||||
|
assert_success
|
||||||
|
# Output should be minimal
|
||||||
|
[[ -z "$output" ]] || [[ ! "$output" =~ "Detected" ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "ralph enable-ci --quiet still creates files" {
|
||||||
|
run bash "$RALPH_ENABLE_CI" --from none --quiet
|
||||||
|
|
||||||
|
assert_success
|
||||||
|
[[ -f ".ralph/PROMPT.md" ]]
|
||||||
|
}
|
||||||
|
|
@ -27,6 +27,7 @@ setup() {
|
||||||
export CLAUDE_SESSION_FILE="$RALPH_DIR/.claude_session_id"
|
export CLAUDE_SESSION_FILE="$RALPH_DIR/.claude_session_id"
|
||||||
export RALPH_SESSION_FILE="$RALPH_DIR/.ralph_session"
|
export RALPH_SESSION_FILE="$RALPH_DIR/.ralph_session"
|
||||||
export RALPH_SESSION_HISTORY_FILE="$RALPH_DIR/.ralph_session_history"
|
export RALPH_SESSION_HISTORY_FILE="$RALPH_DIR/.ralph_session_history"
|
||||||
|
export RESPONSE_ANALYSIS_FILE="$RALPH_DIR/.response_analysis"
|
||||||
export CLAUDE_MIN_VERSION="2.0.76"
|
export CLAUDE_MIN_VERSION="2.0.76"
|
||||||
export CLAUDE_CODE_CMD="claude"
|
export CLAUDE_CODE_CMD="claude"
|
||||||
export CLAUDE_USE_CONTINUE="true"
|
export CLAUDE_USE_CONTINUE="true"
|
||||||
|
|
|
||||||
269
tests/unit/test_task_sources.bats
Normal file
269
tests/unit/test_task_sources.bats
Normal file
|
|
@ -0,0 +1,269 @@
|
||||||
|
#!/usr/bin/env bats
|
||||||
|
# Unit tests for lib/task_sources.sh
|
||||||
|
# Tests beads integration, GitHub integration, PRD extraction, and task normalization
|
||||||
|
|
||||||
|
load '../helpers/test_helper'
|
||||||
|
load '../helpers/fixtures'
|
||||||
|
|
||||||
|
# Path to task_sources.sh
|
||||||
|
TASK_SOURCES="${BATS_TEST_DIRNAME}/../../lib/task_sources.sh"
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
# Create temporary test directory
|
||||||
|
TEST_DIR="$(mktemp -d)"
|
||||||
|
cd "$TEST_DIR"
|
||||||
|
|
||||||
|
# Source the library
|
||||||
|
source "$TASK_SOURCES"
|
||||||
|
}
|
||||||
|
|
||||||
|
teardown() {
|
||||||
|
if [[ -n "$TEST_DIR" ]] && [[ -d "$TEST_DIR" ]]; then
|
||||||
|
cd /
|
||||||
|
rm -rf "$TEST_DIR"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# BEADS DETECTION (3 tests)
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
@test "check_beads_available returns false when no .beads directory" {
|
||||||
|
run check_beads_available
|
||||||
|
assert_failure
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "check_beads_available returns false when bd command not found" {
|
||||||
|
mkdir -p .beads
|
||||||
|
# bd command likely won't exist in test environment
|
||||||
|
if command -v bd &>/dev/null; then
|
||||||
|
skip "bd command is available"
|
||||||
|
fi
|
||||||
|
run check_beads_available
|
||||||
|
assert_failure
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "get_beads_count returns 0 when beads unavailable" {
|
||||||
|
run get_beads_count
|
||||||
|
assert_output "0"
|
||||||
|
}
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# GITHUB DETECTION (3 tests)
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
@test "check_github_available returns false when no gh command" {
|
||||||
|
# gh command may not exist in test environment
|
||||||
|
if ! command -v gh &>/dev/null; then
|
||||||
|
run check_github_available
|
||||||
|
assert_failure
|
||||||
|
else
|
||||||
|
skip "gh command is available"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "check_github_available returns false when not in git repo" {
|
||||||
|
run check_github_available
|
||||||
|
assert_failure
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "get_github_issue_count returns 0 when GitHub unavailable" {
|
||||||
|
run get_github_issue_count
|
||||||
|
assert_output "0"
|
||||||
|
}
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# PRD EXTRACTION (6 tests)
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
@test "extract_prd_tasks extracts checkbox items" {
|
||||||
|
cat > prd.md << 'EOF'
|
||||||
|
# Requirements
|
||||||
|
|
||||||
|
- [ ] Implement user authentication
|
||||||
|
- [x] Set up database
|
||||||
|
- [ ] Add API endpoints
|
||||||
|
EOF
|
||||||
|
|
||||||
|
run extract_prd_tasks "prd.md"
|
||||||
|
|
||||||
|
assert_success
|
||||||
|
[[ "$output" =~ "Implement user authentication" ]]
|
||||||
|
[[ "$output" =~ "Add API endpoints" ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "extract_prd_tasks extracts numbered list items" {
|
||||||
|
cat > prd.md << 'EOF'
|
||||||
|
# Requirements
|
||||||
|
|
||||||
|
1. Implement user authentication
|
||||||
|
2. Set up database
|
||||||
|
3. Add API endpoints
|
||||||
|
EOF
|
||||||
|
|
||||||
|
run extract_prd_tasks "prd.md"
|
||||||
|
|
||||||
|
assert_success
|
||||||
|
[[ "$output" =~ "Implement user authentication" ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "extract_prd_tasks returns empty for file without tasks" {
|
||||||
|
cat > prd.md << 'EOF'
|
||||||
|
# Empty Document
|
||||||
|
|
||||||
|
This document has no tasks.
|
||||||
|
EOF
|
||||||
|
|
||||||
|
run extract_prd_tasks "prd.md"
|
||||||
|
|
||||||
|
assert_success
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "extract_prd_tasks returns error for missing file" {
|
||||||
|
run extract_prd_tasks "nonexistent.md"
|
||||||
|
assert_failure
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "extract_prd_tasks normalizes checked items to unchecked" {
|
||||||
|
cat > prd.md << 'EOF'
|
||||||
|
- [x] Completed task
|
||||||
|
- [X] Another completed
|
||||||
|
EOF
|
||||||
|
|
||||||
|
run extract_prd_tasks "prd.md"
|
||||||
|
|
||||||
|
assert_success
|
||||||
|
[[ "$output" =~ "[ ]" ]]
|
||||||
|
[[ ! "$output" =~ "[x]" ]]
|
||||||
|
[[ ! "$output" =~ "[X]" ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "extract_prd_tasks limits output to 30 tasks" {
|
||||||
|
# Create PRD with 40 tasks
|
||||||
|
{
|
||||||
|
echo "# Tasks"
|
||||||
|
for i in {1..40}; do
|
||||||
|
echo "- [ ] Task $i"
|
||||||
|
done
|
||||||
|
} > prd.md
|
||||||
|
|
||||||
|
run extract_prd_tasks "prd.md"
|
||||||
|
|
||||||
|
# Count the number of task lines
|
||||||
|
task_count=$(echo "$output" | grep -c '^\- \[' || echo "0")
|
||||||
|
[[ "$task_count" -le 30 ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# TASK NORMALIZATION (5 tests)
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
@test "normalize_tasks converts bullet points to checkboxes" {
|
||||||
|
input="- First task
|
||||||
|
* Second task"
|
||||||
|
|
||||||
|
run normalize_tasks "$input"
|
||||||
|
|
||||||
|
assert_success
|
||||||
|
[[ "$output" =~ "- [ ] First task" ]]
|
||||||
|
[[ "$output" =~ "- [ ] Second task" ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "normalize_tasks converts numbered items to checkboxes" {
|
||||||
|
input="1. First task
|
||||||
|
2. Second task"
|
||||||
|
|
||||||
|
run normalize_tasks "$input"
|
||||||
|
|
||||||
|
assert_success
|
||||||
|
[[ "$output" =~ "- [ ]" ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "normalize_tasks preserves existing checkboxes" {
|
||||||
|
input="- [ ] Already a task"
|
||||||
|
|
||||||
|
run normalize_tasks "$input"
|
||||||
|
|
||||||
|
assert_success
|
||||||
|
[[ "$output" =~ "- [ ] Already a task" ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "normalize_tasks handles plain text lines" {
|
||||||
|
input="Plain text task"
|
||||||
|
|
||||||
|
run normalize_tasks "$input"
|
||||||
|
|
||||||
|
assert_success
|
||||||
|
[[ "$output" =~ "- [ ] Plain text task" ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "normalize_tasks handles empty input" {
|
||||||
|
run normalize_tasks ""
|
||||||
|
assert_success
|
||||||
|
}
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# TASK PRIORITIZATION (3 tests)
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
@test "prioritize_tasks puts critical tasks in High Priority" {
|
||||||
|
input="- [ ] Critical bug fix
|
||||||
|
- [ ] Normal task"
|
||||||
|
|
||||||
|
output=$(prioritize_tasks "$input" || true)
|
||||||
|
|
||||||
|
[[ "$output" =~ "## High Priority" ]]
|
||||||
|
# Critical should be before Medium
|
||||||
|
high_section="${output%%## Medium*}"
|
||||||
|
[[ "$high_section" =~ "Critical bug fix" ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "prioritize_tasks puts optional tasks in Low Priority" {
|
||||||
|
input="- [ ] Nice to have feature
|
||||||
|
- [ ] Normal task"
|
||||||
|
|
||||||
|
run prioritize_tasks "$input"
|
||||||
|
|
||||||
|
assert_success
|
||||||
|
[[ "$output" =~ "## Low Priority" ]]
|
||||||
|
low_section="${output##*## Low Priority}"
|
||||||
|
[[ "$low_section" =~ "Nice to have" ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "prioritize_tasks puts regular tasks in Medium Priority" {
|
||||||
|
input="- [ ] Regular task"
|
||||||
|
|
||||||
|
output=$(prioritize_tasks "$input" || true)
|
||||||
|
|
||||||
|
[[ "$output" =~ "## Medium Priority" ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# COMBINED IMPORT (3 tests)
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
@test "import_tasks_from_sources handles prd source" {
|
||||||
|
mkdir -p docs
|
||||||
|
cat > docs/prd.md << 'EOF'
|
||||||
|
# Requirements
|
||||||
|
- [ ] Test task
|
||||||
|
EOF
|
||||||
|
|
||||||
|
run import_tasks_from_sources "prd" "docs/prd.md" ""
|
||||||
|
|
||||||
|
assert_success
|
||||||
|
[[ "$output" =~ "Test task" ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "import_tasks_from_sources handles empty sources" {
|
||||||
|
run import_tasks_from_sources "" "" ""
|
||||||
|
|
||||||
|
assert_failure
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "import_tasks_from_sources handles none source" {
|
||||||
|
run import_tasks_from_sources "none" "" ""
|
||||||
|
|
||||||
|
# 'none' doesn't import anything, so fails
|
||||||
|
assert_failure
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue