ralph-cli-sandboxed 0.2.9 → 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 (73) hide show
  1. package/README.md +99 -15
  2. package/dist/commands/action.d.ts +7 -0
  3. package/dist/commands/action.js +276 -0
  4. package/dist/commands/chat.d.ts +8 -0
  5. package/dist/commands/chat.js +701 -0
  6. package/dist/commands/config.d.ts +1 -0
  7. package/dist/commands/config.js +51 -0
  8. package/dist/commands/daemon.d.ts +23 -0
  9. package/dist/commands/daemon.js +422 -0
  10. package/dist/commands/docker.js +82 -4
  11. package/dist/commands/fix-config.d.ts +4 -0
  12. package/dist/commands/fix-config.js +388 -0
  13. package/dist/commands/help.js +80 -0
  14. package/dist/commands/init.js +135 -1
  15. package/dist/commands/listen.d.ts +8 -0
  16. package/dist/commands/listen.js +280 -0
  17. package/dist/commands/notify.d.ts +7 -0
  18. package/dist/commands/notify.js +165 -0
  19. package/dist/commands/once.js +8 -8
  20. package/dist/commands/prd.js +2 -2
  21. package/dist/commands/run.js +25 -12
  22. package/dist/config/languages.json +4 -0
  23. package/dist/index.js +14 -0
  24. package/dist/providers/telegram.d.ts +39 -0
  25. package/dist/providers/telegram.js +256 -0
  26. package/dist/templates/macos-scripts.d.ts +42 -0
  27. package/dist/templates/macos-scripts.js +448 -0
  28. package/dist/tui/ConfigEditor.d.ts +7 -0
  29. package/dist/tui/ConfigEditor.js +313 -0
  30. package/dist/tui/components/ArrayEditor.d.ts +22 -0
  31. package/dist/tui/components/ArrayEditor.js +193 -0
  32. package/dist/tui/components/BooleanToggle.d.ts +19 -0
  33. package/dist/tui/components/BooleanToggle.js +43 -0
  34. package/dist/tui/components/EditorPanel.d.ts +50 -0
  35. package/dist/tui/components/EditorPanel.js +232 -0
  36. package/dist/tui/components/HelpPanel.d.ts +13 -0
  37. package/dist/tui/components/HelpPanel.js +69 -0
  38. package/dist/tui/components/JsonSnippetEditor.d.ts +24 -0
  39. package/dist/tui/components/JsonSnippetEditor.js +380 -0
  40. package/dist/tui/components/KeyValueEditor.d.ts +34 -0
  41. package/dist/tui/components/KeyValueEditor.js +261 -0
  42. package/dist/tui/components/ObjectEditor.d.ts +23 -0
  43. package/dist/tui/components/ObjectEditor.js +227 -0
  44. package/dist/tui/components/PresetSelector.d.ts +23 -0
  45. package/dist/tui/components/PresetSelector.js +58 -0
  46. package/dist/tui/components/Preview.d.ts +18 -0
  47. package/dist/tui/components/Preview.js +190 -0
  48. package/dist/tui/components/ScrollableContainer.d.ts +38 -0
  49. package/dist/tui/components/ScrollableContainer.js +77 -0
  50. package/dist/tui/components/SectionNav.d.ts +31 -0
  51. package/dist/tui/components/SectionNav.js +130 -0
  52. package/dist/tui/components/StringEditor.d.ts +21 -0
  53. package/dist/tui/components/StringEditor.js +29 -0
  54. package/dist/tui/hooks/useConfig.d.ts +16 -0
  55. package/dist/tui/hooks/useConfig.js +89 -0
  56. package/dist/tui/hooks/useTerminalSize.d.ts +21 -0
  57. package/dist/tui/hooks/useTerminalSize.js +48 -0
  58. package/dist/tui/utils/presets.d.ts +52 -0
  59. package/dist/tui/utils/presets.js +191 -0
  60. package/dist/tui/utils/validation.d.ts +49 -0
  61. package/dist/tui/utils/validation.js +198 -0
  62. package/dist/utils/chat-client.d.ts +144 -0
  63. package/dist/utils/chat-client.js +102 -0
  64. package/dist/utils/config.d.ts +52 -0
  65. package/dist/utils/daemon-client.d.ts +36 -0
  66. package/dist/utils/daemon-client.js +70 -0
  67. package/dist/utils/message-queue.d.ts +58 -0
  68. package/dist/utils/message-queue.js +133 -0
  69. package/dist/utils/notification.d.ts +28 -1
  70. package/dist/utils/notification.js +146 -20
  71. package/docs/MACOS-DEVELOPMENT.md +435 -0
  72. package/docs/RALPH-SETUP-TEMPLATE.md +262 -0
  73. package/package.json +6 -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,8 +14,13 @@ 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
20
+ daemon <sub> Host daemon for sandbox-to-host communication
21
+ notify [msg] Send notification to host from sandbox
22
+ action [name] Execute host actions from config.json
23
+ chat <sub> Chat client integration (Telegram, etc.)
19
24
  help Show this help message
20
25
 
21
26
  prd <subcommand> (Alias) Manage PRD entries - same as add/list/status/toggle/clean
@@ -47,6 +52,10 @@ FIX-PRD OPTIONS:
47
52
  <backup-file> Restore PRD from a specific backup file
48
53
  --verify, -v Only verify format, don't attempt to fix
49
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
+
50
59
  DOCKER SUBCOMMANDS:
51
60
  docker init Generate Dockerfile and scripts
52
61
  docker build Build image (always fetches latest Claude Code)
@@ -54,6 +63,29 @@ DOCKER SUBCOMMANDS:
54
63
  docker clean Remove Docker image and associated resources
55
64
  docker help Show docker help message
56
65
 
66
+ DAEMON SUBCOMMANDS:
67
+ daemon start Start daemon on host (listens for sandbox requests)
68
+ daemon stop Stop the daemon
69
+ daemon status Show daemon status
70
+ daemon help Show daemon help message
71
+
72
+ CHAT SUBCOMMANDS:
73
+ chat start Start chat daemon (Telegram bot)
74
+ chat status Show chat configuration status
75
+ chat test [id] Test connection by sending a message
76
+ chat help Show chat help message
77
+
78
+ NOTIFY OPTIONS:
79
+ [message] Message to send as notification
80
+ --action, -a <name> Execute specific daemon action (default: notify)
81
+ --debug, -d Show debug output
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
+
57
89
  EXAMPLES:
58
90
  ralph init # Initialize ralph (interactive CLI, language, tech selection)
59
91
  ralph init -y # Initialize with defaults (Claude + Node.js, no prompts)
@@ -75,10 +107,19 @@ EXAMPLES:
75
107
  ralph fix-prd # Validate/recover corrupted PRD file
76
108
  ralph fix-prd --verify # Check PRD format without fixing
77
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
78
113
  ralph prompt # Display resolved prompt
79
114
  ralph docker init # Generate Dockerfile for sandboxed env
80
115
  ralph docker build # Build Docker image
81
116
  ralph docker run # Run container (auto-init/build if needed)
117
+ ralph daemon start # Start daemon on host (in separate terminal)
118
+ ralph notify "Task done!" # Send notification from sandbox to host
119
+ ralph chat start # Start Telegram chat daemon
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
82
123
 
83
124
  CONFIGURATION:
84
125
  After running 'ralph init', you'll have:
@@ -110,6 +151,45 @@ CLI CONFIGURATION:
110
151
  - custom: Configure your own CLI
111
152
 
112
153
  Customize 'command', 'args', and 'yoloArgs' for other AI CLIs.
154
+
155
+ DAEMON CONFIGURATION:
156
+ The daemon allows sandbox-to-host communication without external network.
157
+ Configure custom actions in .ralph/config.json:
158
+ {
159
+ "notifyCommand": "ntfy pub mytopic",
160
+ "daemon": {
161
+ "actions": {
162
+ "build": {
163
+ "command": "./scripts/build.sh",
164
+ "description": "Run build on host"
165
+ }
166
+ }
167
+ }
168
+ }
169
+
170
+ Usage flow:
171
+ 1. Start daemon on host: ralph daemon start
172
+ 2. Run sandbox: ralph docker run
173
+ 3. From sandbox, notify: ralph notify "Task complete!"
174
+
175
+ CHAT CONFIGURATION:
176
+ Enable Telegram chat integration to control ralph from your phone:
177
+ {
178
+ "chat": {
179
+ "enabled": true,
180
+ "provider": "telegram",
181
+ "telegram": {
182
+ "botToken": "YOUR_BOT_TOKEN",
183
+ "allowedChatIds": ["123456789"]
184
+ }
185
+ }
186
+ }
187
+
188
+ Setup:
189
+ 1. Create bot with @BotFather on Telegram
190
+ 2. Add bot token to config.json
191
+ 3. Start chat daemon: ralph chat start
192
+ 4. Send commands to your bot: abc run, abc status, abc add <task>
113
193
  `;
114
194
  export function help(_args) {
115
195
  console.log(HELP_TEXT.trim());
@@ -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
@@ -196,12 +228,35 @@ export async function init(args) {
196
228
  firewall: {
197
229
  allowedDomains: [],
198
230
  },
231
+ autoStart: false,
232
+ restartCount: 0,
199
233
  },
200
234
  // Claude-specific configuration (MCP servers and skills)
201
235
  claude: {
202
236
  mcpServers: {},
203
237
  skills: selectedSkills,
204
238
  },
239
+ // Chat client configuration (e.g., Telegram)
240
+ chat: {
241
+ enabled: false,
242
+ provider: "telegram",
243
+ telegram: {
244
+ botToken: "",
245
+ allowedChatIds: [],
246
+ },
247
+ },
248
+ // Daemon configuration for sandbox-to-host communication
249
+ daemon: {
250
+ actions: macOsActions,
251
+ // Event handlers - each event can trigger multiple daemon actions
252
+ // Available events: task_complete, ralph_complete, iteration_complete, error
253
+ events: {
254
+ // Example: notify after each task completes
255
+ // task_complete: [{ action: "notify", message: "Task complete: {{task}}" }],
256
+ // Example: notify when ralph finishes all work
257
+ // ralph_complete: [{ action: "notify", message: "Ralph finished!" }],
258
+ },
259
+ },
205
260
  };
206
261
  const configPath = join(ralphDir, CONFIG_FILE);
207
262
  writeFileSync(configPath, JSON.stringify(configData, null, 2) + "\n");
@@ -241,6 +296,85 @@ export async function init(args) {
241
296
  else {
242
297
  console.log(`Skipped ${RALPH_DIR}/${PROGRESS_FILE} (already exists)`);
243
298
  }
299
+ // Create .gitignore if not exists (protects secrets like API tokens)
300
+ const gitignorePath = join(ralphDir, ".gitignore");
301
+ if (!existsSync(gitignorePath)) {
302
+ const gitignoreContent = `# Ralph CLI - Ignore sensitive and runtime files
303
+ # config.json may contain API tokens (Telegram, etc.)
304
+ config.json
305
+
306
+ # Runtime state files
307
+ messages.json
308
+ chat-state.json
309
+
310
+ # Service logs
311
+ daemon.log
312
+ chat.log
313
+
314
+ # Docker build artifacts
315
+ docker/.config-hash
316
+ `;
317
+ writeFileSync(gitignorePath, gitignoreContent);
318
+ console.log(`Created ${RALPH_DIR}/.gitignore`);
319
+ }
320
+ else {
321
+ console.log(`Skipped ${RALPH_DIR}/.gitignore (already exists)`);
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
+ }
244
378
  // Copy PRD guide file from package if not exists
245
379
  const prdGuidePath = join(ralphDir, PRD_GUIDE_FILE);
246
380
  if (!existsSync(prdGuidePath)) {
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Listen command - runs in sandbox to process commands from host.
3
+ * This enables Telegram/chat commands to execute inside the container.
4
+ */
5
+ /**
6
+ * Main listen command handler.
7
+ */
8
+ export declare function listen(args: string[]): Promise<void>;