@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.
- package/README.md +306 -0
- package/README.zh.md +304 -0
- package/bin/pmspec.js +5 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.js +39 -0
- package/dist/commands/analyze.d.ts +4 -0
- package/dist/commands/analyze.js +240 -0
- package/dist/commands/breakdown.d.ts +4 -0
- package/dist/commands/breakdown.js +194 -0
- package/dist/commands/create.d.ts +4 -0
- package/dist/commands/create.js +529 -0
- package/dist/commands/history.d.ts +4 -0
- package/dist/commands/history.js +213 -0
- package/dist/commands/import.d.ts +4 -0
- package/dist/commands/import.js +196 -0
- package/dist/commands/index-legacy.d.ts +4 -0
- package/dist/commands/index-legacy.js +27 -0
- package/dist/commands/init.d.ts +3 -0
- package/dist/commands/init.js +60 -0
- package/dist/commands/list.d.ts +3 -0
- package/dist/commands/list.js +127 -0
- package/dist/commands/search.d.ts +7 -0
- package/dist/commands/search.js +183 -0
- package/dist/commands/serve.d.ts +3 -0
- package/dist/commands/serve.js +68 -0
- package/dist/commands/show.d.ts +3 -0
- package/dist/commands/show.js +152 -0
- package/dist/commands/simple.d.ts +7 -0
- package/dist/commands/simple.js +360 -0
- package/dist/commands/update.d.ts +4 -0
- package/dist/commands/update.js +247 -0
- package/dist/commands/validate.d.ts +3 -0
- package/dist/commands/validate.js +74 -0
- package/dist/core/changelog-service.d.ts +88 -0
- package/dist/core/changelog-service.js +208 -0
- package/dist/core/changelog.d.ts +113 -0
- package/dist/core/changelog.js +147 -0
- package/dist/core/importers.d.ts +343 -0
- package/dist/core/importers.js +715 -0
- package/dist/core/parser.d.ts +50 -0
- package/dist/core/parser.js +246 -0
- package/dist/core/project.d.ts +155 -0
- package/dist/core/project.js +138 -0
- package/dist/core/search.d.ts +119 -0
- package/dist/core/search.js +299 -0
- package/dist/core/simple-model.d.ts +54 -0
- package/dist/core/simple-model.js +20 -0
- package/dist/core/team.d.ts +41 -0
- package/dist/core/team.js +57 -0
- package/dist/core/workload.d.ts +49 -0
- package/dist/core/workload.js +116 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +11 -0
- package/dist/utils/csv-handler.d.ts +15 -0
- package/dist/utils/csv-handler.js +224 -0
- package/dist/utils/markdown.d.ts +43 -0
- package/dist/utils/markdown.js +202 -0
- package/dist/utils/validation.d.ts +35 -0
- package/dist/utils/validation.js +178 -0
- 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,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,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
|