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 +29 -66
- package/dist/commands/docker.js +279 -34
- package/dist/commands/help.js +1 -0
- package/dist/commands/init.d.ts +1 -1
- package/dist/commands/init.js +127 -71
- package/dist/commands/run.js +34 -13
- package/dist/config/languages.json +243 -2
- package/dist/utils/config.d.ts +39 -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();
|
|
@@ -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
|
-
|
|
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:
|
|
324
|
+
echo "Allowed: ${allowedList}"
|
|
220
325
|
`;
|
|
221
|
-
|
|
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
|
-
|
|
233
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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>;
|