ralph-cli-sandboxed 0.2.4 → 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();
@@ -33,9 +69,61 @@ function getCliProviderSnippet(cliProvider) {
33
69
  }
34
70
  return provider.docker.install;
35
71
  }
36
- function generateDockerfile(language, javaVersion, cliProvider) {
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
+ }
98
+ // Build git config section if configured
99
+ let gitConfigSection = '';
100
+ if (dockerConfig?.git && (dockerConfig.git.name || dockerConfig.git.email)) {
101
+ const gitCommands = [];
102
+ if (dockerConfig.git.name) {
103
+ gitCommands.push(`git config --global user.name "${dockerConfig.git.name}"`);
104
+ }
105
+ if (dockerConfig.git.email) {
106
+ gitCommands.push(`git config --global user.email "${dockerConfig.git.email}"`);
107
+ }
108
+ gitConfigSection = `
109
+ # Configure git identity
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}
125
+ `;
126
+ }
39
127
  return `# Ralph CLI Sandbox Environment
40
128
  # Based on Claude Code devcontainer
41
129
  # Generated by ralph-cli
@@ -70,7 +158,7 @@ RUN apt-get update && apt-get install -y \\
70
158
  iproute2 \\
71
159
  dnsutils \\
72
160
  zsh \\
73
- && rm -rf /var/lib/apt/lists/*
161
+ ${customPackages} && rm -rf /var/lib/apt/lists/*
74
162
 
75
163
  # Setup zsh with oh-my-zsh and plugins (no theme, we set custom prompt)
76
164
  RUN sh -c "$(wget -O- https://github.com/deluan/zsh-in-docker/releases/download/v\${ZSH_IN_DOCKER_VERSION}/zsh-in-docker.sh)" -- \\
@@ -115,7 +203,7 @@ RUN echo "node ALL=(ALL) NOPASSWD: /usr/local/bin/init-firewall.sh" >> /etc/sudo
115
203
  RUN mkdir -p /workspace && chown node:node /workspace
116
204
  RUN mkdir -p /home/node/.claude && chown node:node /home/node/.claude
117
205
  RUN mkdir -p /commandhistory && chown node:node /commandhistory
118
-
206
+ ${asciinemaDir}
119
207
  # Copy firewall script
120
208
  COPY init-firewall.sh /usr/local/bin/init-firewall.sh
121
209
  RUN chmod +x /usr/local/bin/init-firewall.sh
@@ -130,16 +218,33 @@ ENV EDITOR=nano
130
218
  # Add bash aliases and prompt (fallback if using bash)
131
219
  RUN echo 'alias ll="ls -la"' >> /etc/bash.bashrc && \\
132
220
  echo 'PS1="\\[\\033[43;30m\\][ralph]\\w\\[\\033[0m\\]\\$ "' >> /etc/bash.bashrc
133
-
221
+ ${rootBuildCommands}${asciinemaInstall}
134
222
  # Switch to non-root user
135
223
  USER node
224
+ ${gitConfigSection}${nodeBuildCommands}
136
225
  WORKDIR /workspace
137
226
 
138
227
  # Default to zsh
139
228
  CMD ["zsh"]
140
229
  `;
141
230
  }
142
- 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
143
248
  # Firewall initialization script for Ralph sandbox
144
249
  # Based on Claude Code devcontainer firewall
145
250
 
@@ -195,7 +300,7 @@ done
195
300
  for ip in $(dig +short api.anthropic.com); do
196
301
  ipset add allowed_ips $ip 2>/dev/null || true
197
302
  done
198
-
303
+ ${customDomainsSection}
199
304
  # Allow host network (for mounted volumes, etc.)
200
305
  HOST_NETWORK=$(ip route | grep default | awk '{print $3}' | head -1)
201
306
  if [ -n "$HOST_NETWORK" ]; then
@@ -216,9 +321,63 @@ iptables -I OUTPUT -p tcp --dport 443 -m set --match-set allowed_ips dst -j ACCE
216
321
  iptables -I OUTPUT -p tcp --dport 80 -m set --match-set allowed_ips dst -j ACCEPT
217
322
 
218
323
  echo "Firewall initialized. Only allowed destinations are accessible."
219
- echo "Allowed: GitHub, npm, Anthropic API, local network"
324
+ echo "Allowed: ${allowedList}"
220
325
  `;
221
- function generateDockerCompose(imageName) {
326
+ }
327
+ function generateDockerCompose(imageName, dockerConfig) {
328
+ // Build ports section if configured
329
+ let portsSection = '';
330
+ if (dockerConfig?.ports && dockerConfig.ports.length > 0) {
331
+ const portLines = dockerConfig.ports.map(port => ` - "${port}"`).join('\n');
332
+ portsSection = ` ports:\n${portLines}\n`;
333
+ }
334
+ // Build volumes array: base volumes + custom volumes
335
+ const baseVolumes = [
336
+ ' # Mount project root (two levels up from .ralph/docker/)',
337
+ ' - ../..:/workspace',
338
+ " # Mount host's ~/.claude for Pro/Max OAuth credentials",
339
+ ' - ${HOME}/.claude:/home/node/.claude',
340
+ ` - ${imageName}-history:/commandhistory`,
341
+ ];
342
+ if (dockerConfig?.volumes && dockerConfig.volumes.length > 0) {
343
+ const customVolumeLines = dockerConfig.volumes.map(vol => ` - ${vol}`);
344
+ baseVolumes.push(...customVolumeLines);
345
+ }
346
+ const volumesSection = baseVolumes.join('\n');
347
+ // Build environment section if configured
348
+ let environmentSection = '';
349
+ const envEntries = [];
350
+ // Add user-configured environment variables
351
+ if (dockerConfig?.environment && Object.keys(dockerConfig.environment).length > 0) {
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`;
358
+ }
359
+ else {
360
+ // Keep the commented placeholder for users who don't have config
361
+ environmentSection = ` # Uncomment to use API key instead of OAuth:
362
+ # environment:
363
+ # - ANTHROPIC_API_KEY=\${ANTHROPIC_API_KEY}\n`;
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
+ }
222
381
  return `# Ralph CLI Docker Compose
223
382
  # Generated by ralph-cli
224
383
 
@@ -228,23 +387,14 @@ services:
228
387
  build:
229
388
  context: .
230
389
  dockerfile: Dockerfile
231
- volumes:
232
- # Mount project root (two levels up from .ralph/docker/)
233
- - ../..:/workspace
234
- # Mount host's ~/.claude for Pro/Max OAuth credentials
235
- - \${HOME}/.claude:/home/node/.claude
236
- - ${imageName}-history:/commandhistory
237
- # Uncomment to use API key instead of OAuth:
238
- # environment:
239
- # - ANTHROPIC_API_KEY=\${ANTHROPIC_API_KEY}
240
- working_dir: /workspace
390
+ ${portsSection} volumes:
391
+ ${volumesSection}
392
+ ${environmentSection} working_dir: /workspace
241
393
  stdin_open: true
242
394
  tty: true
243
395
  cap_add:
244
396
  - NET_ADMIN # Required for firewall
245
- # Uncomment to enable firewall sandboxing:
246
- # command: bash -c "sudo /usr/local/bin/init-firewall.sh && zsh"
247
-
397
+ ${commandSection}
248
398
  volumes:
249
399
  ${imageName}-history:
250
400
  `;
@@ -255,17 +405,31 @@ dist
255
405
  .git
256
406
  *.log
257
407
  `;
258
- async function generateFiles(ralphDir, language, imageName, force = false, javaVersion, cliProvider) {
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) {
259
422
  const dockerDir = join(ralphDir, DOCKER_DIR);
260
423
  // Create docker directory
261
424
  if (!existsSync(dockerDir)) {
262
425
  mkdirSync(dockerDir, { recursive: true });
263
426
  console.log(`Created ${DOCKER_DIR}/`);
264
427
  }
428
+ const customDomains = dockerConfig?.firewall?.allowedDomains || [];
265
429
  const files = [
266
- { name: "Dockerfile", content: generateDockerfile(language, javaVersion, cliProvider) },
267
- { name: "init-firewall.sh", content: FIREWALL_SCRIPT },
268
- { name: "docker-compose.yml", content: generateDockerCompose(imageName) },
430
+ { name: "Dockerfile", content: generateDockerfile(language, javaVersion, cliProvider, dockerConfig) },
431
+ { name: "init-firewall.sh", content: generateFirewallScript(customDomains) },
432
+ { name: "docker-compose.yml", content: generateDockerCompose(imageName, dockerConfig) },
269
433
  { name: ".dockerignore", content: DOCKERIGNORE },
270
434
  ];
271
435
  for (const file of files) {
@@ -283,6 +447,58 @@ async function generateFiles(ralphDir, language, imageName, force = false, javaV
283
447
  }
284
448
  console.log(`Created ${DOCKER_DIR}/${file.name}`);
285
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);
286
502
  }
287
503
  async function buildImage(ralphDir) {
288
504
  const dockerDir = join(ralphDir, DOCKER_DIR);
@@ -290,9 +506,17 @@ async function buildImage(ralphDir) {
290
506
  console.error("Dockerfile not found. Run 'ralph docker' first.");
291
507
  process.exit(1);
292
508
  }
293
- console.log("Building Docker image...\n");
294
- // Get image name for compose project name
509
+ // Get config and check for changes
295
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");
296
520
  const imageName = config.imageName || `ralph-${basename(process.cwd()).toLowerCase().replace(/[^a-z0-9-]/g, "-")}`;
297
521
  return new Promise((resolve, reject) => {
298
522
  // Use --no-cache and --pull to ensure we always get the latest CLI versions
@@ -356,15 +580,34 @@ function getCliProviderConfig(cliProvider) {
356
580
  modelConfig: provider.modelConfig,
357
581
  };
358
582
  }
359
- async function runContainer(ralphDir, imageName, language, javaVersion, cliProvider) {
583
+ async function runContainer(ralphDir, imageName, language, javaVersion, cliProvider, dockerConfig, claudeConfig) {
360
584
  const dockerDir = join(ralphDir, DOCKER_DIR);
361
585
  const dockerfileExists = existsSync(join(dockerDir, "Dockerfile"));
362
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
+ }
363
606
  // Auto-init and build if docker folder or image doesn't exist
364
607
  if (!dockerfileExists || !hasImage) {
365
608
  if (!dockerfileExists) {
366
609
  console.log("Docker folder not found. Initializing docker setup...\n");
367
- await generateFiles(ralphDir, language, imageName, true, javaVersion, cliProvider);
610
+ await generateFiles(ralphDir, language, imageName, true, javaVersion, cliProvider, dockerConfig, claudeConfig);
368
611
  console.log("");
369
612
  }
370
613
  if (!hasImage) {
@@ -612,7 +855,7 @@ export async function dockerInit(silent = false) {
612
855
  console.log(`CLI provider: ${config.cliProvider}`);
613
856
  }
614
857
  console.log(`Image name: ${imageName}\n`);
615
- await generateFiles(ralphDir, config.language, imageName, true, config.javaVersion, config.cliProvider);
858
+ await generateFiles(ralphDir, config.language, imageName, true, config.javaVersion, config.cliProvider, config.docker, config.claude);
616
859
  if (!silent) {
617
860
  console.log(`
618
861
  Docker files generated in .ralph/docker/
@@ -639,6 +882,7 @@ USAGE:
639
882
  ralph docker init -y Generate files, overwrite without prompting
640
883
  ralph docker build Build image (fetches latest CLI versions)
641
884
  ralph docker build --clean Clean existing image and rebuild from scratch
885
+ (alias: --no-cache)
642
886
  ralph docker run Run container (auto-init and build if needed)
643
887
  ralph docker clean Remove Docker image and associated resources
644
888
  ralph docker help Show this help message
@@ -693,14 +937,15 @@ INSTALLING PACKAGES (works with Docker & Podman):
693
937
  switch (subcommand) {
694
938
  case "build":
695
939
  // Handle build --clean combination: clean first, then build
696
- if (hasFlag("--clean")) {
940
+ // Also support --no-cache as alias for --clean
941
+ if (hasFlag("--clean") || hasFlag("--no-cache") || hasFlag("-no-cache")) {
697
942
  await cleanImage(imageName, ralphDir);
698
943
  console.log(""); // Add spacing between clean and build output
699
944
  }
700
945
  await buildImage(ralphDir);
701
946
  break;
702
947
  case "run":
703
- await runContainer(ralphDir, imageName, config.language, config.javaVersion, config.cliProvider);
948
+ await runContainer(ralphDir, imageName, config.language, config.javaVersion, config.cliProvider, config.docker, config.claude);
704
949
  break;
705
950
  case "clean":
706
951
  await cleanImage(imageName, ralphDir);
@@ -719,7 +964,7 @@ INSTALLING PACKAGES (works with Docker & Podman):
719
964
  console.log(`CLI provider: ${config.cliProvider}`);
720
965
  }
721
966
  console.log(`Image name: ${imageName}\n`);
722
- await generateFiles(ralphDir, config.language, imageName, force, config.javaVersion, config.cliProvider);
967
+ await generateFiles(ralphDir, config.language, imageName, force, config.javaVersion, config.cliProvider, config.docker, config.claude);
723
968
  console.log(`
724
969
  Docker files generated in .ralph/docker/
725
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>;