cralph 1.0.0-beta.0 → 1.0.0-beta.2
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 +35 -14
- package/package.json +1 -1
- package/src/cli.ts +16 -23
- package/src/paths.ts +117 -72
- package/src/platform.ts +113 -0
- package/src/runner.ts +3 -19
- package/src/state.ts +55 -0
package/README.md
CHANGED
|
@@ -36,7 +36,7 @@ bun add -g cralph
|
|
|
36
36
|
## Quick Start
|
|
37
37
|
|
|
38
38
|
```bash
|
|
39
|
-
# In
|
|
39
|
+
# In any directory without .ralph/ - creates starter structure
|
|
40
40
|
cralph
|
|
41
41
|
|
|
42
42
|
# Edit rule.md with your instructions, then run again
|
|
@@ -59,10 +59,11 @@ cralph --yes
|
|
|
59
59
|
## How It Works
|
|
60
60
|
|
|
61
61
|
1. Checks Claude CLI auth (cached for 6 hours)
|
|
62
|
-
2.
|
|
63
|
-
3.
|
|
64
|
-
4.
|
|
65
|
-
5.
|
|
62
|
+
2. Looks for `.ralph/` in current directory only (not subdirectories)
|
|
63
|
+
3. Loads config from `.ralph/paths.json` or creates starter structure
|
|
64
|
+
4. Runs `claude -p --dangerously-skip-permissions` in a loop
|
|
65
|
+
5. Claude updates `.ralph/TODO.md` after each iteration
|
|
66
|
+
6. Stops when Claude outputs `<promise>COMPLETE</promise>`
|
|
66
67
|
|
|
67
68
|
## Config
|
|
68
69
|
|
|
@@ -102,23 +103,31 @@ Claude maintains this structure:
|
|
|
102
103
|
Any relevant context
|
|
103
104
|
```
|
|
104
105
|
|
|
105
|
-
## First Run (
|
|
106
|
+
## First Run (No .ralph/ in cwd)
|
|
106
107
|
|
|
107
108
|
```
|
|
108
|
-
|
|
109
|
+
❯ No .ralph/ found in /path/to/dir
|
|
110
|
+
● 📦 Create starter structure
|
|
111
|
+
○ ⚙️ Configure manually
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Select **Create starter structure** to generate the default config:
|
|
109
115
|
|
|
116
|
+
```
|
|
110
117
|
ℹ Created .ralph/refs/ directory
|
|
111
118
|
ℹ Created .ralph/rule.md with starter template
|
|
112
119
|
ℹ Created .ralph/paths.json
|
|
113
120
|
|
|
114
|
-
|
|
115
|
-
│
|
|
116
|
-
│
|
|
117
|
-
│
|
|
118
|
-
|
|
121
|
+
╭─────────────────────────────────────────────────╮
|
|
122
|
+
│ 1. Add source files to .ralph/refs/ │
|
|
123
|
+
│ 2. Edit .ralph/rule.md with your instructions │
|
|
124
|
+
│ 3. Run cralph again │
|
|
125
|
+
╰─────────────────────────────────────────────────╯
|
|
119
126
|
```
|
|
120
127
|
|
|
121
|
-
|
|
128
|
+
Select **Configure manually** to skip starter creation and pick your own refs/rule/output.
|
|
129
|
+
|
|
130
|
+
Use `--yes` to auto-create starter structure (for CI/automation).
|
|
122
131
|
|
|
123
132
|
## Prompts
|
|
124
133
|
|
|
@@ -140,13 +149,25 @@ Use `--yes` to skip confirmation (for CI/automation).
|
|
|
140
149
|
- **Enter** - Confirm
|
|
141
150
|
- **Ctrl+C** - Exit
|
|
142
151
|
|
|
152
|
+
## Platform Support
|
|
153
|
+
|
|
154
|
+
cralph works on **macOS**, **Linux**, and **Windows** with platform-specific handling:
|
|
155
|
+
|
|
156
|
+
| Platform | Protected Directories Skipped |
|
|
157
|
+
|----------|------------------------------|
|
|
158
|
+
| macOS | Library, Photos Library, Photo Booth Library |
|
|
159
|
+
| Linux | lost+found, proc, sys |
|
|
160
|
+
| Windows | System Volume Information, $Recycle.Bin, Windows |
|
|
161
|
+
|
|
162
|
+
Permission errors (`EPERM`, `EACCES`) are handled gracefully on all platforms, allowing the CLI to run from any directory.
|
|
163
|
+
|
|
143
164
|
## Testing
|
|
144
165
|
|
|
145
166
|
```bash
|
|
146
167
|
bun test
|
|
147
168
|
```
|
|
148
169
|
|
|
149
|
-
- **Unit tests** - Config, prompt building, CLI
|
|
170
|
+
- **Unit tests** - Config, prompt building, CLI, access error handling, platform detection, shutdown state
|
|
150
171
|
- **E2E tests** - Full loop with Claude (requires auth)
|
|
151
172
|
|
|
152
173
|
## Requirements
|
package/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -5,38 +5,39 @@ import { consola } from "consola";
|
|
|
5
5
|
import { resolve, join } from "path";
|
|
6
6
|
import { mkdir } from "fs/promises";
|
|
7
7
|
import {
|
|
8
|
-
buildConfig,
|
|
9
8
|
loadPathsFile,
|
|
10
9
|
validateConfig,
|
|
11
10
|
selectRefs,
|
|
12
11
|
selectRule,
|
|
13
12
|
selectOutput,
|
|
14
13
|
checkForPathsFile,
|
|
14
|
+
resolvePathsConfig,
|
|
15
|
+
toRelativePath,
|
|
15
16
|
} from "./paths";
|
|
16
|
-
import { run,
|
|
17
|
+
import { run, checkClaudeAuth } from "./runner";
|
|
17
18
|
import type { RalphConfig } from "./types";
|
|
19
|
+
import { setShuttingDown, isShuttingDown, cleanupSubprocess } from "./state";
|
|
18
20
|
|
|
19
21
|
// Graceful shutdown on Ctrl+C
|
|
20
22
|
function setupGracefulExit() {
|
|
21
|
-
let shuttingDown = false;
|
|
22
|
-
|
|
23
23
|
process.on("SIGINT", () => {
|
|
24
|
-
if (
|
|
24
|
+
if (isShuttingDown()) {
|
|
25
25
|
// Force exit on second Ctrl+C
|
|
26
|
-
|
|
26
|
+
Bun.exit(1);
|
|
27
27
|
}
|
|
28
|
-
|
|
28
|
+
setShuttingDown();
|
|
29
29
|
cleanupSubprocess();
|
|
30
30
|
console.log("\n");
|
|
31
31
|
consola.info("Cancelled.");
|
|
32
|
-
// Use
|
|
33
|
-
|
|
32
|
+
// Use Bun.exit for immediate termination
|
|
33
|
+
Bun.exit(0);
|
|
34
34
|
});
|
|
35
35
|
|
|
36
36
|
// Also handle SIGTERM
|
|
37
37
|
process.on("SIGTERM", () => {
|
|
38
|
+
setShuttingDown();
|
|
38
39
|
cleanupSubprocess();
|
|
39
|
-
|
|
40
|
+
Bun.exit(0);
|
|
40
41
|
});
|
|
41
42
|
}
|
|
42
43
|
|
|
@@ -109,11 +110,7 @@ const main = defineCommand({
|
|
|
109
110
|
// Use existing config file
|
|
110
111
|
consola.info(`Loading config from ${pathsFileResult.path}`);
|
|
111
112
|
const loaded = await loadPathsFile(pathsFileResult.path);
|
|
112
|
-
config =
|
|
113
|
-
refs: loaded.refs.map((r) => resolve(cwd, r)),
|
|
114
|
-
rule: resolve(cwd, loaded.rule),
|
|
115
|
-
output: resolve(cwd, loaded.output),
|
|
116
|
-
};
|
|
113
|
+
config = resolvePathsConfig(loaded, cwd);
|
|
117
114
|
} else {
|
|
118
115
|
// Load existing config for edit mode defaults
|
|
119
116
|
let existingConfig: RalphConfig | null = null;
|
|
@@ -123,11 +120,7 @@ const main = defineCommand({
|
|
|
123
120
|
const file = Bun.file(filePath);
|
|
124
121
|
if (await file.exists()) {
|
|
125
122
|
const loaded = await loadPathsFile(filePath);
|
|
126
|
-
existingConfig =
|
|
127
|
-
refs: loaded.refs.map((r) => resolve(cwd, r)),
|
|
128
|
-
rule: resolve(cwd, loaded.rule),
|
|
129
|
-
output: resolve(cwd, loaded.output),
|
|
130
|
-
};
|
|
123
|
+
existingConfig = resolvePathsConfig(loaded, cwd);
|
|
131
124
|
}
|
|
132
125
|
} else {
|
|
133
126
|
consola.info("Interactive configuration mode");
|
|
@@ -159,9 +152,9 @@ const main = defineCommand({
|
|
|
159
152
|
await mkdir(ralphDir, { recursive: true });
|
|
160
153
|
|
|
161
154
|
const pathsConfig = {
|
|
162
|
-
refs: config.refs.map((r) =>
|
|
163
|
-
rule:
|
|
164
|
-
output: config.output
|
|
155
|
+
refs: config.refs.map((r) => toRelativePath(r, cwd)),
|
|
156
|
+
rule: toRelativePath(config.rule, cwd),
|
|
157
|
+
output: toRelativePath(config.output, cwd),
|
|
165
158
|
};
|
|
166
159
|
await Bun.write(
|
|
167
160
|
join(ralphDir, "paths.json"),
|
package/src/paths.ts
CHANGED
|
@@ -1,43 +1,69 @@
|
|
|
1
1
|
import { consola } from "consola";
|
|
2
2
|
import { resolve, join } from "path";
|
|
3
|
-
import { readdir, stat, mkdir } from "fs/promises";
|
|
3
|
+
import { readdir, stat, mkdir, type Dirent } from "fs/promises";
|
|
4
4
|
import type { PathsFileConfig, RalphConfig } from "./types";
|
|
5
|
+
import { isAccessError, shouldExcludeDir } from "./platform";
|
|
6
|
+
import { isShuttingDown } from "./state";
|
|
7
|
+
|
|
8
|
+
// Starter rule template for new projects
|
|
9
|
+
const STARTER_RULE = `I want a file named hello.txt
|
|
10
|
+
`;
|
|
5
11
|
|
|
6
12
|
// Dim text helper
|
|
7
13
|
const dim = (s: string) => `\x1b[2m${s}\x1b[0m`;
|
|
8
14
|
const CONTROLS = dim("↑↓ Navigate • Space Toggle • Enter • Ctrl+C Exit");
|
|
9
15
|
|
|
10
16
|
/**
|
|
11
|
-
*
|
|
17
|
+
* Convert a PathsFileConfig to a resolved RalphConfig
|
|
12
18
|
*/
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
19
|
+
export function resolvePathsConfig(loaded: PathsFileConfig, cwd: string): RalphConfig {
|
|
20
|
+
return {
|
|
21
|
+
refs: loaded.refs.map((r) => resolve(cwd, r)),
|
|
22
|
+
rule: resolve(cwd, loaded.rule),
|
|
23
|
+
output: resolve(cwd, loaded.output),
|
|
24
|
+
};
|
|
18
25
|
}
|
|
19
26
|
|
|
20
27
|
/**
|
|
21
|
-
*
|
|
28
|
+
* Convert an absolute path to a relative path for config storage
|
|
29
|
+
*/
|
|
30
|
+
export function toRelativePath(absolutePath: string, cwd: string): string {
|
|
31
|
+
if (absolutePath === cwd) return ".";
|
|
32
|
+
return "./" + absolutePath.replace(cwd + "/", "");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Check if a directory entry should be skipped during traversal
|
|
37
|
+
*/
|
|
38
|
+
function shouldSkipDirectory(entry: Dirent): boolean {
|
|
39
|
+
if (!entry.isDirectory()) return true;
|
|
40
|
+
if (entry.name.startsWith(".")) return true;
|
|
41
|
+
if (shouldExcludeDir(entry.name)) return true;
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* List directories in a given path
|
|
22
47
|
*/
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
48
|
+
async function listDirectories(basePath: string): Promise<string[]> {
|
|
49
|
+
try {
|
|
50
|
+
const entries = await readdir(basePath, { withFileTypes: true });
|
|
51
|
+
return entries
|
|
52
|
+
.filter((e) => e.isDirectory() && !e.name.startsWith("."))
|
|
53
|
+
.map((e) => e.name);
|
|
54
|
+
} catch (error) {
|
|
55
|
+
// Silently skip directories we can't access
|
|
56
|
+
if (isAccessError(error)) {
|
|
57
|
+
return [];
|
|
58
|
+
}
|
|
59
|
+
throw error;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
36
62
|
|
|
37
63
|
/**
|
|
38
64
|
* List directories recursively up to a certain depth
|
|
39
65
|
*/
|
|
40
|
-
async function listDirectoriesRecursive(
|
|
66
|
+
export async function listDirectoriesRecursive(
|
|
41
67
|
basePath: string,
|
|
42
68
|
maxDepth: number = 3
|
|
43
69
|
): Promise<string[]> {
|
|
@@ -46,12 +72,19 @@ async function listDirectoriesRecursive(
|
|
|
46
72
|
async function walk(dir: string, depth: number) {
|
|
47
73
|
if (depth > maxDepth) return;
|
|
48
74
|
|
|
49
|
-
|
|
75
|
+
let entries;
|
|
76
|
+
try {
|
|
77
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
78
|
+
} catch (error) {
|
|
79
|
+
// Silently skip directories we can't access
|
|
80
|
+
if (isAccessError(error)) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
throw error;
|
|
84
|
+
}
|
|
85
|
+
|
|
50
86
|
for (const entry of entries) {
|
|
51
|
-
|
|
52
|
-
if (!entry.isDirectory()) continue;
|
|
53
|
-
if (entry.name.startsWith(".")) continue;
|
|
54
|
-
if (EXCLUDED_DIRS.includes(entry.name)) continue;
|
|
87
|
+
if (shouldSkipDirectory(entry)) continue;
|
|
55
88
|
|
|
56
89
|
const fullPath = join(dir, entry.name);
|
|
57
90
|
results.push(fullPath);
|
|
@@ -66,20 +99,28 @@ async function listDirectoriesRecursive(
|
|
|
66
99
|
/**
|
|
67
100
|
* List files matching patterns in a directory (recursive)
|
|
68
101
|
*/
|
|
69
|
-
async function listFilesRecursive(
|
|
102
|
+
export async function listFilesRecursive(
|
|
70
103
|
basePath: string,
|
|
71
104
|
extensions: string[]
|
|
72
105
|
): Promise<string[]> {
|
|
73
106
|
const results: string[] = [];
|
|
74
107
|
|
|
75
108
|
async function walk(dir: string) {
|
|
76
|
-
|
|
109
|
+
let entries;
|
|
110
|
+
try {
|
|
111
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
112
|
+
} catch (error) {
|
|
113
|
+
// Silently skip directories we can't access
|
|
114
|
+
if (isAccessError(error)) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
throw error;
|
|
118
|
+
}
|
|
119
|
+
|
|
77
120
|
for (const entry of entries) {
|
|
78
121
|
const fullPath = join(dir, entry.name);
|
|
79
122
|
if (entry.isDirectory()) {
|
|
80
|
-
|
|
81
|
-
if (entry.name.startsWith(".")) continue;
|
|
82
|
-
if (EXCLUDED_DIRS.includes(entry.name)) continue;
|
|
123
|
+
if (shouldSkipDirectory(entry)) continue;
|
|
83
124
|
await walk(fullPath);
|
|
84
125
|
} else if (entry.isFile()) {
|
|
85
126
|
if (extensions.some((ext) => entry.name.endsWith(ext))) {
|
|
@@ -140,27 +181,58 @@ export async function createStarterStructure(cwd: string): Promise<void> {
|
|
|
140
181
|
* @param autoConfirm - If true, skip confirmation prompts
|
|
141
182
|
*/
|
|
142
183
|
export async function selectRefs(cwd: string, defaults?: string[], autoConfirm?: boolean): Promise<string[]> {
|
|
143
|
-
//
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
184
|
+
// Check if .ralph/ exists in cwd - if not, offer to create starter structure
|
|
185
|
+
const ralphDir = join(cwd, ".ralph");
|
|
186
|
+
let ralphExists = false;
|
|
187
|
+
try {
|
|
188
|
+
await stat(ralphDir);
|
|
189
|
+
ralphExists = true;
|
|
190
|
+
} catch {
|
|
191
|
+
ralphExists = false;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (!ralphExists) {
|
|
147
195
|
// Ask before creating starter structure (skip if autoConfirm)
|
|
148
196
|
if (!autoConfirm) {
|
|
149
|
-
|
|
150
|
-
|
|
197
|
+
console.log(CONTROLS);
|
|
198
|
+
const action = await consola.prompt(
|
|
199
|
+
`No .ralph/ found in ${cwd}`,
|
|
151
200
|
{
|
|
152
|
-
type: "
|
|
153
|
-
|
|
201
|
+
type: "select",
|
|
202
|
+
options: [
|
|
203
|
+
{ label: "📦 Create starter structure", value: "create" },
|
|
204
|
+
{ label: "⚙️ Configure manually", value: "manual" },
|
|
205
|
+
],
|
|
154
206
|
}
|
|
155
207
|
);
|
|
156
208
|
|
|
157
|
-
|
|
158
|
-
|
|
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");
|
|
159
212
|
}
|
|
213
|
+
|
|
214
|
+
if (action === "create") {
|
|
215
|
+
// Double-check we're not shutting down before executing
|
|
216
|
+
if (isShuttingDown()) {
|
|
217
|
+
throw new Error("Selection cancelled");
|
|
218
|
+
}
|
|
219
|
+
await createStarterStructure(cwd);
|
|
220
|
+
process.exit(0);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// action === "manual" - continue to directory selection
|
|
224
|
+
} else {
|
|
225
|
+
// Auto-confirm mode: create starter structure
|
|
226
|
+
await createStarterStructure(cwd);
|
|
227
|
+
process.exit(0);
|
|
160
228
|
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Get all directories up to 3 levels deep
|
|
232
|
+
let allDirs = await listDirectoriesRecursive(cwd, 3);
|
|
233
|
+
|
|
234
|
+
if (allDirs.length === 0) {
|
|
235
|
+
throw new Error("No directories found to select from");
|
|
164
236
|
}
|
|
165
237
|
|
|
166
238
|
// Convert to relative paths for display
|
|
@@ -192,9 +264,6 @@ export async function selectRefs(cwd: string, defaults?: string[], autoConfirm?:
|
|
|
192
264
|
return selected as string[];
|
|
193
265
|
}
|
|
194
266
|
|
|
195
|
-
const STARTER_RULE = `I want a file named hello.txt
|
|
196
|
-
`;
|
|
197
|
-
|
|
198
267
|
/**
|
|
199
268
|
* Prompt user to select a rule file
|
|
200
269
|
*/
|
|
@@ -332,27 +401,3 @@ export async function validateConfig(config: RalphConfig): Promise<void> {
|
|
|
332
401
|
|
|
333
402
|
// Output directory will be created if needed
|
|
334
403
|
}
|
|
335
|
-
|
|
336
|
-
/**
|
|
337
|
-
* Interactive configuration builder
|
|
338
|
-
*/
|
|
339
|
-
export async function buildConfig(cwd: string): Promise<RalphConfig> {
|
|
340
|
-
// Check for existing paths file first
|
|
341
|
-
const pathsFile = await checkForPathsFile(cwd);
|
|
342
|
-
|
|
343
|
-
if (pathsFile) {
|
|
344
|
-
const loaded = await loadPathsFile(pathsFile);
|
|
345
|
-
return {
|
|
346
|
-
refs: loaded.refs.map((r) => resolve(cwd, r)),
|
|
347
|
-
rule: resolve(cwd, loaded.rule),
|
|
348
|
-
output: resolve(cwd, loaded.output),
|
|
349
|
-
};
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
// Interactive selection
|
|
353
|
-
const refs = await selectRefs(cwd);
|
|
354
|
-
const rule = await selectRule(cwd);
|
|
355
|
-
const output = await selectOutput(cwd);
|
|
356
|
-
|
|
357
|
-
return { refs, rule, output };
|
|
358
|
-
}
|
package/src/platform.ts
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { platform } from "os";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Supported platforms
|
|
5
|
+
*/
|
|
6
|
+
export type Platform = "darwin" | "linux" | "win32" | "unknown";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Get the current platform
|
|
10
|
+
*/
|
|
11
|
+
export function getPlatform(): Platform {
|
|
12
|
+
const p = platform();
|
|
13
|
+
if (p === "darwin" || p === "linux" || p === "win32") {
|
|
14
|
+
return p;
|
|
15
|
+
}
|
|
16
|
+
return "unknown";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Platform-specific configuration
|
|
21
|
+
*/
|
|
22
|
+
interface PlatformConfig {
|
|
23
|
+
/** Error codes that indicate permission/access denied */
|
|
24
|
+
accessErrorCodes: string[];
|
|
25
|
+
/** Directories to always exclude from scanning */
|
|
26
|
+
systemExcludedDirs: string[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Platform configurations
|
|
31
|
+
*/
|
|
32
|
+
const PLATFORM_CONFIGS: Record<Platform, PlatformConfig> = {
|
|
33
|
+
darwin: {
|
|
34
|
+
accessErrorCodes: ["EPERM", "EACCES"],
|
|
35
|
+
systemExcludedDirs: [
|
|
36
|
+
// macOS protected directories
|
|
37
|
+
"Library",
|
|
38
|
+
"Photos Library.photoslibrary",
|
|
39
|
+
"Photo Booth Library",
|
|
40
|
+
],
|
|
41
|
+
},
|
|
42
|
+
linux: {
|
|
43
|
+
accessErrorCodes: ["EPERM", "EACCES"],
|
|
44
|
+
systemExcludedDirs: [
|
|
45
|
+
// Linux protected directories
|
|
46
|
+
"lost+found",
|
|
47
|
+
"proc",
|
|
48
|
+
"sys",
|
|
49
|
+
],
|
|
50
|
+
},
|
|
51
|
+
win32: {
|
|
52
|
+
accessErrorCodes: ["EPERM", "EACCES"],
|
|
53
|
+
systemExcludedDirs: [
|
|
54
|
+
// Windows protected directories
|
|
55
|
+
"System Volume Information",
|
|
56
|
+
"$Recycle.Bin",
|
|
57
|
+
"Windows",
|
|
58
|
+
],
|
|
59
|
+
},
|
|
60
|
+
unknown: {
|
|
61
|
+
accessErrorCodes: ["EPERM", "EACCES"],
|
|
62
|
+
systemExcludedDirs: [],
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Get platform-specific configuration
|
|
68
|
+
*/
|
|
69
|
+
export function getPlatformConfig(): PlatformConfig {
|
|
70
|
+
return PLATFORM_CONFIGS[getPlatform()];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Check if an error is a permission/access error for the current platform
|
|
75
|
+
*/
|
|
76
|
+
export function isAccessError(error: unknown): boolean {
|
|
77
|
+
if (error && typeof error === "object" && "code" in error) {
|
|
78
|
+
const code = (error as { code: string }).code;
|
|
79
|
+
return getPlatformConfig().accessErrorCodes.includes(code);
|
|
80
|
+
}
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Check if a directory should be excluded on the current platform
|
|
86
|
+
*/
|
|
87
|
+
export function isSystemExcludedDir(dirName: string): boolean {
|
|
88
|
+
return getPlatformConfig().systemExcludedDirs.includes(dirName);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Common directories to exclude across all platforms (project-related)
|
|
93
|
+
*/
|
|
94
|
+
export const EXCLUDED_DIRS = [
|
|
95
|
+
"node_modules",
|
|
96
|
+
"dist",
|
|
97
|
+
"build",
|
|
98
|
+
".git",
|
|
99
|
+
".next",
|
|
100
|
+
".nuxt",
|
|
101
|
+
".output",
|
|
102
|
+
"coverage",
|
|
103
|
+
"__pycache__",
|
|
104
|
+
"vendor",
|
|
105
|
+
".cache",
|
|
106
|
+
];
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Check if a directory should be excluded (combines common + platform-specific)
|
|
110
|
+
*/
|
|
111
|
+
export function shouldExcludeDir(dirName: string): boolean {
|
|
112
|
+
return EXCLUDED_DIRS.includes(dirName) || isSystemExcludedDir(dirName);
|
|
113
|
+
}
|
package/src/runner.ts
CHANGED
|
@@ -4,6 +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
8
|
|
|
8
9
|
const COMPLETION_SIGNAL = "<promise>COMPLETE</promise>";
|
|
9
10
|
const AUTH_CACHE_TTL_MS = 6 * 60 * 60 * 1000; // 6 hours
|
|
@@ -175,23 +176,6 @@ async function log(state: RunnerState, message: string): Promise<void> {
|
|
|
175
176
|
await Bun.write(state.logFile, existing + logLine);
|
|
176
177
|
}
|
|
177
178
|
|
|
178
|
-
// Track current subprocess for cleanup
|
|
179
|
-
let currentProc: ReturnType<typeof Bun.spawn> | null = null;
|
|
180
|
-
|
|
181
|
-
/**
|
|
182
|
-
* Kill any running subprocess on exit
|
|
183
|
-
*/
|
|
184
|
-
export function cleanupSubprocess() {
|
|
185
|
-
if (currentProc) {
|
|
186
|
-
try {
|
|
187
|
-
currentProc.kill();
|
|
188
|
-
} catch {
|
|
189
|
-
// Process may have already exited
|
|
190
|
-
}
|
|
191
|
-
currentProc = null;
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
179
|
/**
|
|
196
180
|
* Run a single Claude iteration
|
|
197
181
|
*/
|
|
@@ -213,14 +197,14 @@ async function runIteration(
|
|
|
213
197
|
cwd,
|
|
214
198
|
});
|
|
215
199
|
|
|
216
|
-
|
|
200
|
+
setCurrentProcess(proc);
|
|
217
201
|
|
|
218
202
|
// Collect output
|
|
219
203
|
const stdout = await new Response(proc.stdout).text();
|
|
220
204
|
const stderr = await new Response(proc.stderr).text();
|
|
221
205
|
const exitCode = await proc.exited;
|
|
222
206
|
|
|
223
|
-
|
|
207
|
+
setCurrentProcess(null);
|
|
224
208
|
|
|
225
209
|
const output = stdout + stderr;
|
|
226
210
|
|
package/src/state.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Global state shared across modules
|
|
3
|
+
*
|
|
4
|
+
* This module provides centralized state management for:
|
|
5
|
+
* - Graceful shutdown handling (Ctrl+C / SIGINT / SIGTERM)
|
|
6
|
+
* - Subprocess tracking for cleanup
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// Shutdown state
|
|
10
|
+
let shuttingDown = false;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Mark the process as shutting down
|
|
14
|
+
*/
|
|
15
|
+
export function setShuttingDown(): void {
|
|
16
|
+
shuttingDown = true;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Check if the process is shutting down (Ctrl+C was pressed)
|
|
21
|
+
*/
|
|
22
|
+
export function isShuttingDown(): boolean {
|
|
23
|
+
return shuttingDown;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Reset shutdown state (for testing purposes only)
|
|
28
|
+
*/
|
|
29
|
+
export function resetShutdownState(): void {
|
|
30
|
+
shuttingDown = false;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Subprocess tracking
|
|
34
|
+
let currentProc: ReturnType<typeof Bun.spawn> | null = null;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Set the current running subprocess for tracking
|
|
38
|
+
*/
|
|
39
|
+
export function setCurrentProcess(proc: ReturnType<typeof Bun.spawn> | null): void {
|
|
40
|
+
currentProc = proc;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Kill any running subprocess on exit
|
|
45
|
+
*/
|
|
46
|
+
export function cleanupSubprocess(): void {
|
|
47
|
+
if (currentProc) {
|
|
48
|
+
try {
|
|
49
|
+
currentProc.kill();
|
|
50
|
+
} catch {
|
|
51
|
+
// Process may have already exited
|
|
52
|
+
}
|
|
53
|
+
currentProc = null;
|
|
54
|
+
}
|
|
55
|
+
}
|