ralphctl 0.1.0 → 0.1.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 +58 -24
- package/dist/add-HGJCLWED.mjs +14 -0
- package/dist/add-MRGCS3US.mjs +14 -0
- package/dist/chunk-6PYTKGB5.mjs +316 -0
- package/dist/chunk-7TG3EAQ2.mjs +20 -0
- package/dist/chunk-EKMZZRWI.mjs +521 -0
- package/dist/chunk-JON4GCLR.mjs +59 -0
- package/dist/chunk-LOR7QBXX.mjs +3683 -0
- package/dist/chunk-MNMQC36F.mjs +556 -0
- package/dist/chunk-MRKOFVTM.mjs +537 -0
- package/dist/chunk-NTWO2LXB.mjs +52 -0
- package/dist/chunk-QBXHAXHI.mjs +562 -0
- package/dist/chunk-WGHJI3OI.mjs +214 -0
- package/dist/cli.mjs +4245 -0
- package/dist/create-MG7E7PLQ.mjs +10 -0
- package/dist/handle-UG5M2OON.mjs +22 -0
- package/dist/multiline-OHSNFCRG.mjs +40 -0
- package/dist/project-NT3L4FTB.mjs +28 -0
- package/dist/resolver-WSFWKACM.mjs +153 -0
- package/dist/sprint-4VHDLGFN.mjs +37 -0
- package/dist/wizard-LRELAN2J.mjs +196 -0
- package/package.json +19 -28
- package/CHANGELOG.md +0 -94
- package/bin/ralphctl +0 -13
- package/src/ai/executor.ts +0 -973
- package/src/ai/lifecycle.ts +0 -45
- package/src/ai/parser.ts +0 -40
- package/src/ai/permissions.ts +0 -207
- package/src/ai/process-manager.ts +0 -248
- package/src/ai/prompts/index.ts +0 -89
- package/src/ai/rate-limiter.ts +0 -89
- package/src/ai/runner.ts +0 -478
- package/src/ai/session.ts +0 -319
- package/src/ai/task-context.ts +0 -270
- package/src/cli-metadata.ts +0 -7
- package/src/cli.ts +0 -65
- package/src/commands/completion/index.ts +0 -33
- package/src/commands/config/config.ts +0 -58
- package/src/commands/config/index.ts +0 -33
- package/src/commands/dashboard/dashboard.ts +0 -5
- package/src/commands/dashboard/index.ts +0 -6
- package/src/commands/doctor/doctor.ts +0 -271
- package/src/commands/doctor/index.ts +0 -25
- package/src/commands/progress/index.ts +0 -25
- package/src/commands/progress/log.ts +0 -64
- package/src/commands/progress/show.ts +0 -14
- package/src/commands/project/add.ts +0 -336
- package/src/commands/project/index.ts +0 -104
- package/src/commands/project/list.ts +0 -31
- package/src/commands/project/remove.ts +0 -43
- package/src/commands/project/repo.ts +0 -118
- package/src/commands/project/show.ts +0 -49
- package/src/commands/sprint/close.ts +0 -180
- package/src/commands/sprint/context.ts +0 -109
- package/src/commands/sprint/create.ts +0 -60
- package/src/commands/sprint/current.ts +0 -75
- package/src/commands/sprint/delete.ts +0 -72
- package/src/commands/sprint/health.ts +0 -229
- package/src/commands/sprint/ideate.ts +0 -496
- package/src/commands/sprint/index.ts +0 -226
- package/src/commands/sprint/list.ts +0 -86
- package/src/commands/sprint/plan-utils.ts +0 -207
- package/src/commands/sprint/plan.ts +0 -549
- package/src/commands/sprint/refine.ts +0 -359
- package/src/commands/sprint/requirements.ts +0 -58
- package/src/commands/sprint/show.ts +0 -140
- package/src/commands/sprint/start.ts +0 -119
- package/src/commands/sprint/switch.ts +0 -20
- package/src/commands/task/add.ts +0 -316
- package/src/commands/task/import.ts +0 -150
- package/src/commands/task/index.ts +0 -123
- package/src/commands/task/list.ts +0 -145
- package/src/commands/task/next.ts +0 -45
- package/src/commands/task/remove.ts +0 -47
- package/src/commands/task/reorder.ts +0 -45
- package/src/commands/task/show.ts +0 -111
- package/src/commands/task/status.ts +0 -99
- package/src/commands/ticket/add.ts +0 -265
- package/src/commands/ticket/edit.ts +0 -166
- package/src/commands/ticket/index.ts +0 -114
- package/src/commands/ticket/list.ts +0 -128
- package/src/commands/ticket/refine-utils.ts +0 -89
- package/src/commands/ticket/refine.ts +0 -268
- package/src/commands/ticket/remove.ts +0 -48
- package/src/commands/ticket/show.ts +0 -74
- package/src/completion/handle.ts +0 -30
- package/src/completion/resolver.ts +0 -241
- package/src/interactive/dashboard.ts +0 -268
- package/src/interactive/escapable.ts +0 -81
- package/src/interactive/file-browser.ts +0 -153
- package/src/interactive/index.ts +0 -429
- package/src/interactive/menu.ts +0 -403
- package/src/interactive/selectors.ts +0 -273
- package/src/interactive/wizard.ts +0 -221
- package/src/providers/claude.ts +0 -53
- package/src/providers/copilot.ts +0 -86
- package/src/providers/index.ts +0 -43
- package/src/providers/types.ts +0 -85
- package/src/schemas/index.ts +0 -130
- package/src/store/config.ts +0 -74
- package/src/store/progress.ts +0 -230
- package/src/store/project.ts +0 -276
- package/src/store/sprint.ts +0 -229
- package/src/store/task.ts +0 -443
- package/src/store/ticket.ts +0 -178
- package/src/theme/index.ts +0 -215
- package/src/theme/ui.ts +0 -872
- package/src/utils/detect-scripts.ts +0 -247
- package/src/utils/editor-input.ts +0 -41
- package/src/utils/editor.ts +0 -37
- package/src/utils/exit-codes.ts +0 -27
- package/src/utils/file-lock.ts +0 -135
- package/src/utils/git.ts +0 -185
- package/src/utils/ids.ts +0 -37
- package/src/utils/issue-fetch.ts +0 -244
- package/src/utils/json-extract.ts +0 -62
- package/src/utils/multiline.ts +0 -61
- package/src/utils/path-selector.ts +0 -236
- package/src/utils/paths.ts +0 -108
- package/src/utils/provider.ts +0 -34
- package/src/utils/requirements-export.ts +0 -63
- package/src/utils/storage.ts +0 -107
- package/tsconfig.json +0 -25
- /package/{src/ai → dist}/prompts/ideate-auto.md +0 -0
- /package/{src/ai → dist}/prompts/ideate.md +0 -0
- /package/{src/ai → dist}/prompts/plan-auto.md +0 -0
- /package/{src/ai → dist}/prompts/plan-common.md +0 -0
- /package/{src/ai → dist}/prompts/plan-interactive.md +0 -0
- /package/{src/ai → dist}/prompts/task-execution.md +0 -0
- /package/{src/ai → dist}/prompts/ticket-refine.md +0 -0
package/src/ai/lifecycle.ts
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import { spawnSync } from 'node:child_process';
|
|
2
|
-
import { assertSafeCwd } from '@src/utils/paths.ts';
|
|
3
|
-
|
|
4
|
-
/** Lifecycle events where hooks can fire. Extend this union for new phases. */
|
|
5
|
-
export type LifecycleEvent = 'sprintStart' | 'taskComplete';
|
|
6
|
-
|
|
7
|
-
export interface HookResult {
|
|
8
|
-
passed: boolean;
|
|
9
|
-
output: string;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
/** Default timeout for lifecycle hooks: 5 minutes. Override via RALPHCTL_SETUP_TIMEOUT_MS. */
|
|
13
|
-
const DEFAULT_HOOK_TIMEOUT_MS = 5 * 60 * 1000;
|
|
14
|
-
|
|
15
|
-
function getHookTimeoutMs(): number {
|
|
16
|
-
const envVal = process.env['RALPHCTL_SETUP_TIMEOUT_MS'];
|
|
17
|
-
if (envVal) {
|
|
18
|
-
const parsed = Number(envVal);
|
|
19
|
-
if (!Number.isNaN(parsed) && parsed > 0) return parsed;
|
|
20
|
-
}
|
|
21
|
-
return DEFAULT_HOOK_TIMEOUT_MS;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Run a lifecycle hook script in a project directory.
|
|
26
|
-
*
|
|
27
|
-
* Scripts are user-configured via `project add` or `project repo add` —
|
|
28
|
-
* they are NOT arbitrary AI-generated commands.
|
|
29
|
-
*/
|
|
30
|
-
export function runLifecycleHook(projectPath: string, script: string, event: LifecycleEvent): HookResult {
|
|
31
|
-
assertSafeCwd(projectPath);
|
|
32
|
-
const timeoutMs = getHookTimeoutMs();
|
|
33
|
-
|
|
34
|
-
const result = spawnSync(script, {
|
|
35
|
-
cwd: projectPath,
|
|
36
|
-
shell: true,
|
|
37
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
38
|
-
encoding: 'utf-8',
|
|
39
|
-
timeout: timeoutMs,
|
|
40
|
-
env: { ...process.env, RALPHCTL_LIFECYCLE_EVENT: event },
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
const output = [result.stdout, result.stderr].filter(Boolean).join('\n').trim();
|
|
44
|
-
return { passed: result.status === 0, output };
|
|
45
|
-
}
|
package/src/ai/parser.ts
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
export interface ExecutionResult {
|
|
2
|
-
success: boolean;
|
|
3
|
-
output: string;
|
|
4
|
-
blockedReason?: string;
|
|
5
|
-
verified?: boolean;
|
|
6
|
-
verificationOutput?: string;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Parse execution result from AI provider output.
|
|
11
|
-
* Checks for task-verified, task-complete, and task-blocked signals.
|
|
12
|
-
*/
|
|
13
|
-
export function parseExecutionResult(output: string): ExecutionResult {
|
|
14
|
-
// Check for verification signal
|
|
15
|
-
const verifiedMatch = /<task-verified>([\s\S]*?)<\/task-verified>/.exec(output);
|
|
16
|
-
const verified = verifiedMatch !== null;
|
|
17
|
-
const verificationOutput = verifiedMatch?.[1]?.trim();
|
|
18
|
-
|
|
19
|
-
// Check for completion signal
|
|
20
|
-
if (output.includes('<task-complete>')) {
|
|
21
|
-
if (!verified) {
|
|
22
|
-
return {
|
|
23
|
-
success: false,
|
|
24
|
-
output,
|
|
25
|
-
blockedReason:
|
|
26
|
-
'Task marked complete without verification. Output <task-verified> with verification results before <task-complete>.',
|
|
27
|
-
};
|
|
28
|
-
}
|
|
29
|
-
return { success: true, output, verified, verificationOutput };
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// Check for blocked signal
|
|
33
|
-
const blockedMatch = /<task-blocked>([\s\S]*?)<\/task-blocked>/.exec(output);
|
|
34
|
-
if (blockedMatch) {
|
|
35
|
-
return { success: false, output, blockedReason: blockedMatch[1]?.trim(), verified, verificationOutput };
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// No signal found - treat as incomplete
|
|
39
|
-
return { success: false, output, blockedReason: 'No completion signal received', verified, verificationOutput };
|
|
40
|
-
}
|
package/src/ai/permissions.ts
DELETED
|
@@ -1,207 +0,0 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
-
import { join } from 'node:path';
|
|
3
|
-
import { homedir } from 'node:os';
|
|
4
|
-
import type { AiProvider } from '@src/schemas/index.ts';
|
|
5
|
-
|
|
6
|
-
interface PermissionsConfig {
|
|
7
|
-
allow?: string[];
|
|
8
|
-
deny?: string[];
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
interface SettingsFile {
|
|
12
|
-
permissions?: PermissionsConfig;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export interface ProviderPermissions {
|
|
16
|
-
allow: string[];
|
|
17
|
-
deny: string[];
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Get AI provider permissions from settings files.
|
|
22
|
-
* For Claude: checks .claude/settings.local.json and ~/.claude/settings.json
|
|
23
|
-
* For Copilot: returns empty permissions (Copilot uses --available-tools/--excluded-tools flags)
|
|
24
|
-
*
|
|
25
|
-
* @param projectPath - Project directory to check for settings
|
|
26
|
-
* @param provider - AI provider (defaults to 'claude' for backward compat)
|
|
27
|
-
* @returns Combined permissions from both sources
|
|
28
|
-
*/
|
|
29
|
-
export function getProviderPermissions(projectPath: string, provider?: AiProvider): ProviderPermissions {
|
|
30
|
-
const permissions: ProviderPermissions = {
|
|
31
|
-
allow: [],
|
|
32
|
-
deny: [],
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
// Copilot manages permissions via CLI flags, not settings files
|
|
36
|
-
if (provider === 'copilot') {
|
|
37
|
-
return permissions;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// Check project-level settings (.claude/settings.local.json)
|
|
41
|
-
const projectSettingsPath = join(projectPath, '.claude', 'settings.local.json');
|
|
42
|
-
if (existsSync(projectSettingsPath)) {
|
|
43
|
-
try {
|
|
44
|
-
const content = readFileSync(projectSettingsPath, 'utf-8');
|
|
45
|
-
const settings = JSON.parse(content) as SettingsFile;
|
|
46
|
-
if (settings.permissions?.allow) {
|
|
47
|
-
permissions.allow.push(...settings.permissions.allow);
|
|
48
|
-
}
|
|
49
|
-
if (settings.permissions?.deny) {
|
|
50
|
-
permissions.deny.push(...settings.permissions.deny);
|
|
51
|
-
}
|
|
52
|
-
} catch {
|
|
53
|
-
// Ignore parse errors
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// Check user-level settings (~/.claude/settings.json)
|
|
58
|
-
const userSettingsPath = join(homedir(), '.claude', 'settings.json');
|
|
59
|
-
if (existsSync(userSettingsPath)) {
|
|
60
|
-
try {
|
|
61
|
-
const content = readFileSync(userSettingsPath, 'utf-8');
|
|
62
|
-
const settings = JSON.parse(content) as SettingsFile;
|
|
63
|
-
if (settings.permissions?.allow) {
|
|
64
|
-
permissions.allow.push(...settings.permissions.allow);
|
|
65
|
-
}
|
|
66
|
-
if (settings.permissions?.deny) {
|
|
67
|
-
permissions.deny.push(...settings.permissions.deny);
|
|
68
|
-
}
|
|
69
|
-
} catch {
|
|
70
|
-
// Ignore parse errors
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
return permissions;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Check if a specific tool/command is allowed in permissions.
|
|
79
|
-
*
|
|
80
|
-
* Permission patterns:
|
|
81
|
-
* - "Bash(command:*)" - matches "Bash" tool with command starting with "command"
|
|
82
|
-
* - "Bash(git commit:*)" - matches git commit with any message
|
|
83
|
-
* - "Bash(*)" - matches any Bash command
|
|
84
|
-
*
|
|
85
|
-
* @returns true if explicitly allowed, false if denied, 'ask' if no match
|
|
86
|
-
*/
|
|
87
|
-
export function isToolAllowed(permissions: ProviderPermissions, tool: string, specifier?: string): boolean | 'ask' {
|
|
88
|
-
// Check deny list first (deny takes precedence)
|
|
89
|
-
for (const pattern of permissions.deny) {
|
|
90
|
-
if (matchesPattern(pattern, tool, specifier)) {
|
|
91
|
-
return false;
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// Check allow list
|
|
96
|
-
for (const pattern of permissions.allow) {
|
|
97
|
-
if (matchesPattern(pattern, tool, specifier)) {
|
|
98
|
-
return true;
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// No explicit permission - will ask
|
|
103
|
-
return 'ask';
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* Match a permission pattern against a tool call.
|
|
108
|
-
*
|
|
109
|
-
* Pattern formats:
|
|
110
|
-
* - "ToolName" - matches tool name exactly
|
|
111
|
-
* - "ToolName(specifier)" - matches exact specifier
|
|
112
|
-
* - "ToolName(prefix*)" - matches specifier starting with prefix
|
|
113
|
-
* - "ToolName(*)" - matches any specifier for this tool
|
|
114
|
-
*/
|
|
115
|
-
function matchesPattern(pattern: string, tool: string, specifier?: string): boolean {
|
|
116
|
-
// Parse pattern
|
|
117
|
-
const parenIdx = pattern.indexOf('(');
|
|
118
|
-
|
|
119
|
-
if (parenIdx === -1) {
|
|
120
|
-
// Pattern is just tool name
|
|
121
|
-
return pattern === tool;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
const patternTool = pattern.slice(0, parenIdx);
|
|
125
|
-
if (patternTool !== tool) {
|
|
126
|
-
return false;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
// Extract specifier pattern (remove parentheses)
|
|
130
|
-
const specPattern = pattern.slice(parenIdx + 1, -1);
|
|
131
|
-
|
|
132
|
-
if (specPattern === '*') {
|
|
133
|
-
// Matches any specifier
|
|
134
|
-
return true;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
if (!specifier) {
|
|
138
|
-
return false;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
if (specPattern.endsWith(':*')) {
|
|
142
|
-
// Prefix match (e.g., "git commit:*" matches "git commit -m 'msg'")
|
|
143
|
-
const prefix = specPattern.slice(0, -2);
|
|
144
|
-
return specifier.startsWith(prefix);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
if (specPattern.endsWith('*')) {
|
|
148
|
-
// Simple prefix match
|
|
149
|
-
const prefix = specPattern.slice(0, -1);
|
|
150
|
-
return specifier.startsWith(prefix);
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// Exact match
|
|
154
|
-
return specPattern === specifier;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
export interface PermissionWarning {
|
|
158
|
-
tool: string;
|
|
159
|
-
specifier?: string;
|
|
160
|
-
message: string;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
/**
|
|
164
|
-
* Check permissions for common operations needed during task execution.
|
|
165
|
-
*
|
|
166
|
-
* For Claude: reads settings files and warns about operations that may need approval.
|
|
167
|
-
* For Copilot: returns no warnings (all tools granted via --allow-all-tools).
|
|
168
|
-
*
|
|
169
|
-
* @returns Array of warnings for operations that may need approval
|
|
170
|
-
*/
|
|
171
|
-
export function checkTaskPermissions(
|
|
172
|
-
projectPath: string,
|
|
173
|
-
options: {
|
|
174
|
-
checkScript?: string | null;
|
|
175
|
-
needsCommit?: boolean;
|
|
176
|
-
provider?: AiProvider;
|
|
177
|
-
}
|
|
178
|
-
): PermissionWarning[] {
|
|
179
|
-
const warnings: PermissionWarning[] = [];
|
|
180
|
-
const permissions = getProviderPermissions(projectPath, options.provider);
|
|
181
|
-
|
|
182
|
-
// Check git commit permission
|
|
183
|
-
if (options.needsCommit !== false) {
|
|
184
|
-
const commitAllowed = isToolAllowed(permissions, 'Bash', 'git commit');
|
|
185
|
-
if (commitAllowed !== true) {
|
|
186
|
-
warnings.push({
|
|
187
|
-
tool: 'Bash',
|
|
188
|
-
specifier: 'git commit',
|
|
189
|
-
message: 'Git commits may require manual approval',
|
|
190
|
-
});
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
// Check check script permission
|
|
195
|
-
if (options.checkScript) {
|
|
196
|
-
const checkAllowed = isToolAllowed(permissions, 'Bash', options.checkScript);
|
|
197
|
-
if (checkAllowed !== true) {
|
|
198
|
-
warnings.push({
|
|
199
|
-
tool: 'Bash',
|
|
200
|
-
specifier: options.checkScript,
|
|
201
|
-
message: `Check script "${options.checkScript}" may require approval`,
|
|
202
|
-
});
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
return warnings;
|
|
207
|
-
}
|
|
@@ -1,248 +0,0 @@
|
|
|
1
|
-
import type { ChildProcess } from 'node:child_process';
|
|
2
|
-
import { EXIT_INTERRUPTED } from '@src/utils/exit-codes.ts';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Graceful shutdown timeout - how long to wait for children to exit after SIGTERM
|
|
6
|
-
*/
|
|
7
|
-
const GRACEFUL_SHUTDOWN_TIMEOUT_MS = 5000;
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Double-signal window - time window for second Ctrl+C to trigger force-quit
|
|
11
|
-
*/
|
|
12
|
-
const FORCE_QUIT_WINDOW_MS = 5000;
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Singleton manager for all AI provider child processes.
|
|
16
|
-
* Ensures proper cleanup on SIGINT/SIGTERM with graceful shutdown sequence.
|
|
17
|
-
*
|
|
18
|
-
* Features:
|
|
19
|
-
* - First SIGINT: Graceful shutdown (SIGTERM to children, wait 5s, then SIGKILL)
|
|
20
|
-
* - Second SIGINT (within 5s): Force-quit (immediate SIGKILL, exit code 1)
|
|
21
|
-
* - Exit code 130 for SIGINT (standard Unix convention: 128 + 2)
|
|
22
|
-
* - Automatic child cleanup via event listeners
|
|
23
|
-
* - Cleanup callbacks for spinners and temp resources
|
|
24
|
-
*/
|
|
25
|
-
export class ProcessManager {
|
|
26
|
-
private static instance: ProcessManager | null = null;
|
|
27
|
-
|
|
28
|
-
/** All active AI child processes */
|
|
29
|
-
private children = new Set<ChildProcess>();
|
|
30
|
-
|
|
31
|
-
/** Cleanup callbacks (for stopping spinners, removing temp files) */
|
|
32
|
-
private cleanupCallbacks = new Set<() => void>();
|
|
33
|
-
|
|
34
|
-
/** Whether we're currently shutting down */
|
|
35
|
-
private exiting = false;
|
|
36
|
-
|
|
37
|
-
/** Whether signal handlers have been installed */
|
|
38
|
-
private handlersInstalled = false;
|
|
39
|
-
|
|
40
|
-
/** Timestamp of first SIGINT (for double-signal detection) */
|
|
41
|
-
private firstSigintAt: number | null = null;
|
|
42
|
-
|
|
43
|
-
private constructor() {
|
|
44
|
-
// Private constructor for singleton
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Get the singleton instance.
|
|
49
|
-
*/
|
|
50
|
-
public static getInstance(): ProcessManager {
|
|
51
|
-
ProcessManager.instance ??= new ProcessManager();
|
|
52
|
-
return ProcessManager.instance;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Reset the singleton for testing.
|
|
57
|
-
* @internal
|
|
58
|
-
*/
|
|
59
|
-
public static resetForTesting(): void {
|
|
60
|
-
if (ProcessManager.instance) {
|
|
61
|
-
ProcessManager.instance.dispose();
|
|
62
|
-
ProcessManager.instance = null;
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Register a child process for tracking.
|
|
68
|
-
* Automatically installs signal handlers on first registration.
|
|
69
|
-
* Throws an error if called during shutdown.
|
|
70
|
-
*
|
|
71
|
-
* @throws Error if called during shutdown
|
|
72
|
-
*/
|
|
73
|
-
public registerChild(child: ChildProcess): void {
|
|
74
|
-
if (this.exiting) {
|
|
75
|
-
throw new Error('Cannot register child process during shutdown');
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
this.children.add(child);
|
|
79
|
-
|
|
80
|
-
// Auto-cleanup when child exits
|
|
81
|
-
child.once('close', () => {
|
|
82
|
-
this.children.delete(child);
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
// Install signal handlers on first child registration
|
|
86
|
-
if (!this.handlersInstalled) {
|
|
87
|
-
this.installSignalHandlers();
|
|
88
|
-
this.handlersInstalled = true;
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* Eagerly install signal handlers without requiring a child registration.
|
|
94
|
-
* Call this at the top of execution loops so Ctrl+C works even before
|
|
95
|
-
* the first AI process is spawned (e.g. while the spinner is visible).
|
|
96
|
-
* Idempotent — safe to call multiple times.
|
|
97
|
-
*/
|
|
98
|
-
public ensureHandlers(): void {
|
|
99
|
-
if (!this.handlersInstalled) {
|
|
100
|
-
this.installSignalHandlers();
|
|
101
|
-
this.handlersInstalled = true;
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* Check if a shutdown is in progress.
|
|
107
|
-
* Used by execution loops to break immediately on Ctrl+C.
|
|
108
|
-
*/
|
|
109
|
-
public isShuttingDown(): boolean {
|
|
110
|
-
return this.exiting;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* Manually unregister a child process.
|
|
115
|
-
* Normally not needed - children auto-unregister via event listeners.
|
|
116
|
-
*/
|
|
117
|
-
public unregisterChild(child: ChildProcess): void {
|
|
118
|
-
this.children.delete(child);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
* Register a cleanup callback (for spinners, temp files, etc.).
|
|
123
|
-
* Returns a deregister function.
|
|
124
|
-
*/
|
|
125
|
-
public registerCleanup(callback: () => void): () => void {
|
|
126
|
-
this.cleanupCallbacks.add(callback);
|
|
127
|
-
return () => {
|
|
128
|
-
this.cleanupCallbacks.delete(callback);
|
|
129
|
-
};
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
/**
|
|
133
|
-
* Kill all tracked child processes with the given signal.
|
|
134
|
-
* Catches errors (ESRCH = already dead, EPERM = permission denied).
|
|
135
|
-
*/
|
|
136
|
-
public killAll(signal: NodeJS.Signals): void {
|
|
137
|
-
for (const child of this.children) {
|
|
138
|
-
try {
|
|
139
|
-
child.kill(signal);
|
|
140
|
-
} catch (err) {
|
|
141
|
-
const error = err as NodeJS.ErrnoException;
|
|
142
|
-
if (error.code === 'ESRCH') {
|
|
143
|
-
// Process already dead - silently remove
|
|
144
|
-
this.children.delete(child);
|
|
145
|
-
} else if (error.code === 'EPERM') {
|
|
146
|
-
// Permission denied - log warning
|
|
147
|
-
console.warn(`Warning: Permission denied killing process ${String(child.pid)}`);
|
|
148
|
-
} else {
|
|
149
|
-
// Unknown error - log but continue
|
|
150
|
-
console.error(`Error killing process ${String(child.pid)}:`, error.message);
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
/**
|
|
157
|
-
* Graceful shutdown sequence:
|
|
158
|
-
* 1. Run all cleanup callbacks (stop spinners)
|
|
159
|
-
* 2. Send SIGINT to all children (what AI CLI processes expect)
|
|
160
|
-
* 3. Wait up to 5 seconds for children to exit
|
|
161
|
-
* 4. Send SIGKILL to any remaining children (force)
|
|
162
|
-
* 5. Exit with code 130 (SIGINT) or 1 (force-quit)
|
|
163
|
-
*
|
|
164
|
-
* Double Ctrl+C: immediate SIGKILL + exit(1)
|
|
165
|
-
*/
|
|
166
|
-
public async shutdown(signal: NodeJS.Signals): Promise<void> {
|
|
167
|
-
// Double-signal force-quit check MUST run before the exiting guard,
|
|
168
|
-
// otherwise the second Ctrl+C is swallowed and force-quit never fires.
|
|
169
|
-
if (signal === 'SIGINT' && this.firstSigintAt) {
|
|
170
|
-
const now = Date.now();
|
|
171
|
-
if (now - this.firstSigintAt < FORCE_QUIT_WINDOW_MS) {
|
|
172
|
-
console.log('\n\nForce quit (double signal) — killing all processes immediately...');
|
|
173
|
-
this.killAll('SIGKILL');
|
|
174
|
-
process.exit(1);
|
|
175
|
-
return;
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
if (this.exiting) {
|
|
180
|
-
return; // Already shutting down (non-SIGINT duplicate)
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
this.exiting = true;
|
|
184
|
-
|
|
185
|
-
// Record timestamp for double-signal detection
|
|
186
|
-
if (signal === 'SIGINT') {
|
|
187
|
-
this.firstSigintAt = Date.now();
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
console.log('\n\nShutting down gracefully... (press Ctrl+C again to force-quit)');
|
|
191
|
-
|
|
192
|
-
// Run cleanup callbacks
|
|
193
|
-
for (const callback of this.cleanupCallbacks) {
|
|
194
|
-
try {
|
|
195
|
-
callback();
|
|
196
|
-
} catch (err) {
|
|
197
|
-
const error = err as Error;
|
|
198
|
-
console.error('Error in cleanup callback:', error.message);
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
this.cleanupCallbacks.clear();
|
|
202
|
-
|
|
203
|
-
// Send SIGINT to children — claude CLI handles SIGINT for graceful shutdown.
|
|
204
|
-
// SIGTERM may be ignored by some child process trees.
|
|
205
|
-
this.killAll('SIGINT');
|
|
206
|
-
|
|
207
|
-
// Wait for children to exit (with timeout)
|
|
208
|
-
const waitStart = Date.now();
|
|
209
|
-
while (this.children.size > 0 && Date.now() - waitStart < GRACEFUL_SHUTDOWN_TIMEOUT_MS) {
|
|
210
|
-
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
// Force-kill any remaining children
|
|
214
|
-
if (this.children.size > 0) {
|
|
215
|
-
console.log(`Force-killing ${String(this.children.size)} remaining process(es)...`);
|
|
216
|
-
this.killAll('SIGKILL');
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
// Exit with appropriate code
|
|
220
|
-
process.exit(signal === 'SIGINT' ? EXIT_INTERRUPTED : 1);
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
/**
|
|
224
|
-
* Clean up all resources (for testing).
|
|
225
|
-
* @internal
|
|
226
|
-
*/
|
|
227
|
-
public dispose(): void {
|
|
228
|
-
this.children.clear();
|
|
229
|
-
this.cleanupCallbacks.clear();
|
|
230
|
-
this.exiting = false;
|
|
231
|
-
this.handlersInstalled = false;
|
|
232
|
-
this.firstSigintAt = null;
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
/**
|
|
236
|
-
* Install signal handlers for SIGINT and SIGTERM.
|
|
237
|
-
* Uses process.on() (persistent) not process.once() (one-shot).
|
|
238
|
-
*/
|
|
239
|
-
private installSignalHandlers(): void {
|
|
240
|
-
process.on('SIGINT', () => {
|
|
241
|
-
void this.shutdown('SIGINT');
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
process.on('SIGTERM', () => {
|
|
245
|
-
void this.shutdown('SIGTERM');
|
|
246
|
-
});
|
|
247
|
-
}
|
|
248
|
-
}
|
package/src/ai/prompts/index.ts
DELETED
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
import { readFileSync } from 'node:fs';
|
|
2
|
-
import { dirname, join } from 'node:path';
|
|
3
|
-
import { fileURLToPath } from 'node:url';
|
|
4
|
-
|
|
5
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
6
|
-
|
|
7
|
-
function loadTemplate(name: string): string {
|
|
8
|
-
return readFileSync(join(__dirname, `${name}.md`), 'utf-8');
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
function buildPlanPrompt(template: string, context: string, schema: string): string {
|
|
12
|
-
const common = loadTemplate('plan-common');
|
|
13
|
-
return template.replace('{{COMMON}}', common).replace('{{CONTEXT}}', context).replace('{{SCHEMA}}', schema);
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export function buildInteractivePrompt(context: string, outputFile: string, schema: string): string {
|
|
17
|
-
const template = loadTemplate('plan-interactive');
|
|
18
|
-
return buildPlanPrompt(template, context, schema).replace('{{OUTPUT_FILE}}', outputFile);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export function buildAutoPrompt(context: string, schema: string): string {
|
|
22
|
-
const template = loadTemplate('plan-auto');
|
|
23
|
-
return buildPlanPrompt(template, context, schema);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export function buildTaskExecutionPrompt(progressFilePath: string, noCommit: boolean, contextFileName: string): string {
|
|
27
|
-
const template = loadTemplate('task-execution');
|
|
28
|
-
const commitStep = noCommit
|
|
29
|
-
? ''
|
|
30
|
-
: '\n> **Before continuing:** Create a git commit with a descriptive message for the changes made.\n';
|
|
31
|
-
const commitConstraint = noCommit ? '' : '- **Must commit** — Create a git commit before signaling completion.\n';
|
|
32
|
-
return template
|
|
33
|
-
.replace('{{PROGRESS_FILE}}', progressFilePath)
|
|
34
|
-
.replace('{{COMMIT_STEP}}', commitStep)
|
|
35
|
-
.replace('{{COMMIT_CONSTRAINT}}', commitConstraint)
|
|
36
|
-
.replaceAll('{{CONTEXT_FILE}}', contextFileName);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export function buildTicketRefinePrompt(
|
|
40
|
-
ticketContent: string,
|
|
41
|
-
outputFile: string,
|
|
42
|
-
schema: string,
|
|
43
|
-
issueContext = ''
|
|
44
|
-
): string {
|
|
45
|
-
const template = loadTemplate('ticket-refine');
|
|
46
|
-
return template
|
|
47
|
-
.replace('{{TICKET}}', ticketContent)
|
|
48
|
-
.replace('{{OUTPUT_FILE}}', outputFile)
|
|
49
|
-
.replace('{{SCHEMA}}', schema)
|
|
50
|
-
.replace('{{ISSUE_CONTEXT}}', issueContext);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export function buildIdeatePrompt(
|
|
54
|
-
ideaTitle: string,
|
|
55
|
-
ideaDescription: string,
|
|
56
|
-
projectName: string,
|
|
57
|
-
repositories: string,
|
|
58
|
-
outputFile: string,
|
|
59
|
-
schema: string
|
|
60
|
-
): string {
|
|
61
|
-
const template = loadTemplate('ideate');
|
|
62
|
-
const common = loadTemplate('plan-common');
|
|
63
|
-
return template
|
|
64
|
-
.replace('{{IDEA_TITLE}}', ideaTitle)
|
|
65
|
-
.replace('{{IDEA_DESCRIPTION}}', ideaDescription)
|
|
66
|
-
.replace('{{PROJECT_NAME}}', projectName)
|
|
67
|
-
.replace('{{REPOSITORIES}}', repositories)
|
|
68
|
-
.replace('{{OUTPUT_FILE}}', outputFile)
|
|
69
|
-
.replace('{{SCHEMA}}', schema)
|
|
70
|
-
.replace('{{COMMON}}', common);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
export function buildIdeateAutoPrompt(
|
|
74
|
-
ideaTitle: string,
|
|
75
|
-
ideaDescription: string,
|
|
76
|
-
projectName: string,
|
|
77
|
-
repositories: string,
|
|
78
|
-
schema: string
|
|
79
|
-
): string {
|
|
80
|
-
const template = loadTemplate('ideate-auto');
|
|
81
|
-
const common = loadTemplate('plan-common');
|
|
82
|
-
return template
|
|
83
|
-
.replace('{{IDEA_TITLE}}', ideaTitle)
|
|
84
|
-
.replace('{{IDEA_DESCRIPTION}}', ideaDescription)
|
|
85
|
-
.replace('{{PROJECT_NAME}}', projectName)
|
|
86
|
-
.replace('{{REPOSITORIES}}', repositories)
|
|
87
|
-
.replace('{{SCHEMA}}', schema)
|
|
88
|
-
.replace('{{COMMON}}', common);
|
|
89
|
-
}
|