ralph-cli-sandboxed 0.2.5 → 0.2.6

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
@@ -85,12 +85,28 @@ After running `ralph init`, you'll have:
85
85
 
86
86
  ### Supported Languages
87
87
 
88
- - **Bun** (TypeScript) - `bun check`, `bun test`
89
- - **Node.js** (TypeScript) - `npm run typecheck`, `npm test`
90
- - **Python** - `mypy .`, `pytest`
91
- - **Go** - `go build ./...`, `go test ./...`
92
- - **Rust** - `cargo check`, `cargo test`
93
- - **Custom** - Define your own commands
88
+ Ralph supports 18 programming languages with pre-configured build/test commands:
89
+
90
+ | Language | Check Command | Test Command |
91
+ |----------|--------------|--------------|
92
+ | Bun (TypeScript) | `bun check` | `bun test` |
93
+ | Node.js (TypeScript) | `npm run typecheck` | `npm test` |
94
+ | Python | `mypy .` | `pytest` |
95
+ | Go | `go build ./...` | `go test ./...` |
96
+ | Rust | `cargo check` | `cargo test` |
97
+ | Java | `mvn compile` | `mvn test` |
98
+ | Kotlin | `gradle build` | `gradle test` |
99
+ | C#/.NET | `dotnet build` | `dotnet test` |
100
+ | Ruby | `bundle exec rubocop` | `bundle exec rspec` |
101
+ | PHP | `composer validate` | `vendor/bin/phpunit` |
102
+ | Swift | `swift build` | `swift test` |
103
+ | Elixir | `mix compile` | `mix test` |
104
+ | Scala | `sbt compile` | `sbt test` |
105
+ | Zig | `zig build` | `zig build test` |
106
+ | Haskell | `stack build` | `stack test` |
107
+ | Clojure | `lein check` | `lein test` |
108
+ | Deno (TypeScript) | `deno check **/*.ts` | `deno test` |
109
+ | Custom | User-defined | User-defined |
94
110
 
95
111
  ### Supported CLI Providers
96
112
 
@@ -147,7 +163,7 @@ The PRD (`prd.json`) is an array of requirements:
147
163
  ]
148
164
  ```
149
165
 
150
- Categories: `ui`, `feature`, `bugfix`, `setup`, `development`, `testing`, `docs`
166
+ Categories: `setup`, `feature`, `bugfix`, `refactor`, `docs`, `test`, `release`, `config`, `ui`
151
167
 
152
168
  ### Advanced: File References
153
169
 
@@ -191,13 +207,7 @@ Features:
191
207
  - Your `~/.claude` credentials mounted automatically (Pro/Max OAuth)
192
208
  - Language-specific tooling pre-installed
193
209
 
194
- ### Installing packages in container
195
-
196
- ```bash
197
- # Run as root to install packages
198
- docker compose run -u root ralph apt-get update
199
- docker compose run -u root ralph apt-get install <package>
200
- ```
210
+ See [docs/DOCKER.md](docs/DOCKER.md) for detailed Docker configuration, customization, and troubleshooting.
201
211
 
202
212
  ## How It Works
203
213
 
@@ -212,71 +222,24 @@ When all PRD items pass, Claude outputs `<promise>COMPLETE</promise>` and stops.
212
222
 
213
223
  ## Security
214
224
 
215
- ### Container Requirement
216
-
217
- **It is strongly recommended to run ralph inside a Docker container for security.** The Ralph Wiggum technique involves running an AI agent autonomously, which means granting it elevated permissions to execute code and modify files without manual approval for each action.
218
-
219
- ### The `--dangerously-skip-permissions` Flag
220
-
221
- When running inside a container, ralph automatically passes the `--dangerously-skip-permissions` flag to Claude Code. This flag:
225
+ **It is strongly recommended to run ralph inside a Docker container for security.** The Ralph Wiggum technique involves running an AI agent autonomously with elevated permissions.
222
226
 
223
- - Allows Claude to execute commands and modify files without prompting for permission
224
- - Is **only** enabled when ralph detects it's running inside a container
225
- - Is required for autonomous operation (otherwise Claude would pause for approval on every action)
227
+ When running inside a container, ralph automatically passes `--dangerously-skip-permissions` to Claude Code, allowing autonomous operation. This flag is only enabled in containers for safety.
226
228
 
227
- **Warning:** The `--dangerously-skip-permissions` flag gives the AI agent full control over the environment. This is why container isolation is critical:
228
-
229
- - The container provides a sandbox boundary
230
- - Network access is restricted to essential services (GitHub, npm, Anthropic API)
231
- - Your host system remains protected even if something goes wrong
232
-
233
- ### Container Detection
234
-
235
- Ralph detects container environments by checking:
236
- - `DEVCONTAINER` environment variable
237
- - Presence of `/.dockerenv` file
238
- - Container indicators in `/proc/1/cgroup`
239
- - `container` environment variable
240
-
241
- If you're running outside a container and need autonomous mode, use `ralph docker` to set up a safe sandbox environment first.
229
+ See [docs/SECURITY.md](docs/SECURITY.md) for detailed security information, container detection, and best practices.
242
230
 
243
231
  ## Development
244
232
 
245
233
  To contribute or test changes to ralph locally:
246
234
 
247
235
  ```bash
248
- # Clone the repository
249
236
  git clone https://github.com/choas/ralph-cli-sandboxed
250
237
  cd ralph-cli-sandboxed
251
-
252
- # Install dependencies
253
238
  npm install
254
-
255
- # Run ralph in development mode (without building)
256
- npm run dev -- <args>
257
-
258
- # Examples:
259
- npm run dev -- --version
260
- npm run dev -- list
261
- npm run dev -- once
239
+ npm run dev -- <args> # Run from TypeScript source
262
240
  ```
263
241
 
264
- The `npm run dev -- <args>` command runs ralph directly from TypeScript source using `tsx`, allowing you to test changes without rebuilding.
265
-
266
- ### Platform-Specific Dependencies
267
-
268
- The `node_modules` folder contains platform-specific binaries (e.g., esbuild). If you switch between running on your host machine and inside a Docker/Podman container, you'll need to reinstall dependencies:
269
-
270
- ```bash
271
- # When switching environments (host <-> container)
272
- rm -rf node_modules && npm install
273
- ```
274
-
275
- Alternatively, when mounting your project into a container, use a separate volume for node_modules to keep host and container dependencies isolated:
276
-
277
- ```bash
278
- podman run -v $(pwd):/workspace -v /workspace/node_modules your-image
279
- ```
242
+ See [docs/DEVELOPMENT.md](docs/DEVELOPMENT.md) for detailed development setup, project structure, and contribution guidelines.
280
243
 
281
244
  ## Requirements
282
245
 
@@ -1,10 +1,46 @@
1
- import { existsSync, writeFileSync, mkdirSync, chmodSync } from "fs";
1
+ import { existsSync, writeFileSync, readFileSync, mkdirSync, chmodSync } from "fs";
2
2
  import { join, basename } from "path";
3
3
  import { spawn } from "child_process";
4
+ import { createHash } from "crypto";
4
5
  import { loadConfig, getRalphDir } from "../utils/config.js";
5
6
  import { promptConfirm } from "../utils/prompt.js";
6
7
  import { getLanguagesJson, getCliProvidersJson } from "../templates/prompts.js";
7
8
  const DOCKER_DIR = "docker";
9
+ const CONFIG_HASH_FILE = ".config-hash";
10
+ // Compute hash of docker-relevant config fields
11
+ function computeConfigHash(config) {
12
+ const relevantConfig = {
13
+ language: config.language,
14
+ javaVersion: config.javaVersion,
15
+ cliProvider: config.cliProvider,
16
+ docker: config.docker,
17
+ claude: config.claude,
18
+ };
19
+ const content = JSON.stringify(relevantConfig, null, 2);
20
+ return createHash('sha256').update(content).digest('hex').substring(0, 16);
21
+ }
22
+ // Save config hash to docker directory
23
+ function saveConfigHash(dockerDir, hash) {
24
+ writeFileSync(join(dockerDir, CONFIG_HASH_FILE), hash + '\n');
25
+ }
26
+ // Load saved config hash, returns null if not found
27
+ function loadConfigHash(dockerDir) {
28
+ const hashPath = join(dockerDir, CONFIG_HASH_FILE);
29
+ if (!existsSync(hashPath)) {
30
+ return null;
31
+ }
32
+ return readFileSync(hashPath, 'utf-8').trim();
33
+ }
34
+ // Check if config has changed since last docker init
35
+ function hasConfigChanged(ralphDir, config) {
36
+ const dockerDir = join(ralphDir, DOCKER_DIR);
37
+ const savedHash = loadConfigHash(dockerDir);
38
+ if (!savedHash) {
39
+ return false; // No hash file means docker init hasn't run yet
40
+ }
41
+ const currentHash = computeConfigHash(config);
42
+ return savedHash !== currentHash;
43
+ }
8
44
  // Get language Docker snippet from config, with version substitution
9
45
  function getLanguageSnippet(language, javaVersion) {
10
46
  const languagesJson = getLanguagesJson();
@@ -36,6 +72,29 @@ function getCliProviderSnippet(cliProvider) {
36
72
  function generateDockerfile(language, javaVersion, cliProvider, dockerConfig) {
37
73
  const languageSnippet = getLanguageSnippet(language, javaVersion);
38
74
  const cliSnippet = getCliProviderSnippet(cliProvider);
75
+ // Build custom packages section
76
+ let customPackages = '';
77
+ if (dockerConfig?.packages && dockerConfig.packages.length > 0) {
78
+ customPackages = dockerConfig.packages.map(pkg => ` ${pkg} \\`).join('\n') + '\n';
79
+ }
80
+ // Build root build commands section
81
+ let rootBuildCommands = '';
82
+ if (dockerConfig?.buildCommands?.root && dockerConfig.buildCommands.root.length > 0) {
83
+ const commands = dockerConfig.buildCommands.root.map(cmd => `RUN ${cmd}`).join('\n');
84
+ rootBuildCommands = `
85
+ # Custom build commands (root)
86
+ ${commands}
87
+ `;
88
+ }
89
+ // Build node build commands section
90
+ let nodeBuildCommands = '';
91
+ if (dockerConfig?.buildCommands?.node && dockerConfig.buildCommands.node.length > 0) {
92
+ const commands = dockerConfig.buildCommands.node.map(cmd => `RUN ${cmd}`).join('\n');
93
+ nodeBuildCommands = `
94
+ # Custom build commands (node user)
95
+ ${commands}
96
+ `;
97
+ }
39
98
  // Build git config section if configured
40
99
  let gitConfigSection = '';
41
100
  if (dockerConfig?.git && (dockerConfig.git.name || dockerConfig.git.email)) {
@@ -49,6 +108,20 @@ function generateDockerfile(language, javaVersion, cliProvider, dockerConfig) {
49
108
  gitConfigSection = `
50
109
  # Configure git identity
51
110
  RUN ${gitCommands.join(' \\\n && ')}
111
+ `;
112
+ }
113
+ // Build asciinema installation section if enabled
114
+ let asciinemaInstall = '';
115
+ let asciinemaDir = '';
116
+ if (dockerConfig?.asciinema?.enabled) {
117
+ const outputDir = dockerConfig.asciinema.outputDir || '.recordings';
118
+ asciinemaInstall = `
119
+ # Install asciinema for terminal recording/streaming
120
+ RUN apt-get update && apt-get install -y asciinema && rm -rf /var/lib/apt/lists/*
121
+ `;
122
+ asciinemaDir = `
123
+ # Create asciinema recordings directory
124
+ RUN mkdir -p /workspace/${outputDir} && chown node:node /workspace/${outputDir}
52
125
  `;
53
126
  }
54
127
  return `# Ralph CLI Sandbox Environment
@@ -85,7 +158,7 @@ RUN apt-get update && apt-get install -y \\
85
158
  iproute2 \\
86
159
  dnsutils \\
87
160
  zsh \\
88
- && rm -rf /var/lib/apt/lists/*
161
+ ${customPackages} && rm -rf /var/lib/apt/lists/*
89
162
 
90
163
  # Setup zsh with oh-my-zsh and plugins (no theme, we set custom prompt)
91
164
  RUN sh -c "$(wget -O- https://github.com/deluan/zsh-in-docker/releases/download/v\${ZSH_IN_DOCKER_VERSION}/zsh-in-docker.sh)" -- \\
@@ -130,7 +203,7 @@ RUN echo "node ALL=(ALL) NOPASSWD: /usr/local/bin/init-firewall.sh" >> /etc/sudo
130
203
  RUN mkdir -p /workspace && chown node:node /workspace
131
204
  RUN mkdir -p /home/node/.claude && chown node:node /home/node/.claude
132
205
  RUN mkdir -p /commandhistory && chown node:node /commandhistory
133
-
206
+ ${asciinemaDir}
134
207
  # Copy firewall script
135
208
  COPY init-firewall.sh /usr/local/bin/init-firewall.sh
136
209
  RUN chmod +x /usr/local/bin/init-firewall.sh
@@ -145,17 +218,33 @@ ENV EDITOR=nano
145
218
  # Add bash aliases and prompt (fallback if using bash)
146
219
  RUN echo 'alias ll="ls -la"' >> /etc/bash.bashrc && \\
147
220
  echo 'PS1="\\[\\033[43;30m\\][ralph]\\w\\[\\033[0m\\]\\$ "' >> /etc/bash.bashrc
148
-
221
+ ${rootBuildCommands}${asciinemaInstall}
149
222
  # Switch to non-root user
150
223
  USER node
151
- ${gitConfigSection}
224
+ ${gitConfigSection}${nodeBuildCommands}
152
225
  WORKDIR /workspace
153
226
 
154
227
  # Default to zsh
155
228
  CMD ["zsh"]
156
229
  `;
157
230
  }
158
- const FIREWALL_SCRIPT = `#!/bin/bash
231
+ function generateFirewallScript(customDomains = []) {
232
+ // Generate custom domains section if any are configured
233
+ let customDomainsSection = '';
234
+ if (customDomains.length > 0) {
235
+ const domainList = customDomains.join(' ');
236
+ customDomainsSection = `
237
+ # Custom allowed domains (from config)
238
+ for ip in $(dig +short ${domainList}); do
239
+ ipset add allowed_ips $ip 2>/dev/null || true
240
+ done
241
+ `;
242
+ }
243
+ // Generate echo line with custom domains if configured
244
+ const allowedList = customDomains.length > 0
245
+ ? `GitHub, npm, Anthropic API, local network, ${customDomains.join(', ')}`
246
+ : 'GitHub, npm, Anthropic API, local network';
247
+ return `#!/bin/bash
159
248
  # Firewall initialization script for Ralph sandbox
160
249
  # Based on Claude Code devcontainer firewall
161
250
 
@@ -211,7 +300,7 @@ done
211
300
  for ip in $(dig +short api.anthropic.com); do
212
301
  ipset add allowed_ips $ip 2>/dev/null || true
213
302
  done
214
-
303
+ ${customDomainsSection}
215
304
  # Allow host network (for mounted volumes, etc.)
216
305
  HOST_NETWORK=$(ip route | grep default | awk '{print $3}' | head -1)
217
306
  if [ -n "$HOST_NETWORK" ]; then
@@ -232,8 +321,9 @@ iptables -I OUTPUT -p tcp --dport 443 -m set --match-set allowed_ips dst -j ACCE
232
321
  iptables -I OUTPUT -p tcp --dport 80 -m set --match-set allowed_ips dst -j ACCEPT
233
322
 
234
323
  echo "Firewall initialized. Only allowed destinations are accessible."
235
- echo "Allowed: GitHub, npm, Anthropic API, local network"
324
+ echo "Allowed: ${allowedList}"
236
325
  `;
326
+ }
237
327
  function generateDockerCompose(imageName, dockerConfig) {
238
328
  // Build ports section if configured
239
329
  let portsSection = '';
@@ -256,11 +346,15 @@ function generateDockerCompose(imageName, dockerConfig) {
256
346
  const volumesSection = baseVolumes.join('\n');
257
347
  // Build environment section if configured
258
348
  let environmentSection = '';
349
+ const envEntries = [];
350
+ // Add user-configured environment variables
259
351
  if (dockerConfig?.environment && Object.keys(dockerConfig.environment).length > 0) {
260
- const envLines = Object.entries(dockerConfig.environment)
261
- .map(([key, value]) => ` - ${key}=${value}`)
262
- .join('\n');
263
- environmentSection = ` environment:\n${envLines}\n`;
352
+ for (const [key, value] of Object.entries(dockerConfig.environment)) {
353
+ envEntries.push(` - ${key}=${value}`);
354
+ }
355
+ }
356
+ if (envEntries.length > 0) {
357
+ environmentSection = ` environment:\n${envEntries.join('\n')}\n`;
264
358
  }
265
359
  else {
266
360
  // Keep the commented placeholder for users who don't have config
@@ -268,6 +362,22 @@ function generateDockerCompose(imageName, dockerConfig) {
268
362
  # environment:
269
363
  # - ANTHROPIC_API_KEY=\${ANTHROPIC_API_KEY}\n`;
270
364
  }
365
+ // Build command section if configured
366
+ let commandSection = '';
367
+ if (dockerConfig?.asciinema?.enabled && dockerConfig?.asciinema?.autoRecord) {
368
+ // Wrap with asciinema recording
369
+ const outputDir = dockerConfig.asciinema.outputDir || '.recordings';
370
+ const innerCommand = dockerConfig.startCommand || 'zsh';
371
+ commandSection = ` command: bash -c "mkdir -p /workspace/${outputDir} && asciinema rec -c '${innerCommand}' /workspace/${outputDir}/session-$$(date +%Y%m%d-%H%M%S).cast"\n`;
372
+ }
373
+ else if (dockerConfig?.startCommand) {
374
+ commandSection = ` command: ${dockerConfig.startCommand}\n`;
375
+ }
376
+ else {
377
+ // Keep the commented placeholder for users who don't have config
378
+ commandSection = ` # Uncomment to enable firewall sandboxing:
379
+ # command: bash -c "sudo /usr/local/bin/init-firewall.sh && zsh"\n`;
380
+ }
271
381
  return `# Ralph CLI Docker Compose
272
382
  # Generated by ralph-cli
273
383
 
@@ -284,9 +394,7 @@ ${environmentSection} working_dir: /workspace
284
394
  tty: true
285
395
  cap_add:
286
396
  - NET_ADMIN # Required for firewall
287
- # Uncomment to enable firewall sandboxing:
288
- # command: bash -c "sudo /usr/local/bin/init-firewall.sh && zsh"
289
-
397
+ ${commandSection}
290
398
  volumes:
291
399
  ${imageName}-history:
292
400
  `;
@@ -297,16 +405,30 @@ dist
297
405
  .git
298
406
  *.log
299
407
  `;
300
- async function generateFiles(ralphDir, language, imageName, force = false, javaVersion, cliProvider, dockerConfig) {
408
+ // Generate .mcp.json content for Claude Code MCP servers
409
+ function generateMcpJson(mcpServers) {
410
+ return JSON.stringify({ mcpServers }, null, 2);
411
+ }
412
+ // Generate skill file content with YAML frontmatter
413
+ function generateSkillFile(skill) {
414
+ const lines = ['---', `description: ${skill.description}`];
415
+ if (skill.userInvocable === false) {
416
+ lines.push('user-invocable: false');
417
+ }
418
+ lines.push('---', '', skill.instructions, '');
419
+ return lines.join('\n');
420
+ }
421
+ async function generateFiles(ralphDir, language, imageName, force = false, javaVersion, cliProvider, dockerConfig, claudeConfig) {
301
422
  const dockerDir = join(ralphDir, DOCKER_DIR);
302
423
  // Create docker directory
303
424
  if (!existsSync(dockerDir)) {
304
425
  mkdirSync(dockerDir, { recursive: true });
305
426
  console.log(`Created ${DOCKER_DIR}/`);
306
427
  }
428
+ const customDomains = dockerConfig?.firewall?.allowedDomains || [];
307
429
  const files = [
308
430
  { name: "Dockerfile", content: generateDockerfile(language, javaVersion, cliProvider, dockerConfig) },
309
- { name: "init-firewall.sh", content: FIREWALL_SCRIPT },
431
+ { name: "init-firewall.sh", content: generateFirewallScript(customDomains) },
310
432
  { name: "docker-compose.yml", content: generateDockerCompose(imageName, dockerConfig) },
311
433
  { name: ".dockerignore", content: DOCKERIGNORE },
312
434
  ];
@@ -325,6 +447,58 @@ async function generateFiles(ralphDir, language, imageName, force = false, javaV
325
447
  }
326
448
  console.log(`Created ${DOCKER_DIR}/${file.name}`);
327
449
  }
450
+ // Generate Claude config files at project root
451
+ const projectRoot = process.cwd();
452
+ // Generate .mcp.json if MCP servers are configured
453
+ if (claudeConfig?.mcpServers && Object.keys(claudeConfig.mcpServers).length > 0) {
454
+ const mcpJsonPath = join(projectRoot, '.mcp.json');
455
+ if (existsSync(mcpJsonPath) && !force) {
456
+ const overwrite = await promptConfirm('.mcp.json already exists. Overwrite?');
457
+ if (!overwrite) {
458
+ console.log('Skipped .mcp.json');
459
+ }
460
+ else {
461
+ writeFileSync(mcpJsonPath, generateMcpJson(claudeConfig.mcpServers));
462
+ console.log('Created .mcp.json');
463
+ }
464
+ }
465
+ else {
466
+ writeFileSync(mcpJsonPath, generateMcpJson(claudeConfig.mcpServers));
467
+ console.log('Created .mcp.json');
468
+ }
469
+ }
470
+ // Generate skill files if skills are configured
471
+ if (claudeConfig?.skills && claudeConfig.skills.length > 0) {
472
+ const commandsDir = join(projectRoot, '.claude', 'commands');
473
+ if (!existsSync(commandsDir)) {
474
+ mkdirSync(commandsDir, { recursive: true });
475
+ console.log('Created .claude/commands/');
476
+ }
477
+ for (const skill of claudeConfig.skills) {
478
+ const skillPath = join(commandsDir, `${skill.name}.md`);
479
+ if (existsSync(skillPath) && !force) {
480
+ const overwrite = await promptConfirm(`.claude/commands/${skill.name}.md already exists. Overwrite?`);
481
+ if (!overwrite) {
482
+ console.log(`Skipped .claude/commands/${skill.name}.md`);
483
+ continue;
484
+ }
485
+ }
486
+ writeFileSync(skillPath, generateSkillFile(skill));
487
+ console.log(`Created .claude/commands/${skill.name}.md`);
488
+ }
489
+ }
490
+ // Save config hash for change detection
491
+ const configForHash = {
492
+ language,
493
+ checkCommand: '',
494
+ testCommand: '',
495
+ javaVersion,
496
+ cliProvider,
497
+ docker: dockerConfig,
498
+ claude: claudeConfig,
499
+ };
500
+ const hash = computeConfigHash(configForHash);
501
+ saveConfigHash(dockerDir, hash);
328
502
  }
329
503
  async function buildImage(ralphDir) {
330
504
  const dockerDir = join(ralphDir, DOCKER_DIR);
@@ -332,9 +506,17 @@ async function buildImage(ralphDir) {
332
506
  console.error("Dockerfile not found. Run 'ralph docker' first.");
333
507
  process.exit(1);
334
508
  }
335
- console.log("Building Docker image...\n");
336
- // Get image name for compose project name
509
+ // Get config and check for changes
337
510
  const config = loadConfig();
511
+ // Check if config has changed since last docker init
512
+ if (hasConfigChanged(ralphDir, config)) {
513
+ const regenerate = await promptConfirm("Config has changed since last docker init. Regenerate Docker files?");
514
+ if (regenerate) {
515
+ await generateFiles(ralphDir, config.language, config.imageName || `ralph-${basename(process.cwd()).toLowerCase().replace(/[^a-z0-9-]/g, "-")}`, true, config.javaVersion, config.cliProvider, config.docker, config.claude);
516
+ console.log("");
517
+ }
518
+ }
519
+ console.log("Building Docker image...\n");
338
520
  const imageName = config.imageName || `ralph-${basename(process.cwd()).toLowerCase().replace(/[^a-z0-9-]/g, "-")}`;
339
521
  return new Promise((resolve, reject) => {
340
522
  // Use --no-cache and --pull to ensure we always get the latest CLI versions
@@ -398,15 +580,34 @@ function getCliProviderConfig(cliProvider) {
398
580
  modelConfig: provider.modelConfig,
399
581
  };
400
582
  }
401
- async function runContainer(ralphDir, imageName, language, javaVersion, cliProvider, dockerConfig) {
583
+ async function runContainer(ralphDir, imageName, language, javaVersion, cliProvider, dockerConfig, claudeConfig) {
402
584
  const dockerDir = join(ralphDir, DOCKER_DIR);
403
585
  const dockerfileExists = existsSync(join(dockerDir, "Dockerfile"));
404
586
  const hasImage = await imageExists(imageName);
587
+ // Check if config has changed since last docker init
588
+ if (dockerfileExists) {
589
+ const configForHash = {
590
+ language,
591
+ checkCommand: '',
592
+ testCommand: '',
593
+ javaVersion,
594
+ cliProvider,
595
+ docker: dockerConfig,
596
+ claude: claudeConfig,
597
+ };
598
+ if (hasConfigChanged(ralphDir, configForHash)) {
599
+ const regenerate = await promptConfirm("Config has changed since last docker init. Regenerate Docker files?");
600
+ if (regenerate) {
601
+ await generateFiles(ralphDir, language, imageName, true, javaVersion, cliProvider, dockerConfig, claudeConfig);
602
+ console.log("");
603
+ }
604
+ }
605
+ }
405
606
  // Auto-init and build if docker folder or image doesn't exist
406
607
  if (!dockerfileExists || !hasImage) {
407
608
  if (!dockerfileExists) {
408
609
  console.log("Docker folder not found. Initializing docker setup...\n");
409
- await generateFiles(ralphDir, language, imageName, true, javaVersion, cliProvider, dockerConfig);
610
+ await generateFiles(ralphDir, language, imageName, true, javaVersion, cliProvider, dockerConfig, claudeConfig);
410
611
  console.log("");
411
612
  }
412
613
  if (!hasImage) {
@@ -654,7 +855,7 @@ export async function dockerInit(silent = false) {
654
855
  console.log(`CLI provider: ${config.cliProvider}`);
655
856
  }
656
857
  console.log(`Image name: ${imageName}\n`);
657
- await generateFiles(ralphDir, config.language, imageName, true, config.javaVersion, config.cliProvider, config.docker);
858
+ await generateFiles(ralphDir, config.language, imageName, true, config.javaVersion, config.cliProvider, config.docker, config.claude);
658
859
  if (!silent) {
659
860
  console.log(`
660
861
  Docker files generated in .ralph/docker/
@@ -681,6 +882,7 @@ USAGE:
681
882
  ralph docker init -y Generate files, overwrite without prompting
682
883
  ralph docker build Build image (fetches latest CLI versions)
683
884
  ralph docker build --clean Clean existing image and rebuild from scratch
885
+ (alias: --no-cache)
684
886
  ralph docker run Run container (auto-init and build if needed)
685
887
  ralph docker clean Remove Docker image and associated resources
686
888
  ralph docker help Show this help message
@@ -735,14 +937,15 @@ INSTALLING PACKAGES (works with Docker & Podman):
735
937
  switch (subcommand) {
736
938
  case "build":
737
939
  // Handle build --clean combination: clean first, then build
738
- if (hasFlag("--clean")) {
940
+ // Also support --no-cache as alias for --clean
941
+ if (hasFlag("--clean") || hasFlag("--no-cache") || hasFlag("-no-cache")) {
739
942
  await cleanImage(imageName, ralphDir);
740
943
  console.log(""); // Add spacing between clean and build output
741
944
  }
742
945
  await buildImage(ralphDir);
743
946
  break;
744
947
  case "run":
745
- await runContainer(ralphDir, imageName, config.language, config.javaVersion, config.cliProvider, config.docker);
948
+ await runContainer(ralphDir, imageName, config.language, config.javaVersion, config.cliProvider, config.docker, config.claude);
746
949
  break;
747
950
  case "clean":
748
951
  await cleanImage(imageName, ralphDir);
@@ -761,7 +964,7 @@ INSTALLING PACKAGES (works with Docker & Podman):
761
964
  console.log(`CLI provider: ${config.cliProvider}`);
762
965
  }
763
966
  console.log(`Image name: ${imageName}\n`);
764
- await generateFiles(ralphDir, config.language, imageName, force, config.javaVersion, config.cliProvider, config.docker);
967
+ await generateFiles(ralphDir, config.language, imageName, force, config.javaVersion, config.cliProvider, config.docker, config.claude);
765
968
  console.log(`
766
969
  Docker files generated in .ralph/docker/
767
970
 
@@ -56,6 +56,7 @@ DOCKER SUBCOMMANDS:
56
56
 
57
57
  EXAMPLES:
58
58
  ralph init # Initialize ralph (interactive CLI, language, tech selection)
59
+ ralph init -y # Initialize with defaults (Claude + Node.js, no prompts)
59
60
  ralph once # Run single iteration
60
61
  ralph run # Run until all tasks complete (default)
61
62
  ralph run 5 # Run exactly 5 iterations
@@ -1 +1 @@
1
- export declare function init(_args: string[]): Promise<void>;
1
+ export declare function init(args: string[]): Promise<void>;