@lusipad/pmspec 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/README.md +306 -0
  2. package/README.zh.md +304 -0
  3. package/bin/pmspec.js +5 -0
  4. package/dist/cli/index.d.ts +3 -0
  5. package/dist/cli/index.js +39 -0
  6. package/dist/commands/analyze.d.ts +4 -0
  7. package/dist/commands/analyze.js +240 -0
  8. package/dist/commands/breakdown.d.ts +4 -0
  9. package/dist/commands/breakdown.js +194 -0
  10. package/dist/commands/create.d.ts +4 -0
  11. package/dist/commands/create.js +529 -0
  12. package/dist/commands/history.d.ts +4 -0
  13. package/dist/commands/history.js +213 -0
  14. package/dist/commands/import.d.ts +4 -0
  15. package/dist/commands/import.js +196 -0
  16. package/dist/commands/index-legacy.d.ts +4 -0
  17. package/dist/commands/index-legacy.js +27 -0
  18. package/dist/commands/init.d.ts +3 -0
  19. package/dist/commands/init.js +60 -0
  20. package/dist/commands/list.d.ts +3 -0
  21. package/dist/commands/list.js +127 -0
  22. package/dist/commands/search.d.ts +7 -0
  23. package/dist/commands/search.js +183 -0
  24. package/dist/commands/serve.d.ts +3 -0
  25. package/dist/commands/serve.js +68 -0
  26. package/dist/commands/show.d.ts +3 -0
  27. package/dist/commands/show.js +152 -0
  28. package/dist/commands/simple.d.ts +7 -0
  29. package/dist/commands/simple.js +360 -0
  30. package/dist/commands/update.d.ts +4 -0
  31. package/dist/commands/update.js +247 -0
  32. package/dist/commands/validate.d.ts +3 -0
  33. package/dist/commands/validate.js +74 -0
  34. package/dist/core/changelog-service.d.ts +88 -0
  35. package/dist/core/changelog-service.js +208 -0
  36. package/dist/core/changelog.d.ts +113 -0
  37. package/dist/core/changelog.js +147 -0
  38. package/dist/core/importers.d.ts +343 -0
  39. package/dist/core/importers.js +715 -0
  40. package/dist/core/parser.d.ts +50 -0
  41. package/dist/core/parser.js +246 -0
  42. package/dist/core/project.d.ts +155 -0
  43. package/dist/core/project.js +138 -0
  44. package/dist/core/search.d.ts +119 -0
  45. package/dist/core/search.js +299 -0
  46. package/dist/core/simple-model.d.ts +54 -0
  47. package/dist/core/simple-model.js +20 -0
  48. package/dist/core/team.d.ts +41 -0
  49. package/dist/core/team.js +57 -0
  50. package/dist/core/workload.d.ts +49 -0
  51. package/dist/core/workload.js +116 -0
  52. package/dist/index.d.ts +15 -0
  53. package/dist/index.js +11 -0
  54. package/dist/utils/csv-handler.d.ts +15 -0
  55. package/dist/utils/csv-handler.js +224 -0
  56. package/dist/utils/markdown.d.ts +43 -0
  57. package/dist/utils/markdown.js +202 -0
  58. package/dist/utils/validation.d.ts +35 -0
  59. package/dist/utils/validation.js +178 -0
  60. package/package.json +71 -0
@@ -0,0 +1,360 @@
1
+ import { Command } from 'commander';
2
+ import chalk from 'chalk';
3
+ import { CSVHandler } from '../utils/csv-handler.js';
4
+ const simpleCommand = new Command('simple')
5
+ .description('简化的项目管理 - 只维护一个功能表')
6
+ .option('--format <format>', '输出格式: csv, md', 'csv')
7
+ .option('--output <file>', '输出文件路径', 'features')
8
+ .action(async (options, command) => {
9
+ try {
10
+ const filePath = `${options.output}.${options.format}`;
11
+ if (options.format === 'csv') {
12
+ await createCSVTemplate(filePath);
13
+ }
14
+ else if (options.format === 'md') {
15
+ await createMarkdownTemplate(filePath);
16
+ }
17
+ else {
18
+ console.error(chalk.red('错误: 格式必须是 csv 或 md'));
19
+ process.exit(1);
20
+ }
21
+ console.log(chalk.green(`✓ 已创建模板文件: ${filePath}`));
22
+ console.log(chalk.yellow('请编辑文件添加功能,然后使用 AI 生成项目结构'));
23
+ }
24
+ catch (error) {
25
+ console.error(chalk.red('错误:'), error.message);
26
+ process.exit(1);
27
+ }
28
+ });
29
+ // 生成命令
30
+ const generateCommand = new Command('generate')
31
+ .description('从功能表生成项目结构')
32
+ .option('--input <file>', '功能表文件路径', 'features.csv')
33
+ .option('--output <dir>', '输出目录', 'generated')
34
+ .option('--interactive', '交互式 AI 生成')
35
+ .action(async (options, command) => {
36
+ try {
37
+ const features = await CSVHandler.readFeatures(options.input);
38
+ if (features.length === 0) {
39
+ console.log(chalk.yellow('警告: 没有找到功能数据'));
40
+ return;
41
+ }
42
+ console.log(chalk.blue(`📊 读取到 ${features.length} 个功能`));
43
+ // 显示功能统计
44
+ displayFeaturesSummary(features);
45
+ if (options.interactive) {
46
+ // 生成 AI prompt
47
+ await generateAIPrompt(features, options.input);
48
+ }
49
+ else {
50
+ // 简单的本地生成(不使用 AI)
51
+ await generateProjectStructure(features, options.output);
52
+ }
53
+ }
54
+ catch (error) {
55
+ console.error(chalk.red('错误:'), error.message);
56
+ process.exit(1);
57
+ }
58
+ });
59
+ // 列表命令
60
+ const listCommand = new Command('list')
61
+ .description('显示功能列表')
62
+ .option('--input <file>', '功能表文件路径', 'features.csv')
63
+ .option('--assignee <name>', '按分配人筛选')
64
+ .option('--status <status>', '按状态筛选')
65
+ .option('--priority <priority>', '按优先级筛选')
66
+ .action(async (options, command) => {
67
+ try {
68
+ const features = await CSVHandler.readFeatures(options.input);
69
+ let filteredFeatures = features;
70
+ if (options.assignee) {
71
+ filteredFeatures = filteredFeatures.filter(f => f.assignee.toLowerCase().includes(options.assignee.toLowerCase()));
72
+ }
73
+ if (options.status) {
74
+ filteredFeatures = filteredFeatures.filter(f => f.status === options.status);
75
+ }
76
+ if (options.priority) {
77
+ filteredFeatures = filteredFeatures.filter(f => f.priority === options.priority);
78
+ }
79
+ if (filteredFeatures.length === 0) {
80
+ console.log(chalk.yellow('没有找到匹配的功能'));
81
+ return;
82
+ }
83
+ displayFeaturesTable(filteredFeatures);
84
+ }
85
+ catch (error) {
86
+ console.error(chalk.red('错误:'), error.message);
87
+ process.exit(1);
88
+ }
89
+ });
90
+ // 统计命令
91
+ const statsCommand = new Command('stats')
92
+ .description('显示项目统计信息')
93
+ .option('--input <file>', '功能表文件路径', 'features.csv')
94
+ .action(async (options, command) => {
95
+ try {
96
+ const features = await CSVHandler.readFeatures(options.input);
97
+ if (features.length === 0) {
98
+ console.log(chalk.yellow('没有功能数据'));
99
+ return;
100
+ }
101
+ displayStatistics(features);
102
+ }
103
+ catch (error) {
104
+ console.error(chalk.red('错误:'), error.message);
105
+ process.exit(1);
106
+ }
107
+ });
108
+ function displayFeaturesTable(features) {
109
+ console.log(chalk.blue.bold('\n📋 功能列表\n'));
110
+ // 计算列宽
111
+ const maxWidths = {
112
+ id: Math.max(8, ...features.map(f => f.id.length)),
113
+ name: Math.max(8, ...features.map(f => f.name.length)),
114
+ assignee: Math.max(6, ...features.map(f => f.assignee.length)),
115
+ priority: 8,
116
+ status: 8,
117
+ estimate: 8
118
+ };
119
+ // 表头
120
+ const header = `│ ${pad('ID', maxWidths.id)} │ ${pad('功能名称', maxWidths.name)} │ ${pad('分配给', maxWidths.assignee)} │ ${pad('优先级', maxWidths.priority)} │ ${pad('状态', maxWidths.status)} │ ${pad('工时', maxWidths.estimate)} │`;
121
+ const separator = `├${pad('', maxWidths.id + 2, '─')}┼${pad('', maxWidths.name + 2, '─')}┼${pad('', maxWidths.assignee + 2, '─')}┼${pad('', maxWidths.priority + 2, '─')}┼${pad('', maxWidths.status + 2, '─')}┼${pad('', maxWidths.estimate + 2, '─')}┤`;
122
+ console.log(header);
123
+ console.log(separator);
124
+ // 数据行
125
+ for (const feature of features) {
126
+ const priorityColor = getPriorityColor(feature.priority);
127
+ const statusColor = getStatusColor(feature.status);
128
+ const row = `│ ${pad(feature.id, maxWidths.id)} │ ${pad(feature.name, maxWidths.name)} │ ${pad(feature.assignee, maxWidths.assignee)} │ ${priorityColor(pad(feature.priority, maxWidths.priority))} │ ${statusColor(pad(feature.status, maxWidths.status))} │ ${pad(feature.estimate + 'h', maxWidths.estimate)} │`;
129
+ console.log(row);
130
+ }
131
+ console.log(`\n总计: ${features.length} 个功能`);
132
+ }
133
+ function displayStatistics(features) {
134
+ console.log(chalk.blue.bold('\n📊 项目统计\n'));
135
+ // 基础统计
136
+ const totalFeatures = features.length;
137
+ const totalHours = features.reduce((sum, f) => sum + f.estimate, 0);
138
+ const completedFeatures = features.filter(f => f.status === 'done').length;
139
+ const completionRate = ((completedFeatures / totalFeatures) * 100).toFixed(1);
140
+ console.log(`总功能数: ${totalFeatures}`);
141
+ console.log(`预估总工时: ${totalHours}h`);
142
+ console.log(`已完成: ${completedFeatures} (${completionRate}%)`);
143
+ // 按状态统计
144
+ console.log(chalk.yellow('\n按状态统计:'));
145
+ const statusStats = new Map();
146
+ features.forEach(f => {
147
+ statusStats.set(f.status, (statusStats.get(f.status) || 0) + 1);
148
+ });
149
+ for (const [status, count] of statusStats) {
150
+ const color = getStatusColor(status);
151
+ console.log(`${color(status)}: ${count}`);
152
+ }
153
+ // 按优先级统计
154
+ console.log(chalk.yellow('\n按优先级统计:'));
155
+ const priorityStats = new Map();
156
+ features.forEach(f => {
157
+ priorityStats.set(f.priority, (priorityStats.get(f.priority) || 0) + 1);
158
+ });
159
+ for (const [priority, count] of priorityStats) {
160
+ const color = getPriorityColor(priority);
161
+ console.log(`${color(priority)}: ${count}`);
162
+ }
163
+ // 按人员统计
164
+ console.log(chalk.yellow('\n按人员统计:'));
165
+ const assigneeStats = new Map();
166
+ features.forEach(f => {
167
+ const current = assigneeStats.get(f.assignee) || { count: 0, hours: 0 };
168
+ assigneeStats.set(f.assignee, {
169
+ count: current.count + 1,
170
+ hours: current.hours + f.estimate
171
+ });
172
+ });
173
+ for (const [assignee, stats] of assigneeStats) {
174
+ console.log(`${assignee}: ${stats.count} 个功能, ${stats.hours}h`);
175
+ }
176
+ }
177
+ function pad(text, width, fillChar = ' ') {
178
+ return (text + fillChar.repeat(width)).slice(0, width);
179
+ }
180
+ function getPriorityColor(priority) {
181
+ switch (priority) {
182
+ case 'critical': return (text) => chalk.red(text);
183
+ case 'high': return (text) => chalk.yellow(text);
184
+ case 'medium': return (text) => chalk.blue(text);
185
+ case 'low': return (text) => chalk.gray(text);
186
+ default: return (text) => text;
187
+ }
188
+ }
189
+ function getStatusColor(status) {
190
+ switch (status) {
191
+ case 'done': return (text) => chalk.green(text);
192
+ case 'in-progress': return (text) => chalk.blue(text);
193
+ case 'blocked': return (text) => chalk.red(text);
194
+ case 'todo': return (text) => chalk.gray(text);
195
+ default: return (text) => text;
196
+ }
197
+ }
198
+ async function createCSVTemplate(filePath) {
199
+ const template = `ID,功能名称,描述,预估工作量(h),分配给,优先级,状态,分组,标签,创建日期,截止日期
200
+ feat-001,用户登录功能,实现用户登录、注册和密码重置功能,16,Alice,high,todo,认证,React;Node.js,2024-01-01,2024-01-15
201
+ feat-002,商品展示功能,展示商品列表、详情和搜索功能,20,Bob,medium,todo,电商,React;CSS,2024-01-01,2024-01-20`;
202
+ await CSVHandler.writeFeatures(filePath, CSVHandler.parseCSV(template));
203
+ }
204
+ async function createMarkdownTemplate(filePath) {
205
+ const template = `# 功能列表
206
+
207
+ # Feature: 用户登录功能
208
+ - **ID**: feat-001
209
+ - **描述**: 实现用户登录、注册和密码重置功能
210
+ - **预估工作量**: 16h
211
+ - **分配给**: Alice
212
+ - **优先级**: high
213
+ - **状态**: todo
214
+ - **分组**: 认证
215
+ - **标签**: React, Node.js
216
+ - **创建日期**: 2024-01-01
217
+ - **截止日期**: 2024-01-15
218
+
219
+ ---
220
+
221
+ # Feature: 商品展示功能
222
+ - **ID**: feat-002
223
+ - **描述**: 展示商品列表、详情和搜索功能
224
+ - **预估工作量**: 20h
225
+ - **分配给**: Bob
226
+ - **优先级**: medium
227
+ - **状态**: todo
228
+ - **分组**: 电商
229
+ - **标签**: React, CSS
230
+ - **创建��期**: 2024-01-01
231
+ - **截止日期**: 2024-01-20`;
232
+ await CSVHandler.writeFeatures(filePath, CSVHandler.parseMarkdown(template));
233
+ }
234
+ function displayFeaturesSummary(features) {
235
+ console.log(chalk.blue('\n📋 功能概览\n'));
236
+ // 基础统计
237
+ const totalHours = features.reduce((sum, f) => sum + f.estimate, 0);
238
+ const categories = new Set(features.map(f => f.category).filter(Boolean));
239
+ const assignees = new Set(features.map(f => f.assignee));
240
+ console.log(`总功能数: ${features.length}`);
241
+ console.log(`总预估工时: ${totalHours}h`);
242
+ console.log(`涉及领域: ${categories.size} 个`);
243
+ console.log(`涉及人员: ${assignees.size} 个`);
244
+ // 按分组显示
245
+ console.log(chalk.yellow('\n按分组统计:'));
246
+ const categoryStats = new Map();
247
+ features.forEach(f => {
248
+ if (f.category) {
249
+ if (!categoryStats.has(f.category)) {
250
+ categoryStats.set(f.category, []);
251
+ }
252
+ categoryStats.get(f.category).push(f);
253
+ }
254
+ });
255
+ for (const [category, categoryFeatures] of categoryStats) {
256
+ const categoryHours = categoryFeatures.reduce((sum, f) => sum + f.estimate, 0);
257
+ console.log(`${category}: ${categoryFeatures.length} 个功能, ${categoryHours}h`);
258
+ }
259
+ // 未分类的功能
260
+ const uncategorized = features.filter(f => !f.category);
261
+ if (uncategorized.length > 0) {
262
+ const uncategorizedHours = uncategorized.reduce((sum, f) => sum + f.estimate, 0);
263
+ console.log(`未分类: ${uncategorized.length} 个功能, ${uncategorizedHours}h`);
264
+ }
265
+ }
266
+ async function generateAIPrompt(features, inputFile) {
267
+ const featuresData = features.map(f => `${f.id},${f.name},${f.description},${f.estimate}h,${f.assignee},${f.priority},${f.status},${f.category || ''},${f.tags.join(';')},${f.createdDate || ''},${f.dueDate || ''}`).join('\n');
268
+ const prompt = `# PMSpec 项目结构生成
269
+
270
+ 根据功能表自动生成完整的项目结构,包括 Epic 分组、User Stories、技术文档等。
271
+
272
+ ## 功能表数据
273
+
274
+ ${featuresData}
275
+
276
+ ## 输出格式
277
+
278
+ 请根据功能表生成以下内容:
279
+
280
+ ### 1. Epic 分组
281
+ 将相关功能分组为 Epic,每组功能形成一个 Epic 文件。
282
+
283
+ ### 2. User Stories
284
+ 为每个 Feature 生成详细的 User Stories。
285
+
286
+ ### 3. 项目文档
287
+ - 项目概览
288
+ - 团队结构
289
+ - 时间线规划
290
+
291
+ ## 生成指导原则
292
+
293
+ 1. **Epic 分组原则**:
294
+ - 按业务领域或功能模块分组
295
+ - 每个 Epic 包含 2-5 个相关 Feature
296
+ - Epic 名称要体现业务价值
297
+
298
+ 2. **User Story 原则**:
299
+ - 每个 Feature 生成 2-4 个 User Stories
300
+ - Story 要体现用户价值
301
+ - 估算工时要合理(1-8 小时)
302
+
303
+ 3. **技术文档原则**:
304
+ - 基于功能的技术要求生成技能列表
305
+ - 考虑依赖关系和实施顺序
306
+ - 提供清晰的验收标准
307
+
308
+ 请按照以下格式输出:
309
+
310
+ \`\`\`markdown
311
+ ## Epic 分组结果
312
+
313
+ ### Epic: [Epic 名称]
314
+ - **包含功能**: [功能1, 功能2]
315
+ - **预估工时**: [总工时]h
316
+ - **业务价值**: [描述]
317
+
318
+ ## 详细 User Stories
319
+
320
+ ### [Feature 名称]
321
+ - **Epic**: [所属 Epic]
322
+ - **User Stories**:
323
+ - STORY-XXX: As [用户], I want [功能] so that [价值] ([工时]h)
324
+ - STORY-XXX: As [用户], I want [功能] so that [价值] ([工时]h)
325
+
326
+ ## 技术分析
327
+
328
+ ### 技能需求
329
+ - [技能1]: [需要的人数]
330
+ - [技能2]: [需要的人数]
331
+
332
+ ### 实施建议
333
+ 1. [实施步骤1]
334
+ 2. [实施步骤2]
335
+ 3. [实施步骤3]
336
+
337
+ ### 风险提示
338
+ - [风险1]: [影响]
339
+ - [风险2]: [影响]
340
+ \`\`\``;
341
+ console.log(chalk.yellow('\n🤖 AI 生成提示已准备'));
342
+ console.log(chalk.cyan('─'.repeat(50)));
343
+ console.log(chalk.blue('请在 Claude 中运行以下命令:'));
344
+ console.log(chalk.cyan('/pmspec-generate'));
345
+ console.log(chalk.cyan('然后将上面的提示内容粘贴到 Claude 中'));
346
+ console.log(chalk.cyan('─'.repeat(50)));
347
+ console.log(chalk.yellow('\n💡 提示:'));
348
+ console.log(chalk.gray('1. 你可以将上面的提示内容保存到文件中'));
349
+ console.log(chalk.gray('2. AI 生成完成后,可以手动或使用脚本创建文件'));
350
+ console.log(chalk.gray('3. 建议先在小范围内测试生成的结构'));
351
+ }
352
+ async function generateProjectStructure(features, outputDir) {
353
+ // 简单的本地生成逻辑(不使用 AI)
354
+ console.log(chalk.yellow('\n📝 本地生成模式(非 AI)'));
355
+ // 这里可以实现简单的规则生成逻辑
356
+ // 但主要还是推荐使用 AI 生成
357
+ console.log(chalk.gray('提示:使用 --interactive 选项可以获得更好的 AI 生成结果'));
358
+ }
359
+ export { simpleCommand, generateCommand, listCommand, statsCommand };
360
+ //# sourceMappingURL=simple.js.map
@@ -0,0 +1,4 @@
1
+ import { Command } from 'commander';
2
+ declare const updateCommand: Command;
3
+ export { updateCommand };
4
+ //# sourceMappingURL=update.d.ts.map
@@ -0,0 +1,247 @@
1
+ import { Command } from 'commander';
2
+ import chalk from 'chalk';
3
+ import { readEpicFile, readFeatureFile } from '../core/parser.js';
4
+ import { writeEpicFile, writeFeatureFile } from '../utils/markdown.js';
5
+ import { EpicStatus, FeatureStatus, StoryStatus } from '../core/project.js';
6
+ import { join } from 'path';
7
+ import { getChangelogService } from '../core/changelog-service.js';
8
+ const updateCommand = new Command('update')
9
+ .description('Update status, actual hours, or assignee of existing items')
10
+ .argument('<id>', 'ID of Epic, Feature, or Story to update')
11
+ .option('-s, --status <status>', 'Update status')
12
+ .option('-a, --actual <hours>', 'Update actual hours')
13
+ .option('--assignee <name>', 'Update assignee (for Features)')
14
+ .action(async (id, options, command) => {
15
+ try {
16
+ // Determine item type from ID
17
+ const itemType = getItemType(id);
18
+ if (itemType === 'epic') {
19
+ await updateEpic(id, options);
20
+ }
21
+ else if (itemType === 'feature') {
22
+ await updateFeature(id, options);
23
+ }
24
+ else if (itemType === 'story') {
25
+ await updateStory(id, options);
26
+ }
27
+ else {
28
+ console.error(chalk.red(`Error: Invalid ID format: ${id}`));
29
+ console.error(chalk.yellow('Expected format: EPIC-001, FEAT-001, or STORY-001'));
30
+ process.exit(1);
31
+ }
32
+ }
33
+ catch (error) {
34
+ console.error(chalk.red('Error:'), error.message);
35
+ process.exit(1);
36
+ }
37
+ });
38
+ function getItemType(id) {
39
+ if (id.startsWith('EPIC-'))
40
+ return 'epic';
41
+ if (id.startsWith('FEAT-'))
42
+ return 'feature';
43
+ if (id.startsWith('STORY-'))
44
+ return 'story';
45
+ return null;
46
+ }
47
+ async function updateEpic(id, options) {
48
+ const filePath = `pmspace/epics/${id.toLowerCase()}.md`;
49
+ try {
50
+ const epic = await readEpicFile(filePath);
51
+ let changed = false;
52
+ const changes = {};
53
+ // Update status
54
+ if (options.status) {
55
+ if (!EpicStatus.options.includes(options.status)) {
56
+ console.error(chalk.red(`Error: Invalid status "${options.status}" for Epic`));
57
+ console.error(chalk.yellow('Valid statuses:', EpicStatus.options.join(', ')));
58
+ process.exit(1);
59
+ }
60
+ changes.status = { oldValue: epic.status, newValue: options.status };
61
+ epic.status = options.status;
62
+ changed = true;
63
+ console.log(chalk.green(`✓ Updated ${id} status to ${options.status}`));
64
+ }
65
+ // Update actual hours
66
+ if (options.actual !== undefined) {
67
+ const actualHours = parseFloat(options.actual);
68
+ if (isNaN(actualHours) || actualHours < 0) {
69
+ console.error(chalk.red('Error: Actual hours must be a non-negative number'));
70
+ process.exit(1);
71
+ }
72
+ changes.actual = { oldValue: epic.actual, newValue: actualHours };
73
+ epic.actual = actualHours;
74
+ changed = true;
75
+ // Calculate variance
76
+ const variance = epic.actual - epic.estimate;
77
+ const variancePercent = ((variance / epic.estimate) * 100).toFixed(1);
78
+ const varianceStr = variance >= 0 ? `+${variance}h (+${variancePercent}%)` : `${variance}h (${variancePercent}%)`;
79
+ console.log(chalk.green(`✓ Updated ${id} actual hours to ${actualHours}h`));
80
+ console.log(chalk.blue(` Estimate: ${epic.estimate}h, Actual: ${epic.actual}h, Variance: ${varianceStr}`));
81
+ }
82
+ // Warn about unsupported options for epics
83
+ if (options.assignee) {
84
+ console.log(chalk.yellow(`Warning: Assignee cannot be set on Epics (use Features instead)`));
85
+ }
86
+ if (changed) {
87
+ await writeEpicFile(filePath, epic);
88
+ // Record changelog entries
89
+ try {
90
+ await getChangelogService().recordUpdates('epic', id, changes);
91
+ }
92
+ catch {
93
+ // Silently fail if changelog can't be written
94
+ }
95
+ }
96
+ else {
97
+ console.log(chalk.yellow('No changes specified. Use --status, --actual, or --assignee'));
98
+ }
99
+ }
100
+ catch (error) {
101
+ if (error.code === 'ENOENT') {
102
+ console.error(chalk.red(`Error: Epic ${id} not found`));
103
+ }
104
+ else {
105
+ throw error;
106
+ }
107
+ }
108
+ }
109
+ async function updateFeature(id, options) {
110
+ const filePath = `pmspace/features/${id.toLowerCase()}.md`;
111
+ try {
112
+ const feature = await readFeatureFile(filePath);
113
+ let changed = false;
114
+ const changes = {};
115
+ // Update status
116
+ if (options.status) {
117
+ if (!FeatureStatus.options.includes(options.status)) {
118
+ console.error(chalk.red(`Error: Invalid status "${options.status}" for Feature`));
119
+ console.error(chalk.yellow('Valid statuses:', FeatureStatus.options.join(', ')));
120
+ process.exit(1);
121
+ }
122
+ changes.status = { oldValue: feature.status, newValue: options.status };
123
+ feature.status = options.status;
124
+ changed = true;
125
+ console.log(chalk.green(`✓ Updated ${id} status to ${options.status}`));
126
+ }
127
+ // Update actual hours
128
+ if (options.actual !== undefined) {
129
+ const actualHours = parseFloat(options.actual);
130
+ if (isNaN(actualHours) || actualHours < 0) {
131
+ console.error(chalk.red('Error: Actual hours must be a non-negative number'));
132
+ process.exit(1);
133
+ }
134
+ changes.actual = { oldValue: feature.actual, newValue: actualHours };
135
+ feature.actual = actualHours;
136
+ changed = true;
137
+ // Calculate variance
138
+ const variance = feature.actual - feature.estimate;
139
+ const variancePercent = ((variance / feature.estimate) * 100).toFixed(1);
140
+ const varianceStr = variance >= 0 ? `+${variance}h (+${variancePercent}%)` : `${variance}h (${variancePercent}%)`;
141
+ console.log(chalk.green(`✓ Updated ${id} actual hours to ${actualHours}h`));
142
+ console.log(chalk.blue(` Estimate: ${feature.estimate}h, Actual: ${feature.actual}h, Variance: ${varianceStr}`));
143
+ }
144
+ // Update assignee
145
+ if (options.assignee !== undefined) {
146
+ changes.assignee = { oldValue: feature.assignee, newValue: options.assignee || undefined };
147
+ feature.assignee = options.assignee || undefined;
148
+ changed = true;
149
+ if (feature.assignee) {
150
+ console.log(chalk.green(`✓ Updated ${id} assignee to ${feature.assignee}`));
151
+ }
152
+ else {
153
+ console.log(chalk.green(`✓ Removed assignee from ${id}`));
154
+ }
155
+ }
156
+ if (changed) {
157
+ await writeFeatureFile(filePath, feature);
158
+ // Record changelog entries
159
+ try {
160
+ await getChangelogService().recordUpdates('feature', id, changes);
161
+ }
162
+ catch {
163
+ // Silently fail if changelog can't be written
164
+ }
165
+ }
166
+ else {
167
+ console.log(chalk.yellow('No changes specified. Use --status, --actual, or --assignee'));
168
+ }
169
+ }
170
+ catch (error) {
171
+ if (error.code === 'ENOENT') {
172
+ console.error(chalk.red(`Error: Feature ${id} not found`));
173
+ }
174
+ else {
175
+ throw error;
176
+ }
177
+ }
178
+ }
179
+ async function updateStory(id, options) {
180
+ // Find the feature that contains this story
181
+ let found = false;
182
+ let featurePath = '';
183
+ let feature = null;
184
+ let storyIndex = -1;
185
+ try {
186
+ const featureFiles = await import('fs/promises').then(fs => fs.readdir('pmspace/features'));
187
+ for (const file of featureFiles) {
188
+ if (file.endsWith('.md')) {
189
+ const path = join('pmspace/features', file);
190
+ const f = await readFeatureFile(path);
191
+ const storyIdx = f.userStories.findIndex((s) => s.id === id);
192
+ if (storyIdx !== -1) {
193
+ found = true;
194
+ featurePath = path;
195
+ feature = f;
196
+ storyIndex = storyIdx;
197
+ break;
198
+ }
199
+ }
200
+ }
201
+ }
202
+ catch {
203
+ // Directory doesn't exist
204
+ }
205
+ if (!found) {
206
+ console.error(chalk.red(`Error: Story ${id} not found`));
207
+ process.exit(1);
208
+ }
209
+ let changed = false;
210
+ const story = feature.userStories[storyIndex];
211
+ const changes = {};
212
+ // Update status
213
+ if (options.status) {
214
+ if (!StoryStatus.options.includes(options.status)) {
215
+ console.error(chalk.red(`Error: Invalid status "${options.status}" for Story`));
216
+ console.error(chalk.yellow('Valid statuses:', StoryStatus.options.join(', ')));
217
+ process.exit(1);
218
+ }
219
+ changes.status = { oldValue: story.status, newValue: options.status };
220
+ story.status = options.status;
221
+ changed = true;
222
+ console.log(chalk.green(`✓ Updated ${id} status to ${options.status}`));
223
+ }
224
+ // Update actual hours (stories don't have actual hours in our model)
225
+ if (options.actual !== undefined) {
226
+ console.log(chalk.yellow(`Warning: Stories don't track actual hours (use Feature instead)`));
227
+ }
228
+ // Warn about unsupported options for stories
229
+ if (options.assignee) {
230
+ console.log(chalk.yellow(`Warning: Assignee cannot be set on Stories (use Features instead)`));
231
+ }
232
+ if (changed) {
233
+ await writeFeatureFile(featurePath, feature);
234
+ // Record changelog entries
235
+ try {
236
+ await getChangelogService().recordUpdates('story', id, changes);
237
+ }
238
+ catch {
239
+ // Silently fail if changelog can't be written
240
+ }
241
+ }
242
+ else {
243
+ console.log(chalk.yellow('No changes specified. Use --status for stories'));
244
+ }
245
+ }
246
+ export { updateCommand };
247
+ //# sourceMappingURL=update.js.map
@@ -0,0 +1,3 @@
1
+ import { Command } from 'commander';
2
+ export declare const validateCommand: Command;
3
+ //# sourceMappingURL=validate.d.ts.map
@@ -0,0 +1,74 @@
1
+ import { Command } from 'commander';
2
+ import { readdir } from 'fs/promises';
3
+ import { join } from 'path';
4
+ import chalk from 'chalk';
5
+ import { readEpicFile, readFeatureFile, readTeamFile } from '../core/parser.js';
6
+ import { validateProject, formatValidationIssues } from '../utils/validation.js';
7
+ export const validateCommand = new Command('validate')
8
+ .description('Validate project structure and data integrity')
9
+ .argument('[id]', 'Optional: validate specific Epic or Feature')
10
+ .action(async (id) => {
11
+ const pmspaceDir = join(process.cwd(), 'pmspace');
12
+ try {
13
+ if (id) {
14
+ await validateSpecific(pmspaceDir, id);
15
+ }
16
+ else {
17
+ await validateAll(pmspaceDir);
18
+ }
19
+ }
20
+ catch (error) {
21
+ console.error(chalk.red('Error:'), error.message);
22
+ process.exit(1);
23
+ }
24
+ });
25
+ async function validateAll(pmspaceDir) {
26
+ console.log(chalk.cyan('Validating project...\n'));
27
+ // Read all Epics
28
+ const epicsDir = join(pmspaceDir, 'epics');
29
+ const epicFiles = await readdir(epicsDir);
30
+ const epics = [];
31
+ for (const file of epicFiles.filter(f => f.endsWith('.md'))) {
32
+ const epic = await readEpicFile(join(epicsDir, file));
33
+ epics.push(epic);
34
+ }
35
+ // Read all Features
36
+ const featuresDir = join(pmspaceDir, 'features');
37
+ const featureFiles = await readdir(featuresDir);
38
+ const features = [];
39
+ for (const file of featureFiles.filter(f => f.endsWith('.md'))) {
40
+ const feature = await readFeatureFile(join(featuresDir, file));
41
+ features.push(feature);
42
+ }
43
+ // Read Team (optional)
44
+ let team;
45
+ try {
46
+ team = await readTeamFile(join(pmspaceDir, 'team.md'));
47
+ }
48
+ catch {
49
+ // Team file not required
50
+ }
51
+ // Validate
52
+ const result = validateProject(epics, features, team);
53
+ console.log(formatValidationIssues(result));
54
+ if (!result.valid) {
55
+ process.exit(1);
56
+ }
57
+ }
58
+ async function validateSpecific(pmspaceDir, id) {
59
+ if (id.startsWith('EPIC-')) {
60
+ const filePath = join(pmspaceDir, 'epics', `${id.toLowerCase()}.md`);
61
+ const epic = await readEpicFile(filePath);
62
+ console.log(chalk.green(`✓ ${epic.id} is valid`));
63
+ }
64
+ else if (id.startsWith('FEAT-')) {
65
+ const filePath = join(pmspaceDir, 'features', `${id.toLowerCase()}.md`);
66
+ const feature = await readFeatureFile(filePath);
67
+ console.log(chalk.green(`✓ ${feature.id} is valid`));
68
+ }
69
+ else {
70
+ console.error(chalk.red(`Invalid ID: ${id}`));
71
+ process.exit(1);
72
+ }
73
+ }
74
+ //# sourceMappingURL=validate.js.map