ralph-cli-sandboxed 0.1.5 → 0.2.0

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
@@ -29,8 +29,8 @@ ralph add
29
29
  # 3. Run a single iteration
30
30
  ralph once
31
31
 
32
- # 4. Or run multiple iterations
33
- ralph run 5
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 <n>` | Run n automation iterations |
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` | Generate Docker sandbox environment |
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 --build
122
+ ralph docker build
123
123
 
124
124
  # Run container
125
- ralph docker --run
125
+ ralph docker run
126
126
  ```
127
127
 
128
128
  Features:
@@ -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
- function generateDockerfile(language, javaVersion) {
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\nRUN curl -fsSL https://claude.ai/install.sh | bash";
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
- # Install Claude Code CLI
96
- RUN npm install -g @anthropic-ai/claude-code@\${CLAUDE_CODE_VERSION}
106
+ ${cliSnippet}
97
107
 
98
- # Install ralph-cli-claude from npm registry
99
- RUN npm install -g ralph-cli-claude
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,9 +290,9 @@ 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 (fetching latest Claude Code)...\n");
293
+ console.log("Building Docker image...\n");
284
294
  return new Promise((resolve, reject) => {
285
- // Use --no-cache and --pull to ensure we always get the latest Claude Code version
295
+ // Use --no-cache and --pull to ensure we always get the latest CLI versions
286
296
  const proc = spawn("docker", ["compose", "build", "--no-cache", "--pull"], {
287
297
  cwd: dockerDir,
288
298
  stdio: "inherit",
@@ -319,7 +329,7 @@ async function imageExists(imageName) {
319
329
  });
320
330
  });
321
331
  }
322
- async function runContainer(ralphDir, imageName, language, javaVersion) {
332
+ async function runContainer(ralphDir, imageName, language, javaVersion, cliProvider) {
323
333
  const dockerDir = join(ralphDir, DOCKER_DIR);
324
334
  const dockerfileExists = existsSync(join(dockerDir, "Dockerfile"));
325
335
  const hasImage = await imageExists(imageName);
@@ -327,7 +337,7 @@ async function runContainer(ralphDir, imageName, language, javaVersion) {
327
337
  if (!dockerfileExists || !hasImage) {
328
338
  if (!dockerfileExists) {
329
339
  console.log("Docker folder not found. Initializing docker setup...\n");
330
- await generateFiles(ralphDir, language, imageName, true, javaVersion);
340
+ await generateFiles(ralphDir, language, imageName, true, javaVersion, cliProvider);
331
341
  console.log("");
332
342
  }
333
343
  if (!hasImage) {
@@ -514,23 +524,24 @@ async function cleanImage(imageName, ralphDir) {
514
524
  });
515
525
  });
516
526
  console.log("\nDocker image and associated resources cleaned.");
517
- console.log("Run 'ralph docker --build' to rebuild the image.");
527
+ console.log("Run 'ralph docker build' to rebuild the image.");
518
528
  }
519
529
  export async function docker(args) {
520
- const hasFlag = (flag) => args.includes(flag);
521
- const flag = args[0];
530
+ const subcommand = args[0];
531
+ const subArgs = args.slice(1);
522
532
  // Show help without requiring init
523
- if (flag === "--help" || flag === "-h") {
533
+ if (subcommand === "help" || subcommand === "--help" || subcommand === "-h") {
524
534
  console.log(`
525
535
  ralph docker - Generate and manage Docker sandbox environment
526
536
 
527
537
  USAGE:
528
- ralph docker Generate Dockerfile and scripts
529
- ralph docker -y Generate files, overwrite without prompting
530
- ralph docker --build Build image (always fetches latest Claude Code)
531
- ralph docker --build --clean Clean existing image and rebuild from scratch
532
- ralph docker --run Run container (auto-init and build if needed)
533
- ralph docker --clean Remove Docker image and associated resources
538
+ ralph docker init Generate Dockerfile and scripts
539
+ ralph docker init -y Generate files, overwrite without prompting
540
+ ralph docker build Build image (fetches latest CLI versions)
541
+ ralph docker build --clean Clean existing image and rebuild from scratch
542
+ ralph docker run Run container (auto-init and build if needed)
543
+ ralph docker clean Remove Docker image and associated resources
544
+ ralph docker help Show this help message
534
545
 
535
546
  FILES GENERATED:
536
547
  .ralph/docker/
@@ -544,11 +555,11 @@ AUTHENTICATION:
544
555
  API key users: Uncomment ANTHROPIC_API_KEY in docker-compose.yml.
545
556
 
546
557
  EXAMPLES:
547
- ralph docker # Generate files
548
- ralph docker --build # Build image
549
- ralph docker --build --clean # Clean and rebuild from scratch
550
- ralph docker --run # Start interactive shell
551
- ralph docker --clean # Remove image and volumes
558
+ ralph docker init # Generate files
559
+ ralph docker build # Build image
560
+ ralph docker build --clean # Clean and rebuild from scratch
561
+ ralph docker run # Start interactive shell
562
+ ralph docker clean # Remove image and volumes
552
563
 
553
564
  # Or use docker compose directly:
554
565
  cd .ralph/docker && docker compose run --rm ralph
@@ -578,38 +589,48 @@ INSTALLING PACKAGES (works with Docker & Podman):
578
589
  const config = loadConfig();
579
590
  // Get image name from config or generate default
580
591
  const imageName = config.imageName || `ralph-${basename(process.cwd()).toLowerCase().replace(/[^a-z0-9-]/g, "-")}`;
581
- // Handle --build --clean combination: clean first, then build
582
- if (hasFlag("--build") && hasFlag("--clean")) {
583
- await cleanImage(imageName, ralphDir);
584
- console.log(""); // Add spacing between clean and build output
585
- await buildImage(ralphDir);
586
- }
587
- else if (hasFlag("--build")) {
588
- await buildImage(ralphDir);
589
- }
590
- else if (hasFlag("--run")) {
591
- await runContainer(ralphDir, imageName, config.language, config.javaVersion);
592
- }
593
- else if (hasFlag("--clean")) {
594
- await cleanImage(imageName, ralphDir);
595
- }
596
- else {
597
- const force = flag === "-y" || flag === "--yes";
598
- console.log(`Generating Docker files for: ${config.language}`);
599
- if ((config.language === "java" || config.language === "kotlin") && config.javaVersion) {
600
- console.log(`Java version: ${config.javaVersion}`);
601
- }
602
- console.log(`Image name: ${imageName}\n`);
603
- await generateFiles(ralphDir, config.language, imageName, force, config.javaVersion);
604
- console.log(`
592
+ const hasFlag = (flag) => subArgs.includes(flag);
593
+ switch (subcommand) {
594
+ case "build":
595
+ // Handle build --clean combination: clean first, then build
596
+ if (hasFlag("--clean")) {
597
+ await cleanImage(imageName, ralphDir);
598
+ console.log(""); // Add spacing between clean and build output
599
+ }
600
+ await buildImage(ralphDir);
601
+ break;
602
+ case "run":
603
+ await runContainer(ralphDir, imageName, config.language, config.javaVersion, config.cliProvider);
604
+ break;
605
+ case "clean":
606
+ await cleanImage(imageName, ralphDir);
607
+ break;
608
+ case "init":
609
+ default: {
610
+ // Default to init if no subcommand or unrecognized subcommand
611
+ const force = subcommand === "init"
612
+ ? (subArgs[0] === "-y" || subArgs[0] === "--yes")
613
+ : (subcommand === "-y" || subcommand === "--yes");
614
+ console.log(`Generating Docker files for: ${config.language}`);
615
+ if ((config.language === "java" || config.language === "kotlin") && config.javaVersion) {
616
+ console.log(`Java version: ${config.javaVersion}`);
617
+ }
618
+ if (config.cliProvider && config.cliProvider !== "claude") {
619
+ console.log(`CLI provider: ${config.cliProvider}`);
620
+ }
621
+ console.log(`Image name: ${imageName}\n`);
622
+ await generateFiles(ralphDir, config.language, imageName, force, config.javaVersion, config.cliProvider);
623
+ console.log(`
605
624
  Docker files generated in .ralph/docker/
606
625
 
607
626
  Next steps:
608
- 1. Build the image: ralph docker --build
609
- 2. Run container: ralph docker --run
627
+ 1. Build the image: ralph docker build
628
+ 2. Run container: ralph docker run
610
629
 
611
630
  Or use docker compose directly:
612
631
  cd .ralph/docker && docker compose run --rm ralph
613
632
  `);
633
+ break;
634
+ }
614
635
  }
615
636
  }
@@ -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 <n> [opts] Run n automation iterations
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 Generate Docker sandbox environment
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 OPTIONS:
23
- --tech-stack, -t Enable technology stack selection prompt
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
- --all, -a Run until all tasks are complete, showing progress
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 only)
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 5 # Run 5 iterations
46
- ralph run --all # Run until all tasks complete (shows progress)
47
- ralph run --all -c feature # Complete all feature tasks only
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 # Generate Dockerfile for sandboxed env
61
- ralph docker --build # Build Docker image
62
- ralph docker --run # Run container (auto-init/build if needed)
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 can be configured in .ralph/config.json:
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
- Default uses Claude Code. Customize 'command' and 'args' for other AI CLIs.
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());
@@ -1 +1 @@
1
- export declare function init(args: string[]): Promise<void>;
1
+ export declare function init(_args: string[]): Promise<void>;
@@ -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 { promptSelect, promptConfirm, promptInput, promptMultiSelect } from "../utils/prompt.js";
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 hasFlag(args, ...flags) {
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,51 @@ export async function init(args) {
34
29
  mkdirSync(ralphDir, { recursive: true });
35
30
  console.log(`Created ${RALPH_DIR}/`);
36
31
  }
37
- // Select language
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
+ cliConfig = {
49
+ command: customCommand || "claude",
50
+ args: customArgs,
51
+ yoloArgs: customYoloArgs.length > 0 ? customYoloArgs : undefined,
52
+ };
53
+ }
54
+ else {
55
+ cliConfig = {
56
+ command: selectedProvider.command,
57
+ args: selectedProvider.defaultArgs,
58
+ yoloArgs: selectedProvider.yoloArgs.length > 0 ? selectedProvider.yoloArgs : undefined,
59
+ };
60
+ }
61
+ console.log(`\nSelected CLI provider: ${CLI_PROVIDERS[selectedCliProviderKey].name}`);
62
+ // Step 2: Select language (second)
38
63
  const LANGUAGES = getLanguages();
39
64
  const languageKeys = Object.keys(LANGUAGES);
40
65
  const languageNames = languageKeys.map(k => `${LANGUAGES[k].name} - ${LANGUAGES[k].description}`);
41
- const selectedName = await promptSelect("Select your project language/runtime:", languageNames);
66
+ const selectedName = await promptSelectWithArrows("Select your project language/runtime:", languageNames);
42
67
  const selectedIndex = languageNames.indexOf(selectedName);
43
68
  const selectedKey = languageKeys[selectedIndex];
44
69
  const config = LANGUAGES[selectedKey];
45
- // Select technology stack if available (only when --tech-stack flag is provided)
70
+ console.log(`\nSelected language: ${config.name}`);
71
+ // Step 3: Select technology stack if available (third)
46
72
  let selectedTechnologies = [];
47
- if (showTechStack && config.technologies && config.technologies.length > 0) {
73
+ if (config.technologies && config.technologies.length > 0) {
48
74
  const techOptions = config.technologies.map(t => `${t.name} - ${t.description}`);
49
75
  const techNames = config.technologies.map(t => t.name);
50
- selectedTechnologies = await promptMultiSelect("Select your technology stack (select multiple or add custom):", techOptions);
76
+ selectedTechnologies = await promptMultiSelectWithArrows("Select your technology stack (optional):", techOptions);
51
77
  // Convert display names back to just technology names for predefined options
52
78
  selectedTechnologies = selectedTechnologies.map(sel => {
53
79
  const idx = techOptions.indexOf(sel);
@@ -60,7 +86,7 @@ export async function init(args) {
60
86
  console.log("\nNo technologies selected.");
61
87
  }
62
88
  }
63
- // Allow custom commands
89
+ // Allow custom commands for "none" language
64
90
  let checkCommand = config.checkCommand;
65
91
  let testCommand = config.testCommand;
66
92
  if (selectedKey === "none") {
@@ -81,7 +107,8 @@ export async function init(args) {
81
107
  checkCommand: finalConfig.checkCommand,
82
108
  testCommand: finalConfig.testCommand,
83
109
  imageName,
84
- cli: DEFAULT_CLI_CONFIG,
110
+ cli: cliConfig,
111
+ cliProvider: selectedCliProviderKey,
85
112
  };
86
113
  // Add technologies if any were selected
87
114
  if (selectedTechnologies.length > 0) {
@@ -141,6 +168,7 @@ export async function init(args) {
141
168
  console.log("\nNext steps:");
142
169
  console.log(" 1. Read .ralph/HOW-TO-WRITE-PRDs.md for guidance on writing PRDs");
143
170
  console.log(" 2. Edit .ralph/prd.json to add your project requirements");
144
- console.log(" 3. Run 'ralph once' to start the first iteration");
145
- console.log(" 4. Or run 'ralph run 5' for 5 automated iterations");
171
+ console.log(" 3. Run 'ralph docker init' to generate Docker configuration");
172
+ console.log(" 4. Run 'ralph docker build' to build the container");
173
+ console.log(" 5. Run 'ralph docker run' to start ralph in the container");
146
174
  }
@@ -15,10 +15,12 @@ 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 + runtime 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"];
19
21
  const cliArgs = [
20
22
  ...(cliConfig.args ?? []),
21
- "--dangerously-skip-permissions",
23
+ ...yoloArgs,
22
24
  "-p",
23
25
  `@${paths.prd} @${paths.progress} ${prompt}`,
24
26
  ];
@@ -28,13 +28,15 @@ 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 + runtime args
31
+ // Build CLI arguments: config args + yolo args + prompt args
32
32
  const cliArgs = [
33
33
  ...(cliConfig.args ?? []),
34
34
  ];
35
- // Only add --dangerously-skip-permissions when running in a container
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
- cliArgs.push("--dangerously-skip-permissions");
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
  cliArgs.push("-p", `@${filteredPrdPath} @${paths.progress} ${prompt}`);
@@ -101,7 +103,7 @@ export async function run(args) {
101
103
  // Parse flags
102
104
  let category;
103
105
  let loopMode = false;
104
- let allMode = false;
106
+ let allModeExplicit = false;
105
107
  const filteredArgs = [];
106
108
  for (let i = 0; i < args.length; i++) {
107
109
  if (args[i] === "--category" || args[i] === "-c") {
@@ -119,7 +121,7 @@ export async function run(args) {
119
121
  loopMode = true;
120
122
  }
121
123
  else if (args[i] === "--all" || args[i] === "-a") {
122
- allMode = true;
124
+ allModeExplicit = true;
123
125
  }
124
126
  else {
125
127
  filteredArgs.push(args[i]);
@@ -131,16 +133,14 @@ export async function run(args) {
131
133
  console.error(`Valid categories: ${CATEGORIES.join(", ")}`);
132
134
  process.exit(1);
133
135
  }
136
+ // Determine the mode:
137
+ // - If --loop is specified, use loop mode
138
+ // - If a specific number of iterations is provided, use that
139
+ // - Otherwise, default to --all mode (run until all tasks complete)
140
+ const hasIterationArg = filteredArgs.length > 0 && !isNaN(parseInt(filteredArgs[0])) && parseInt(filteredArgs[0]) >= 1;
141
+ const allMode = !loopMode && (allModeExplicit || !hasIterationArg);
134
142
  // In loop mode or all mode, iterations argument is optional (defaults to unlimited)
135
143
  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
144
  requireContainer("run");
145
145
  checkFilesExist();
146
146
  const config = loadConfig();
@@ -0,0 +1,93 @@
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
+ "docker": {
10
+ "install": "# Install Claude Code CLI\nRUN curl -fsSL https://claude.ai/install.sh | bash"
11
+ },
12
+ "envVars": ["ANTHROPIC_API_KEY"],
13
+ "credentialMount": "~/.claude:/home/node/.claude"
14
+ },
15
+ "aider": {
16
+ "name": "Aider",
17
+ "description": "AI pair programming in your terminal",
18
+ "command": "aider",
19
+ "defaultArgs": ["--yes"],
20
+ "yoloArgs": ["--yes-always"],
21
+ "docker": {
22
+ "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",
23
+ "note": "Check 'aider --help' for available flags"
24
+ },
25
+ "envVars": ["OPENAI_API_KEY", "ANTHROPIC_API_KEY"],
26
+ "credentialMount": null
27
+ },
28
+ "codex": {
29
+ "name": "OpenAI Codex CLI",
30
+ "description": "OpenAI's Codex CLI for code generation",
31
+ "command": "codex",
32
+ "defaultArgs": ["--approval-mode", "suggest"],
33
+ "yoloArgs": ["--approval-mode", "full-auto"],
34
+ "docker": {
35
+ "install": "# Install OpenAI Codex CLI\nRUN npm install -g @openai/codex",
36
+ "note": "Check 'codex --help' for available flags"
37
+ },
38
+ "envVars": ["OPENAI_API_KEY"],
39
+ "credentialMount": null
40
+ },
41
+ "gemini": {
42
+ "name": "Gemini CLI",
43
+ "description": "Google's Gemini CLI for code assistance",
44
+ "command": "gemini",
45
+ "defaultArgs": [],
46
+ "yoloArgs": ["-y"],
47
+ "docker": {
48
+ "install": "# Install Google Gemini CLI\nRUN npm install -g @google/gemini-cli",
49
+ "note": "Check 'gemini --help' for available flags"
50
+ },
51
+ "envVars": ["GOOGLE_API_KEY", "GEMINI_API_KEY"],
52
+ "credentialMount": "~/.gemini:/home/node/.gemini"
53
+ },
54
+ "opencode": {
55
+ "name": "OpenCode",
56
+ "description": "Open source AI coding agent for the terminal",
57
+ "command": "opencode",
58
+ "defaultArgs": [],
59
+ "yoloArgs": ["--yolo"],
60
+ "docker": {
61
+ "install": "# Install OpenCode\nRUN curl -fsSL https://opencode.ai/install | bash \\\n && echo 'export PATH=\"$HOME/.opencode/bin:$PATH\"' >> /home/node/.zshrc",
62
+ "note": "Check 'opencode --help' for available flags"
63
+ },
64
+ "envVars": ["ANTHROPIC_API_KEY", "OPENAI_API_KEY", "GOOGLE_API_KEY"],
65
+ "credentialMount": null
66
+ },
67
+ "amp": {
68
+ "name": "AMP CLI",
69
+ "description": "Sourcegraph's AMP coding agent",
70
+ "command": "amp",
71
+ "defaultArgs": [],
72
+ "yoloArgs": ["--yolo"],
73
+ "docker": {
74
+ "install": "# Install AMP CLI\nRUN curl -fsSL https://ampcode.com/install.sh | bash \\\n && echo 'export PATH=\"$HOME/.amp/bin:$PATH\"' >> /home/node/.zshrc",
75
+ "note": "Check 'amp --help' for available flags"
76
+ },
77
+ "envVars": ["ANTHROPIC_API_KEY", "OPENAI_API_KEY"],
78
+ "credentialMount": null
79
+ },
80
+ "custom": {
81
+ "name": "Custom CLI",
82
+ "description": "Configure your own AI CLI tool",
83
+ "command": "",
84
+ "defaultArgs": [],
85
+ "yoloArgs": [],
86
+ "docker": {
87
+ "install": "# Custom CLI - add your installation commands here"
88
+ },
89
+ "envVars": [],
90
+ "credentialMount": null
91
+ }
92
+ }
93
+ }
@@ -27,7 +27,24 @@ 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
+ docker: {
37
+ install: string;
38
+ };
39
+ envVars: string[];
40
+ credentialMount: string | null;
41
+ }
42
+ interface CliProvidersJson {
43
+ providers: Record<string, CliProviderConfig>;
44
+ }
30
45
  export declare function getLanguagesJson(): LanguagesJson;
46
+ export declare function getCliProvidersJson(): CliProvidersJson;
47
+ export declare function getCliProviders(): Record<string, CliProviderConfig>;
31
48
  export declare function getLanguages(): Record<string, LanguageConfig>;
32
49
  export declare const LANGUAGES: Record<string, LanguageConfig>;
33
50
  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();
@@ -1,6 +1,7 @@
1
1
  export interface CliConfig {
2
2
  command: string;
3
3
  args?: string[];
4
+ yoloArgs?: string[];
4
5
  }
5
6
  export interface RalphConfig {
6
7
  language: string;
@@ -11,6 +12,7 @@ export interface RalphConfig {
11
12
  technologies?: string[];
12
13
  javaVersion?: number;
13
14
  cli?: CliConfig;
15
+ cliProvider?: string;
14
16
  }
15
17
  export declare const DEFAULT_CLI_CONFIG: CliConfig;
16
18
  export declare function getCliConfig(config: RalphConfig): CliConfig;
@@ -91,8 +91,9 @@ export function requireContainer(commandName) {
91
91
  console.error("For security, ralph executes AI agents only in isolated container environments.");
92
92
  console.error("");
93
93
  console.error("To set up a container:");
94
- console.error(" ralph docker --build # Build the container image");
95
- console.error(" ralph docker --run # Run ralph inside the container");
94
+ console.error(" ralph docker init # Generate Docker configuration files");
95
+ console.error(" ralph docker build # Build the container image");
96
+ console.error(" ralph docker run # Run ralph inside the container");
96
97
  process.exit(1);
97
98
  }
98
99
  }
@@ -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[]>;
@@ -13,6 +13,66 @@ 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.stdout.write("\x1B[?25h"); // Show cursor
72
+ };
73
+ process.stdin.on("data", onKeypress);
74
+ });
75
+ }
16
76
  export async function promptInput(message) {
17
77
  const prompt = createPrompt();
18
78
  const answer = await prompt.question(message);
@@ -99,3 +159,79 @@ export async function promptMultiSelect(message, options) {
99
159
  }
100
160
  }
101
161
  }
162
+ export async function promptMultiSelectWithArrows(message, options) {
163
+ return new Promise((resolve) => {
164
+ let selectedIndex = 0;
165
+ const selected = new Set();
166
+ // Add a "Done" option at the end
167
+ const allOptions = [...options, "[Done - press Enter]"];
168
+ // Hide cursor
169
+ process.stdout.write("\x1B[?25l");
170
+ const render = () => {
171
+ process.stdout.write(`\x1B[${allOptions.length}A`); // Move up
172
+ allOptions.forEach((opt, i) => {
173
+ const isLastOption = i === allOptions.length - 1;
174
+ const cursor = i === selectedIndex ? "\x1B[36m❯\x1B[0m" : " ";
175
+ const checkbox = isLastOption ? "" : (selected.has(i) ? "\x1B[32m[x]\x1B[0m" : "[ ]");
176
+ const text = i === selectedIndex ? `\x1B[36m${opt}\x1B[0m` : opt;
177
+ process.stdout.write(`\x1B[2K${cursor} ${checkbox} ${text}\n`);
178
+ });
179
+ };
180
+ const initialRender = () => {
181
+ console.log(`\n${message}`);
182
+ console.log("(Use arrow keys to navigate, Space to select, Enter to confirm)\n");
183
+ allOptions.forEach((opt, i) => {
184
+ const isLastOption = i === allOptions.length - 1;
185
+ const cursor = i === selectedIndex ? "\x1B[36m❯\x1B[0m" : " ";
186
+ const checkbox = isLastOption ? "" : (selected.has(i) ? "\x1B[32m[x]\x1B[0m" : "[ ]");
187
+ const text = i === selectedIndex ? `\x1B[36m${opt}\x1B[0m` : opt;
188
+ console.log(`${cursor} ${checkbox} ${text}`);
189
+ });
190
+ };
191
+ initialRender();
192
+ if (process.stdin.isTTY) {
193
+ process.stdin.setRawMode(true);
194
+ }
195
+ process.stdin.resume();
196
+ process.stdin.setEncoding("utf8");
197
+ const onKeypress = (key) => {
198
+ if (key === "\x1B[A" || key === "k") { // Up
199
+ selectedIndex = (selectedIndex - 1 + allOptions.length) % allOptions.length;
200
+ render();
201
+ }
202
+ else if (key === "\x1B[B" || key === "j") { // Down
203
+ selectedIndex = (selectedIndex + 1) % allOptions.length;
204
+ render();
205
+ }
206
+ else if (key === " ") { // Space - toggle selection
207
+ const isLastOption = selectedIndex === allOptions.length - 1;
208
+ if (!isLastOption) {
209
+ if (selected.has(selectedIndex)) {
210
+ selected.delete(selectedIndex);
211
+ }
212
+ else {
213
+ selected.add(selectedIndex);
214
+ }
215
+ render();
216
+ }
217
+ }
218
+ else if (key === "\r" || key === "\n") { // Enter - confirm
219
+ cleanup();
220
+ const result = options.filter((_, i) => selected.has(i));
221
+ resolve(result);
222
+ }
223
+ else if (key === "\x03") { // Ctrl+C
224
+ cleanup();
225
+ process.exit(0);
226
+ }
227
+ };
228
+ const cleanup = () => {
229
+ process.stdin.removeListener("data", onKeypress);
230
+ if (process.stdin.isTTY) {
231
+ process.stdin.setRawMode(false);
232
+ }
233
+ process.stdout.write("\x1B[?25h"); // Show cursor
234
+ };
235
+ process.stdin.on("data", onKeypress);
236
+ });
237
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ralph-cli-sandboxed",
3
- "version": "0.1.5",
3
+ "version": "0.2.0",
4
4
  "description": "AI-driven development automation CLI for Claude Code",
5
5
  "type": "module",
6
6
  "bin": {