thrivekit 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/commands/explain.md +114 -0
- package/.claude/commands/idea.md +370 -0
- package/.claude/commands/my-dna.md +122 -0
- package/.claude/commands/prd.md +286 -0
- package/.claude/commands/review.md +167 -0
- package/.claude/commands/sign.md +32 -0
- package/.claude/commands/styleguide.md +450 -0
- package/.claude/commands/tour.md +301 -0
- package/.claude/commands/vibe-check.md +116 -0
- package/.claude/commands/vibe-help.md +47 -0
- package/.claude/commands/vibe-list.md +203 -0
- package/.claude/settings.json +75 -0
- package/.claude/settings.local.json +12 -0
- package/.pre-commit-hooks.yaml +102 -0
- package/LICENSE +21 -0
- package/README.md +214 -0
- package/bin/postinstall.sh +29 -0
- package/bin/ralph.sh +171 -0
- package/bin/thrivekit.sh +24 -0
- package/bin/vibe-check.js +19 -0
- package/dist/checks/check-any-types.d.ts +6 -0
- package/dist/checks/check-any-types.d.ts.map +1 -0
- package/dist/checks/check-any-types.js +73 -0
- package/dist/checks/check-any-types.js.map +1 -0
- package/dist/checks/check-commented-code.d.ts +6 -0
- package/dist/checks/check-commented-code.d.ts.map +1 -0
- package/dist/checks/check-commented-code.js +81 -0
- package/dist/checks/check-commented-code.js.map +1 -0
- package/dist/checks/check-console-error.d.ts +6 -0
- package/dist/checks/check-console-error.d.ts.map +1 -0
- package/dist/checks/check-console-error.js +41 -0
- package/dist/checks/check-console-error.js.map +1 -0
- package/dist/checks/check-debug-statements.d.ts +6 -0
- package/dist/checks/check-debug-statements.d.ts.map +1 -0
- package/dist/checks/check-debug-statements.js +120 -0
- package/dist/checks/check-debug-statements.js.map +1 -0
- package/dist/checks/check-deep-nesting.d.ts +6 -0
- package/dist/checks/check-deep-nesting.d.ts.map +1 -0
- package/dist/checks/check-deep-nesting.js +116 -0
- package/dist/checks/check-deep-nesting.js.map +1 -0
- package/dist/checks/check-docker-platform.d.ts +6 -0
- package/dist/checks/check-docker-platform.d.ts.map +1 -0
- package/dist/checks/check-docker-platform.js +42 -0
- package/dist/checks/check-docker-platform.js.map +1 -0
- package/dist/checks/check-dry-violations.d.ts +6 -0
- package/dist/checks/check-dry-violations.d.ts.map +1 -0
- package/dist/checks/check-dry-violations.js +124 -0
- package/dist/checks/check-dry-violations.js.map +1 -0
- package/dist/checks/check-empty-catch.d.ts +6 -0
- package/dist/checks/check-empty-catch.d.ts.map +1 -0
- package/dist/checks/check-empty-catch.js +111 -0
- package/dist/checks/check-empty-catch.js.map +1 -0
- package/dist/checks/check-function-length.d.ts +6 -0
- package/dist/checks/check-function-length.d.ts.map +1 -0
- package/dist/checks/check-function-length.js +152 -0
- package/dist/checks/check-function-length.js.map +1 -0
- package/dist/checks/check-hardcoded-ai-models.d.ts +10 -0
- package/dist/checks/check-hardcoded-ai-models.d.ts.map +1 -0
- package/dist/checks/check-hardcoded-ai-models.js +102 -0
- package/dist/checks/check-hardcoded-ai-models.js.map +1 -0
- package/dist/checks/check-hardcoded-urls.d.ts +6 -0
- package/dist/checks/check-hardcoded-urls.d.ts.map +1 -0
- package/dist/checks/check-hardcoded-urls.js +124 -0
- package/dist/checks/check-hardcoded-urls.js.map +1 -0
- package/dist/checks/check-magic-numbers.d.ts +6 -0
- package/dist/checks/check-magic-numbers.d.ts.map +1 -0
- package/dist/checks/check-magic-numbers.js +116 -0
- package/dist/checks/check-magic-numbers.js.map +1 -0
- package/dist/checks/check-secrets.d.ts +6 -0
- package/dist/checks/check-secrets.d.ts.map +1 -0
- package/dist/checks/check-secrets.js +138 -0
- package/dist/checks/check-secrets.js.map +1 -0
- package/dist/checks/check-snake-case-ts.d.ts +6 -0
- package/dist/checks/check-snake-case-ts.d.ts.map +1 -0
- package/dist/checks/check-snake-case-ts.js +78 -0
- package/dist/checks/check-snake-case-ts.js.map +1 -0
- package/dist/checks/check-todo-fixme.d.ts +6 -0
- package/dist/checks/check-todo-fixme.d.ts.map +1 -0
- package/dist/checks/check-todo-fixme.js +41 -0
- package/dist/checks/check-todo-fixme.js.map +1 -0
- package/dist/checks/check-unsafe-html.d.ts +6 -0
- package/dist/checks/check-unsafe-html.d.ts.map +1 -0
- package/dist/checks/check-unsafe-html.js +101 -0
- package/dist/checks/check-unsafe-html.js.map +1 -0
- package/dist/checks/index.d.ts +30 -0
- package/dist/checks/index.d.ts.map +1 -0
- package/dist/checks/index.js +57 -0
- package/dist/checks/index.js.map +1 -0
- package/dist/cli.d.ts +13 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +206 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- package/dist/utils/file-reader.d.ts +24 -0
- package/dist/utils/file-reader.d.ts.map +1 -0
- package/dist/utils/file-reader.js +140 -0
- package/dist/utils/file-reader.js.map +1 -0
- package/dist/utils/patterns.d.ts +27 -0
- package/dist/utils/patterns.d.ts.map +1 -0
- package/dist/utils/patterns.js +84 -0
- package/dist/utils/patterns.js.map +1 -0
- package/dist/utils/reporters.d.ts +21 -0
- package/dist/utils/reporters.d.ts.map +1 -0
- package/dist/utils/reporters.js +115 -0
- package/dist/utils/reporters.js.map +1 -0
- package/dist/utils/types.d.ts +71 -0
- package/dist/utils/types.d.ts.map +1 -0
- package/dist/utils/types.js +5 -0
- package/dist/utils/types.js.map +1 -0
- package/package.json +82 -0
- package/ralph/api.sh +210 -0
- package/ralph/backup.sh +838 -0
- package/ralph/browser-verify/README.md +135 -0
- package/ralph/browser-verify/verify.ts +450 -0
- package/ralph/checks/check-fastapi-responses.py +155 -0
- package/ralph/hooks/hooks-config.json +72 -0
- package/ralph/hooks/inject-context.sh +44 -0
- package/ralph/hooks/install.sh +207 -0
- package/ralph/hooks/log-tools.sh +45 -0
- package/ralph/hooks/protect-prd.sh +27 -0
- package/ralph/hooks/save-learnings.sh +36 -0
- package/ralph/hooks/warn-debug.sh +54 -0
- package/ralph/hooks/warn-empty-catch.sh +63 -0
- package/ralph/hooks/warn-secrets.sh +89 -0
- package/ralph/hooks/warn-urls.sh +77 -0
- package/ralph/init.sh +388 -0
- package/ralph/loop.sh +570 -0
- package/ralph/playwright.sh +238 -0
- package/ralph/prd.sh +295 -0
- package/ralph/setup/feature-tour.sh +155 -0
- package/ralph/setup/quick-setup.sh +239 -0
- package/ralph/setup/tutorial.sh +159 -0
- package/ralph/setup/ui.sh +136 -0
- package/ralph/setup.sh +353 -0
- package/ralph/signs.sh +150 -0
- package/ralph/utils.sh +682 -0
- package/ralph/verify/browser.sh +324 -0
- package/ralph/verify/lint.sh +363 -0
- package/ralph/verify/review.sh +164 -0
- package/ralph/verify/tests.sh +81 -0
- package/ralph/verify.sh +224 -0
- package/templates/PROMPT.md +235 -0
- package/templates/config/fullstack.json +86 -0
- package/templates/config/go.json +81 -0
- package/templates/config/minimal.json +76 -0
- package/templates/config/node.json +81 -0
- package/templates/config/python.json +81 -0
- package/templates/config/rust.json +81 -0
- package/templates/examples/CLAUDE-django.md +174 -0
- package/templates/examples/CLAUDE-fastapi.md +270 -0
- package/templates/examples/CLAUDE-fastmcp.md +352 -0
- package/templates/examples/CLAUDE-fullstack.md +256 -0
- package/templates/examples/CLAUDE-node.md +246 -0
- package/templates/examples/CLAUDE-react.md +138 -0
- package/templates/optional/cursorrules.template +147 -0
- package/templates/optional/eslint.config.js +34 -0
- package/templates/optional/lint-staged.config.js +34 -0
- package/templates/optional/ruff.toml +125 -0
- package/templates/optional/vibe-check.yml +116 -0
- package/templates/optional/vscode-settings.json +127 -0
- package/templates/signs.json +46 -0
package/ralph/utils.sh
ADDED
|
@@ -0,0 +1,682 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# shellcheck shell=bash
|
|
3
|
+
# utils.sh - Shared utility functions for ralph
|
|
4
|
+
|
|
5
|
+
# Constants - Output limits
|
|
6
|
+
readonly MAX_LOG_LINES=30
|
|
7
|
+
readonly MAX_PROGRESS_LINES=10
|
|
8
|
+
readonly MAX_GIT_STATUS_LINES=10
|
|
9
|
+
readonly MAX_OUTPUT_PREVIEW_LINES=20
|
|
10
|
+
readonly MAX_ERROR_PREVIEW_LINES=40
|
|
11
|
+
readonly MAX_LINT_ERROR_LINES=20
|
|
12
|
+
readonly MAX_PROGRESS_FILE_LINES=1000
|
|
13
|
+
|
|
14
|
+
# Constants - Timeouts (centralized to avoid magic numbers)
|
|
15
|
+
readonly ITERATION_DELAY_SECONDS=2
|
|
16
|
+
readonly DEFAULT_TIMEOUT_SECONDS=600
|
|
17
|
+
readonly DEFAULT_MAX_ITERATIONS=20
|
|
18
|
+
readonly CODE_REVIEW_TIMEOUT_SECONDS=120
|
|
19
|
+
readonly BROWSER_TIMEOUT_SECONDS=60
|
|
20
|
+
readonly BROWSER_PAGE_TIMEOUT_MS=30000
|
|
21
|
+
readonly CURL_TIMEOUT_SECONDS=10
|
|
22
|
+
|
|
23
|
+
# Common project directories (avoid duplication across files)
|
|
24
|
+
readonly FRONTEND_DIRS=("apps/web" "frontend" "client" "web")
|
|
25
|
+
readonly BACKEND_DIRS=("apps/api" "api" "backend" "server")
|
|
26
|
+
|
|
27
|
+
# Track temp files for safe cleanup
|
|
28
|
+
RALPH_TEMP_FILES=()
|
|
29
|
+
|
|
30
|
+
# Colors for output
|
|
31
|
+
RED='\033[0;31m'
|
|
32
|
+
GREEN='\033[0;32m'
|
|
33
|
+
YELLOW='\033[1;33m'
|
|
34
|
+
BLUE='\033[0;34m'
|
|
35
|
+
NC='\033[0m' # No Color
|
|
36
|
+
|
|
37
|
+
# Get existing frontend directories in this project
|
|
38
|
+
get_frontend_dirs() {
|
|
39
|
+
local dirs=()
|
|
40
|
+
for d in "${FRONTEND_DIRS[@]}"; do
|
|
41
|
+
[[ -d "$d" ]] && dirs+=("$d")
|
|
42
|
+
done
|
|
43
|
+
[[ ${#dirs[@]} -gt 0 ]] && printf '%s\n' "${dirs[@]}"
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
# Get existing backend directories in this project
|
|
47
|
+
get_backend_dirs() {
|
|
48
|
+
local dirs=()
|
|
49
|
+
for d in "${BACKEND_DIRS[@]}"; do
|
|
50
|
+
[[ -d "$d" ]] && dirs+=("$d")
|
|
51
|
+
done
|
|
52
|
+
[[ ${#dirs[@]} -gt 0 ]] && printf '%s\n' "${dirs[@]}"
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
# Progress bar for story display
|
|
56
|
+
progress_bar() {
|
|
57
|
+
local current=$1 total=$2 width=${3:-6}
|
|
58
|
+
local filled=$((current * width / total))
|
|
59
|
+
local empty=$((width - filled))
|
|
60
|
+
printf '%*s' "$filled" '' | tr ' ' '█'
|
|
61
|
+
printf '%*s' "$empty" '' | tr ' ' '░'
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
# Emoji for story type
|
|
65
|
+
type_emoji() {
|
|
66
|
+
case "$1" in
|
|
67
|
+
frontend) echo "📦" ;;
|
|
68
|
+
backend) echo "⚙️" ;;
|
|
69
|
+
testing) echo "🧪" ;;
|
|
70
|
+
*) echo "📝" ;;
|
|
71
|
+
esac
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
# Print colored output
|
|
75
|
+
print_error() { echo -e "${RED}Error: $1${NC}" >&2; }
|
|
76
|
+
print_success() { echo -e "${GREEN}$1${NC}"; }
|
|
77
|
+
print_warning() { echo -e "${YELLOW}$1${NC}"; }
|
|
78
|
+
print_info() { echo -e "${BLUE}$1${NC}"; }
|
|
79
|
+
|
|
80
|
+
# Require a file to exist
|
|
81
|
+
require_file() {
|
|
82
|
+
local file="$1"
|
|
83
|
+
local msg="${2:-File not found: $file}"
|
|
84
|
+
if [[ ! -f "$file" ]]; then
|
|
85
|
+
print_error "$msg"
|
|
86
|
+
exit 1
|
|
87
|
+
fi
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
# Require a directory to exist
|
|
91
|
+
require_dir() {
|
|
92
|
+
local dir="$1"
|
|
93
|
+
local msg="${2:-Directory not found: $dir}"
|
|
94
|
+
if [[ ! -d "$dir" ]]; then
|
|
95
|
+
print_error "$msg"
|
|
96
|
+
exit 1
|
|
97
|
+
fi
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
# Require a command to be available
|
|
101
|
+
require_command() {
|
|
102
|
+
local cmd="$1"
|
|
103
|
+
local msg="${2:-Command not found: $cmd}"
|
|
104
|
+
if ! command -v "$cmd" &>/dev/null; then
|
|
105
|
+
print_error "$msg"
|
|
106
|
+
exit 1
|
|
107
|
+
fi
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
# Check all required dependencies
|
|
111
|
+
check_dependencies() {
|
|
112
|
+
local missing=()
|
|
113
|
+
|
|
114
|
+
# Required
|
|
115
|
+
command -v jq &>/dev/null || missing+=("jq (brew install jq)")
|
|
116
|
+
command -v claude &>/dev/null || missing+=("claude CLI (npm install -g @anthropic-ai/claude-code)")
|
|
117
|
+
|
|
118
|
+
# Optional but recommended
|
|
119
|
+
if ! command -v git &>/dev/null; then
|
|
120
|
+
print_warning "Warning: git not found, auto-commit disabled"
|
|
121
|
+
fi
|
|
122
|
+
|
|
123
|
+
if [[ ${#missing[@]} -gt 0 ]]; then
|
|
124
|
+
print_error "Missing required dependencies:"
|
|
125
|
+
printf ' - %s\n' "${missing[@]}"
|
|
126
|
+
exit 1
|
|
127
|
+
fi
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
# Log progress to progress.txt (with rotation to prevent unbounded growth)
|
|
131
|
+
log_progress() {
|
|
132
|
+
local story="$1"
|
|
133
|
+
local status="$2"
|
|
134
|
+
local msg="${3:-}"
|
|
135
|
+
local timestamp
|
|
136
|
+
local progress_file="$RALPH_DIR/progress.txt"
|
|
137
|
+
|
|
138
|
+
timestamp=$(date -Iseconds 2>/dev/null || date +%Y-%m-%dT%H:%M:%S)
|
|
139
|
+
echo "[$timestamp] $status $story $msg" >> "$progress_file"
|
|
140
|
+
|
|
141
|
+
# Rotate if file exceeds max lines (keep last half)
|
|
142
|
+
if [[ -f "$progress_file" ]]; then
|
|
143
|
+
local line_count
|
|
144
|
+
line_count=$(wc -l < "$progress_file" 2>/dev/null || echo "0")
|
|
145
|
+
if [[ "$line_count" -gt "$MAX_PROGRESS_FILE_LINES" ]]; then
|
|
146
|
+
local keep_lines=$((MAX_PROGRESS_FILE_LINES / 2))
|
|
147
|
+
tail -"$keep_lines" "$progress_file" > "$progress_file.tmp" && mv "$progress_file.tmp" "$progress_file"
|
|
148
|
+
fi
|
|
149
|
+
fi
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
# Get a value from config.json with a default
|
|
153
|
+
get_config() {
|
|
154
|
+
local key="$1"
|
|
155
|
+
local default="$2"
|
|
156
|
+
local config="$RALPH_DIR/config.json"
|
|
157
|
+
|
|
158
|
+
if [[ -f "$config" ]]; then
|
|
159
|
+
local value
|
|
160
|
+
value=$(jq -r "$key // empty" "$config" 2>/dev/null)
|
|
161
|
+
if [[ -n "$value" && "$value" != "null" ]]; then
|
|
162
|
+
echo "$value"
|
|
163
|
+
return
|
|
164
|
+
fi
|
|
165
|
+
fi
|
|
166
|
+
echo "$default"
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
# Cross-platform timeout (macOS needs gtimeout from coreutils)
|
|
170
|
+
run_with_timeout() {
|
|
171
|
+
local seconds="$1"
|
|
172
|
+
shift
|
|
173
|
+
|
|
174
|
+
if command -v timeout &>/dev/null; then
|
|
175
|
+
timeout "$seconds" "$@"
|
|
176
|
+
elif command -v gtimeout &>/dev/null; then
|
|
177
|
+
gtimeout "$seconds" "$@"
|
|
178
|
+
else
|
|
179
|
+
# Fallback: just run without timeout (safe - Claude sessions complete on their own)
|
|
180
|
+
"$@"
|
|
181
|
+
fi
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
# Safely update JSON file atomically
|
|
185
|
+
# Usage: update_json <file> [jq args...] <filter>
|
|
186
|
+
# Example: update_json file.json --arg id "TASK-001" '.stories[] | select(.id==$id)'
|
|
187
|
+
update_json() {
|
|
188
|
+
local file="$1"
|
|
189
|
+
shift
|
|
190
|
+
local tmpfile lockdir
|
|
191
|
+
tmpfile=$(mktemp)
|
|
192
|
+
lockdir="${file}.lock"
|
|
193
|
+
|
|
194
|
+
# Acquire lock (mkdir is atomic)
|
|
195
|
+
local attempts=0
|
|
196
|
+
while ! mkdir "$lockdir" 2>/dev/null; do
|
|
197
|
+
((attempts++))
|
|
198
|
+
if [[ $attempts -gt 50 ]]; then
|
|
199
|
+
print_error "Could not acquire lock on $file"
|
|
200
|
+
rm -f "$tmpfile"
|
|
201
|
+
return 1
|
|
202
|
+
fi
|
|
203
|
+
sleep 0.1
|
|
204
|
+
done
|
|
205
|
+
|
|
206
|
+
# All remaining args go to jq (supports --arg, --argjson, etc.)
|
|
207
|
+
local result=0
|
|
208
|
+
if jq "$@" "$file" > "$tmpfile" 2>/dev/null; then
|
|
209
|
+
mv "$tmpfile" "$file"
|
|
210
|
+
else
|
|
211
|
+
rm -f "$tmpfile"
|
|
212
|
+
result=1
|
|
213
|
+
fi
|
|
214
|
+
|
|
215
|
+
# Release lock
|
|
216
|
+
rmdir "$lockdir" 2>/dev/null
|
|
217
|
+
return $result
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
# Create a temp file and track it for cleanup
|
|
221
|
+
create_temp_file() {
|
|
222
|
+
local suffix="${1:-.tmp}"
|
|
223
|
+
local tmpfile
|
|
224
|
+
# macOS mktemp doesn't support suffixes, so create then rename
|
|
225
|
+
tmpfile=$(mktemp 2>&1) || {
|
|
226
|
+
print_error "mktemp failed: $tmpfile"
|
|
227
|
+
return 1
|
|
228
|
+
}
|
|
229
|
+
if [[ "$suffix" != ".tmp" && -n "$suffix" ]]; then
|
|
230
|
+
if ! mv "$tmpfile" "${tmpfile}${suffix}" 2>/dev/null; then
|
|
231
|
+
print_error "Failed to rename temp file"
|
|
232
|
+
rm -f "$tmpfile"
|
|
233
|
+
return 1
|
|
234
|
+
fi
|
|
235
|
+
tmpfile="${tmpfile}${suffix}"
|
|
236
|
+
fi
|
|
237
|
+
RALPH_TEMP_FILES+=("$tmpfile")
|
|
238
|
+
echo "$tmpfile"
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
# Clean up only tracked temp files on exit
|
|
242
|
+
cleanup() {
|
|
243
|
+
if [[ ${#RALPH_TEMP_FILES[@]} -gt 0 ]]; then
|
|
244
|
+
for f in "${RALPH_TEMP_FILES[@]}"; do
|
|
245
|
+
rm -f "$f" 2>/dev/null
|
|
246
|
+
done
|
|
247
|
+
fi
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
# Set up trap for cleanup
|
|
251
|
+
trap cleanup EXIT
|
|
252
|
+
|
|
253
|
+
# Validate a command doesn't contain dangerous patterns
|
|
254
|
+
# Returns 0 if safe, 1 if dangerous
|
|
255
|
+
# Note: This is defense-in-depth. Commands come from user config, so user must trust their own config.
|
|
256
|
+
validate_command() {
|
|
257
|
+
local cmd="$1"
|
|
258
|
+
|
|
259
|
+
# Block obviously dangerous patterns (defense-in-depth, not security boundary)
|
|
260
|
+
local dangerous_patterns=(
|
|
261
|
+
# Destructive file operations
|
|
262
|
+
'rm[[:space:]]+-rf[[:space:]]+/' # rm -rf /
|
|
263
|
+
'rm[[:space:]]+-rf[[:space:]]+~' # rm -rf ~ (home dir)
|
|
264
|
+
'rm[[:space:]]+-rf[[:space:]]+\*' # rm -rf *
|
|
265
|
+
'rm[[:space:]]+-rf[[:space:]]+\.\.' # rm -rf ..
|
|
266
|
+
'rm[[:space:]].*--no-preserve-root' # rm with --no-preserve-root
|
|
267
|
+
# Remote code execution
|
|
268
|
+
'curl.*\|.*bash' # curl | bash
|
|
269
|
+
'curl.*\|.*sh[[:space:]]*$' # curl | sh
|
|
270
|
+
'wget.*\|.*bash' # wget | bash
|
|
271
|
+
'wget.*\|.*sh[[:space:]]*$' # wget | sh
|
|
272
|
+
'curl.*>[[:space:]]*/tmp/.*&&.*bash' # curl > /tmp/x && bash
|
|
273
|
+
# Code injection
|
|
274
|
+
'\$\([^)]*eval' # $(eval ...)
|
|
275
|
+
'eval[[:space:]]+\$' # eval $var
|
|
276
|
+
'eval[[:space:]]+["\x27]' # eval "..." or eval '...'
|
|
277
|
+
# System damage
|
|
278
|
+
'>[[:space:]]*/dev/sd' # write to disk devices
|
|
279
|
+
'>[[:space:]]*/dev/nvme' # write to nvme devices
|
|
280
|
+
'mkfs\.' # format filesystems
|
|
281
|
+
'dd[[:space:]]+if=' # dd commands
|
|
282
|
+
':(){:|:&};:' # fork bomb
|
|
283
|
+
# Credential theft
|
|
284
|
+
'cat.*\.ssh/id_' # read SSH keys
|
|
285
|
+
'cat.*/etc/shadow' # read shadow file
|
|
286
|
+
'cat.*\.aws/credentials' # read AWS creds
|
|
287
|
+
'cat.*\.env' # read env files (often has secrets)
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
for pattern in "${dangerous_patterns[@]}"; do
|
|
291
|
+
if [[ "$cmd" =~ $pattern ]]; then
|
|
292
|
+
print_error "Command blocked (dangerous pattern): $cmd"
|
|
293
|
+
log_progress "BLOCKED dangerous command: $cmd"
|
|
294
|
+
return 1
|
|
295
|
+
fi
|
|
296
|
+
done
|
|
297
|
+
|
|
298
|
+
return 0
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
# Validate a URL is safe (http/https only, no internal IPs in production)
|
|
302
|
+
validate_url() {
|
|
303
|
+
local url="$1"
|
|
304
|
+
|
|
305
|
+
# Must start with http:// or https://
|
|
306
|
+
if [[ ! "$url" =~ ^https?:// ]]; then
|
|
307
|
+
print_error "Invalid URL scheme (must be http or https): $url"
|
|
308
|
+
return 1
|
|
309
|
+
fi
|
|
310
|
+
|
|
311
|
+
# Block file:// and other dangerous schemes
|
|
312
|
+
if [[ "$url" =~ ^(file|ftp|data|javascript): ]]; then
|
|
313
|
+
print_error "Dangerous URL scheme: $url"
|
|
314
|
+
return 1
|
|
315
|
+
fi
|
|
316
|
+
|
|
317
|
+
return 0
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
# Safely execute a command (validates first, uses bash -c instead of eval)
|
|
321
|
+
safe_exec() {
|
|
322
|
+
local cmd="$1"
|
|
323
|
+
local log_file="${2:-/dev/null}"
|
|
324
|
+
|
|
325
|
+
# Validate command first
|
|
326
|
+
if ! validate_command "$cmd"; then
|
|
327
|
+
return 1
|
|
328
|
+
fi
|
|
329
|
+
|
|
330
|
+
# Execute with bash -c instead of eval
|
|
331
|
+
bash -c "$cmd" > "$log_file" 2>&1
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
# Set up or show notification config
|
|
335
|
+
ralph_notify() {
|
|
336
|
+
local config_dir="$HOME/.config/ralph"
|
|
337
|
+
local config_file="$config_dir/notify"
|
|
338
|
+
|
|
339
|
+
if [[ $# -eq 0 ]]; then
|
|
340
|
+
# Show current config
|
|
341
|
+
if [[ -f "$config_file" ]]; then
|
|
342
|
+
echo "Notification config (~/.config/ralph/notify):"
|
|
343
|
+
cat "$config_file"
|
|
344
|
+
else
|
|
345
|
+
echo "No notification configured."
|
|
346
|
+
echo ""
|
|
347
|
+
echo "To set up iMessage notifications (macOS):"
|
|
348
|
+
echo " npx ralph notify +15551234567"
|
|
349
|
+
echo ""
|
|
350
|
+
echo "Ralph will text you when the loop finishes."
|
|
351
|
+
fi
|
|
352
|
+
return 0
|
|
353
|
+
fi
|
|
354
|
+
|
|
355
|
+
local phone="$1"
|
|
356
|
+
|
|
357
|
+
# Validate phone format (basic check)
|
|
358
|
+
if [[ ! "$phone" =~ ^\+?[0-9]{10,15}$ ]]; then
|
|
359
|
+
print_error "Invalid phone number format. Use: +15551234567"
|
|
360
|
+
return 1
|
|
361
|
+
fi
|
|
362
|
+
|
|
363
|
+
# Create config directory and file
|
|
364
|
+
mkdir -p "$config_dir"
|
|
365
|
+
echo "phone=$phone" > "$config_file"
|
|
366
|
+
|
|
367
|
+
print_success "Notification configured!"
|
|
368
|
+
echo "Phone: $phone"
|
|
369
|
+
echo ""
|
|
370
|
+
echo "Ralph will send iMessage when the loop finishes."
|
|
371
|
+
echo "(Requires macOS with Messages signed into your Apple ID)"
|
|
372
|
+
|
|
373
|
+
# Test notification
|
|
374
|
+
echo ""
|
|
375
|
+
read -p "Send a test message? [y/N] " -n 1 -r
|
|
376
|
+
echo ""
|
|
377
|
+
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
|
378
|
+
send_notification "🧪 Test from Ralph - notifications are working!"
|
|
379
|
+
fi
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
# Send notification via iMessage (macOS only)
|
|
383
|
+
# Reads phone from ~/.config/ralph/notify (global, one-time setup)
|
|
384
|
+
send_notification() {
|
|
385
|
+
local message="$1"
|
|
386
|
+
local config_file="$HOME/.config/ralph/notify"
|
|
387
|
+
|
|
388
|
+
# No config file, skip silently
|
|
389
|
+
if [[ ! -f "$config_file" ]]; then
|
|
390
|
+
return 0
|
|
391
|
+
fi
|
|
392
|
+
|
|
393
|
+
local phone=""
|
|
394
|
+
phone=$(grep -E '^phone=' "$config_file" 2>/dev/null | cut -d'=' -f2 | tr -d '"' | tr -d "'" | xargs)
|
|
395
|
+
|
|
396
|
+
# No phone configured, skip silently
|
|
397
|
+
if [[ -z "$phone" ]]; then
|
|
398
|
+
return 0
|
|
399
|
+
fi
|
|
400
|
+
|
|
401
|
+
# macOS only - use iMessage
|
|
402
|
+
if [[ "$(uname)" == "Darwin" ]]; then
|
|
403
|
+
# Escape message for AppleScript (replace backslashes and quotes)
|
|
404
|
+
local escaped_message="${message//\\/\\\\}"
|
|
405
|
+
escaped_message="${escaped_message//\"/\\\"}"
|
|
406
|
+
osascript -e "tell application \"Messages\" to send \"$escaped_message\" to buddy \"$phone\"" 2>/dev/null || {
|
|
407
|
+
print_warning "Failed to send iMessage notification (is Messages app signed in?)"
|
|
408
|
+
return 1
|
|
409
|
+
}
|
|
410
|
+
print_info "Notification sent to $phone"
|
|
411
|
+
else
|
|
412
|
+
print_warning "Notifications only supported on macOS"
|
|
413
|
+
fi
|
|
414
|
+
|
|
415
|
+
return 0
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
# Validate PRD structure
|
|
419
|
+
# Returns 0 if valid, 1 if invalid with helpful error messages
|
|
420
|
+
validate_prd() {
|
|
421
|
+
local prd_file="$1"
|
|
422
|
+
|
|
423
|
+
# Check file exists
|
|
424
|
+
if [[ ! -f "$prd_file" ]]; then
|
|
425
|
+
print_error "PRD file not found: $prd_file"
|
|
426
|
+
return 1
|
|
427
|
+
fi
|
|
428
|
+
|
|
429
|
+
# Check valid JSON
|
|
430
|
+
if ! jq -e . "$prd_file" >/dev/null 2>&1; then
|
|
431
|
+
print_error "prd.json is not valid JSON."
|
|
432
|
+
echo ""
|
|
433
|
+
echo "Fix it manually or regenerate with:"
|
|
434
|
+
echo " /idea 'your feature'"
|
|
435
|
+
echo ""
|
|
436
|
+
return 1
|
|
437
|
+
fi
|
|
438
|
+
|
|
439
|
+
# Check feature.name is set
|
|
440
|
+
local feature_name
|
|
441
|
+
feature_name=$(jq -r '.feature.name // empty' "$prd_file" 2>/dev/null)
|
|
442
|
+
if [[ -z "$feature_name" || "$feature_name" == "null" ]]; then
|
|
443
|
+
print_error "prd.json is missing .feature.name"
|
|
444
|
+
echo ""
|
|
445
|
+
echo "Add a feature name to your PRD or regenerate with:"
|
|
446
|
+
echo " /idea 'your feature'"
|
|
447
|
+
echo ""
|
|
448
|
+
return 1
|
|
449
|
+
fi
|
|
450
|
+
|
|
451
|
+
# Check for stories array
|
|
452
|
+
if ! jq -e '.stories' "$prd_file" >/dev/null 2>&1; then
|
|
453
|
+
print_error "prd.json is missing 'stories' array."
|
|
454
|
+
echo ""
|
|
455
|
+
echo "Regenerate with: /idea 'your feature'"
|
|
456
|
+
echo ""
|
|
457
|
+
return 1
|
|
458
|
+
fi
|
|
459
|
+
|
|
460
|
+
# Check stories is not empty
|
|
461
|
+
local story_count
|
|
462
|
+
story_count=$(jq '.stories | length' "$prd_file" 2>/dev/null || echo "0")
|
|
463
|
+
if [[ "$story_count" == "0" ]]; then
|
|
464
|
+
print_error "prd.json has no stories."
|
|
465
|
+
echo ""
|
|
466
|
+
echo "Regenerate with: /idea 'your feature'"
|
|
467
|
+
echo ""
|
|
468
|
+
return 1
|
|
469
|
+
fi
|
|
470
|
+
|
|
471
|
+
# Check each story has required fields
|
|
472
|
+
local invalid_stories
|
|
473
|
+
invalid_stories=$(jq -r '.stories[] | select(.id == null or .id == "" or .title == null or .title == "") | .id // "unnamed"' "$prd_file" 2>/dev/null)
|
|
474
|
+
if [[ -n "$invalid_stories" ]]; then
|
|
475
|
+
print_error "Some stories are missing required fields (id, title):"
|
|
476
|
+
echo "$invalid_stories" | head -5
|
|
477
|
+
echo ""
|
|
478
|
+
echo "Fix the PRD or regenerate with: /idea 'your feature'"
|
|
479
|
+
echo ""
|
|
480
|
+
return 1
|
|
481
|
+
fi
|
|
482
|
+
|
|
483
|
+
# Check stories have passes field (initialize if missing)
|
|
484
|
+
local missing_passes
|
|
485
|
+
missing_passes=$(jq '[.stories[] | select(.passes == null)] | length' "$prd_file" 2>/dev/null || echo "0")
|
|
486
|
+
if [[ "$missing_passes" != "0" ]]; then
|
|
487
|
+
print_info "Initializing $missing_passes stories with passes=false..."
|
|
488
|
+
update_json "$prd_file" '(.stories[] | select(.passes == null) | .passes) = false'
|
|
489
|
+
fi
|
|
490
|
+
|
|
491
|
+
# Check feature name exists
|
|
492
|
+
local feature_name
|
|
493
|
+
feature_name=$(jq -r '.feature.name // empty' "$prd_file" 2>/dev/null)
|
|
494
|
+
if [[ -z "$feature_name" ]]; then
|
|
495
|
+
print_warning "PRD is missing feature name (will show as 'unnamed')"
|
|
496
|
+
fi
|
|
497
|
+
|
|
498
|
+
return 0
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
# Detect Python runner (uv, poetry, pipenv, or plain python)
|
|
502
|
+
detect_python_runner() {
|
|
503
|
+
local search_dir="${1:-.}"
|
|
504
|
+
|
|
505
|
+
# Check for uv (uv.lock or pyproject.toml with uv)
|
|
506
|
+
if [[ -f "$search_dir/uv.lock" ]]; then
|
|
507
|
+
echo "uv run"
|
|
508
|
+
return 0
|
|
509
|
+
fi
|
|
510
|
+
|
|
511
|
+
# Check for poetry
|
|
512
|
+
if [[ -f "$search_dir/poetry.lock" ]]; then
|
|
513
|
+
echo "poetry run"
|
|
514
|
+
return 0
|
|
515
|
+
fi
|
|
516
|
+
|
|
517
|
+
# Check for pipenv
|
|
518
|
+
if [[ -f "$search_dir/Pipfile.lock" ]]; then
|
|
519
|
+
echo "pipenv run"
|
|
520
|
+
return 0
|
|
521
|
+
fi
|
|
522
|
+
|
|
523
|
+
# Default to plain command (assumes activated venv or global)
|
|
524
|
+
echo ""
|
|
525
|
+
return 0
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
# Auto-detect migration tool and return the command
|
|
529
|
+
detect_migration_tool() {
|
|
530
|
+
local search_dir="${1:-.}"
|
|
531
|
+
local py_runner
|
|
532
|
+
py_runner=$(detect_python_runner "$search_dir")
|
|
533
|
+
|
|
534
|
+
# Alembic (Python/FastAPI/SQLAlchemy)
|
|
535
|
+
if [[ -f "$search_dir/alembic.ini" ]] || [[ -d "$search_dir/alembic" ]]; then
|
|
536
|
+
echo "cd $search_dir && ${py_runner}${py_runner:+ }alembic upgrade head"
|
|
537
|
+
return 0
|
|
538
|
+
fi
|
|
539
|
+
|
|
540
|
+
# Prisma (Node.js)
|
|
541
|
+
if [[ -d "$search_dir/prisma/migrations" ]] || [[ -f "$search_dir/prisma/schema.prisma" ]]; then
|
|
542
|
+
echo "cd $search_dir && npx prisma migrate deploy"
|
|
543
|
+
return 0
|
|
544
|
+
fi
|
|
545
|
+
|
|
546
|
+
# Django
|
|
547
|
+
if [[ -f "$search_dir/manage.py" ]] && [[ -d "$search_dir" ]] && find "$search_dir" -type d -name "migrations" -print -quit | grep -q .; then
|
|
548
|
+
echo "cd $search_dir && ${py_runner}${py_runner:+ }python manage.py migrate"
|
|
549
|
+
return 0
|
|
550
|
+
fi
|
|
551
|
+
|
|
552
|
+
# Sequelize (Node.js)
|
|
553
|
+
if [[ -f "$search_dir/.sequelizerc" ]]; then
|
|
554
|
+
echo "cd $search_dir && npx sequelize-cli db:migrate"
|
|
555
|
+
return 0
|
|
556
|
+
fi
|
|
557
|
+
|
|
558
|
+
# TypeORM (Node.js)
|
|
559
|
+
if [[ -f "$search_dir/ormconfig.json" ]] || grep -q '"typeorm"' "$search_dir/package.json" 2>/dev/null; then
|
|
560
|
+
echo "cd $search_dir && npx typeorm migration:run"
|
|
561
|
+
return 0
|
|
562
|
+
fi
|
|
563
|
+
|
|
564
|
+
# Knex (Node.js)
|
|
565
|
+
if [[ -f "$search_dir/knexfile.js" ]] || [[ -f "$search_dir/knexfile.ts" ]]; then
|
|
566
|
+
echo "cd $search_dir && npx knex migrate:latest"
|
|
567
|
+
return 0
|
|
568
|
+
fi
|
|
569
|
+
|
|
570
|
+
return 1
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
# Find all migration tools in project (searches common app directories)
|
|
574
|
+
find_all_migration_tools() {
|
|
575
|
+
local tools=()
|
|
576
|
+
|
|
577
|
+
# Check root
|
|
578
|
+
local root_tool
|
|
579
|
+
if root_tool=$(detect_migration_tool "."); then
|
|
580
|
+
tools+=("$root_tool")
|
|
581
|
+
fi
|
|
582
|
+
|
|
583
|
+
# Check common app directories
|
|
584
|
+
for dir in apps/* packages/* services/* api backend server; do
|
|
585
|
+
if [[ -d "$dir" ]]; then
|
|
586
|
+
local tool
|
|
587
|
+
if tool=$(detect_migration_tool "$dir"); then
|
|
588
|
+
tools+=("$tool")
|
|
589
|
+
fi
|
|
590
|
+
fi
|
|
591
|
+
done
|
|
592
|
+
|
|
593
|
+
# Return unique tools
|
|
594
|
+
printf '%s\n' "${tools[@]}" | sort -u
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
# Ensure database migrations are applied before verification
|
|
598
|
+
# Migration commands are idempotent - they no-op if nothing pending
|
|
599
|
+
run_migrations_if_needed() {
|
|
600
|
+
local pre_sha="$1" # unused now, kept for API compatibility
|
|
601
|
+
local config="$RALPH_DIR/config.json"
|
|
602
|
+
|
|
603
|
+
local migrate_cmd=""
|
|
604
|
+
|
|
605
|
+
# Try config first
|
|
606
|
+
if [[ -f "$config" ]]; then
|
|
607
|
+
migrate_cmd=$(jq -r '.migrations.command // empty' "$config" 2>/dev/null)
|
|
608
|
+
fi
|
|
609
|
+
|
|
610
|
+
# Auto-detect if not configured
|
|
611
|
+
if [[ -z "$migrate_cmd" ]]; then
|
|
612
|
+
local detected_tools
|
|
613
|
+
detected_tools=$(find_all_migration_tools)
|
|
614
|
+
|
|
615
|
+
if [[ -z "$detected_tools" ]]; then
|
|
616
|
+
return 0 # No migrations to run
|
|
617
|
+
fi
|
|
618
|
+
|
|
619
|
+
# Run all detected migration tools
|
|
620
|
+
local failed=0
|
|
621
|
+
while IFS= read -r tool_cmd; do
|
|
622
|
+
[[ -z "$tool_cmd" ]] && continue
|
|
623
|
+
echo -n " Migrations (auto-detected)... "
|
|
624
|
+
|
|
625
|
+
local log_file
|
|
626
|
+
log_file=$(mktemp)
|
|
627
|
+
|
|
628
|
+
if safe_exec "$tool_cmd" "$log_file"; then
|
|
629
|
+
if grep -qiE "applying|migrating|running|upgrade" "$log_file" 2>/dev/null; then
|
|
630
|
+
print_success "applied"
|
|
631
|
+
else
|
|
632
|
+
echo "up to date"
|
|
633
|
+
fi
|
|
634
|
+
else
|
|
635
|
+
print_error "failed"
|
|
636
|
+
echo " Command: $tool_cmd"
|
|
637
|
+
tail -10 "$log_file" | sed 's/^/ /'
|
|
638
|
+
# Save failure context for Claude
|
|
639
|
+
{
|
|
640
|
+
echo "Migration command: $tool_cmd"
|
|
641
|
+
echo ""
|
|
642
|
+
cat "$log_file"
|
|
643
|
+
} > "$RALPH_DIR/last_migration_failure.log"
|
|
644
|
+
failed=1
|
|
645
|
+
fi
|
|
646
|
+
rm -f "$log_file"
|
|
647
|
+
done <<< "$detected_tools"
|
|
648
|
+
|
|
649
|
+
return $failed
|
|
650
|
+
fi
|
|
651
|
+
|
|
652
|
+
# Always run migrations - commands are idempotent (no-op if nothing pending)
|
|
653
|
+
# This ensures DB schema is always in sync before tests run
|
|
654
|
+
echo -n " Ensuring migrations applied... "
|
|
655
|
+
|
|
656
|
+
local log_file
|
|
657
|
+
log_file=$(mktemp)
|
|
658
|
+
|
|
659
|
+
if safe_exec "$migrate_cmd" "$log_file"; then
|
|
660
|
+
# Check if any migrations were actually applied
|
|
661
|
+
if grep -qiE "applying|migrating|running|upgrade" "$log_file" 2>/dev/null; then
|
|
662
|
+
print_success "applied"
|
|
663
|
+
else
|
|
664
|
+
echo "up to date"
|
|
665
|
+
fi
|
|
666
|
+
rm -f "$log_file"
|
|
667
|
+
return 0
|
|
668
|
+
else
|
|
669
|
+
print_error "failed"
|
|
670
|
+
echo ""
|
|
671
|
+
echo " Migration error:"
|
|
672
|
+
tail -20 "$log_file" | sed 's/^/ /'
|
|
673
|
+
# Save failure context for Claude
|
|
674
|
+
{
|
|
675
|
+
echo "Migration command: $migrate_cmd"
|
|
676
|
+
echo ""
|
|
677
|
+
cat "$log_file"
|
|
678
|
+
} > "$RALPH_DIR/last_migration_failure.log"
|
|
679
|
+
rm -f "$log_file"
|
|
680
|
+
return 1
|
|
681
|
+
fi
|
|
682
|
+
}
|