cralph 1.0.0-beta.2 → 1.0.0-beta.4
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/prompt.ts +55 -14
- package/src/runner.ts +14 -4
- 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.4",
|
|
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/prompt.ts
CHANGED
|
@@ -2,26 +2,67 @@ import type { RalphConfig } from "./types";
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* The main ralph prompt template.
|
|
5
|
-
*
|
|
5
|
+
* Based on ralph-ref best practices for autonomous agent loops.
|
|
6
6
|
*/
|
|
7
|
-
const BASE_PROMPT = `You are an autonomous agent running in a loop.
|
|
7
|
+
const BASE_PROMPT = `You are an autonomous coding agent running in a loop.
|
|
8
8
|
|
|
9
9
|
FIRST: Read and internalize the rules provided below.
|
|
10
10
|
|
|
11
|
-
Your
|
|
11
|
+
## Your Task Each Iteration
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
1. Read the TODO file and check the Patterns section first
|
|
14
|
+
2. Pick the FIRST uncompleted task (marked with [ ])
|
|
15
|
+
3. Implement that SINGLE task
|
|
16
|
+
4. Run quality checks (typecheck, lint, test - whatever the project requires)
|
|
17
|
+
5. If checks pass, mark the task [x] complete
|
|
18
|
+
6. Append your progress with learnings (see format below)
|
|
19
|
+
7. If ALL tasks are complete, output the completion signal
|
|
14
20
|
|
|
15
|
-
|
|
16
|
-
Keep the TODO structure with these sections:
|
|
17
|
-
- **# Tasks** - Checklist with [ ] for pending and [x] for done
|
|
18
|
-
- **# Notes** - Any relevant notes or context
|
|
21
|
+
## Critical Rules
|
|
19
22
|
|
|
20
|
-
|
|
23
|
+
- **ONE task per iteration** - Do not try to complete multiple tasks
|
|
24
|
+
- **Quality first** - Do NOT mark a task complete if tests/typecheck fail
|
|
25
|
+
- **Keep changes focused** - Minimal, targeted changes only
|
|
26
|
+
- **Follow existing patterns** - Match the codebase style
|
|
27
|
+
|
|
28
|
+
## Progress Format
|
|
29
|
+
|
|
30
|
+
After completing a task, APPEND to the Notes section:
|
|
31
|
+
|
|
32
|
+
\`\`\`
|
|
33
|
+
## [Task Title] - Done
|
|
34
|
+
- What was implemented
|
|
35
|
+
- Files changed
|
|
36
|
+
- **Learnings:**
|
|
37
|
+
- Patterns discovered
|
|
38
|
+
- Gotchas encountered
|
|
39
|
+
\`\`\`
|
|
40
|
+
|
|
41
|
+
## Consolidate Patterns
|
|
42
|
+
|
|
43
|
+
If you discover a REUSABLE pattern, add it to the **# Patterns** section at the TOP of the TODO file:
|
|
44
|
+
|
|
45
|
+
\`\`\`
|
|
46
|
+
# Patterns
|
|
47
|
+
- Example: Use \`sql<number>\` template for aggregations
|
|
48
|
+
- Example: Always update X when changing Y
|
|
49
|
+
\`\`\`
|
|
50
|
+
|
|
51
|
+
Only add patterns that are general and reusable, not task-specific details.
|
|
52
|
+
|
|
53
|
+
## Refs (Read-Only)
|
|
54
|
+
|
|
55
|
+
If refs paths are provided, they are READ-ONLY reference material. Never modify files in refs.
|
|
56
|
+
|
|
57
|
+
## Stop Condition
|
|
58
|
+
|
|
59
|
+
After completing a task, check if ALL tasks are marked [x] complete.
|
|
60
|
+
|
|
61
|
+
If ALL tasks are done, output exactly:
|
|
21
62
|
|
|
22
63
|
<promise>COMPLETE</promise>
|
|
23
64
|
|
|
24
|
-
|
|
65
|
+
If there are still pending tasks, end your response normally (the loop will continue).`;
|
|
25
66
|
|
|
26
67
|
/**
|
|
27
68
|
* Build the complete prompt with config and rules injected
|
|
@@ -37,18 +78,18 @@ export function buildPrompt(config: RalphConfig, rulesContent: string, todoFile:
|
|
|
37
78
|
|
|
38
79
|
## Configuration
|
|
39
80
|
|
|
40
|
-
**TODO file (update after each
|
|
81
|
+
**TODO file (read first, update after each task):**
|
|
41
82
|
${todoFile}
|
|
42
83
|
|
|
43
|
-
**Refs
|
|
84
|
+
**Refs (read-only reference material):**
|
|
44
85
|
${refsList}
|
|
45
86
|
|
|
46
|
-
**Output directory:**
|
|
87
|
+
**Output directory (write your work here):**
|
|
47
88
|
${config.output}
|
|
48
89
|
|
|
49
90
|
---
|
|
50
91
|
|
|
51
|
-
## Rules
|
|
92
|
+
## Rules (Your Instructions)
|
|
52
93
|
|
|
53
94
|
${rulesContent}
|
|
54
95
|
`;
|
package/src/runner.ts
CHANGED
|
@@ -4,19 +4,27 @@ 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
|
|
11
11
|
|
|
12
|
-
const INITIAL_TODO_CONTENT = `#
|
|
12
|
+
const INITIAL_TODO_CONTENT = `# Patterns
|
|
13
|
+
|
|
14
|
+
_None yet - add reusable patterns discovered during work_
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
# Tasks
|
|
13
19
|
|
|
14
20
|
- [ ] Task 1
|
|
15
21
|
- [ ] Task 2
|
|
16
22
|
|
|
23
|
+
---
|
|
24
|
+
|
|
17
25
|
# Notes
|
|
18
26
|
|
|
19
|
-
|
|
27
|
+
_Append progress and learnings here after each iteration_
|
|
20
28
|
`;
|
|
21
29
|
|
|
22
30
|
/**
|
|
@@ -103,7 +111,6 @@ export async function checkClaudeAuth(): Promise<boolean> {
|
|
|
103
111
|
}
|
|
104
112
|
}
|
|
105
113
|
|
|
106
|
-
|
|
107
114
|
/**
|
|
108
115
|
* Check if the TODO file is in a clean/initial state
|
|
109
116
|
*/
|
|
@@ -150,10 +157,13 @@ Ralph Session: ${state.startTime.toISOString()}
|
|
|
150
157
|
"Found existing TODO with progress. Reset to start fresh?",
|
|
151
158
|
{
|
|
152
159
|
type: "confirm",
|
|
160
|
+
cancel: "symbol",
|
|
153
161
|
initial: true,
|
|
154
162
|
}
|
|
155
163
|
);
|
|
156
164
|
|
|
165
|
+
throwIfCancelled(response);
|
|
166
|
+
|
|
157
167
|
if (response === true) {
|
|
158
168
|
await Bun.write(state.todoFile, INITIAL_TODO_CONTENT);
|
|
159
169
|
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
|
/**
|