cralph 1.0.0-beta.2 → 1.0.0-beta.3
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/index.ts +4 -2
- package/package.json +3 -2
- package/src/cli.ts +24 -6
- package/src/paths.ts +25 -28
- package/src/platform.ts +77 -0
- package/src/runner.ts +4 -2
- package/src/state.ts +28 -3
package/index.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
// Re-export for programmatic usage
|
|
2
2
|
export * from "./src/types";
|
|
3
|
-
export {
|
|
3
|
+
export { loadPathsFile, validateConfig, resolvePathsConfig, toRelativePath } from "./src/paths";
|
|
4
4
|
export { createPrompt, buildPrompt } from "./src/prompt";
|
|
5
|
-
export { run,
|
|
5
|
+
export { run, checkClaudeAuth } from "./src/runner";
|
|
6
|
+
export { cleanupSubprocess, setShuttingDown, isShuttingDown } from "./src/state";
|
|
7
|
+
export { getPlatform, isClaudeInstalled, checkClaudeInstallation } from "./src/platform";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cralph",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.3",
|
|
4
4
|
"description": "Claude in a loop. Point at refs, give it a rule, let it cook.",
|
|
5
5
|
"author": "mguleryuz",
|
|
6
6
|
"license": "MIT",
|
|
@@ -30,7 +30,8 @@
|
|
|
30
30
|
"assets"
|
|
31
31
|
],
|
|
32
32
|
"scripts": {
|
|
33
|
-
"start": "bun run src/cli.ts"
|
|
33
|
+
"start": "bun run src/cli.ts",
|
|
34
|
+
"type-check": "tsgo --noEmit --pretty"
|
|
34
35
|
},
|
|
35
36
|
"devDependencies": {
|
|
36
37
|
"@types/bun": "latest"
|
package/src/cli.ts
CHANGED
|
@@ -16,28 +16,30 @@ import {
|
|
|
16
16
|
} from "./paths";
|
|
17
17
|
import { run, checkClaudeAuth } from "./runner";
|
|
18
18
|
import type { RalphConfig } from "./types";
|
|
19
|
-
import { setShuttingDown, isShuttingDown, cleanupSubprocess } from "./state";
|
|
19
|
+
import { setShuttingDown, isShuttingDown, cleanupSubprocess, throwIfCancelled } from "./state";
|
|
20
|
+
import { checkClaudeInstallation } from "./platform";
|
|
20
21
|
|
|
21
22
|
// Graceful shutdown on Ctrl+C
|
|
22
23
|
function setupGracefulExit() {
|
|
24
|
+
const exit = (code: number) => process.exit(code);
|
|
25
|
+
|
|
23
26
|
process.on("SIGINT", () => {
|
|
24
27
|
if (isShuttingDown()) {
|
|
25
28
|
// Force exit on second Ctrl+C
|
|
26
|
-
|
|
29
|
+
exit(1);
|
|
27
30
|
}
|
|
28
31
|
setShuttingDown();
|
|
29
32
|
cleanupSubprocess();
|
|
30
33
|
console.log("\n");
|
|
31
34
|
consola.info("Cancelled.");
|
|
32
|
-
|
|
33
|
-
Bun.exit(0);
|
|
35
|
+
exit(0);
|
|
34
36
|
});
|
|
35
37
|
|
|
36
38
|
// Also handle SIGTERM
|
|
37
39
|
process.on("SIGTERM", () => {
|
|
38
40
|
setShuttingDown();
|
|
39
41
|
cleanupSubprocess();
|
|
40
|
-
|
|
42
|
+
exit(0);
|
|
41
43
|
});
|
|
42
44
|
}
|
|
43
45
|
|
|
@@ -88,7 +90,17 @@ const main = defineCommand({
|
|
|
88
90
|
let config: RalphConfig;
|
|
89
91
|
|
|
90
92
|
try {
|
|
91
|
-
// Check Claude
|
|
93
|
+
// Check Claude CLI is installed first
|
|
94
|
+
const claudeCheck = await checkClaudeInstallation();
|
|
95
|
+
|
|
96
|
+
if (!claudeCheck.installed) {
|
|
97
|
+
consola.error("Claude CLI is not installed\n");
|
|
98
|
+
consola.box(claudeCheck.installInstructions);
|
|
99
|
+
consola.info("After installing, run cralph again.");
|
|
100
|
+
process.exit(1);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Check Claude authentication
|
|
92
104
|
consola.start("Checking Claude authentication...");
|
|
93
105
|
const isAuthed = await checkClaudeAuth();
|
|
94
106
|
|
|
@@ -144,8 +156,11 @@ const main = defineCommand({
|
|
|
144
156
|
// Offer to save config
|
|
145
157
|
const saveConfig = await consola.prompt("Save configuration to .ralph/paths.json?", {
|
|
146
158
|
type: "confirm",
|
|
159
|
+
cancel: "symbol",
|
|
147
160
|
initial: true,
|
|
148
161
|
});
|
|
162
|
+
|
|
163
|
+
throwIfCancelled(saveConfig);
|
|
149
164
|
|
|
150
165
|
if (saveConfig === true) {
|
|
151
166
|
const ralphDir = join(cwd, ".ralph");
|
|
@@ -179,8 +194,11 @@ const main = defineCommand({
|
|
|
179
194
|
if (!args.yes) {
|
|
180
195
|
const proceed = await consola.prompt("Start processing?", {
|
|
181
196
|
type: "confirm",
|
|
197
|
+
cancel: "symbol",
|
|
182
198
|
initial: true,
|
|
183
199
|
});
|
|
200
|
+
|
|
201
|
+
throwIfCancelled(proceed);
|
|
184
202
|
|
|
185
203
|
if (proceed !== true) {
|
|
186
204
|
consola.info("Cancelled.");
|
package/src/paths.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { consola } from "consola";
|
|
2
2
|
import { resolve, join } from "path";
|
|
3
|
-
import { readdir, stat, mkdir
|
|
3
|
+
import { readdir, stat, mkdir } from "fs/promises";
|
|
4
|
+
import type { Dirent } from "fs";
|
|
4
5
|
import type { PathsFileConfig, RalphConfig } from "./types";
|
|
5
6
|
import { isAccessError, shouldExcludeDir } from "./platform";
|
|
6
|
-
import {
|
|
7
|
+
import { throwIfCancelled } from "./state";
|
|
7
8
|
|
|
8
9
|
// Starter rule template for new projects
|
|
9
10
|
const STARTER_RULE = `I want a file named hello.txt
|
|
@@ -199,6 +200,7 @@ export async function selectRefs(cwd: string, defaults?: string[], autoConfirm?:
|
|
|
199
200
|
`No .ralph/ found in ${cwd}`,
|
|
200
201
|
{
|
|
201
202
|
type: "select",
|
|
203
|
+
cancel: "symbol",
|
|
202
204
|
options: [
|
|
203
205
|
{ label: "📦 Create starter structure", value: "create" },
|
|
204
206
|
{ label: "⚙️ Configure manually", value: "manual" },
|
|
@@ -206,16 +208,9 @@ export async function selectRefs(cwd: string, defaults?: string[], autoConfirm?:
|
|
|
206
208
|
}
|
|
207
209
|
);
|
|
208
210
|
|
|
209
|
-
|
|
210
|
-
if (typeof action === "symbol" || isShuttingDown() || (action !== "create" && action !== "manual")) {
|
|
211
|
-
throw new Error("Selection cancelled");
|
|
212
|
-
}
|
|
211
|
+
throwIfCancelled(action);
|
|
213
212
|
|
|
214
213
|
if (action === "create") {
|
|
215
|
-
// Double-check we're not shutting down before executing
|
|
216
|
-
if (isShuttingDown()) {
|
|
217
|
-
throw new Error("Selection cancelled");
|
|
218
|
-
}
|
|
219
214
|
await createStarterStructure(cwd);
|
|
220
215
|
process.exit(0);
|
|
221
216
|
}
|
|
@@ -252,16 +247,19 @@ export async function selectRefs(cwd: string, defaults?: string[], autoConfirm?:
|
|
|
252
247
|
console.log(CONTROLS);
|
|
253
248
|
const selected = await consola.prompt("Select refs directories:", {
|
|
254
249
|
type: "multiselect",
|
|
250
|
+
cancel: "symbol",
|
|
255
251
|
options,
|
|
256
252
|
initial: initialValues,
|
|
257
253
|
});
|
|
258
254
|
|
|
259
|
-
// Handle cancel (symbol) or empty result
|
|
260
|
-
|
|
255
|
+
// Handle cancel (symbol), shutdown, or empty result
|
|
256
|
+
throwIfCancelled(selected);
|
|
257
|
+
if (!selected || (Array.isArray(selected) && selected.length === 0)) {
|
|
261
258
|
throw new Error("Selection cancelled");
|
|
262
259
|
}
|
|
263
260
|
|
|
264
|
-
|
|
261
|
+
// Cast is safe: multiselect with string values returns string[]
|
|
262
|
+
return selected as unknown as string[];
|
|
265
263
|
}
|
|
266
264
|
|
|
267
265
|
/**
|
|
@@ -285,17 +283,18 @@ export async function selectRule(cwd: string, defaultRule?: string): Promise<str
|
|
|
285
283
|
hint: f === defaultRule ? "current" : (f.endsWith(".mdc") ? "cursor rule" : "markdown"),
|
|
286
284
|
}));
|
|
287
285
|
|
|
288
|
-
// Find
|
|
289
|
-
const
|
|
286
|
+
// Find initial value for default selection
|
|
287
|
+
const initialValue = defaultRule && files.includes(defaultRule) ? defaultRule : files[0];
|
|
290
288
|
|
|
291
289
|
console.log(CONTROLS);
|
|
292
290
|
const selected = await consola.prompt("Select rule file:", {
|
|
293
291
|
type: "select",
|
|
292
|
+
cancel: "symbol",
|
|
294
293
|
options,
|
|
295
|
-
initial:
|
|
294
|
+
initial: initialValue,
|
|
296
295
|
});
|
|
297
296
|
|
|
298
|
-
|
|
297
|
+
throwIfCancelled(selected);
|
|
299
298
|
return selected as string;
|
|
300
299
|
}
|
|
301
300
|
|
|
@@ -317,21 +316,20 @@ export async function selectOutput(cwd: string, defaultOutput?: string): Promise
|
|
|
317
316
|
})),
|
|
318
317
|
];
|
|
319
318
|
|
|
320
|
-
//
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
if (idx >= 0) initialIndex = idx;
|
|
325
|
-
}
|
|
319
|
+
// Determine initial value for default selection
|
|
320
|
+
const initialValue = defaultDir && (defaultDir === "." || dirs.includes(defaultDir))
|
|
321
|
+
? defaultDir
|
|
322
|
+
: ".";
|
|
326
323
|
|
|
327
324
|
console.log(CONTROLS);
|
|
328
325
|
const selected = await consola.prompt("Select output directory:", {
|
|
329
326
|
type: "select",
|
|
327
|
+
cancel: "symbol",
|
|
330
328
|
options,
|
|
331
|
-
initial:
|
|
329
|
+
initial: initialValue,
|
|
332
330
|
});
|
|
333
331
|
|
|
334
|
-
|
|
332
|
+
throwIfCancelled(selected);
|
|
335
333
|
|
|
336
334
|
if (selected === ".") {
|
|
337
335
|
return cwd;
|
|
@@ -360,6 +358,7 @@ export async function checkForPathsFile(cwd: string, autoRun?: boolean): Promise
|
|
|
360
358
|
`Found .ralph/paths.json. What would you like to do?`,
|
|
361
359
|
{
|
|
362
360
|
type: "select",
|
|
361
|
+
cancel: "symbol",
|
|
363
362
|
options: [
|
|
364
363
|
{ label: "🚀 Run with this config", value: "run" },
|
|
365
364
|
{ label: "✏️ Edit configuration", value: "edit" },
|
|
@@ -367,9 +366,7 @@ export async function checkForPathsFile(cwd: string, autoRun?: boolean): Promise
|
|
|
367
366
|
}
|
|
368
367
|
);
|
|
369
368
|
|
|
370
|
-
|
|
371
|
-
throw new Error("Selection cancelled");
|
|
372
|
-
}
|
|
369
|
+
throwIfCancelled(action);
|
|
373
370
|
|
|
374
371
|
if (action === "run") {
|
|
375
372
|
return { action: "run", path: filePath };
|
package/src/platform.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { platform } from "os";
|
|
2
|
+
import { which } from "bun";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Supported platforms
|
|
@@ -111,3 +112,79 @@ export const EXCLUDED_DIRS = [
|
|
|
111
112
|
export function shouldExcludeDir(dirName: string): boolean {
|
|
112
113
|
return EXCLUDED_DIRS.includes(dirName) || isSystemExcludedDir(dirName);
|
|
113
114
|
}
|
|
115
|
+
|
|
116
|
+
// ============================================================================
|
|
117
|
+
// Claude CLI Detection
|
|
118
|
+
// ============================================================================
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Platform-specific install instructions for Claude CLI
|
|
122
|
+
*/
|
|
123
|
+
const CLAUDE_INSTALL_INSTRUCTIONS: Record<Platform, string> = {
|
|
124
|
+
darwin: `Install Claude CLI:
|
|
125
|
+
npm install -g @anthropic-ai/claude-code
|
|
126
|
+
|
|
127
|
+
Or via Homebrew:
|
|
128
|
+
brew install claude`,
|
|
129
|
+
linux: `Install Claude CLI:
|
|
130
|
+
npm install -g @anthropic-ai/claude-code`,
|
|
131
|
+
win32: `Install Claude CLI:
|
|
132
|
+
npm install -g @anthropic-ai/claude-code`,
|
|
133
|
+
unknown: `Install Claude CLI:
|
|
134
|
+
npm install -g @anthropic-ai/claude-code`,
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Get platform-specific Claude CLI install instructions
|
|
139
|
+
*/
|
|
140
|
+
export function getClaudeInstallInstructions(): string {
|
|
141
|
+
return CLAUDE_INSTALL_INSTRUCTIONS[getPlatform()];
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Check if Claude CLI is installed and available in PATH
|
|
146
|
+
*/
|
|
147
|
+
export async function isClaudeInstalled(): Promise<boolean> {
|
|
148
|
+
try {
|
|
149
|
+
const claudePath = which("claude");
|
|
150
|
+
return claudePath !== null;
|
|
151
|
+
} catch {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Result of Claude CLI check
|
|
158
|
+
*/
|
|
159
|
+
export interface ClaudeCheckResult {
|
|
160
|
+
installed: boolean;
|
|
161
|
+
path?: string;
|
|
162
|
+
installInstructions: string;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Check Claude CLI installation and return detailed result
|
|
167
|
+
*/
|
|
168
|
+
export async function checkClaudeInstallation(): Promise<ClaudeCheckResult> {
|
|
169
|
+
const installInstructions = getClaudeInstallInstructions();
|
|
170
|
+
|
|
171
|
+
try {
|
|
172
|
+
const claudePath = which("claude");
|
|
173
|
+
if (claudePath) {
|
|
174
|
+
return {
|
|
175
|
+
installed: true,
|
|
176
|
+
path: claudePath,
|
|
177
|
+
installInstructions,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
return {
|
|
181
|
+
installed: false,
|
|
182
|
+
installInstructions,
|
|
183
|
+
};
|
|
184
|
+
} catch {
|
|
185
|
+
return {
|
|
186
|
+
installed: false,
|
|
187
|
+
installInstructions,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
}
|
package/src/runner.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { mkdir } from "fs/promises";
|
|
|
4
4
|
import { homedir } from "os";
|
|
5
5
|
import type { RalphConfig, RunnerState, IterationResult } from "./types";
|
|
6
6
|
import { createPrompt } from "./prompt";
|
|
7
|
-
import { setCurrentProcess } from "./state";
|
|
7
|
+
import { setCurrentProcess, throwIfCancelled } from "./state";
|
|
8
8
|
|
|
9
9
|
const COMPLETION_SIGNAL = "<promise>COMPLETE</promise>";
|
|
10
10
|
const AUTH_CACHE_TTL_MS = 6 * 60 * 60 * 1000; // 6 hours
|
|
@@ -103,7 +103,6 @@ export async function checkClaudeAuth(): Promise<boolean> {
|
|
|
103
103
|
}
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
-
|
|
107
106
|
/**
|
|
108
107
|
* Check if the TODO file is in a clean/initial state
|
|
109
108
|
*/
|
|
@@ -150,10 +149,13 @@ Ralph Session: ${state.startTime.toISOString()}
|
|
|
150
149
|
"Found existing TODO with progress. Reset to start fresh?",
|
|
151
150
|
{
|
|
152
151
|
type: "confirm",
|
|
152
|
+
cancel: "symbol",
|
|
153
153
|
initial: true,
|
|
154
154
|
}
|
|
155
155
|
);
|
|
156
156
|
|
|
157
|
+
throwIfCancelled(response);
|
|
158
|
+
|
|
157
159
|
if (response === true) {
|
|
158
160
|
await Bun.write(state.todoFile, INITIAL_TODO_CONTENT);
|
|
159
161
|
consola.info("TODO reset to clean state");
|
package/src/state.ts
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Global state shared across modules
|
|
3
|
-
*
|
|
3
|
+
*
|
|
4
4
|
* This module provides centralized state management for:
|
|
5
5
|
* - Graceful shutdown handling (Ctrl+C / SIGINT / SIGTERM)
|
|
6
|
+
* - Prompt cancellation detection
|
|
6
7
|
* - Subprocess tracking for cleanup
|
|
7
8
|
*/
|
|
8
9
|
|
|
9
|
-
//
|
|
10
|
+
// ============================================================================
|
|
11
|
+
// Shutdown State
|
|
12
|
+
// ============================================================================
|
|
13
|
+
|
|
10
14
|
let shuttingDown = false;
|
|
11
15
|
|
|
12
16
|
/**
|
|
@@ -30,7 +34,28 @@ export function resetShutdownState(): void {
|
|
|
30
34
|
shuttingDown = false;
|
|
31
35
|
}
|
|
32
36
|
|
|
33
|
-
//
|
|
37
|
+
// ============================================================================
|
|
38
|
+
// Prompt Cancellation
|
|
39
|
+
// ============================================================================
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Check if a prompt result indicates cancellation and throw if so.
|
|
43
|
+
* Handles Symbol returns from consola prompts (when cancel: "symbol")
|
|
44
|
+
* and shutdown state.
|
|
45
|
+
*
|
|
46
|
+
* @param result - The result from a consola.prompt() call
|
|
47
|
+
* @throws Error with message "Selection cancelled" if cancelled
|
|
48
|
+
*/
|
|
49
|
+
export function throwIfCancelled(result: unknown): void {
|
|
50
|
+
if (typeof result === "symbol" || isShuttingDown()) {
|
|
51
|
+
throw new Error("Selection cancelled");
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ============================================================================
|
|
56
|
+
// Subprocess Tracking
|
|
57
|
+
// ============================================================================
|
|
58
|
+
|
|
34
59
|
let currentProc: ReturnType<typeof Bun.spawn> | null = null;
|
|
35
60
|
|
|
36
61
|
/**
|