ralph-cli-sandboxed 0.3.0 → 0.4.0

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.
Files changed (55) hide show
  1. package/dist/commands/action.d.ts +7 -0
  2. package/dist/commands/action.js +276 -0
  3. package/dist/commands/chat.js +95 -7
  4. package/dist/commands/config.js +6 -18
  5. package/dist/commands/fix-config.d.ts +4 -0
  6. package/dist/commands/fix-config.js +388 -0
  7. package/dist/commands/help.js +17 -0
  8. package/dist/commands/init.js +89 -2
  9. package/dist/commands/listen.js +50 -9
  10. package/dist/commands/prd.js +2 -2
  11. package/dist/config/languages.json +4 -0
  12. package/dist/index.js +4 -0
  13. package/dist/providers/telegram.d.ts +6 -2
  14. package/dist/providers/telegram.js +68 -2
  15. package/dist/templates/macos-scripts.d.ts +42 -0
  16. package/dist/templates/macos-scripts.js +448 -0
  17. package/dist/tui/ConfigEditor.d.ts +7 -0
  18. package/dist/tui/ConfigEditor.js +313 -0
  19. package/dist/tui/components/ArrayEditor.d.ts +22 -0
  20. package/dist/tui/components/ArrayEditor.js +193 -0
  21. package/dist/tui/components/BooleanToggle.d.ts +19 -0
  22. package/dist/tui/components/BooleanToggle.js +43 -0
  23. package/dist/tui/components/EditorPanel.d.ts +50 -0
  24. package/dist/tui/components/EditorPanel.js +232 -0
  25. package/dist/tui/components/HelpPanel.d.ts +13 -0
  26. package/dist/tui/components/HelpPanel.js +69 -0
  27. package/dist/tui/components/JsonSnippetEditor.d.ts +24 -0
  28. package/dist/tui/components/JsonSnippetEditor.js +380 -0
  29. package/dist/tui/components/KeyValueEditor.d.ts +34 -0
  30. package/dist/tui/components/KeyValueEditor.js +261 -0
  31. package/dist/tui/components/ObjectEditor.d.ts +23 -0
  32. package/dist/tui/components/ObjectEditor.js +227 -0
  33. package/dist/tui/components/PresetSelector.d.ts +23 -0
  34. package/dist/tui/components/PresetSelector.js +58 -0
  35. package/dist/tui/components/Preview.d.ts +18 -0
  36. package/dist/tui/components/Preview.js +190 -0
  37. package/dist/tui/components/ScrollableContainer.d.ts +38 -0
  38. package/dist/tui/components/ScrollableContainer.js +77 -0
  39. package/dist/tui/components/SectionNav.d.ts +31 -0
  40. package/dist/tui/components/SectionNav.js +130 -0
  41. package/dist/tui/components/StringEditor.d.ts +21 -0
  42. package/dist/tui/components/StringEditor.js +29 -0
  43. package/dist/tui/hooks/useConfig.d.ts +16 -0
  44. package/dist/tui/hooks/useConfig.js +89 -0
  45. package/dist/tui/hooks/useTerminalSize.d.ts +21 -0
  46. package/dist/tui/hooks/useTerminalSize.js +48 -0
  47. package/dist/tui/utils/presets.d.ts +52 -0
  48. package/dist/tui/utils/presets.js +191 -0
  49. package/dist/tui/utils/validation.d.ts +49 -0
  50. package/dist/tui/utils/validation.js +198 -0
  51. package/dist/utils/chat-client.d.ts +31 -1
  52. package/dist/utils/chat-client.js +27 -1
  53. package/dist/utils/config.d.ts +7 -2
  54. package/docs/MACOS-DEVELOPMENT.md +435 -0
  55. package/package.json +1 -1
@@ -0,0 +1,388 @@
1
+ import { existsSync, readFileSync, writeFileSync, copyFileSync } from "fs";
2
+ import { getPaths } from "../utils/config.js";
3
+ import { promptConfirm } from "../utils/prompt.js";
4
+ import { getLanguages, getCliProviders } from "../templates/prompts.js";
5
+ /**
6
+ * Configuration sections that can be individually validated and recovered.
7
+ */
8
+ const CONFIG_SECTIONS = [
9
+ "language",
10
+ "checkCommand",
11
+ "testCommand",
12
+ "imageName",
13
+ "cli",
14
+ "cliProvider",
15
+ "notifyCommand",
16
+ "notifications",
17
+ "technologies",
18
+ "javaVersion",
19
+ "docker",
20
+ "claude",
21
+ "chat",
22
+ "daemon",
23
+ ];
24
+ /**
25
+ * Attempts to parse JSON, returning the error position if parsing fails.
26
+ */
27
+ function parseJsonWithError(content) {
28
+ try {
29
+ return { data: JSON.parse(content) };
30
+ }
31
+ catch (err) {
32
+ if (err instanceof SyntaxError) {
33
+ const errorMsg = err.message;
34
+ // Extract position from error message (e.g., "at position 123" or "at line 5 column 10")
35
+ const posMatch = errorMsg.match(/at position (\d+)/);
36
+ const lineColMatch = errorMsg.match(/at line (\d+) column (\d+)/);
37
+ if (lineColMatch) {
38
+ return {
39
+ error: errorMsg,
40
+ line: parseInt(lineColMatch[1], 10),
41
+ column: parseInt(lineColMatch[2], 10),
42
+ };
43
+ }
44
+ else if (posMatch) {
45
+ const position = parseInt(posMatch[1], 10);
46
+ const lines = content.substring(0, position).split("\n");
47
+ return {
48
+ error: errorMsg,
49
+ line: lines.length,
50
+ column: lines[lines.length - 1].length + 1,
51
+ };
52
+ }
53
+ return { error: errorMsg };
54
+ }
55
+ return { error: String(err) };
56
+ }
57
+ }
58
+ /**
59
+ * Generates default config values (matching ralph init defaults).
60
+ */
61
+ function getDefaultConfig() {
62
+ const LANGUAGES = getLanguages();
63
+ const CLI_PROVIDERS = getCliProviders();
64
+ const defaultLanguage = "node";
65
+ const defaultProvider = CLI_PROVIDERS["claude"];
66
+ const langConfig = LANGUAGES[defaultLanguage];
67
+ return {
68
+ language: defaultLanguage,
69
+ checkCommand: langConfig.checkCommand,
70
+ testCommand: langConfig.testCommand,
71
+ imageName: "ralph-project",
72
+ cli: {
73
+ command: defaultProvider.command,
74
+ args: defaultProvider.defaultArgs,
75
+ yoloArgs: defaultProvider.yoloArgs.length > 0 ? defaultProvider.yoloArgs : undefined,
76
+ promptArgs: defaultProvider.promptArgs ?? [],
77
+ },
78
+ cliProvider: "claude",
79
+ notifyCommand: "",
80
+ technologies: [],
81
+ docker: {
82
+ ports: [],
83
+ volumes: [],
84
+ environment: {},
85
+ git: {
86
+ name: "",
87
+ email: "",
88
+ },
89
+ packages: [],
90
+ buildCommands: {
91
+ root: [],
92
+ node: [],
93
+ },
94
+ startCommand: "",
95
+ asciinema: {
96
+ enabled: false,
97
+ autoRecord: false,
98
+ outputDir: ".recordings",
99
+ streamJson: {
100
+ enabled: false,
101
+ saveRawJson: true,
102
+ },
103
+ },
104
+ firewall: {
105
+ allowedDomains: [],
106
+ },
107
+ autoStart: false,
108
+ restartCount: 0,
109
+ },
110
+ claude: {
111
+ mcpServers: {},
112
+ skills: [],
113
+ },
114
+ chat: {
115
+ enabled: false,
116
+ provider: "telegram",
117
+ telegram: {
118
+ botToken: "",
119
+ allowedChatIds: [],
120
+ },
121
+ },
122
+ daemon: {
123
+ actions: {},
124
+ events: {},
125
+ },
126
+ };
127
+ }
128
+ /**
129
+ * Validates a specific section of the config.
130
+ */
131
+ function validateSection(section, value) {
132
+ if (value === undefined || value === null) {
133
+ return false;
134
+ }
135
+ switch (section) {
136
+ case "language":
137
+ case "checkCommand":
138
+ case "testCommand":
139
+ case "imageName":
140
+ case "cliProvider":
141
+ case "notifyCommand":
142
+ return typeof value === "string";
143
+ case "technologies":
144
+ return Array.isArray(value) && value.every((item) => typeof item === "string");
145
+ case "javaVersion":
146
+ return value === null || typeof value === "number";
147
+ case "cli":
148
+ if (typeof value !== "object" || value === null)
149
+ return false;
150
+ const cli = value;
151
+ return typeof cli.command === "string";
152
+ case "notifications":
153
+ if (typeof value !== "object" || value === null)
154
+ return false;
155
+ const notif = value;
156
+ return typeof notif.provider === "string";
157
+ case "docker":
158
+ case "claude":
159
+ case "chat":
160
+ case "daemon":
161
+ return typeof value === "object" && value !== null;
162
+ default:
163
+ return true;
164
+ }
165
+ }
166
+ /**
167
+ * Attempts to extract a value from potentially corrupt JSON using regex.
168
+ * This is a best-effort approach for partially corrupt files.
169
+ */
170
+ function extractSectionFromCorrupt(content, section) {
171
+ // Try to find the section in the raw content
172
+ const patterns = {
173
+ language: /"language"\s*:\s*"([^"]+)"/,
174
+ checkCommand: /"checkCommand"\s*:\s*"([^"]+)"/,
175
+ testCommand: /"testCommand"\s*:\s*"([^"]+)"/,
176
+ imageName: /"imageName"\s*:\s*"([^"]+)"/,
177
+ cliProvider: /"cliProvider"\s*:\s*"([^"]+)"/,
178
+ notifyCommand: /"notifyCommand"\s*:\s*"([^"]*)"/,
179
+ };
180
+ const pattern = patterns[section];
181
+ if (pattern) {
182
+ const match = content.match(pattern);
183
+ if (match) {
184
+ return match[1];
185
+ }
186
+ }
187
+ return undefined;
188
+ }
189
+ /**
190
+ * Attempts to recover valid sections from a corrupt config.
191
+ */
192
+ function recoverSections(corruptContent, parsedPartial) {
193
+ const defaultConfig = getDefaultConfig();
194
+ const recoveredConfig = {};
195
+ const result = {
196
+ recovered: [],
197
+ reset: [],
198
+ errors: [],
199
+ };
200
+ for (const section of CONFIG_SECTIONS) {
201
+ let value = undefined;
202
+ let source = "default";
203
+ // First, try to get from parsed partial (if JSON was partially valid)
204
+ if (parsedPartial && section in parsedPartial) {
205
+ value = parsedPartial[section];
206
+ source = "parsed";
207
+ }
208
+ // If not found or invalid, try regex extraction for simple string fields
209
+ if ((value === undefined || !validateSection(section, value)) && typeof corruptContent === "string") {
210
+ const extracted = extractSectionFromCorrupt(corruptContent, section);
211
+ if (extracted !== undefined && validateSection(section, extracted)) {
212
+ value = extracted;
213
+ source = "extracted";
214
+ }
215
+ }
216
+ // Validate the value
217
+ if (validateSection(section, value)) {
218
+ recoveredConfig[section] = value;
219
+ result.recovered.push(section);
220
+ }
221
+ else {
222
+ // Use default value
223
+ recoveredConfig[section] = defaultConfig[section];
224
+ result.reset.push(section);
225
+ }
226
+ }
227
+ return result;
228
+ }
229
+ /**
230
+ * Creates a backup of the config file.
231
+ */
232
+ function createBackup(configPath) {
233
+ const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
234
+ const backupPath = configPath.replace("config.json", `config.json.backup.${timestamp}`);
235
+ copyFileSync(configPath, backupPath);
236
+ return backupPath;
237
+ }
238
+ /**
239
+ * Merges recovered sections into a valid config object.
240
+ */
241
+ function buildRecoveredConfig(corruptContent, parsedPartial) {
242
+ const defaultConfig = getDefaultConfig();
243
+ const result = {
244
+ recovered: [],
245
+ reset: [],
246
+ errors: [],
247
+ };
248
+ const config = {};
249
+ for (const section of CONFIG_SECTIONS) {
250
+ let value = undefined;
251
+ // First, try to get from parsed partial
252
+ if (parsedPartial && section in parsedPartial) {
253
+ value = parsedPartial[section];
254
+ }
255
+ // If not found or invalid, try regex extraction
256
+ if (value === undefined || !validateSection(section, value)) {
257
+ const extracted = extractSectionFromCorrupt(corruptContent, section);
258
+ if (extracted !== undefined && validateSection(section, extracted)) {
259
+ value = extracted;
260
+ }
261
+ }
262
+ // Validate and assign
263
+ if (validateSection(section, value)) {
264
+ config[section] = value;
265
+ result.recovered.push(section);
266
+ }
267
+ else {
268
+ config[section] = defaultConfig[section];
269
+ result.reset.push(section);
270
+ }
271
+ }
272
+ return { config: config, result };
273
+ }
274
+ /**
275
+ * Main fix-config command handler.
276
+ */
277
+ export async function fixConfig(args) {
278
+ const verifyOnly = args.includes("--verify") || args.includes("-v");
279
+ const skipPrompt = args.includes("-y") || args.includes("--yes");
280
+ const paths = getPaths();
281
+ const configPath = paths.config;
282
+ // Check if config file exists
283
+ if (!existsSync(configPath)) {
284
+ console.error("Error: .ralph/config.json not found. Run 'ralph init' first.");
285
+ process.exit(1);
286
+ }
287
+ console.log("Checking config.json...\n");
288
+ // Read the raw content
289
+ const rawContent = readFileSync(configPath, "utf-8");
290
+ // Attempt to parse the JSON
291
+ const parseResult = parseJsonWithError(rawContent);
292
+ if (parseResult.data) {
293
+ // JSON is syntactically valid
294
+ const config = parseResult.data;
295
+ // Validate required fields
296
+ const missingFields = [];
297
+ if (typeof config.language !== "string")
298
+ missingFields.push("language");
299
+ if (typeof config.checkCommand !== "string")
300
+ missingFields.push("checkCommand");
301
+ if (typeof config.testCommand !== "string")
302
+ missingFields.push("testCommand");
303
+ if (missingFields.length === 0) {
304
+ console.log("\x1b[32m✓ config.json is valid.\x1b[0m");
305
+ return;
306
+ }
307
+ console.log("\x1b[33m⚠ config.json is missing required fields:\x1b[0m");
308
+ missingFields.forEach((field) => console.log(` - ${field}`));
309
+ if (verifyOnly) {
310
+ process.exit(1);
311
+ }
312
+ // Offer to fix missing fields
313
+ if (!skipPrompt) {
314
+ const confirm = await promptConfirm("\nFix missing fields with defaults?");
315
+ if (!confirm) {
316
+ console.log("Aborted.");
317
+ return;
318
+ }
319
+ }
320
+ // Create backup
321
+ const backupPath = createBackup(configPath);
322
+ console.log(`\nCreated backup: ${backupPath}`);
323
+ // Merge with defaults
324
+ const defaultConfig = getDefaultConfig();
325
+ const fixedConfig = { ...defaultConfig, ...config };
326
+ // Ensure required fields exist
327
+ for (const field of missingFields) {
328
+ fixedConfig[field] = defaultConfig[field];
329
+ }
330
+ writeFileSync(configPath, JSON.stringify(fixedConfig, null, 2) + "\n");
331
+ console.log("\n\x1b[32m✓ config.json repaired.\x1b[0m");
332
+ console.log(` Added defaults for: ${missingFields.join(", ")}`);
333
+ return;
334
+ }
335
+ // JSON parsing failed
336
+ console.log("\x1b[31m✗ config.json contains invalid JSON.\x1b[0m");
337
+ console.log(` Error: ${parseResult.error}`);
338
+ if (parseResult.line) {
339
+ console.log(` Location: line ${parseResult.line}, column ${parseResult.column || "?"}`);
340
+ }
341
+ if (verifyOnly) {
342
+ process.exit(1);
343
+ }
344
+ // Attempt recovery
345
+ console.log("\nAttempting to recover valid sections...\n");
346
+ // Try to parse partially (some JSON parsers are more lenient)
347
+ let parsedPartial = null;
348
+ try {
349
+ // Try JSON5-style parsing by removing trailing commas and comments
350
+ const cleaned = rawContent
351
+ .replace(/,\s*([\]}])/g, "$1") // Remove trailing commas
352
+ .replace(/\/\/.*$/gm, "") // Remove single-line comments
353
+ .replace(/\/\*[\s\S]*?\*\//g, ""); // Remove multi-line comments
354
+ parsedPartial = JSON.parse(cleaned);
355
+ }
356
+ catch {
357
+ // Partial parsing failed, continue with regex extraction
358
+ }
359
+ const { config: recoveredConfig, result } = buildRecoveredConfig(rawContent, parsedPartial);
360
+ // Report results
361
+ console.log("Recovery analysis:");
362
+ if (result.recovered.length > 0) {
363
+ console.log(`\x1b[32m Recoverable sections (${result.recovered.length}):\x1b[0m`);
364
+ result.recovered.forEach((section) => console.log(` ✓ ${section}`));
365
+ }
366
+ if (result.reset.length > 0) {
367
+ console.log(`\x1b[33m Reset to defaults (${result.reset.length}):\x1b[0m`);
368
+ result.reset.forEach((section) => console.log(` ⚠ ${section}`));
369
+ }
370
+ // Confirm before applying
371
+ if (!skipPrompt) {
372
+ console.log();
373
+ const confirm = await promptConfirm("Apply these fixes?");
374
+ if (!confirm) {
375
+ console.log("Aborted.");
376
+ return;
377
+ }
378
+ }
379
+ // Create backup of corrupt file
380
+ const backupPath = createBackup(configPath);
381
+ console.log(`\nCreated backup: ${backupPath}`);
382
+ // Write the recovered config
383
+ writeFileSync(configPath, JSON.stringify(recoveredConfig, null, 2) + "\n");
384
+ console.log("\n\x1b[32m✓ config.json repaired.\x1b[0m");
385
+ console.log(` Recovered: ${result.recovered.length} sections`);
386
+ console.log(` Reset to defaults: ${result.reset.length} sections`);
387
+ console.log(` Original backup: ${backupPath}`);
388
+ }
@@ -14,10 +14,12 @@ COMMANDS:
14
14
  toggle <n> Toggle passes status for entry n
15
15
  clean Remove all passing entries from the PRD
16
16
  fix-prd [opts] Validate and recover corrupted PRD file
17
+ fix-config [opts] Validate and recover corrupted config.json
17
18
  prompt [opts] Display resolved prompt (for testing in Claude Code)
18
19
  docker <sub> Manage Docker sandbox environment
19
20
  daemon <sub> Host daemon for sandbox-to-host communication
20
21
  notify [msg] Send notification to host from sandbox
22
+ action [name] Execute host actions from config.json
21
23
  chat <sub> Chat client integration (Telegram, etc.)
22
24
  help Show this help message
23
25
 
@@ -50,6 +52,10 @@ FIX-PRD OPTIONS:
50
52
  <backup-file> Restore PRD from a specific backup file
51
53
  --verify, -v Only verify format, don't attempt to fix
52
54
 
55
+ FIX-CONFIG OPTIONS:
56
+ --verify, -v Only verify format, don't attempt to fix
57
+ -y, --yes Skip confirmation prompt, apply fixes automatically
58
+
53
59
  DOCKER SUBCOMMANDS:
54
60
  docker init Generate Dockerfile and scripts
55
61
  docker build Build image (always fetches latest Claude Code)
@@ -74,6 +80,12 @@ NOTIFY OPTIONS:
74
80
  --action, -a <name> Execute specific daemon action (default: notify)
75
81
  --debug, -d Show debug output
76
82
 
83
+ ACTION OPTIONS:
84
+ [name] Name of the action to execute
85
+ [args...] Arguments to pass to the action command
86
+ --list, -l List all configured actions
87
+ --debug, -d Show debug output
88
+
77
89
  EXAMPLES:
78
90
  ralph init # Initialize ralph (interactive CLI, language, tech selection)
79
91
  ralph init -y # Initialize with defaults (Claude + Node.js, no prompts)
@@ -95,6 +107,9 @@ EXAMPLES:
95
107
  ralph fix-prd # Validate/recover corrupted PRD file
96
108
  ralph fix-prd --verify # Check PRD format without fixing
97
109
  ralph fix-prd backup.prd.2024-01-15.json # Restore from specific backup
110
+ ralph fix-config # Validate/recover corrupted config.json
111
+ ralph fix-config --verify # Check config format without fixing
112
+ ralph fix-config -y # Auto-fix without prompts
98
113
  ralph prompt # Display resolved prompt
99
114
  ralph docker init # Generate Dockerfile for sandboxed env
100
115
  ralph docker build # Build Docker image
@@ -103,6 +118,8 @@ EXAMPLES:
103
118
  ralph notify "Task done!" # Send notification from sandbox to host
104
119
  ralph chat start # Start Telegram chat daemon
105
120
  ralph chat test 123456 # Test chat connection
121
+ ralph action --list # List available host actions
122
+ ralph action build # Execute 'build' action on host
106
123
 
107
124
  CONFIGURATION:
108
125
  After running 'ralph init', you'll have:
@@ -1,7 +1,8 @@
1
- import { existsSync, writeFileSync, mkdirSync, copyFileSync } from "fs";
1
+ import { existsSync, writeFileSync, mkdirSync, copyFileSync, chmodSync } from "fs";
2
2
  import { join, basename, dirname } from "path";
3
3
  import { fileURLToPath } from "url";
4
4
  import { getLanguages, generatePromptTemplate, DEFAULT_PRD, DEFAULT_PROGRESS, getCliProviders, getSkillsForLanguage } from "../templates/prompts.js";
5
+ import { generateGenXcodeScript, hasSwiftUI, hasFastlane, generateFastfile, generateAppfile, generateFastlaneReadmeSection } from "../templates/macos-scripts.js";
5
6
  import { promptSelectWithArrows, promptConfirm, promptInput, promptMultiSelectWithArrows } from "../utils/prompt.js";
6
7
  import { dockerInit } from "./docker.js";
7
8
  // Get package root directory (works for both dev and installed package)
@@ -155,6 +156,37 @@ export async function init(args) {
155
156
  // Generate image name from directory name
156
157
  const projectName = basename(cwd).toLowerCase().replace(/[^a-z0-9-]/g, "-");
157
158
  const imageName = `ralph-${projectName}`;
159
+ // Generate macOS development actions for Swift + SwiftUI projects
160
+ const macOsActions = {};
161
+ if (selectedKey === "swift" && hasSwiftUI(selectedTechnologies)) {
162
+ macOsActions.gen_xcode = {
163
+ command: "./scripts/gen_xcode.sh",
164
+ description: "Generate Xcode project from Swift package",
165
+ };
166
+ macOsActions.build = {
167
+ command: "xcodebuild -project *.xcodeproj -scheme * -configuration Debug build",
168
+ description: "Build the Xcode project in Debug mode",
169
+ };
170
+ macOsActions.test = {
171
+ command: "xcodebuild -project *.xcodeproj -scheme * test",
172
+ description: "Run tests via xcodebuild",
173
+ };
174
+ // Add Fastlane actions if Fastlane technology is selected
175
+ if (hasFastlane(selectedTechnologies)) {
176
+ macOsActions.fastlane_init = {
177
+ command: "cd scripts/fastlane && fastlane init",
178
+ description: "Initialize Fastlane credentials (interactive)",
179
+ };
180
+ macOsActions.fastlane_beta = {
181
+ command: "cd scripts/fastlane && fastlane beta",
182
+ description: "Deploy beta build via Fastlane",
183
+ };
184
+ macOsActions.fastlane_release = {
185
+ command: "cd scripts/fastlane && fastlane release",
186
+ description: "Deploy release build via Fastlane",
187
+ };
188
+ }
189
+ }
158
190
  // Write config file with all available options (defaults or empty values)
159
191
  const configData = {
160
192
  // Required fields
@@ -215,7 +247,7 @@ export async function init(args) {
215
247
  },
216
248
  // Daemon configuration for sandbox-to-host communication
217
249
  daemon: {
218
- actions: {},
250
+ actions: macOsActions,
219
251
  // Event handlers - each event can trigger multiple daemon actions
220
252
  // Available events: task_complete, ralph_complete, iteration_complete, error
221
253
  events: {
@@ -288,6 +320,61 @@ docker/.config-hash
288
320
  else {
289
321
  console.log(`Skipped ${RALPH_DIR}/.gitignore (already exists)`);
290
322
  }
323
+ // Generate macOS/Swift development scripts if Swift + SwiftUI selected
324
+ if (selectedKey === "swift" && hasSwiftUI(selectedTechnologies)) {
325
+ const scriptsDir = join(cwd, "scripts");
326
+ const genXcodePath = join(scriptsDir, "gen_xcode.sh");
327
+ if (!existsSync(scriptsDir)) {
328
+ mkdirSync(scriptsDir, { recursive: true });
329
+ console.log("Created scripts/");
330
+ }
331
+ // Use a clean project name (PascalCase) for the Swift project
332
+ const swiftProjectName = basename(cwd)
333
+ .replace(/[^a-zA-Z0-9]+/g, " ")
334
+ .split(" ")
335
+ .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
336
+ .join("") || "App";
337
+ if (!existsSync(genXcodePath)) {
338
+ writeFileSync(genXcodePath, generateGenXcodeScript(swiftProjectName));
339
+ chmodSync(genXcodePath, 0o755);
340
+ console.log("Created scripts/gen_xcode.sh");
341
+ }
342
+ else {
343
+ console.log("Skipped scripts/gen_xcode.sh (already exists)");
344
+ }
345
+ // Generate Fastlane configuration if Fastlane technology is selected
346
+ if (hasFastlane(selectedTechnologies)) {
347
+ const fastlaneDir = join(scriptsDir, "fastlane");
348
+ const fastfilePath = join(fastlaneDir, "Fastfile");
349
+ const appfilePath = join(fastlaneDir, "Appfile");
350
+ const readmePath = join(fastlaneDir, "README.md");
351
+ if (!existsSync(fastlaneDir)) {
352
+ mkdirSync(fastlaneDir, { recursive: true });
353
+ console.log("Created scripts/fastlane/");
354
+ }
355
+ if (!existsSync(fastfilePath)) {
356
+ writeFileSync(fastfilePath, generateFastfile(swiftProjectName));
357
+ console.log("Created scripts/fastlane/Fastfile");
358
+ }
359
+ else {
360
+ console.log("Skipped scripts/fastlane/Fastfile (already exists)");
361
+ }
362
+ if (!existsSync(appfilePath)) {
363
+ writeFileSync(appfilePath, generateAppfile(swiftProjectName));
364
+ console.log("Created scripts/fastlane/Appfile");
365
+ }
366
+ else {
367
+ console.log("Skipped scripts/fastlane/Appfile (already exists)");
368
+ }
369
+ if (!existsSync(readmePath)) {
370
+ writeFileSync(readmePath, generateFastlaneReadmeSection(swiftProjectName));
371
+ console.log("Created scripts/fastlane/README.md");
372
+ }
373
+ else {
374
+ console.log("Skipped scripts/fastlane/README.md (already exists)");
375
+ }
376
+ }
377
+ }
291
378
  // Copy PRD guide file from package if not exists
292
379
  const prdGuidePath = join(ralphDir, PRD_GUIDE_FILE);
293
380
  if (!existsSync(prdGuidePath)) {
@@ -91,8 +91,16 @@ async function processMessage(message, messagesPath, debug) {
91
91
  }
92
92
  case "run": {
93
93
  // Start ralph run in background
94
- console.log("[listen] Starting ralph run...");
95
- const proc = spawn("ralph", ["run"], {
94
+ // Support optional category filter: run [category]
95
+ const runArgs = ["run"];
96
+ if (message.args && message.args.length > 0) {
97
+ runArgs.push("--category", message.args[0]);
98
+ console.log(`[listen] Starting ralph run with category: ${message.args[0]}...`);
99
+ }
100
+ else {
101
+ console.log("[listen] Starting ralph run...");
102
+ }
103
+ const proc = spawn("ralph", runArgs, {
96
104
  stdio: "inherit",
97
105
  cwd: "/workspace",
98
106
  detached: true,
@@ -100,7 +108,7 @@ async function processMessage(message, messagesPath, debug) {
100
108
  proc.unref();
101
109
  respondToMessage(messagesPath, message.id, {
102
110
  success: true,
103
- output: "Ralph run started",
111
+ output: message.args?.length ? `Ralph run started (category: ${message.args[0]})` : "Ralph run started",
104
112
  });
105
113
  break;
106
114
  }
@@ -121,10 +129,42 @@ async function processMessage(message, messagesPath, debug) {
121
129
  });
122
130
  break;
123
131
  }
132
+ case "claude": {
133
+ // Run Claude Code with the provided prompt in YOLO mode
134
+ const prompt = args?.join(" ") || "";
135
+ if (!prompt) {
136
+ respondToMessage(messagesPath, message.id, {
137
+ success: false,
138
+ error: "No prompt provided",
139
+ });
140
+ return;
141
+ }
142
+ console.log(`[listen] Running Claude Code with prompt: ${prompt.substring(0, 50)}...`);
143
+ // Build the command: claude -p "prompt" --dangerously-skip-permissions
144
+ // Using --print to get non-interactive output
145
+ const escapedPrompt = prompt.replace(/'/g, "'\\''");
146
+ const command = `claude -p '${escapedPrompt}' --dangerously-skip-permissions --print`;
147
+ // Run with 5 minute timeout
148
+ const result = await executeCommand(command, 300000);
149
+ // Truncate long output
150
+ let output = result.output;
151
+ if (output.length > 4000) {
152
+ output = output.substring(0, 4000) + "\n...(truncated)";
153
+ }
154
+ respondToMessage(messagesPath, message.id, {
155
+ success: result.success,
156
+ output,
157
+ error: result.error,
158
+ });
159
+ if (debug) {
160
+ console.log(`[listen] Claude Code result: ${result.success ? "OK" : "FAILED"}`);
161
+ }
162
+ break;
163
+ }
124
164
  default:
125
165
  respondToMessage(messagesPath, message.id, {
126
166
  success: false,
127
- error: `Unknown action: ${action}`,
167
+ error: `Unknown action: ${action}. Supported: exec, run, status, ping, claude`,
128
168
  });
129
169
  }
130
170
  }
@@ -139,7 +179,7 @@ async function startListening(debug) {
139
179
  console.log(`Messages file: ${messagesPath}`);
140
180
  console.log("");
141
181
  console.log("Listening for commands from host...");
142
- console.log("Supported actions: exec, run, status, ping");
182
+ console.log("Supported actions: exec, run, status, ping, claude");
143
183
  console.log("");
144
184
  console.log("Press Ctrl+C to stop.");
145
185
  // Process any pending messages on startup
@@ -212,10 +252,11 @@ DESCRIPTION:
212
252
  processes them and writes responses back.
213
253
 
214
254
  SUPPORTED ACTIONS:
215
- exec [cmd] Execute a shell command in the sandbox
216
- run Start ralph run
217
- status Get PRD status
218
- ping Health check
255
+ exec [cmd] Execute a shell command in the sandbox
256
+ run Start ralph run
257
+ status Get PRD status
258
+ ping Health check
259
+ claude [prompt] Run Claude Code with prompt (YOLO mode)
219
260
 
220
261
  SETUP:
221
262
  1. Start the daemon on the host: ralph daemon start
@@ -91,8 +91,8 @@ export function prdList(category, passesFilter) {
91
91
  console.log("\nPRD Entries:\n");
92
92
  }
93
93
  filteredPrd.forEach(({ entry, originalIndex }) => {
94
- const status = entry.passes ? "\x1b[32m[PASS]\x1b[0m" : "\x1b[33m[ ]\x1b[0m";
95
- console.log(` ${originalIndex + 1}. ${status} [${entry.category}] ${entry.description}`);
94
+ const statusEmoji = entry.passes ? "" : "";
95
+ console.log(` ${originalIndex + 1}. ${statusEmoji} [${entry.category}] ${entry.description}`);
96
96
  entry.steps.forEach((step, j) => {
97
97
  console.log(` ${j + 1}. ${step}`);
98
98
  });
@@ -266,6 +266,10 @@
266
266
  { "name": "Hummingbird", "description": "Lightweight Swift web framework" },
267
267
  { "name": "Fluent ORM", "description": "Swift ORM for Vapor" },
268
268
  { "name": "SwiftNIO", "description": "Event-driven network framework" },
269
+ { "name": "SwiftUI", "description": "Apple declarative UI framework" },
270
+ { "name": "Fastlane", "description": "iOS/macOS deployment automation" },
271
+ { "name": "Combine", "description": "Reactive programming framework" },
272
+ { "name": "Swift Testing", "description": "Modern testing framework for Swift" },
269
273
  { "name": "XCTest", "description": "Swift testing framework" },
270
274
  { "name": "PostgreSQL", "description": "Advanced SQL database" },
271
275
  { "name": "SQLite", "description": "Embedded SQL database" },