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 CHANGED
@@ -1,5 +1,7 @@
1
1
  // Re-export for programmatic usage
2
2
  export * from "./src/types";
3
- export { buildConfig, loadPathsFile, validateConfig } from "./src/paths";
3
+ export { loadPathsFile, validateConfig, resolvePathsConfig, toRelativePath } from "./src/paths";
4
4
  export { createPrompt, buildPrompt } from "./src/prompt";
5
- export { run, cleanupSubprocess } from "./src/runner";
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.2",
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
- Bun.exit(1);
29
+ exit(1);
27
30
  }
28
31
  setShuttingDown();
29
32
  cleanupSubprocess();
30
33
  console.log("\n");
31
34
  consola.info("Cancelled.");
32
- // Use Bun.exit for immediate termination
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
- Bun.exit(0);
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 authentication first - before any prompts
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, type Dirent } from "fs/promises";
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 { isShuttingDown } from "./state";
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
- // Handle Ctrl+C (returns Symbol) or shutdown in progress or unexpected values
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
- if (typeof selected === "symbol" || !selected || (Array.isArray(selected) && selected.length === 0)) {
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
- return selected as string[];
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 index of default for initial selection
289
- const initialIndex = defaultRule ? files.findIndex((f) => f === defaultRule) : 0;
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: initialIndex >= 0 ? initialIndex : 0,
294
+ initial: initialValue,
296
295
  });
297
296
 
298
- if (typeof selected === "symbol") throw new Error("Selection cancelled");
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
- // Find initial index
321
- let initialIndex = 0;
322
- if (defaultDir) {
323
- const idx = defaultDir === "." ? 0 : dirs.findIndex((d) => d === defaultDir) + 1;
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: initialIndex,
329
+ initial: initialValue,
332
330
  });
333
331
 
334
- if (typeof selected === "symbol") throw new Error("Selection cancelled");
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
- if (typeof action === "symbol") {
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
- // Shutdown state
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
- // Subprocess tracking
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
  /**