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 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 (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
- # 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,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 (fetching latest Claude Code)...\n");
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 Claude Code version
286
- const proc = spawn("docker", ["compose", "build", "--no-cache", "--pull"], {
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
- async function runContainer(ralphDir, imageName, language, javaVersion) {
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
- const proc = spawn("docker", ["compose", "run", "--rm", "ralph"], {
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 image name pattern
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
- const volumePattern = `docker_${imageName}`;
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 (named after imageName, not generic docker_default)
504
- const networkName = `docker_${imageName}_default`;
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 --build' to rebuild the image.");
583
+ console.log("Run 'ralph docker build' to rebuild the image.");
518
584
  }
519
585
  export async function docker(args) {
520
- const hasFlag = (flag) => args.includes(flag);
521
- const flag = args[0];
586
+ const subcommand = args[0];
587
+ const subArgs = args.slice(1);
522
588
  // Show help without requiring init
523
- if (flag === "--help" || flag === "-h") {
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 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
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 # 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
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
- // 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(`
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 --build
609
- 2. Run container: ralph docker --run
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
  }
@@ -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,55 @@ 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
+ 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 promptSelect("Select your project language/runtime:", languageNames);
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
- // Select technology stack if available (only when --tech-stack flag is provided)
74
+ console.log(`\nSelected language: ${config.name}`);
75
+ // Step 3: Select technology stack if available (third)
46
76
  let selectedTechnologies = [];
47
- if (showTechStack && config.technologies && config.technologies.length > 0) {
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 promptMultiSelect("Select your technology stack (select multiple or add custom):", techOptions);
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: DEFAULT_CLI_CONFIG,
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 once' to start the first iteration");
145
- console.log(" 4. Or run 'ralph run 5' for 5 automated iterations");
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
  }
@@ -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 + 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"];
21
+ const promptArgs = cliConfig.promptArgs ?? ["-p"];
22
+ const promptValue = `@${paths.prd} @${paths.progress} ${prompt}`;
19
23
  const cliArgs = [
20
24
  ...(cliConfig.args ?? []),
21
- "--dangerously-skip-permissions",
22
- "-p",
23
- `@${paths.prd} @${paths.progress} ${prompt}`,
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) {
@@ -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 + 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
- cliArgs.push("-p", `@${filteredPrdPath} @${paths.progress} ${prompt}`);
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 allMode = false;
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
- allMode = true;
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();
@@ -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;
@@ -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
- return config.cli ?? DEFAULT_CLI_CONFIG;
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 --build # Build the container image");
95
- console.error(" ralph docker --run # Run ralph inside the container");
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
  }
@@ -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,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.5",
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"