@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,183 @@
1
+ import { Command } from 'commander';
2
+ import chalk from 'chalk';
3
+ import { SearchService, highlightMatches } from '../core/search.js';
4
+ /**
5
+ * Search command for CLI
6
+ */
7
+ const searchCommand = new Command('search')
8
+ .description('搜索项目中的 Epic、Feature、Story 和 Milestone')
9
+ .argument('<query>', '搜索关键词')
10
+ .option('--type <type>', '限制搜索类型: epic, feature, story, milestone')
11
+ .option('--limit <number>', '返回结果数量限制', '20')
12
+ .option('--no-highlight', '禁用高亮显示')
13
+ .option('--json', '以 JSON 格式输出')
14
+ .action(async (query, options) => {
15
+ const startTime = Date.now();
16
+ try {
17
+ // Initialize search service
18
+ const searchService = new SearchService(process.cwd());
19
+ await searchService.index();
20
+ // Build search options
21
+ const searchOptions = {
22
+ limit: parseInt(options.limit, 10) || 20,
23
+ };
24
+ // Handle type filter
25
+ if (options.type) {
26
+ const validTypes = ['epic', 'feature', 'story', 'milestone'];
27
+ const requestedType = options.type.toLowerCase();
28
+ if (!validTypes.includes(requestedType)) {
29
+ console.error(chalk.red(`错误: 无效的类型 "${options.type}"`));
30
+ console.error(chalk.gray(`有效类型: ${validTypes.join(', ')}`));
31
+ process.exit(1);
32
+ }
33
+ searchOptions.type = requestedType;
34
+ }
35
+ // Perform search
36
+ const results = searchService.search(query, searchOptions);
37
+ const searchTime = Date.now() - startTime;
38
+ // Output results
39
+ if (options.json) {
40
+ outputJson(results, searchTime);
41
+ }
42
+ else {
43
+ outputFormatted(query, results, searchTime, options.highlight !== false);
44
+ }
45
+ }
46
+ catch (error) {
47
+ console.error(chalk.red('搜索错误:'), error.message);
48
+ process.exit(1);
49
+ }
50
+ });
51
+ /**
52
+ * Output results as JSON
53
+ */
54
+ function outputJson(results, searchTime) {
55
+ const output = {
56
+ results,
57
+ meta: {
58
+ count: results.length,
59
+ searchTime: `${searchTime}ms`,
60
+ },
61
+ };
62
+ console.log(JSON.stringify(output, null, 2));
63
+ }
64
+ /**
65
+ * Output formatted results for terminal
66
+ */
67
+ function outputFormatted(query, results, searchTime, useHighlight) {
68
+ console.log(chalk.blue.bold(`\n🔍 搜索结果: "${query}"\n`));
69
+ if (results.length === 0) {
70
+ console.log(chalk.yellow('没有找到匹配的结果'));
71
+ console.log(chalk.gray(`\n耗时: ${searchTime}ms`));
72
+ return;
73
+ }
74
+ // Group results by type
75
+ const grouped = groupByType(results);
76
+ // Output each group
77
+ for (const [type, typeResults] of Object.entries(grouped)) {
78
+ const typeLabel = getTypeLabel(type);
79
+ const typeColor = getTypeColor(type);
80
+ console.log(typeColor(`\n═══ ${typeLabel} (${typeResults.length}) ═══\n`));
81
+ for (const result of typeResults) {
82
+ outputResult(result, useHighlight);
83
+ }
84
+ }
85
+ // Summary
86
+ console.log(chalk.gray(`\n────────────────────────────────────`));
87
+ console.log(chalk.green(`找到 ${results.length} 个结果`));
88
+ console.log(chalk.gray(`耗时: ${searchTime}ms`));
89
+ }
90
+ /**
91
+ * Output a single search result
92
+ */
93
+ function outputResult(result, useHighlight) {
94
+ const typeColor = getTypeColor(result.type);
95
+ const typeEmoji = getTypeEmoji(result.type);
96
+ // ID and Title
97
+ const title = useHighlight
98
+ ? highlightMatches(result.title, result.matches)
99
+ : result.title;
100
+ console.log(`${typeEmoji} ${typeColor(result.id)} ${chalk.white.bold(title)}`);
101
+ // Description (truncated)
102
+ if (result.description) {
103
+ const desc = useHighlight
104
+ ? highlightMatches(result.description, result.matches)
105
+ : result.description;
106
+ const truncatedDesc = truncate(desc, 100);
107
+ console.log(chalk.gray(` ${truncatedDesc}`));
108
+ }
109
+ // Parent reference
110
+ if (result.parentId) {
111
+ console.log(chalk.gray.dim(` └─ 属于: ${result.parentId}`));
112
+ }
113
+ // Score (debug info)
114
+ // console.log(chalk.gray.dim(` 分数: ${result.score.toFixed(2)}`));
115
+ console.log('');
116
+ }
117
+ /**
118
+ * Group results by type
119
+ */
120
+ function groupByType(results) {
121
+ const groups = {};
122
+ for (const result of results) {
123
+ if (!groups[result.type]) {
124
+ groups[result.type] = [];
125
+ }
126
+ groups[result.type].push(result);
127
+ }
128
+ // Sort types in preferred order
129
+ const order = ['epic', 'feature', 'story', 'milestone'];
130
+ const ordered = {};
131
+ for (const type of order) {
132
+ if (groups[type]) {
133
+ ordered[type] = groups[type];
134
+ }
135
+ }
136
+ return ordered;
137
+ }
138
+ /**
139
+ * Get display label for type
140
+ */
141
+ function getTypeLabel(type) {
142
+ const labels = {
143
+ epic: 'Epics',
144
+ feature: 'Features',
145
+ story: 'User Stories',
146
+ milestone: 'Milestones',
147
+ };
148
+ return labels[type] || type;
149
+ }
150
+ /**
151
+ * Get color for type
152
+ */
153
+ function getTypeColor(type) {
154
+ const colors = {
155
+ epic: chalk.magenta,
156
+ feature: chalk.cyan,
157
+ story: chalk.blue,
158
+ milestone: chalk.green,
159
+ };
160
+ return colors[type] || chalk.white;
161
+ }
162
+ /**
163
+ * Get emoji for type
164
+ */
165
+ function getTypeEmoji(type) {
166
+ const emojis = {
167
+ epic: '📦',
168
+ feature: '✨',
169
+ story: '📝',
170
+ milestone: '🎯',
171
+ };
172
+ return emojis[type] || '•';
173
+ }
174
+ /**
175
+ * Truncate text to specified length
176
+ */
177
+ function truncate(text, maxLength) {
178
+ if (text.length <= maxLength)
179
+ return text;
180
+ return text.substring(0, maxLength - 3) + '...';
181
+ }
182
+ export { searchCommand };
183
+ //# sourceMappingURL=search.js.map
@@ -0,0 +1,3 @@
1
+ import { Command } from 'commander';
2
+ export declare const serveCommand: Command;
3
+ //# sourceMappingURL=serve.d.ts.map
@@ -0,0 +1,68 @@
1
+ import { Command } from 'commander';
2
+ import { spawn } from 'child_process';
3
+ import path from 'path';
4
+ import { fileURLToPath } from 'url';
5
+ import open from 'open';
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = path.dirname(__filename);
8
+ export const serveCommand = new Command('serve')
9
+ .description('Start the PMSpec Web UI server')
10
+ .option('-p, --port <port>', 'Port to run the server on', '3000')
11
+ .option('-o, --open', 'Open browser automatically')
12
+ .action(async (options) => {
13
+ const port = parseInt(options.port) || 3000;
14
+ const shouldOpen = options.open || false;
15
+ console.log(`\n🚀 Starting PMSpec Web Server...\n`);
16
+ // Path to backend server
17
+ const backendPath = path.join(__dirname, '../../web/backend/src/server.ts');
18
+ // Check if backend exists
19
+ const fs = await import('fs/promises');
20
+ try {
21
+ await fs.access(backendPath);
22
+ }
23
+ catch (error) {
24
+ console.error('❌ Error: Web backend not found.');
25
+ console.error(' Please ensure the web/ directory is set up correctly.');
26
+ process.exit(1);
27
+ }
28
+ // Start backend server
29
+ const backendProcess = spawn('npx', ['ts-node-dev', '--respawn', '--transpile-only', backendPath], {
30
+ cwd: path.join(__dirname, '../../web/backend'),
31
+ env: { ...process.env, PORT: port.toString() },
32
+ stdio: 'inherit',
33
+ shell: true,
34
+ });
35
+ // Wait a bit for server to start
36
+ setTimeout(() => {
37
+ const url = `http://localhost:${port}`;
38
+ if (shouldOpen) {
39
+ console.log(`\n📖 Opening browser at ${url}...\n`);
40
+ open(url).catch((err) => {
41
+ console.error('Could not open browser:', err.message);
42
+ });
43
+ }
44
+ console.log(`\n✨ PMSpec Web UI is running!`);
45
+ console.log(` - URL: ${url}`);
46
+ console.log(` - API: ${url}/api`);
47
+ console.log(`\n Press Ctrl+C to stop the server.\n`);
48
+ }, 2000);
49
+ // Handle graceful shutdown
50
+ const cleanup = () => {
51
+ console.log('\n\n⏹ Shutting down PMSpec Web Server...');
52
+ backendProcess.kill('SIGTERM');
53
+ process.exit(0);
54
+ };
55
+ process.on('SIGINT', cleanup);
56
+ process.on('SIGTERM', cleanup);
57
+ backendProcess.on('error', (error) => {
58
+ console.error('❌ Failed to start server:', error);
59
+ process.exit(1);
60
+ });
61
+ backendProcess.on('exit', (code) => {
62
+ if (code !== 0 && code !== null) {
63
+ console.error(`❌ Server exited with code ${code}`);
64
+ process.exit(code);
65
+ }
66
+ });
67
+ });
68
+ //# sourceMappingURL=serve.js.map
@@ -0,0 +1,3 @@
1
+ import { Command } from 'commander';
2
+ export declare const showCommand: Command;
3
+ //# sourceMappingURL=show.d.ts.map
@@ -0,0 +1,152 @@
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, readMilestoneFile } from '../core/parser.js';
6
+ export const showCommand = new Command('show')
7
+ .description('Show details of an Epic, Feature, or Milestone')
8
+ .argument('<id>', 'ID of Epic, Feature, or Milestone (e.g., EPIC-001, FEAT-001, MILE-001)')
9
+ .action(async (id) => {
10
+ const pmspaceDir = join(process.cwd(), 'pmspace');
11
+ try {
12
+ if (id.startsWith('EPIC-')) {
13
+ await showEpic(pmspaceDir, id);
14
+ }
15
+ else if (id.startsWith('FEAT-')) {
16
+ await showFeature(pmspaceDir, id);
17
+ }
18
+ else if (id.startsWith('MILE-')) {
19
+ await showMilestone(pmspaceDir, id);
20
+ }
21
+ else {
22
+ console.error(chalk.red(`Invalid ID format: ${id}`));
23
+ process.exit(1);
24
+ }
25
+ }
26
+ catch (error) {
27
+ if (error.code === 'ENOENT') {
28
+ console.error(chalk.red(`${id} not found.`));
29
+ process.exit(1);
30
+ }
31
+ throw error;
32
+ }
33
+ });
34
+ async function showEpic(pmspaceDir, id) {
35
+ const filePath = join(pmspaceDir, 'epics', `${id.toLowerCase()}.md`);
36
+ const epic = await readEpicFile(filePath);
37
+ console.log(chalk.bold.cyan(`\n# Epic: ${epic.title}\n`));
38
+ console.log(chalk.gray(`ID: ${epic.id}`));
39
+ console.log(chalk.gray(`Status: ${epic.status}`));
40
+ if (epic.owner) {
41
+ console.log(chalk.gray(`Owner: ${epic.owner}`));
42
+ }
43
+ console.log(chalk.gray(`Estimate: ${epic.estimate} hours`));
44
+ console.log(chalk.gray(`Actual: ${epic.actual} hours`));
45
+ if (epic.description) {
46
+ console.log(chalk.bold('\n## Description\n'));
47
+ console.log(epic.description);
48
+ }
49
+ if (epic.features.length > 0) {
50
+ console.log(chalk.bold('\n## Features\n'));
51
+ // Calculate progress
52
+ const featuresDir = join(pmspaceDir, 'features');
53
+ const files = await readdir(featuresDir);
54
+ const featureFiles = files.filter(f => f.endsWith('.md'));
55
+ let completed = 0;
56
+ for (const featureId of epic.features) {
57
+ try {
58
+ const filePath = join(featuresDir, `${featureId.toLowerCase()}.md`);
59
+ const feature = await readFeatureFile(filePath);
60
+ const checkbox = feature.status === 'done' ? '[x]' : '[ ]';
61
+ console.log(` ${checkbox} ${feature.id}: ${feature.title}`);
62
+ if (feature.status === 'done')
63
+ completed++;
64
+ }
65
+ catch {
66
+ console.log(` [ ] ${featureId}: [Not found]`);
67
+ }
68
+ }
69
+ const progress = epic.features.length > 0 ? Math.round((completed / epic.features.length) * 100) : 0;
70
+ console.log(chalk.gray(`\nProgress: ${completed}/${epic.features.length} (${progress}%)`));
71
+ }
72
+ console.log('');
73
+ }
74
+ async function showFeature(pmspaceDir, id) {
75
+ const filePath = join(pmspaceDir, 'features', `${id.toLowerCase()}.md`);
76
+ const feature = await readFeatureFile(filePath);
77
+ console.log(chalk.bold.cyan(`\n# Feature: ${feature.title}\n`));
78
+ console.log(chalk.gray(`ID: ${feature.id}`));
79
+ console.log(chalk.gray(`Epic: ${feature.epicId}`));
80
+ console.log(chalk.gray(`Status: ${feature.status}`));
81
+ if (feature.assignee) {
82
+ console.log(chalk.gray(`Assignee: ${feature.assignee}`));
83
+ }
84
+ console.log(chalk.gray(`Estimate: ${feature.estimate} hours`));
85
+ console.log(chalk.gray(`Actual: ${feature.actual} hours`));
86
+ if (feature.skillsRequired.length > 0) {
87
+ console.log(chalk.gray(`Skills: ${feature.skillsRequired.join(', ')}`));
88
+ }
89
+ if (feature.description) {
90
+ console.log(chalk.bold('\n## Description\n'));
91
+ console.log(feature.description);
92
+ }
93
+ if (feature.userStories.length > 0) {
94
+ console.log(chalk.bold('\n## User Stories\n'));
95
+ for (const story of feature.userStories) {
96
+ const checkbox = story.status === 'done' ? '[x]' : '[ ]';
97
+ console.log(` ${checkbox} ${story.id}: ${story.title} (${story.estimate}h)`);
98
+ }
99
+ }
100
+ if (feature.acceptanceCriteria.length > 0) {
101
+ console.log(chalk.bold('\n## Acceptance Criteria\n'));
102
+ for (const criterion of feature.acceptanceCriteria) {
103
+ console.log(` - [ ] ${criterion}`);
104
+ }
105
+ }
106
+ if (feature.dependencies && feature.dependencies.length > 0) {
107
+ console.log(chalk.bold('\n## Dependencies\n'));
108
+ const blocks = feature.dependencies.filter(d => d.type === 'blocks');
109
+ const relatesTo = feature.dependencies.filter(d => d.type === 'relates-to');
110
+ if (blocks.length > 0) {
111
+ console.log(chalk.gray(` Blocks: ${blocks.map(d => d.featureId).join(', ')}`));
112
+ }
113
+ if (relatesTo.length > 0) {
114
+ console.log(chalk.gray(` Relates to: ${relatesTo.map(d => d.featureId).join(', ')}`));
115
+ }
116
+ }
117
+ console.log('');
118
+ }
119
+ async function showMilestone(pmspaceDir, id) {
120
+ const filePath = join(pmspaceDir, 'milestones', `${id.toLowerCase()}.md`);
121
+ const milestone = await readMilestoneFile(filePath);
122
+ console.log(chalk.bold.cyan(`\n# Milestone: ${milestone.title}\n`));
123
+ console.log(chalk.gray(`ID: ${milestone.id}`));
124
+ console.log(chalk.gray(`Target Date: ${milestone.targetDate}`));
125
+ console.log(chalk.gray(`Status: ${milestone.status}`));
126
+ if (milestone.description) {
127
+ console.log(chalk.bold('\n## Description\n'));
128
+ console.log(milestone.description);
129
+ }
130
+ if (milestone.features.length > 0) {
131
+ console.log(chalk.bold('\n## Features\n'));
132
+ const featuresDir = join(pmspaceDir, 'features');
133
+ let completed = 0;
134
+ for (const featureId of milestone.features) {
135
+ try {
136
+ const featurePath = join(featuresDir, `${featureId.toLowerCase()}.md`);
137
+ const feature = await readFeatureFile(featurePath);
138
+ const checkbox = feature.status === 'done' ? '[x]' : '[ ]';
139
+ console.log(` ${checkbox} ${feature.id}: ${feature.title}`);
140
+ if (feature.status === 'done')
141
+ completed++;
142
+ }
143
+ catch {
144
+ console.log(` [ ] ${featureId}: [Not found]`);
145
+ }
146
+ }
147
+ const progress = milestone.features.length > 0 ? Math.round((completed / milestone.features.length) * 100) : 0;
148
+ console.log(chalk.gray(`\nProgress: ${completed}/${milestone.features.length} (${progress}%)`));
149
+ }
150
+ console.log('');
151
+ }
152
+ //# sourceMappingURL=show.js.map
@@ -0,0 +1,7 @@
1
+ import { Command } from 'commander';
2
+ declare const simpleCommand: Command;
3
+ declare const generateCommand: Command;
4
+ declare const listCommand: Command;
5
+ declare const statsCommand: Command;
6
+ export { simpleCommand, generateCommand, listCommand, statsCommand };
7
+ //# sourceMappingURL=simple.d.ts.map