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.
- package/README.md +99 -15
- package/dist/commands/action.d.ts +7 -0
- package/dist/commands/action.js +276 -0
- package/dist/commands/chat.d.ts +8 -0
- package/dist/commands/chat.js +701 -0
- package/dist/commands/config.d.ts +1 -0
- package/dist/commands/config.js +51 -0
- package/dist/commands/daemon.d.ts +23 -0
- package/dist/commands/daemon.js +422 -0
- package/dist/commands/docker.js +82 -4
- package/dist/commands/fix-config.d.ts +4 -0
- package/dist/commands/fix-config.js +388 -0
- package/dist/commands/help.js +80 -0
- package/dist/commands/init.js +135 -1
- package/dist/commands/listen.d.ts +8 -0
- package/dist/commands/listen.js +280 -0
- package/dist/commands/notify.d.ts +7 -0
- package/dist/commands/notify.js +165 -0
- package/dist/commands/once.js +8 -8
- package/dist/commands/prd.js +2 -2
- package/dist/commands/run.js +25 -12
- package/dist/config/languages.json +4 -0
- package/dist/index.js +14 -0
- package/dist/providers/telegram.d.ts +39 -0
- package/dist/providers/telegram.js +256 -0
- 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 +144 -0
- package/dist/utils/chat-client.js +102 -0
- package/dist/utils/config.d.ts +52 -0
- package/dist/utils/daemon-client.d.ts +36 -0
- package/dist/utils/daemon-client.js +70 -0
- package/dist/utils/message-queue.d.ts +58 -0
- package/dist/utils/message-queue.js +133 -0
- package/dist/utils/notification.d.ts +28 -1
- package/dist/utils/notification.js +146 -20
- package/docs/MACOS-DEVELOPMENT.md +435 -0
- package/docs/RALPH-SETUP-TEMPLATE.md +262 -0
- 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
|
+
}
|
package/dist/commands/help.js
CHANGED
|
@@ -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());
|
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
|
|
@@ -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)) {
|