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 +29 -66
- package/dist/commands/docker.js +228 -25
- package/dist/commands/help.js +1 -0
- package/dist/commands/init.d.ts +1 -1
- package/dist/commands/init.js +127 -71
- package/dist/config/languages.json +1 -1
- package/dist/utils/config.d.ts +30 -0
- package/dist/utils/prompt.d.ts +1 -1
- package/dist/utils/prompt.js +8 -2
- package/docs/DEVELOPMENT.md +161 -0
- package/docs/DOCKER.md +225 -0
- package/docs/HOW-TO-WRITE-PRDs.md +4 -2
- package/docs/PRD-GENERATOR.md +2 -1
- package/docs/SECURITY.md +78 -0
- package/docs/run-state-machine.md +73 -64
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -85,12 +85,28 @@ After running `ralph init`, you'll have:
|
|
|
85
85
|
|
|
86
86
|
### Supported Languages
|
|
87
87
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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: `
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
package/dist/commands/docker.js
CHANGED
|
@@ -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
|
-
|
|
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:
|
|
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
|
|
261
|
-
.
|
|
262
|
-
|
|
263
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
package/dist/commands/help.js
CHANGED
|
@@ -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
|
package/dist/commands/init.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare function init(
|
|
1
|
+
export declare function init(args: string[]): Promise<void>;
|