@arvorco/relentless 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/.claude/commands/relentless.analyze.md +20 -0
- package/.claude/commands/relentless.checklist.md +15 -0
- package/.claude/commands/relentless.clarify.md +19 -0
- package/.claude/commands/relentless.constitution.md +78 -0
- package/.claude/commands/relentless.implement.md +15 -0
- package/.claude/commands/relentless.plan.md +22 -0
- package/.claude/commands/relentless.plan.old.md +89 -0
- package/.claude/commands/relentless.specify.md +254 -0
- package/.claude/commands/relentless.tasks.md +25 -0
- package/.claude/commands/relentless.taskstoissues.md +15 -0
- package/.claude/settings.local.json +23 -0
- package/.claude/skills/analyze/SKILL.md +149 -0
- package/.claude/skills/checklist/SKILL.md +173 -0
- package/.claude/skills/checklist/templates/checklist-template.md +40 -0
- package/.claude/skills/clarify/SKILL.md +174 -0
- package/.claude/skills/constitution/SKILL.md +150 -0
- package/.claude/skills/constitution/templates/constitution-template.md +228 -0
- package/.claude/skills/implement/SKILL.md +141 -0
- package/.claude/skills/plan/SKILL.md +179 -0
- package/.claude/skills/plan/templates/plan-template.md +104 -0
- package/.claude/skills/prd/SKILL.md +242 -0
- package/.claude/skills/relentless/SKILL.md +265 -0
- package/.claude/skills/specify/SKILL.md +220 -0
- package/.claude/skills/specify/scripts/bash/check-prerequisites.sh +166 -0
- package/.claude/skills/specify/scripts/bash/common.sh +156 -0
- package/.claude/skills/specify/scripts/bash/create-new-feature.sh +305 -0
- package/.claude/skills/specify/scripts/bash/setup-plan.sh +61 -0
- package/.claude/skills/specify/scripts/bash/update-agent-context.sh +799 -0
- package/.claude/skills/specify/templates/spec-template.md +115 -0
- package/.claude/skills/tasks/SKILL.md +202 -0
- package/.claude/skills/tasks/templates/tasks-template.md +251 -0
- package/.claude/skills/taskstoissues/SKILL.md +97 -0
- package/.specify/memory/constitution.md +50 -0
- package/.specify/scripts/bash/check-prerequisites.sh +166 -0
- package/.specify/scripts/bash/common.sh +156 -0
- package/.specify/scripts/bash/create-new-feature.sh +297 -0
- package/.specify/scripts/bash/setup-plan.sh +61 -0
- package/.specify/scripts/bash/update-agent-context.sh +799 -0
- package/.specify/templates/agent-file-template.md +28 -0
- package/.specify/templates/checklist-template.md +40 -0
- package/.specify/templates/plan-template.md +104 -0
- package/.specify/templates/spec-template.md +115 -0
- package/.specify/templates/tasks-template.md +251 -0
- package/CHANGES_SUMMARY.md +255 -0
- package/CLAUDE.md +92 -0
- package/GEMINI_SETUP.md +256 -0
- package/LICENSE +21 -0
- package/README.md +1171 -0
- package/REFACTOR_SUMMARY.md +267 -0
- package/bin/relentless.ts +536 -0
- package/bun.lock +352 -0
- package/eslint.config.js +37 -0
- package/package.json +61 -0
- package/prd.json.example +64 -0
- package/prompt.md +108 -0
- package/ralph.sh +80 -0
- package/relentless/config.json +38 -0
- package/relentless/features/.gitkeep +0 -0
- package/relentless/features/ghsk-ideas/prd.json +229 -0
- package/relentless/features/ghsk-ideas/prd.md +191 -0
- package/relentless/features/ghsk-ideas/progress.txt +408 -0
- package/relentless/prompt.md +79 -0
- package/skills/checklist/SKILL.md +349 -0
- package/skills/clarify/SKILL.md +476 -0
- package/skills/prd/SKILL.md +242 -0
- package/skills/relentless/SKILL.md +268 -0
- package/skills/tasks/SKILL.md +577 -0
- package/src/agents/amp.ts +115 -0
- package/src/agents/claude.ts +185 -0
- package/src/agents/codex.ts +89 -0
- package/src/agents/droid.ts +90 -0
- package/src/agents/gemini.ts +109 -0
- package/src/agents/index.ts +16 -0
- package/src/agents/opencode.ts +88 -0
- package/src/agents/registry.ts +95 -0
- package/src/agents/types.ts +101 -0
- package/src/config/index.ts +8 -0
- package/src/config/loader.ts +237 -0
- package/src/config/schema.ts +115 -0
- package/src/execution/index.ts +8 -0
- package/src/execution/router.ts +49 -0
- package/src/execution/runner.ts +512 -0
- package/src/index.ts +11 -0
- package/src/init/index.ts +7 -0
- package/src/init/scaffolder.ts +377 -0
- package/src/prd/analyzer.ts +512 -0
- package/src/prd/index.ts +11 -0
- package/src/prd/issues.ts +249 -0
- package/src/prd/parser.ts +281 -0
- package/src/prd/progress.ts +198 -0
- package/src/prd/types.ts +170 -0
- package/src/tui/App.tsx +85 -0
- package/src/tui/TUIRunner.tsx +400 -0
- package/src/tui/components/AgentOutput.tsx +45 -0
- package/src/tui/components/AgentStatus.tsx +64 -0
- package/src/tui/components/CurrentStory.tsx +66 -0
- package/src/tui/components/Header.tsx +49 -0
- package/src/tui/components/ProgressBar.tsx +39 -0
- package/src/tui/components/StoryGrid.tsx +86 -0
- package/src/tui/hooks/useTUI.ts +147 -0
- package/src/tui/hooks/useTimer.ts +51 -0
- package/src/tui/index.tsx +17 -0
- package/src/tui/theme.ts +41 -0
- package/src/tui/types.ts +77 -0
- package/templates/constitution.md +228 -0
- package/templates/plan.md +273 -0
- package/tsconfig.json +27 -0
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project Scaffolder
|
|
3
|
+
*
|
|
4
|
+
* Creates Relentless files in a project's relentless/ directory
|
|
5
|
+
*
|
|
6
|
+
* Structure:
|
|
7
|
+
* relentless/
|
|
8
|
+
* ├── config.json
|
|
9
|
+
* ├── prompt.md
|
|
10
|
+
* └── features/
|
|
11
|
+
* └── <feature-name>/
|
|
12
|
+
* ├── prd.md
|
|
13
|
+
* ├── prd.json
|
|
14
|
+
* └── progress.txt
|
|
15
|
+
*
|
|
16
|
+
* Best practices:
|
|
17
|
+
* - All relentless files go in relentless/ subdirectory
|
|
18
|
+
* - Each feature gets its own folder with prd.md, prd.json, progress.txt
|
|
19
|
+
* - Only skills are installed to .claude/skills/ (expected by Claude Code)
|
|
20
|
+
* - Does not modify project root files (CLAUDE.md, AGENTS.md, etc.)
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { existsSync, mkdirSync } from "node:fs";
|
|
24
|
+
import { join } from "node:path";
|
|
25
|
+
import chalk from "chalk";
|
|
26
|
+
import { checkAgentHealth } from "../agents/registry";
|
|
27
|
+
import { DEFAULT_CONFIG } from "../config/schema";
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Get the relentless root directory
|
|
31
|
+
*/
|
|
32
|
+
const relentlessRoot = import.meta.dir.replace("/src/init", "");
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Files to create in the relentless/ directory
|
|
36
|
+
*/
|
|
37
|
+
const RELENTLESS_FILES: Record<string, () => string> = {
|
|
38
|
+
"config.json": () => JSON.stringify(DEFAULT_CONFIG, null, 2),
|
|
39
|
+
"prompt.md": () => PROMPT_TEMPLATE,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const PROMPT_TEMPLATE = `# Relentless Agent Instructions
|
|
43
|
+
|
|
44
|
+
You are an autonomous coding agent working on a software project.
|
|
45
|
+
|
|
46
|
+
## Before You Start
|
|
47
|
+
|
|
48
|
+
1. **Review the codebase** - Understand the current state, architecture, and patterns
|
|
49
|
+
2. **Read progress.txt** - Check the Codebase Patterns section for learnings from previous iterations
|
|
50
|
+
3. **Read the PRD** - Understand what needs to be done
|
|
51
|
+
|
|
52
|
+
## Your Task
|
|
53
|
+
|
|
54
|
+
1. Read the PRD at \`relentless/features/<feature>/prd.json\`
|
|
55
|
+
2. Read the progress log at \`relentless/features/<feature>/progress.txt\`
|
|
56
|
+
3. Check you're on the correct branch from PRD \`branchName\`. If not, check it out or create from main.
|
|
57
|
+
4. Pick the **highest priority** user story where \`passes: false\`
|
|
58
|
+
5. **Review relevant code** before implementing - understand existing patterns
|
|
59
|
+
6. Implement that single user story
|
|
60
|
+
7. Run quality checks (typecheck, lint, test - whatever your project requires)
|
|
61
|
+
8. If checks pass, commit ALL changes with message: \`feat: [Story ID] - [Story Title]\`
|
|
62
|
+
9. Update the PRD to set \`passes: true\` for the completed story
|
|
63
|
+
10. Append your progress to \`relentless/features/<feature>/progress.txt\`
|
|
64
|
+
|
|
65
|
+
## Progress Report Format
|
|
66
|
+
|
|
67
|
+
APPEND to progress.txt (never replace, always append):
|
|
68
|
+
\`\`\`
|
|
69
|
+
## [Date/Time] - [Story ID]
|
|
70
|
+
- What was implemented
|
|
71
|
+
- Files changed
|
|
72
|
+
- **Learnings for future iterations:**
|
|
73
|
+
- Patterns discovered
|
|
74
|
+
- Gotchas encountered
|
|
75
|
+
- Useful context
|
|
76
|
+
---
|
|
77
|
+
\`\`\`
|
|
78
|
+
|
|
79
|
+
## Quality Requirements
|
|
80
|
+
|
|
81
|
+
- ALL commits must pass your project's quality checks (typecheck, lint, test)
|
|
82
|
+
- Do NOT commit broken code
|
|
83
|
+
- Keep changes focused and minimal
|
|
84
|
+
- Follow existing code patterns
|
|
85
|
+
- Review code before modifying it
|
|
86
|
+
|
|
87
|
+
## Stop Condition
|
|
88
|
+
|
|
89
|
+
After completing a user story, check if ALL stories have \`passes: true\`.
|
|
90
|
+
|
|
91
|
+
If ALL stories are complete and passing, reply with:
|
|
92
|
+
<promise>COMPLETE</promise>
|
|
93
|
+
|
|
94
|
+
If there are still stories with \`passes: false\`, end your response normally (another iteration will pick up the next story).
|
|
95
|
+
|
|
96
|
+
## Important
|
|
97
|
+
|
|
98
|
+
- Work on ONE story per iteration
|
|
99
|
+
- Review existing code before implementing
|
|
100
|
+
- Commit frequently
|
|
101
|
+
- Keep CI green
|
|
102
|
+
`;
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Default progress.txt content for a new feature with YAML frontmatter
|
|
106
|
+
*/
|
|
107
|
+
export function createProgressTemplate(featureName: string): string {
|
|
108
|
+
const started = new Date().toISOString();
|
|
109
|
+
return `---
|
|
110
|
+
feature: ${featureName}
|
|
111
|
+
started: ${started}
|
|
112
|
+
last_updated: ${started}
|
|
113
|
+
stories_completed: 0
|
|
114
|
+
patterns: []
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
# Progress Log: ${featureName}
|
|
118
|
+
|
|
119
|
+
## Codebase Patterns
|
|
120
|
+
|
|
121
|
+
<!-- Patterns discovered during development will be added here -->
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
`;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Initialize Relentless in a project
|
|
129
|
+
*/
|
|
130
|
+
export async function initProject(projectDir: string = process.cwd()): Promise<void> {
|
|
131
|
+
console.log(chalk.bold.blue("\n🚀 Initializing Relentless\n"));
|
|
132
|
+
|
|
133
|
+
// Check installed agents
|
|
134
|
+
console.log(chalk.dim("Detecting installed agents..."));
|
|
135
|
+
const health = await checkAgentHealth();
|
|
136
|
+
const installed = health.filter((h) => h.installed);
|
|
137
|
+
|
|
138
|
+
console.log(`\nFound ${chalk.green(installed.length)} installed agents:`);
|
|
139
|
+
for (const agent of installed) {
|
|
140
|
+
console.log(` ${chalk.green("✓")} ${agent.displayName}`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const notInstalled = health.filter((h) => !h.installed);
|
|
144
|
+
if (notInstalled.length > 0) {
|
|
145
|
+
console.log(chalk.dim(`\nNot installed: ${notInstalled.map((a) => a.displayName).join(", ")}`));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Create relentless directory structure
|
|
149
|
+
const relentlessDir = join(projectDir, "relentless");
|
|
150
|
+
const featuresDir = join(relentlessDir, "features");
|
|
151
|
+
|
|
152
|
+
for (const dir of [relentlessDir, featuresDir]) {
|
|
153
|
+
if (!existsSync(dir)) {
|
|
154
|
+
mkdirSync(dir, { recursive: true });
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Create relentless files
|
|
159
|
+
console.log(chalk.dim("\nCreating relentless files..."));
|
|
160
|
+
|
|
161
|
+
for (const [filename, contentFn] of Object.entries(RELENTLESS_FILES)) {
|
|
162
|
+
const path = join(relentlessDir, filename);
|
|
163
|
+
|
|
164
|
+
if (existsSync(path)) {
|
|
165
|
+
console.log(` ${chalk.yellow("⚠")} relentless/${filename} already exists, skipping`);
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
await Bun.write(path, contentFn());
|
|
170
|
+
console.log(` ${chalk.green("✓")} relentless/${filename}`);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Note: constitution.md is NOT copied - it should be created by /relentless.constitution command
|
|
174
|
+
// This ensures each project gets a personalized constitution
|
|
175
|
+
|
|
176
|
+
// Create features directory with .gitkeep
|
|
177
|
+
const gitkeepPath = join(featuresDir, ".gitkeep");
|
|
178
|
+
if (!existsSync(gitkeepPath)) {
|
|
179
|
+
await Bun.write(gitkeepPath, "");
|
|
180
|
+
console.log(` ${chalk.green("✓")} relentless/features/.gitkeep`);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Copy skills to .claude/skills/ (this is expected by Claude Code)
|
|
184
|
+
console.log(chalk.dim("\nInstalling skills..."));
|
|
185
|
+
const skillsDir = join(projectDir, ".claude", "skills");
|
|
186
|
+
if (!existsSync(skillsDir)) {
|
|
187
|
+
mkdirSync(skillsDir, { recursive: true });
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const sourceSkillsDir = join(relentlessRoot, ".claude", "skills");
|
|
191
|
+
|
|
192
|
+
if (existsSync(sourceSkillsDir)) {
|
|
193
|
+
const skills = [
|
|
194
|
+
"prd",
|
|
195
|
+
"relentless",
|
|
196
|
+
"constitution",
|
|
197
|
+
"specify",
|
|
198
|
+
"plan",
|
|
199
|
+
"tasks",
|
|
200
|
+
"checklist",
|
|
201
|
+
"clarify",
|
|
202
|
+
"analyze",
|
|
203
|
+
"implement",
|
|
204
|
+
"taskstoissues",
|
|
205
|
+
];
|
|
206
|
+
|
|
207
|
+
for (const skill of skills) {
|
|
208
|
+
const sourcePath = join(sourceSkillsDir, skill);
|
|
209
|
+
const destPath = join(skillsDir, skill);
|
|
210
|
+
|
|
211
|
+
if (existsSync(sourcePath) && !existsSync(destPath)) {
|
|
212
|
+
await Bun.spawn(["cp", "-r", sourcePath, destPath]).exited;
|
|
213
|
+
console.log(` ${chalk.green("✓")} .claude/skills/${skill}`);
|
|
214
|
+
} else if (existsSync(destPath)) {
|
|
215
|
+
console.log(` ${chalk.yellow("⚠")} .claude/skills/${skill} already exists, skipping`);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Copy commands to .claude/commands/ (for Claude Code)
|
|
221
|
+
console.log(chalk.dim("\nInstalling commands..."));
|
|
222
|
+
const commandsDir = join(projectDir, ".claude", "commands");
|
|
223
|
+
if (!existsSync(commandsDir)) {
|
|
224
|
+
mkdirSync(commandsDir, { recursive: true });
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const sourceCommandsDir = join(relentlessRoot, ".claude", "commands");
|
|
228
|
+
|
|
229
|
+
if (existsSync(sourceCommandsDir)) {
|
|
230
|
+
const commands = [
|
|
231
|
+
"relentless.analyze.md",
|
|
232
|
+
"relentless.checklist.md",
|
|
233
|
+
"relentless.clarify.md",
|
|
234
|
+
"relentless.constitution.md",
|
|
235
|
+
"relentless.implement.md",
|
|
236
|
+
"relentless.plan.md",
|
|
237
|
+
"relentless.specify.md",
|
|
238
|
+
"relentless.tasks.md",
|
|
239
|
+
"relentless.taskstoissues.md",
|
|
240
|
+
];
|
|
241
|
+
|
|
242
|
+
for (const command of commands) {
|
|
243
|
+
const sourcePath = join(sourceCommandsDir, command);
|
|
244
|
+
const destPath = join(commandsDir, command);
|
|
245
|
+
|
|
246
|
+
if (existsSync(sourcePath) && !existsSync(destPath)) {
|
|
247
|
+
const content = await Bun.file(sourcePath).text();
|
|
248
|
+
await Bun.write(destPath, content);
|
|
249
|
+
console.log(` ${chalk.green("✓")} .claude/commands/${command}`);
|
|
250
|
+
} else if (existsSync(destPath)) {
|
|
251
|
+
console.log(` ${chalk.yellow("⚠")} .claude/commands/${command} already exists, skipping`);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Print next steps
|
|
257
|
+
console.log(chalk.bold.green("\n✅ Relentless initialized!\n"));
|
|
258
|
+
console.log(chalk.dim("Structure:"));
|
|
259
|
+
console.log(chalk.dim(" relentless/"));
|
|
260
|
+
console.log(chalk.dim(" ├── config.json # Configuration"));
|
|
261
|
+
console.log(chalk.dim(" ├── constitution.md # Project governance"));
|
|
262
|
+
console.log(chalk.dim(" ├── prompt.md # Base prompt template"));
|
|
263
|
+
console.log(chalk.dim(" └── features/ # Feature folders"));
|
|
264
|
+
console.log(chalk.dim(" └── <feature>/ # Each feature has:"));
|
|
265
|
+
console.log(chalk.dim(" ├── prd.md # PRD markdown"));
|
|
266
|
+
console.log(chalk.dim(" ├── prd.json # PRD JSON"));
|
|
267
|
+
console.log(chalk.dim(" └── progress.txt # Progress log\n"));
|
|
268
|
+
|
|
269
|
+
console.log("Next steps:");
|
|
270
|
+
console.log(chalk.dim("1. Create project constitution (recommended):"));
|
|
271
|
+
console.log(` ${chalk.cyan("/relentless.constitution")}`);
|
|
272
|
+
console.log(chalk.dim("\n2. Create a feature specification:"));
|
|
273
|
+
console.log(` ${chalk.cyan("/relentless.specify Add user authentication")}`);
|
|
274
|
+
console.log(chalk.dim("\n3. Generate plan, tasks, and checklist:"));
|
|
275
|
+
console.log(` ${chalk.cyan("/relentless.plan")}`);
|
|
276
|
+
console.log(` ${chalk.cyan("/relentless.tasks")}`);
|
|
277
|
+
console.log(` ${chalk.cyan("/relentless.checklist")}`);
|
|
278
|
+
console.log(chalk.dim("\n4. Convert to JSON and run:"));
|
|
279
|
+
console.log(` ${chalk.cyan("relentless convert relentless/features/NNN-feature/tasks.md --feature NNN-feature")}`);
|
|
280
|
+
console.log(` ${chalk.cyan("relentless run --feature NNN-feature --tui")}`);
|
|
281
|
+
console.log("");
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Options for creating a feature
|
|
286
|
+
*/
|
|
287
|
+
export interface CreateFeatureOptions {
|
|
288
|
+
/** Include plan.md template */
|
|
289
|
+
withPlan?: boolean;
|
|
290
|
+
/** Auto-number the feature directory (e.g., 001-feature-name) */
|
|
291
|
+
autoNumber?: boolean;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Get the next feature number by finding the highest existing number
|
|
296
|
+
*/
|
|
297
|
+
function getNextFeatureNumber(projectDir: string): number {
|
|
298
|
+
const featuresDir = join(projectDir, "relentless", "features");
|
|
299
|
+
|
|
300
|
+
if (!existsSync(featuresDir)) {
|
|
301
|
+
return 1;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const features = listFeatures(projectDir);
|
|
305
|
+
|
|
306
|
+
// Extract numbers from features with format NNN-name
|
|
307
|
+
const numbers = features
|
|
308
|
+
.map((feature) => {
|
|
309
|
+
const match = feature.match(/^(\d{3})-/);
|
|
310
|
+
return match ? parseInt(match[1], 10) : 0;
|
|
311
|
+
})
|
|
312
|
+
.filter((n) => n > 0);
|
|
313
|
+
|
|
314
|
+
// Return next number (or 1 if no numbered features exist)
|
|
315
|
+
return numbers.length > 0 ? Math.max(...numbers) + 1 : 1;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Create a new feature folder
|
|
320
|
+
*/
|
|
321
|
+
export async function createFeature(
|
|
322
|
+
projectDir: string,
|
|
323
|
+
featureName: string,
|
|
324
|
+
options: CreateFeatureOptions = {}
|
|
325
|
+
): Promise<string> {
|
|
326
|
+
// Generate numbered directory name if autoNumber is enabled
|
|
327
|
+
let finalFeatureName = featureName;
|
|
328
|
+
if (options.autoNumber) {
|
|
329
|
+
const nextNumber = getNextFeatureNumber(projectDir);
|
|
330
|
+
const numberPrefix = nextNumber.toString().padStart(3, "0");
|
|
331
|
+
finalFeatureName = `${numberPrefix}-${featureName}`;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const featureDir = join(projectDir, "relentless", "features", finalFeatureName);
|
|
335
|
+
|
|
336
|
+
if (existsSync(featureDir)) {
|
|
337
|
+
throw new Error(`Feature '${finalFeatureName}' already exists`);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
mkdirSync(featureDir, { recursive: true });
|
|
341
|
+
|
|
342
|
+
// Create progress.txt
|
|
343
|
+
const progressPath = join(featureDir, "progress.txt");
|
|
344
|
+
await Bun.write(progressPath, createProgressTemplate(finalFeatureName));
|
|
345
|
+
|
|
346
|
+
// Copy plan.md template if requested
|
|
347
|
+
if (options.withPlan) {
|
|
348
|
+
const planSourcePath = join(relentlessRoot, "templates", "plan.md");
|
|
349
|
+
const planDestPath = join(featureDir, "plan.md");
|
|
350
|
+
|
|
351
|
+
if (existsSync(planSourcePath)) {
|
|
352
|
+
const planContent = await Bun.file(planSourcePath).text();
|
|
353
|
+
await Bun.write(planDestPath, planContent);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
return featureDir;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* List all features
|
|
362
|
+
*/
|
|
363
|
+
export function listFeatures(projectDir: string): string[] {
|
|
364
|
+
const featuresDir = join(projectDir, "relentless", "features");
|
|
365
|
+
|
|
366
|
+
if (!existsSync(featuresDir)) {
|
|
367
|
+
return [];
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const entries = Bun.spawnSync(["ls", "-1", featuresDir]);
|
|
371
|
+
const output = new TextDecoder().decode(entries.stdout);
|
|
372
|
+
|
|
373
|
+
return output
|
|
374
|
+
.split("\n")
|
|
375
|
+
.map((s) => s.trim())
|
|
376
|
+
.filter((s) => s && s !== ".gitkeep");
|
|
377
|
+
}
|