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,312 @@
1
+ /**
2
+ * New Command
3
+ * Create a new feature specification from template
4
+ */
5
+
6
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
7
+ import { join, dirname } from 'node:path';
8
+ import { fileURLToPath } from 'node:url';
9
+ import { spawn } from 'node:child_process';
10
+ import { logger } from '../utils/logger.js';
11
+ import { loadConfigWithDefaults, hasConfig } from '../utils/config.js';
12
+ import pc from 'picocolors';
13
+ import * as prompts from '@clack/prompts';
14
+
15
+ export interface NewOptions {
16
+ /** Open in editor after creation */
17
+ edit?: boolean;
18
+ /** Editor to use (defaults to $EDITOR or 'code') */
19
+ editor?: string;
20
+ /** Skip confirmation */
21
+ yes?: boolean;
22
+ /** Force overwrite if file exists */
23
+ force?: boolean;
24
+ }
25
+
26
+ /**
27
+ * Default spec template content
28
+ */
29
+ const DEFAULT_SPEC_TEMPLATE = `# {{feature}} Feature Specification
30
+
31
+ **Status:** Planned
32
+ **Version:** 1.0
33
+ **Last Updated:** {{date}}
34
+
35
+ ## Purpose
36
+
37
+ Describe what this feature does and why it's needed.
38
+
39
+ ## User Stories
40
+
41
+ - As a user, I want [action] so that [benefit]
42
+ - As an admin, I want [action] so that [benefit]
43
+
44
+ ## Requirements
45
+
46
+ ### Functional Requirements
47
+ - [ ] Requirement 1 - Description of what the system must do
48
+ - [ ] Requirement 2 - Another functional requirement
49
+
50
+ ### Non-Functional Requirements
51
+ - [ ] Performance: [target metrics]
52
+ - [ ] Security: [security considerations]
53
+ - [ ] Accessibility: [WCAG level]
54
+
55
+ ## Technical Notes
56
+
57
+ - **Uses:** Existing patterns or components to leverage
58
+ - **Location:** Where the code should live
59
+ - **Dependencies:** External libraries or APIs needed
60
+ - **Database:** Schema changes required (if any)
61
+
62
+ ## Visual Requirements
63
+
64
+ (For UI features - delete this section if backend-only)
65
+
66
+ - **Layout:** Describe the layout structure and responsive behavior
67
+ - **Components:** List the UI components needed
68
+ - **States:**
69
+ - Empty: What to show when there's no data
70
+ - Loading: Skeleton or spinner pattern
71
+ - Error: How to display errors
72
+ - **Mobile:** How the layout adapts on small screens
73
+
74
+ ## API Endpoints
75
+
76
+ (If applicable)
77
+
78
+ | Method | Endpoint | Description |
79
+ |--------|----------|-------------|
80
+ | GET | \`/api/{{feature}}\` | Fetch data |
81
+ | POST | \`/api/{{feature}}\` | Create new |
82
+
83
+ ## Acceptance Criteria
84
+
85
+ - [ ] Criteria 1 - Specific, testable condition
86
+ - [ ] Criteria 2 - Another acceptance criterion
87
+ - [ ] Criteria 3 - E2E testable scenario
88
+
89
+ ## Out of Scope
90
+
91
+ - Feature X (planned for future iteration)
92
+ - Integration Y (separate spec)
93
+
94
+ ## Open Questions
95
+
96
+ - [ ] Question 1 - Decision needed
97
+ - [ ] Question 2 - Clarification required
98
+ `;
99
+
100
+ /**
101
+ * Find the _example.md template
102
+ */
103
+ async function findExampleTemplate(projectRoot: string): Promise<string | null> {
104
+ const config = await loadConfigWithDefaults(projectRoot);
105
+ const specsDir = config.paths.specs;
106
+
107
+ // Check multiple locations
108
+ const possiblePaths = [
109
+ join(projectRoot, specsDir, '_example.md'),
110
+ join(projectRoot, '.ralph', 'specs', '_example.md'),
111
+ join(projectRoot, 'specs', '_example.md'),
112
+ ];
113
+
114
+ for (const templatePath of possiblePaths) {
115
+ if (existsSync(templatePath)) {
116
+ return templatePath;
117
+ }
118
+ }
119
+
120
+ return null;
121
+ }
122
+
123
+ /**
124
+ * Get template directory from the package
125
+ */
126
+ function getPackageTemplateDir(): string {
127
+ const __filename = fileURLToPath(import.meta.url);
128
+ const __dirname = dirname(__filename);
129
+ // Go up from commands/ to src/ or dist/, then to templates/
130
+ return join(__dirname, '..', 'templates', 'specs');
131
+ }
132
+
133
+ /**
134
+ * Process template variables
135
+ */
136
+ function processTemplate(template: string, feature: string): string {
137
+ const date = new Date().toISOString().split('T')[0];
138
+ const featureTitle = feature
139
+ .split('-')
140
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
141
+ .join(' ');
142
+
143
+ return template
144
+ .replace(/\{\{feature\}\}/g, feature)
145
+ .replace(/\{\{featureTitle\}\}/g, featureTitle)
146
+ .replace(/\{\{date\}\}/g, date)
147
+ .replace(/YYYY-MM-DD/g, date);
148
+ }
149
+
150
+ /**
151
+ * Open file in editor
152
+ */
153
+ function openInEditor(filePath: string, editor?: string): void {
154
+ const editorCmd = editor || process.env.EDITOR || 'code';
155
+
156
+ logger.info(`Opening in editor: ${editorCmd}`);
157
+
158
+ const child = spawn(editorCmd, [filePath], {
159
+ detached: true,
160
+ stdio: 'ignore',
161
+ });
162
+
163
+ child.unref();
164
+ }
165
+
166
+ /**
167
+ * Create a new feature specification
168
+ */
169
+ export async function newCommand(feature: string, options: NewOptions = {}): Promise<void> {
170
+ const projectRoot = process.cwd();
171
+
172
+ // Validate feature name
173
+ if (!feature || typeof feature !== 'string') {
174
+ logger.error('Feature name is required');
175
+ process.exit(1);
176
+ }
177
+
178
+ // Sanitize feature name (allow alphanumeric, hyphens, underscores)
179
+ if (!/^[a-zA-Z0-9_-]+$/.test(feature)) {
180
+ logger.error('Feature name must contain only letters, numbers, hyphens, and underscores');
181
+ logger.info('Example: ralph new my-feature or ralph new user_auth');
182
+ process.exit(1);
183
+ }
184
+
185
+ // Check for reserved names
186
+ const reservedNames = ['_example', '_template', 'config', 'ralph'];
187
+ if (reservedNames.includes(feature.toLowerCase())) {
188
+ logger.error(`"${feature}" is a reserved name. Please choose a different feature name.`);
189
+ process.exit(1);
190
+ }
191
+
192
+ logger.info(`Creating new feature spec: ${pc.bold(feature)}`);
193
+ console.log('');
194
+
195
+ // Check for config
196
+ if (!hasConfig(projectRoot)) {
197
+ logger.warn('No ralph.config.js found. Run "ralph init" first to configure your project.');
198
+ logger.info('Using default paths...');
199
+ console.log('');
200
+ }
201
+
202
+ // Load config
203
+ const config = await loadConfigWithDefaults(projectRoot);
204
+ const specsDir = join(projectRoot, config.paths.specs);
205
+
206
+ // Create specs directory if it doesn't exist
207
+ if (!existsSync(specsDir)) {
208
+ logger.info(`Creating specs directory: ${specsDir}`);
209
+ mkdirSync(specsDir, { recursive: true });
210
+ }
211
+
212
+ // Check if spec already exists
213
+ const specPath = join(specsDir, `${feature}.md`);
214
+ if (existsSync(specPath)) {
215
+ if (!options.force) {
216
+ logger.error(`Spec file already exists: ${specPath}`);
217
+
218
+ if (!options.yes) {
219
+ const shouldOverwrite = await prompts.confirm({
220
+ message: 'Do you want to overwrite the existing spec?',
221
+ initialValue: false,
222
+ });
223
+
224
+ if (prompts.isCancel(shouldOverwrite) || !shouldOverwrite) {
225
+ logger.info('Cancelled');
226
+ return;
227
+ }
228
+ } else {
229
+ logger.info('Use --force to overwrite');
230
+ return;
231
+ }
232
+ }
233
+
234
+ logger.warn('Overwriting existing spec file');
235
+ }
236
+
237
+ // Find or use default template
238
+ let templateContent: string;
239
+
240
+ // Try to find _example.md template
241
+ const exampleTemplate = await findExampleTemplate(projectRoot);
242
+ if (exampleTemplate) {
243
+ logger.info(`Using template: ${exampleTemplate}`);
244
+ templateContent = readFileSync(exampleTemplate, 'utf-8');
245
+ } else {
246
+ // Try package template
247
+ const packageTemplateDir = getPackageTemplateDir();
248
+ const packageTemplate = join(packageTemplateDir, '_example.md.tmpl');
249
+ if (existsSync(packageTemplate)) {
250
+ logger.info(`Using package template`);
251
+ templateContent = readFileSync(packageTemplate, 'utf-8');
252
+ } else {
253
+ // Use default template
254
+ logger.info('Using default template');
255
+ templateContent = DEFAULT_SPEC_TEMPLATE;
256
+ }
257
+ }
258
+
259
+ // Process template
260
+ const specContent = processTemplate(templateContent, feature);
261
+
262
+ // Confirm with user (unless --yes)
263
+ if (!options.yes) {
264
+ console.log('');
265
+ console.log(pc.cyan('--- Spec Preview ---'));
266
+ console.log(`File: ${specPath}`);
267
+ console.log('');
268
+
269
+ // Show first few lines of the processed template
270
+ const previewLines = specContent.split('\n').slice(0, 15);
271
+ console.log(pc.dim(previewLines.join('\n')));
272
+ if (specContent.split('\n').length > 15) {
273
+ console.log(pc.dim('...'));
274
+ }
275
+ console.log('');
276
+
277
+ const shouldCreate = await prompts.confirm({
278
+ message: 'Create this spec file?',
279
+ initialValue: true,
280
+ });
281
+
282
+ if (prompts.isCancel(shouldCreate) || !shouldCreate) {
283
+ logger.info('Cancelled');
284
+ return;
285
+ }
286
+ }
287
+
288
+ // Write the spec file
289
+ try {
290
+ writeFileSync(specPath, specContent, 'utf-8');
291
+ logger.success(`Created spec: ${specPath}`);
292
+ } catch (error) {
293
+ logger.error(`Failed to create spec: ${error instanceof Error ? error.message : String(error)}`);
294
+ process.exit(1);
295
+ }
296
+
297
+ // Open in editor if requested
298
+ if (options.edit) {
299
+ try {
300
+ openInEditor(specPath, options.editor);
301
+ } catch (error) {
302
+ logger.warn(`Could not open editor: ${error instanceof Error ? error.message : String(error)}`);
303
+ logger.info(`Manually open: ${specPath}`);
304
+ }
305
+ }
306
+
307
+ // Display next steps
308
+ console.log('');
309
+ console.log('Next steps:');
310
+ console.log(` 1. Edit the spec: ${pc.cyan(`$EDITOR ${specPath}`)}`);
311
+ console.log(` 2. When ready, run: ${pc.cyan(`ralph run ${feature}`)}`);
312
+ }
@@ -0,0 +1,214 @@
1
+ /**
2
+ * Run Command
3
+ * Executes the feature development loop for a specific feature
4
+ */
5
+
6
+ import { spawn } from 'node:child_process';
7
+ import { existsSync } from 'node:fs';
8
+ import { join, dirname } from 'node:path';
9
+ import { logger } from '../utils/logger.js';
10
+ import {
11
+ loadConfigWithDefaults,
12
+ hasConfig,
13
+ type RalphConfig,
14
+ } from '../utils/config.js';
15
+ import pc from 'picocolors';
16
+
17
+ export interface RunOptions {
18
+ worktree?: boolean;
19
+ resume?: boolean;
20
+ model?: string;
21
+ maxIterations?: number;
22
+ maxE2eAttempts?: number;
23
+ }
24
+
25
+ /**
26
+ * Find the feature-loop.sh script
27
+ * Checks: 1) .ralph/scripts/ 2) ralph/ (parent ralph repo)
28
+ */
29
+ function findFeatureLoopScript(projectRoot: string): string | null {
30
+ // Check .ralph/scripts first
31
+ const localScript = join(projectRoot, '.ralph', 'scripts', 'feature-loop.sh');
32
+ if (existsSync(localScript)) {
33
+ return localScript;
34
+ }
35
+
36
+ // Check for ralph directory as sibling (development setup)
37
+ const siblingRalph = join(projectRoot, '..', 'ralph', 'feature-loop.sh');
38
+ if (existsSync(siblingRalph)) {
39
+ return siblingRalph;
40
+ }
41
+
42
+ // Check for ralph directory as parent (when running from within ralph-cli)
43
+ const parentRalph = join(projectRoot, 'feature-loop.sh');
44
+ if (existsSync(parentRalph)) {
45
+ return parentRalph;
46
+ }
47
+
48
+ return null;
49
+ }
50
+
51
+ /**
52
+ * Validate that the spec file exists
53
+ */
54
+ async function validateSpecFile(projectRoot: string, feature: string): Promise<string | null> {
55
+ const config = await loadConfigWithDefaults(projectRoot);
56
+ const specsDir = config.paths.specs;
57
+
58
+ // Check various possible spec locations
59
+ const possiblePaths = [
60
+ join(projectRoot, specsDir, `${feature}.md`),
61
+ join(projectRoot, '.ralph', 'specs', `${feature}.md`),
62
+ join(projectRoot, 'specs', `${feature}.md`),
63
+ ];
64
+
65
+ for (const specPath of possiblePaths) {
66
+ if (existsSync(specPath)) {
67
+ return specPath;
68
+ }
69
+ }
70
+
71
+ return null;
72
+ }
73
+
74
+ /**
75
+ * Run the feature development loop for a specific feature
76
+ */
77
+ export async function runCommand(feature: string, options: RunOptions = {}): Promise<void> {
78
+ const projectRoot = process.cwd();
79
+
80
+ // Validate feature name
81
+ if (!feature || typeof feature !== 'string') {
82
+ logger.error('Feature name is required');
83
+ process.exit(1);
84
+ }
85
+
86
+ // Sanitize feature name (allow alphanumeric, hyphens, underscores)
87
+ if (!/^[a-zA-Z0-9_-]+$/.test(feature)) {
88
+ logger.error('Feature name must contain only letters, numbers, hyphens, and underscores');
89
+ process.exit(1);
90
+ }
91
+
92
+ logger.info(`Running feature loop for: ${pc.bold(feature)}`);
93
+ console.log('');
94
+
95
+ // Check for config
96
+ if (!hasConfig(projectRoot)) {
97
+ logger.warn('No ralph.config.js found. Run "ralph init" first to configure your project.');
98
+ logger.info('Attempting to run with default settings...');
99
+ console.log('');
100
+ }
101
+
102
+ // Load config
103
+ const config = await loadConfigWithDefaults(projectRoot);
104
+
105
+ // Validate spec file exists
106
+ const specFile = await validateSpecFile(projectRoot, feature);
107
+ if (!specFile) {
108
+ logger.error(`Spec file not found: ${feature}.md`);
109
+ logger.info(`Create the spec first: ralph new ${feature}`);
110
+ logger.info(`Expected location: ${join(projectRoot, config.paths.specs, `${feature}.md`)}`);
111
+ process.exit(1);
112
+ }
113
+
114
+ logger.info(`Found spec: ${specFile}`);
115
+
116
+ // Find the feature-loop.sh script
117
+ const scriptPath = findFeatureLoopScript(projectRoot);
118
+ if (!scriptPath) {
119
+ logger.error('feature-loop.sh script not found');
120
+ logger.info('The script should be in .ralph/scripts/ or the ralph/ directory');
121
+ logger.info('Run "ralph init" to generate the necessary scripts');
122
+ process.exit(1);
123
+ }
124
+
125
+ logger.info(`Using script: ${scriptPath}`);
126
+ console.log('');
127
+
128
+ // Build command arguments
129
+ const args: string[] = [feature];
130
+
131
+ // Add max iterations
132
+ const maxIterations = options.maxIterations ?? config.loop.maxIterations;
133
+ args.push(String(maxIterations));
134
+
135
+ // Add max E2E attempts
136
+ const maxE2eAttempts = options.maxE2eAttempts ?? config.loop.maxE2eAttempts;
137
+ args.push(String(maxE2eAttempts));
138
+
139
+ // Add flags
140
+ if (options.worktree) {
141
+ args.push('--worktree');
142
+ }
143
+
144
+ if (options.resume) {
145
+ args.push('--resume');
146
+ }
147
+
148
+ if (options.model) {
149
+ args.push('--model', options.model);
150
+ }
151
+
152
+ // Display configuration
153
+ console.log(pc.cyan('--- Run Configuration ---'));
154
+ console.log(` Feature: ${pc.bold(feature)}`);
155
+ console.log(` Spec: ${specFile}`);
156
+ console.log(` Max Iterations: ${maxIterations}`);
157
+ console.log(` Max E2E Attempts: ${maxE2eAttempts}`);
158
+ console.log(` Model: ${options.model || config.loop.defaultModel}`);
159
+ console.log(` Worktree: ${options.worktree ? 'enabled' : 'disabled'}`);
160
+ console.log(` Resume: ${options.resume ? 'enabled' : 'disabled'}`);
161
+ console.log('');
162
+
163
+ // Execute the feature-loop.sh script
164
+ logger.info('Starting feature loop...');
165
+ console.log('');
166
+
167
+ const scriptDir = dirname(scriptPath);
168
+
169
+ return new Promise((resolve, reject) => {
170
+ try {
171
+ const child = spawn('bash', [scriptPath, ...args], {
172
+ cwd: scriptDir,
173
+ stdio: 'inherit',
174
+ env: {
175
+ ...process.env,
176
+ // Pass config paths to script
177
+ RALPH_CONFIG_ROOT: config.paths.root,
178
+ RALPH_SPEC_DIR: config.paths.specs,
179
+ RALPH_SCRIPTS_DIR: config.paths.scripts,
180
+ },
181
+ });
182
+
183
+ child.on('error', (error) => {
184
+ logger.error(`Failed to start feature loop: ${error.message}`);
185
+ if (process.env.DEBUG) {
186
+ console.error(error.stack);
187
+ }
188
+ reject(error);
189
+ });
190
+
191
+ child.on('close', (code) => {
192
+ console.log('');
193
+ if (code === 0) {
194
+ logger.success('Feature loop completed successfully!');
195
+ resolve();
196
+ } else if (code === 130) {
197
+ // SIGINT (Ctrl+C)
198
+ logger.info('Feature loop interrupted by user');
199
+ resolve();
200
+ } else {
201
+ logger.error(`Feature loop exited with code: ${code}`);
202
+ logger.info('Use --resume to continue from where you left off');
203
+ reject(new Error(`Feature loop exited with code: ${code || 1}`));
204
+ }
205
+ });
206
+ } catch (error) {
207
+ logger.error(`Unexpected error starting feature loop: ${error instanceof Error ? error.message : String(error)}`);
208
+ if (process.env.DEBUG && error instanceof Error) {
209
+ console.error(error.stack);
210
+ }
211
+ reject(error);
212
+ }
213
+ });
214
+ }
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Config Generator
3
+ * Generates ralph.config.js file from scan results
4
+ */
5
+
6
+ import type { ScanResult } from '../scanner/types.js';
7
+ import { extractVariables, type TemplateVariables } from './templates.js';
8
+
9
+ /**
10
+ * Ralph configuration structure
11
+ */
12
+ export interface RalphConfig {
13
+ name: string;
14
+ stack: {
15
+ framework: {
16
+ name: string;
17
+ version: string;
18
+ variant: string;
19
+ };
20
+ packageManager: string;
21
+ testing: {
22
+ unit: string;
23
+ e2e: string;
24
+ };
25
+ styling: string;
26
+ };
27
+ commands: {
28
+ dev: string;
29
+ build: string;
30
+ test: string;
31
+ lint: string;
32
+ typecheck: string;
33
+ };
34
+ paths: {
35
+ root: string;
36
+ prompts: string;
37
+ guides: string;
38
+ specs: string;
39
+ scripts: string;
40
+ learnings: string;
41
+ agents: string;
42
+ };
43
+ loop: {
44
+ maxIterations: number;
45
+ maxE2eAttempts: number;
46
+ defaultModel: string;
47
+ planningModel: string;
48
+ };
49
+ }
50
+
51
+ /**
52
+ * Generate ralph config object from scan result
53
+ */
54
+ export function generateConfig(scanResult: ScanResult, customVars: Record<string, string> = {}): RalphConfig {
55
+ const vars = extractVariables(scanResult, customVars);
56
+
57
+ return {
58
+ name: vars.projectName,
59
+ stack: {
60
+ framework: {
61
+ name: vars.framework,
62
+ version: vars.frameworkVersion,
63
+ variant: vars.frameworkVariant,
64
+ },
65
+ packageManager: vars.packageManager,
66
+ testing: {
67
+ unit: vars.unitTest,
68
+ e2e: vars.e2eTest,
69
+ },
70
+ styling: vars.styling,
71
+ },
72
+ commands: {
73
+ dev: vars.devCommand,
74
+ build: vars.buildCommand,
75
+ test: vars.testCommand,
76
+ lint: vars.lintCommand,
77
+ typecheck: vars.typecheckCommand,
78
+ },
79
+ paths: {
80
+ root: '.ralph',
81
+ prompts: '.ralph/prompts',
82
+ guides: '.ralph/guides',
83
+ specs: '.ralph/specs',
84
+ scripts: '.ralph/scripts',
85
+ learnings: '.ralph/LEARNINGS.md',
86
+ agents: '.ralph/AGENTS.md',
87
+ },
88
+ loop: {
89
+ maxIterations: 10,
90
+ maxE2eAttempts: 5,
91
+ defaultModel: 'sonnet',
92
+ planningModel: 'opus',
93
+ },
94
+ };
95
+ }
96
+
97
+ /**
98
+ * Generate ralph.config.js file content as JavaScript module
99
+ */
100
+ export function generateConfigFile(config: RalphConfig): string {
101
+ const content = `module.exports = ${JSON.stringify(config, null, 2)};
102
+ `;
103
+
104
+ // Fix JSON to valid JS (unquote keys)
105
+ return content
106
+ .replace(/"(\w+)":/g, '$1:')
107
+ .replace(/: "([^"]+)"/g, ": '$1'");
108
+ }
109
+
110
+ /**
111
+ * Generate ralph.config.js from scan result
112
+ */
113
+ export function generateConfigFileFromScan(scanResult: ScanResult, customVars: Record<string, string> = {}): string {
114
+ const config = generateConfig(scanResult, customVars);
115
+ return generateConfigFile(config);
116
+ }