ralph-cli-claude 0.1.3 → 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
@@ -193,6 +193,21 @@ npm run dev -- once
193
193
 
194
194
  The `npm run dev -- <args>` command runs ralph directly from TypeScript source using `tsx`, allowing you to test changes without rebuilding.
195
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
+
196
211
  ## Requirements
197
212
 
198
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 },
@@ -302,11 +361,40 @@ async function buildImage(ralphDir) {
302
361
  });
303
362
  });
304
363
  }
305
- 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) {
306
383
  const dockerDir = join(ralphDir, DOCKER_DIR);
307
- if (!existsSync(join(dockerDir, "Dockerfile"))) {
308
- console.error("Dockerfile not found. Run 'ralph docker' first.");
309
- 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
+ }
310
398
  }
311
399
  console.log("Starting Docker container...\n");
312
400
  return new Promise((resolve, reject) => {
@@ -327,7 +415,167 @@ async function runContainer(ralphDir) {
327
415
  });
328
416
  });
329
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
+ }
330
577
  export async function docker(args) {
578
+ const hasFlag = (flag) => args.includes(flag);
331
579
  const flag = args[0];
332
580
  // Show help without requiring init
333
581
  if (flag === "--help" || flag === "-h") {
@@ -338,7 +586,9 @@ USAGE:
338
586
  ralph docker Generate Dockerfile and scripts
339
587
  ralph docker -y Generate files, overwrite without prompting
340
588
  ralph docker --build Build image (always fetches latest Claude Code)
341
- ralph docker --run Run container with project mounted
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
342
592
 
343
593
  FILES GENERATED:
344
594
  .ralph/docker/
@@ -354,7 +604,9 @@ AUTHENTICATION:
354
604
  EXAMPLES:
355
605
  ralph docker # Generate files
356
606
  ralph docker --build # Build image
607
+ ralph docker --build --clean # Clean and rebuild from scratch
357
608
  ralph docker --run # Start interactive shell
609
+ ralph docker --clean # Remove image and volumes
358
610
 
359
611
  # Or use docker compose directly:
360
612
  cd .ralph/docker && docker compose run --rm ralph
@@ -384,17 +636,29 @@ INSTALLING PACKAGES (works with Docker & Podman):
384
636
  const config = loadConfig();
385
637
  // Get image name from config or generate default
386
638
  const imageName = config.imageName || `ralph-${basename(process.cwd()).toLowerCase().replace(/[^a-z0-9-]/g, "-")}`;
387
- 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
388
643
  await buildImage(ralphDir);
389
644
  }
390
- else if (flag === "--run") {
391
- 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);
392
653
  }
393
654
  else {
394
655
  const force = flag === "-y" || flag === "--yes";
395
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
+ }
396
660
  console.log(`Image name: ${imageName}\n`);
397
- await generateFiles(ralphDir, config.language, imageName, force);
661
+ await generateFiles(ralphDir, config.language, imageName, force, config.javaVersion);
398
662
  console.log(`
399
663
  Docker files generated in .ralph/docker/
400
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) => {