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.
- package/LICENSE +21 -0
- package/README.md +341 -0
- package/bin/ralph.js +8 -0
- package/dist/ai/enhancer.d.ts +100 -0
- package/dist/ai/enhancer.d.ts.map +1 -0
- package/dist/ai/enhancer.js +233 -0
- package/dist/ai/enhancer.js.map +1 -0
- package/dist/ai/index.d.ts +8 -0
- package/dist/ai/index.d.ts.map +1 -0
- package/dist/ai/index.js +11 -0
- package/dist/ai/index.js.map +1 -0
- package/dist/ai/prompts.d.ts +26 -0
- package/dist/ai/prompts.d.ts.map +1 -0
- package/dist/ai/prompts.js +201 -0
- package/dist/ai/prompts.js.map +1 -0
- package/dist/ai/providers.d.ts +35 -0
- package/dist/ai/providers.d.ts.map +1 -0
- package/dist/ai/providers.js +104 -0
- package/dist/ai/providers.js.map +1 -0
- package/dist/cli.d.ts +6 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +196 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/init.d.ts +16 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +124 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/monitor.d.ts +17 -0
- package/dist/commands/monitor.d.ts.map +1 -0
- package/dist/commands/monitor.js +342 -0
- package/dist/commands/monitor.js.map +1 -0
- package/dist/commands/new.d.ts +19 -0
- package/dist/commands/new.d.ts.map +1 -0
- package/dist/commands/new.js +272 -0
- package/dist/commands/new.js.map +1 -0
- package/dist/commands/run.d.ts +16 -0
- package/dist/commands/run.d.ts.map +1 -0
- package/dist/commands/run.js +175 -0
- package/dist/commands/run.js.map +1 -0
- package/dist/generator/config.d.ts +59 -0
- package/dist/generator/config.d.ts.map +1 -0
- package/dist/generator/config.js +68 -0
- package/dist/generator/config.js.map +1 -0
- package/dist/generator/index.d.ts +64 -0
- package/dist/generator/index.d.ts.map +1 -0
- package/dist/generator/index.js +147 -0
- package/dist/generator/index.js.map +1 -0
- package/dist/generator/templates.d.ts +70 -0
- package/dist/generator/templates.d.ts.map +1 -0
- package/dist/generator/templates.js +296 -0
- package/dist/generator/templates.js.map +1 -0
- package/dist/generator/writer.d.ts +93 -0
- package/dist/generator/writer.d.ts.map +1 -0
- package/dist/generator/writer.js +213 -0
- package/dist/generator/writer.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -0
- package/dist/scanner/detectors/core/framework.d.ts +11 -0
- package/dist/scanner/detectors/core/framework.d.ts.map +1 -0
- package/dist/scanner/detectors/core/framework.js +275 -0
- package/dist/scanner/detectors/core/framework.js.map +1 -0
- package/dist/scanner/detectors/core/packageManager.d.ts +11 -0
- package/dist/scanner/detectors/core/packageManager.d.ts.map +1 -0
- package/dist/scanner/detectors/core/packageManager.js +74 -0
- package/dist/scanner/detectors/core/packageManager.js.map +1 -0
- package/dist/scanner/detectors/core/styling.d.ts +12 -0
- package/dist/scanner/detectors/core/styling.d.ts.map +1 -0
- package/dist/scanner/detectors/core/styling.js +230 -0
- package/dist/scanner/detectors/core/styling.js.map +1 -0
- package/dist/scanner/detectors/core/testing.d.ts +12 -0
- package/dist/scanner/detectors/core/testing.d.ts.map +1 -0
- package/dist/scanner/detectors/core/testing.js +190 -0
- package/dist/scanner/detectors/core/testing.js.map +1 -0
- package/dist/scanner/detectors/data/api.d.ts +12 -0
- package/dist/scanner/detectors/data/api.d.ts.map +1 -0
- package/dist/scanner/detectors/data/api.js +261 -0
- package/dist/scanner/detectors/data/api.js.map +1 -0
- package/dist/scanner/detectors/data/database.d.ts +12 -0
- package/dist/scanner/detectors/data/database.d.ts.map +1 -0
- package/dist/scanner/detectors/data/database.js +213 -0
- package/dist/scanner/detectors/data/database.js.map +1 -0
- package/dist/scanner/detectors/data/orm.d.ts +12 -0
- package/dist/scanner/detectors/data/orm.d.ts.map +1 -0
- package/dist/scanner/detectors/data/orm.js +160 -0
- package/dist/scanner/detectors/data/orm.js.map +1 -0
- package/dist/scanner/detectors/frontend/formHandling.d.ts +12 -0
- package/dist/scanner/detectors/frontend/formHandling.d.ts.map +1 -0
- package/dist/scanner/detectors/frontend/formHandling.js +211 -0
- package/dist/scanner/detectors/frontend/formHandling.js.map +1 -0
- package/dist/scanner/detectors/frontend/stateManagement.d.ts +12 -0
- package/dist/scanner/detectors/frontend/stateManagement.d.ts.map +1 -0
- package/dist/scanner/detectors/frontend/stateManagement.js +221 -0
- package/dist/scanner/detectors/frontend/stateManagement.js.map +1 -0
- package/dist/scanner/detectors/frontend/uiComponents.d.ts +12 -0
- package/dist/scanner/detectors/frontend/uiComponents.d.ts.map +1 -0
- package/dist/scanner/detectors/frontend/uiComponents.js +285 -0
- package/dist/scanner/detectors/frontend/uiComponents.js.map +1 -0
- package/dist/scanner/detectors/infra/deployment.d.ts +12 -0
- package/dist/scanner/detectors/infra/deployment.d.ts.map +1 -0
- package/dist/scanner/detectors/infra/deployment.js +301 -0
- package/dist/scanner/detectors/infra/deployment.js.map +1 -0
- package/dist/scanner/detectors/infra/monorepo.d.ts +12 -0
- package/dist/scanner/detectors/infra/monorepo.d.ts.map +1 -0
- package/dist/scanner/detectors/infra/monorepo.js +219 -0
- package/dist/scanner/detectors/infra/monorepo.js.map +1 -0
- package/dist/scanner/detectors/mcp/mcpProject.d.ts +12 -0
- package/dist/scanner/detectors/mcp/mcpProject.d.ts.map +1 -0
- package/dist/scanner/detectors/mcp/mcpProject.js +154 -0
- package/dist/scanner/detectors/mcp/mcpProject.js.map +1 -0
- package/dist/scanner/detectors/mcp/mcpServers.d.ts +17 -0
- package/dist/scanner/detectors/mcp/mcpServers.d.ts.map +1 -0
- package/dist/scanner/detectors/mcp/mcpServers.js +193 -0
- package/dist/scanner/detectors/mcp/mcpServers.js.map +1 -0
- package/dist/scanner/detectors/services/analytics.d.ts +12 -0
- package/dist/scanner/detectors/services/analytics.d.ts.map +1 -0
- package/dist/scanner/detectors/services/analytics.js +236 -0
- package/dist/scanner/detectors/services/analytics.js.map +1 -0
- package/dist/scanner/detectors/services/auth.d.ts +12 -0
- package/dist/scanner/detectors/services/auth.d.ts.map +1 -0
- package/dist/scanner/detectors/services/auth.js +217 -0
- package/dist/scanner/detectors/services/auth.js.map +1 -0
- package/dist/scanner/detectors/services/email.d.ts +12 -0
- package/dist/scanner/detectors/services/email.d.ts.map +1 -0
- package/dist/scanner/detectors/services/email.js +211 -0
- package/dist/scanner/detectors/services/email.js.map +1 -0
- package/dist/scanner/detectors/services/payments.d.ts +12 -0
- package/dist/scanner/detectors/services/payments.d.ts.map +1 -0
- package/dist/scanner/detectors/services/payments.js +185 -0
- package/dist/scanner/detectors/services/payments.js.map +1 -0
- package/dist/scanner/detectors/utils.d.ts +160 -0
- package/dist/scanner/detectors/utils.d.ts.map +1 -0
- package/dist/scanner/detectors/utils.js +222 -0
- package/dist/scanner/detectors/utils.js.map +1 -0
- package/dist/scanner/index.d.ts +42 -0
- package/dist/scanner/index.d.ts.map +1 -0
- package/dist/scanner/index.js +282 -0
- package/dist/scanner/index.js.map +1 -0
- package/dist/scanner/registry.d.ts +43 -0
- package/dist/scanner/registry.d.ts.map +1 -0
- package/dist/scanner/registry.js +243 -0
- package/dist/scanner/registry.js.map +1 -0
- package/dist/scanner/types.d.ts +112 -0
- package/dist/scanner/types.d.ts.map +1 -0
- package/dist/scanner/types.js +6 -0
- package/dist/scanner/types.js.map +1 -0
- package/dist/templates/config/ralph.config.js.tmpl +38 -0
- package/dist/templates/guides/AGENTS.md.tmpl +100 -0
- package/dist/templates/guides/FRONTEND.md.tmpl +523 -0
- package/dist/templates/guides/PERFORMANCE.md.tmpl +264 -0
- package/dist/templates/guides/SECURITY.md.tmpl +100 -0
- package/dist/templates/prompts/PROMPT.md.tmpl +77 -0
- package/dist/templates/prompts/PROMPT_e2e.md.tmpl +234 -0
- package/dist/templates/prompts/PROMPT_feature.md.tmpl +83 -0
- package/dist/templates/prompts/PROMPT_review.md.tmpl +167 -0
- package/dist/templates/prompts/PROMPT_verify.md.tmpl +72 -0
- package/dist/templates/root/.gitignore.tmpl +5 -0
- package/dist/templates/root/LEARNINGS.md.tmpl +24 -0
- package/dist/templates/root/README.md.tmpl +61 -0
- package/dist/templates/scripts/feature-loop.sh.tmpl +267 -0
- package/dist/templates/scripts/loop.sh.tmpl +59 -0
- package/dist/templates/scripts/ralph-monitor.sh.tmpl +244 -0
- package/dist/templates/specs/README.md.tmpl +57 -0
- package/dist/templates/specs/_example.md.tmpl +71 -0
- package/dist/utils/config.d.ts +95 -0
- package/dist/utils/config.d.ts.map +1 -0
- package/dist/utils/config.js +148 -0
- package/dist/utils/config.js.map +1 -0
- package/dist/utils/header.d.ts +5 -0
- package/dist/utils/header.d.ts.map +1 -0
- package/dist/utils/header.js +15 -0
- package/dist/utils/header.js.map +1 -0
- package/dist/utils/logger.d.ts +11 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +24 -0
- package/dist/utils/logger.js.map +1 -0
- package/package.json +44 -0
- package/src/ai/enhancer.ts +350 -0
- package/src/ai/index.ts +38 -0
- package/src/ai/prompts.ts +217 -0
- package/src/ai/providers.ts +136 -0
- package/src/cli.ts +255 -0
- package/src/commands/init.ts +149 -0
- package/src/commands/monitor.ts +412 -0
- package/src/commands/new.ts +312 -0
- package/src/commands/run.ts +214 -0
- package/src/generator/config.ts +116 -0
- package/src/generator/index.ts +227 -0
- package/src/generator/templates.ts +412 -0
- package/src/generator/writer.ts +293 -0
- package/src/index.ts +41 -0
- package/src/scanner/detectors/core/framework.ts +332 -0
- package/src/scanner/detectors/core/packageManager.ts +91 -0
- package/src/scanner/detectors/core/styling.ts +261 -0
- package/src/scanner/detectors/core/testing.ts +221 -0
- package/src/scanner/detectors/data/api.ts +303 -0
- package/src/scanner/detectors/data/database.ts +245 -0
- package/src/scanner/detectors/data/orm.ts +180 -0
- package/src/scanner/detectors/frontend/formHandling.ts +244 -0
- package/src/scanner/detectors/frontend/stateManagement.ts +261 -0
- package/src/scanner/detectors/frontend/uiComponents.ts +328 -0
- package/src/scanner/detectors/infra/deployment.ts +343 -0
- package/src/scanner/detectors/infra/monorepo.ts +251 -0
- package/src/scanner/detectors/mcp/mcpProject.ts +176 -0
- package/src/scanner/detectors/mcp/mcpServers.ts +237 -0
- package/src/scanner/detectors/services/analytics.ts +273 -0
- package/src/scanner/detectors/services/auth.ts +254 -0
- package/src/scanner/detectors/services/email.ts +244 -0
- package/src/scanner/detectors/services/payments.ts +213 -0
- package/src/scanner/detectors/utils.ts +251 -0
- package/src/scanner/index.ts +354 -0
- package/src/scanner/registry.ts +301 -0
- package/src/scanner/types.ts +152 -0
- package/src/templates/config/ralph.config.js.tmpl +38 -0
- package/src/templates/guides/AGENTS.md.tmpl +100 -0
- package/src/templates/guides/FRONTEND.md.tmpl +523 -0
- package/src/templates/guides/PERFORMANCE.md.tmpl +264 -0
- package/src/templates/guides/SECURITY.md.tmpl +100 -0
- package/src/templates/prompts/PROMPT.md.tmpl +77 -0
- package/src/templates/prompts/PROMPT_e2e.md.tmpl +234 -0
- package/src/templates/prompts/PROMPT_feature.md.tmpl +83 -0
- package/src/templates/prompts/PROMPT_review.md.tmpl +167 -0
- package/src/templates/prompts/PROMPT_verify.md.tmpl +72 -0
- package/src/templates/root/.gitignore.tmpl +5 -0
- package/src/templates/root/LEARNINGS.md.tmpl +24 -0
- package/src/templates/root/README.md.tmpl +61 -0
- package/src/templates/scripts/feature-loop.sh.tmpl +267 -0
- package/src/templates/scripts/loop.sh.tmpl +59 -0
- package/src/templates/scripts/ralph-monitor.sh.tmpl +244 -0
- package/src/templates/specs/README.md.tmpl +57 -0
- package/src/templates/specs/_example.md.tmpl +71 -0
- package/src/utils/config.ts +221 -0
- package/src/utils/header.ts +15 -0
- package/src/utils/logger.ts +28 -0
- 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
|
+
}
|