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 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+
@@ -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 curl -fsSL https://go.dev/dl/go1.22.0.linux-amd64.tar.gz | tar -C /usr/local -xzf -
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
- function generateDockerfile(language) {
42
- const languageSnippet = LANGUAGE_SNIPPETS[language] || LANGUAGE_SNIPPETS.none;
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
- const proc = spawn("docker", ["compose", "build"], {
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 runContainer(ralphDir) {
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
- if (!existsSync(join(dockerDir, "Dockerfile"))) {
307
- console.error("Dockerfile not found. Run 'ralph docker' first.");
308
- process.exit(1);
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 the Docker image
340
- ralph docker --run Run container with project mounted
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
- if (flag === "--build") {
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 (flag === "--run") {
390
- await runContainer(ralphDir);
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
 
@@ -5,38 +5,65 @@ USAGE:
5
5
  ralph <command> [options]
6
6
 
7
7
  COMMANDS:
8
- init Initialize ralph in current project
8
+ init [opts] Initialize ralph in current project
9
9
  once Run a single automation iteration
10
- run <n> Run n automation iterations
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 List all PRD entries
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 for your project
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 --run # Run container interactively
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 Shared prompt template
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
  `;
@@ -1 +1 @@
1
- export declare function init(_args: string[]): Promise<void>;
1
+ export declare function init(args: string[]): Promise<void>;
@@ -1,15 +1,25 @@
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";
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
- export async function init(_args) {
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 = generatePrompt(finalConfig);
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. 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");
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
  }
@@ -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 prompt = loadPrompt();
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) => {