ralph-cli-claude 0.1.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/README.md +149 -0
- package/dist/commands/docker.d.ts +1 -0
- package/dist/commands/docker.js +406 -0
- package/dist/commands/help.d.ts +1 -0
- package/dist/commands/help.js +44 -0
- package/dist/commands/init.d.ts +1 -0
- package/dist/commands/init.js +98 -0
- package/dist/commands/once.d.ts +1 -0
- package/dist/commands/once.js +28 -0
- package/dist/commands/prd.d.ts +1 -0
- package/dist/commands/prd.js +149 -0
- package/dist/commands/run.d.ts +1 -0
- package/dist/commands/run.js +64 -0
- package/dist/commands/scripts.d.ts +1 -0
- package/dist/commands/scripts.js +115 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +44 -0
- package/dist/templates/prompts.d.ts +10 -0
- package/dist/templates/prompts.js +72 -0
- package/dist/utils/config.d.ts +17 -0
- package/dist/utils/config.js +47 -0
- package/dist/utils/prompt.d.ts +7 -0
- package/dist/utils/prompt.js +53 -0
- package/package.json +53 -0
package/README.md
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# ralph-cli-claude
|
|
2
|
+
|
|
3
|
+
AI-driven development automation CLI for [Claude Code](https://github.com/anthropics/claude-code).
|
|
4
|
+
|
|
5
|
+
Ralph automates iterative development by having Claude work through a PRD (Product Requirements Document), implementing features one at a time, running tests, and committing changes.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
# Use directly with npx
|
|
11
|
+
npx ralph-cli-claude init
|
|
12
|
+
|
|
13
|
+
# Or install globally
|
|
14
|
+
npm install -g ralph-cli-claude
|
|
15
|
+
ralph init
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Quick Start
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
# 1. Initialize ralph in your project
|
|
22
|
+
ralph init
|
|
23
|
+
|
|
24
|
+
# 2. Add requirements to your PRD
|
|
25
|
+
ralph prd add
|
|
26
|
+
|
|
27
|
+
# 3. Run a single iteration
|
|
28
|
+
ralph once
|
|
29
|
+
|
|
30
|
+
# 4. Or run multiple iterations
|
|
31
|
+
ralph run 5
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Commands
|
|
35
|
+
|
|
36
|
+
| Command | Description |
|
|
37
|
+
|---------|-------------|
|
|
38
|
+
| `ralph init` | Initialize ralph in current project |
|
|
39
|
+
| `ralph once` | Run a single automation iteration |
|
|
40
|
+
| `ralph run <n>` | Run n automation iterations |
|
|
41
|
+
| `ralph prd add` | Add a new PRD entry (interactive) |
|
|
42
|
+
| `ralph prd list` | List all PRD entries |
|
|
43
|
+
| `ralph prd status` | Show PRD completion status |
|
|
44
|
+
| `ralph prd toggle <n>` | Toggle passes status for entry n |
|
|
45
|
+
| `ralph scripts` | Generate shell scripts for sandboxed environments |
|
|
46
|
+
| `ralph docker` | Generate Docker sandbox environment |
|
|
47
|
+
| `ralph help` | Show help message |
|
|
48
|
+
|
|
49
|
+
## Configuration
|
|
50
|
+
|
|
51
|
+
After running `ralph init`, you'll have:
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
.ralph/
|
|
55
|
+
├── config.json # Project configuration
|
|
56
|
+
├── prompt.md # Shared prompt template
|
|
57
|
+
├── prd.json # Product requirements document
|
|
58
|
+
└── progress.txt # Progress tracking file
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Supported Languages
|
|
62
|
+
|
|
63
|
+
- **Bun** (TypeScript) - `bun check`, `bun test`
|
|
64
|
+
- **Node.js** (TypeScript) - `npm run typecheck`, `npm test`
|
|
65
|
+
- **Python** - `mypy .`, `pytest`
|
|
66
|
+
- **Go** - `go build ./...`, `go test ./...`
|
|
67
|
+
- **Rust** - `cargo check`, `cargo test`
|
|
68
|
+
- **Custom** - Define your own commands
|
|
69
|
+
|
|
70
|
+
## PRD Format
|
|
71
|
+
|
|
72
|
+
The PRD (`prd.json`) is an array of requirements:
|
|
73
|
+
|
|
74
|
+
```json
|
|
75
|
+
[
|
|
76
|
+
{
|
|
77
|
+
"category": "feature",
|
|
78
|
+
"description": "Add user authentication",
|
|
79
|
+
"steps": [
|
|
80
|
+
"Create login form",
|
|
81
|
+
"Implement JWT tokens",
|
|
82
|
+
"Add protected routes"
|
|
83
|
+
],
|
|
84
|
+
"passes": false
|
|
85
|
+
}
|
|
86
|
+
]
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Categories: `ui`, `feature`, `bugfix`, `setup`, `development`, `testing`, `docs`
|
|
90
|
+
|
|
91
|
+
## Docker Sandbox
|
|
92
|
+
|
|
93
|
+
Run ralph in an isolated Docker container:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
# Generate Docker files
|
|
97
|
+
ralph docker
|
|
98
|
+
|
|
99
|
+
# Build the image
|
|
100
|
+
ralph docker --build
|
|
101
|
+
|
|
102
|
+
# Run container
|
|
103
|
+
ralph docker --run
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Features:
|
|
107
|
+
- Based on [Claude Code devcontainer](https://github.com/anthropics/claude-code/tree/main/.devcontainer)
|
|
108
|
+
- Network sandboxing (firewall allows only GitHub, npm, Anthropic API)
|
|
109
|
+
- Your `~/.claude` credentials mounted automatically (Pro/Max OAuth)
|
|
110
|
+
- Language-specific tooling pre-installed
|
|
111
|
+
|
|
112
|
+
### Installing packages in container
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
# Run as root to install packages
|
|
116
|
+
docker compose run -u root ralph apt-get update
|
|
117
|
+
docker compose run -u root ralph apt-get install <package>
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Shell Scripts
|
|
121
|
+
|
|
122
|
+
For environments where the CLI isn't available:
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
ralph scripts
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Generates `ralph.sh` and `ralph-once.sh` in your project root.
|
|
129
|
+
|
|
130
|
+
## How It Works
|
|
131
|
+
|
|
132
|
+
1. **Read PRD**: Claude reads your requirements from `prd.json`
|
|
133
|
+
2. **Implement**: Works on the highest priority incomplete feature
|
|
134
|
+
3. **Verify**: Runs your check and test commands
|
|
135
|
+
4. **Update**: Marks the feature as complete in the PRD
|
|
136
|
+
5. **Commit**: Creates a git commit for the feature
|
|
137
|
+
6. **Repeat**: Continues to the next feature (in `run` mode)
|
|
138
|
+
|
|
139
|
+
When all PRD items pass, Claude outputs `<promise>COMPLETE</promise>` and stops.
|
|
140
|
+
|
|
141
|
+
## Requirements
|
|
142
|
+
|
|
143
|
+
- Node.js 18+
|
|
144
|
+
- [Claude Code CLI](https://github.com/anthropics/claude-code) installed
|
|
145
|
+
- Claude Pro/Max subscription or Anthropic API key
|
|
146
|
+
|
|
147
|
+
## License
|
|
148
|
+
|
|
149
|
+
MIT
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function docker(args: string[]): Promise<void>;
|
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
import { existsSync, writeFileSync, mkdirSync, chmodSync } from "fs";
|
|
2
|
+
import { join, basename } from "path";
|
|
3
|
+
import { spawn } from "child_process";
|
|
4
|
+
import { loadConfig, getRalphDir } from "../utils/config.js";
|
|
5
|
+
import { promptConfirm } from "../utils/prompt.js";
|
|
6
|
+
const DOCKER_DIR = "docker";
|
|
7
|
+
// Language-specific Dockerfile snippets
|
|
8
|
+
const LANGUAGE_SNIPPETS = {
|
|
9
|
+
bun: `
|
|
10
|
+
# Install Bun
|
|
11
|
+
RUN curl -fsSL https://bun.sh/install | bash
|
|
12
|
+
ENV PATH="/home/node/.bun/bin:$PATH"
|
|
13
|
+
`,
|
|
14
|
+
node: `
|
|
15
|
+
# Node.js already included in base image
|
|
16
|
+
`,
|
|
17
|
+
python: `
|
|
18
|
+
# Install Python and tools
|
|
19
|
+
RUN apt-get update && apt-get install -y \\
|
|
20
|
+
python3 \\
|
|
21
|
+
python3-pip \\
|
|
22
|
+
python3-venv \\
|
|
23
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
24
|
+
RUN pip3 install --break-system-packages mypy pytest
|
|
25
|
+
`,
|
|
26
|
+
go: `
|
|
27
|
+
# Install Go
|
|
28
|
+
RUN curl -fsSL https://go.dev/dl/go1.22.0.linux-amd64.tar.gz | tar -C /usr/local -xzf -
|
|
29
|
+
ENV PATH="/usr/local/go/bin:/home/node/go/bin:$PATH"
|
|
30
|
+
ENV GOPATH="/home/node/go"
|
|
31
|
+
`,
|
|
32
|
+
rust: `
|
|
33
|
+
# Install Rust
|
|
34
|
+
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
|
|
35
|
+
ENV PATH="/home/node/.cargo/bin:$PATH"
|
|
36
|
+
`,
|
|
37
|
+
none: `
|
|
38
|
+
# Custom language - add your dependencies here
|
|
39
|
+
`,
|
|
40
|
+
};
|
|
41
|
+
function generateDockerfile(language) {
|
|
42
|
+
const languageSnippet = LANGUAGE_SNIPPETS[language] || LANGUAGE_SNIPPETS.none;
|
|
43
|
+
return `# Ralph CLI Sandbox Environment
|
|
44
|
+
# Based on Claude Code devcontainer
|
|
45
|
+
# Generated by ralph-cli
|
|
46
|
+
|
|
47
|
+
FROM node:20-bookworm
|
|
48
|
+
|
|
49
|
+
ARG DEBIAN_FRONTEND=noninteractive
|
|
50
|
+
ARG TZ=UTC
|
|
51
|
+
ARG CLAUDE_CODE_VERSION="latest"
|
|
52
|
+
ARG ZSH_IN_DOCKER_VERSION="1.2.1"
|
|
53
|
+
|
|
54
|
+
# Set timezone
|
|
55
|
+
ENV TZ=\${TZ}
|
|
56
|
+
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
|
|
57
|
+
|
|
58
|
+
# Install system dependencies
|
|
59
|
+
RUN apt-get update && apt-get install -y \\
|
|
60
|
+
git \\
|
|
61
|
+
curl \\
|
|
62
|
+
wget \\
|
|
63
|
+
nano \\
|
|
64
|
+
vim \\
|
|
65
|
+
less \\
|
|
66
|
+
procps \\
|
|
67
|
+
sudo \\
|
|
68
|
+
man-db \\
|
|
69
|
+
unzip \\
|
|
70
|
+
gnupg2 \\
|
|
71
|
+
jq \\
|
|
72
|
+
fzf \\
|
|
73
|
+
iptables \\
|
|
74
|
+
ipset \\
|
|
75
|
+
iproute2 \\
|
|
76
|
+
dnsutils \\
|
|
77
|
+
zsh \\
|
|
78
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
79
|
+
|
|
80
|
+
# Setup zsh with oh-my-zsh and plugins (no theme, we set custom prompt)
|
|
81
|
+
RUN sh -c "$(wget -O- https://github.com/deluan/zsh-in-docker/releases/download/v\${ZSH_IN_DOCKER_VERSION}/zsh-in-docker.sh)" -- \\
|
|
82
|
+
-t "" \\
|
|
83
|
+
-p git \\
|
|
84
|
+
-p fzf \\
|
|
85
|
+
-a "source /usr/share/doc/fzf/examples/key-bindings.zsh 2>/dev/null || true" \\
|
|
86
|
+
-a "source /usr/share/doc/fzf/examples/completion.zsh 2>/dev/null || true" \\
|
|
87
|
+
-a "export HISTFILE=/commandhistory/.zsh_history" \\
|
|
88
|
+
-a 'alias ll="ls -la"'
|
|
89
|
+
|
|
90
|
+
# Set custom prompt for node user (after oh-my-zsh to avoid override)
|
|
91
|
+
RUN cp -r /root/.oh-my-zsh /home/node/.oh-my-zsh && chown -R node:node /home/node/.oh-my-zsh && \\
|
|
92
|
+
cp /root/.zshrc /home/node/.zshrc && chown node:node /home/node/.zshrc && \\
|
|
93
|
+
sed -i 's|/root/.oh-my-zsh|/home/node/.oh-my-zsh|g' /home/node/.zshrc && \\
|
|
94
|
+
echo 'PROMPT="%K{yellow}%F{black}[ralph]%f%k%K{yellow}%F{black}%d%f%k\\$ "' >> /home/node/.zshrc
|
|
95
|
+
|
|
96
|
+
# Install Claude Code CLI
|
|
97
|
+
RUN npm install -g @anthropic-ai/claude-code@\${CLAUDE_CODE_VERSION}
|
|
98
|
+
|
|
99
|
+
# Install ralph-cli-claude
|
|
100
|
+
RUN npm install -g ralph-cli-claude || echo "ralph-cli-claude not yet published, will use local"
|
|
101
|
+
${languageSnippet}
|
|
102
|
+
# Setup sudo only for firewall script (no general sudo for security)
|
|
103
|
+
RUN echo "node ALL=(ALL) NOPASSWD: /usr/local/bin/init-firewall.sh" >> /etc/sudoers.d/node-firewall
|
|
104
|
+
|
|
105
|
+
# Create directories
|
|
106
|
+
RUN mkdir -p /workspace && chown node:node /workspace
|
|
107
|
+
RUN mkdir -p /home/node/.claude && chown node:node /home/node/.claude
|
|
108
|
+
RUN mkdir -p /commandhistory && chown node:node /commandhistory
|
|
109
|
+
|
|
110
|
+
# Copy firewall script
|
|
111
|
+
COPY init-firewall.sh /usr/local/bin/init-firewall.sh
|
|
112
|
+
RUN chmod +x /usr/local/bin/init-firewall.sh
|
|
113
|
+
|
|
114
|
+
# Set environment variables
|
|
115
|
+
ENV DEVCONTAINER=true
|
|
116
|
+
ENV NODE_OPTIONS="--max-old-space-size=4096"
|
|
117
|
+
ENV CLAUDE_CONFIG_DIR="/home/node/.claude"
|
|
118
|
+
ENV SHELL=/bin/zsh
|
|
119
|
+
ENV EDITOR=nano
|
|
120
|
+
|
|
121
|
+
# Add bash aliases and prompt (fallback if using bash)
|
|
122
|
+
RUN echo 'alias ll="ls -la"' >> /etc/bash.bashrc && \\
|
|
123
|
+
echo 'PS1="\\[\\033[43;30m\\][ralph]\\w\\[\\033[0m\\]\\$ "' >> /etc/bash.bashrc
|
|
124
|
+
|
|
125
|
+
# Switch to non-root user
|
|
126
|
+
USER node
|
|
127
|
+
WORKDIR /workspace
|
|
128
|
+
|
|
129
|
+
# Default to zsh
|
|
130
|
+
CMD ["zsh"]
|
|
131
|
+
`;
|
|
132
|
+
}
|
|
133
|
+
const FIREWALL_SCRIPT = `#!/bin/bash
|
|
134
|
+
# Firewall initialization script for Ralph sandbox
|
|
135
|
+
# Based on Claude Code devcontainer firewall
|
|
136
|
+
|
|
137
|
+
set -e
|
|
138
|
+
|
|
139
|
+
echo "Initializing sandbox firewall..."
|
|
140
|
+
|
|
141
|
+
# Get Docker DNS before flushing
|
|
142
|
+
DOCKER_DNS=$(cat /etc/resolv.conf | grep nameserver | head -1 | awk '{print $2}')
|
|
143
|
+
|
|
144
|
+
# Flush existing rules
|
|
145
|
+
iptables -F
|
|
146
|
+
iptables -X
|
|
147
|
+
iptables -t nat -F
|
|
148
|
+
iptables -t nat -X
|
|
149
|
+
iptables -t mangle -F
|
|
150
|
+
iptables -t mangle -X
|
|
151
|
+
|
|
152
|
+
# Create ipset for allowed IPs
|
|
153
|
+
ipset destroy allowed_ips 2>/dev/null || true
|
|
154
|
+
ipset create allowed_ips hash:net
|
|
155
|
+
|
|
156
|
+
# Allow localhost
|
|
157
|
+
iptables -A OUTPUT -o lo -j ACCEPT
|
|
158
|
+
iptables -A INPUT -i lo -j ACCEPT
|
|
159
|
+
|
|
160
|
+
# Allow established connections
|
|
161
|
+
iptables -A OUTPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
|
|
162
|
+
iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
|
|
163
|
+
|
|
164
|
+
# Allow DNS
|
|
165
|
+
iptables -A OUTPUT -p udp --dport 53 -j ACCEPT
|
|
166
|
+
iptables -A OUTPUT -p tcp --dport 53 -j ACCEPT
|
|
167
|
+
if [ -n "$DOCKER_DNS" ]; then
|
|
168
|
+
iptables -A OUTPUT -d $DOCKER_DNS -j ACCEPT
|
|
169
|
+
fi
|
|
170
|
+
|
|
171
|
+
# Allow SSH (for git)
|
|
172
|
+
iptables -A OUTPUT -p tcp --dport 22 -j ACCEPT
|
|
173
|
+
|
|
174
|
+
# Add allowed domains to ipset
|
|
175
|
+
# GitHub
|
|
176
|
+
for ip in $(dig +short github.com api.github.com raw.githubusercontent.com); do
|
|
177
|
+
ipset add allowed_ips $ip 2>/dev/null || true
|
|
178
|
+
done
|
|
179
|
+
|
|
180
|
+
# npm registry
|
|
181
|
+
for ip in $(dig +short registry.npmjs.org); do
|
|
182
|
+
ipset add allowed_ips $ip 2>/dev/null || true
|
|
183
|
+
done
|
|
184
|
+
|
|
185
|
+
# Anthropic API
|
|
186
|
+
for ip in $(dig +short api.anthropic.com); do
|
|
187
|
+
ipset add allowed_ips $ip 2>/dev/null || true
|
|
188
|
+
done
|
|
189
|
+
|
|
190
|
+
# Allow host network (for mounted volumes, etc.)
|
|
191
|
+
HOST_NETWORK=$(ip route | grep default | awk '{print $3}' | head -1)
|
|
192
|
+
if [ -n "$HOST_NETWORK" ]; then
|
|
193
|
+
HOST_SUBNET=$(echo $HOST_NETWORK | sed 's/\\.[0-9]*$/.0\\/24/')
|
|
194
|
+
ipset add allowed_ips $HOST_SUBNET 2>/dev/null || true
|
|
195
|
+
fi
|
|
196
|
+
|
|
197
|
+
# Allow traffic to allowed IPs
|
|
198
|
+
iptables -A OUTPUT -m set --match-set allowed_ips dst -j ACCEPT
|
|
199
|
+
|
|
200
|
+
# Set default policies to DROP
|
|
201
|
+
iptables -P INPUT DROP
|
|
202
|
+
iptables -P FORWARD DROP
|
|
203
|
+
iptables -P OUTPUT DROP
|
|
204
|
+
|
|
205
|
+
# Allow HTTPS to allowed IPs
|
|
206
|
+
iptables -I OUTPUT -p tcp --dport 443 -m set --match-set allowed_ips dst -j ACCEPT
|
|
207
|
+
iptables -I OUTPUT -p tcp --dport 80 -m set --match-set allowed_ips dst -j ACCEPT
|
|
208
|
+
|
|
209
|
+
echo "Firewall initialized. Only allowed destinations are accessible."
|
|
210
|
+
echo "Allowed: GitHub, npm, Anthropic API, local network"
|
|
211
|
+
`;
|
|
212
|
+
function generateDockerCompose(imageName) {
|
|
213
|
+
return `# Ralph CLI Docker Compose
|
|
214
|
+
# Generated by ralph-cli
|
|
215
|
+
|
|
216
|
+
services:
|
|
217
|
+
ralph:
|
|
218
|
+
image: ${imageName}
|
|
219
|
+
build:
|
|
220
|
+
context: .
|
|
221
|
+
dockerfile: Dockerfile
|
|
222
|
+
volumes:
|
|
223
|
+
# Mount project root (two levels up from .ralph/docker/)
|
|
224
|
+
- ../..:/workspace
|
|
225
|
+
# Mount host's ~/.claude for Pro/Max OAuth credentials
|
|
226
|
+
- \${HOME}/.claude:/home/node/.claude
|
|
227
|
+
- ${imageName}-history:/commandhistory
|
|
228
|
+
# Uncomment to use API key instead of OAuth:
|
|
229
|
+
# environment:
|
|
230
|
+
# - ANTHROPIC_API_KEY=\${ANTHROPIC_API_KEY}
|
|
231
|
+
working_dir: /workspace
|
|
232
|
+
stdin_open: true
|
|
233
|
+
tty: true
|
|
234
|
+
cap_add:
|
|
235
|
+
- NET_ADMIN # Required for firewall
|
|
236
|
+
# Uncomment to enable firewall sandboxing:
|
|
237
|
+
# command: bash -c "sudo /usr/local/bin/init-firewall.sh && zsh"
|
|
238
|
+
|
|
239
|
+
volumes:
|
|
240
|
+
${imageName}-history:
|
|
241
|
+
`;
|
|
242
|
+
}
|
|
243
|
+
const DOCKERIGNORE = `# Docker ignore file
|
|
244
|
+
node_modules
|
|
245
|
+
dist
|
|
246
|
+
.git
|
|
247
|
+
*.log
|
|
248
|
+
`;
|
|
249
|
+
async function generateFiles(ralphDir, language, imageName) {
|
|
250
|
+
const dockerDir = join(ralphDir, DOCKER_DIR);
|
|
251
|
+
// Create docker directory
|
|
252
|
+
if (!existsSync(dockerDir)) {
|
|
253
|
+
mkdirSync(dockerDir, { recursive: true });
|
|
254
|
+
console.log(`Created ${DOCKER_DIR}/`);
|
|
255
|
+
}
|
|
256
|
+
const files = [
|
|
257
|
+
{ name: "Dockerfile", content: generateDockerfile(language) },
|
|
258
|
+
{ name: "init-firewall.sh", content: FIREWALL_SCRIPT },
|
|
259
|
+
{ name: "docker-compose.yml", content: generateDockerCompose(imageName) },
|
|
260
|
+
{ name: ".dockerignore", content: DOCKERIGNORE },
|
|
261
|
+
];
|
|
262
|
+
for (const file of files) {
|
|
263
|
+
const filePath = join(dockerDir, file.name);
|
|
264
|
+
if (existsSync(filePath)) {
|
|
265
|
+
const overwrite = await promptConfirm(`${DOCKER_DIR}/${file.name} already exists. Overwrite?`);
|
|
266
|
+
if (!overwrite) {
|
|
267
|
+
console.log(`Skipped ${file.name}`);
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
writeFileSync(filePath, file.content);
|
|
272
|
+
if (file.name.endsWith(".sh")) {
|
|
273
|
+
chmodSync(filePath, 0o755);
|
|
274
|
+
}
|
|
275
|
+
console.log(`Created ${DOCKER_DIR}/${file.name}`);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
async function buildImage(ralphDir) {
|
|
279
|
+
const dockerDir = join(ralphDir, DOCKER_DIR);
|
|
280
|
+
if (!existsSync(join(dockerDir, "Dockerfile"))) {
|
|
281
|
+
console.error("Dockerfile not found. Run 'ralph docker' first.");
|
|
282
|
+
process.exit(1);
|
|
283
|
+
}
|
|
284
|
+
console.log("Building Docker image...\n");
|
|
285
|
+
return new Promise((resolve, reject) => {
|
|
286
|
+
const proc = spawn("docker", ["compose", "build"], {
|
|
287
|
+
cwd: dockerDir,
|
|
288
|
+
stdio: "inherit",
|
|
289
|
+
});
|
|
290
|
+
proc.on("close", (code) => {
|
|
291
|
+
if (code === 0) {
|
|
292
|
+
console.log("\nDocker image built successfully!");
|
|
293
|
+
resolve();
|
|
294
|
+
}
|
|
295
|
+
else {
|
|
296
|
+
reject(new Error(`Docker build failed with code ${code}`));
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
proc.on("error", (err) => {
|
|
300
|
+
reject(new Error(`Failed to run docker: ${err.message}`));
|
|
301
|
+
});
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
async function runContainer(ralphDir) {
|
|
305
|
+
const dockerDir = join(ralphDir, DOCKER_DIR);
|
|
306
|
+
if (!existsSync(join(dockerDir, "Dockerfile"))) {
|
|
307
|
+
console.error("Dockerfile not found. Run 'ralph docker' first.");
|
|
308
|
+
process.exit(1);
|
|
309
|
+
}
|
|
310
|
+
console.log("Starting Docker container...\n");
|
|
311
|
+
return new Promise((resolve, reject) => {
|
|
312
|
+
const proc = spawn("docker", ["compose", "run", "--rm", "ralph"], {
|
|
313
|
+
cwd: dockerDir,
|
|
314
|
+
stdio: "inherit",
|
|
315
|
+
});
|
|
316
|
+
proc.on("close", (code) => {
|
|
317
|
+
if (code === 0) {
|
|
318
|
+
resolve();
|
|
319
|
+
}
|
|
320
|
+
else {
|
|
321
|
+
reject(new Error(`Docker run failed with code ${code}`));
|
|
322
|
+
}
|
|
323
|
+
});
|
|
324
|
+
proc.on("error", (err) => {
|
|
325
|
+
reject(new Error(`Failed to run docker: ${err.message}`));
|
|
326
|
+
});
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
export async function docker(args) {
|
|
330
|
+
const flag = args[0];
|
|
331
|
+
// Show help without requiring init
|
|
332
|
+
if (flag === "--help" || flag === "-h") {
|
|
333
|
+
console.log(`
|
|
334
|
+
ralph docker - Generate and manage Docker sandbox environment
|
|
335
|
+
|
|
336
|
+
USAGE:
|
|
337
|
+
ralph docker Generate Dockerfile and scripts
|
|
338
|
+
ralph docker --build Build the Docker image
|
|
339
|
+
ralph docker --run Run container with project mounted
|
|
340
|
+
|
|
341
|
+
FILES GENERATED:
|
|
342
|
+
.ralph/docker/
|
|
343
|
+
├── Dockerfile Based on Claude Code devcontainer
|
|
344
|
+
├── init-firewall.sh Sandbox firewall script
|
|
345
|
+
├── docker-compose.yml Container orchestration
|
|
346
|
+
└── .dockerignore Build exclusions
|
|
347
|
+
|
|
348
|
+
AUTHENTICATION:
|
|
349
|
+
Pro/Max users: Your ~/.claude credentials are mounted automatically.
|
|
350
|
+
API key users: Uncomment ANTHROPIC_API_KEY in docker-compose.yml.
|
|
351
|
+
|
|
352
|
+
EXAMPLES:
|
|
353
|
+
ralph docker # Generate files
|
|
354
|
+
ralph docker --build # Build image
|
|
355
|
+
ralph docker --run # Start interactive shell
|
|
356
|
+
|
|
357
|
+
# Or use docker compose directly:
|
|
358
|
+
cd .ralph/docker && docker compose run --rm ralph
|
|
359
|
+
|
|
360
|
+
# Run ralph automation in container:
|
|
361
|
+
docker compose run --rm ralph ralph once
|
|
362
|
+
|
|
363
|
+
INSTALLING PACKAGES (works with Docker & Podman):
|
|
364
|
+
# 1. Run as root to install packages:
|
|
365
|
+
docker compose run -u root ralph apt-get update
|
|
366
|
+
docker compose run -u root ralph apt-get install <package>
|
|
367
|
+
|
|
368
|
+
# 2. Or commit changes to a new image:
|
|
369
|
+
docker run -it --name temp -u root <image> bash
|
|
370
|
+
# inside: apt-get update && apt-get install <package>
|
|
371
|
+
# exit, then:
|
|
372
|
+
docker commit temp <image>:custom
|
|
373
|
+
docker rm temp
|
|
374
|
+
`);
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
const ralphDir = getRalphDir();
|
|
378
|
+
if (!existsSync(ralphDir)) {
|
|
379
|
+
console.error("Error: .ralph/ directory not found. Run 'ralph init' first.");
|
|
380
|
+
process.exit(1);
|
|
381
|
+
}
|
|
382
|
+
const config = loadConfig();
|
|
383
|
+
// Get image name from config or generate default
|
|
384
|
+
const imageName = config.imageName || `ralph-${basename(process.cwd()).toLowerCase().replace(/[^a-z0-9-]/g, "-")}`;
|
|
385
|
+
if (flag === "--build") {
|
|
386
|
+
await buildImage(ralphDir);
|
|
387
|
+
}
|
|
388
|
+
else if (flag === "--run") {
|
|
389
|
+
await runContainer(ralphDir);
|
|
390
|
+
}
|
|
391
|
+
else {
|
|
392
|
+
console.log(`Generating Docker files for: ${config.language}`);
|
|
393
|
+
console.log(`Image name: ${imageName}\n`);
|
|
394
|
+
await generateFiles(ralphDir, config.language, imageName);
|
|
395
|
+
console.log(`
|
|
396
|
+
Docker files generated in .ralph/docker/
|
|
397
|
+
|
|
398
|
+
Next steps:
|
|
399
|
+
1. Build the image: ralph docker --build
|
|
400
|
+
2. Run container: ralph docker --run
|
|
401
|
+
|
|
402
|
+
Or use docker compose directly:
|
|
403
|
+
cd .ralph/docker && docker compose run --rm ralph
|
|
404
|
+
`);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function help(_args: string[]): void;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
const HELP_TEXT = `
|
|
2
|
+
ralph - AI-driven development automation CLI
|
|
3
|
+
|
|
4
|
+
USAGE:
|
|
5
|
+
ralph <command> [options]
|
|
6
|
+
|
|
7
|
+
COMMANDS:
|
|
8
|
+
init Initialize ralph in current project
|
|
9
|
+
once Run a single automation iteration
|
|
10
|
+
run <n> Run n automation iterations
|
|
11
|
+
prd <subcommand> Manage PRD entries
|
|
12
|
+
scripts Generate shell scripts (for sandboxed environments)
|
|
13
|
+
docker Generate Docker sandbox environment
|
|
14
|
+
help Show this help message
|
|
15
|
+
|
|
16
|
+
PRD SUBCOMMANDS:
|
|
17
|
+
prd add Add a new PRD entry (interactive)
|
|
18
|
+
prd list List all PRD entries
|
|
19
|
+
prd status Show PRD completion status
|
|
20
|
+
prd toggle <n> Toggle passes status for entry n
|
|
21
|
+
|
|
22
|
+
EXAMPLES:
|
|
23
|
+
ralph init # Initialize ralph for your project
|
|
24
|
+
ralph once # Run single iteration
|
|
25
|
+
ralph run 5 # Run 5 iterations
|
|
26
|
+
ralph prd add # Add new PRD entry
|
|
27
|
+
ralph prd list # Show all entries
|
|
28
|
+
ralph prd status # Show completion summary
|
|
29
|
+
ralph scripts # Generate ralph.sh and ralph-once.sh
|
|
30
|
+
ralph docker # Generate Dockerfile for sandboxed env
|
|
31
|
+
ralph docker --build # Build Docker image
|
|
32
|
+
ralph docker --run # Run container interactively
|
|
33
|
+
|
|
34
|
+
CONFIGURATION:
|
|
35
|
+
After running 'ralph init', you'll have:
|
|
36
|
+
.ralph/
|
|
37
|
+
├── config.json Project configuration
|
|
38
|
+
├── prompt.md Shared prompt template
|
|
39
|
+
├── prd.json Product requirements document
|
|
40
|
+
└── progress.txt Progress tracking file
|
|
41
|
+
`;
|
|
42
|
+
export function help(_args) {
|
|
43
|
+
console.log(HELP_TEXT.trim());
|
|
44
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function init(_args: string[]): Promise<void>;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { existsSync, writeFileSync, mkdirSync } from "fs";
|
|
2
|
+
import { join, basename } from "path";
|
|
3
|
+
import { LANGUAGES, generatePrompt, DEFAULT_PRD, DEFAULT_PROGRESS } from "../templates/prompts.js";
|
|
4
|
+
import { promptSelect, promptConfirm, promptInput } from "../utils/prompt.js";
|
|
5
|
+
const RALPH_DIR = ".ralph";
|
|
6
|
+
const CONFIG_FILE = "config.json";
|
|
7
|
+
const PROMPT_FILE = "prompt.md";
|
|
8
|
+
const PRD_FILE = "prd.json";
|
|
9
|
+
const PROGRESS_FILE = "progress.txt";
|
|
10
|
+
export async function init(_args) {
|
|
11
|
+
const cwd = process.cwd();
|
|
12
|
+
const ralphDir = join(cwd, RALPH_DIR);
|
|
13
|
+
console.log("Initializing ralph in current directory...\n");
|
|
14
|
+
// Check for existing .ralph directory
|
|
15
|
+
if (existsSync(ralphDir)) {
|
|
16
|
+
const reinit = await promptConfirm(".ralph/ directory already exists. Re-initialize?");
|
|
17
|
+
if (!reinit) {
|
|
18
|
+
console.log("Aborted.");
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
mkdirSync(ralphDir, { recursive: true });
|
|
24
|
+
console.log(`Created ${RALPH_DIR}/`);
|
|
25
|
+
}
|
|
26
|
+
// Select language
|
|
27
|
+
const languageKeys = Object.keys(LANGUAGES);
|
|
28
|
+
const languageNames = languageKeys.map(k => `${LANGUAGES[k].name} - ${LANGUAGES[k].description}`);
|
|
29
|
+
const selectedName = await promptSelect("Select your project language/runtime:", languageNames);
|
|
30
|
+
const selectedIndex = languageNames.indexOf(selectedName);
|
|
31
|
+
const selectedKey = languageKeys[selectedIndex];
|
|
32
|
+
const config = LANGUAGES[selectedKey];
|
|
33
|
+
// Allow custom commands
|
|
34
|
+
let checkCommand = config.checkCommand;
|
|
35
|
+
let testCommand = config.testCommand;
|
|
36
|
+
if (selectedKey === "none") {
|
|
37
|
+
checkCommand = await promptInput("\nEnter your type/build check command: ") || checkCommand;
|
|
38
|
+
testCommand = await promptInput("Enter your test command: ") || testCommand;
|
|
39
|
+
}
|
|
40
|
+
const finalConfig = {
|
|
41
|
+
...config,
|
|
42
|
+
checkCommand,
|
|
43
|
+
testCommand,
|
|
44
|
+
};
|
|
45
|
+
// Generate image name from directory name
|
|
46
|
+
const projectName = basename(cwd).toLowerCase().replace(/[^a-z0-9-]/g, "-");
|
|
47
|
+
const imageName = `ralph-${projectName}`;
|
|
48
|
+
// Write config file
|
|
49
|
+
const configData = {
|
|
50
|
+
language: selectedKey,
|
|
51
|
+
checkCommand: finalConfig.checkCommand,
|
|
52
|
+
testCommand: finalConfig.testCommand,
|
|
53
|
+
imageName,
|
|
54
|
+
};
|
|
55
|
+
const configPath = join(ralphDir, CONFIG_FILE);
|
|
56
|
+
writeFileSync(configPath, JSON.stringify(configData, null, 2) + "\n");
|
|
57
|
+
console.log(`\nCreated ${RALPH_DIR}/${CONFIG_FILE}`);
|
|
58
|
+
// Write prompt file (ask if exists)
|
|
59
|
+
const prompt = generatePrompt(finalConfig);
|
|
60
|
+
const promptPath = join(ralphDir, PROMPT_FILE);
|
|
61
|
+
if (existsSync(promptPath)) {
|
|
62
|
+
const overwritePrompt = await promptConfirm(`${RALPH_DIR}/${PROMPT_FILE} already exists. Overwrite?`);
|
|
63
|
+
if (overwritePrompt) {
|
|
64
|
+
writeFileSync(promptPath, prompt + "\n");
|
|
65
|
+
console.log(`Updated ${RALPH_DIR}/${PROMPT_FILE}`);
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
console.log(`Skipped ${RALPH_DIR}/${PROMPT_FILE}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
writeFileSync(promptPath, prompt + "\n");
|
|
73
|
+
console.log(`Created ${RALPH_DIR}/${PROMPT_FILE}`);
|
|
74
|
+
}
|
|
75
|
+
// Create PRD if not exists
|
|
76
|
+
const prdPath = join(ralphDir, PRD_FILE);
|
|
77
|
+
if (!existsSync(prdPath)) {
|
|
78
|
+
writeFileSync(prdPath, DEFAULT_PRD + "\n");
|
|
79
|
+
console.log(`Created ${RALPH_DIR}/${PRD_FILE}`);
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
console.log(`Skipped ${RALPH_DIR}/${PRD_FILE} (already exists)`);
|
|
83
|
+
}
|
|
84
|
+
// Create progress file if not exists
|
|
85
|
+
const progressPath = join(ralphDir, PROGRESS_FILE);
|
|
86
|
+
if (!existsSync(progressPath)) {
|
|
87
|
+
writeFileSync(progressPath, DEFAULT_PROGRESS);
|
|
88
|
+
console.log(`Created ${RALPH_DIR}/${PROGRESS_FILE}`);
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
console.log(`Skipped ${RALPH_DIR}/${PROGRESS_FILE} (already exists)`);
|
|
92
|
+
}
|
|
93
|
+
console.log("\nRalph initialized successfully!");
|
|
94
|
+
console.log("\nNext steps:");
|
|
95
|
+
console.log(" 1. Edit .ralph/prd.json to add your project requirements");
|
|
96
|
+
console.log(" 2. Run 'ralph once' to start the first iteration");
|
|
97
|
+
console.log(" 3. Or run 'ralph run 5' for 5 automated iterations");
|
|
98
|
+
}
|