claude-autopm 1.25.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/README.md +111 -0
- 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/commands/pm/epicStatus.js +263 -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,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local PRD Listing Command
|
|
3
|
+
*
|
|
4
|
+
* Lists all Product Requirements Documents (PRDs) in local mode.
|
|
5
|
+
* Supports filtering and sorting.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* /pm:prd-list --local
|
|
9
|
+
* /pm:prd-list --local --status approved
|
|
10
|
+
*
|
|
11
|
+
* @module pm-prd-list-local
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const fs = require('fs').promises;
|
|
15
|
+
const path = require('path');
|
|
16
|
+
const { parseFrontmatter } = require('../lib/frontmatter');
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Lists all local PRDs with optional filtering
|
|
20
|
+
*
|
|
21
|
+
* @param {Object} options - Listing options
|
|
22
|
+
* @param {string} options.status - Filter by status (draft, approved, etc.)
|
|
23
|
+
* @returns {Promise<Array>} Array of PRD metadata objects
|
|
24
|
+
*/
|
|
25
|
+
async function listLocalPRDs(options = {}) {
|
|
26
|
+
const prdsDir = path.join(process.cwd(), '.claude', 'prds');
|
|
27
|
+
|
|
28
|
+
// Ensure directory exists
|
|
29
|
+
try {
|
|
30
|
+
await fs.access(prdsDir);
|
|
31
|
+
} catch (err) {
|
|
32
|
+
if (err.code === 'ENOENT') {
|
|
33
|
+
return []; // No PRDs directory = no PRDs
|
|
34
|
+
}
|
|
35
|
+
throw err;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Read directory
|
|
39
|
+
const files = await fs.readdir(prdsDir);
|
|
40
|
+
const mdFiles = files.filter(f => f.endsWith('.md'));
|
|
41
|
+
|
|
42
|
+
// Parallelize file reading/parsing with Promise.allSettled
|
|
43
|
+
const prdPromises = mdFiles.map(async (file) => {
|
|
44
|
+
try {
|
|
45
|
+
const filepath = path.join(prdsDir, file);
|
|
46
|
+
const content = await fs.readFile(filepath, 'utf8');
|
|
47
|
+
const { frontmatter } = parseFrontmatter(content);
|
|
48
|
+
// Only include files with valid frontmatter containing required fields
|
|
49
|
+
// A valid PRD must have at least an 'id' field
|
|
50
|
+
if (frontmatter && typeof frontmatter === 'object' && frontmatter.id) {
|
|
51
|
+
return {
|
|
52
|
+
filename: file,
|
|
53
|
+
...frontmatter
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
} catch (err) {
|
|
57
|
+
// Skip files that can't be parsed
|
|
58
|
+
console.warn(`Warning: Could not parse ${file}:`, err.message);
|
|
59
|
+
}
|
|
60
|
+
return null;
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const settled = await Promise.allSettled(prdPromises);
|
|
64
|
+
const prds = settled
|
|
65
|
+
.filter(r => r.status === 'fulfilled' && r.value)
|
|
66
|
+
.map(r => r.value);
|
|
67
|
+
// Filter by status if specified
|
|
68
|
+
let filtered = prds;
|
|
69
|
+
if (options.status) {
|
|
70
|
+
filtered = filtered.filter(p => p.status === options.status);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Sort by creation timestamp (newest first)
|
|
74
|
+
filtered.sort((a, b) => {
|
|
75
|
+
// Use createdAt if available (full timestamp), fallback to created (date only)
|
|
76
|
+
const dateA = new Date(a.createdAt || a.created || 0);
|
|
77
|
+
const dateB = new Date(b.createdAt || b.created || 0);
|
|
78
|
+
return dateB - dateA;
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
return filtered;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Formats PRD list for display
|
|
86
|
+
*
|
|
87
|
+
* @param {Array} prds - Array of PRD objects
|
|
88
|
+
* @returns {string} Formatted string for display
|
|
89
|
+
*/
|
|
90
|
+
function formatPRDList(prds) {
|
|
91
|
+
if (prds.length === 0) {
|
|
92
|
+
return 'No PRDs found.';
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const lines = ['', 'Local PRDs:', ''];
|
|
96
|
+
|
|
97
|
+
prds.forEach((prd, index) => {
|
|
98
|
+
lines.push(
|
|
99
|
+
`${index + 1}. [${prd.id}] ${prd.title}`,
|
|
100
|
+
` Status: ${prd.status} | Priority: ${prd.priority} | Created: ${prd.created}`,
|
|
101
|
+
''
|
|
102
|
+
);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
return lines.join('\n');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
module.exports = {
|
|
109
|
+
listLocalPRDs,
|
|
110
|
+
formatPRDList
|
|
111
|
+
};
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local PRD Creation Command
|
|
3
|
+
*
|
|
4
|
+
* Creates a new Product Requirements Document (PRD) in local mode.
|
|
5
|
+
* PRDs are stored in .claude/prds/ directory.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* /pm:prd-new --local "Feature Name"
|
|
9
|
+
*
|
|
10
|
+
* @module pm-prd-new-local
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const fs = require('fs').promises;
|
|
14
|
+
const path = require('path');
|
|
15
|
+
const { stringifyFrontmatter } = require('../lib/frontmatter');
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Creates a new PRD in the local .claude/prds/ directory
|
|
19
|
+
*
|
|
20
|
+
* @param {string} name - PRD title/name
|
|
21
|
+
* @param {Object} options - Optional configuration
|
|
22
|
+
* @param {string} options.id - Custom PRD ID (auto-generated if not provided)
|
|
23
|
+
* @param {string} options.author - Author name (default: 'ClaudeAutoPM')
|
|
24
|
+
* @param {string} options.priority - Priority level (default: 'medium')
|
|
25
|
+
* @returns {Promise<Object>} Created PRD metadata
|
|
26
|
+
* @throws {Error} If name is invalid or PRD already exists
|
|
27
|
+
*/
|
|
28
|
+
async function createLocalPRD(name, options = {}) {
|
|
29
|
+
// Validate name
|
|
30
|
+
if (!name || typeof name !== 'string' || name.trim().length === 0) {
|
|
31
|
+
throw new Error('PRD name is required and must be a non-empty string');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const sanitizedName = name.trim();
|
|
35
|
+
|
|
36
|
+
// Generate unique ID
|
|
37
|
+
const id = options.id || generatePRDId();
|
|
38
|
+
|
|
39
|
+
// Create frontmatter
|
|
40
|
+
const now = new Date();
|
|
41
|
+
const frontmatter = {
|
|
42
|
+
id,
|
|
43
|
+
title: sanitizedName,
|
|
44
|
+
created: now.toISOString().split('T')[0],
|
|
45
|
+
createdAt: now.toISOString(), // Include full timestamp for sorting
|
|
46
|
+
author: options.author || 'ClaudeAutoPM',
|
|
47
|
+
status: 'draft',
|
|
48
|
+
priority: options.priority || 'medium',
|
|
49
|
+
version: '1.0'
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// Create PRD body template
|
|
53
|
+
const body = createPRDTemplate(sanitizedName);
|
|
54
|
+
|
|
55
|
+
// Generate full markdown content
|
|
56
|
+
const content = stringifyFrontmatter(frontmatter, body);
|
|
57
|
+
|
|
58
|
+
// Generate filename (sanitize name + add ID for uniqueness)
|
|
59
|
+
const filename = `${id}-${sanitizeFilename(sanitizedName)}`;
|
|
60
|
+
const filepath = path.join(process.cwd(), '.claude', 'prds', filename);
|
|
61
|
+
|
|
62
|
+
// Check if file already exists
|
|
63
|
+
try {
|
|
64
|
+
await fs.access(filepath);
|
|
65
|
+
throw new Error(`PRD already exists: ${filename}`);
|
|
66
|
+
} catch (err) {
|
|
67
|
+
if (err.code !== 'ENOENT') {
|
|
68
|
+
throw err;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Ensure directory exists
|
|
73
|
+
await fs.mkdir(path.dirname(filepath), { recursive: true });
|
|
74
|
+
|
|
75
|
+
// Write to file
|
|
76
|
+
await fs.writeFile(filepath, content, 'utf8');
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
id,
|
|
80
|
+
filepath,
|
|
81
|
+
frontmatter
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Creates a PRD template with standard sections
|
|
87
|
+
*
|
|
88
|
+
* @param {string} name - PRD title
|
|
89
|
+
* @returns {string} PRD template content
|
|
90
|
+
*/
|
|
91
|
+
function createPRDTemplate(name) {
|
|
92
|
+
return `# Product Requirements Document: ${name}
|
|
93
|
+
|
|
94
|
+
## 1. Executive Summary
|
|
95
|
+
|
|
96
|
+
### Overview
|
|
97
|
+
[Describe the feature/product in 2-3 sentences]
|
|
98
|
+
|
|
99
|
+
### Business Value
|
|
100
|
+
[Why is this important?]
|
|
101
|
+
|
|
102
|
+
### Success Metrics
|
|
103
|
+
[How will we measure success?]
|
|
104
|
+
|
|
105
|
+
## 2. Background
|
|
106
|
+
|
|
107
|
+
### Problem Statement
|
|
108
|
+
[What problem are we solving?]
|
|
109
|
+
|
|
110
|
+
### Current State
|
|
111
|
+
[What exists today?]
|
|
112
|
+
|
|
113
|
+
### Goals and Objectives
|
|
114
|
+
[What are we trying to achieve?]
|
|
115
|
+
|
|
116
|
+
## 3. User Stories
|
|
117
|
+
|
|
118
|
+
[Epic-level user stories]
|
|
119
|
+
|
|
120
|
+
## 4. Functional Requirements
|
|
121
|
+
|
|
122
|
+
[Detailed requirements]
|
|
123
|
+
|
|
124
|
+
## 5. Non-Functional Requirements
|
|
125
|
+
|
|
126
|
+
[Performance, security, etc.]
|
|
127
|
+
|
|
128
|
+
## 6. Out of Scope
|
|
129
|
+
|
|
130
|
+
[What we're NOT doing]
|
|
131
|
+
|
|
132
|
+
## 7. Timeline
|
|
133
|
+
|
|
134
|
+
[Key milestones]
|
|
135
|
+
`;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Counter for ensuring unique IDs even when created at the same millisecond
|
|
139
|
+
let idCounter = 0;
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Generates a unique PRD ID
|
|
143
|
+
*
|
|
144
|
+
* Format: prd-XXX (3 digits from timestamp + counter)
|
|
145
|
+
*
|
|
146
|
+
* @returns {string} Generated PRD ID
|
|
147
|
+
*/
|
|
148
|
+
function generatePRDId() {
|
|
149
|
+
const timestamp = Date.now();
|
|
150
|
+
const random = Math.floor(Math.random() * 900) + 100; // 100-999
|
|
151
|
+
const counter = (idCounter++) % 10; // 0-9 cycling counter
|
|
152
|
+
|
|
153
|
+
// Use last digit of timestamp + last 2 digits of random = 3 digits total
|
|
154
|
+
const suffix = (timestamp % 10).toString() +
|
|
155
|
+
(Math.floor(random / 10) % 10).toString() +
|
|
156
|
+
counter.toString();
|
|
157
|
+
|
|
158
|
+
return `prd-${suffix}`;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Sanitizes a PRD name for use as a filename
|
|
163
|
+
*
|
|
164
|
+
* - Converts to lowercase
|
|
165
|
+
* - Replaces spaces with hyphens
|
|
166
|
+
* - Removes special characters except hyphens
|
|
167
|
+
* - Truncates to safe length (max 100 chars before .md)
|
|
168
|
+
* - Adds .md extension
|
|
169
|
+
*
|
|
170
|
+
* @param {string} name - PRD name to sanitize
|
|
171
|
+
* @returns {string} Sanitized filename
|
|
172
|
+
*/
|
|
173
|
+
function sanitizeFilename(name) {
|
|
174
|
+
const sanitized = name
|
|
175
|
+
.toLowerCase()
|
|
176
|
+
.replace(/\s+/g, '-') // Replace spaces with hyphens
|
|
177
|
+
.replace(/[^a-z0-9-]/g, '') // Remove special characters
|
|
178
|
+
.replace(/-+/g, '-') // Collapse multiple hyphens
|
|
179
|
+
.replace(/^-|-$/g, ''); // Remove leading/trailing hyphens
|
|
180
|
+
|
|
181
|
+
// Truncate to safe length (100 chars + .md = 103 total)
|
|
182
|
+
// With prd-XXX- prefix (8 chars), max base name is ~92 chars
|
|
183
|
+
const maxLength = 92;
|
|
184
|
+
const truncated = sanitized.length > maxLength
|
|
185
|
+
? sanitized.substring(0, maxLength)
|
|
186
|
+
: sanitized;
|
|
187
|
+
|
|
188
|
+
return truncated + '.md';
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
module.exports = {
|
|
192
|
+
createLocalPRD,
|
|
193
|
+
createPRDTemplate,
|
|
194
|
+
generatePRDId,
|
|
195
|
+
sanitizeFilename
|
|
196
|
+
};
|
|
@@ -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
|
+
}
|