ralph-cli-sandboxed 0.2.5 → 0.2.7

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.
@@ -1,9 +1,140 @@
1
1
  import { spawn } from "child_process";
2
- import { existsSync, readFileSync, writeFileSync, unlinkSync } from "fs";
2
+ import { existsSync, readFileSync, writeFileSync, unlinkSync, appendFileSync, mkdirSync } from "fs";
3
3
  import { join } from "path";
4
4
  import { checkFilesExist, loadConfig, loadPrompt, getPaths, getCliConfig, requireContainer } from "../utils/config.js";
5
5
  import { resolvePromptVariables } from "../templates/prompts.js";
6
6
  import { validatePrd, smartMerge, readPrdFile, writePrd, expandPrdFileReferences } from "../utils/prd-validator.js";
7
+ /**
8
+ * Parses a stream-json line and extracts displayable text.
9
+ * Formats output similar to Claude Code's normal terminal display.
10
+ */
11
+ function parseStreamJsonLine(line, debug = false) {
12
+ try {
13
+ const json = JSON.parse(line);
14
+ if (debug && json.type) {
15
+ process.stderr.write(`[stream-json] type: ${json.type}\n`);
16
+ }
17
+ // Handle Claude Code CLI stream-json events
18
+ const type = json.type;
19
+ switch (type) {
20
+ // === Text Content ===
21
+ case "content_block_delta":
22
+ // Incremental text updates - the main streaming content
23
+ if (json.delta?.type === "text_delta") {
24
+ return json.delta.text || "";
25
+ }
26
+ // Tool input being streamed
27
+ if (json.delta?.type === "input_json_delta") {
28
+ return ""; // Don't show partial JSON, wait for complete tool call
29
+ }
30
+ return json.delta?.text || "";
31
+ case "text":
32
+ return json.text || "";
33
+ // === Tool Use ===
34
+ case "content_block_start":
35
+ if (json.content_block?.type === "tool_use") {
36
+ const toolName = json.content_block?.name || "unknown";
37
+ return `\n── Tool: ${toolName} ──\n`;
38
+ }
39
+ if (json.content_block?.type === "text") {
40
+ return json.content_block?.text || "";
41
+ }
42
+ return "";
43
+ case "content_block_stop":
44
+ // End of a content block - add newline after tool use
45
+ return "";
46
+ // === Tool Results ===
47
+ case "tool_result":
48
+ const toolOutput = json.content || json.output || "";
49
+ const truncated = typeof toolOutput === "string" && toolOutput.length > 500
50
+ ? toolOutput.substring(0, 500) + "... (truncated)"
51
+ : toolOutput;
52
+ return `\n── Tool Result ──\n${typeof truncated === "string" ? truncated : JSON.stringify(truncated, null, 2)}\n`;
53
+ // === Assistant Messages ===
54
+ case "assistant":
55
+ const contents = json.message?.content || json.content || [];
56
+ let output = "";
57
+ for (const block of contents) {
58
+ if (block.type === "text") {
59
+ output += block.text || "";
60
+ }
61
+ else if (block.type === "tool_use") {
62
+ output += `\n── Tool: ${block.name} ──\n`;
63
+ if (block.input) {
64
+ output += JSON.stringify(block.input, null, 2) + "\n";
65
+ }
66
+ }
67
+ }
68
+ return output;
69
+ case "message_start":
70
+ // Beginning of a new message
71
+ return "\n";
72
+ case "message_delta":
73
+ // Message completion info (stop_reason, usage)
74
+ if (json.delta?.stop_reason) {
75
+ return `\n[${json.delta.stop_reason}]\n`;
76
+ }
77
+ return "";
78
+ case "message_stop":
79
+ return "\n";
80
+ // === System/User Events ===
81
+ case "system":
82
+ if (json.message) {
83
+ return `[System] ${json.message}\n`;
84
+ }
85
+ return "";
86
+ case "user":
87
+ // User message echo - usually not needed to display
88
+ return "";
89
+ // === Results and Errors ===
90
+ case "result":
91
+ if (json.result !== undefined) {
92
+ return `\n── Result ──\n${JSON.stringify(json.result, null, 2)}\n`;
93
+ }
94
+ return "";
95
+ case "error":
96
+ const errMsg = json.error?.message || JSON.stringify(json.error);
97
+ return `\n[Error] ${errMsg}\n`;
98
+ // === File Operations (Claude Code specific) ===
99
+ case "file_edit":
100
+ case "file_write":
101
+ const filePath = json.path || json.file || "unknown";
102
+ return `\n── Writing: ${filePath} ──\n`;
103
+ case "file_read":
104
+ const readPath = json.path || json.file || "unknown";
105
+ return `── Reading: ${readPath} ──\n`;
106
+ case "bash":
107
+ case "command":
108
+ const cmd = json.command || json.content || "";
109
+ return `\n── Running: ${cmd} ──\n`;
110
+ case "bash_output":
111
+ case "command_output":
112
+ const cmdOutput = json.output || json.content || "";
113
+ return cmdOutput + "\n";
114
+ default:
115
+ // Fallback: check for common text fields
116
+ if (json.text)
117
+ return json.text;
118
+ if (json.content && typeof json.content === "string")
119
+ return json.content;
120
+ if (json.message && typeof json.message === "string")
121
+ return json.message;
122
+ if (json.output && typeof json.output === "string")
123
+ return json.output;
124
+ if (debug) {
125
+ process.stderr.write(`[stream-json] unhandled type: ${type}, keys: ${Object.keys(json).join(", ")}\n`);
126
+ }
127
+ return "";
128
+ }
129
+ }
130
+ catch (e) {
131
+ // Not valid JSON
132
+ if (debug) {
133
+ process.stderr.write(`[stream-json] parse error: ${e}\n`);
134
+ }
135
+ return "";
136
+ }
137
+ }
7
138
  const CATEGORIES = ["ui", "feature", "bugfix", "setup", "development", "testing", "docs"];
8
139
  /**
9
140
  * Creates a filtered PRD file containing only incomplete items (passes: false).
@@ -71,9 +202,11 @@ function syncPassesFromTasks(tasksPath, prdPath) {
71
202
  return 0;
72
203
  }
73
204
  }
74
- async function runIteration(prompt, paths, sandboxed, filteredPrdPath, cliConfig, debug, model) {
205
+ async function runIteration(prompt, paths, sandboxed, filteredPrdPath, cliConfig, debug, model, streamJson) {
75
206
  return new Promise((resolve, reject) => {
76
207
  let output = "";
208
+ let jsonLogPath;
209
+ let lineBuffer = ""; // Buffer for incomplete JSON lines
77
210
  // Build CLI arguments: config args + yolo args + model args + prompt args
78
211
  const cliArgs = [
79
212
  ...(cliConfig.args ?? []),
@@ -84,6 +217,19 @@ async function runIteration(prompt, paths, sandboxed, filteredPrdPath, cliConfig
84
217
  const yoloArgs = cliConfig.yoloArgs ?? ["--dangerously-skip-permissions"];
85
218
  cliArgs.push(...yoloArgs);
86
219
  }
220
+ // Add stream-json output format if enabled
221
+ if (streamJson?.enabled) {
222
+ cliArgs.push("--output-format", "stream-json", "--verbose", "--print");
223
+ // Setup JSON log file if saving raw JSON
224
+ if (streamJson.saveRawJson) {
225
+ const outputDir = join(process.cwd(), streamJson.outputDir);
226
+ if (!existsSync(outputDir)) {
227
+ mkdirSync(outputDir, { recursive: true });
228
+ }
229
+ const timestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
230
+ jsonLogPath = join(outputDir, `ralph-run-${timestamp}.jsonl`);
231
+ }
232
+ }
87
233
  // Add model args if model is specified
88
234
  if (model && cliConfig.modelArgs) {
89
235
  cliArgs.push(...cliConfig.modelArgs, model);
@@ -95,16 +241,85 @@ async function runIteration(prompt, paths, sandboxed, filteredPrdPath, cliConfig
95
241
  cliArgs.push(...promptArgs, promptValue);
96
242
  if (debug) {
97
243
  console.log(`[debug] ${cliConfig.command} ${cliArgs.map(a => a.includes(" ") ? `"${a}"` : a).join(" ")}\n`);
244
+ if (jsonLogPath) {
245
+ console.log(`[debug] Saving raw JSON to: ${jsonLogPath}\n`);
246
+ }
98
247
  }
99
248
  const proc = spawn(cliConfig.command, cliArgs, {
100
249
  stdio: ["inherit", "pipe", "inherit"],
101
250
  });
102
251
  proc.stdout.on("data", (data) => {
103
252
  const chunk = data.toString();
104
- output += chunk;
105
- process.stdout.write(chunk);
253
+ if (streamJson?.enabled) {
254
+ // Process stream-json output: parse JSON and display clean text
255
+ lineBuffer += chunk;
256
+ const lines = lineBuffer.split("\n");
257
+ // Keep the last incomplete line in the buffer
258
+ lineBuffer = lines.pop() || "";
259
+ for (const line of lines) {
260
+ const trimmedLine = line.trim();
261
+ if (!trimmedLine)
262
+ continue;
263
+ // Check if this is a JSON line
264
+ if (trimmedLine.startsWith("{")) {
265
+ // Save raw JSON if enabled
266
+ if (jsonLogPath) {
267
+ try {
268
+ appendFileSync(jsonLogPath, trimmedLine + "\n");
269
+ }
270
+ catch {
271
+ // Ignore write errors
272
+ }
273
+ }
274
+ // Parse and display clean text
275
+ const text = parseStreamJsonLine(trimmedLine, debug);
276
+ if (text) {
277
+ process.stdout.write(text);
278
+ output += text; // Accumulate parsed text for completion detection
279
+ }
280
+ }
281
+ else {
282
+ // Non-JSON line - display as-is (might be status messages, errors, etc.)
283
+ process.stdout.write(trimmedLine + "\n");
284
+ output += trimmedLine + "\n";
285
+ }
286
+ }
287
+ }
288
+ else {
289
+ // Standard output: pass through as-is
290
+ output += chunk;
291
+ process.stdout.write(chunk);
292
+ }
106
293
  });
107
294
  proc.on("close", (code) => {
295
+ // Process any remaining buffered content
296
+ if (streamJson?.enabled && lineBuffer.trim()) {
297
+ const trimmedLine = lineBuffer.trim();
298
+ if (trimmedLine.startsWith("{")) {
299
+ if (jsonLogPath) {
300
+ try {
301
+ appendFileSync(jsonLogPath, trimmedLine + "\n");
302
+ }
303
+ catch {
304
+ // Ignore write errors
305
+ }
306
+ }
307
+ const text = parseStreamJsonLine(trimmedLine, debug);
308
+ if (text) {
309
+ process.stdout.write(text);
310
+ output += text;
311
+ }
312
+ }
313
+ else {
314
+ // Non-JSON remaining content
315
+ process.stdout.write(trimmedLine + "\n");
316
+ output += trimmedLine + "\n";
317
+ }
318
+ }
319
+ // Ensure final newline for clean output
320
+ if (streamJson?.enabled) {
321
+ process.stdout.write("\n");
322
+ }
108
323
  resolve({ exitCode: code ?? 0, output });
109
324
  });
110
325
  proc.on("error", (err) => {
@@ -265,6 +480,13 @@ export async function run(args) {
265
480
  });
266
481
  const paths = getPaths();
267
482
  const cliConfig = getCliConfig(config);
483
+ // Check if stream-json output is enabled
484
+ const streamJsonConfig = config.docker?.asciinema?.streamJson;
485
+ const streamJson = streamJsonConfig?.enabled ? {
486
+ enabled: true,
487
+ saveRawJson: streamJsonConfig.saveRawJson !== false, // default true
488
+ outputDir: config.docker?.asciinema?.outputDir || ".recordings",
489
+ } : undefined;
268
490
  // Progress tracking: stop only if no tasks complete after N iterations
269
491
  const MAX_ITERATIONS_WITHOUT_PROGRESS = 3;
270
492
  // Get requested iteration count (may be adjusted dynamically)
@@ -285,6 +507,12 @@ export async function run(args) {
285
507
  if (category) {
286
508
  console.log(`Filtering PRD items by category: ${category}`);
287
509
  }
510
+ if (streamJson?.enabled) {
511
+ console.log("Stream JSON output enabled - displaying formatted Claude output");
512
+ if (streamJson.saveRawJson) {
513
+ console.log(`Raw JSON logs will be saved to: ${streamJson.outputDir}/`);
514
+ }
515
+ }
288
516
  console.log();
289
517
  // Track temp file for cleanup
290
518
  let filteredPrdPath = null;
@@ -381,7 +609,7 @@ export async function run(args) {
381
609
  break;
382
610
  }
383
611
  }
384
- const { exitCode, output } = await runIteration(prompt, paths, sandboxed, filteredPrdPath, cliConfig, debug, model);
612
+ const { exitCode, output } = await runIteration(prompt, paths, sandboxed, filteredPrdPath, cliConfig, debug, model, streamJson);
385
613
  // Sync any completed items from prd-tasks.json back to prd.json
386
614
  // This catches cases where the LLM updated prd-tasks.json instead of prd.json
387
615
  syncPassesFromTasks(filteredPrdPath, paths.prd);
@@ -259,7 +259,7 @@
259
259
  "checkCommand": "swift build",
260
260
  "testCommand": "swift test",
261
261
  "docker": {
262
- "install": "# Install Swift toolchain\nRUN apt-get update && apt-get install -y \\\n binutils \\\n git \\\n gnupg2 \\\n libc6-dev \\\n libcurl4-openssl-dev \\\n libedit2 \\\n libgcc-11-dev \\\n libpython3-dev \\\n libsqlite3-0 \\\n libstdc++-11-dev \\\n libxml2-dev \\\n libz3-dev \\\n pkg-config \\\n tzdata \\\n unzip \\\n zlib1g-dev \\\n && rm -rf /var/lib/apt/lists/*\nRUN ARCH=$(dpkg --print-architecture) && \\\n if [ \"$ARCH\" = \"amd64\" ]; then SWIFT_ARCH=\"x86_64\"; else SWIFT_ARCH=\"aarch64\"; fi && \\\n curl -fsSL https://download.swift.org/swift-5.10-release/ubuntu2204/swift-5.10-RELEASE/swift-5.10-RELEASE-ubuntu22.04-${SWIFT_ARCH}.tar.gz | tar -xz -C /opt\nENV PATH=\"/opt/swift-5.10-RELEASE-ubuntu22.04/usr/bin:$PATH\""
262
+ "install": "# Install Swift toolchain\nRUN apt-get update && apt-get install -y \\\n binutils \\\n git \\\n gnupg2 \\\n libc6-dev \\\n libcurl4-openssl-dev \\\n libedit2 \\\n libgcc-11-dev \\\n libpython3-dev \\\n libsqlite3-0 \\\n libstdc++-11-dev \\\n libxml2-dev \\\n libz3-dev \\\n pkg-config \\\n tzdata \\\n unzip \\\n zlib1g-dev \\\n && rm -rf /var/lib/apt/lists/*\nRUN ARCH=$(dpkg --print-architecture) && \\\n if [ \"$ARCH\" = \"amd64\" ]; then \\\n SWIFT_PLATFORM=\"ubuntu2204\"; \\\n SWIFT_FILE=\"swift-5.10-RELEASE-ubuntu22.04\"; \\\n else \\\n SWIFT_PLATFORM=\"ubuntu2204-aarch64\"; \\\n SWIFT_FILE=\"swift-5.10-RELEASE-ubuntu22.04-aarch64\"; \\\n fi && \\\n curl -fsSL https://download.swift.org/swift-5.10-release/${SWIFT_PLATFORM}/swift-5.10-RELEASE/${SWIFT_FILE}.tar.gz | tar -xz -C /opt && \\\n mv /opt/${SWIFT_FILE} /opt/swift\nENV PATH=\"/opt/swift/usr/bin:$PATH\""
263
263
  },
264
264
  "technologies": [
265
265
  { "name": "Vapor", "description": "Server-side Swift web framework" },
@@ -0,0 +1,12 @@
1
+ {
2
+ "skills": {
3
+ "swift": [
4
+ {
5
+ "name": "swift-main-naming",
6
+ "description": "Prevents naming files main.swift when using @main attribute",
7
+ "instructions": "IMPORTANT: In Swift, files containing the @main attribute MUST NOT be named main.swift.\n\nWhen the @main attribute is used (e.g., @main struct App), Swift automatically generates an entry point. If the file is also named main.swift, Swift treats it as having a manual entry point, causing a conflict.\n\nRULES:\n- Never name a file main.swift if it contains @main attribute\n- Use descriptive names like App.swift, MyApp.swift, or the actual type name\n- If you encounter a main.swift with @main, rename it to match the type (e.g., struct MyApp -> MyApp.swift)\n\nEXAMPLES:\n\nBAD:\n```\n// main.swift\n@main\nstruct App {\n static func main() { ... }\n}\n```\n\nGOOD:\n```\n// App.swift\n@main\nstruct App {\n static func main() { ... }\n}\n```",
8
+ "userInvocable": false
9
+ }
10
+ ]
11
+ }
12
+ }
@@ -48,9 +48,20 @@ export interface CliProviderConfig {
48
48
  interface CliProvidersJson {
49
49
  providers: Record<string, CliProviderConfig>;
50
50
  }
51
+ export interface SkillDefinition {
52
+ name: string;
53
+ description: string;
54
+ instructions: string;
55
+ userInvocable?: boolean;
56
+ }
57
+ interface SkillsJson {
58
+ skills: Record<string, SkillDefinition[]>;
59
+ }
51
60
  export declare function getLanguagesJson(): LanguagesJson;
52
61
  export declare function getCliProvidersJson(): CliProvidersJson;
53
62
  export declare function getCliProviders(): Record<string, CliProviderConfig>;
63
+ export declare function getSkillsJson(): SkillsJson;
64
+ export declare function getSkillsForLanguage(language: string): SkillDefinition[];
54
65
  export declare function getLanguages(): Record<string, LanguageConfig>;
55
66
  export declare const LANGUAGES: Record<string, LanguageConfig>;
56
67
  export declare function generatePromptTemplate(): string;
@@ -17,6 +17,12 @@ function loadCliProvidersConfig() {
17
17
  const content = readFileSync(configPath, "utf-8");
18
18
  return JSON.parse(content);
19
19
  }
20
+ // Load skills from JSON config file
21
+ function loadSkillsConfig() {
22
+ const configPath = join(__dirname, "..", "config", "skills.json");
23
+ const content = readFileSync(configPath, "utf-8");
24
+ return JSON.parse(content);
25
+ }
20
26
  // Convert JSON config to the legacy format for compatibility
21
27
  function convertToLanguageConfig(config) {
22
28
  return {
@@ -31,6 +37,7 @@ function convertToLanguageConfig(config) {
31
37
  let _languagesCache = null;
32
38
  let _languagesJsonCache = null;
33
39
  let _cliProvidersCache = null;
40
+ let _skillsCache = null;
34
41
  export function getLanguagesJson() {
35
42
  if (!_languagesJsonCache) {
36
43
  _languagesJsonCache = loadLanguagesConfig();
@@ -46,6 +53,16 @@ export function getCliProvidersJson() {
46
53
  export function getCliProviders() {
47
54
  return getCliProvidersJson().providers;
48
55
  }
56
+ export function getSkillsJson() {
57
+ if (!_skillsCache) {
58
+ _skillsCache = loadSkillsConfig();
59
+ }
60
+ return _skillsCache;
61
+ }
62
+ export function getSkillsForLanguage(language) {
63
+ const skills = getSkillsJson().skills;
64
+ return skills[language] || [];
65
+ }
49
66
  export function getLanguages() {
50
67
  if (!_languagesCache) {
51
68
  const json = getLanguagesJson();
@@ -5,6 +5,27 @@ export interface CliConfig {
5
5
  promptArgs?: string[];
6
6
  modelArgs?: string[];
7
7
  }
8
+ export interface McpServerConfig {
9
+ command: string;
10
+ args?: string[];
11
+ env?: Record<string, string>;
12
+ }
13
+ export interface SkillConfig {
14
+ name: string;
15
+ description: string;
16
+ instructions: string;
17
+ userInvocable?: boolean;
18
+ }
19
+ export interface StreamJsonConfig {
20
+ enabled: boolean;
21
+ saveRawJson?: boolean;
22
+ }
23
+ export interface AsciinemaConfig {
24
+ enabled: boolean;
25
+ autoRecord?: boolean;
26
+ outputDir?: string;
27
+ streamJson?: StreamJsonConfig;
28
+ }
8
29
  export interface RalphConfig {
9
30
  language: string;
10
31
  checkCommand: string;
@@ -23,6 +44,20 @@ export interface RalphConfig {
23
44
  name?: string;
24
45
  email?: string;
25
46
  };
47
+ packages?: string[];
48
+ buildCommands?: {
49
+ root?: string[];
50
+ node?: string[];
51
+ };
52
+ startCommand?: string;
53
+ asciinema?: AsciinemaConfig;
54
+ firewall?: {
55
+ allowedDomains?: string[];
56
+ };
57
+ };
58
+ claude?: {
59
+ mcpServers?: Record<string, McpServerConfig>;
60
+ skills?: SkillConfig[];
26
61
  };
27
62
  }
28
63
  export declare const DEFAULT_CLI_CONFIG: CliConfig;
@@ -5,6 +5,6 @@ export declare function createPrompt(): {
5
5
  export declare function promptSelectWithArrows(message: string, options: string[]): Promise<string>;
6
6
  export declare function promptInput(message: string): Promise<string>;
7
7
  export declare function promptSelect(message: string, options: string[]): Promise<string>;
8
- export declare function promptConfirm(message: string): Promise<boolean>;
8
+ export declare function promptConfirm(message: string, defaultValue?: boolean): Promise<boolean>;
9
9
  export declare function promptMultiSelect(message: string, options: string[]): Promise<string[]>;
10
10
  export declare function promptMultiSelectWithArrows(message: string, options: string[]): Promise<string[]>;
@@ -96,11 +96,17 @@ export async function promptSelect(message, options) {
96
96
  console.log("Invalid selection.");
97
97
  }
98
98
  }
99
- export async function promptConfirm(message) {
99
+ export async function promptConfirm(message, defaultValue = true) {
100
100
  const prompt = createPrompt();
101
+ const hint = defaultValue ? "(Y/n)" : "(y/N)";
101
102
  while (true) {
102
- const answer = await prompt.question(`${message} (y/n): `);
103
+ const answer = await prompt.question(`${message} ${hint}: `);
103
104
  const normalized = answer.trim().toLowerCase();
105
+ // Empty input returns default
106
+ if (normalized === "") {
107
+ prompt.close();
108
+ return defaultValue;
109
+ }
104
110
  if (normalized === "y" || normalized === "yes") {
105
111
  prompt.close();
106
112
  return true;
@@ -0,0 +1,161 @@
1
+ # Development
2
+
3
+ Guide for contributing to ralph-cli-sandboxed.
4
+
5
+ ## Setup
6
+
7
+ ```bash
8
+ # Clone the repository
9
+ git clone https://github.com/choas/ralph-cli-sandboxed
10
+ cd ralph-cli-sandboxed
11
+
12
+ # Install dependencies
13
+ npm install
14
+ ```
15
+
16
+ ## Development Mode
17
+
18
+ Run ralph directly from TypeScript source without building:
19
+
20
+ ```bash
21
+ npm run dev -- <args>
22
+
23
+ # Examples:
24
+ npm run dev -- --version
25
+ npm run dev -- list
26
+ npm run dev -- once
27
+ npm run dev -- help
28
+ ```
29
+
30
+ This uses `tsx` to run TypeScript directly, allowing you to test changes immediately.
31
+
32
+ ## Building
33
+
34
+ ```bash
35
+ # Build for distribution
36
+ npm run build
37
+
38
+ # This runs:
39
+ # 1. tsc - Compiles TypeScript to dist/
40
+ # 2. Copies config files to dist/config/
41
+ ```
42
+
43
+ ## Project Structure
44
+
45
+ ```
46
+ ralph-cli-sandboxed/
47
+ ├── src/
48
+ │ ├── index.ts # CLI entry point
49
+ │ ├── commands/ # Command implementations
50
+ │ │ ├── init.ts # ralph init
51
+ │ │ ├── run.ts # ralph run
52
+ │ │ ├── once.ts # ralph once
53
+ │ │ ├── prd.ts # PRD management commands
54
+ │ │ ├── docker.ts # Docker commands
55
+ │ │ ├── prompt.ts # ralph prompt
56
+ │ │ ├── fix-prd.ts # ralph fix-prd
57
+ │ │ └── help.ts # ralph help
58
+ │ ├── utils/
59
+ │ │ ├── config.ts # Configuration loading
60
+ │ │ ├── prd-validator.ts # PRD validation and recovery
61
+ │ │ └── prompt.ts # Interactive prompts
62
+ │ ├── templates/
63
+ │ │ └── prompts.ts # Prompt template generation
64
+ │ └── config/
65
+ │ ├── languages.json # Language configurations
66
+ │ └── cli-providers.json # CLI provider configurations
67
+ ├── docs/ # Documentation
68
+ ├── dist/ # Compiled output (generated)
69
+ └── package.json
70
+ ```
71
+
72
+ ## Adding a New Language
73
+
74
+ Edit `src/config/languages.json`:
75
+
76
+ ```json
77
+ {
78
+ "languages": {
79
+ "your-language": {
80
+ "name": "Your Language",
81
+ "description": "Description here",
82
+ "checkCommand": "your-check-command",
83
+ "testCommand": "your-test-command",
84
+ "docker": {
85
+ "install": "# Installation commands for Dockerfile"
86
+ },
87
+ "technologies": [
88
+ { "name": "Framework", "description": "Description" }
89
+ ]
90
+ }
91
+ }
92
+ }
93
+ ```
94
+
95
+ ## Adding a New CLI Provider
96
+
97
+ Edit `src/config/cli-providers.json`:
98
+
99
+ ```json
100
+ {
101
+ "providers": {
102
+ "your-cli": {
103
+ "name": "Your CLI",
104
+ "description": "Description",
105
+ "command": "cli-command",
106
+ "defaultArgs": [],
107
+ "yoloArgs": ["--auto-approve-flag"],
108
+ "promptArgs": ["--prompt"],
109
+ "docker": {
110
+ "install": "# Installation commands"
111
+ },
112
+ "envVars": ["YOUR_API_KEY"],
113
+ "modelArgs": ["--model"]
114
+ }
115
+ }
116
+ }
117
+ ```
118
+
119
+ ## Testing Changes
120
+
121
+ Since ralph automates AI agents, testing requires caution:
122
+
123
+ 1. **Use a test project** - Create a sample project to test changes
124
+ 2. **Use `ralph once`** - Run single iterations for testing
125
+ 3. **Check output** - Review `.ralph/progress.txt` and git commits
126
+
127
+ ## Platform-Specific Dependencies
128
+
129
+ The `node_modules` folder contains platform-specific binaries. If you switch between environments:
130
+
131
+ ```bash
132
+ # When switching between host and container
133
+ rm -rf node_modules && npm install
134
+ ```
135
+
136
+ Or use a separate volume for container node_modules:
137
+
138
+ ```bash
139
+ docker run -v $(pwd):/workspace -v /workspace/node_modules your-image
140
+ ```
141
+
142
+ ## Code Style
143
+
144
+ - TypeScript with ES2022 target
145
+ - Node.js 18+ required
146
+ - Use async/await for asynchronous operations
147
+ - Keep functions focused and small
148
+
149
+ ## Submitting Changes
150
+
151
+ 1. Fork the repository
152
+ 2. Create a feature branch
153
+ 3. Make your changes
154
+ 4. Test thoroughly
155
+ 5. Submit a pull request
156
+
157
+ ## Requirements
158
+
159
+ - Node.js 18+
160
+ - npm
161
+ - Docker (for testing container functionality)