ralph-cli-claude 0.1.1 → 0.1.5
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 +68 -0
- package/dist/commands/docker.js +284 -19
- package/dist/commands/help.js +34 -7
- package/dist/commands/init.d.ts +1 -1
- package/dist/commands/init.js +55 -10
- package/dist/commands/once.js +10 -2
- package/dist/commands/prd.js +94 -22
- package/dist/commands/prompt.d.ts +1 -0
- package/dist/commands/prompt.js +52 -0
- package/dist/commands/run.js +274 -28
- package/dist/index.js +2 -0
- package/dist/templates/prompts.d.ts +13 -1
- package/dist/templates/prompts.js +115 -5
- package/dist/utils/config.d.ts +3 -0
- package/dist/utils/prompt.d.ts +1 -0
- package/dist/utils/prompt.js +48 -0
- package/docs/HOW-TO-WRITE-PRDs.md +212 -0
- package/docs/PRD-GENERATOR.md +361 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -140,6 +140,74 @@ Generates `ralph.sh` and `ralph-once.sh` in your project root.
|
|
|
140
140
|
|
|
141
141
|
When all PRD items pass, Claude outputs `<promise>COMPLETE</promise>` and stops.
|
|
142
142
|
|
|
143
|
+
## Security
|
|
144
|
+
|
|
145
|
+
### Container Requirement
|
|
146
|
+
|
|
147
|
+
**It is strongly recommended to run ralph inside a Docker container for security.** The Ralph Wiggum technique involves running an AI agent autonomously, which means granting it elevated permissions to execute code and modify files without manual approval for each action.
|
|
148
|
+
|
|
149
|
+
### The `--dangerously-skip-permissions` Flag
|
|
150
|
+
|
|
151
|
+
When running inside a container, ralph automatically passes the `--dangerously-skip-permissions` flag to Claude Code. This flag:
|
|
152
|
+
|
|
153
|
+
- Allows Claude to execute commands and modify files without prompting for permission
|
|
154
|
+
- Is **only** enabled when ralph detects it's running inside a container
|
|
155
|
+
- Is required for autonomous operation (otherwise Claude would pause for approval on every action)
|
|
156
|
+
|
|
157
|
+
**Warning:** The `--dangerously-skip-permissions` flag gives the AI agent full control over the environment. This is why container isolation is critical:
|
|
158
|
+
|
|
159
|
+
- The container provides a sandbox boundary
|
|
160
|
+
- Network access is restricted to essential services (GitHub, npm, Anthropic API)
|
|
161
|
+
- Your host system remains protected even if something goes wrong
|
|
162
|
+
|
|
163
|
+
### Container Detection
|
|
164
|
+
|
|
165
|
+
Ralph detects container environments by checking:
|
|
166
|
+
- `DEVCONTAINER` environment variable
|
|
167
|
+
- Presence of `/.dockerenv` file
|
|
168
|
+
- Container indicators in `/proc/1/cgroup`
|
|
169
|
+
- `container` environment variable
|
|
170
|
+
|
|
171
|
+
If you're running outside a container and need autonomous mode, use `ralph docker` to set up a safe sandbox environment first.
|
|
172
|
+
|
|
173
|
+
## Development
|
|
174
|
+
|
|
175
|
+
To contribute or test changes to ralph locally:
|
|
176
|
+
|
|
177
|
+
```bash
|
|
178
|
+
# Clone the repository
|
|
179
|
+
git clone https://github.com/anthropics/ralph-cli-claude
|
|
180
|
+
cd ralph-cli-claude
|
|
181
|
+
|
|
182
|
+
# Install dependencies
|
|
183
|
+
npm install
|
|
184
|
+
|
|
185
|
+
# Run ralph in development mode (without building)
|
|
186
|
+
npm run dev -- <args>
|
|
187
|
+
|
|
188
|
+
# Examples:
|
|
189
|
+
npm run dev -- --version
|
|
190
|
+
npm run dev -- prd list
|
|
191
|
+
npm run dev -- once
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
The `npm run dev -- <args>` command runs ralph directly from TypeScript source using `tsx`, allowing you to test changes without rebuilding.
|
|
195
|
+
|
|
196
|
+
### Platform-Specific Dependencies
|
|
197
|
+
|
|
198
|
+
The `node_modules` folder contains platform-specific binaries (e.g., esbuild). If you switch between running on your host machine and inside a Docker/Podman container, you'll need to reinstall dependencies:
|
|
199
|
+
|
|
200
|
+
```bash
|
|
201
|
+
# When switching environments (host <-> container)
|
|
202
|
+
rm -rf node_modules && npm install
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Alternatively, when mounting your project into a container, use a separate volume for node_modules to keep host and container dependencies isolated:
|
|
206
|
+
|
|
207
|
+
```bash
|
|
208
|
+
podman run -v $(pwd):/workspace -v /workspace/node_modules your-image
|
|
209
|
+
```
|
|
210
|
+
|
|
143
211
|
## Requirements
|
|
144
212
|
|
|
145
213
|
- Node.js 18+
|
package/dist/commands/docker.js
CHANGED
|
@@ -24,8 +24,9 @@ RUN apt-get update && apt-get install -y \\
|
|
|
24
24
|
RUN pip3 install --break-system-packages mypy pytest
|
|
25
25
|
`,
|
|
26
26
|
go: `
|
|
27
|
-
# Install Go
|
|
28
|
-
RUN
|
|
27
|
+
# Install Go (architecture-aware)
|
|
28
|
+
RUN ARCH=$(dpkg --print-architecture) && \\
|
|
29
|
+
curl -fsSL "https://go.dev/dl/go1.22.0.linux-\${ARCH}.tar.gz" | tar -C /usr/local -xzf -
|
|
29
30
|
ENV PATH="/usr/local/go/bin:/home/node/go/bin:$PATH"
|
|
30
31
|
ENV GOPATH="/home/node/go"
|
|
31
32
|
`,
|
|
@@ -38,8 +39,51 @@ ENV PATH="/home/node/.cargo/bin:$PATH"
|
|
|
38
39
|
# Custom language - add your dependencies here
|
|
39
40
|
`,
|
|
40
41
|
};
|
|
41
|
-
|
|
42
|
-
|
|
42
|
+
// Java snippet generator (supports configurable Java version)
|
|
43
|
+
function getJavaSnippet(javaVersion = 17) {
|
|
44
|
+
return `
|
|
45
|
+
# Install Java ${javaVersion} and Maven
|
|
46
|
+
RUN apt-get update && apt-get install -y \\
|
|
47
|
+
openjdk-${javaVersion}-jdk \\
|
|
48
|
+
maven \\
|
|
49
|
+
&& rm -rf /var/lib/apt/lists/* \\
|
|
50
|
+
&& ln -s /usr/lib/jvm/java-${javaVersion}-openjdk-$(dpkg --print-architecture) /usr/lib/jvm/java-${javaVersion}-openjdk
|
|
51
|
+
ENV JAVA_HOME="/usr/lib/jvm/java-${javaVersion}-openjdk"
|
|
52
|
+
`;
|
|
53
|
+
}
|
|
54
|
+
// Kotlin snippet generator (supports configurable Java version)
|
|
55
|
+
function getKotlinSnippet(javaVersion = 17) {
|
|
56
|
+
return `
|
|
57
|
+
# Install Kotlin and Gradle (Java ${javaVersion})
|
|
58
|
+
RUN apt-get update && apt-get install -y \\
|
|
59
|
+
openjdk-${javaVersion}-jdk \\
|
|
60
|
+
&& rm -rf /var/lib/apt/lists/* \\
|
|
61
|
+
&& ln -s /usr/lib/jvm/java-${javaVersion}-openjdk-$(dpkg --print-architecture) /usr/lib/jvm/java-${javaVersion}-openjdk
|
|
62
|
+
ENV JAVA_HOME="/usr/lib/jvm/java-${javaVersion}-openjdk"
|
|
63
|
+
# Install Gradle
|
|
64
|
+
RUN curl -fsSL https://services.gradle.org/distributions/gradle-8.5-bin.zip -o /tmp/gradle.zip && \\
|
|
65
|
+
unzip -d /opt /tmp/gradle.zip && \\
|
|
66
|
+
rm /tmp/gradle.zip
|
|
67
|
+
ENV PATH="/opt/gradle-8.5/bin:$PATH"
|
|
68
|
+
# Install Kotlin compiler
|
|
69
|
+
RUN curl -fsSL https://github.com/JetBrains/kotlin/releases/download/v1.9.22/kotlin-compiler-1.9.22.zip -o /tmp/kotlin.zip && \\
|
|
70
|
+
unzip -d /opt /tmp/kotlin.zip && \\
|
|
71
|
+
rm /tmp/kotlin.zip
|
|
72
|
+
ENV PATH="/opt/kotlinc/bin:$PATH"
|
|
73
|
+
`;
|
|
74
|
+
}
|
|
75
|
+
// Get language snippet, with special handling for Java/Kotlin
|
|
76
|
+
function getLanguageSnippet(language, javaVersion) {
|
|
77
|
+
if (language === "java") {
|
|
78
|
+
return getJavaSnippet(javaVersion);
|
|
79
|
+
}
|
|
80
|
+
if (language === "kotlin") {
|
|
81
|
+
return getKotlinSnippet(javaVersion);
|
|
82
|
+
}
|
|
83
|
+
return LANGUAGE_SNIPPETS[language] || LANGUAGE_SNIPPETS.none;
|
|
84
|
+
}
|
|
85
|
+
function generateDockerfile(language, javaVersion) {
|
|
86
|
+
const languageSnippet = getLanguageSnippet(language, javaVersion);
|
|
43
87
|
return `# Ralph CLI Sandbox Environment
|
|
44
88
|
# Based on Claude Code devcontainer
|
|
45
89
|
# Generated by ralph-cli
|
|
@@ -91,7 +135,22 @@ RUN sh -c "$(wget -O- https://github.com/deluan/zsh-in-docker/releases/download/
|
|
|
91
135
|
RUN cp -r /root/.oh-my-zsh /home/node/.oh-my-zsh && chown -R node:node /home/node/.oh-my-zsh && \\
|
|
92
136
|
cp /root/.zshrc /home/node/.zshrc && chown node:node /home/node/.zshrc && \\
|
|
93
137
|
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
|
|
138
|
+
echo 'PROMPT="%K{yellow}%F{black}[ralph]%f%k%K{yellow}%F{black}%d%f%k\\$ "' >> /home/node/.zshrc && \\
|
|
139
|
+
echo '' >> /home/node/.zshrc && \\
|
|
140
|
+
echo '# Ralph ASCII art banner' >> /home/node/.zshrc && \\
|
|
141
|
+
echo 'if [ -z "$RALPH_BANNER_SHOWN" ]; then' >> /home/node/.zshrc && \\
|
|
142
|
+
echo ' export RALPH_BANNER_SHOWN=1' >> /home/node/.zshrc && \\
|
|
143
|
+
echo ' echo ""' >> /home/node/.zshrc && \\
|
|
144
|
+
echo ' echo " ____ _ _ ____ _ _ "' >> /home/node/.zshrc && \\
|
|
145
|
+
echo ' echo "| _ \\\\ / \\\\ | | | _ \\\\| | | |"' >> /home/node/.zshrc && \\
|
|
146
|
+
echo ' echo "| |_) | / _ \\\\ | | | |_) | |_| |"' >> /home/node/.zshrc && \\
|
|
147
|
+
echo ' echo "| _ < / ___ \\\\| |___| __/| _ |"' >> /home/node/.zshrc && \\
|
|
148
|
+
echo ' echo "|_| \\\\_\\\\/_/ \\\\_\\\\_____|_| |_| |_|"' >> /home/node/.zshrc && \\
|
|
149
|
+
echo ' echo ""' >> /home/node/.zshrc && \\
|
|
150
|
+
echo ' RALPH_VERSION=$(ralph --version 2>/dev/null | head -1 || echo "unknown")' >> /home/node/.zshrc && \\
|
|
151
|
+
echo ' echo "CLI - Version $RALPH_VERSION"' >> /home/node/.zshrc && \\
|
|
152
|
+
echo ' echo ""' >> /home/node/.zshrc && \\
|
|
153
|
+
echo 'fi' >> /home/node/.zshrc
|
|
95
154
|
|
|
96
155
|
# Install Claude Code CLI
|
|
97
156
|
RUN npm install -g @anthropic-ai/claude-code@\${CLAUDE_CODE_VERSION}
|
|
@@ -246,7 +305,7 @@ dist
|
|
|
246
305
|
.git
|
|
247
306
|
*.log
|
|
248
307
|
`;
|
|
249
|
-
async function generateFiles(ralphDir, language, imageName, force = false) {
|
|
308
|
+
async function generateFiles(ralphDir, language, imageName, force = false, javaVersion) {
|
|
250
309
|
const dockerDir = join(ralphDir, DOCKER_DIR);
|
|
251
310
|
// Create docker directory
|
|
252
311
|
if (!existsSync(dockerDir)) {
|
|
@@ -254,7 +313,7 @@ async function generateFiles(ralphDir, language, imageName, force = false) {
|
|
|
254
313
|
console.log(`Created ${DOCKER_DIR}/`);
|
|
255
314
|
}
|
|
256
315
|
const files = [
|
|
257
|
-
{ name: "Dockerfile", content: generateDockerfile(language) },
|
|
316
|
+
{ name: "Dockerfile", content: generateDockerfile(language, javaVersion) },
|
|
258
317
|
{ name: "init-firewall.sh", content: FIREWALL_SCRIPT },
|
|
259
318
|
{ name: "docker-compose.yml", content: generateDockerCompose(imageName) },
|
|
260
319
|
{ name: ".dockerignore", content: DOCKERIGNORE },
|
|
@@ -281,9 +340,10 @@ async function buildImage(ralphDir) {
|
|
|
281
340
|
console.error("Dockerfile not found. Run 'ralph docker' first.");
|
|
282
341
|
process.exit(1);
|
|
283
342
|
}
|
|
284
|
-
console.log("Building Docker image...\n");
|
|
343
|
+
console.log("Building Docker image (fetching latest Claude Code)...\n");
|
|
285
344
|
return new Promise((resolve, reject) => {
|
|
286
|
-
|
|
345
|
+
// Use --no-cache and --pull to ensure we always get the latest Claude Code version
|
|
346
|
+
const proc = spawn("docker", ["compose", "build", "--no-cache", "--pull"], {
|
|
287
347
|
cwd: dockerDir,
|
|
288
348
|
stdio: "inherit",
|
|
289
349
|
});
|
|
@@ -301,11 +361,40 @@ async function buildImage(ralphDir) {
|
|
|
301
361
|
});
|
|
302
362
|
});
|
|
303
363
|
}
|
|
304
|
-
async function
|
|
364
|
+
async function imageExists(imageName) {
|
|
365
|
+
return new Promise((resolve) => {
|
|
366
|
+
const proc = spawn("docker", ["images", "-q", imageName], {
|
|
367
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
368
|
+
});
|
|
369
|
+
let output = "";
|
|
370
|
+
proc.stdout.on("data", (data) => {
|
|
371
|
+
output += data.toString();
|
|
372
|
+
});
|
|
373
|
+
proc.on("close", () => {
|
|
374
|
+
// If output is non-empty, image exists
|
|
375
|
+
resolve(output.trim().length > 0);
|
|
376
|
+
});
|
|
377
|
+
proc.on("error", () => {
|
|
378
|
+
resolve(false);
|
|
379
|
+
});
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
async function runContainer(ralphDir, imageName, language, javaVersion) {
|
|
305
383
|
const dockerDir = join(ralphDir, DOCKER_DIR);
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
384
|
+
const dockerfileExists = existsSync(join(dockerDir, "Dockerfile"));
|
|
385
|
+
const hasImage = await imageExists(imageName);
|
|
386
|
+
// Auto-init and build if docker folder or image doesn't exist
|
|
387
|
+
if (!dockerfileExists || !hasImage) {
|
|
388
|
+
if (!dockerfileExists) {
|
|
389
|
+
console.log("Docker folder not found. Initializing docker setup...\n");
|
|
390
|
+
await generateFiles(ralphDir, language, imageName, true, javaVersion);
|
|
391
|
+
console.log("");
|
|
392
|
+
}
|
|
393
|
+
if (!hasImage) {
|
|
394
|
+
console.log("Docker image not found. Building image...\n");
|
|
395
|
+
await buildImage(ralphDir);
|
|
396
|
+
console.log("");
|
|
397
|
+
}
|
|
309
398
|
}
|
|
310
399
|
console.log("Starting Docker container...\n");
|
|
311
400
|
return new Promise((resolve, reject) => {
|
|
@@ -326,7 +415,167 @@ async function runContainer(ralphDir) {
|
|
|
326
415
|
});
|
|
327
416
|
});
|
|
328
417
|
}
|
|
418
|
+
async function cleanImage(imageName, ralphDir) {
|
|
419
|
+
const dockerDir = join(ralphDir, DOCKER_DIR);
|
|
420
|
+
console.log(`Cleaning Docker image: ${imageName}...\n`);
|
|
421
|
+
// First, stop any running containers via docker compose
|
|
422
|
+
if (existsSync(join(dockerDir, "docker-compose.yml"))) {
|
|
423
|
+
// Stop running containers first
|
|
424
|
+
await new Promise((resolve) => {
|
|
425
|
+
const proc = spawn("docker", ["compose", "stop", "--timeout", "5"], {
|
|
426
|
+
cwd: dockerDir,
|
|
427
|
+
stdio: "inherit",
|
|
428
|
+
});
|
|
429
|
+
proc.on("close", () => {
|
|
430
|
+
resolve();
|
|
431
|
+
});
|
|
432
|
+
proc.on("error", () => {
|
|
433
|
+
resolve();
|
|
434
|
+
});
|
|
435
|
+
});
|
|
436
|
+
// Remove containers, volumes, networks, and local images
|
|
437
|
+
await new Promise((resolve) => {
|
|
438
|
+
const proc = spawn("docker", ["compose", "down", "--rmi", "local", "-v", "--remove-orphans", "--timeout", "5"], {
|
|
439
|
+
cwd: dockerDir,
|
|
440
|
+
stdio: "inherit",
|
|
441
|
+
});
|
|
442
|
+
proc.on("close", () => {
|
|
443
|
+
// Continue regardless of exit code (image may not exist)
|
|
444
|
+
resolve();
|
|
445
|
+
});
|
|
446
|
+
proc.on("error", () => {
|
|
447
|
+
resolve();
|
|
448
|
+
});
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
// Find and forcibly remove any containers using volumes with our image name pattern
|
|
452
|
+
// This handles orphaned containers from previous runs or pods
|
|
453
|
+
const volumePattern = `docker_${imageName}`;
|
|
454
|
+
await new Promise((resolve) => {
|
|
455
|
+
// List all containers (including stopped) and filter by volume name pattern
|
|
456
|
+
const proc = spawn("docker", ["ps", "-aq", "--filter", `volume=${volumePattern}`], {
|
|
457
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
458
|
+
});
|
|
459
|
+
let output = "";
|
|
460
|
+
proc.stdout.on("data", (data) => {
|
|
461
|
+
output += data.toString();
|
|
462
|
+
});
|
|
463
|
+
proc.on("close", async () => {
|
|
464
|
+
const containerIds = output.trim().split("\n").filter((id) => id.length > 0);
|
|
465
|
+
if (containerIds.length > 0) {
|
|
466
|
+
// Force remove these containers
|
|
467
|
+
await new Promise((innerResolve) => {
|
|
468
|
+
const rmProc = spawn("docker", ["rm", "-f", ...containerIds], {
|
|
469
|
+
stdio: "inherit",
|
|
470
|
+
});
|
|
471
|
+
rmProc.on("close", () => innerResolve());
|
|
472
|
+
rmProc.on("error", () => innerResolve());
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
resolve();
|
|
476
|
+
});
|
|
477
|
+
proc.on("error", () => {
|
|
478
|
+
resolve();
|
|
479
|
+
});
|
|
480
|
+
});
|
|
481
|
+
// Also try to remove the image directly (in case it was built outside compose)
|
|
482
|
+
await new Promise((resolve) => {
|
|
483
|
+
const proc = spawn("docker", ["rmi", "-f", imageName], {
|
|
484
|
+
stdio: "inherit",
|
|
485
|
+
});
|
|
486
|
+
proc.on("close", () => {
|
|
487
|
+
resolve();
|
|
488
|
+
});
|
|
489
|
+
proc.on("error", () => {
|
|
490
|
+
resolve();
|
|
491
|
+
});
|
|
492
|
+
});
|
|
493
|
+
// Clean up volumes matching our pattern
|
|
494
|
+
await new Promise((resolve) => {
|
|
495
|
+
// List volumes matching our pattern
|
|
496
|
+
const proc = spawn("docker", ["volume", "ls", "-q", "--filter", `name=${volumePattern}`], {
|
|
497
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
498
|
+
});
|
|
499
|
+
let output = "";
|
|
500
|
+
proc.stdout.on("data", (data) => {
|
|
501
|
+
output += data.toString();
|
|
502
|
+
});
|
|
503
|
+
proc.on("close", async () => {
|
|
504
|
+
const volumeNames = output.trim().split("\n").filter((name) => name.length > 0);
|
|
505
|
+
if (volumeNames.length > 0) {
|
|
506
|
+
// Force remove these volumes
|
|
507
|
+
await new Promise((innerResolve) => {
|
|
508
|
+
const rmProc = spawn("docker", ["volume", "rm", "-f", ...volumeNames], {
|
|
509
|
+
stdio: "inherit",
|
|
510
|
+
});
|
|
511
|
+
rmProc.on("close", () => innerResolve());
|
|
512
|
+
rmProc.on("error", () => innerResolve());
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
resolve();
|
|
516
|
+
});
|
|
517
|
+
proc.on("error", () => {
|
|
518
|
+
resolve();
|
|
519
|
+
});
|
|
520
|
+
});
|
|
521
|
+
// Also try removing the simple volume name pattern
|
|
522
|
+
const volumeName = `${imageName}-history`;
|
|
523
|
+
await new Promise((resolve) => {
|
|
524
|
+
const proc = spawn("docker", ["volume", "rm", "-f", volumeName], {
|
|
525
|
+
stdio: "inherit",
|
|
526
|
+
});
|
|
527
|
+
proc.on("close", () => {
|
|
528
|
+
resolve();
|
|
529
|
+
});
|
|
530
|
+
proc.on("error", () => {
|
|
531
|
+
resolve();
|
|
532
|
+
});
|
|
533
|
+
});
|
|
534
|
+
// For Podman: clean up any orphaned pods matching our pattern
|
|
535
|
+
await new Promise((resolve) => {
|
|
536
|
+
const proc = spawn("docker", ["pod", "ls", "-q", "--filter", `name=docker`], {
|
|
537
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
538
|
+
});
|
|
539
|
+
let output = "";
|
|
540
|
+
proc.stdout.on("data", (data) => {
|
|
541
|
+
output += data.toString();
|
|
542
|
+
});
|
|
543
|
+
proc.on("close", async () => {
|
|
544
|
+
const podIds = output.trim().split("\n").filter((id) => id.length > 0);
|
|
545
|
+
if (podIds.length > 0) {
|
|
546
|
+
// Force remove these pods (this also removes their containers)
|
|
547
|
+
await new Promise((innerResolve) => {
|
|
548
|
+
const rmProc = spawn("docker", ["pod", "rm", "-f", ...podIds], {
|
|
549
|
+
stdio: "inherit",
|
|
550
|
+
});
|
|
551
|
+
rmProc.on("close", () => innerResolve());
|
|
552
|
+
rmProc.on("error", () => innerResolve());
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
resolve();
|
|
556
|
+
});
|
|
557
|
+
proc.on("error", () => {
|
|
558
|
+
// docker pod command doesn't exist (not Podman) - ignore
|
|
559
|
+
resolve();
|
|
560
|
+
});
|
|
561
|
+
});
|
|
562
|
+
// Clean up the docker_default network if it exists and is empty
|
|
563
|
+
await new Promise((resolve) => {
|
|
564
|
+
const proc = spawn("docker", ["network", "rm", "docker_default"], {
|
|
565
|
+
stdio: "inherit",
|
|
566
|
+
});
|
|
567
|
+
proc.on("close", () => {
|
|
568
|
+
resolve();
|
|
569
|
+
});
|
|
570
|
+
proc.on("error", () => {
|
|
571
|
+
resolve();
|
|
572
|
+
});
|
|
573
|
+
});
|
|
574
|
+
console.log("\nDocker image and associated resources cleaned.");
|
|
575
|
+
console.log("Run 'ralph docker --build' to rebuild the image.");
|
|
576
|
+
}
|
|
329
577
|
export async function docker(args) {
|
|
578
|
+
const hasFlag = (flag) => args.includes(flag);
|
|
330
579
|
const flag = args[0];
|
|
331
580
|
// Show help without requiring init
|
|
332
581
|
if (flag === "--help" || flag === "-h") {
|
|
@@ -336,8 +585,10 @@ ralph docker - Generate and manage Docker sandbox environment
|
|
|
336
585
|
USAGE:
|
|
337
586
|
ralph docker Generate Dockerfile and scripts
|
|
338
587
|
ralph docker -y Generate files, overwrite without prompting
|
|
339
|
-
ralph docker --build Build
|
|
340
|
-
ralph docker --
|
|
588
|
+
ralph docker --build Build image (always fetches latest Claude Code)
|
|
589
|
+
ralph docker --build --clean Clean existing image and rebuild from scratch
|
|
590
|
+
ralph docker --run Run container (auto-init and build if needed)
|
|
591
|
+
ralph docker --clean Remove Docker image and associated resources
|
|
341
592
|
|
|
342
593
|
FILES GENERATED:
|
|
343
594
|
.ralph/docker/
|
|
@@ -353,7 +604,9 @@ AUTHENTICATION:
|
|
|
353
604
|
EXAMPLES:
|
|
354
605
|
ralph docker # Generate files
|
|
355
606
|
ralph docker --build # Build image
|
|
607
|
+
ralph docker --build --clean # Clean and rebuild from scratch
|
|
356
608
|
ralph docker --run # Start interactive shell
|
|
609
|
+
ralph docker --clean # Remove image and volumes
|
|
357
610
|
|
|
358
611
|
# Or use docker compose directly:
|
|
359
612
|
cd .ralph/docker && docker compose run --rm ralph
|
|
@@ -383,17 +636,29 @@ INSTALLING PACKAGES (works with Docker & Podman):
|
|
|
383
636
|
const config = loadConfig();
|
|
384
637
|
// Get image name from config or generate default
|
|
385
638
|
const imageName = config.imageName || `ralph-${basename(process.cwd()).toLowerCase().replace(/[^a-z0-9-]/g, "-")}`;
|
|
386
|
-
|
|
639
|
+
// Handle --build --clean combination: clean first, then build
|
|
640
|
+
if (hasFlag("--build") && hasFlag("--clean")) {
|
|
641
|
+
await cleanImage(imageName, ralphDir);
|
|
642
|
+
console.log(""); // Add spacing between clean and build output
|
|
387
643
|
await buildImage(ralphDir);
|
|
388
644
|
}
|
|
389
|
-
else if (
|
|
390
|
-
await
|
|
645
|
+
else if (hasFlag("--build")) {
|
|
646
|
+
await buildImage(ralphDir);
|
|
647
|
+
}
|
|
648
|
+
else if (hasFlag("--run")) {
|
|
649
|
+
await runContainer(ralphDir, imageName, config.language, config.javaVersion);
|
|
650
|
+
}
|
|
651
|
+
else if (hasFlag("--clean")) {
|
|
652
|
+
await cleanImage(imageName, ralphDir);
|
|
391
653
|
}
|
|
392
654
|
else {
|
|
393
655
|
const force = flag === "-y" || flag === "--yes";
|
|
394
656
|
console.log(`Generating Docker files for: ${config.language}`);
|
|
657
|
+
if ((config.language === "java" || config.language === "kotlin") && config.javaVersion) {
|
|
658
|
+
console.log(`Java version: ${config.javaVersion}`);
|
|
659
|
+
}
|
|
395
660
|
console.log(`Image name: ${imageName}\n`);
|
|
396
|
-
await generateFiles(ralphDir, config.language, imageName, force);
|
|
661
|
+
await generateFiles(ralphDir, config.language, imageName, force, config.javaVersion);
|
|
397
662
|
console.log(`
|
|
398
663
|
Docker files generated in .ralph/docker/
|
|
399
664
|
|
package/dist/commands/help.js
CHANGED
|
@@ -5,38 +5,65 @@ USAGE:
|
|
|
5
5
|
ralph <command> [options]
|
|
6
6
|
|
|
7
7
|
COMMANDS:
|
|
8
|
-
init
|
|
8
|
+
init [opts] Initialize ralph in current project
|
|
9
9
|
once Run a single automation iteration
|
|
10
|
-
run <n>
|
|
10
|
+
run <n> [opts] Run n automation iterations
|
|
11
11
|
prd <subcommand> Manage PRD entries
|
|
12
|
+
prompt [opts] Display resolved prompt (for testing in Claude Code)
|
|
12
13
|
scripts Generate shell scripts (for sandboxed environments)
|
|
13
14
|
docker Generate Docker sandbox environment
|
|
14
15
|
help Show this help message
|
|
15
16
|
|
|
17
|
+
INIT OPTIONS:
|
|
18
|
+
--tech-stack, -t Enable technology stack selection prompt
|
|
19
|
+
|
|
20
|
+
RUN OPTIONS:
|
|
21
|
+
--all, -a Run until all tasks are complete, showing progress
|
|
22
|
+
--loop, -l Run continuously, waiting for new items when complete
|
|
23
|
+
--category, -c <category> Filter PRD items by category
|
|
24
|
+
Valid: ui, feature, bugfix, setup, development, testing, docs
|
|
25
|
+
|
|
16
26
|
PRD SUBCOMMANDS:
|
|
17
27
|
prd add Add a new PRD entry (interactive)
|
|
18
|
-
prd list
|
|
28
|
+
prd list [opts] List all PRD entries
|
|
19
29
|
prd status Show PRD completion status
|
|
20
30
|
prd toggle <n> Toggle passes status for entry n
|
|
21
31
|
prd toggle --all Toggle all PRD entries
|
|
32
|
+
prd clean Remove all passing entries from the PRD
|
|
33
|
+
|
|
34
|
+
PRD LIST OPTIONS:
|
|
35
|
+
--category, -c <category> Filter PRD items by category
|
|
36
|
+
Valid: ui, feature, bugfix, setup, development, testing, docs
|
|
22
37
|
|
|
23
38
|
EXAMPLES:
|
|
24
|
-
ralph init # Initialize ralph
|
|
39
|
+
ralph init # Initialize ralph (language selection only)
|
|
40
|
+
ralph init --tech-stack # Initialize with technology stack selection
|
|
25
41
|
ralph once # Run single iteration
|
|
26
42
|
ralph run 5 # Run 5 iterations
|
|
43
|
+
ralph run --all # Run until all tasks complete (shows progress)
|
|
44
|
+
ralph run --all -c feature # Complete all feature tasks only
|
|
45
|
+
ralph run --loop # Run continuously until interrupted
|
|
46
|
+
ralph run --loop -c feature # Loop mode, only feature items
|
|
47
|
+
ralph run 5 --category feature # Run 5 iterations, only feature items
|
|
48
|
+
ralph run 3 -c bugfix # Run 3 iterations, only bugfix items
|
|
27
49
|
ralph prd add # Add new PRD entry
|
|
28
50
|
ralph prd list # Show all entries
|
|
51
|
+
ralph prd list -c feature # Show only feature entries
|
|
29
52
|
ralph prd status # Show completion summary
|
|
53
|
+
ralph prompt # Display resolved prompt
|
|
54
|
+
ralph prompt --raw # Display template with $variables
|
|
30
55
|
ralph scripts # Generate ralph.sh and ralph-once.sh
|
|
31
56
|
ralph docker # Generate Dockerfile for sandboxed env
|
|
32
57
|
ralph docker --build # Build Docker image
|
|
33
|
-
ralph docker --
|
|
58
|
+
ralph docker --build --clean # Clean and rebuild from scratch
|
|
59
|
+
ralph docker --run # Run container (auto-init/build if needed)
|
|
60
|
+
ralph docker --clean # Remove image and volumes
|
|
34
61
|
|
|
35
62
|
CONFIGURATION:
|
|
36
63
|
After running 'ralph init', you'll have:
|
|
37
64
|
.ralph/
|
|
38
|
-
├── config.json Project configuration
|
|
39
|
-
├── prompt.md
|
|
65
|
+
├── config.json Project configuration (language, commands, javaVersion)
|
|
66
|
+
├── prompt.md Prompt template with $variables ($language, $checkCommand, etc.)
|
|
40
67
|
├── prd.json Product requirements document
|
|
41
68
|
└── progress.txt Progress tracking file
|
|
42
69
|
`;
|
package/dist/commands/init.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare function init(
|
|
1
|
+
export declare function init(args: string[]): Promise<void>;
|
package/dist/commands/init.js
CHANGED
|
@@ -1,15 +1,25 @@
|
|
|
1
|
-
import { existsSync, writeFileSync, mkdirSync } from "fs";
|
|
2
|
-
import { join, basename } from "path";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
1
|
+
import { existsSync, writeFileSync, mkdirSync, copyFileSync } from "fs";
|
|
2
|
+
import { join, basename, dirname } from "path";
|
|
3
|
+
import { fileURLToPath } from "url";
|
|
4
|
+
import { LANGUAGES, generatePromptTemplate, DEFAULT_PRD, DEFAULT_PROGRESS } from "../templates/prompts.js";
|
|
5
|
+
import { promptSelect, promptConfirm, promptInput, promptMultiSelect } from "../utils/prompt.js";
|
|
6
|
+
// Get package root directory (works for both dev and installed package)
|
|
7
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
8
|
+
const __dirname = dirname(__filename);
|
|
9
|
+
const PACKAGE_ROOT = join(__dirname, "..", ".."); // dist/commands -> dist -> package root
|
|
5
10
|
const RALPH_DIR = ".ralph";
|
|
6
11
|
const CONFIG_FILE = "config.json";
|
|
7
12
|
const PROMPT_FILE = "prompt.md";
|
|
8
13
|
const PRD_FILE = "prd.json";
|
|
9
14
|
const PROGRESS_FILE = "progress.txt";
|
|
10
|
-
|
|
15
|
+
const PRD_GUIDE_FILE = "HOW-TO-WRITE-PRDs.md";
|
|
16
|
+
function hasFlag(args, ...flags) {
|
|
17
|
+
return args.some(arg => flags.includes(arg));
|
|
18
|
+
}
|
|
19
|
+
export async function init(args) {
|
|
11
20
|
const cwd = process.cwd();
|
|
12
21
|
const ralphDir = join(cwd, RALPH_DIR);
|
|
22
|
+
const showTechStack = hasFlag(args, "--tech-stack", "-t");
|
|
13
23
|
console.log("Initializing ralph in current directory...\n");
|
|
14
24
|
// Check for existing .ralph directory
|
|
15
25
|
if (existsSync(ralphDir)) {
|
|
@@ -30,6 +40,24 @@ export async function init(_args) {
|
|
|
30
40
|
const selectedIndex = languageNames.indexOf(selectedName);
|
|
31
41
|
const selectedKey = languageKeys[selectedIndex];
|
|
32
42
|
const config = LANGUAGES[selectedKey];
|
|
43
|
+
// Select technology stack if available (only when --tech-stack flag is provided)
|
|
44
|
+
let selectedTechnologies = [];
|
|
45
|
+
if (showTechStack && config.technologies && config.technologies.length > 0) {
|
|
46
|
+
const techOptions = config.technologies.map(t => `${t.name} - ${t.description}`);
|
|
47
|
+
const techNames = config.technologies.map(t => t.name);
|
|
48
|
+
selectedTechnologies = await promptMultiSelect("Select your technology stack (select multiple or add custom):", techOptions);
|
|
49
|
+
// Convert display names back to just technology names for predefined options
|
|
50
|
+
selectedTechnologies = selectedTechnologies.map(sel => {
|
|
51
|
+
const idx = techOptions.indexOf(sel);
|
|
52
|
+
return idx >= 0 ? techNames[idx] : sel;
|
|
53
|
+
});
|
|
54
|
+
if (selectedTechnologies.length > 0) {
|
|
55
|
+
console.log(`\nSelected technologies: ${selectedTechnologies.join(", ")}`);
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
console.log("\nNo technologies selected.");
|
|
59
|
+
}
|
|
60
|
+
}
|
|
33
61
|
// Allow custom commands
|
|
34
62
|
let checkCommand = config.checkCommand;
|
|
35
63
|
let testCommand = config.testCommand;
|
|
@@ -52,11 +80,15 @@ export async function init(_args) {
|
|
|
52
80
|
testCommand: finalConfig.testCommand,
|
|
53
81
|
imageName,
|
|
54
82
|
};
|
|
83
|
+
// Add technologies if any were selected
|
|
84
|
+
if (selectedTechnologies.length > 0) {
|
|
85
|
+
configData.technologies = selectedTechnologies;
|
|
86
|
+
}
|
|
55
87
|
const configPath = join(ralphDir, CONFIG_FILE);
|
|
56
88
|
writeFileSync(configPath, JSON.stringify(configData, null, 2) + "\n");
|
|
57
89
|
console.log(`\nCreated ${RALPH_DIR}/${CONFIG_FILE}`);
|
|
58
|
-
// Write prompt file (ask if exists)
|
|
59
|
-
const prompt =
|
|
90
|
+
// Write prompt file (ask if exists) - uses template with $variables
|
|
91
|
+
const prompt = generatePromptTemplate();
|
|
60
92
|
const promptPath = join(ralphDir, PROMPT_FILE);
|
|
61
93
|
if (existsSync(promptPath)) {
|
|
62
94
|
const overwritePrompt = await promptConfirm(`${RALPH_DIR}/${PROMPT_FILE} already exists. Overwrite?`);
|
|
@@ -90,9 +122,22 @@ export async function init(_args) {
|
|
|
90
122
|
else {
|
|
91
123
|
console.log(`Skipped ${RALPH_DIR}/${PROGRESS_FILE} (already exists)`);
|
|
92
124
|
}
|
|
125
|
+
// Copy PRD guide file from package if not exists
|
|
126
|
+
const prdGuidePath = join(ralphDir, PRD_GUIDE_FILE);
|
|
127
|
+
if (!existsSync(prdGuidePath)) {
|
|
128
|
+
const sourcePath = join(PACKAGE_ROOT, "docs", PRD_GUIDE_FILE);
|
|
129
|
+
if (existsSync(sourcePath)) {
|
|
130
|
+
copyFileSync(sourcePath, prdGuidePath);
|
|
131
|
+
console.log(`Created ${RALPH_DIR}/${PRD_GUIDE_FILE}`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
console.log(`Skipped ${RALPH_DIR}/${PRD_GUIDE_FILE} (already exists)`);
|
|
136
|
+
}
|
|
93
137
|
console.log("\nRalph initialized successfully!");
|
|
94
138
|
console.log("\nNext steps:");
|
|
95
|
-
console.log(" 1.
|
|
96
|
-
console.log(" 2.
|
|
97
|
-
console.log(" 3.
|
|
139
|
+
console.log(" 1. Read .ralph/HOW-TO-WRITE-PRDs.md for guidance on writing PRDs");
|
|
140
|
+
console.log(" 2. Edit .ralph/prd.json to add your project requirements");
|
|
141
|
+
console.log(" 3. Run 'ralph once' to start the first iteration");
|
|
142
|
+
console.log(" 4. Or run 'ralph run 5' for 5 automated iterations");
|
|
98
143
|
}
|
package/dist/commands/once.js
CHANGED
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
import { spawn } from "child_process";
|
|
2
|
-
import { checkFilesExist, loadPrompt, getPaths } from "../utils/config.js";
|
|
2
|
+
import { checkFilesExist, loadConfig, loadPrompt, getPaths } from "../utils/config.js";
|
|
3
|
+
import { resolvePromptVariables } from "../templates/prompts.js";
|
|
3
4
|
export async function once(_args) {
|
|
4
5
|
checkFilesExist();
|
|
5
|
-
const
|
|
6
|
+
const config = loadConfig();
|
|
7
|
+
const template = loadPrompt();
|
|
8
|
+
const prompt = resolvePromptVariables(template, {
|
|
9
|
+
language: config.language,
|
|
10
|
+
checkCommand: config.checkCommand,
|
|
11
|
+
testCommand: config.testCommand,
|
|
12
|
+
technologies: config.technologies,
|
|
13
|
+
});
|
|
6
14
|
const paths = getPaths();
|
|
7
15
|
console.log("Starting single ralph iteration...\n");
|
|
8
16
|
return new Promise((resolve, reject) => {
|