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.
- package/dist/commands/action.d.ts +7 -0
- package/dist/commands/action.js +276 -0
- package/dist/commands/chat.js +95 -7
- package/dist/commands/config.js +6 -18
- package/dist/commands/fix-config.d.ts +4 -0
- package/dist/commands/fix-config.js +388 -0
- package/dist/commands/help.js +17 -0
- package/dist/commands/init.js +89 -2
- package/dist/commands/listen.js +50 -9
- package/dist/commands/prd.js +2 -2
- package/dist/config/languages.json +4 -0
- package/dist/index.js +4 -0
- package/dist/providers/telegram.d.ts +6 -2
- package/dist/providers/telegram.js +68 -2
- package/dist/templates/macos-scripts.d.ts +42 -0
- package/dist/templates/macos-scripts.js +448 -0
- package/dist/tui/ConfigEditor.d.ts +7 -0
- package/dist/tui/ConfigEditor.js +313 -0
- package/dist/tui/components/ArrayEditor.d.ts +22 -0
- package/dist/tui/components/ArrayEditor.js +193 -0
- package/dist/tui/components/BooleanToggle.d.ts +19 -0
- package/dist/tui/components/BooleanToggle.js +43 -0
- package/dist/tui/components/EditorPanel.d.ts +50 -0
- package/dist/tui/components/EditorPanel.js +232 -0
- package/dist/tui/components/HelpPanel.d.ts +13 -0
- package/dist/tui/components/HelpPanel.js +69 -0
- package/dist/tui/components/JsonSnippetEditor.d.ts +24 -0
- package/dist/tui/components/JsonSnippetEditor.js +380 -0
- package/dist/tui/components/KeyValueEditor.d.ts +34 -0
- package/dist/tui/components/KeyValueEditor.js +261 -0
- package/dist/tui/components/ObjectEditor.d.ts +23 -0
- package/dist/tui/components/ObjectEditor.js +227 -0
- package/dist/tui/components/PresetSelector.d.ts +23 -0
- package/dist/tui/components/PresetSelector.js +58 -0
- package/dist/tui/components/Preview.d.ts +18 -0
- package/dist/tui/components/Preview.js +190 -0
- package/dist/tui/components/ScrollableContainer.d.ts +38 -0
- package/dist/tui/components/ScrollableContainer.js +77 -0
- package/dist/tui/components/SectionNav.d.ts +31 -0
- package/dist/tui/components/SectionNav.js +130 -0
- package/dist/tui/components/StringEditor.d.ts +21 -0
- package/dist/tui/components/StringEditor.js +29 -0
- package/dist/tui/hooks/useConfig.d.ts +16 -0
- package/dist/tui/hooks/useConfig.js +89 -0
- package/dist/tui/hooks/useTerminalSize.d.ts +21 -0
- package/dist/tui/hooks/useTerminalSize.js +48 -0
- package/dist/tui/utils/presets.d.ts +52 -0
- package/dist/tui/utils/presets.js +191 -0
- package/dist/tui/utils/validation.d.ts +49 -0
- package/dist/tui/utils/validation.js +198 -0
- package/dist/utils/chat-client.d.ts +31 -1
- package/dist/utils/chat-client.js +27 -1
- package/dist/utils/config.d.ts +7 -2
- package/docs/MACOS-DEVELOPMENT.md +435 -0
- 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
|
+
}
|
package/dist/commands/help.js
CHANGED
|
@@ -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:
|
package/dist/commands/init.js
CHANGED
|
@@ -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)) {
|
package/dist/commands/listen.js
CHANGED
|
@@ -91,8 +91,16 @@ async function processMessage(message, messagesPath, debug) {
|
|
|
91
91
|
}
|
|
92
92
|
case "run": {
|
|
93
93
|
// Start ralph run in background
|
|
94
|
-
|
|
95
|
-
const
|
|
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]
|
|
216
|
-
run
|
|
217
|
-
status
|
|
218
|
-
ping
|
|
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
|
package/dist/commands/prd.js
CHANGED
|
@@ -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
|
|
95
|
-
console.log(` ${originalIndex + 1}. ${
|
|
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" },
|