claude-autopm 1.26.0 → 1.27.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/autopm/.claude/agents/frameworks/e2e-test-engineer.md +1 -18
- package/autopm/.claude/agents/frameworks/nats-messaging-expert.md +1 -18
- package/autopm/.claude/agents/frameworks/react-frontend-engineer.md +1 -18
- package/autopm/.claude/agents/frameworks/react-ui-expert.md +1 -18
- package/autopm/.claude/agents/frameworks/tailwindcss-expert.md +1 -18
- package/autopm/.claude/agents/frameworks/ux-design-expert.md +1 -18
- package/autopm/.claude/agents/languages/bash-scripting-expert.md +1 -18
- package/autopm/.claude/agents/languages/javascript-frontend-engineer.md +1 -18
- package/autopm/.claude/agents/languages/nodejs-backend-engineer.md +1 -18
- package/autopm/.claude/agents/languages/python-backend-engineer.md +1 -18
- package/autopm/.claude/agents/languages/python-backend-expert.md +1 -18
- package/autopm/.claude/commands/pm/epic-decompose.md +19 -5
- package/autopm/.claude/commands/pm/prd-new.md +14 -1
- package/autopm/.claude/includes/task-creation-excellence.md +18 -0
- package/autopm/.claude/lib/ai-task-generator.js +84 -0
- package/autopm/.claude/lib/cli-parser.js +148 -0
- package/autopm/.claude/lib/dependency-analyzer.js +157 -0
- package/autopm/.claude/lib/frontmatter.js +224 -0
- package/autopm/.claude/lib/task-utils.js +64 -0
- package/autopm/.claude/scripts/pm-epic-decompose-local.js +158 -0
- package/autopm/.claude/scripts/pm-epic-list-local.js +103 -0
- package/autopm/.claude/scripts/pm-epic-show-local.js +70 -0
- package/autopm/.claude/scripts/pm-epic-update-local.js +56 -0
- package/autopm/.claude/scripts/pm-prd-list-local.js +111 -0
- package/autopm/.claude/scripts/pm-prd-new-local.js +196 -0
- package/autopm/.claude/scripts/pm-prd-parse-local.js +360 -0
- package/autopm/.claude/scripts/pm-prd-show-local.js +101 -0
- package/autopm/.claude/scripts/pm-prd-update-local.js +153 -0
- package/autopm/.claude/scripts/pm-sync-download-local.js +424 -0
- package/autopm/.claude/scripts/pm-sync-upload-local.js +473 -0
- package/autopm/.claude/scripts/pm-task-list-local.js +86 -0
- package/autopm/.claude/scripts/pm-task-show-local.js +92 -0
- package/autopm/.claude/scripts/pm-task-update-local.js +109 -0
- package/autopm/.claude/scripts/setup-local-mode.js +127 -0
- package/package.json +5 -3
- package/scripts/create-task-issues.sh +26 -0
- package/scripts/fix-invalid-command-refs.sh +4 -3
- package/scripts/fix-invalid-refs-simple.sh +8 -3
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* TASK-005: PRD-to-Epic Parser (Local Mode)
|
|
4
|
+
*
|
|
5
|
+
* Parse PRD markdown to Epic structure with proper section extraction
|
|
6
|
+
* Uses markdown-it for robust markdown parsing (Context7 verified)
|
|
7
|
+
*
|
|
8
|
+
* @module pm-prd-parse-local
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const fs = require('fs').promises;
|
|
12
|
+
const path = require('path');
|
|
13
|
+
const MarkdownIt = require('markdown-it');
|
|
14
|
+
const { parseFrontmatter, stringifyFrontmatter } = require('../lib/frontmatter');
|
|
15
|
+
const { showLocalPRD } = require('./pm-prd-show-local');
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Parse PRD to Epic structure
|
|
19
|
+
*
|
|
20
|
+
* @param {string} prdId - PRD identifier (e.g., 'prd-347')
|
|
21
|
+
* @returns {Promise<object>} Epic data with structure:
|
|
22
|
+
* - epicId: Generated epic ID
|
|
23
|
+
* - epicDir: Path to epic directory
|
|
24
|
+
* - epicPath: Path to epic.md file
|
|
25
|
+
* - frontmatter: Epic frontmatter object
|
|
26
|
+
* - sections: Extracted PRD sections
|
|
27
|
+
*/
|
|
28
|
+
async function parseLocalPRD(prdId) {
|
|
29
|
+
// 1. Load PRD
|
|
30
|
+
const prd = await showLocalPRD(prdId);
|
|
31
|
+
const prdMeta = prd.frontmatter;
|
|
32
|
+
const prdBody = prd.body;
|
|
33
|
+
|
|
34
|
+
// 2. Parse markdown sections
|
|
35
|
+
const sections = extractSections(prdBody);
|
|
36
|
+
|
|
37
|
+
// 3. Generate Epic frontmatter
|
|
38
|
+
const epicId = generateEpicId(prdId);
|
|
39
|
+
const epicFrontmatter = {
|
|
40
|
+
id: epicId,
|
|
41
|
+
prd_id: prdId,
|
|
42
|
+
title: `${prdMeta.title} - Implementation Epic`,
|
|
43
|
+
created: new Date().toISOString().split('T')[0],
|
|
44
|
+
status: 'planning',
|
|
45
|
+
github_issue: null,
|
|
46
|
+
tasks_total: 0,
|
|
47
|
+
tasks_completed: 0
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// 4. Build Epic body
|
|
51
|
+
const epicBody = buildEpicBody(sections, prdMeta);
|
|
52
|
+
|
|
53
|
+
// 5. Create epic directory
|
|
54
|
+
const epicDir = path.join(process.cwd(), '.claude', 'epics',
|
|
55
|
+
`${epicId}-${slugify(prdMeta.title)}`);
|
|
56
|
+
await fs.mkdir(epicDir, { recursive: true });
|
|
57
|
+
|
|
58
|
+
// 6. Write epic.md
|
|
59
|
+
const epicContent = stringifyFrontmatter(epicFrontmatter, epicBody);
|
|
60
|
+
const epicPath = path.join(epicDir, 'epic.md');
|
|
61
|
+
await fs.writeFile(epicPath, epicContent);
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
epicId,
|
|
65
|
+
epicDir,
|
|
66
|
+
epicPath,
|
|
67
|
+
frontmatter: epicFrontmatter,
|
|
68
|
+
sections
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Extract sections from PRD markdown
|
|
74
|
+
* Uses markdown-it to parse headings and content
|
|
75
|
+
*
|
|
76
|
+
* @param {string} markdown - PRD markdown content
|
|
77
|
+
* @returns {object} Extracted sections:
|
|
78
|
+
* - overview: Project overview/summary
|
|
79
|
+
* - goals: Project goals/objectives
|
|
80
|
+
* - userStories: Array of user story objects
|
|
81
|
+
* - requirements: Technical requirements
|
|
82
|
+
* - timeline: Timeline/milestones
|
|
83
|
+
*/
|
|
84
|
+
function extractSections(markdown) {
|
|
85
|
+
const md = new MarkdownIt();
|
|
86
|
+
const tokens = md.parse(markdown, {});
|
|
87
|
+
|
|
88
|
+
const sections = {
|
|
89
|
+
overview: '',
|
|
90
|
+
goals: '',
|
|
91
|
+
userStories: [],
|
|
92
|
+
requirements: '',
|
|
93
|
+
timeline: ''
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
let currentSection = null;
|
|
97
|
+
let currentContent = [];
|
|
98
|
+
let currentHeading = '';
|
|
99
|
+
let inList = false;
|
|
100
|
+
|
|
101
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
102
|
+
const token = tokens[i];
|
|
103
|
+
|
|
104
|
+
if (token.type === 'heading_open') {
|
|
105
|
+
// Only process ## headings (level 2), not ### (level 3)
|
|
106
|
+
if (token.tag === 'h2') {
|
|
107
|
+
// Save previous section
|
|
108
|
+
if (currentSection) {
|
|
109
|
+
const content = currentContent.join('\n').trim();
|
|
110
|
+
if (currentSection === 'userStories') {
|
|
111
|
+
sections[currentSection] = parseUserStories(content);
|
|
112
|
+
} else {
|
|
113
|
+
sections[currentSection] = content;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Get heading content from next token
|
|
118
|
+
const nextToken = tokens[i + 1];
|
|
119
|
+
if (nextToken && nextToken.type === 'inline') {
|
|
120
|
+
currentHeading = nextToken.content.toLowerCase();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Start new section
|
|
124
|
+
currentContent = [];
|
|
125
|
+
inList = false;
|
|
126
|
+
|
|
127
|
+
// Determine section type from heading
|
|
128
|
+
if (currentHeading.includes('overview') || currentHeading.includes('summary')) {
|
|
129
|
+
currentSection = 'overview';
|
|
130
|
+
} else if (currentHeading.includes('goal') || currentHeading.includes('objective')) {
|
|
131
|
+
currentSection = 'goals';
|
|
132
|
+
} else if (currentHeading.includes('user stor')) {
|
|
133
|
+
currentSection = 'userStories';
|
|
134
|
+
} else if (currentHeading.includes('requirement')) {
|
|
135
|
+
currentSection = 'requirements';
|
|
136
|
+
} else if (currentHeading.includes('timeline') || currentHeading.includes('milestone')) {
|
|
137
|
+
currentSection = 'timeline';
|
|
138
|
+
} else {
|
|
139
|
+
currentSection = null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Skip the inline token (heading content) and closing tag
|
|
143
|
+
i += 2;
|
|
144
|
+
} else if (token.tag === 'h3' && currentSection) {
|
|
145
|
+
// Preserve ### headings within sections
|
|
146
|
+
const nextToken = tokens[i + 1];
|
|
147
|
+
if (nextToken && nextToken.type === 'inline') {
|
|
148
|
+
currentContent.push('');
|
|
149
|
+
currentContent.push('### ' + nextToken.content);
|
|
150
|
+
currentContent.push('');
|
|
151
|
+
}
|
|
152
|
+
i += 2; // Skip inline and closing tag
|
|
153
|
+
}
|
|
154
|
+
} else if (currentSection && token.type === 'inline' && token.content) {
|
|
155
|
+
currentContent.push(token.content);
|
|
156
|
+
} else if (currentSection && token.type === 'fence' && token.content) {
|
|
157
|
+
// Preserve code blocks
|
|
158
|
+
currentContent.push('');
|
|
159
|
+
currentContent.push('```' + (token.info || ''));
|
|
160
|
+
currentContent.push(token.content.trim());
|
|
161
|
+
currentContent.push('```');
|
|
162
|
+
currentContent.push('');
|
|
163
|
+
} else if (currentSection && token.type === 'bullet_list_open') {
|
|
164
|
+
// Handle bullet lists
|
|
165
|
+
if (currentContent.length > 0 && currentContent[currentContent.length - 1] !== '') {
|
|
166
|
+
currentContent.push('');
|
|
167
|
+
}
|
|
168
|
+
inList = true;
|
|
169
|
+
} else if (currentSection && token.type === 'bullet_list_close') {
|
|
170
|
+
inList = false;
|
|
171
|
+
if (currentContent.length > 0 && currentContent[currentContent.length - 1] !== '') {
|
|
172
|
+
currentContent.push('');
|
|
173
|
+
}
|
|
174
|
+
} else if (currentSection && token.type === 'list_item_open') {
|
|
175
|
+
// Add list item marker - look ahead for paragraph
|
|
176
|
+
let contentFound = false;
|
|
177
|
+
for (let j = i + 1; j < tokens.length && !contentFound; j++) {
|
|
178
|
+
if (tokens[j].type === 'inline' && tokens[j].content) {
|
|
179
|
+
currentContent.push('- ' + tokens[j].content);
|
|
180
|
+
contentFound = true;
|
|
181
|
+
} else if (tokens[j].type === 'list_item_close') {
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
} else if (currentSection && token.type === 'paragraph_open' && !inList) {
|
|
186
|
+
// Add blank line before paragraph (except first one or in lists)
|
|
187
|
+
if (currentContent.length > 0 && currentContent[currentContent.length - 1] !== '') {
|
|
188
|
+
currentContent.push('');
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Save last section
|
|
194
|
+
if (currentSection) {
|
|
195
|
+
const content = currentContent.join('\n').trim();
|
|
196
|
+
if (currentSection === 'userStories') {
|
|
197
|
+
sections[currentSection] = parseUserStories(content);
|
|
198
|
+
} else {
|
|
199
|
+
sections[currentSection] = content;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return sections;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Parse user stories from text
|
|
208
|
+
* Looks for "As a...", "I want...", "So that..." patterns
|
|
209
|
+
*
|
|
210
|
+
* @param {string} text - Text containing user stories
|
|
211
|
+
* @returns {Array<object>} Array of user story objects with raw text
|
|
212
|
+
*/
|
|
213
|
+
function parseUserStories(text) {
|
|
214
|
+
const stories = [];
|
|
215
|
+
const lines = text.split('\n');
|
|
216
|
+
|
|
217
|
+
let currentStory = null;
|
|
218
|
+
|
|
219
|
+
for (const line of lines) {
|
|
220
|
+
const trimmed = line.trim();
|
|
221
|
+
|
|
222
|
+
// Check if line starts a new user story (matches "As a" or "As an", with optional bold)
|
|
223
|
+
if (/^\*\*As an?\b/i.test(trimmed) || /^As an?\b/i.test(trimmed)) {
|
|
224
|
+
// Save previous story
|
|
225
|
+
if (currentStory) {
|
|
226
|
+
stories.push(currentStory);
|
|
227
|
+
}
|
|
228
|
+
// Start new story
|
|
229
|
+
currentStory = { raw: trimmed };
|
|
230
|
+
} else if (currentStory && trimmed) {
|
|
231
|
+
// Continue current story
|
|
232
|
+
currentStory.raw += '\n' + trimmed;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Save last story
|
|
237
|
+
if (currentStory) {
|
|
238
|
+
stories.push(currentStory);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return stories;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Build Epic body from PRD sections
|
|
246
|
+
*
|
|
247
|
+
* @param {object} sections - Extracted PRD sections
|
|
248
|
+
* @param {object} prdMeta - PRD frontmatter metadata
|
|
249
|
+
* @returns {string} Epic markdown body
|
|
250
|
+
*/
|
|
251
|
+
function buildEpicBody(sections, prdMeta) {
|
|
252
|
+
// Build user stories section
|
|
253
|
+
let userStoriesSection = '';
|
|
254
|
+
if (sections.userStories && sections.userStories.length > 0) {
|
|
255
|
+
userStoriesSection = sections.userStories
|
|
256
|
+
.map((s, i) => `${i + 1}. ${s.raw}`)
|
|
257
|
+
.join('\n\n');
|
|
258
|
+
} else {
|
|
259
|
+
userStoriesSection = 'To be defined from PRD user stories.';
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return `# Epic: ${prdMeta.title}
|
|
263
|
+
|
|
264
|
+
## Overview
|
|
265
|
+
|
|
266
|
+
${sections.overview || 'To be defined based on PRD.'}
|
|
267
|
+
|
|
268
|
+
## Technical Architecture
|
|
269
|
+
|
|
270
|
+
### Goals
|
|
271
|
+
${sections.goals || 'Extract from PRD goals and objectives.'}
|
|
272
|
+
|
|
273
|
+
### User Stories
|
|
274
|
+
${userStoriesSection}
|
|
275
|
+
|
|
276
|
+
## Implementation Tasks
|
|
277
|
+
|
|
278
|
+
Tasks will be created via epic decomposition.
|
|
279
|
+
|
|
280
|
+
## Dependencies
|
|
281
|
+
|
|
282
|
+
### Between Tasks
|
|
283
|
+
To be determined during task breakdown.
|
|
284
|
+
|
|
285
|
+
### External Dependencies
|
|
286
|
+
${sections.requirements ? 'See PRD requirements section.' : 'To be identified.'}
|
|
287
|
+
|
|
288
|
+
## Timeline
|
|
289
|
+
|
|
290
|
+
${sections.timeline || 'See PRD for timeline and milestones.'}
|
|
291
|
+
|
|
292
|
+
## Related Documents
|
|
293
|
+
|
|
294
|
+
- PRD: \`.claude/prds/${slugify(prdMeta.title)}.md\`
|
|
295
|
+
`;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Generate unique epic ID from PRD ID
|
|
300
|
+
*
|
|
301
|
+
* @param {string} prdId - PRD identifier (e.g., 'prd-347')
|
|
302
|
+
* @returns {string} Epic identifier (e.g., 'epic-347')
|
|
303
|
+
*/
|
|
304
|
+
function generateEpicId(prdId) {
|
|
305
|
+
// prd-347 → epic-347
|
|
306
|
+
const num = prdId.replace('prd-', '');
|
|
307
|
+
return `epic-${num}`;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Slugify title for directory/file names
|
|
312
|
+
* Converts to lowercase, removes special chars, replaces spaces with hyphens
|
|
313
|
+
*
|
|
314
|
+
* @param {string} text - Text to slugify
|
|
315
|
+
* @returns {string} Slugified text
|
|
316
|
+
*/
|
|
317
|
+
function slugify(text) {
|
|
318
|
+
return text
|
|
319
|
+
.toLowerCase()
|
|
320
|
+
.replace(/[^\w\s-]/g, '')
|
|
321
|
+
.replace(/\s+/g, '-')
|
|
322
|
+
.replace(/-+/g, '-');
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
module.exports = {
|
|
326
|
+
parseLocalPRD,
|
|
327
|
+
extractSections,
|
|
328
|
+
parseUserStories,
|
|
329
|
+
buildEpicBody,
|
|
330
|
+
generateEpicId,
|
|
331
|
+
slugify
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
// CLI execution
|
|
335
|
+
if (require.main === module) {
|
|
336
|
+
const args = process.argv.slice(2);
|
|
337
|
+
const prdId = args[0];
|
|
338
|
+
|
|
339
|
+
if (!prdId) {
|
|
340
|
+
console.error('Usage: pm-prd-parse-local.js <prd-id>');
|
|
341
|
+
process.exit(1);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
parseLocalPRD(prdId)
|
|
345
|
+
.then(epic => {
|
|
346
|
+
console.log('✅ Epic created successfully!');
|
|
347
|
+
console.log(`Epic ID: ${epic.epicId}`);
|
|
348
|
+
console.log(`Epic Path: ${epic.epicPath}`);
|
|
349
|
+
console.log(`\nSections extracted:`);
|
|
350
|
+
console.log(`- Overview: ${epic.sections.overview ? '✓' : '✗'}`);
|
|
351
|
+
console.log(`- Goals: ${epic.sections.goals ? '✓' : '✗'}`);
|
|
352
|
+
console.log(`- User Stories: ${epic.sections.userStories.length} found`);
|
|
353
|
+
console.log(`- Requirements: ${epic.sections.requirements ? '✓' : '✗'}`);
|
|
354
|
+
console.log(`- Timeline: ${epic.sections.timeline ? '✓' : '✗'}`);
|
|
355
|
+
})
|
|
356
|
+
.catch(err => {
|
|
357
|
+
console.error('❌ Error parsing PRD:', err.message);
|
|
358
|
+
process.exit(1);
|
|
359
|
+
});
|
|
360
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local PRD Show Command
|
|
3
|
+
*
|
|
4
|
+
* Displays a specific Product Requirements Document (PRD) by ID.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* /pm:prd-show --local <id>
|
|
8
|
+
*
|
|
9
|
+
* @module pm-prd-show-local
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const fs = require('fs').promises;
|
|
13
|
+
const path = require('path');
|
|
14
|
+
const { parseFrontmatter } = require('../lib/frontmatter');
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Shows a specific PRD by ID
|
|
18
|
+
*
|
|
19
|
+
* @param {string} id - PRD ID to display
|
|
20
|
+
* @returns {Promise<Object>} PRD data including frontmatter and body
|
|
21
|
+
* @throws {Error} If PRD not found or ID is invalid
|
|
22
|
+
*/
|
|
23
|
+
async function showLocalPRD(id) {
|
|
24
|
+
if (!id || typeof id !== 'string' || id.trim().length === 0) {
|
|
25
|
+
throw new Error('PRD ID is required');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const prdsDir = path.join(process.cwd(), '.claude', 'prds');
|
|
29
|
+
|
|
30
|
+
// Ensure directory exists
|
|
31
|
+
try {
|
|
32
|
+
await fs.access(prdsDir);
|
|
33
|
+
} catch (err) {
|
|
34
|
+
if (err.code === 'ENOENT') {
|
|
35
|
+
throw new Error(`PRD not found: ${id}`);
|
|
36
|
+
}
|
|
37
|
+
throw err;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Read all PRD files
|
|
41
|
+
const files = await fs.readdir(prdsDir);
|
|
42
|
+
|
|
43
|
+
// Search for PRD by ID
|
|
44
|
+
for (const file of files) {
|
|
45
|
+
if (!file.endsWith('.md')) continue;
|
|
46
|
+
|
|
47
|
+
const filepath = path.join(prdsDir, file);
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
const content = await fs.readFile(filepath, 'utf8');
|
|
51
|
+
const { frontmatter, body } = parseFrontmatter(content);
|
|
52
|
+
|
|
53
|
+
if (frontmatter && frontmatter.id === id) {
|
|
54
|
+
return {
|
|
55
|
+
filepath,
|
|
56
|
+
filename: file,
|
|
57
|
+
frontmatter,
|
|
58
|
+
body,
|
|
59
|
+
content
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
} catch (err) {
|
|
63
|
+
// Skip files that can't be parsed
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// PRD not found
|
|
69
|
+
throw new Error(`PRD not found: ${id}`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Formats PRD for display
|
|
74
|
+
*
|
|
75
|
+
* @param {Object} prd - PRD object from showLocalPRD
|
|
76
|
+
* @returns {string} Formatted PRD for display
|
|
77
|
+
*/
|
|
78
|
+
function formatPRD(prd) {
|
|
79
|
+
const { frontmatter, body } = prd;
|
|
80
|
+
|
|
81
|
+
const header = [
|
|
82
|
+
'',
|
|
83
|
+
`PRD: ${frontmatter.title}`,
|
|
84
|
+
`ID: ${frontmatter.id}`,
|
|
85
|
+
`Status: ${frontmatter.status}`,
|
|
86
|
+
`Priority: ${frontmatter.priority}`,
|
|
87
|
+
`Created: ${frontmatter.created}`,
|
|
88
|
+
`Author: ${frontmatter.author}`,
|
|
89
|
+
`Version: ${frontmatter.version}`,
|
|
90
|
+
'',
|
|
91
|
+
'─'.repeat(80),
|
|
92
|
+
''
|
|
93
|
+
].join('\n');
|
|
94
|
+
|
|
95
|
+
return header + body;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
module.exports = {
|
|
99
|
+
showLocalPRD,
|
|
100
|
+
formatPRD
|
|
101
|
+
};
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local PRD Update Command
|
|
3
|
+
*
|
|
4
|
+
* Updates frontmatter fields in a Product Requirements Document (PRD).
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* /pm:prd-update --local <id> <field> <value>
|
|
8
|
+
*
|
|
9
|
+
* @module pm-prd-update-local
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const fs = require('fs').promises;
|
|
13
|
+
const path = require('path');
|
|
14
|
+
const { parseFrontmatter, stringifyFrontmatter } = require('../lib/frontmatter');
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Updates a frontmatter field in a PRD
|
|
18
|
+
*
|
|
19
|
+
* @param {string} id - PRD ID
|
|
20
|
+
* @param {string} field - Frontmatter field to update
|
|
21
|
+
* @param {*} value - New value for the field
|
|
22
|
+
* @returns {Promise<Object>} Updated PRD data
|
|
23
|
+
* @throws {Error} If PRD not found or parameters invalid
|
|
24
|
+
*/
|
|
25
|
+
async function updateLocalPRD(id, field, value) {
|
|
26
|
+
// Validate parameters
|
|
27
|
+
if (!id || typeof id !== 'string' || id.trim().length === 0) {
|
|
28
|
+
throw new Error('PRD ID is required');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (!field || typeof field !== 'string' || field.trim().length === 0) {
|
|
32
|
+
throw new Error('Field is required');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const prdsDir = path.join(process.cwd(), '.claude', 'prds');
|
|
36
|
+
|
|
37
|
+
// Ensure directory exists
|
|
38
|
+
try {
|
|
39
|
+
await fs.access(prdsDir);
|
|
40
|
+
} catch (err) {
|
|
41
|
+
if (err.code === 'ENOENT') {
|
|
42
|
+
throw new Error(`PRD not found: ${id}`);
|
|
43
|
+
}
|
|
44
|
+
throw err;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Read all PRD files to find the target
|
|
48
|
+
const files = await fs.readdir(prdsDir);
|
|
49
|
+
|
|
50
|
+
for (const file of files) {
|
|
51
|
+
if (!file.endsWith('.md')) continue;
|
|
52
|
+
|
|
53
|
+
const filepath = path.join(prdsDir, file);
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
const content = await fs.readFile(filepath, 'utf8');
|
|
57
|
+
const { frontmatter, body } = parseFrontmatter(content);
|
|
58
|
+
|
|
59
|
+
if (frontmatter && frontmatter.id === id) {
|
|
60
|
+
// Update the field
|
|
61
|
+
const updatedFrontmatter = {
|
|
62
|
+
...frontmatter,
|
|
63
|
+
[field]: value
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
// Reconstruct the file
|
|
67
|
+
const updatedContent = stringifyFrontmatter(updatedFrontmatter, body);
|
|
68
|
+
|
|
69
|
+
// Write back to file
|
|
70
|
+
await fs.writeFile(filepath, updatedContent, 'utf8');
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
filepath,
|
|
74
|
+
filename: file,
|
|
75
|
+
frontmatter: updatedFrontmatter,
|
|
76
|
+
body
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
} catch (err) {
|
|
80
|
+
// Skip files that can't be parsed
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// PRD not found
|
|
86
|
+
throw new Error(`PRD not found: ${id}`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Updates multiple fields at once
|
|
91
|
+
*
|
|
92
|
+
* @param {string} id - PRD ID
|
|
93
|
+
* @param {Object} updates - Object with field: value pairs
|
|
94
|
+
* @returns {Promise<Object>} Updated PRD data
|
|
95
|
+
*/
|
|
96
|
+
async function updateMultipleFields(id, updates) {
|
|
97
|
+
// Validate parameters
|
|
98
|
+
if (!id || typeof id !== 'string' || id.trim().length === 0) {
|
|
99
|
+
throw new Error('PRD ID is required');
|
|
100
|
+
}
|
|
101
|
+
if (!updates || typeof updates !== 'object') {
|
|
102
|
+
throw new Error('Updates must be an object');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const prdsDir = path.join(process.cwd(), '.claude', 'prds');
|
|
106
|
+
let files;
|
|
107
|
+
try {
|
|
108
|
+
files = await fs.readdir(prdsDir);
|
|
109
|
+
} catch (err) {
|
|
110
|
+
if (err.code === 'ENOENT') {
|
|
111
|
+
throw new Error(`PRD not found: ${id}`);
|
|
112
|
+
}
|
|
113
|
+
throw err;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
for (const file of files) {
|
|
117
|
+
if (!file.endsWith('.md')) continue;
|
|
118
|
+
|
|
119
|
+
const filepath = path.join(prdsDir, file);
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
const content = await fs.readFile(filepath, 'utf8');
|
|
123
|
+
const { frontmatter, body } = parseFrontmatter(content);
|
|
124
|
+
|
|
125
|
+
if (frontmatter && frontmatter.id === id) {
|
|
126
|
+
// Apply all updates
|
|
127
|
+
const updatedFrontmatter = {
|
|
128
|
+
...frontmatter,
|
|
129
|
+
...updates
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const updatedContent = stringifyFrontmatter(updatedFrontmatter, body);
|
|
133
|
+
await fs.writeFile(filepath, updatedContent, 'utf8');
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
filepath,
|
|
137
|
+
filename: file,
|
|
138
|
+
frontmatter: updatedFrontmatter,
|
|
139
|
+
body
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
} catch (err) {
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
throw new Error(`PRD not found: ${id}`);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
module.exports = {
|
|
151
|
+
updateLocalPRD,
|
|
152
|
+
updateMultipleFields
|
|
153
|
+
};
|