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.
@@ -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(_args) {
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
- const reinit = await promptConfirm(".ralph/ directory already exists. Re-initialize?");
24
- if (!reinit) {
25
- console.log("Aborted.");
26
- return;
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 providerKeys = Object.keys(CLI_PROVIDERS);
36
- const providerNames = providerKeys.map(k => `${CLI_PROVIDERS[k].name} - ${CLI_PROVIDERS[k].description}`);
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
- // Handle custom CLI provider
43
- if (selectedCliProviderKey === "custom") {
44
- const customCommand = await promptInput("\nEnter your CLI command: ");
45
- const customArgsInput = await promptInput("Enter default arguments (space-separated): ");
46
- const customArgs = customArgsInput.trim() ? customArgsInput.trim().split(/\s+/) : [];
47
- const customYoloArgsInput = await promptInput("Enter yolo/auto-approve arguments (space-separated): ");
48
- const customYoloArgs = customYoloArgsInput.trim() ? customYoloArgsInput.trim().split(/\s+/) : [];
49
- const customPromptArgsInput = await promptInput("Enter prompt arguments (e.g., -p for flag-based, leave empty for positional): ");
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: customCommand || "claude",
53
- args: customArgs,
54
- yoloArgs: customYoloArgs.length > 0 ? customYoloArgs : undefined,
55
- promptArgs: customPromptArgs,
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
- cliConfig = {
60
- command: selectedProvider.command,
61
- args: selectedProvider.defaultArgs,
62
- yoloArgs: selectedProvider.yoloArgs.length > 0 ? selectedProvider.yoloArgs : undefined,
63
- promptArgs: selectedProvider.promptArgs ?? [],
64
- };
65
- }
66
- console.log(`\nSelected CLI provider: ${CLI_PROVIDERS[selectedCliProviderKey].name}`);
67
- // Step 2: Select language (second)
68
- const LANGUAGES = getLanguages();
69
- const languageKeys = Object.keys(LANGUAGES);
70
- const languageNames = languageKeys.map(k => `${LANGUAGES[k].name} - ${LANGUAGES[k].description}`);
71
- const selectedName = await promptSelectWithArrows("Select your project language/runtime:", languageNames);
72
- const selectedIndex = languageNames.indexOf(selectedName);
73
- const selectedKey = languageKeys[selectedIndex];
74
- const config = LANGUAGES[selectedKey];
75
- console.log(`\nSelected language: ${config.name}`);
76
- // Step 3: Select technology stack if available (third)
77
- let selectedTechnologies = [];
78
- if (config.technologies && config.technologies.length > 0) {
79
- const techOptions = config.technologies.map(t => `${t.name} - ${t.description}`);
80
- const techNames = config.technologies.map(t => t.name);
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
- console.log("\nNo technologies selected.");
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
- ...config,
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(`Created ${RALPH_DIR}/${PROMPT_FILE}`);
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);
@@ -265,8 +265,8 @@ export async function run(args) {
265
265
  });
266
266
  const paths = getPaths();
267
267
  const cliConfig = getCliConfig(config);
268
- // Safety margin for iteration limit (recalculated dynamically each iteration)
269
- const ITERATION_SAFETY_MARGIN = 3;
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 (allMode && iterationCount > dynamicMaxIterations) {
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 ${Math.min(requestedIterations, dynamicMaxIterations)}`);
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)