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 +15 -0
- package/dist/commands/docker.js +280 -16
- 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 +51 -12
- package/dist/commands/prompt.d.ts +1 -0
- package/dist/commands/prompt.js +52 -0
- package/dist/commands/run.js +222 -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 +2 -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
|
@@ -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+
|
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 },
|
|
@@ -302,11 +361,40 @@ async function buildImage(ralphDir) {
|
|
|
302
361
|
});
|
|
303
362
|
});
|
|
304
363
|
}
|
|
305
|
-
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) {
|
|
306
383
|
const dockerDir = join(ralphDir, DOCKER_DIR);
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
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 --
|
|
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
|
-
|
|
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 (
|
|
391
|
-
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);
|
|
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
|
|
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) => {
|