cralph 1.0.0-alpha.0 → 1.0.0-alpha.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 +74 -18
- package/assets/ralph.png +0 -0
- package/index.ts +1 -1
- package/package.json +4 -3
- package/src/cli.ts +109 -47
- package/src/paths.ts +168 -129
- package/src/prompt.ts +4 -4
- package/src/runner.ts +21 -0
- package/src/types.ts +3 -8
package/README.md
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
# cralph
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
<p align="center">
|
|
4
|
+
<img src="assets/ralph.png" alt="Ralph cooking" width="500">
|
|
5
|
+
</p>
|
|
6
|
+
|
|
7
|
+
Claude in a loop. Point at refs, give it a rule, let it cook.
|
|
4
8
|
|
|
5
9
|
```
|
|
6
|
-
refs/ ──loop──>
|
|
7
|
-
(source) │ (
|
|
10
|
+
refs/ ──loop──> ./
|
|
11
|
+
(source) │ (output in cwd)
|
|
8
12
|
│
|
|
9
|
-
|
|
13
|
+
rule.md
|
|
10
14
|
```
|
|
11
15
|
|
|
12
16
|
## What is Ralph?
|
|
@@ -34,37 +38,37 @@ npm install -g cralph
|
|
|
34
38
|
## Usage
|
|
35
39
|
|
|
36
40
|
```bash
|
|
37
|
-
#
|
|
41
|
+
# Run - auto-detects ralph.paths.json in cwd
|
|
38
42
|
cralph
|
|
39
43
|
|
|
40
|
-
#
|
|
41
|
-
cralph
|
|
44
|
+
# First run (no config) - interactive mode generates ralph.paths.json
|
|
45
|
+
cralph
|
|
42
46
|
|
|
43
|
-
#
|
|
44
|
-
cralph --
|
|
47
|
+
# Override with flags
|
|
48
|
+
cralph --refs ./source --rule ./rule.md --output .
|
|
45
49
|
```
|
|
46
50
|
|
|
47
51
|
## Path Selection
|
|
48
52
|
|
|
49
|
-
|
|
53
|
+
Simple multiselect for all paths:
|
|
50
54
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
55
|
+
- **Space** to toggle selection
|
|
56
|
+
- **Enter** to confirm
|
|
57
|
+
- **Ctrl+C** to exit
|
|
58
|
+
- Shows all directories up to 3 levels deep
|
|
59
|
+
- Pre-selects current values in edit mode
|
|
56
60
|
|
|
57
61
|
## Config File
|
|
58
62
|
|
|
59
63
|
```json
|
|
60
64
|
{
|
|
61
65
|
"refs": ["./refs", "./more-refs"],
|
|
62
|
-
"
|
|
63
|
-
"output": "
|
|
66
|
+
"rule": "./.cursor/rules/my-rules.mdc",
|
|
67
|
+
"output": "."
|
|
64
68
|
}
|
|
65
69
|
```
|
|
66
70
|
|
|
67
|
-
Name it `ralph.paths.json` and cralph auto-detects it.
|
|
71
|
+
Name it `ralph.paths.json` and cralph auto-detects it. Output is typically `.` (current directory) since you'll run cralph in your repo.
|
|
68
72
|
|
|
69
73
|
## How It Works
|
|
70
74
|
|
|
@@ -73,6 +77,58 @@ Name it `ralph.paths.json` and cralph auto-detects it.
|
|
|
73
77
|
3. Runs `claude -p --dangerously-skip-permissions` in a loop
|
|
74
78
|
4. Stops when Claude outputs `<promise>COMPLETE</promise>`
|
|
75
79
|
|
|
80
|
+
## Expected Behavior
|
|
81
|
+
|
|
82
|
+
**Auto-detect existing config:**
|
|
83
|
+
```
|
|
84
|
+
❯ Found ralph.paths.json. What would you like to do?
|
|
85
|
+
● 🚀 Run with this config
|
|
86
|
+
○ ✏️ Edit configuration
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
**Interactive Mode (no config file):**
|
|
90
|
+
```
|
|
91
|
+
ℹ Interactive configuration mode
|
|
92
|
+
|
|
93
|
+
↑↓ Navigate • Space Toggle • Enter • Ctrl+C Exit
|
|
94
|
+
❯ Select refs directories:
|
|
95
|
+
◻ 📁 src
|
|
96
|
+
◻ 📁 src/components
|
|
97
|
+
◼ 📁 docs
|
|
98
|
+
|
|
99
|
+
↑↓ Navigate • Space Toggle • Enter • Ctrl+C Exit
|
|
100
|
+
❯ Select rule file:
|
|
101
|
+
● 📄 .cursor/rules/my-rules.mdc (cursor rule)
|
|
102
|
+
○ 📄 README.md
|
|
103
|
+
|
|
104
|
+
↑↓ Navigate • Space Toggle • Enter • Ctrl+C Exit
|
|
105
|
+
❯ Select output directory:
|
|
106
|
+
● 📍 Current directory (.)
|
|
107
|
+
○ 📁 docs
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
**Save config after selection:**
|
|
111
|
+
```
|
|
112
|
+
? Save configuration to ralph.paths.json? (Y/n)
|
|
113
|
+
✔ Saved ralph.paths.json
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
**Cancellation:**
|
|
117
|
+
- Press `Ctrl+C` at any time to exit
|
|
118
|
+
- Running Claude processes are terminated cleanly
|
|
119
|
+
|
|
120
|
+
**Output Files:**
|
|
121
|
+
- `.ralph/ralph.log` - Session log with timestamps
|
|
122
|
+
- `.ralph/TODO.md` - Agent status tracker
|
|
123
|
+
|
|
124
|
+
## Testing
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
bun test
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Tests validate config loading, prompt building, and CLI behavior without calling Claude.
|
|
131
|
+
|
|
76
132
|
## Requirements
|
|
77
133
|
|
|
78
134
|
- [Bun](https://bun.sh)
|
package/assets/ralph.png
ADDED
|
Binary file
|
package/index.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cralph",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
4
|
-
"description": "Claude in a loop. Point at refs, give it
|
|
3
|
+
"version": "1.0.0-alpha.2",
|
|
4
|
+
"description": "Claude in a loop. Point at refs, give it a rule, let it cook.",
|
|
5
5
|
"author": "mguleryuz",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": {
|
|
@@ -26,7 +26,8 @@
|
|
|
26
26
|
"src",
|
|
27
27
|
"index.ts",
|
|
28
28
|
"README.md",
|
|
29
|
-
"LICENSE"
|
|
29
|
+
"LICENSE",
|
|
30
|
+
"assets"
|
|
30
31
|
],
|
|
31
32
|
"scripts": {
|
|
32
33
|
"start": "bun run src/cli.ts"
|
package/src/cli.ts
CHANGED
|
@@ -8,91 +8,146 @@ import {
|
|
|
8
8
|
loadPathsFile,
|
|
9
9
|
validateConfig,
|
|
10
10
|
selectRefs,
|
|
11
|
-
|
|
11
|
+
selectRule,
|
|
12
12
|
selectOutput,
|
|
13
|
+
checkForPathsFile,
|
|
13
14
|
} from "./paths";
|
|
14
|
-
import { run } from "./runner";
|
|
15
|
+
import { run, cleanupSubprocess } from "./runner";
|
|
15
16
|
import type { RalphConfig } from "./types";
|
|
16
17
|
|
|
18
|
+
// Graceful shutdown on Ctrl+C
|
|
19
|
+
function setupGracefulExit() {
|
|
20
|
+
let shuttingDown = false;
|
|
21
|
+
|
|
22
|
+
process.on("SIGINT", () => {
|
|
23
|
+
if (shuttingDown) {
|
|
24
|
+
// Force exit on second Ctrl+C
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
shuttingDown = true;
|
|
28
|
+
cleanupSubprocess();
|
|
29
|
+
console.log("\n");
|
|
30
|
+
consola.info("Cancelled.");
|
|
31
|
+
// Use setImmediate to ensure output is flushed
|
|
32
|
+
setImmediate(() => process.exit(0));
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// Also handle SIGTERM
|
|
36
|
+
process.on("SIGTERM", () => {
|
|
37
|
+
cleanupSubprocess();
|
|
38
|
+
process.exit(0);
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
17
42
|
const main = defineCommand({
|
|
18
43
|
meta: {
|
|
19
44
|
name: "cralph",
|
|
20
45
|
version: "1.0.0",
|
|
21
|
-
description: "Claude in a loop. Point at refs, give it
|
|
46
|
+
description: "Claude in a loop. Point at refs, give it a rule, let it cook.",
|
|
22
47
|
},
|
|
23
48
|
args: {
|
|
24
49
|
refs: {
|
|
25
50
|
type: "string",
|
|
26
51
|
description: "Comma-separated refs paths (source material)",
|
|
52
|
+
valueHint: "path1,path2",
|
|
53
|
+
alias: "r",
|
|
27
54
|
required: false,
|
|
28
55
|
},
|
|
29
|
-
|
|
56
|
+
rule: {
|
|
30
57
|
type: "string",
|
|
31
|
-
description: "Path to
|
|
58
|
+
description: "Path to rule file (.mdc or .md)",
|
|
59
|
+
valueHint: "rule.md",
|
|
60
|
+
alias: "u",
|
|
32
61
|
required: false,
|
|
33
62
|
},
|
|
34
63
|
output: {
|
|
35
64
|
type: "string",
|
|
36
|
-
description: "Output directory",
|
|
65
|
+
description: "Output directory where results will be written",
|
|
66
|
+
valueHint: ".",
|
|
67
|
+
alias: "o",
|
|
37
68
|
required: false,
|
|
38
69
|
},
|
|
39
|
-
|
|
40
|
-
type: "
|
|
41
|
-
description: "
|
|
70
|
+
help: {
|
|
71
|
+
type: "boolean",
|
|
72
|
+
description: "Show this help message",
|
|
73
|
+
alias: "h",
|
|
42
74
|
required: false,
|
|
43
75
|
},
|
|
44
76
|
},
|
|
45
77
|
async run({ args }) {
|
|
78
|
+
setupGracefulExit();
|
|
46
79
|
const cwd = process.cwd();
|
|
47
80
|
let config: RalphConfig;
|
|
48
81
|
|
|
49
82
|
try {
|
|
50
|
-
//
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
83
|
+
// Check for existing paths file in cwd
|
|
84
|
+
const pathsFileResult = await checkForPathsFile(cwd);
|
|
85
|
+
|
|
86
|
+
if (pathsFileResult?.action === "run") {
|
|
87
|
+
// Use existing config file
|
|
88
|
+
consola.info(`Loading config from ${pathsFileResult.path}`);
|
|
89
|
+
const loaded = await loadPathsFile(pathsFileResult.path);
|
|
55
90
|
config = {
|
|
56
91
|
refs: loaded.refs.map((r) => resolve(cwd, r)),
|
|
57
|
-
|
|
92
|
+
rule: resolve(cwd, loaded.rule),
|
|
58
93
|
output: resolve(cwd, loaded.output),
|
|
59
94
|
};
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
95
|
+
} else {
|
|
96
|
+
// Load existing config for edit mode defaults
|
|
97
|
+
let existingConfig: RalphConfig | null = null;
|
|
98
|
+
if (pathsFileResult?.action === "edit") {
|
|
99
|
+
consola.info("Edit configuration");
|
|
100
|
+
const candidates = ["ralph.paths.json", ".ralph.paths.json", "paths.json"];
|
|
101
|
+
for (const candidate of candidates) {
|
|
102
|
+
const filePath = resolve(cwd, candidate);
|
|
103
|
+
const file = Bun.file(filePath);
|
|
104
|
+
if (await file.exists()) {
|
|
105
|
+
const loaded = await loadPathsFile(filePath);
|
|
106
|
+
existingConfig = {
|
|
107
|
+
refs: loaded.refs.map((r) => resolve(cwd, r)),
|
|
108
|
+
rule: resolve(cwd, loaded.rule),
|
|
109
|
+
output: resolve(cwd, loaded.output),
|
|
110
|
+
};
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
77
114
|
} else {
|
|
78
|
-
|
|
115
|
+
consola.info("Interactive configuration mode");
|
|
79
116
|
}
|
|
80
117
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
118
|
+
// Interactive selection
|
|
119
|
+
const refs = args.refs
|
|
120
|
+
? args.refs.split(",").map((r) => resolve(cwd, r.trim()))
|
|
121
|
+
: await selectRefs(cwd, existingConfig?.refs);
|
|
122
|
+
|
|
123
|
+
const rule = args.rule
|
|
124
|
+
? resolve(cwd, args.rule)
|
|
125
|
+
: await selectRule(cwd, existingConfig?.rule);
|
|
126
|
+
|
|
127
|
+
const output = args.output
|
|
128
|
+
? resolve(cwd, args.output)
|
|
129
|
+
: await selectOutput(cwd, existingConfig?.output);
|
|
87
130
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
131
|
+
config = { refs, rule, output };
|
|
132
|
+
|
|
133
|
+
// Offer to save config
|
|
134
|
+
const saveConfig = await consola.prompt("Save configuration to ralph.paths.json?", {
|
|
135
|
+
type: "confirm",
|
|
136
|
+
initial: true,
|
|
137
|
+
});
|
|
94
138
|
|
|
95
|
-
|
|
139
|
+
if (saveConfig === true) {
|
|
140
|
+
const pathsConfig = {
|
|
141
|
+
refs: config.refs.map((r) => "./" + r.replace(cwd + "/", "")),
|
|
142
|
+
rule: "./" + config.rule.replace(cwd + "/", ""),
|
|
143
|
+
output: config.output === cwd ? "." : "./" + config.output.replace(cwd + "/", ""),
|
|
144
|
+
};
|
|
145
|
+
await Bun.write(
|
|
146
|
+
resolve(cwd, "ralph.paths.json"),
|
|
147
|
+
JSON.stringify(pathsConfig, null, 2)
|
|
148
|
+
);
|
|
149
|
+
consola.success("Saved ralph.paths.json");
|
|
150
|
+
}
|
|
96
151
|
}
|
|
97
152
|
|
|
98
153
|
// Validate configuration
|
|
@@ -102,7 +157,7 @@ const main = defineCommand({
|
|
|
102
157
|
// Show config summary
|
|
103
158
|
consola.info("Configuration:");
|
|
104
159
|
consola.info(` Refs: ${config.refs.join(", ")}`);
|
|
105
|
-
consola.info(`
|
|
160
|
+
consola.info(` Rule: ${config.rule}`);
|
|
106
161
|
consola.info(` Output: ${config.output}`);
|
|
107
162
|
console.log();
|
|
108
163
|
|
|
@@ -120,6 +175,13 @@ const main = defineCommand({
|
|
|
120
175
|
// Run the main loop
|
|
121
176
|
await run(config);
|
|
122
177
|
} catch (error) {
|
|
178
|
+
// Handle graceful cancellation
|
|
179
|
+
if (error instanceof Error && error.message.includes("cancelled")) {
|
|
180
|
+
console.log();
|
|
181
|
+
consola.info("Cancelled.");
|
|
182
|
+
process.exit(0);
|
|
183
|
+
}
|
|
184
|
+
|
|
123
185
|
if (error instanceof Error) {
|
|
124
186
|
consola.error(error.message);
|
|
125
187
|
} else {
|
package/src/paths.ts
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { consola } from "consola";
|
|
2
|
-
import { resolve, join
|
|
2
|
+
import { resolve, join } from "path";
|
|
3
3
|
import { readdir, stat } from "fs/promises";
|
|
4
|
-
import type {
|
|
4
|
+
import type { PathsFileConfig, RalphConfig } from "./types";
|
|
5
|
+
|
|
6
|
+
// Dim text helper
|
|
7
|
+
const dim = (s: string) => `\x1b[2m${s}\x1b[0m`;
|
|
8
|
+
const CONTROLS = dim("↑↓ Navigate • Space Toggle • Enter • Ctrl+C Exit");
|
|
5
9
|
|
|
6
10
|
/**
|
|
7
11
|
* List directories in a given path
|
|
@@ -13,6 +17,52 @@ async function listDirectories(basePath: string): Promise<string[]> {
|
|
|
13
17
|
.map((e) => e.name);
|
|
14
18
|
}
|
|
15
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Directories to exclude from listing
|
|
22
|
+
*/
|
|
23
|
+
const EXCLUDED_DIRS = [
|
|
24
|
+
"node_modules",
|
|
25
|
+
"dist",
|
|
26
|
+
"build",
|
|
27
|
+
".git",
|
|
28
|
+
".next",
|
|
29
|
+
".nuxt",
|
|
30
|
+
".output",
|
|
31
|
+
"coverage",
|
|
32
|
+
"__pycache__",
|
|
33
|
+
"vendor",
|
|
34
|
+
".cache",
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* List directories recursively up to a certain depth
|
|
39
|
+
*/
|
|
40
|
+
async function listDirectoriesRecursive(
|
|
41
|
+
basePath: string,
|
|
42
|
+
maxDepth: number = 3
|
|
43
|
+
): Promise<string[]> {
|
|
44
|
+
const results: string[] = [];
|
|
45
|
+
|
|
46
|
+
async function walk(dir: string, depth: number) {
|
|
47
|
+
if (depth > maxDepth) return;
|
|
48
|
+
|
|
49
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
50
|
+
for (const entry of entries) {
|
|
51
|
+
// Skip hidden and excluded directories
|
|
52
|
+
if (!entry.isDirectory()) continue;
|
|
53
|
+
if (entry.name.startsWith(".")) continue;
|
|
54
|
+
if (EXCLUDED_DIRS.includes(entry.name)) continue;
|
|
55
|
+
|
|
56
|
+
const fullPath = join(dir, entry.name);
|
|
57
|
+
results.push(fullPath);
|
|
58
|
+
await walk(fullPath, depth + 1);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
await walk(basePath, 1);
|
|
63
|
+
return results;
|
|
64
|
+
}
|
|
65
|
+
|
|
16
66
|
/**
|
|
17
67
|
* List files matching patterns in a directory (recursive)
|
|
18
68
|
*/
|
|
@@ -26,7 +76,10 @@ async function listFilesRecursive(
|
|
|
26
76
|
const entries = await readdir(dir, { withFileTypes: true });
|
|
27
77
|
for (const entry of entries) {
|
|
28
78
|
const fullPath = join(dir, entry.name);
|
|
29
|
-
if (entry.isDirectory()
|
|
79
|
+
if (entry.isDirectory()) {
|
|
80
|
+
// Skip hidden and excluded directories
|
|
81
|
+
if (entry.name.startsWith(".")) continue;
|
|
82
|
+
if (EXCLUDED_DIRS.includes(entry.name)) continue;
|
|
30
83
|
await walk(fullPath);
|
|
31
84
|
} else if (entry.isFile()) {
|
|
32
85
|
if (extensions.some((ext) => entry.name.endsWith(ext))) {
|
|
@@ -40,26 +93,6 @@ async function listFilesRecursive(
|
|
|
40
93
|
return results;
|
|
41
94
|
}
|
|
42
95
|
|
|
43
|
-
/**
|
|
44
|
-
* Prompt user for path selection mode
|
|
45
|
-
*/
|
|
46
|
-
async function askSelectionMode(label: string): Promise<PathSelectionMode> {
|
|
47
|
-
const mode = await consola.prompt(`How would you like to specify ${label}?`, {
|
|
48
|
-
type: "select",
|
|
49
|
-
options: [
|
|
50
|
-
{ label: "Select from current directory", value: "select" },
|
|
51
|
-
{ label: "Enter path manually", value: "manual" },
|
|
52
|
-
{ label: "Use paths file", value: "file" },
|
|
53
|
-
],
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
if (typeof mode === "symbol") {
|
|
57
|
-
throw new Error("Selection cancelled");
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
return mode as PathSelectionMode;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
96
|
/**
|
|
64
97
|
* Load configuration from a paths file
|
|
65
98
|
*/
|
|
@@ -73,143 +106,149 @@ export async function loadPathsFile(filePath: string): Promise<PathsFileConfig>
|
|
|
73
106
|
}
|
|
74
107
|
|
|
75
108
|
/**
|
|
76
|
-
* Prompt user to select refs directories
|
|
109
|
+
* Prompt user to select refs directories (simple multiselect)
|
|
77
110
|
*/
|
|
78
|
-
export async function selectRefs(cwd: string): Promise<string[]> {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
);
|
|
86
|
-
if (typeof input === "symbol") throw new Error("Selection cancelled");
|
|
87
|
-
return input
|
|
88
|
-
.split(",")
|
|
89
|
-
.map((p) => resolve(cwd, p.trim()))
|
|
90
|
-
.filter(Boolean);
|
|
111
|
+
export async function selectRefs(cwd: string, defaults?: string[]): Promise<string[]> {
|
|
112
|
+
// Get all directories up to 3 levels deep
|
|
113
|
+
const allDirs = await listDirectoriesRecursive(cwd, 3);
|
|
114
|
+
|
|
115
|
+
if (allDirs.length === 0) {
|
|
116
|
+
consola.warn("No directories found");
|
|
117
|
+
throw new Error("No directories available to select");
|
|
91
118
|
}
|
|
92
119
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
120
|
+
// Convert to relative paths for display
|
|
121
|
+
const options = allDirs.map((d) => {
|
|
122
|
+
const relative = d.replace(cwd + "/", "");
|
|
123
|
+
const isDefault = defaults?.includes(d);
|
|
124
|
+
return {
|
|
125
|
+
label: `📁 ${relative}`,
|
|
126
|
+
value: d,
|
|
127
|
+
hint: isDefault ? "current" : undefined,
|
|
128
|
+
};
|
|
129
|
+
});
|
|
99
130
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
options: dirs.map((d) => ({ label: d, value: d })),
|
|
103
|
-
});
|
|
131
|
+
// Get initial selections (indices of defaults)
|
|
132
|
+
const initialValues = defaults?.filter((d) => allDirs.includes(d)) || [];
|
|
104
133
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
134
|
+
console.log(CONTROLS);
|
|
135
|
+
const selected = await consola.prompt("Select refs directories:", {
|
|
136
|
+
type: "multiselect",
|
|
137
|
+
options,
|
|
138
|
+
initial: initialValues,
|
|
139
|
+
});
|
|
108
140
|
|
|
109
|
-
//
|
|
110
|
-
|
|
141
|
+
// Handle cancel (symbol) or empty result
|
|
142
|
+
if (typeof selected === "symbol" || !selected || (Array.isArray(selected) && selected.length === 0)) {
|
|
143
|
+
throw new Error("Selection cancelled");
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return selected as string[];
|
|
111
147
|
}
|
|
112
148
|
|
|
113
149
|
/**
|
|
114
|
-
* Prompt user to select a
|
|
150
|
+
* Prompt user to select a rule file
|
|
115
151
|
*/
|
|
116
|
-
export async function
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
type: "text",
|
|
122
|
-
});
|
|
123
|
-
if (typeof input === "symbol") throw new Error("Selection cancelled");
|
|
124
|
-
return resolve(cwd, input.trim());
|
|
152
|
+
export async function selectRule(cwd: string, defaultRule?: string): Promise<string> {
|
|
153
|
+
const files = await listFilesRecursive(cwd, [".mdc", ".md"]);
|
|
154
|
+
if (files.length === 0) {
|
|
155
|
+
consola.warn("No .mdc or .md files found");
|
|
156
|
+
throw new Error("No rule files available to select");
|
|
125
157
|
}
|
|
126
158
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
// Show relative paths for readability
|
|
135
|
-
const options = files.map((f) => ({
|
|
136
|
-
label: f.replace(cwd + "/", ""),
|
|
137
|
-
value: f,
|
|
138
|
-
}));
|
|
159
|
+
// Show relative paths for readability
|
|
160
|
+
const options = files.map((f) => ({
|
|
161
|
+
label: `📄 ${f.replace(cwd + "/", "")}`,
|
|
162
|
+
value: f,
|
|
163
|
+
hint: f === defaultRule ? "current" : (f.endsWith(".mdc") ? "cursor rule" : "markdown"),
|
|
164
|
+
}));
|
|
139
165
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
options,
|
|
143
|
-
});
|
|
166
|
+
// Find index of default for initial selection
|
|
167
|
+
const initialIndex = defaultRule ? files.findIndex((f) => f === defaultRule) : 0;
|
|
144
168
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
169
|
+
console.log(CONTROLS);
|
|
170
|
+
const selected = await consola.prompt("Select rule file:", {
|
|
171
|
+
type: "select",
|
|
172
|
+
options,
|
|
173
|
+
initial: initialIndex >= 0 ? initialIndex : 0,
|
|
174
|
+
});
|
|
148
175
|
|
|
149
|
-
throw new Error("
|
|
176
|
+
if (typeof selected === "symbol") throw new Error("Selection cancelled");
|
|
177
|
+
return selected as string;
|
|
150
178
|
}
|
|
151
179
|
|
|
152
180
|
/**
|
|
153
181
|
* Prompt user to select output directory
|
|
154
182
|
*/
|
|
155
|
-
export async function selectOutput(cwd: string): Promise<string> {
|
|
156
|
-
const
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
|
|
183
|
+
export async function selectOutput(cwd: string, defaultOutput?: string): Promise<string> {
|
|
184
|
+
const dirs = await listDirectories(cwd);
|
|
185
|
+
|
|
186
|
+
// Determine default value for matching
|
|
187
|
+
const defaultDir = defaultOutput === cwd ? "." : defaultOutput?.replace(cwd + "/", "");
|
|
188
|
+
|
|
189
|
+
const options = [
|
|
190
|
+
{ label: "📍 Current directory (.)", value: ".", hint: defaultDir === "." ? "current" : "Output here" },
|
|
191
|
+
...dirs.map((d) => ({
|
|
192
|
+
label: `📁 ${d}`,
|
|
193
|
+
value: d,
|
|
194
|
+
hint: d === defaultDir ? "current" : undefined,
|
|
195
|
+
})),
|
|
196
|
+
];
|
|
197
|
+
|
|
198
|
+
// Find initial index
|
|
199
|
+
let initialIndex = 0;
|
|
200
|
+
if (defaultDir) {
|
|
201
|
+
const idx = defaultDir === "." ? 0 : dirs.findIndex((d) => d === defaultDir) + 1;
|
|
202
|
+
if (idx >= 0) initialIndex = idx;
|
|
165
203
|
}
|
|
166
204
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
const selected = await consola.prompt("Select output directory:", {
|
|
175
|
-
type: "select",
|
|
176
|
-
options,
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
if (typeof selected === "symbol") throw new Error("Selection cancelled");
|
|
180
|
-
|
|
181
|
-
if (selected === "__new__") {
|
|
182
|
-
const newDir = await consola.prompt("Enter new directory name:", {
|
|
183
|
-
type: "text",
|
|
184
|
-
default: "docs",
|
|
185
|
-
});
|
|
186
|
-
if (typeof newDir === "symbol") throw new Error("Selection cancelled");
|
|
187
|
-
return resolve(cwd, newDir.trim());
|
|
188
|
-
}
|
|
205
|
+
console.log(CONTROLS);
|
|
206
|
+
const selected = await consola.prompt("Select output directory:", {
|
|
207
|
+
type: "select",
|
|
208
|
+
options,
|
|
209
|
+
initial: initialIndex,
|
|
210
|
+
});
|
|
189
211
|
|
|
190
|
-
|
|
212
|
+
if (typeof selected === "symbol") throw new Error("Selection cancelled");
|
|
213
|
+
|
|
214
|
+
if (selected === ".") {
|
|
215
|
+
return cwd;
|
|
191
216
|
}
|
|
192
217
|
|
|
193
|
-
|
|
218
|
+
return resolve(cwd, selected as string);
|
|
194
219
|
}
|
|
195
220
|
|
|
196
221
|
/**
|
|
197
222
|
* Check if a paths file exists and offer to use it
|
|
223
|
+
* Returns: { action: "run", path: string } | { action: "edit" } | null
|
|
198
224
|
*/
|
|
199
|
-
export async function checkForPathsFile(cwd: string): Promise<string | null> {
|
|
225
|
+
export async function checkForPathsFile(cwd: string): Promise<{ action: "run"; path: string } | { action: "edit" } | null> {
|
|
200
226
|
const candidates = ["ralph.paths.json", ".ralph.paths.json", "paths.json"];
|
|
201
227
|
|
|
202
228
|
for (const candidate of candidates) {
|
|
203
229
|
const filePath = join(cwd, candidate);
|
|
204
230
|
const file = Bun.file(filePath);
|
|
205
231
|
if (await file.exists()) {
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
{
|
|
232
|
+
console.log(CONTROLS);
|
|
233
|
+
const action = await consola.prompt(
|
|
234
|
+
`Found ${candidate}. What would you like to do?`,
|
|
235
|
+
{
|
|
236
|
+
type: "select",
|
|
237
|
+
options: [
|
|
238
|
+
{ label: "🚀 Run with this config", value: "run" },
|
|
239
|
+
{ label: "✏️ Edit configuration", value: "edit" },
|
|
240
|
+
],
|
|
241
|
+
}
|
|
209
242
|
);
|
|
210
|
-
|
|
211
|
-
|
|
243
|
+
|
|
244
|
+
if (typeof action === "symbol") {
|
|
245
|
+
throw new Error("Selection cancelled");
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (action === "run") {
|
|
249
|
+
return { action: "run", path: filePath };
|
|
212
250
|
}
|
|
251
|
+
return { action: "edit" };
|
|
213
252
|
}
|
|
214
253
|
}
|
|
215
254
|
|
|
@@ -229,10 +268,10 @@ export async function validateConfig(config: RalphConfig): Promise<void> {
|
|
|
229
268
|
}
|
|
230
269
|
}
|
|
231
270
|
|
|
232
|
-
// Check
|
|
233
|
-
const
|
|
234
|
-
if (!(await
|
|
235
|
-
throw new Error(`
|
|
271
|
+
// Check rule file
|
|
272
|
+
const ruleFile = Bun.file(config.rule);
|
|
273
|
+
if (!(await ruleFile.exists())) {
|
|
274
|
+
throw new Error(`Rule file does not exist: ${config.rule}`);
|
|
236
275
|
}
|
|
237
276
|
|
|
238
277
|
// Output directory will be created if needed
|
|
@@ -249,15 +288,15 @@ export async function buildConfig(cwd: string): Promise<RalphConfig> {
|
|
|
249
288
|
const loaded = await loadPathsFile(pathsFile);
|
|
250
289
|
return {
|
|
251
290
|
refs: loaded.refs.map((r) => resolve(cwd, r)),
|
|
252
|
-
|
|
291
|
+
rule: resolve(cwd, loaded.rule),
|
|
253
292
|
output: resolve(cwd, loaded.output),
|
|
254
293
|
};
|
|
255
294
|
}
|
|
256
295
|
|
|
257
296
|
// Interactive selection
|
|
258
297
|
const refs = await selectRefs(cwd);
|
|
259
|
-
const
|
|
298
|
+
const rule = await selectRule(cwd);
|
|
260
299
|
const output = await selectOutput(cwd);
|
|
261
300
|
|
|
262
|
-
return { refs,
|
|
301
|
+
return { refs, rule, output };
|
|
263
302
|
}
|
package/src/prompt.ts
CHANGED
|
@@ -49,11 +49,11 @@ ${rulesContent}
|
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
/**
|
|
52
|
-
* Read
|
|
52
|
+
* Read rule file and build complete prompt
|
|
53
53
|
*/
|
|
54
54
|
export async function createPrompt(config: RalphConfig): Promise<string> {
|
|
55
|
-
const
|
|
56
|
-
const
|
|
55
|
+
const ruleFile = Bun.file(config.rule);
|
|
56
|
+
const ruleContent = await ruleFile.text();
|
|
57
57
|
|
|
58
|
-
return buildPrompt(config,
|
|
58
|
+
return buildPrompt(config, ruleContent);
|
|
59
59
|
}
|
package/src/runner.ts
CHANGED
|
@@ -85,6 +85,23 @@ async function countRefs(refs: string[]): Promise<number> {
|
|
|
85
85
|
return count;
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
+
// Track current subprocess for cleanup
|
|
89
|
+
let currentProc: ReturnType<typeof Bun.spawn> | null = null;
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Kill any running subprocess on exit
|
|
93
|
+
*/
|
|
94
|
+
export function cleanupSubprocess() {
|
|
95
|
+
if (currentProc) {
|
|
96
|
+
try {
|
|
97
|
+
currentProc.kill();
|
|
98
|
+
} catch {
|
|
99
|
+
// Process may have already exited
|
|
100
|
+
}
|
|
101
|
+
currentProc = null;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
88
105
|
/**
|
|
89
106
|
* Run a single Claude iteration
|
|
90
107
|
*/
|
|
@@ -105,11 +122,15 @@ async function runIteration(
|
|
|
105
122
|
stderr: "pipe",
|
|
106
123
|
cwd,
|
|
107
124
|
});
|
|
125
|
+
|
|
126
|
+
currentProc = proc;
|
|
108
127
|
|
|
109
128
|
// Collect output
|
|
110
129
|
const stdout = await new Response(proc.stdout).text();
|
|
111
130
|
const stderr = await new Response(proc.stderr).text();
|
|
112
131
|
const exitCode = await proc.exited;
|
|
132
|
+
|
|
133
|
+
currentProc = null;
|
|
113
134
|
|
|
114
135
|
const output = stdout + stderr;
|
|
115
136
|
|
package/src/types.ts
CHANGED
|
@@ -1,14 +1,9 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Path selection mode for CLI prompts
|
|
3
|
-
*/
|
|
4
|
-
export type PathSelectionMode = "manual" | "select" | "file";
|
|
5
|
-
|
|
6
1
|
/**
|
|
7
2
|
* Configuration loaded from a paths file (e.g., ralph.paths.json)
|
|
8
3
|
*/
|
|
9
4
|
export interface PathsFileConfig {
|
|
10
5
|
refs: string[];
|
|
11
|
-
|
|
6
|
+
rule: string;
|
|
12
7
|
output: string;
|
|
13
8
|
}
|
|
14
9
|
|
|
@@ -18,8 +13,8 @@ export interface PathsFileConfig {
|
|
|
18
13
|
export interface RalphConfig {
|
|
19
14
|
/** Paths to reference material directories/files */
|
|
20
15
|
refs: string[];
|
|
21
|
-
/** Path to the
|
|
22
|
-
|
|
16
|
+
/** Path to the rule file (.mdc or .md) */
|
|
17
|
+
rule: string;
|
|
23
18
|
/** Output directory for generated docs */
|
|
24
19
|
output: string;
|
|
25
20
|
}
|