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.
- package/README.md +29 -66
- package/dist/commands/docker.js +329 -25
- package/dist/commands/help.js +1 -0
- package/dist/commands/init.d.ts +1 -1
- package/dist/commands/init.js +156 -72
- package/dist/commands/once.js +251 -13
- package/dist/commands/run.js +233 -5
- package/dist/config/languages.json +1 -1
- package/dist/config/skills.json +12 -0
- package/dist/templates/prompts.d.ts +11 -0
- package/dist/templates/prompts.js +17 -0
- package/dist/utils/config.d.ts +35 -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/dist/commands/run.js
CHANGED
|
@@ -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
|
-
|
|
105
|
-
|
|
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
|
|
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();
|
package/dist/utils/config.d.ts
CHANGED
|
@@ -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;
|
package/dist/utils/prompt.d.ts
CHANGED
|
@@ -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[]>;
|
package/dist/utils/prompt.js
CHANGED
|
@@ -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}
|
|
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)
|