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/dist/commands/init.js
CHANGED
|
@@ -14,118 +14,174 @@ const PROMPT_FILE = "prompt.md";
|
|
|
14
14
|
const PRD_FILE = "prd.json";
|
|
15
15
|
const PROGRESS_FILE = "progress.txt";
|
|
16
16
|
const PRD_GUIDE_FILE = "HOW-TO-WRITE-PRDs.md";
|
|
17
|
-
export async function init(
|
|
17
|
+
export async function init(args) {
|
|
18
18
|
const cwd = process.cwd();
|
|
19
19
|
const ralphDir = join(cwd, RALPH_DIR);
|
|
20
|
+
const useDefaults = args.includes("-y") || args.includes("--yes");
|
|
20
21
|
console.log("Initializing ralph in current directory...\n");
|
|
21
22
|
// Check for existing .ralph directory
|
|
22
23
|
if (existsSync(ralphDir)) {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
if (!useDefaults) {
|
|
25
|
+
const reinit = await promptConfirm(".ralph/ directory already exists. Re-initialize?");
|
|
26
|
+
if (!reinit) {
|
|
27
|
+
console.log("Aborted.");
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
27
30
|
}
|
|
28
31
|
}
|
|
29
32
|
else {
|
|
30
33
|
mkdirSync(ralphDir, { recursive: true });
|
|
31
34
|
console.log(`Created ${RALPH_DIR}/`);
|
|
32
35
|
}
|
|
33
|
-
// Step 1: Select CLI provider (first)
|
|
34
36
|
const CLI_PROVIDERS = getCliProviders();
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
const selectedProviderName = await promptSelectWithArrows("Select your AI CLI provider:", providerNames);
|
|
38
|
-
const selectedProviderIndex = providerNames.indexOf(selectedProviderName);
|
|
39
|
-
const selectedCliProviderKey = providerKeys[selectedProviderIndex];
|
|
40
|
-
const selectedProvider = CLI_PROVIDERS[selectedCliProviderKey];
|
|
37
|
+
const LANGUAGES = getLanguages();
|
|
38
|
+
let selectedCliProviderKey;
|
|
41
39
|
let cliConfig;
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
const
|
|
50
|
-
const customPromptArgs = customPromptArgsInput.trim() ? customPromptArgsInput.trim().split(/\s+/) : [];
|
|
40
|
+
let selectedKey;
|
|
41
|
+
let selectedTechnologies = [];
|
|
42
|
+
let checkCommand;
|
|
43
|
+
let testCommand;
|
|
44
|
+
if (useDefaults) {
|
|
45
|
+
// Use defaults: Claude CLI + Node.js
|
|
46
|
+
selectedCliProviderKey = "claude";
|
|
47
|
+
const provider = CLI_PROVIDERS[selectedCliProviderKey];
|
|
51
48
|
cliConfig = {
|
|
52
|
-
command:
|
|
53
|
-
args:
|
|
54
|
-
yoloArgs:
|
|
55
|
-
promptArgs:
|
|
49
|
+
command: provider.command,
|
|
50
|
+
args: provider.defaultArgs,
|
|
51
|
+
yoloArgs: provider.yoloArgs.length > 0 ? provider.yoloArgs : undefined,
|
|
52
|
+
promptArgs: provider.promptArgs ?? [],
|
|
56
53
|
};
|
|
54
|
+
selectedKey = "node";
|
|
55
|
+
const config = LANGUAGES[selectedKey];
|
|
56
|
+
checkCommand = config.checkCommand;
|
|
57
|
+
testCommand = config.testCommand;
|
|
58
|
+
console.log(`Using defaults: ${CLI_PROVIDERS[selectedCliProviderKey].name} + ${LANGUAGES[selectedKey].name}`);
|
|
57
59
|
}
|
|
58
60
|
else {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
selectedTechnologies = await promptMultiSelectWithArrows("Select your technology stack (optional):", techOptions);
|
|
82
|
-
// Convert display names back to just technology names for predefined options
|
|
83
|
-
selectedTechnologies = selectedTechnologies.map(sel => {
|
|
84
|
-
const idx = techOptions.indexOf(sel);
|
|
85
|
-
return idx >= 0 ? techNames[idx] : sel;
|
|
86
|
-
});
|
|
87
|
-
if (selectedTechnologies.length > 0) {
|
|
88
|
-
console.log(`\nSelected technologies: ${selectedTechnologies.join(", ")}`);
|
|
61
|
+
// Step 1: Select CLI provider (first)
|
|
62
|
+
const providerKeys = Object.keys(CLI_PROVIDERS);
|
|
63
|
+
const providerNames = providerKeys.map(k => `${CLI_PROVIDERS[k].name} - ${CLI_PROVIDERS[k].description}`);
|
|
64
|
+
const selectedProviderName = await promptSelectWithArrows("Select your AI CLI provider:", providerNames);
|
|
65
|
+
const selectedProviderIndex = providerNames.indexOf(selectedProviderName);
|
|
66
|
+
selectedCliProviderKey = providerKeys[selectedProviderIndex];
|
|
67
|
+
const selectedProvider = CLI_PROVIDERS[selectedCliProviderKey];
|
|
68
|
+
// Handle custom CLI provider
|
|
69
|
+
if (selectedCliProviderKey === "custom") {
|
|
70
|
+
const customCommand = await promptInput("\nEnter your CLI command: ");
|
|
71
|
+
const customArgsInput = await promptInput("Enter default arguments (space-separated): ");
|
|
72
|
+
const customArgs = customArgsInput.trim() ? customArgsInput.trim().split(/\s+/) : [];
|
|
73
|
+
const customYoloArgsInput = await promptInput("Enter yolo/auto-approve arguments (space-separated): ");
|
|
74
|
+
const customYoloArgs = customYoloArgsInput.trim() ? customYoloArgsInput.trim().split(/\s+/) : [];
|
|
75
|
+
const customPromptArgsInput = await promptInput("Enter prompt arguments (e.g., -p for flag-based, leave empty for positional): ");
|
|
76
|
+
const customPromptArgs = customPromptArgsInput.trim() ? customPromptArgsInput.trim().split(/\s+/) : [];
|
|
77
|
+
cliConfig = {
|
|
78
|
+
command: customCommand || "claude",
|
|
79
|
+
args: customArgs,
|
|
80
|
+
yoloArgs: customYoloArgs.length > 0 ? customYoloArgs : undefined,
|
|
81
|
+
promptArgs: customPromptArgs,
|
|
82
|
+
};
|
|
89
83
|
}
|
|
90
84
|
else {
|
|
91
|
-
|
|
85
|
+
cliConfig = {
|
|
86
|
+
command: selectedProvider.command,
|
|
87
|
+
args: selectedProvider.defaultArgs,
|
|
88
|
+
yoloArgs: selectedProvider.yoloArgs.length > 0 ? selectedProvider.yoloArgs : undefined,
|
|
89
|
+
promptArgs: selectedProvider.promptArgs ?? [],
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
console.log(`\nSelected CLI provider: ${CLI_PROVIDERS[selectedCliProviderKey].name}`);
|
|
93
|
+
// Step 2: Select language (second)
|
|
94
|
+
const languageKeys = Object.keys(LANGUAGES);
|
|
95
|
+
const languageNames = languageKeys.map(k => `${LANGUAGES[k].name} - ${LANGUAGES[k].description}`);
|
|
96
|
+
const selectedName = await promptSelectWithArrows("Select your project language/runtime:", languageNames);
|
|
97
|
+
const selectedIndex = languageNames.indexOf(selectedName);
|
|
98
|
+
selectedKey = languageKeys[selectedIndex];
|
|
99
|
+
const config = LANGUAGES[selectedKey];
|
|
100
|
+
console.log(`\nSelected language: ${config.name}`);
|
|
101
|
+
// Step 3: Select technology stack if available (third)
|
|
102
|
+
if (config.technologies && config.technologies.length > 0) {
|
|
103
|
+
const techOptions = config.technologies.map(t => `${t.name} - ${t.description}`);
|
|
104
|
+
const techNames = config.technologies.map(t => t.name);
|
|
105
|
+
selectedTechnologies = await promptMultiSelectWithArrows("Select your technology stack (optional):", techOptions);
|
|
106
|
+
// Convert display names back to just technology names for predefined options
|
|
107
|
+
selectedTechnologies = selectedTechnologies.map(sel => {
|
|
108
|
+
const idx = techOptions.indexOf(sel);
|
|
109
|
+
return idx >= 0 ? techNames[idx] : sel;
|
|
110
|
+
});
|
|
111
|
+
if (selectedTechnologies.length > 0) {
|
|
112
|
+
console.log(`\nSelected technologies: ${selectedTechnologies.join(", ")}`);
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
console.log("\nNo technologies selected.");
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
// Allow custom commands for "none" language
|
|
119
|
+
checkCommand = config.checkCommand;
|
|
120
|
+
testCommand = config.testCommand;
|
|
121
|
+
if (selectedKey === "none") {
|
|
122
|
+
checkCommand = await promptInput("\nEnter your type/build check command: ") || checkCommand;
|
|
123
|
+
testCommand = await promptInput("Enter your test command: ") || testCommand;
|
|
92
124
|
}
|
|
93
|
-
}
|
|
94
|
-
// Allow custom commands for "none" language
|
|
95
|
-
let checkCommand = config.checkCommand;
|
|
96
|
-
let testCommand = config.testCommand;
|
|
97
|
-
if (selectedKey === "none") {
|
|
98
|
-
checkCommand = await promptInput("\nEnter your type/build check command: ") || checkCommand;
|
|
99
|
-
testCommand = await promptInput("Enter your test command: ") || testCommand;
|
|
100
125
|
}
|
|
101
126
|
const finalConfig = {
|
|
102
|
-
...
|
|
127
|
+
...LANGUAGES[selectedKey],
|
|
103
128
|
checkCommand,
|
|
104
129
|
testCommand,
|
|
105
130
|
};
|
|
106
131
|
// Generate image name from directory name
|
|
107
132
|
const projectName = basename(cwd).toLowerCase().replace(/[^a-z0-9-]/g, "-");
|
|
108
133
|
const imageName = `ralph-${projectName}`;
|
|
109
|
-
// Write config file
|
|
134
|
+
// Write config file with all available options (defaults or empty values)
|
|
110
135
|
const configData = {
|
|
136
|
+
// Required fields
|
|
111
137
|
language: selectedKey,
|
|
112
138
|
checkCommand: finalConfig.checkCommand,
|
|
113
139
|
testCommand: finalConfig.testCommand,
|
|
114
140
|
imageName,
|
|
141
|
+
// CLI configuration
|
|
115
142
|
cli: cliConfig,
|
|
116
143
|
cliProvider: selectedCliProviderKey,
|
|
144
|
+
// Optional fields with defaults/empty values for discoverability
|
|
145
|
+
notifyCommand: "",
|
|
146
|
+
technologies: selectedTechnologies.length > 0 ? selectedTechnologies : [],
|
|
147
|
+
javaVersion: selectedKey === "java" ? 21 : null,
|
|
148
|
+
// Docker configuration options
|
|
149
|
+
docker: {
|
|
150
|
+
ports: [],
|
|
151
|
+
volumes: [],
|
|
152
|
+
environment: {},
|
|
153
|
+
git: {
|
|
154
|
+
name: "",
|
|
155
|
+
email: "",
|
|
156
|
+
},
|
|
157
|
+
packages: [],
|
|
158
|
+
buildCommands: {
|
|
159
|
+
root: [],
|
|
160
|
+
node: [],
|
|
161
|
+
},
|
|
162
|
+
startCommand: "",
|
|
163
|
+
asciinema: {
|
|
164
|
+
enabled: false,
|
|
165
|
+
autoRecord: false,
|
|
166
|
+
outputDir: ".recordings",
|
|
167
|
+
},
|
|
168
|
+
firewall: {
|
|
169
|
+
allowedDomains: [],
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
// Claude-specific configuration (MCP servers and skills)
|
|
173
|
+
claude: {
|
|
174
|
+
mcpServers: {},
|
|
175
|
+
skills: [],
|
|
176
|
+
},
|
|
117
177
|
};
|
|
118
|
-
// Add technologies if any were selected
|
|
119
|
-
if (selectedTechnologies.length > 0) {
|
|
120
|
-
configData.technologies = selectedTechnologies;
|
|
121
|
-
}
|
|
122
178
|
const configPath = join(ralphDir, CONFIG_FILE);
|
|
123
179
|
writeFileSync(configPath, JSON.stringify(configData, null, 2) + "\n");
|
|
124
180
|
console.log(`\nCreated ${RALPH_DIR}/${CONFIG_FILE}`);
|
|
125
181
|
// Write prompt file (ask if exists) - uses template with $variables
|
|
126
182
|
const prompt = generatePromptTemplate();
|
|
127
183
|
const promptPath = join(ralphDir, PROMPT_FILE);
|
|
128
|
-
if (existsSync(promptPath)) {
|
|
184
|
+
if (existsSync(promptPath) && !useDefaults) {
|
|
129
185
|
const overwritePrompt = await promptConfirm(`${RALPH_DIR}/${PROMPT_FILE} already exists. Overwrite?`);
|
|
130
186
|
if (overwritePrompt) {
|
|
131
187
|
writeFileSync(promptPath, prompt + "\n");
|
|
@@ -137,7 +193,7 @@ export async function init(_args) {
|
|
|
137
193
|
}
|
|
138
194
|
else {
|
|
139
195
|
writeFileSync(promptPath, prompt + "\n");
|
|
140
|
-
console.log(
|
|
196
|
+
console.log(`${existsSync(promptPath) ? "Updated" : "Created"} ${RALPH_DIR}/${PROMPT_FILE}`);
|
|
141
197
|
}
|
|
142
198
|
// Create PRD if not exists
|
|
143
199
|
const prdPath = join(ralphDir, PRD_FILE);
|
package/dist/commands/run.js
CHANGED
|
@@ -265,8 +265,8 @@ export async function run(args) {
|
|
|
265
265
|
});
|
|
266
266
|
const paths = getPaths();
|
|
267
267
|
const cliConfig = getCliConfig(config);
|
|
268
|
-
//
|
|
269
|
-
const
|
|
268
|
+
// Progress tracking: stop only if no tasks complete after N iterations
|
|
269
|
+
const MAX_ITERATIONS_WITHOUT_PROGRESS = 3;
|
|
270
270
|
// Get requested iteration count (may be adjusted dynamically)
|
|
271
271
|
const requestedIterations = parseInt(filteredArgs[0]) || Infinity;
|
|
272
272
|
// Container is required, so always run with skip-permissions
|
|
@@ -294,21 +294,19 @@ export async function run(args) {
|
|
|
294
294
|
let consecutiveFailures = 0;
|
|
295
295
|
let lastExitCode = 0;
|
|
296
296
|
let iterationCount = 0;
|
|
297
|
+
// Progress tracking for --all mode
|
|
298
|
+
// Progress = tasks completed OR new tasks added (allows ralph to expand the PRD)
|
|
299
|
+
const initialCounts = countPrdItems(paths.prd, category);
|
|
300
|
+
let lastCompletedCount = initialCounts.complete;
|
|
301
|
+
let lastTotalCount = initialCounts.total;
|
|
302
|
+
let iterationsWithoutProgress = 0;
|
|
297
303
|
try {
|
|
298
304
|
while (true) {
|
|
299
305
|
iterationCount++;
|
|
300
|
-
// Dynamic iteration limit: recalculate based on current incomplete count
|
|
301
|
-
// This allows the limit to expand if tasks are added during the run
|
|
302
306
|
const currentCounts = countPrdItems(paths.prd, category);
|
|
303
|
-
const dynamicMaxIterations = currentCounts.incomplete + ITERATION_SAFETY_MARGIN;
|
|
304
307
|
// Check if we should stop (not in loop mode)
|
|
305
|
-
if (!loopMode) {
|
|
306
|
-
if (
|
|
307
|
-
console.log(`\nStopping: reached iteration limit (${dynamicMaxIterations}) with ${currentCounts.incomplete} tasks remaining.`);
|
|
308
|
-
console.log("This may indicate tasks are not completing. Check the PRD and progress.");
|
|
309
|
-
break;
|
|
310
|
-
}
|
|
311
|
-
if (!allMode && iterationCount > Math.min(requestedIterations, dynamicMaxIterations)) {
|
|
308
|
+
if (!loopMode && !allMode) {
|
|
309
|
+
if (iterationCount > requestedIterations) {
|
|
312
310
|
break;
|
|
313
311
|
}
|
|
314
312
|
}
|
|
@@ -320,7 +318,7 @@ export async function run(args) {
|
|
|
320
318
|
console.log(`Iteration ${iterationCount}`);
|
|
321
319
|
}
|
|
322
320
|
else {
|
|
323
|
-
console.log(`Iteration ${iterationCount} of ${
|
|
321
|
+
console.log(`Iteration ${iterationCount} of ${requestedIterations}`);
|
|
324
322
|
}
|
|
325
323
|
console.log(`${"=".repeat(50)}\n`);
|
|
326
324
|
// Load a valid copy of the PRD before handing to the LLM
|
|
@@ -397,6 +395,29 @@ export async function run(args) {
|
|
|
397
395
|
filteredPrdPath = null;
|
|
398
396
|
// Validate and recover PRD if the LLM corrupted it
|
|
399
397
|
validateAndRecoverPrd(paths.prd, validPrd);
|
|
398
|
+
// Track progress for --all mode: stop if no progress after N iterations
|
|
399
|
+
// Progress = tasks completed OR new tasks added (allows ralph to expand the PRD)
|
|
400
|
+
if (allMode) {
|
|
401
|
+
const progressCounts = countPrdItems(paths.prd, category);
|
|
402
|
+
const tasksCompleted = progressCounts.complete > lastCompletedCount;
|
|
403
|
+
const tasksAdded = progressCounts.total > lastTotalCount;
|
|
404
|
+
if (tasksCompleted || tasksAdded) {
|
|
405
|
+
// Progress made - reset counter
|
|
406
|
+
iterationsWithoutProgress = 0;
|
|
407
|
+
lastCompletedCount = progressCounts.complete;
|
|
408
|
+
lastTotalCount = progressCounts.total;
|
|
409
|
+
}
|
|
410
|
+
else {
|
|
411
|
+
iterationsWithoutProgress++;
|
|
412
|
+
}
|
|
413
|
+
if (iterationsWithoutProgress >= MAX_ITERATIONS_WITHOUT_PROGRESS) {
|
|
414
|
+
console.log(`\nStopping: no progress after ${MAX_ITERATIONS_WITHOUT_PROGRESS} consecutive iterations.`);
|
|
415
|
+
console.log(`(No tasks completed and no new tasks added)`);
|
|
416
|
+
console.log(`Status: ${progressCounts.complete}/${progressCounts.total} complete, ${progressCounts.incomplete} remaining.`);
|
|
417
|
+
console.log("Check the PRD and task definitions for issues.");
|
|
418
|
+
break;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
400
421
|
if (exitCode !== 0) {
|
|
401
422
|
console.error(`\n${cliConfig.command} exited with code ${exitCode}`);
|
|
402
423
|
// Track consecutive failures to detect persistent errors (e.g., missing API key)
|