wiggum-cli 0.1.0

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.
Files changed (236) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +341 -0
  3. package/bin/ralph.js +8 -0
  4. package/dist/ai/enhancer.d.ts +100 -0
  5. package/dist/ai/enhancer.d.ts.map +1 -0
  6. package/dist/ai/enhancer.js +233 -0
  7. package/dist/ai/enhancer.js.map +1 -0
  8. package/dist/ai/index.d.ts +8 -0
  9. package/dist/ai/index.d.ts.map +1 -0
  10. package/dist/ai/index.js +11 -0
  11. package/dist/ai/index.js.map +1 -0
  12. package/dist/ai/prompts.d.ts +26 -0
  13. package/dist/ai/prompts.d.ts.map +1 -0
  14. package/dist/ai/prompts.js +201 -0
  15. package/dist/ai/prompts.js.map +1 -0
  16. package/dist/ai/providers.d.ts +35 -0
  17. package/dist/ai/providers.d.ts.map +1 -0
  18. package/dist/ai/providers.js +104 -0
  19. package/dist/ai/providers.js.map +1 -0
  20. package/dist/cli.d.ts +6 -0
  21. package/dist/cli.d.ts.map +1 -0
  22. package/dist/cli.js +196 -0
  23. package/dist/cli.js.map +1 -0
  24. package/dist/commands/init.d.ts +16 -0
  25. package/dist/commands/init.d.ts.map +1 -0
  26. package/dist/commands/init.js +124 -0
  27. package/dist/commands/init.js.map +1 -0
  28. package/dist/commands/monitor.d.ts +17 -0
  29. package/dist/commands/monitor.d.ts.map +1 -0
  30. package/dist/commands/monitor.js +342 -0
  31. package/dist/commands/monitor.js.map +1 -0
  32. package/dist/commands/new.d.ts +19 -0
  33. package/dist/commands/new.d.ts.map +1 -0
  34. package/dist/commands/new.js +272 -0
  35. package/dist/commands/new.js.map +1 -0
  36. package/dist/commands/run.d.ts +16 -0
  37. package/dist/commands/run.d.ts.map +1 -0
  38. package/dist/commands/run.js +175 -0
  39. package/dist/commands/run.js.map +1 -0
  40. package/dist/generator/config.d.ts +59 -0
  41. package/dist/generator/config.d.ts.map +1 -0
  42. package/dist/generator/config.js +68 -0
  43. package/dist/generator/config.js.map +1 -0
  44. package/dist/generator/index.d.ts +64 -0
  45. package/dist/generator/index.d.ts.map +1 -0
  46. package/dist/generator/index.js +147 -0
  47. package/dist/generator/index.js.map +1 -0
  48. package/dist/generator/templates.d.ts +70 -0
  49. package/dist/generator/templates.d.ts.map +1 -0
  50. package/dist/generator/templates.js +296 -0
  51. package/dist/generator/templates.js.map +1 -0
  52. package/dist/generator/writer.d.ts +93 -0
  53. package/dist/generator/writer.d.ts.map +1 -0
  54. package/dist/generator/writer.js +213 -0
  55. package/dist/generator/writer.js.map +1 -0
  56. package/dist/index.d.ts +12 -0
  57. package/dist/index.d.ts.map +1 -0
  58. package/dist/index.js +17 -0
  59. package/dist/index.js.map +1 -0
  60. package/dist/scanner/detectors/core/framework.d.ts +11 -0
  61. package/dist/scanner/detectors/core/framework.d.ts.map +1 -0
  62. package/dist/scanner/detectors/core/framework.js +275 -0
  63. package/dist/scanner/detectors/core/framework.js.map +1 -0
  64. package/dist/scanner/detectors/core/packageManager.d.ts +11 -0
  65. package/dist/scanner/detectors/core/packageManager.d.ts.map +1 -0
  66. package/dist/scanner/detectors/core/packageManager.js +74 -0
  67. package/dist/scanner/detectors/core/packageManager.js.map +1 -0
  68. package/dist/scanner/detectors/core/styling.d.ts +12 -0
  69. package/dist/scanner/detectors/core/styling.d.ts.map +1 -0
  70. package/dist/scanner/detectors/core/styling.js +230 -0
  71. package/dist/scanner/detectors/core/styling.js.map +1 -0
  72. package/dist/scanner/detectors/core/testing.d.ts +12 -0
  73. package/dist/scanner/detectors/core/testing.d.ts.map +1 -0
  74. package/dist/scanner/detectors/core/testing.js +190 -0
  75. package/dist/scanner/detectors/core/testing.js.map +1 -0
  76. package/dist/scanner/detectors/data/api.d.ts +12 -0
  77. package/dist/scanner/detectors/data/api.d.ts.map +1 -0
  78. package/dist/scanner/detectors/data/api.js +261 -0
  79. package/dist/scanner/detectors/data/api.js.map +1 -0
  80. package/dist/scanner/detectors/data/database.d.ts +12 -0
  81. package/dist/scanner/detectors/data/database.d.ts.map +1 -0
  82. package/dist/scanner/detectors/data/database.js +213 -0
  83. package/dist/scanner/detectors/data/database.js.map +1 -0
  84. package/dist/scanner/detectors/data/orm.d.ts +12 -0
  85. package/dist/scanner/detectors/data/orm.d.ts.map +1 -0
  86. package/dist/scanner/detectors/data/orm.js +160 -0
  87. package/dist/scanner/detectors/data/orm.js.map +1 -0
  88. package/dist/scanner/detectors/frontend/formHandling.d.ts +12 -0
  89. package/dist/scanner/detectors/frontend/formHandling.d.ts.map +1 -0
  90. package/dist/scanner/detectors/frontend/formHandling.js +211 -0
  91. package/dist/scanner/detectors/frontend/formHandling.js.map +1 -0
  92. package/dist/scanner/detectors/frontend/stateManagement.d.ts +12 -0
  93. package/dist/scanner/detectors/frontend/stateManagement.d.ts.map +1 -0
  94. package/dist/scanner/detectors/frontend/stateManagement.js +221 -0
  95. package/dist/scanner/detectors/frontend/stateManagement.js.map +1 -0
  96. package/dist/scanner/detectors/frontend/uiComponents.d.ts +12 -0
  97. package/dist/scanner/detectors/frontend/uiComponents.d.ts.map +1 -0
  98. package/dist/scanner/detectors/frontend/uiComponents.js +285 -0
  99. package/dist/scanner/detectors/frontend/uiComponents.js.map +1 -0
  100. package/dist/scanner/detectors/infra/deployment.d.ts +12 -0
  101. package/dist/scanner/detectors/infra/deployment.d.ts.map +1 -0
  102. package/dist/scanner/detectors/infra/deployment.js +301 -0
  103. package/dist/scanner/detectors/infra/deployment.js.map +1 -0
  104. package/dist/scanner/detectors/infra/monorepo.d.ts +12 -0
  105. package/dist/scanner/detectors/infra/monorepo.d.ts.map +1 -0
  106. package/dist/scanner/detectors/infra/monorepo.js +219 -0
  107. package/dist/scanner/detectors/infra/monorepo.js.map +1 -0
  108. package/dist/scanner/detectors/mcp/mcpProject.d.ts +12 -0
  109. package/dist/scanner/detectors/mcp/mcpProject.d.ts.map +1 -0
  110. package/dist/scanner/detectors/mcp/mcpProject.js +154 -0
  111. package/dist/scanner/detectors/mcp/mcpProject.js.map +1 -0
  112. package/dist/scanner/detectors/mcp/mcpServers.d.ts +17 -0
  113. package/dist/scanner/detectors/mcp/mcpServers.d.ts.map +1 -0
  114. package/dist/scanner/detectors/mcp/mcpServers.js +193 -0
  115. package/dist/scanner/detectors/mcp/mcpServers.js.map +1 -0
  116. package/dist/scanner/detectors/services/analytics.d.ts +12 -0
  117. package/dist/scanner/detectors/services/analytics.d.ts.map +1 -0
  118. package/dist/scanner/detectors/services/analytics.js +236 -0
  119. package/dist/scanner/detectors/services/analytics.js.map +1 -0
  120. package/dist/scanner/detectors/services/auth.d.ts +12 -0
  121. package/dist/scanner/detectors/services/auth.d.ts.map +1 -0
  122. package/dist/scanner/detectors/services/auth.js +217 -0
  123. package/dist/scanner/detectors/services/auth.js.map +1 -0
  124. package/dist/scanner/detectors/services/email.d.ts +12 -0
  125. package/dist/scanner/detectors/services/email.d.ts.map +1 -0
  126. package/dist/scanner/detectors/services/email.js +211 -0
  127. package/dist/scanner/detectors/services/email.js.map +1 -0
  128. package/dist/scanner/detectors/services/payments.d.ts +12 -0
  129. package/dist/scanner/detectors/services/payments.d.ts.map +1 -0
  130. package/dist/scanner/detectors/services/payments.js +185 -0
  131. package/dist/scanner/detectors/services/payments.js.map +1 -0
  132. package/dist/scanner/detectors/utils.d.ts +160 -0
  133. package/dist/scanner/detectors/utils.d.ts.map +1 -0
  134. package/dist/scanner/detectors/utils.js +222 -0
  135. package/dist/scanner/detectors/utils.js.map +1 -0
  136. package/dist/scanner/index.d.ts +42 -0
  137. package/dist/scanner/index.d.ts.map +1 -0
  138. package/dist/scanner/index.js +282 -0
  139. package/dist/scanner/index.js.map +1 -0
  140. package/dist/scanner/registry.d.ts +43 -0
  141. package/dist/scanner/registry.d.ts.map +1 -0
  142. package/dist/scanner/registry.js +243 -0
  143. package/dist/scanner/registry.js.map +1 -0
  144. package/dist/scanner/types.d.ts +112 -0
  145. package/dist/scanner/types.d.ts.map +1 -0
  146. package/dist/scanner/types.js +6 -0
  147. package/dist/scanner/types.js.map +1 -0
  148. package/dist/templates/config/ralph.config.js.tmpl +38 -0
  149. package/dist/templates/guides/AGENTS.md.tmpl +100 -0
  150. package/dist/templates/guides/FRONTEND.md.tmpl +523 -0
  151. package/dist/templates/guides/PERFORMANCE.md.tmpl +264 -0
  152. package/dist/templates/guides/SECURITY.md.tmpl +100 -0
  153. package/dist/templates/prompts/PROMPT.md.tmpl +77 -0
  154. package/dist/templates/prompts/PROMPT_e2e.md.tmpl +234 -0
  155. package/dist/templates/prompts/PROMPT_feature.md.tmpl +83 -0
  156. package/dist/templates/prompts/PROMPT_review.md.tmpl +167 -0
  157. package/dist/templates/prompts/PROMPT_verify.md.tmpl +72 -0
  158. package/dist/templates/root/.gitignore.tmpl +5 -0
  159. package/dist/templates/root/LEARNINGS.md.tmpl +24 -0
  160. package/dist/templates/root/README.md.tmpl +61 -0
  161. package/dist/templates/scripts/feature-loop.sh.tmpl +267 -0
  162. package/dist/templates/scripts/loop.sh.tmpl +59 -0
  163. package/dist/templates/scripts/ralph-monitor.sh.tmpl +244 -0
  164. package/dist/templates/specs/README.md.tmpl +57 -0
  165. package/dist/templates/specs/_example.md.tmpl +71 -0
  166. package/dist/utils/config.d.ts +95 -0
  167. package/dist/utils/config.d.ts.map +1 -0
  168. package/dist/utils/config.js +148 -0
  169. package/dist/utils/config.js.map +1 -0
  170. package/dist/utils/header.d.ts +5 -0
  171. package/dist/utils/header.d.ts.map +1 -0
  172. package/dist/utils/header.js +15 -0
  173. package/dist/utils/header.js.map +1 -0
  174. package/dist/utils/logger.d.ts +11 -0
  175. package/dist/utils/logger.d.ts.map +1 -0
  176. package/dist/utils/logger.js +24 -0
  177. package/dist/utils/logger.js.map +1 -0
  178. package/package.json +44 -0
  179. package/src/ai/enhancer.ts +350 -0
  180. package/src/ai/index.ts +38 -0
  181. package/src/ai/prompts.ts +217 -0
  182. package/src/ai/providers.ts +136 -0
  183. package/src/cli.ts +255 -0
  184. package/src/commands/init.ts +149 -0
  185. package/src/commands/monitor.ts +412 -0
  186. package/src/commands/new.ts +312 -0
  187. package/src/commands/run.ts +214 -0
  188. package/src/generator/config.ts +116 -0
  189. package/src/generator/index.ts +227 -0
  190. package/src/generator/templates.ts +412 -0
  191. package/src/generator/writer.ts +293 -0
  192. package/src/index.ts +41 -0
  193. package/src/scanner/detectors/core/framework.ts +332 -0
  194. package/src/scanner/detectors/core/packageManager.ts +91 -0
  195. package/src/scanner/detectors/core/styling.ts +261 -0
  196. package/src/scanner/detectors/core/testing.ts +221 -0
  197. package/src/scanner/detectors/data/api.ts +303 -0
  198. package/src/scanner/detectors/data/database.ts +245 -0
  199. package/src/scanner/detectors/data/orm.ts +180 -0
  200. package/src/scanner/detectors/frontend/formHandling.ts +244 -0
  201. package/src/scanner/detectors/frontend/stateManagement.ts +261 -0
  202. package/src/scanner/detectors/frontend/uiComponents.ts +328 -0
  203. package/src/scanner/detectors/infra/deployment.ts +343 -0
  204. package/src/scanner/detectors/infra/monorepo.ts +251 -0
  205. package/src/scanner/detectors/mcp/mcpProject.ts +176 -0
  206. package/src/scanner/detectors/mcp/mcpServers.ts +237 -0
  207. package/src/scanner/detectors/services/analytics.ts +273 -0
  208. package/src/scanner/detectors/services/auth.ts +254 -0
  209. package/src/scanner/detectors/services/email.ts +244 -0
  210. package/src/scanner/detectors/services/payments.ts +213 -0
  211. package/src/scanner/detectors/utils.ts +251 -0
  212. package/src/scanner/index.ts +354 -0
  213. package/src/scanner/registry.ts +301 -0
  214. package/src/scanner/types.ts +152 -0
  215. package/src/templates/config/ralph.config.js.tmpl +38 -0
  216. package/src/templates/guides/AGENTS.md.tmpl +100 -0
  217. package/src/templates/guides/FRONTEND.md.tmpl +523 -0
  218. package/src/templates/guides/PERFORMANCE.md.tmpl +264 -0
  219. package/src/templates/guides/SECURITY.md.tmpl +100 -0
  220. package/src/templates/prompts/PROMPT.md.tmpl +77 -0
  221. package/src/templates/prompts/PROMPT_e2e.md.tmpl +234 -0
  222. package/src/templates/prompts/PROMPT_feature.md.tmpl +83 -0
  223. package/src/templates/prompts/PROMPT_review.md.tmpl +167 -0
  224. package/src/templates/prompts/PROMPT_verify.md.tmpl +72 -0
  225. package/src/templates/root/.gitignore.tmpl +5 -0
  226. package/src/templates/root/LEARNINGS.md.tmpl +24 -0
  227. package/src/templates/root/README.md.tmpl +61 -0
  228. package/src/templates/scripts/feature-loop.sh.tmpl +267 -0
  229. package/src/templates/scripts/loop.sh.tmpl +59 -0
  230. package/src/templates/scripts/ralph-monitor.sh.tmpl +244 -0
  231. package/src/templates/specs/README.md.tmpl +57 -0
  232. package/src/templates/specs/_example.md.tmpl +71 -0
  233. package/src/utils/config.ts +221 -0
  234. package/src/utils/header.ts +15 -0
  235. package/src/utils/logger.ts +28 -0
  236. package/tsconfig.json +19 -0
@@ -0,0 +1,149 @@
1
+ /**
2
+ * Init Command
3
+ * Initialize Ralph in the current project - scans and generates configuration
4
+ */
5
+
6
+ import { logger } from '../utils/logger.js';
7
+ import { Scanner, formatScanResult, type ScanResult } from '../scanner/index.js';
8
+ import { Generator, formatGenerationResult } from '../generator/index.js';
9
+ import {
10
+ AIEnhancer,
11
+ formatAIAnalysis,
12
+ type AIProvider,
13
+ type EnhancedScanResult,
14
+ } from '../ai/index.js';
15
+ import * as prompts from '@clack/prompts';
16
+ import pc from 'picocolors';
17
+
18
+ export interface InitOptions {
19
+ ai?: boolean;
20
+ provider?: AIProvider;
21
+ yes?: boolean;
22
+ }
23
+
24
+ /**
25
+ * Initialize Ralph in the current project
26
+ * Scans the project and generates configuration
27
+ */
28
+ export async function initCommand(options: InitOptions): Promise<void> {
29
+ const projectRoot = process.cwd();
30
+
31
+ logger.info('Initializing Ralph...');
32
+ logger.info(`Project: ${projectRoot}`);
33
+ console.log('');
34
+
35
+ // Step 1: Scan the project
36
+ const spinner = prompts.spinner();
37
+ spinner.start('Scanning project...');
38
+
39
+ const scanner = new Scanner();
40
+ let scanResult: ScanResult;
41
+
42
+ try {
43
+ scanResult = await scanner.scan(projectRoot);
44
+ spinner.stop('Project scanned successfully');
45
+ } catch (error) {
46
+ spinner.stop('Scan failed');
47
+ logger.error(`Failed to scan project: ${error instanceof Error ? error.message : String(error)}`);
48
+ process.exit(1);
49
+ }
50
+
51
+ // Display scan results
52
+ console.log('');
53
+ console.log(pc.cyan('--- Scan Results ---'));
54
+ console.log(formatScanResult(scanResult));
55
+ console.log('');
56
+
57
+ // Step 2: AI Enhancement (if enabled)
58
+ let enhancedResult: EnhancedScanResult | undefined;
59
+
60
+ if (options.ai) {
61
+ const provider = options.provider || 'anthropic';
62
+ console.log(pc.cyan(`--- AI Enhancement (${provider}) ---`));
63
+
64
+ const aiEnhancer = new AIEnhancer({
65
+ provider,
66
+ verbose: true,
67
+ });
68
+
69
+ // Check if API key is available
70
+ if (!aiEnhancer.isAvailable()) {
71
+ const envVar = aiEnhancer.getRequiredEnvVar();
72
+ logger.warn(`AI enhancement skipped: ${envVar} not set`);
73
+ logger.info(`To enable AI enhancement, set the ${envVar} environment variable`);
74
+ console.log('');
75
+ } else {
76
+ spinner.start('Running AI analysis...');
77
+
78
+ try {
79
+ enhancedResult = await aiEnhancer.enhance(scanResult);
80
+
81
+ if (enhancedResult.aiEnhanced && enhancedResult.aiAnalysis) {
82
+ spinner.stop('AI analysis complete');
83
+ console.log('');
84
+ console.log(formatAIAnalysis(enhancedResult.aiAnalysis));
85
+
86
+ // Use enhanced result for generation
87
+ scanResult = enhancedResult;
88
+ } else if (enhancedResult.aiError) {
89
+ spinner.stop('AI analysis failed');
90
+ logger.warn(`AI enhancement error: ${enhancedResult.aiError}`);
91
+ console.log('');
92
+ }
93
+ } catch (error) {
94
+ spinner.stop('AI analysis failed');
95
+ logger.warn(`AI enhancement error: ${error instanceof Error ? error.message : String(error)}`);
96
+ console.log('');
97
+ }
98
+ }
99
+ }
100
+
101
+ // Step 3: Confirm with user (unless --yes)
102
+ if (!options.yes) {
103
+ const shouldContinue = await prompts.confirm({
104
+ message: 'Generate Ralph configuration files?',
105
+ initialValue: true,
106
+ });
107
+
108
+ if (prompts.isCancel(shouldContinue) || !shouldContinue) {
109
+ logger.info('Initialization cancelled');
110
+ return;
111
+ }
112
+ }
113
+
114
+ // Step 4: Generate configuration files
115
+ console.log('');
116
+ spinner.start('Generating configuration files...');
117
+
118
+ const generator = new Generator({
119
+ existingFiles: 'backup',
120
+ generateConfig: true,
121
+ verbose: false,
122
+ });
123
+
124
+ try {
125
+ const generationResult = await generator.generate(scanResult);
126
+ spinner.stop('Configuration files generated');
127
+
128
+ console.log('');
129
+ console.log(pc.cyan('--- Generation Results ---'));
130
+ console.log(formatGenerationResult(generationResult));
131
+
132
+ if (generationResult.success) {
133
+ console.log('');
134
+ logger.success('Ralph initialized successfully!');
135
+ console.log('');
136
+ console.log('Next steps:');
137
+ console.log(' 1. Review the generated files in .ralph/');
138
+ console.log(' 2. Customize the prompts in .ralph/prompts/');
139
+ console.log(' 3. Run "ralph new <feature>" to create a feature spec');
140
+ console.log(' 4. Run "ralph run <feature>" to start development');
141
+ } else {
142
+ logger.warn('Initialization completed with some errors');
143
+ }
144
+ } catch (error) {
145
+ spinner.stop('Generation failed');
146
+ logger.error(`Failed to generate files: ${error instanceof Error ? error.message : String(error)}`);
147
+ process.exit(1);
148
+ }
149
+ }
@@ -0,0 +1,412 @@
1
+ /**
2
+ * Monitor Command
3
+ * Display real-time status of a feature loop
4
+ */
5
+
6
+ import { spawn, execFileSync } from 'node:child_process';
7
+ import { existsSync, readFileSync } from 'node:fs';
8
+ import { join, dirname } from 'node:path';
9
+ import { logger } from '../utils/logger.js';
10
+ import { loadConfigWithDefaults, hasConfig } from '../utils/config.js';
11
+ import pc from 'picocolors';
12
+
13
+ export interface MonitorOptions {
14
+ /** Use bash script monitor instead of built-in */
15
+ bash?: boolean;
16
+ /** Use Python TUI monitor */
17
+ python?: boolean;
18
+ /** Refresh interval in seconds */
19
+ interval?: number;
20
+ }
21
+
22
+ interface LoopStatus {
23
+ running: boolean;
24
+ phase: string;
25
+ iteration: number;
26
+ maxIterations: number;
27
+ tokensInput: number;
28
+ tokensOutput: number;
29
+ tasksDone: number;
30
+ tasksPending: number;
31
+ e2eDone: number;
32
+ e2ePending: number;
33
+ branch: string;
34
+ elapsed: string;
35
+ }
36
+
37
+ /**
38
+ * Find the ralph-monitor.sh script
39
+ */
40
+ function findMonitorScript(projectRoot: string): string | null {
41
+ // Check .ralph/scripts first
42
+ const localScript = join(projectRoot, '.ralph', 'scripts', 'ralph-monitor.sh');
43
+ if (existsSync(localScript)) {
44
+ return localScript;
45
+ }
46
+
47
+ // Check for ralph directory as sibling (development setup)
48
+ const siblingRalph = join(projectRoot, '..', 'ralph', 'ralph-monitor.sh');
49
+ if (existsSync(siblingRalph)) {
50
+ return siblingRalph;
51
+ }
52
+
53
+ // Check in current directory (ralph repo)
54
+ const currentRalph = join(projectRoot, 'ralph-monitor.sh');
55
+ if (existsSync(currentRalph)) {
56
+ return currentRalph;
57
+ }
58
+
59
+ return null;
60
+ }
61
+
62
+ /**
63
+ * Check if a process matching pattern is running
64
+ * Uses pgrep with -f flag for full command line matching
65
+ */
66
+ function isProcessRunning(pattern: string): boolean {
67
+ try {
68
+ // Use execFileSync for safer execution
69
+ const result = execFileSync('pgrep', ['-f', pattern], { encoding: 'utf-8' });
70
+ return result.trim().length > 0;
71
+ } catch {
72
+ return false;
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Detect current phase of the loop
78
+ */
79
+ function detectPhase(feature: string): string {
80
+ if (isProcessRunning('PROMPT_feature.md')) return 'Planning';
81
+ if (isProcessRunning('PROMPT_e2e.md')) return 'E2E Testing';
82
+ if (isProcessRunning('PROMPT_verify.md')) return 'Verification';
83
+ if (isProcessRunning('PROMPT_review.md')) return 'PR Review';
84
+ if (isProcessRunning('PROMPT.md')) return 'Implementation';
85
+ if (isProcessRunning(`feature-loop.sh.*${feature}`)) return 'Running';
86
+ return 'Idle';
87
+ }
88
+
89
+ /**
90
+ * Read status from temp files
91
+ */
92
+ function readStatus(feature: string): LoopStatus {
93
+ const statusFile = `/tmp/ralph-loop-${feature}.status`;
94
+ const tokensFile = `/tmp/ralph-loop-${feature}.tokens`;
95
+
96
+ let iteration = 0;
97
+ let maxIterations = 50;
98
+
99
+ // Read status file
100
+ if (existsSync(statusFile)) {
101
+ try {
102
+ const content = readFileSync(statusFile, 'utf-8').trim();
103
+ const parts = content.split('|');
104
+ iteration = parseInt(parts[0]) || 0;
105
+ maxIterations = parseInt(parts[1]) || 50;
106
+ } catch {
107
+ // Ignore errors
108
+ }
109
+ }
110
+
111
+ // Read tokens file
112
+ let tokensInput = 0;
113
+ let tokensOutput = 0;
114
+ if (existsSync(tokensFile)) {
115
+ try {
116
+ const content = readFileSync(tokensFile, 'utf-8').trim();
117
+ const parts = content.split('|');
118
+ tokensInput = parseInt(parts[0]) || 0;
119
+ tokensOutput = parseInt(parts[1]) || 0;
120
+ } catch {
121
+ // Ignore errors
122
+ }
123
+ }
124
+
125
+ return {
126
+ running: isProcessRunning(`feature-loop.sh.*${feature}`),
127
+ phase: detectPhase(feature),
128
+ iteration,
129
+ maxIterations,
130
+ tokensInput,
131
+ tokensOutput,
132
+ tasksDone: 0,
133
+ tasksPending: 0,
134
+ e2eDone: 0,
135
+ e2ePending: 0,
136
+ branch: '',
137
+ elapsed: '',
138
+ };
139
+ }
140
+
141
+ /**
142
+ * Parse implementation plan for task counts
143
+ */
144
+ async function parseImplementationPlan(
145
+ projectRoot: string,
146
+ feature: string
147
+ ): Promise<{ tasksDone: number; tasksPending: number; e2eDone: number; e2ePending: number }> {
148
+ const config = await loadConfigWithDefaults(projectRoot);
149
+ const planPath = join(projectRoot, config.paths.specs, `${feature}-implementation-plan.md`);
150
+
151
+ let tasksDone = 0;
152
+ let tasksPending = 0;
153
+ let e2eDone = 0;
154
+ let e2ePending = 0;
155
+
156
+ if (existsSync(planPath)) {
157
+ try {
158
+ const content = readFileSync(planPath, 'utf-8');
159
+ const lines = content.split('\n');
160
+
161
+ for (const line of lines) {
162
+ if (line.match(/^- \[x\]/)) {
163
+ if (line.includes('E2E:')) {
164
+ e2eDone++;
165
+ } else {
166
+ tasksDone++;
167
+ }
168
+ } else if (line.match(/^- \[ \]/)) {
169
+ if (line.includes('E2E:')) {
170
+ e2ePending++;
171
+ } else {
172
+ tasksPending++;
173
+ }
174
+ }
175
+ }
176
+ } catch {
177
+ // Ignore errors
178
+ }
179
+ }
180
+
181
+ return { tasksDone, tasksPending, e2eDone, e2ePending };
182
+ }
183
+
184
+ /**
185
+ * Get current git branch
186
+ */
187
+ function getGitBranch(projectRoot: string): string {
188
+ try {
189
+ // Try app directory first
190
+ const appDir = join(projectRoot, '..', 'app');
191
+ if (existsSync(appDir)) {
192
+ return execFileSync('git', ['branch', '--show-current'], {
193
+ cwd: appDir,
194
+ encoding: 'utf-8',
195
+ }).trim();
196
+ }
197
+ return execFileSync('git', ['branch', '--show-current'], {
198
+ cwd: projectRoot,
199
+ encoding: 'utf-8',
200
+ }).trim();
201
+ } catch {
202
+ return '-';
203
+ }
204
+ }
205
+
206
+ /**
207
+ * Format number with K/M suffix
208
+ */
209
+ function formatNumber(num: number): string {
210
+ if (num >= 1000000) {
211
+ return (num / 1000000).toFixed(1) + 'M';
212
+ }
213
+ if (num >= 1000) {
214
+ return (num / 1000).toFixed(1) + 'K';
215
+ }
216
+ return String(num);
217
+ }
218
+
219
+ /**
220
+ * Create a progress bar
221
+ */
222
+ function progressBar(percent: number, width: number = 15): string {
223
+ const filled = Math.round((percent / 100) * width);
224
+ const empty = width - filled;
225
+ return pc.green('\u2588'.repeat(filled)) + pc.dim('\u2591'.repeat(empty));
226
+ }
227
+
228
+ /**
229
+ * Display built-in monitor dashboard
230
+ */
231
+ async function displayDashboard(feature: string, projectRoot: string, interval: number = 5): Promise<void> {
232
+ const status = readStatus(feature);
233
+ const tasks = await parseImplementationPlan(projectRoot, feature);
234
+ const branch = getGitBranch(projectRoot);
235
+
236
+ // Calculate progress
237
+ const totalTasks = tasks.tasksDone + tasks.tasksPending;
238
+ const totalE2e = tasks.e2eDone + tasks.e2ePending;
239
+ const totalAll = totalTasks + totalE2e;
240
+ const doneAll = tasks.tasksDone + tasks.e2eDone;
241
+
242
+ const percentTasks = totalTasks > 0 ? Math.round((tasks.tasksDone / totalTasks) * 100) : 0;
243
+ const percentE2e = totalE2e > 0 ? Math.round((tasks.e2eDone / totalE2e) * 100) : 0;
244
+ const percentAll = totalAll > 0 ? Math.round((doneAll / totalAll) * 100) : 0;
245
+
246
+ // Clear screen
247
+ console.clear();
248
+
249
+ // Header
250
+ const timestamp = new Date().toLocaleTimeString();
251
+ console.log(pc.bold('='.repeat(78)));
252
+ console.log(
253
+ pc.bold(' ') +
254
+ pc.cyan('RALPH MONITOR') +
255
+ `: ${pc.bold(feature)}` +
256
+ ` ${pc.dim(timestamp)}`
257
+ );
258
+ console.log(pc.bold('='.repeat(78)));
259
+ console.log('');
260
+
261
+ // Status line
262
+ const phaseColors: Record<string, (s: string) => string> = {
263
+ Planning: pc.blue,
264
+ Implementation: pc.yellow,
265
+ 'E2E Testing': pc.cyan,
266
+ Verification: pc.magenta,
267
+ 'PR Review': pc.green,
268
+ Idle: pc.dim,
269
+ Running: pc.white,
270
+ };
271
+ const phaseColor = phaseColors[status.phase] || pc.white;
272
+
273
+ console.log(
274
+ ` Phase: ${phaseColor(pc.bold(status.phase))}` +
275
+ ` | Iter: ${pc.bold(String(status.iteration))}/${pc.dim(String(status.maxIterations))}` +
276
+ ` | Branch: ${pc.cyan(branch)}`
277
+ );
278
+
279
+ const totalTokens = status.tokensInput + status.tokensOutput;
280
+ console.log(
281
+ ` Tokens: ${pc.magenta(formatNumber(totalTokens))}` +
282
+ pc.dim(` (in:${formatNumber(status.tokensInput)} out:${formatNumber(status.tokensOutput)})`)
283
+ );
284
+
285
+ console.log(pc.dim(' ' + '-'.repeat(74)));
286
+
287
+ // Progress
288
+ console.log('');
289
+ console.log(
290
+ ` ${pc.bold('Implementation:')} ${progressBar(percentTasks)} ${pc.bold(percentTasks + '%')}` +
291
+ ` ${pc.green('\u2713 ' + tasks.tasksDone)} / ${pc.yellow('\u25cb ' + tasks.tasksPending)}`
292
+ );
293
+
294
+ if (totalE2e > 0) {
295
+ console.log(
296
+ ` ${pc.bold('E2E Tests: ')} ${progressBar(percentE2e)} ${pc.bold(percentE2e + '%')}` +
297
+ ` ${pc.green('\u2713 ' + tasks.e2eDone)} / ${pc.yellow('\u25cb ' + tasks.e2ePending)}`
298
+ );
299
+ }
300
+
301
+ console.log(pc.dim(' ' + '-'.repeat(40)));
302
+ console.log(
303
+ ` ${pc.bold('Overall: ')} ${progressBar(percentAll)} ${pc.bold(percentAll + '%')}` +
304
+ ` ${pc.green('\u2713 ' + doneAll)} / ${pc.yellow('\u25cb ' + (totalAll - doneAll))}`
305
+ );
306
+
307
+ // Status indicator
308
+ console.log('');
309
+ if (status.running) {
310
+ console.log(pc.green(' \u25cf Loop is running'));
311
+ } else {
312
+ console.log(pc.yellow(' \u25cb Loop is not running'));
313
+ }
314
+
315
+ console.log('');
316
+ console.log(pc.dim(` Refreshing every ${interval}s | Press Ctrl+C to exit`));
317
+ }
318
+
319
+ /**
320
+ * Launch the monitoring dashboard for a feature
321
+ */
322
+ export async function monitorCommand(feature: string, options: MonitorOptions = {}): Promise<void> {
323
+ const projectRoot = process.cwd();
324
+
325
+ // Validate feature name
326
+ if (!feature || typeof feature !== 'string') {
327
+ logger.error('Feature name is required');
328
+ process.exit(1);
329
+ }
330
+
331
+ // Sanitize feature name (allow alphanumeric, hyphens, underscores)
332
+ if (!/^[a-zA-Z0-9_-]+$/.test(feature)) {
333
+ logger.error('Feature name must contain only letters, numbers, hyphens, and underscores');
334
+ process.exit(1);
335
+ }
336
+
337
+ // Validate interval
338
+ if (options.interval !== undefined && (options.interval < 1 || options.interval > 60)) {
339
+ logger.warn('Interval should be between 1 and 60 seconds. Using default (5).');
340
+ options.interval = 5;
341
+ }
342
+
343
+ logger.info(`Monitoring feature: ${pc.bold(feature)}`);
344
+ console.log('');
345
+
346
+ // Check for bash monitor option
347
+ if (options.bash) {
348
+ const monitorScript = findMonitorScript(projectRoot);
349
+ if (!monitorScript) {
350
+ logger.error('ralph-monitor.sh script not found');
351
+ logger.info('The script should be in .ralph/scripts/ or the ralph/ directory');
352
+ process.exit(1);
353
+ }
354
+
355
+ logger.info(`Using bash monitor: ${monitorScript}`);
356
+ console.log('');
357
+
358
+ const child = spawn('bash', [monitorScript, feature], {
359
+ cwd: dirname(monitorScript),
360
+ stdio: 'inherit',
361
+ });
362
+
363
+ return new Promise((resolve, reject) => {
364
+ child.on('error', reject);
365
+ child.on('close', () => resolve());
366
+ });
367
+ }
368
+
369
+ // Python TUI option
370
+ if (options.python) {
371
+ logger.warn('Python TUI monitor not yet implemented');
372
+ logger.info('Using built-in monitor instead');
373
+ }
374
+
375
+ // Built-in monitor
376
+ const intervalSeconds = options.interval || 5;
377
+ const intervalMs = intervalSeconds * 1000;
378
+
379
+ // Initial display
380
+ try {
381
+ await displayDashboard(feature, projectRoot, intervalSeconds);
382
+ } catch (error) {
383
+ logger.error(`Failed to display dashboard: ${error instanceof Error ? error.message : String(error)}`);
384
+ if (process.env.DEBUG && error instanceof Error) {
385
+ console.error(error.stack);
386
+ }
387
+ process.exit(1);
388
+ }
389
+
390
+ // Refresh loop
391
+ const refreshTimer = setInterval(async () => {
392
+ try {
393
+ await displayDashboard(feature, projectRoot, intervalSeconds);
394
+ } catch (error) {
395
+ // Log error but continue monitoring
396
+ logger.debug(`Dashboard refresh error: ${error instanceof Error ? error.message : String(error)}`);
397
+ }
398
+ }, intervalMs);
399
+
400
+ // Return a Promise that resolves on SIGINT
401
+ return new Promise<void>((resolve) => {
402
+ const cleanup = () => {
403
+ clearInterval(refreshTimer);
404
+ console.log('');
405
+ logger.info('Monitor stopped');
406
+ resolve();
407
+ };
408
+
409
+ process.on('SIGINT', cleanup);
410
+ process.on('SIGTERM', cleanup);
411
+ });
412
+ }