ralph-cli-sandboxed 0.1.5 → 0.2.1
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 +7 -7
- package/dist/commands/docker.js +139 -62
- package/dist/commands/help.js +38 -17
- package/dist/commands/init.d.ts +1 -1
- package/dist/commands/init.js +49 -17
- package/dist/commands/once.js +8 -5
- package/dist/commands/run.js +17 -14
- package/dist/config/cli-providers.json +100 -0
- package/dist/templates/prompts.d.ts +18 -0
- package/dist/templates/prompts.js +16 -0
- package/dist/utils/config.d.ts +3 -0
- package/dist/utils/config.js +25 -3
- package/dist/utils/prompt.d.ts +2 -0
- package/dist/utils/prompt.js +138 -0
- package/package.json +5 -4
package/README.md
CHANGED
|
@@ -29,8 +29,8 @@ ralph add
|
|
|
29
29
|
# 3. Run a single iteration
|
|
30
30
|
ralph once
|
|
31
31
|
|
|
32
|
-
# 4. Or run
|
|
33
|
-
ralph run
|
|
32
|
+
# 4. Or run until all tasks complete (default)
|
|
33
|
+
ralph run
|
|
34
34
|
```
|
|
35
35
|
|
|
36
36
|
## Commands
|
|
@@ -39,13 +39,13 @@ ralph run 5
|
|
|
39
39
|
|---------|-------------|
|
|
40
40
|
| `ralph init` | Initialize ralph in current project |
|
|
41
41
|
| `ralph once` | Run a single automation iteration |
|
|
42
|
-
| `ralph run
|
|
42
|
+
| `ralph run [n]` | Run automation iterations (default: all tasks) |
|
|
43
43
|
| `ralph add` | Add a new PRD entry (interactive) |
|
|
44
44
|
| `ralph list` | List all PRD entries |
|
|
45
45
|
| `ralph status` | Show PRD completion status |
|
|
46
46
|
| `ralph toggle <n>` | Toggle passes status for entry n |
|
|
47
47
|
| `ralph clean` | Remove all passing entries from PRD |
|
|
48
|
-
| `ralph docker
|
|
48
|
+
| `ralph docker <sub>` | Manage Docker sandbox environment |
|
|
49
49
|
| `ralph help` | Show help message |
|
|
50
50
|
|
|
51
51
|
> **Note:** `ralph prd <subcommand>` still works for compatibility (e.g., `ralph prd add`).
|
|
@@ -116,13 +116,13 @@ Run ralph in an isolated Docker container:
|
|
|
116
116
|
|
|
117
117
|
```bash
|
|
118
118
|
# Generate Docker files
|
|
119
|
-
ralph docker
|
|
119
|
+
ralph docker init
|
|
120
120
|
|
|
121
121
|
# Build the image
|
|
122
|
-
ralph docker
|
|
122
|
+
ralph docker build
|
|
123
123
|
|
|
124
124
|
# Run container
|
|
125
|
-
ralph docker
|
|
125
|
+
ralph docker run
|
|
126
126
|
```
|
|
127
127
|
|
|
128
128
|
Features:
|
package/dist/commands/docker.js
CHANGED
|
@@ -3,7 +3,7 @@ import { join, basename } from "path";
|
|
|
3
3
|
import { spawn } from "child_process";
|
|
4
4
|
import { loadConfig, getRalphDir } from "../utils/config.js";
|
|
5
5
|
import { promptConfirm } from "../utils/prompt.js";
|
|
6
|
-
import { getLanguagesJson } from "../templates/prompts.js";
|
|
6
|
+
import { getLanguagesJson, getCliProvidersJson } from "../templates/prompts.js";
|
|
7
7
|
const DOCKER_DIR = "docker";
|
|
8
8
|
// Get language Docker snippet from config, with version substitution
|
|
9
9
|
function getLanguageSnippet(language, javaVersion) {
|
|
@@ -22,8 +22,20 @@ function getLanguageSnippet(language, javaVersion) {
|
|
|
22
22
|
}
|
|
23
23
|
return "\n" + snippet + "\n";
|
|
24
24
|
}
|
|
25
|
-
|
|
25
|
+
// Get CLI provider Docker snippet from config
|
|
26
|
+
function getCliProviderSnippet(cliProvider) {
|
|
27
|
+
const cliProvidersJson = getCliProvidersJson();
|
|
28
|
+
const providerKey = cliProvider || "claude";
|
|
29
|
+
const provider = cliProvidersJson.providers[providerKey];
|
|
30
|
+
if (!provider || !provider.docker) {
|
|
31
|
+
// Default to Claude Code CLI if provider not found
|
|
32
|
+
return "# Install Claude Code CLI (as node user so it installs to /home/node/.local/bin)\nRUN su - node -c 'curl -fsSL https://claude.ai/install.sh | bash' \\\n && echo 'export PATH=\"$HOME/.local/bin:$PATH\"' >> /home/node/.zshrc";
|
|
33
|
+
}
|
|
34
|
+
return provider.docker.install;
|
|
35
|
+
}
|
|
36
|
+
function generateDockerfile(language, javaVersion, cliProvider) {
|
|
26
37
|
const languageSnippet = getLanguageSnippet(language, javaVersion);
|
|
38
|
+
const cliSnippet = getCliProviderSnippet(cliProvider);
|
|
27
39
|
return `# Ralph CLI Sandbox Environment
|
|
28
40
|
# Based on Claude Code devcontainer
|
|
29
41
|
# Generated by ralph-cli
|
|
@@ -32,7 +44,6 @@ FROM node:20-bookworm
|
|
|
32
44
|
|
|
33
45
|
ARG DEBIAN_FRONTEND=noninteractive
|
|
34
46
|
ARG TZ=UTC
|
|
35
|
-
ARG CLAUDE_CODE_VERSION="latest"
|
|
36
47
|
ARG ZSH_IN_DOCKER_VERSION="1.2.1"
|
|
37
48
|
|
|
38
49
|
# Set timezone
|
|
@@ -92,11 +103,10 @@ RUN cp -r /root/.oh-my-zsh /home/node/.oh-my-zsh && chown -R node:node /home/nod
|
|
|
92
103
|
echo ' echo ""' >> /home/node/.zshrc && \\
|
|
93
104
|
echo 'fi' >> /home/node/.zshrc
|
|
94
105
|
|
|
95
|
-
|
|
96
|
-
RUN npm install -g @anthropic-ai/claude-code@\${CLAUDE_CODE_VERSION}
|
|
106
|
+
${cliSnippet}
|
|
97
107
|
|
|
98
|
-
# Install ralph-cli-
|
|
99
|
-
RUN npm install -g ralph-cli-
|
|
108
|
+
# Install ralph-cli-sandboxed from npm registry
|
|
109
|
+
RUN npm install -g ralph-cli-sandboxed
|
|
100
110
|
${languageSnippet}
|
|
101
111
|
# Setup sudo only for firewall script (no general sudo for security)
|
|
102
112
|
RUN echo "node ALL=(ALL) NOPASSWD: /usr/local/bin/init-firewall.sh" >> /etc/sudoers.d/node-firewall
|
|
@@ -245,7 +255,7 @@ dist
|
|
|
245
255
|
.git
|
|
246
256
|
*.log
|
|
247
257
|
`;
|
|
248
|
-
async function generateFiles(ralphDir, language, imageName, force = false, javaVersion) {
|
|
258
|
+
async function generateFiles(ralphDir, language, imageName, force = false, javaVersion, cliProvider) {
|
|
249
259
|
const dockerDir = join(ralphDir, DOCKER_DIR);
|
|
250
260
|
// Create docker directory
|
|
251
261
|
if (!existsSync(dockerDir)) {
|
|
@@ -253,7 +263,7 @@ async function generateFiles(ralphDir, language, imageName, force = false, javaV
|
|
|
253
263
|
console.log(`Created ${DOCKER_DIR}/`);
|
|
254
264
|
}
|
|
255
265
|
const files = [
|
|
256
|
-
{ name: "Dockerfile", content: generateDockerfile(language, javaVersion) },
|
|
266
|
+
{ name: "Dockerfile", content: generateDockerfile(language, javaVersion, cliProvider) },
|
|
257
267
|
{ name: "init-firewall.sh", content: FIREWALL_SCRIPT },
|
|
258
268
|
{ name: "docker-compose.yml", content: generateDockerCompose(imageName) },
|
|
259
269
|
{ name: ".dockerignore", content: DOCKERIGNORE },
|
|
@@ -280,10 +290,14 @@ async function buildImage(ralphDir) {
|
|
|
280
290
|
console.error("Dockerfile not found. Run 'ralph docker' first.");
|
|
281
291
|
process.exit(1);
|
|
282
292
|
}
|
|
283
|
-
console.log("Building Docker image
|
|
293
|
+
console.log("Building Docker image...\n");
|
|
294
|
+
// Get image name for compose project name
|
|
295
|
+
const config = loadConfig();
|
|
296
|
+
const imageName = config.imageName || `ralph-${basename(process.cwd()).toLowerCase().replace(/[^a-z0-9-]/g, "-")}`;
|
|
284
297
|
return new Promise((resolve, reject) => {
|
|
285
|
-
// Use --no-cache and --pull to ensure we always get the latest
|
|
286
|
-
|
|
298
|
+
// Use --no-cache and --pull to ensure we always get the latest CLI versions
|
|
299
|
+
// Use -p to set unique project name per ralph project
|
|
300
|
+
const proc = spawn("docker", ["compose", "-p", imageName, "build", "--no-cache", "--pull"], {
|
|
287
301
|
cwd: dockerDir,
|
|
288
302
|
stdio: "inherit",
|
|
289
303
|
});
|
|
@@ -319,7 +333,28 @@ async function imageExists(imageName) {
|
|
|
319
333
|
});
|
|
320
334
|
});
|
|
321
335
|
}
|
|
322
|
-
|
|
336
|
+
// Get CLI provider configuration
|
|
337
|
+
function getCliProviderConfig(cliProvider) {
|
|
338
|
+
const cliProvidersJson = getCliProvidersJson();
|
|
339
|
+
const providerKey = cliProvider || "claude";
|
|
340
|
+
const provider = cliProvidersJson.providers[providerKey];
|
|
341
|
+
if (!provider) {
|
|
342
|
+
// Default to Claude Code CLI if provider not found
|
|
343
|
+
return {
|
|
344
|
+
name: "Claude Code",
|
|
345
|
+
command: "claude",
|
|
346
|
+
yoloArgs: ["--dangerously-skip-permissions"],
|
|
347
|
+
envVars: ["ANTHROPIC_API_KEY"],
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
return {
|
|
351
|
+
name: provider.name,
|
|
352
|
+
command: provider.command,
|
|
353
|
+
yoloArgs: provider.yoloArgs || [],
|
|
354
|
+
envVars: provider.envVars || [],
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
async function runContainer(ralphDir, imageName, language, javaVersion, cliProvider) {
|
|
323
358
|
const dockerDir = join(ralphDir, DOCKER_DIR);
|
|
324
359
|
const dockerfileExists = existsSync(join(dockerDir, "Dockerfile"));
|
|
325
360
|
const hasImage = await imageExists(imageName);
|
|
@@ -327,7 +362,7 @@ async function runContainer(ralphDir, imageName, language, javaVersion) {
|
|
|
327
362
|
if (!dockerfileExists || !hasImage) {
|
|
328
363
|
if (!dockerfileExists) {
|
|
329
364
|
console.log("Docker folder not found. Initializing docker setup...\n");
|
|
330
|
-
await generateFiles(ralphDir, language, imageName, true, javaVersion);
|
|
365
|
+
await generateFiles(ralphDir, language, imageName, true, javaVersion, cliProvider);
|
|
331
366
|
console.log("");
|
|
332
367
|
}
|
|
333
368
|
if (!hasImage) {
|
|
@@ -336,9 +371,37 @@ async function runContainer(ralphDir, imageName, language, javaVersion) {
|
|
|
336
371
|
console.log("");
|
|
337
372
|
}
|
|
338
373
|
}
|
|
374
|
+
// Get CLI provider info for the startup note
|
|
375
|
+
const cliConfig = getCliProviderConfig(cliProvider);
|
|
376
|
+
const yoloCommand = cliConfig.yoloArgs.length > 0
|
|
377
|
+
? `${cliConfig.command} ${cliConfig.yoloArgs.join(" ")}`
|
|
378
|
+
: cliConfig.command;
|
|
339
379
|
console.log("Starting Docker container...\n");
|
|
380
|
+
// Show note about yolo mode and credentials
|
|
381
|
+
console.log("IMPORTANT: Getting Started");
|
|
382
|
+
console.log("-".repeat(40));
|
|
383
|
+
console.log("");
|
|
384
|
+
console.log("To run ralph automation, you might need to activate YOLO mode");
|
|
385
|
+
console.log("which allows the AI to execute commands without prompts.");
|
|
386
|
+
console.log("");
|
|
387
|
+
console.log(`CLI Provider: ${cliConfig.name}`);
|
|
388
|
+
console.log(`Yolo command: ${yoloCommand}`);
|
|
389
|
+
console.log("");
|
|
390
|
+
console.log("Before running 'ralph run' or 'ralph once', ensure your");
|
|
391
|
+
console.log("credentials are configured:");
|
|
392
|
+
console.log("");
|
|
393
|
+
if (cliConfig.envVars.length > 0) {
|
|
394
|
+
console.log("Required environment variables:");
|
|
395
|
+
for (const envVar of cliConfig.envVars) {
|
|
396
|
+
console.log(` - ${envVar}`);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
console.log("");
|
|
400
|
+
console.log("Set them in docker-compose.yml or export before running.");
|
|
401
|
+
console.log("");
|
|
340
402
|
return new Promise((resolve, reject) => {
|
|
341
|
-
|
|
403
|
+
// Use -p to set unique project name per ralph project
|
|
404
|
+
const proc = spawn("docker", ["compose", "-p", imageName, "run", "--rm", "ralph"], {
|
|
342
405
|
cwd: dockerDir,
|
|
343
406
|
stdio: "inherit",
|
|
344
407
|
});
|
|
@@ -361,8 +424,9 @@ async function cleanImage(imageName, ralphDir) {
|
|
|
361
424
|
// First, stop any running containers via docker compose
|
|
362
425
|
if (existsSync(join(dockerDir, "docker-compose.yml"))) {
|
|
363
426
|
// Stop running containers first
|
|
427
|
+
// Use -p to target only this project's resources
|
|
364
428
|
await new Promise((resolve) => {
|
|
365
|
-
const proc = spawn("docker", ["compose", "stop", "--timeout", "5"], {
|
|
429
|
+
const proc = spawn("docker", ["compose", "-p", imageName, "stop", "--timeout", "5"], {
|
|
366
430
|
cwd: dockerDir,
|
|
367
431
|
stdio: "inherit",
|
|
368
432
|
});
|
|
@@ -374,8 +438,9 @@ async function cleanImage(imageName, ralphDir) {
|
|
|
374
438
|
});
|
|
375
439
|
});
|
|
376
440
|
// Remove containers, volumes, networks, and local images
|
|
441
|
+
// Use -p to target only this project's resources
|
|
377
442
|
await new Promise((resolve) => {
|
|
378
|
-
const proc = spawn("docker", ["compose", "down", "--rmi", "local", "-v", "--remove-orphans", "--timeout", "5"], {
|
|
443
|
+
const proc = spawn("docker", ["compose", "-p", imageName, "down", "--rmi", "local", "-v", "--remove-orphans", "--timeout", "5"], {
|
|
379
444
|
cwd: dockerDir,
|
|
380
445
|
stdio: "inherit",
|
|
381
446
|
});
|
|
@@ -388,9 +453,10 @@ async function cleanImage(imageName, ralphDir) {
|
|
|
388
453
|
});
|
|
389
454
|
});
|
|
390
455
|
}
|
|
391
|
-
// Find and forcibly remove any containers using volumes with our
|
|
456
|
+
// Find and forcibly remove any containers using volumes with our project name pattern
|
|
392
457
|
// This handles orphaned containers from previous runs or pods
|
|
393
|
-
|
|
458
|
+
// Project name is now imageName (via -p flag), so volumes are named ${imageName}_*
|
|
459
|
+
const volumePattern = imageName;
|
|
394
460
|
await new Promise((resolve) => {
|
|
395
461
|
// List all containers (including stopped) and filter by volume name pattern
|
|
396
462
|
const proc = spawn("docker", ["ps", "-aq", "--filter", `volume=${volumePattern}`], {
|
|
@@ -500,8 +566,8 @@ async function cleanImage(imageName, ralphDir) {
|
|
|
500
566
|
resolve();
|
|
501
567
|
});
|
|
502
568
|
});
|
|
503
|
-
// Clean up project-specific network (
|
|
504
|
-
const networkName =
|
|
569
|
+
// Clean up project-specific network (project name is imageName via -p flag)
|
|
570
|
+
const networkName = `${imageName}_default`;
|
|
505
571
|
await new Promise((resolve) => {
|
|
506
572
|
const proc = spawn("docker", ["network", "rm", networkName], {
|
|
507
573
|
stdio: ["ignore", "ignore", "ignore"], // Suppress output - network may not exist
|
|
@@ -514,23 +580,24 @@ async function cleanImage(imageName, ralphDir) {
|
|
|
514
580
|
});
|
|
515
581
|
});
|
|
516
582
|
console.log("\nDocker image and associated resources cleaned.");
|
|
517
|
-
console.log("Run 'ralph docker
|
|
583
|
+
console.log("Run 'ralph docker build' to rebuild the image.");
|
|
518
584
|
}
|
|
519
585
|
export async function docker(args) {
|
|
520
|
-
const
|
|
521
|
-
const
|
|
586
|
+
const subcommand = args[0];
|
|
587
|
+
const subArgs = args.slice(1);
|
|
522
588
|
// Show help without requiring init
|
|
523
|
-
if (
|
|
589
|
+
if (subcommand === "help" || subcommand === "--help" || subcommand === "-h") {
|
|
524
590
|
console.log(`
|
|
525
591
|
ralph docker - Generate and manage Docker sandbox environment
|
|
526
592
|
|
|
527
593
|
USAGE:
|
|
528
|
-
ralph docker
|
|
529
|
-
ralph docker -y
|
|
530
|
-
ralph docker
|
|
531
|
-
ralph docker
|
|
532
|
-
ralph docker
|
|
533
|
-
ralph docker
|
|
594
|
+
ralph docker init Generate Dockerfile and scripts
|
|
595
|
+
ralph docker init -y Generate files, overwrite without prompting
|
|
596
|
+
ralph docker build Build image (fetches latest CLI versions)
|
|
597
|
+
ralph docker build --clean Clean existing image and rebuild from scratch
|
|
598
|
+
ralph docker run Run container (auto-init and build if needed)
|
|
599
|
+
ralph docker clean Remove Docker image and associated resources
|
|
600
|
+
ralph docker help Show this help message
|
|
534
601
|
|
|
535
602
|
FILES GENERATED:
|
|
536
603
|
.ralph/docker/
|
|
@@ -544,11 +611,11 @@ AUTHENTICATION:
|
|
|
544
611
|
API key users: Uncomment ANTHROPIC_API_KEY in docker-compose.yml.
|
|
545
612
|
|
|
546
613
|
EXAMPLES:
|
|
547
|
-
ralph docker
|
|
548
|
-
ralph docker
|
|
549
|
-
ralph docker
|
|
550
|
-
ralph docker
|
|
551
|
-
ralph docker
|
|
614
|
+
ralph docker init # Generate files
|
|
615
|
+
ralph docker build # Build image
|
|
616
|
+
ralph docker build --clean # Clean and rebuild from scratch
|
|
617
|
+
ralph docker run # Start interactive shell
|
|
618
|
+
ralph docker clean # Remove image and volumes
|
|
552
619
|
|
|
553
620
|
# Or use docker compose directly:
|
|
554
621
|
cd .ralph/docker && docker compose run --rm ralph
|
|
@@ -578,38 +645,48 @@ INSTALLING PACKAGES (works with Docker & Podman):
|
|
|
578
645
|
const config = loadConfig();
|
|
579
646
|
// Get image name from config or generate default
|
|
580
647
|
const imageName = config.imageName || `ralph-${basename(process.cwd()).toLowerCase().replace(/[^a-z0-9-]/g, "-")}`;
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
648
|
+
const hasFlag = (flag) => subArgs.includes(flag);
|
|
649
|
+
switch (subcommand) {
|
|
650
|
+
case "build":
|
|
651
|
+
// Handle build --clean combination: clean first, then build
|
|
652
|
+
if (hasFlag("--clean")) {
|
|
653
|
+
await cleanImage(imageName, ralphDir);
|
|
654
|
+
console.log(""); // Add spacing between clean and build output
|
|
655
|
+
}
|
|
656
|
+
await buildImage(ralphDir);
|
|
657
|
+
break;
|
|
658
|
+
case "run":
|
|
659
|
+
await runContainer(ralphDir, imageName, config.language, config.javaVersion, config.cliProvider);
|
|
660
|
+
break;
|
|
661
|
+
case "clean":
|
|
662
|
+
await cleanImage(imageName, ralphDir);
|
|
663
|
+
break;
|
|
664
|
+
case "init":
|
|
665
|
+
default: {
|
|
666
|
+
// Default to init if no subcommand or unrecognized subcommand
|
|
667
|
+
const force = subcommand === "init"
|
|
668
|
+
? (subArgs[0] === "-y" || subArgs[0] === "--yes")
|
|
669
|
+
: (subcommand === "-y" || subcommand === "--yes");
|
|
670
|
+
console.log(`Generating Docker files for: ${config.language}`);
|
|
671
|
+
if ((config.language === "java" || config.language === "kotlin") && config.javaVersion) {
|
|
672
|
+
console.log(`Java version: ${config.javaVersion}`);
|
|
673
|
+
}
|
|
674
|
+
if (config.cliProvider && config.cliProvider !== "claude") {
|
|
675
|
+
console.log(`CLI provider: ${config.cliProvider}`);
|
|
676
|
+
}
|
|
677
|
+
console.log(`Image name: ${imageName}\n`);
|
|
678
|
+
await generateFiles(ralphDir, config.language, imageName, force, config.javaVersion, config.cliProvider);
|
|
679
|
+
console.log(`
|
|
605
680
|
Docker files generated in .ralph/docker/
|
|
606
681
|
|
|
607
682
|
Next steps:
|
|
608
|
-
1. Build the image: ralph docker
|
|
609
|
-
2. Run container: ralph docker
|
|
683
|
+
1. Build the image: ralph docker build
|
|
684
|
+
2. Run container: ralph docker run
|
|
610
685
|
|
|
611
686
|
Or use docker compose directly:
|
|
612
687
|
cd .ralph/docker && docker compose run --rm ralph
|
|
613
688
|
`);
|
|
689
|
+
break;
|
|
690
|
+
}
|
|
614
691
|
}
|
|
615
692
|
}
|
package/dist/commands/help.js
CHANGED
|
@@ -7,23 +7,27 @@ USAGE:
|
|
|
7
7
|
COMMANDS:
|
|
8
8
|
init [opts] Initialize ralph in current project
|
|
9
9
|
once Run a single automation iteration
|
|
10
|
-
run
|
|
10
|
+
run [n] [opts] Run automation iterations (default: all tasks)
|
|
11
11
|
add Add a new PRD entry (interactive)
|
|
12
12
|
list [opts] List all PRD entries
|
|
13
13
|
status Show PRD completion status
|
|
14
14
|
toggle <n> Toggle passes status for entry n
|
|
15
15
|
clean Remove all passing entries from the PRD
|
|
16
16
|
prompt [opts] Display resolved prompt (for testing in Claude Code)
|
|
17
|
-
docker
|
|
17
|
+
docker <sub> Manage Docker sandbox environment
|
|
18
18
|
help Show this help message
|
|
19
19
|
|
|
20
20
|
prd <subcommand> (Alias) Manage PRD entries - same as add/list/status/toggle/clean
|
|
21
21
|
|
|
22
|
-
INIT
|
|
23
|
-
|
|
22
|
+
INIT:
|
|
23
|
+
The init command uses interactive prompts with arrow key navigation:
|
|
24
|
+
1. Select AI CLI provider (Claude Code, Aider, OpenCode, etc.)
|
|
25
|
+
2. Select project language/runtime
|
|
26
|
+
3. Select technology stack (if available for the language)
|
|
24
27
|
|
|
25
28
|
RUN OPTIONS:
|
|
26
|
-
|
|
29
|
+
<n> Run exactly n iterations (overrides default --all behavior)
|
|
30
|
+
--all, -a Run until all tasks are complete (default behavior)
|
|
27
31
|
--loop, -l Run continuously, waiting for new items when complete
|
|
28
32
|
--category, -c <category> Filter PRD items by category
|
|
29
33
|
Valid: ui, feature, bugfix, setup, development, testing, docs
|
|
@@ -38,13 +42,19 @@ TOGGLE OPTIONS:
|
|
|
38
42
|
<n> [n2] [n3]... Toggle one or more entries by number
|
|
39
43
|
--all, -a Toggle all PRD entries
|
|
40
44
|
|
|
45
|
+
DOCKER SUBCOMMANDS:
|
|
46
|
+
docker init Generate Dockerfile and scripts
|
|
47
|
+
docker build Build image (always fetches latest Claude Code)
|
|
48
|
+
docker run Run container (auto-init and build if needed)
|
|
49
|
+
docker clean Remove Docker image and associated resources
|
|
50
|
+
docker help Show docker help message
|
|
51
|
+
|
|
41
52
|
EXAMPLES:
|
|
42
|
-
ralph init # Initialize ralph (language selection
|
|
43
|
-
ralph init --tech-stack # Initialize with technology stack selection
|
|
53
|
+
ralph init # Initialize ralph (interactive CLI, language, tech selection)
|
|
44
54
|
ralph once # Run single iteration
|
|
45
|
-
ralph run
|
|
46
|
-
ralph run
|
|
47
|
-
ralph run
|
|
55
|
+
ralph run # Run until all tasks complete (default)
|
|
56
|
+
ralph run 5 # Run exactly 5 iterations
|
|
57
|
+
ralph run -c feature # Complete all feature tasks only
|
|
48
58
|
ralph run --loop # Run continuously until interrupted
|
|
49
59
|
ralph add # Add new PRD entry
|
|
50
60
|
ralph list # Show all entries
|
|
@@ -57,9 +67,9 @@ EXAMPLES:
|
|
|
57
67
|
ralph toggle --all # Toggle all entries
|
|
58
68
|
ralph clean # Remove passing entries
|
|
59
69
|
ralph prompt # Display resolved prompt
|
|
60
|
-
ralph docker
|
|
61
|
-
ralph docker
|
|
62
|
-
ralph docker
|
|
70
|
+
ralph docker init # Generate Dockerfile for sandboxed env
|
|
71
|
+
ralph docker build # Build Docker image
|
|
72
|
+
ralph docker run # Run container (auto-init/build if needed)
|
|
63
73
|
|
|
64
74
|
CONFIGURATION:
|
|
65
75
|
After running 'ralph init', you'll have:
|
|
@@ -70,15 +80,26 @@ CONFIGURATION:
|
|
|
70
80
|
└── progress.txt Progress tracking file
|
|
71
81
|
|
|
72
82
|
CLI CONFIGURATION:
|
|
73
|
-
The CLI tool
|
|
83
|
+
The CLI tool is configured during 'ralph init' and stored in .ralph/config.json:
|
|
74
84
|
{
|
|
75
85
|
"cli": {
|
|
76
86
|
"command": "claude",
|
|
77
|
-
"args": ["--permission-mode", "acceptEdits"]
|
|
78
|
-
|
|
87
|
+
"args": ["--permission-mode", "acceptEdits"],
|
|
88
|
+
"yoloArgs": ["--dangerously-skip-permissions"]
|
|
89
|
+
},
|
|
90
|
+
"cliProvider": "claude"
|
|
79
91
|
}
|
|
80
92
|
|
|
81
|
-
|
|
93
|
+
Available CLI providers (selected during 'ralph init'):
|
|
94
|
+
- claude: Claude Code (default)
|
|
95
|
+
- aider: AI pair programming
|
|
96
|
+
- codex: OpenAI Codex CLI
|
|
97
|
+
- gemini: Google Gemini CLI
|
|
98
|
+
- opencode: Open source AI coding agent
|
|
99
|
+
- amp: Sourcegraph AMP CLI
|
|
100
|
+
- custom: Configure your own CLI
|
|
101
|
+
|
|
102
|
+
Customize 'command', 'args', and 'yoloArgs' for other AI CLIs.
|
|
82
103
|
`;
|
|
83
104
|
export function help(_args) {
|
|
84
105
|
console.log(HELP_TEXT.trim());
|
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,9 +1,8 @@
|
|
|
1
1
|
import { existsSync, writeFileSync, mkdirSync, copyFileSync } from "fs";
|
|
2
2
|
import { join, basename, dirname } from "path";
|
|
3
3
|
import { fileURLToPath } from "url";
|
|
4
|
-
import { getLanguages, generatePromptTemplate, DEFAULT_PRD, DEFAULT_PROGRESS } from "../templates/prompts.js";
|
|
5
|
-
import {
|
|
6
|
-
import { DEFAULT_CLI_CONFIG } from "../utils/config.js";
|
|
4
|
+
import { getLanguages, generatePromptTemplate, DEFAULT_PRD, DEFAULT_PROGRESS, getCliProviders } from "../templates/prompts.js";
|
|
5
|
+
import { promptSelectWithArrows, promptConfirm, promptInput, promptMultiSelectWithArrows } from "../utils/prompt.js";
|
|
7
6
|
// Get package root directory (works for both dev and installed package)
|
|
8
7
|
const __filename = fileURLToPath(import.meta.url);
|
|
9
8
|
const __dirname = dirname(__filename);
|
|
@@ -14,13 +13,9 @@ const PROMPT_FILE = "prompt.md";
|
|
|
14
13
|
const PRD_FILE = "prd.json";
|
|
15
14
|
const PROGRESS_FILE = "progress.txt";
|
|
16
15
|
const PRD_GUIDE_FILE = "HOW-TO-WRITE-PRDs.md";
|
|
17
|
-
function
|
|
18
|
-
return args.some(arg => flags.includes(arg));
|
|
19
|
-
}
|
|
20
|
-
export async function init(args) {
|
|
16
|
+
export async function init(_args) {
|
|
21
17
|
const cwd = process.cwd();
|
|
22
18
|
const ralphDir = join(cwd, RALPH_DIR);
|
|
23
|
-
const showTechStack = hasFlag(args, "--tech-stack", "-t");
|
|
24
19
|
console.log("Initializing ralph in current directory...\n");
|
|
25
20
|
// Check for existing .ralph directory
|
|
26
21
|
if (existsSync(ralphDir)) {
|
|
@@ -34,20 +29,55 @@ export async function init(args) {
|
|
|
34
29
|
mkdirSync(ralphDir, { recursive: true });
|
|
35
30
|
console.log(`Created ${RALPH_DIR}/`);
|
|
36
31
|
}
|
|
37
|
-
// Select
|
|
32
|
+
// Step 1: Select CLI provider (first)
|
|
33
|
+
const CLI_PROVIDERS = getCliProviders();
|
|
34
|
+
const providerKeys = Object.keys(CLI_PROVIDERS);
|
|
35
|
+
const providerNames = providerKeys.map(k => `${CLI_PROVIDERS[k].name} - ${CLI_PROVIDERS[k].description}`);
|
|
36
|
+
const selectedProviderName = await promptSelectWithArrows("Select your AI CLI provider:", providerNames);
|
|
37
|
+
const selectedProviderIndex = providerNames.indexOf(selectedProviderName);
|
|
38
|
+
const selectedCliProviderKey = providerKeys[selectedProviderIndex];
|
|
39
|
+
const selectedProvider = CLI_PROVIDERS[selectedCliProviderKey];
|
|
40
|
+
let cliConfig;
|
|
41
|
+
// Handle custom CLI provider
|
|
42
|
+
if (selectedCliProviderKey === "custom") {
|
|
43
|
+
const customCommand = await promptInput("\nEnter your CLI command: ");
|
|
44
|
+
const customArgsInput = await promptInput("Enter default arguments (space-separated): ");
|
|
45
|
+
const customArgs = customArgsInput.trim() ? customArgsInput.trim().split(/\s+/) : [];
|
|
46
|
+
const customYoloArgsInput = await promptInput("Enter yolo/auto-approve arguments (space-separated): ");
|
|
47
|
+
const customYoloArgs = customYoloArgsInput.trim() ? customYoloArgsInput.trim().split(/\s+/) : [];
|
|
48
|
+
const customPromptArgsInput = await promptInput("Enter prompt arguments (e.g., -p for flag-based, leave empty for positional): ");
|
|
49
|
+
const customPromptArgs = customPromptArgsInput.trim() ? customPromptArgsInput.trim().split(/\s+/) : [];
|
|
50
|
+
cliConfig = {
|
|
51
|
+
command: customCommand || "claude",
|
|
52
|
+
args: customArgs,
|
|
53
|
+
yoloArgs: customYoloArgs.length > 0 ? customYoloArgs : undefined,
|
|
54
|
+
promptArgs: customPromptArgs,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
cliConfig = {
|
|
59
|
+
command: selectedProvider.command,
|
|
60
|
+
args: selectedProvider.defaultArgs,
|
|
61
|
+
yoloArgs: selectedProvider.yoloArgs.length > 0 ? selectedProvider.yoloArgs : undefined,
|
|
62
|
+
promptArgs: selectedProvider.promptArgs ?? [],
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
console.log(`\nSelected CLI provider: ${CLI_PROVIDERS[selectedCliProviderKey].name}`);
|
|
66
|
+
// Step 2: Select language (second)
|
|
38
67
|
const LANGUAGES = getLanguages();
|
|
39
68
|
const languageKeys = Object.keys(LANGUAGES);
|
|
40
69
|
const languageNames = languageKeys.map(k => `${LANGUAGES[k].name} - ${LANGUAGES[k].description}`);
|
|
41
|
-
const selectedName = await
|
|
70
|
+
const selectedName = await promptSelectWithArrows("Select your project language/runtime:", languageNames);
|
|
42
71
|
const selectedIndex = languageNames.indexOf(selectedName);
|
|
43
72
|
const selectedKey = languageKeys[selectedIndex];
|
|
44
73
|
const config = LANGUAGES[selectedKey];
|
|
45
|
-
|
|
74
|
+
console.log(`\nSelected language: ${config.name}`);
|
|
75
|
+
// Step 3: Select technology stack if available (third)
|
|
46
76
|
let selectedTechnologies = [];
|
|
47
|
-
if (
|
|
77
|
+
if (config.technologies && config.technologies.length > 0) {
|
|
48
78
|
const techOptions = config.technologies.map(t => `${t.name} - ${t.description}`);
|
|
49
79
|
const techNames = config.technologies.map(t => t.name);
|
|
50
|
-
selectedTechnologies = await
|
|
80
|
+
selectedTechnologies = await promptMultiSelectWithArrows("Select your technology stack (optional):", techOptions);
|
|
51
81
|
// Convert display names back to just technology names for predefined options
|
|
52
82
|
selectedTechnologies = selectedTechnologies.map(sel => {
|
|
53
83
|
const idx = techOptions.indexOf(sel);
|
|
@@ -60,7 +90,7 @@ export async function init(args) {
|
|
|
60
90
|
console.log("\nNo technologies selected.");
|
|
61
91
|
}
|
|
62
92
|
}
|
|
63
|
-
// Allow custom commands
|
|
93
|
+
// Allow custom commands for "none" language
|
|
64
94
|
let checkCommand = config.checkCommand;
|
|
65
95
|
let testCommand = config.testCommand;
|
|
66
96
|
if (selectedKey === "none") {
|
|
@@ -81,7 +111,8 @@ export async function init(args) {
|
|
|
81
111
|
checkCommand: finalConfig.checkCommand,
|
|
82
112
|
testCommand: finalConfig.testCommand,
|
|
83
113
|
imageName,
|
|
84
|
-
cli:
|
|
114
|
+
cli: cliConfig,
|
|
115
|
+
cliProvider: selectedCliProviderKey,
|
|
85
116
|
};
|
|
86
117
|
// Add technologies if any were selected
|
|
87
118
|
if (selectedTechnologies.length > 0) {
|
|
@@ -141,6 +172,7 @@ export async function init(args) {
|
|
|
141
172
|
console.log("\nNext steps:");
|
|
142
173
|
console.log(" 1. Read .ralph/HOW-TO-WRITE-PRDs.md for guidance on writing PRDs");
|
|
143
174
|
console.log(" 2. Edit .ralph/prd.json to add your project requirements");
|
|
144
|
-
console.log(" 3. Run 'ralph
|
|
145
|
-
console.log(" 4.
|
|
175
|
+
console.log(" 3. Run 'ralph docker init' to generate Docker configuration");
|
|
176
|
+
console.log(" 4. Run 'ralph docker build' to build the container");
|
|
177
|
+
console.log(" 5. Run 'ralph docker run' to start ralph in the container");
|
|
146
178
|
}
|
package/dist/commands/once.js
CHANGED
|
@@ -15,17 +15,20 @@ export async function once(_args) {
|
|
|
15
15
|
const paths = getPaths();
|
|
16
16
|
const cliConfig = getCliConfig(config);
|
|
17
17
|
console.log("Starting single ralph iteration...\n");
|
|
18
|
-
// Build CLI arguments: config args +
|
|
18
|
+
// Build CLI arguments: config args + yolo args + prompt args
|
|
19
|
+
// Use yoloArgs from config if available, otherwise default to Claude's --dangerously-skip-permissions
|
|
20
|
+
const yoloArgs = cliConfig.yoloArgs ?? ["--dangerously-skip-permissions"];
|
|
21
|
+
const promptArgs = cliConfig.promptArgs ?? ["-p"];
|
|
22
|
+
const promptValue = `@${paths.prd} @${paths.progress} ${prompt}`;
|
|
19
23
|
const cliArgs = [
|
|
20
24
|
...(cliConfig.args ?? []),
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
25
|
+
...yoloArgs,
|
|
26
|
+
...promptArgs,
|
|
27
|
+
promptValue,
|
|
24
28
|
];
|
|
25
29
|
return new Promise((resolve, reject) => {
|
|
26
30
|
const proc = spawn(cliConfig.command, cliArgs, {
|
|
27
31
|
stdio: "inherit",
|
|
28
|
-
shell: true,
|
|
29
32
|
});
|
|
30
33
|
proc.on("close", (code) => {
|
|
31
34
|
if (code !== 0) {
|
package/dist/commands/run.js
CHANGED
|
@@ -28,16 +28,21 @@ function createFilteredPrd(prdPath, category) {
|
|
|
28
28
|
async function runIteration(prompt, paths, sandboxed, filteredPrdPath, cliConfig) {
|
|
29
29
|
return new Promise((resolve, reject) => {
|
|
30
30
|
let output = "";
|
|
31
|
-
// Build CLI arguments: config args +
|
|
31
|
+
// Build CLI arguments: config args + yolo args + prompt args
|
|
32
32
|
const cliArgs = [
|
|
33
33
|
...(cliConfig.args ?? []),
|
|
34
34
|
];
|
|
35
|
-
// Only add
|
|
35
|
+
// Only add yolo args when running in a container
|
|
36
|
+
// Use yoloArgs from config if available, otherwise default to Claude's --dangerously-skip-permissions
|
|
36
37
|
if (sandboxed) {
|
|
37
|
-
|
|
38
|
+
const yoloArgs = cliConfig.yoloArgs ?? ["--dangerously-skip-permissions"];
|
|
39
|
+
cliArgs.push(...yoloArgs);
|
|
38
40
|
}
|
|
39
41
|
// Use the filtered PRD (only incomplete items) for the prompt
|
|
40
|
-
|
|
42
|
+
// promptArgs specifies flags to use (e.g., ["-p"] for Claude, [] for positional)
|
|
43
|
+
const promptArgs = cliConfig.promptArgs ?? ["-p"];
|
|
44
|
+
const promptValue = `@${filteredPrdPath} @${paths.progress} ${prompt}`;
|
|
45
|
+
cliArgs.push(...promptArgs, promptValue);
|
|
41
46
|
const proc = spawn(cliConfig.command, cliArgs, {
|
|
42
47
|
stdio: ["inherit", "pipe", "inherit"],
|
|
43
48
|
});
|
|
@@ -101,7 +106,7 @@ export async function run(args) {
|
|
|
101
106
|
// Parse flags
|
|
102
107
|
let category;
|
|
103
108
|
let loopMode = false;
|
|
104
|
-
let
|
|
109
|
+
let allModeExplicit = false;
|
|
105
110
|
const filteredArgs = [];
|
|
106
111
|
for (let i = 0; i < args.length; i++) {
|
|
107
112
|
if (args[i] === "--category" || args[i] === "-c") {
|
|
@@ -119,7 +124,7 @@ export async function run(args) {
|
|
|
119
124
|
loopMode = true;
|
|
120
125
|
}
|
|
121
126
|
else if (args[i] === "--all" || args[i] === "-a") {
|
|
122
|
-
|
|
127
|
+
allModeExplicit = true;
|
|
123
128
|
}
|
|
124
129
|
else {
|
|
125
130
|
filteredArgs.push(args[i]);
|
|
@@ -131,16 +136,14 @@ export async function run(args) {
|
|
|
131
136
|
console.error(`Valid categories: ${CATEGORIES.join(", ")}`);
|
|
132
137
|
process.exit(1);
|
|
133
138
|
}
|
|
139
|
+
// Determine the mode:
|
|
140
|
+
// - If --loop is specified, use loop mode
|
|
141
|
+
// - If a specific number of iterations is provided, use that
|
|
142
|
+
// - Otherwise, default to --all mode (run until all tasks complete)
|
|
143
|
+
const hasIterationArg = filteredArgs.length > 0 && !isNaN(parseInt(filteredArgs[0])) && parseInt(filteredArgs[0]) >= 1;
|
|
144
|
+
const allMode = !loopMode && (allModeExplicit || !hasIterationArg);
|
|
134
145
|
// In loop mode or all mode, iterations argument is optional (defaults to unlimited)
|
|
135
146
|
const iterations = (loopMode || allMode) ? (parseInt(filteredArgs[0]) || Infinity) : parseInt(filteredArgs[0]);
|
|
136
|
-
if (!loopMode && !allMode && (!iterations || iterations < 1 || isNaN(iterations))) {
|
|
137
|
-
console.error("Usage: ralph run <iterations> [--category <category>]");
|
|
138
|
-
console.error(" ralph run --loop [--category <category>]");
|
|
139
|
-
console.error(" ralph run --all [--category <category>]");
|
|
140
|
-
console.error(" <iterations> must be a positive integer");
|
|
141
|
-
console.error(` <category> must be one of: ${CATEGORIES.join(", ")}`);
|
|
142
|
-
process.exit(1);
|
|
143
|
-
}
|
|
144
147
|
requireContainer("run");
|
|
145
148
|
checkFilesExist();
|
|
146
149
|
const config = loadConfig();
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
{
|
|
2
|
+
"providers": {
|
|
3
|
+
"claude": {
|
|
4
|
+
"name": "Claude Code",
|
|
5
|
+
"description": "Anthropic's Claude Code CLI",
|
|
6
|
+
"command": "claude",
|
|
7
|
+
"defaultArgs": ["--permission-mode", "acceptEdits"],
|
|
8
|
+
"yoloArgs": ["--dangerously-skip-permissions"],
|
|
9
|
+
"promptArgs": ["-p"],
|
|
10
|
+
"docker": {
|
|
11
|
+
"install": "# Install Claude Code CLI (as node user so it installs to /home/node/.local/bin)\nRUN su - node -c 'curl -fsSL https://claude.ai/install.sh | bash' \\\n && echo 'export PATH=\"$HOME/.local/bin:$PATH\"' >> /home/node/.zshrc"
|
|
12
|
+
},
|
|
13
|
+
"envVars": ["ANTHROPIC_API_KEY"],
|
|
14
|
+
"credentialMount": "~/.claude:/home/node/.claude"
|
|
15
|
+
},
|
|
16
|
+
"aider": {
|
|
17
|
+
"name": "Aider",
|
|
18
|
+
"description": "AI pair programming in your terminal",
|
|
19
|
+
"command": "aider",
|
|
20
|
+
"defaultArgs": ["--yes"],
|
|
21
|
+
"yoloArgs": ["--yes-always"],
|
|
22
|
+
"promptArgs": ["--message"],
|
|
23
|
+
"docker": {
|
|
24
|
+
"install": "# Install Aider (requires Python)\nRUN apt-get update && apt-get install -y python3 python3-pip && rm -rf /var/lib/apt/lists/* \\\n && pip3 install --break-system-packages --no-cache-dir aider-chat",
|
|
25
|
+
"note": "Check 'aider --help' for available flags"
|
|
26
|
+
},
|
|
27
|
+
"envVars": ["OPENAI_API_KEY", "ANTHROPIC_API_KEY"],
|
|
28
|
+
"credentialMount": null
|
|
29
|
+
},
|
|
30
|
+
"codex": {
|
|
31
|
+
"name": "OpenAI Codex CLI",
|
|
32
|
+
"description": "OpenAI's Codex CLI for code generation",
|
|
33
|
+
"command": "codex",
|
|
34
|
+
"defaultArgs": ["--approval-mode", "suggest"],
|
|
35
|
+
"yoloArgs": ["--approval-mode", "full-auto"],
|
|
36
|
+
"promptArgs": [],
|
|
37
|
+
"docker": {
|
|
38
|
+
"install": "# Install OpenAI Codex CLI\nRUN npm install -g @openai/codex",
|
|
39
|
+
"note": "Check 'codex --help' for available flags"
|
|
40
|
+
},
|
|
41
|
+
"envVars": ["OPENAI_API_KEY"],
|
|
42
|
+
"credentialMount": null
|
|
43
|
+
},
|
|
44
|
+
"gemini": {
|
|
45
|
+
"name": "Gemini CLI",
|
|
46
|
+
"description": "Google's Gemini CLI for code assistance",
|
|
47
|
+
"command": "gemini",
|
|
48
|
+
"defaultArgs": [],
|
|
49
|
+
"yoloArgs": ["-y"],
|
|
50
|
+
"promptArgs": [],
|
|
51
|
+
"docker": {
|
|
52
|
+
"install": "# Install Google Gemini CLI\nRUN npm install -g @google/gemini-cli",
|
|
53
|
+
"note": "Check 'gemini --help' for available flags"
|
|
54
|
+
},
|
|
55
|
+
"envVars": ["GEMINI_API_KEY", "GOOGLE_API_KEY"],
|
|
56
|
+
"credentialMount": "~/.gemini:/home/node/.gemini"
|
|
57
|
+
},
|
|
58
|
+
"opencode": {
|
|
59
|
+
"name": "OpenCode",
|
|
60
|
+
"description": "Open source AI coding agent for the terminal",
|
|
61
|
+
"command": "opencode",
|
|
62
|
+
"defaultArgs": [],
|
|
63
|
+
"yoloArgs": ["--yolo"],
|
|
64
|
+
"promptArgs": [],
|
|
65
|
+
"docker": {
|
|
66
|
+
"install": "# Install OpenCode (as node user)\nRUN su - node -c 'curl -fsSL https://opencode.ai/install | bash' \\\n && echo 'export PATH=\"$HOME/.opencode/bin:$PATH\"' >> /home/node/.zshrc",
|
|
67
|
+
"note": "Check 'opencode --help' for available flags"
|
|
68
|
+
},
|
|
69
|
+
"envVars": ["ANTHROPIC_API_KEY", "OPENAI_API_KEY", "GOOGLE_API_KEY"],
|
|
70
|
+
"credentialMount": null
|
|
71
|
+
},
|
|
72
|
+
"amp": {
|
|
73
|
+
"name": "AMP CLI",
|
|
74
|
+
"description": "Sourcegraph's AMP coding agent",
|
|
75
|
+
"command": "amp",
|
|
76
|
+
"defaultArgs": [],
|
|
77
|
+
"yoloArgs": ["--yolo"],
|
|
78
|
+
"promptArgs": [],
|
|
79
|
+
"docker": {
|
|
80
|
+
"install": "# Install AMP CLI (as node user)\nRUN su - node -c 'curl -fsSL https://ampcode.com/install.sh | bash' \\\n && echo 'export PATH=\"$HOME/.amp/bin:$PATH\"' >> /home/node/.zshrc",
|
|
81
|
+
"note": "Check 'amp --help' for available flags"
|
|
82
|
+
},
|
|
83
|
+
"envVars": ["ANTHROPIC_API_KEY", "OPENAI_API_KEY"],
|
|
84
|
+
"credentialMount": null
|
|
85
|
+
},
|
|
86
|
+
"custom": {
|
|
87
|
+
"name": "Custom CLI",
|
|
88
|
+
"description": "Configure your own AI CLI tool",
|
|
89
|
+
"command": "",
|
|
90
|
+
"defaultArgs": [],
|
|
91
|
+
"yoloArgs": [],
|
|
92
|
+
"promptArgs": [],
|
|
93
|
+
"docker": {
|
|
94
|
+
"install": "# Custom CLI - add your installation commands here"
|
|
95
|
+
},
|
|
96
|
+
"envVars": [],
|
|
97
|
+
"credentialMount": null
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -27,7 +27,25 @@ export interface LanguageConfig {
|
|
|
27
27
|
interface LanguagesJson {
|
|
28
28
|
languages: Record<string, LanguageConfigJson>;
|
|
29
29
|
}
|
|
30
|
+
export interface CliProviderConfig {
|
|
31
|
+
name: string;
|
|
32
|
+
description: string;
|
|
33
|
+
command: string;
|
|
34
|
+
defaultArgs: string[];
|
|
35
|
+
yoloArgs: string[];
|
|
36
|
+
promptArgs: string[];
|
|
37
|
+
docker: {
|
|
38
|
+
install: string;
|
|
39
|
+
};
|
|
40
|
+
envVars: string[];
|
|
41
|
+
credentialMount: string | null;
|
|
42
|
+
}
|
|
43
|
+
interface CliProvidersJson {
|
|
44
|
+
providers: Record<string, CliProviderConfig>;
|
|
45
|
+
}
|
|
30
46
|
export declare function getLanguagesJson(): LanguagesJson;
|
|
47
|
+
export declare function getCliProvidersJson(): CliProvidersJson;
|
|
48
|
+
export declare function getCliProviders(): Record<string, CliProviderConfig>;
|
|
31
49
|
export declare function getLanguages(): Record<string, LanguageConfig>;
|
|
32
50
|
export declare const LANGUAGES: Record<string, LanguageConfig>;
|
|
33
51
|
export declare function generatePromptTemplate(): string;
|
|
@@ -11,6 +11,12 @@ function loadLanguagesConfig() {
|
|
|
11
11
|
const content = readFileSync(configPath, "utf-8");
|
|
12
12
|
return JSON.parse(content);
|
|
13
13
|
}
|
|
14
|
+
// Load CLI providers from JSON config file
|
|
15
|
+
function loadCliProvidersConfig() {
|
|
16
|
+
const configPath = join(__dirname, "..", "config", "cli-providers.json");
|
|
17
|
+
const content = readFileSync(configPath, "utf-8");
|
|
18
|
+
return JSON.parse(content);
|
|
19
|
+
}
|
|
14
20
|
// Convert JSON config to the legacy format for compatibility
|
|
15
21
|
function convertToLanguageConfig(config) {
|
|
16
22
|
return {
|
|
@@ -24,12 +30,22 @@ function convertToLanguageConfig(config) {
|
|
|
24
30
|
// Lazy-load languages to avoid issues at import time
|
|
25
31
|
let _languagesCache = null;
|
|
26
32
|
let _languagesJsonCache = null;
|
|
33
|
+
let _cliProvidersCache = null;
|
|
27
34
|
export function getLanguagesJson() {
|
|
28
35
|
if (!_languagesJsonCache) {
|
|
29
36
|
_languagesJsonCache = loadLanguagesConfig();
|
|
30
37
|
}
|
|
31
38
|
return _languagesJsonCache;
|
|
32
39
|
}
|
|
40
|
+
export function getCliProvidersJson() {
|
|
41
|
+
if (!_cliProvidersCache) {
|
|
42
|
+
_cliProvidersCache = loadCliProvidersConfig();
|
|
43
|
+
}
|
|
44
|
+
return _cliProvidersCache;
|
|
45
|
+
}
|
|
46
|
+
export function getCliProviders() {
|
|
47
|
+
return getCliProvidersJson().providers;
|
|
48
|
+
}
|
|
33
49
|
export function getLanguages() {
|
|
34
50
|
if (!_languagesCache) {
|
|
35
51
|
const json = getLanguagesJson();
|
package/dist/utils/config.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export interface CliConfig {
|
|
2
2
|
command: string;
|
|
3
3
|
args?: string[];
|
|
4
|
+
yoloArgs?: string[];
|
|
5
|
+
promptArgs?: string[];
|
|
4
6
|
}
|
|
5
7
|
export interface RalphConfig {
|
|
6
8
|
language: string;
|
|
@@ -11,6 +13,7 @@ export interface RalphConfig {
|
|
|
11
13
|
technologies?: string[];
|
|
12
14
|
javaVersion?: number;
|
|
13
15
|
cli?: CliConfig;
|
|
16
|
+
cliProvider?: string;
|
|
14
17
|
}
|
|
15
18
|
export declare const DEFAULT_CLI_CONFIG: CliConfig;
|
|
16
19
|
export declare function getCliConfig(config: RalphConfig): CliConfig;
|
package/dist/utils/config.js
CHANGED
|
@@ -3,9 +3,30 @@ import { join } from "path";
|
|
|
3
3
|
export const DEFAULT_CLI_CONFIG = {
|
|
4
4
|
command: "claude",
|
|
5
5
|
args: ["--permission-mode", "acceptEdits"],
|
|
6
|
+
promptArgs: ["-p"],
|
|
6
7
|
};
|
|
8
|
+
// Lazy import to avoid circular dependency
|
|
9
|
+
let _getCliProviders = null;
|
|
7
10
|
export function getCliConfig(config) {
|
|
8
|
-
|
|
11
|
+
const cliConfig = config.cli ?? DEFAULT_CLI_CONFIG;
|
|
12
|
+
// If promptArgs is already set, use it
|
|
13
|
+
if (cliConfig.promptArgs !== undefined) {
|
|
14
|
+
return cliConfig;
|
|
15
|
+
}
|
|
16
|
+
// Look up promptArgs from cliProvider if available
|
|
17
|
+
if (config.cliProvider) {
|
|
18
|
+
if (!_getCliProviders) {
|
|
19
|
+
// Dynamic import to avoid circular dependency
|
|
20
|
+
_getCliProviders = require("../templates/prompts.js").getCliProviders;
|
|
21
|
+
}
|
|
22
|
+
const providers = _getCliProviders();
|
|
23
|
+
const provider = providers[config.cliProvider];
|
|
24
|
+
if (provider?.promptArgs !== undefined) {
|
|
25
|
+
return { ...cliConfig, promptArgs: provider.promptArgs };
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
// Default to -p for backwards compatibility
|
|
29
|
+
return { ...cliConfig, promptArgs: ["-p"] };
|
|
9
30
|
}
|
|
10
31
|
const RALPH_DIR = ".ralph";
|
|
11
32
|
const CONFIG_FILE = "config.json";
|
|
@@ -91,8 +112,9 @@ export function requireContainer(commandName) {
|
|
|
91
112
|
console.error("For security, ralph executes AI agents only in isolated container environments.");
|
|
92
113
|
console.error("");
|
|
93
114
|
console.error("To set up a container:");
|
|
94
|
-
console.error(" ralph docker
|
|
95
|
-
console.error(" ralph docker
|
|
115
|
+
console.error(" ralph docker init # Generate Docker configuration files");
|
|
116
|
+
console.error(" ralph docker build # Build the container image");
|
|
117
|
+
console.error(" ralph docker run # Run ralph inside the container");
|
|
96
118
|
process.exit(1);
|
|
97
119
|
}
|
|
98
120
|
}
|
package/dist/utils/prompt.d.ts
CHANGED
|
@@ -2,7 +2,9 @@ export declare function createPrompt(): {
|
|
|
2
2
|
question: (query: string) => Promise<string>;
|
|
3
3
|
close: () => void;
|
|
4
4
|
};
|
|
5
|
+
export declare function promptSelectWithArrows(message: string, options: string[]): Promise<string>;
|
|
5
6
|
export declare function promptInput(message: string): Promise<string>;
|
|
6
7
|
export declare function promptSelect(message: string, options: string[]): Promise<string>;
|
|
7
8
|
export declare function promptConfirm(message: string): Promise<boolean>;
|
|
8
9
|
export declare function promptMultiSelect(message: string, options: string[]): Promise<string[]>;
|
|
10
|
+
export declare function promptMultiSelectWithArrows(message: string, options: string[]): Promise<string[]>;
|
package/dist/utils/prompt.js
CHANGED
|
@@ -13,6 +13,67 @@ export function createPrompt() {
|
|
|
13
13
|
close: () => rl.close(),
|
|
14
14
|
};
|
|
15
15
|
}
|
|
16
|
+
export async function promptSelectWithArrows(message, options) {
|
|
17
|
+
return new Promise((resolve) => {
|
|
18
|
+
let selectedIndex = 0;
|
|
19
|
+
// Hide cursor and enable raw mode
|
|
20
|
+
process.stdout.write("\x1B[?25l"); // Hide cursor
|
|
21
|
+
const render = () => {
|
|
22
|
+
// Move cursor up to clear previous render (except first time)
|
|
23
|
+
if (selectedIndex >= 0) {
|
|
24
|
+
process.stdout.write(`\x1B[${options.length}A`); // Move up
|
|
25
|
+
}
|
|
26
|
+
options.forEach((opt, i) => {
|
|
27
|
+
const prefix = i === selectedIndex ? "\x1B[36m❯\x1B[0m" : " ";
|
|
28
|
+
const text = i === selectedIndex ? `\x1B[36m${opt}\x1B[0m` : opt;
|
|
29
|
+
process.stdout.write(`\x1B[2K${prefix} ${text}\n`); // Clear line and write
|
|
30
|
+
});
|
|
31
|
+
};
|
|
32
|
+
const initialRender = () => {
|
|
33
|
+
console.log(`\n${message}\n`);
|
|
34
|
+
options.forEach((opt, i) => {
|
|
35
|
+
const prefix = i === selectedIndex ? "\x1B[36m❯\x1B[0m" : " ";
|
|
36
|
+
const text = i === selectedIndex ? `\x1B[36m${opt}\x1B[0m` : opt;
|
|
37
|
+
console.log(`${prefix} ${text}`);
|
|
38
|
+
});
|
|
39
|
+
};
|
|
40
|
+
initialRender();
|
|
41
|
+
// Set up raw mode for key input
|
|
42
|
+
if (process.stdin.isTTY) {
|
|
43
|
+
process.stdin.setRawMode(true);
|
|
44
|
+
}
|
|
45
|
+
process.stdin.resume();
|
|
46
|
+
process.stdin.setEncoding("utf8");
|
|
47
|
+
const onKeypress = (key) => {
|
|
48
|
+
// Handle arrow keys (escape sequences)
|
|
49
|
+
if (key === "\x1B[A" || key === "k") { // Up arrow or k
|
|
50
|
+
selectedIndex = (selectedIndex - 1 + options.length) % options.length;
|
|
51
|
+
render();
|
|
52
|
+
}
|
|
53
|
+
else if (key === "\x1B[B" || key === "j") { // Down arrow or j
|
|
54
|
+
selectedIndex = (selectedIndex + 1) % options.length;
|
|
55
|
+
render();
|
|
56
|
+
}
|
|
57
|
+
else if (key === "\r" || key === "\n" || key === " ") { // Enter or space
|
|
58
|
+
cleanup();
|
|
59
|
+
resolve(options[selectedIndex]);
|
|
60
|
+
}
|
|
61
|
+
else if (key === "\x03") { // Ctrl+C
|
|
62
|
+
cleanup();
|
|
63
|
+
process.exit(0);
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
const cleanup = () => {
|
|
67
|
+
process.stdin.removeListener("data", onKeypress);
|
|
68
|
+
if (process.stdin.isTTY) {
|
|
69
|
+
process.stdin.setRawMode(false);
|
|
70
|
+
}
|
|
71
|
+
process.stdin.pause();
|
|
72
|
+
process.stdout.write("\x1B[?25h"); // Show cursor
|
|
73
|
+
};
|
|
74
|
+
process.stdin.on("data", onKeypress);
|
|
75
|
+
});
|
|
76
|
+
}
|
|
16
77
|
export async function promptInput(message) {
|
|
17
78
|
const prompt = createPrompt();
|
|
18
79
|
const answer = await prompt.question(message);
|
|
@@ -99,3 +160,80 @@ export async function promptMultiSelect(message, options) {
|
|
|
99
160
|
}
|
|
100
161
|
}
|
|
101
162
|
}
|
|
163
|
+
export async function promptMultiSelectWithArrows(message, options) {
|
|
164
|
+
return new Promise((resolve) => {
|
|
165
|
+
let selectedIndex = 0;
|
|
166
|
+
const selected = new Set();
|
|
167
|
+
// Add a "Done" option at the end
|
|
168
|
+
const allOptions = [...options, "[Done - press Enter]"];
|
|
169
|
+
// Hide cursor
|
|
170
|
+
process.stdout.write("\x1B[?25l");
|
|
171
|
+
const render = () => {
|
|
172
|
+
process.stdout.write(`\x1B[${allOptions.length}A`); // Move up
|
|
173
|
+
allOptions.forEach((opt, i) => {
|
|
174
|
+
const isLastOption = i === allOptions.length - 1;
|
|
175
|
+
const cursor = i === selectedIndex ? "\x1B[36m❯\x1B[0m" : " ";
|
|
176
|
+
const checkbox = isLastOption ? "" : (selected.has(i) ? "\x1B[32m[x]\x1B[0m" : "[ ]");
|
|
177
|
+
const text = i === selectedIndex ? `\x1B[36m${opt}\x1B[0m` : opt;
|
|
178
|
+
process.stdout.write(`\x1B[2K${cursor} ${checkbox} ${text}\n`);
|
|
179
|
+
});
|
|
180
|
+
};
|
|
181
|
+
const initialRender = () => {
|
|
182
|
+
console.log(`\n${message}`);
|
|
183
|
+
console.log("(Use arrow keys to navigate, Space to select, Enter to confirm)\n");
|
|
184
|
+
allOptions.forEach((opt, i) => {
|
|
185
|
+
const isLastOption = i === allOptions.length - 1;
|
|
186
|
+
const cursor = i === selectedIndex ? "\x1B[36m❯\x1B[0m" : " ";
|
|
187
|
+
const checkbox = isLastOption ? "" : (selected.has(i) ? "\x1B[32m[x]\x1B[0m" : "[ ]");
|
|
188
|
+
const text = i === selectedIndex ? `\x1B[36m${opt}\x1B[0m` : opt;
|
|
189
|
+
console.log(`${cursor} ${checkbox} ${text}`);
|
|
190
|
+
});
|
|
191
|
+
};
|
|
192
|
+
initialRender();
|
|
193
|
+
if (process.stdin.isTTY) {
|
|
194
|
+
process.stdin.setRawMode(true);
|
|
195
|
+
}
|
|
196
|
+
process.stdin.resume();
|
|
197
|
+
process.stdin.setEncoding("utf8");
|
|
198
|
+
const onKeypress = (key) => {
|
|
199
|
+
if (key === "\x1B[A" || key === "k") { // Up
|
|
200
|
+
selectedIndex = (selectedIndex - 1 + allOptions.length) % allOptions.length;
|
|
201
|
+
render();
|
|
202
|
+
}
|
|
203
|
+
else if (key === "\x1B[B" || key === "j") { // Down
|
|
204
|
+
selectedIndex = (selectedIndex + 1) % allOptions.length;
|
|
205
|
+
render();
|
|
206
|
+
}
|
|
207
|
+
else if (key === " ") { // Space - toggle selection
|
|
208
|
+
const isLastOption = selectedIndex === allOptions.length - 1;
|
|
209
|
+
if (!isLastOption) {
|
|
210
|
+
if (selected.has(selectedIndex)) {
|
|
211
|
+
selected.delete(selectedIndex);
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
selected.add(selectedIndex);
|
|
215
|
+
}
|
|
216
|
+
render();
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
else if (key === "\r" || key === "\n") { // Enter - confirm
|
|
220
|
+
cleanup();
|
|
221
|
+
const result = options.filter((_, i) => selected.has(i));
|
|
222
|
+
resolve(result);
|
|
223
|
+
}
|
|
224
|
+
else if (key === "\x03") { // Ctrl+C
|
|
225
|
+
cleanup();
|
|
226
|
+
process.exit(0);
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
const cleanup = () => {
|
|
230
|
+
process.stdin.removeListener("data", onKeypress);
|
|
231
|
+
if (process.stdin.isTTY) {
|
|
232
|
+
process.stdin.setRawMode(false);
|
|
233
|
+
}
|
|
234
|
+
process.stdin.pause();
|
|
235
|
+
process.stdout.write("\x1B[?25h"); // Show cursor
|
|
236
|
+
};
|
|
237
|
+
process.stdin.on("data", onKeypress);
|
|
238
|
+
});
|
|
239
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ralph-cli-sandboxed",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "AI-driven development automation CLI for Claude Code",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -13,11 +13,12 @@
|
|
|
13
13
|
"README.md"
|
|
14
14
|
],
|
|
15
15
|
"scripts": {
|
|
16
|
-
"build": "tsc && npm run copy-config",
|
|
16
|
+
"build": "./node_modules/.bin/tsc && npm run copy-config",
|
|
17
17
|
"copy-config": "mkdir -p dist/config && cp src/config/*.json dist/config/",
|
|
18
18
|
"dev": "npx tsx src/index.ts",
|
|
19
|
-
"typecheck": "tsc --noEmit",
|
|
20
|
-
"prepublishOnly": "npm run build"
|
|
19
|
+
"typecheck": "./node_modules/.bin/tsc --noEmit",
|
|
20
|
+
"prepublishOnly": "npm run build",
|
|
21
|
+
"prepare": "npm run build"
|
|
21
22
|
},
|
|
22
23
|
"dependencies": {
|
|
23
24
|
"readline": "^1.3.0"
|