@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,213 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { ChangelogService } from '../core/changelog-service.js';
|
|
4
|
+
const historyCommand = new Command('history')
|
|
5
|
+
.description('Show change history for entities')
|
|
6
|
+
.argument('[entityId]', 'Entity ID to show history for (e.g., FEAT-001, EPIC-001)')
|
|
7
|
+
.option('-a, --all', 'Show all change history')
|
|
8
|
+
.option('--since <date>', 'Show changes since date (YYYY-MM-DD)')
|
|
9
|
+
.option('--until <date>', 'Show changes until date (YYYY-MM-DD)')
|
|
10
|
+
.option('-t, --type <type>', 'Filter by entity type (epic, feature, milestone, story)')
|
|
11
|
+
.option('--action <action>', 'Filter by action (create, update, delete)')
|
|
12
|
+
.option('-n, --limit <number>', 'Limit number of entries', '50')
|
|
13
|
+
.option('--json', 'Output as JSON')
|
|
14
|
+
.option('--diff', 'Show detailed diff view for updates')
|
|
15
|
+
.option('--stats', 'Show changelog statistics')
|
|
16
|
+
.action(async (entityId, options) => {
|
|
17
|
+
try {
|
|
18
|
+
const service = new ChangelogService();
|
|
19
|
+
// Show stats if requested
|
|
20
|
+
if (options.stats) {
|
|
21
|
+
await showStats(service);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
// Validate options
|
|
25
|
+
if (!entityId && !options.all && !options.since && !options.type) {
|
|
26
|
+
console.error(chalk.red('Error: Please specify an entity ID, use --all, or provide filter options'));
|
|
27
|
+
console.log(chalk.yellow('Usage examples:'));
|
|
28
|
+
console.log(chalk.gray(' pmspec history FEAT-001 # Show history for FEAT-001'));
|
|
29
|
+
console.log(chalk.gray(' pmspec history --all # Show all changes'));
|
|
30
|
+
console.log(chalk.gray(' pmspec history --since 2024-01-01'));
|
|
31
|
+
console.log(chalk.gray(' pmspec history --type feature'));
|
|
32
|
+
console.log(chalk.gray(' pmspec history --stats # Show statistics'));
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
// Build query options
|
|
36
|
+
const queryOptions = {
|
|
37
|
+
limit: parseInt(options.limit) || 50,
|
|
38
|
+
};
|
|
39
|
+
if (entityId) {
|
|
40
|
+
queryOptions.entityId = entityId.toUpperCase();
|
|
41
|
+
}
|
|
42
|
+
if (options.since) {
|
|
43
|
+
queryOptions.since = new Date(options.since).toISOString();
|
|
44
|
+
}
|
|
45
|
+
if (options.until) {
|
|
46
|
+
queryOptions.until = new Date(options.until).toISOString();
|
|
47
|
+
}
|
|
48
|
+
if (options.type) {
|
|
49
|
+
queryOptions.entityType = options.type.toLowerCase();
|
|
50
|
+
}
|
|
51
|
+
if (options.action) {
|
|
52
|
+
queryOptions.action = options.action.toLowerCase();
|
|
53
|
+
}
|
|
54
|
+
const entries = await service.query(queryOptions);
|
|
55
|
+
if (entries.length === 0) {
|
|
56
|
+
console.log(chalk.yellow('No change history found matching the criteria'));
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
if (options.json) {
|
|
60
|
+
console.log(JSON.stringify(entries, null, 2));
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
// Display header
|
|
64
|
+
if (entityId) {
|
|
65
|
+
console.log(chalk.blue.bold(`\n📜 Change History for ${entityId.toUpperCase()}\n`));
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
console.log(chalk.blue.bold(`\n📜 Change History (${entries.length} entries)\n`));
|
|
69
|
+
}
|
|
70
|
+
// Display entries
|
|
71
|
+
if (options.diff) {
|
|
72
|
+
displayDiffView(entries);
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
displayTimelineView(entries);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
console.error(chalk.red('Error:'), error.message);
|
|
80
|
+
process.exit(1);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
function displayTimelineView(entries) {
|
|
84
|
+
// Group entries by date
|
|
85
|
+
const grouped = new Map();
|
|
86
|
+
for (const entry of entries) {
|
|
87
|
+
const date = entry.timestamp.split('T')[0];
|
|
88
|
+
if (!grouped.has(date)) {
|
|
89
|
+
grouped.set(date, []);
|
|
90
|
+
}
|
|
91
|
+
grouped.get(date).push(entry);
|
|
92
|
+
}
|
|
93
|
+
for (const [date, dateEntries] of grouped) {
|
|
94
|
+
console.log(chalk.yellow(`\n${formatDate(date)}`));
|
|
95
|
+
console.log(chalk.gray('─'.repeat(40)));
|
|
96
|
+
for (const entry of dateEntries) {
|
|
97
|
+
const time = formatTime(entry.timestamp);
|
|
98
|
+
const icon = getActionIcon(entry.action);
|
|
99
|
+
const color = getActionColor(entry.action);
|
|
100
|
+
console.log(`${chalk.gray(time)} ${icon} ${color(formatEntryDescription(entry))}`);
|
|
101
|
+
if (entry.action === 'update' && entry.field) {
|
|
102
|
+
const oldVal = formatDisplayValue(entry.oldValue);
|
|
103
|
+
const newVal = formatDisplayValue(entry.newValue);
|
|
104
|
+
console.log(chalk.gray(` ${entry.field}: ${oldVal} → ${newVal}`));
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
console.log('');
|
|
109
|
+
}
|
|
110
|
+
function displayDiffView(entries) {
|
|
111
|
+
for (const entry of entries) {
|
|
112
|
+
console.log(chalk.gray('─'.repeat(60)));
|
|
113
|
+
console.log(`${chalk.bold(entry.id)} - ${formatDateTime(entry.timestamp)}`);
|
|
114
|
+
console.log(`${getActionIcon(entry.action)} ${getActionColor(entry.action)(entry.action.toUpperCase())} ${entry.entityType} ${chalk.cyan(entry.entityId)}`);
|
|
115
|
+
if (entry.user) {
|
|
116
|
+
console.log(chalk.gray(` by ${entry.user}`));
|
|
117
|
+
}
|
|
118
|
+
if (entry.action === 'update' && entry.field) {
|
|
119
|
+
console.log('');
|
|
120
|
+
console.log(chalk.yellow(` Field: ${entry.field}`));
|
|
121
|
+
console.log(chalk.red(` - ${formatDisplayValue(entry.oldValue)}`));
|
|
122
|
+
console.log(chalk.green(` + ${formatDisplayValue(entry.newValue)}`));
|
|
123
|
+
}
|
|
124
|
+
console.log('');
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
async function showStats(service) {
|
|
128
|
+
const stats = await service.getStats();
|
|
129
|
+
console.log(chalk.blue.bold('\n📊 Changelog Statistics\n'));
|
|
130
|
+
console.log(`Total entries: ${chalk.cyan(stats.totalEntries)}`);
|
|
131
|
+
console.log(chalk.yellow('\nBy Entity Type:'));
|
|
132
|
+
for (const [type, count] of Object.entries(stats.byEntityType)) {
|
|
133
|
+
console.log(` ${type}: ${count}`);
|
|
134
|
+
}
|
|
135
|
+
console.log(chalk.yellow('\nBy Action:'));
|
|
136
|
+
for (const [action, count] of Object.entries(stats.byAction)) {
|
|
137
|
+
const icon = getActionIcon(action);
|
|
138
|
+
console.log(` ${icon} ${action}: ${count}`);
|
|
139
|
+
}
|
|
140
|
+
console.log(chalk.yellow('\nRecent Activity:'));
|
|
141
|
+
console.log(` Last 24 hours: ${stats.recentActivity.last24h}`);
|
|
142
|
+
console.log(` Last 7 days: ${stats.recentActivity.last7d}`);
|
|
143
|
+
console.log(` Last 30 days: ${stats.recentActivity.last30d}`);
|
|
144
|
+
console.log('');
|
|
145
|
+
}
|
|
146
|
+
function getActionIcon(action) {
|
|
147
|
+
switch (action) {
|
|
148
|
+
case 'create':
|
|
149
|
+
return '✨';
|
|
150
|
+
case 'update':
|
|
151
|
+
return '📝';
|
|
152
|
+
case 'delete':
|
|
153
|
+
return '🗑️';
|
|
154
|
+
default:
|
|
155
|
+
return '•';
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
function getActionColor(action) {
|
|
159
|
+
switch (action) {
|
|
160
|
+
case 'create':
|
|
161
|
+
return (text) => chalk.green(text);
|
|
162
|
+
case 'update':
|
|
163
|
+
return (text) => chalk.blue(text);
|
|
164
|
+
case 'delete':
|
|
165
|
+
return (text) => chalk.red(text);
|
|
166
|
+
default:
|
|
167
|
+
return (text) => text;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
function formatEntryDescription(entry) {
|
|
171
|
+
const entityInfo = `${entry.entityType} ${chalk.cyan(entry.entityId)}`;
|
|
172
|
+
switch (entry.action) {
|
|
173
|
+
case 'create':
|
|
174
|
+
return `Created ${entityInfo}`;
|
|
175
|
+
case 'delete':
|
|
176
|
+
return `Deleted ${entityInfo}`;
|
|
177
|
+
case 'update':
|
|
178
|
+
return `Updated ${entityInfo}${entry.field ? ` (${entry.field})` : ''}`;
|
|
179
|
+
default:
|
|
180
|
+
return `${entry.action} ${entityInfo}`;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
function formatDisplayValue(value) {
|
|
184
|
+
if (value === undefined || value === null) {
|
|
185
|
+
return chalk.gray('(empty)');
|
|
186
|
+
}
|
|
187
|
+
if (typeof value === 'object') {
|
|
188
|
+
return JSON.stringify(value);
|
|
189
|
+
}
|
|
190
|
+
return String(value);
|
|
191
|
+
}
|
|
192
|
+
function formatDate(dateStr) {
|
|
193
|
+
const date = new Date(dateStr);
|
|
194
|
+
return date.toLocaleDateString('en-US', {
|
|
195
|
+
weekday: 'long',
|
|
196
|
+
year: 'numeric',
|
|
197
|
+
month: 'long',
|
|
198
|
+
day: 'numeric',
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
function formatTime(timestamp) {
|
|
202
|
+
const date = new Date(timestamp);
|
|
203
|
+
return date.toLocaleTimeString('en-US', {
|
|
204
|
+
hour: '2-digit',
|
|
205
|
+
minute: '2-digit',
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
function formatDateTime(timestamp) {
|
|
209
|
+
const date = new Date(timestamp);
|
|
210
|
+
return date.toLocaleString();
|
|
211
|
+
}
|
|
212
|
+
export { historyCommand };
|
|
213
|
+
//# sourceMappingURL=history.js.map
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { getImporter, getAllImporters, isValidSource, } from '../core/importers.js';
|
|
4
|
+
const importCommand = new Command('import')
|
|
5
|
+
.description('从外部工具导入功能数据')
|
|
6
|
+
.argument('<source>', '导入源: jira, linear, github')
|
|
7
|
+
.option('--file <path>', '导入文件路径')
|
|
8
|
+
.option('--output <file>', '输出文件路径', 'features.csv')
|
|
9
|
+
.option('--dry-run', '预览导入结果,不实际写入文件')
|
|
10
|
+
.option('--merge', '合并到现有项目而不是覆盖')
|
|
11
|
+
.action(async (source, options) => {
|
|
12
|
+
try {
|
|
13
|
+
// Validate source
|
|
14
|
+
if (!isValidSource(source)) {
|
|
15
|
+
const availableSources = getAllImporters().map(i => i.source).join(', ');
|
|
16
|
+
console.error(chalk.red(`错误: 不支持的导入源 "${source}"`));
|
|
17
|
+
console.error(chalk.yellow(`可用的导入源: ${availableSources}`));
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
// Validate file option
|
|
21
|
+
if (!options.file) {
|
|
22
|
+
console.error(chalk.red('错误: 必须指定 --file 参数'));
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
const importer = getImporter(source);
|
|
26
|
+
console.log(chalk.blue(`\n📥 ${importer.name}`));
|
|
27
|
+
console.log(chalk.gray(`${importer.description}\n`));
|
|
28
|
+
// Perform import
|
|
29
|
+
const result = await importer.import({
|
|
30
|
+
file: options.file,
|
|
31
|
+
dryRun: options.dryRun || false,
|
|
32
|
+
merge: options.merge || false,
|
|
33
|
+
outputFile: options.output,
|
|
34
|
+
});
|
|
35
|
+
// Display results
|
|
36
|
+
displayImportResult(result, options.dryRun);
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
console.error(chalk.red('导入失败:'), error.message);
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
// Jira subcommand
|
|
44
|
+
const jiraCommand = new Command('jira')
|
|
45
|
+
.description('从 Jira JSON 导出文件导入')
|
|
46
|
+
.option('--file <path>', 'Jira 导出文件路径 (必需)')
|
|
47
|
+
.option('--output <file>', '输出文件路径', 'features.csv')
|
|
48
|
+
.option('--dry-run', '预览导入结果')
|
|
49
|
+
.option('--merge', '合并到现有项目')
|
|
50
|
+
.action(async (options) => {
|
|
51
|
+
await runImport('jira', options);
|
|
52
|
+
});
|
|
53
|
+
// Linear subcommand
|
|
54
|
+
const linearCommand = new Command('linear')
|
|
55
|
+
.description('从 Linear JSON 导出文件导入')
|
|
56
|
+
.option('--file <path>', 'Linear 导出文件路径 (必需)')
|
|
57
|
+
.option('--output <file>', '输出文件路径', 'features.csv')
|
|
58
|
+
.option('--dry-run', '预览导入结果')
|
|
59
|
+
.option('--merge', '合并到现有项目')
|
|
60
|
+
.action(async (options) => {
|
|
61
|
+
await runImport('linear', options);
|
|
62
|
+
});
|
|
63
|
+
// GitHub subcommand
|
|
64
|
+
const githubCommand = new Command('github')
|
|
65
|
+
.description('从 GitHub Issues JSON 文件导入')
|
|
66
|
+
.option('--file <path>', 'GitHub 导出文件路径 (必需)')
|
|
67
|
+
.option('--output <file>', '输出文件路径', 'features.csv')
|
|
68
|
+
.option('--dry-run', '预览导入结果')
|
|
69
|
+
.option('--merge', '合并到现有项目')
|
|
70
|
+
.action(async (options) => {
|
|
71
|
+
await runImport('github', options);
|
|
72
|
+
});
|
|
73
|
+
// Add subcommands
|
|
74
|
+
importCommand.addCommand(jiraCommand);
|
|
75
|
+
importCommand.addCommand(linearCommand);
|
|
76
|
+
importCommand.addCommand(githubCommand);
|
|
77
|
+
async function runImport(source, options) {
|
|
78
|
+
try {
|
|
79
|
+
if (!options.file) {
|
|
80
|
+
console.error(chalk.red('错误: 必须指定 --file 参数'));
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
|
83
|
+
const importer = getImporter(source);
|
|
84
|
+
console.log(chalk.blue(`\n📥 ${importer.name}`));
|
|
85
|
+
console.log(chalk.gray(`${importer.description}\n`));
|
|
86
|
+
const result = await importer.import({
|
|
87
|
+
file: options.file,
|
|
88
|
+
dryRun: options.dryRun || false,
|
|
89
|
+
merge: options.merge || false,
|
|
90
|
+
outputFile: options.output,
|
|
91
|
+
});
|
|
92
|
+
displayImportResult(result, options.dryRun);
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
console.error(chalk.red('导入失败:'), error.message);
|
|
96
|
+
process.exit(1);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function displayImportResult(result, dryRun) {
|
|
100
|
+
const { stats, errors, warnings, features, epics, milestones } = result;
|
|
101
|
+
// Header
|
|
102
|
+
if (dryRun) {
|
|
103
|
+
console.log(chalk.yellow('🔍 预览模式 - 不会写入任何文件\n'));
|
|
104
|
+
}
|
|
105
|
+
// Statistics
|
|
106
|
+
console.log(chalk.blue.bold('📊 导入统计'));
|
|
107
|
+
console.log(chalk.gray('─'.repeat(40)));
|
|
108
|
+
console.log(`总项目数: ${stats.totalItems}`);
|
|
109
|
+
console.log(`导入功能: ${chalk.green(stats.featuresImported)}`);
|
|
110
|
+
console.log(`导入 Epic: ${chalk.cyan(stats.epicsImported)}`);
|
|
111
|
+
console.log(`导入 Milestone: ${chalk.cyan(stats.milestonesImported)}`);
|
|
112
|
+
console.log(`跳过: ${stats.skipped}`);
|
|
113
|
+
console.log(`错误: ${stats.errors > 0 ? chalk.red(stats.errors) : stats.errors}`);
|
|
114
|
+
console.log(chalk.gray('─'.repeat(40)));
|
|
115
|
+
// Epics summary
|
|
116
|
+
if (epics.length > 0) {
|
|
117
|
+
console.log(chalk.cyan.bold('\n📁 Epic/分类'));
|
|
118
|
+
for (const epic of epics) {
|
|
119
|
+
console.log(` • ${epic.name} ${chalk.gray(`(${epic.originalId})`)}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
// Milestones summary
|
|
123
|
+
if (milestones.length > 0) {
|
|
124
|
+
console.log(chalk.cyan.bold('\n🎯 Milestones'));
|
|
125
|
+
for (const milestone of milestones) {
|
|
126
|
+
const dueInfo = milestone.dueDate ? chalk.gray(` 截止: ${milestone.dueDate}`) : '';
|
|
127
|
+
console.log(` • ${milestone.name}${dueInfo}`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
// Features preview (first 5)
|
|
131
|
+
if (features.length > 0) {
|
|
132
|
+
console.log(chalk.green.bold('\n✨ 功能预览 (前5项)'));
|
|
133
|
+
const previewFeatures = features.slice(0, 5);
|
|
134
|
+
for (const feature of previewFeatures) {
|
|
135
|
+
const priorityColor = getPriorityColor(feature.priority);
|
|
136
|
+
const statusColor = getStatusColor(feature.status);
|
|
137
|
+
console.log(` ${chalk.gray(feature.id)} ${feature.name}`);
|
|
138
|
+
console.log(` ${priorityColor(feature.priority)} | ${statusColor(feature.status)} | ${feature.assignee} | ${feature.estimate}h`);
|
|
139
|
+
if (feature.category) {
|
|
140
|
+
console.log(` ${chalk.gray('分类:')} ${feature.category}`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if (features.length > 5) {
|
|
144
|
+
console.log(chalk.gray(` ... 还有 ${features.length - 5} 个功能`));
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
// Errors
|
|
148
|
+
if (errors.length > 0) {
|
|
149
|
+
console.log(chalk.red.bold('\n❌ 错误'));
|
|
150
|
+
for (const error of errors) {
|
|
151
|
+
const location = error.field ? `[${error.field}] ` : '';
|
|
152
|
+
console.log(` • ${location}${error.message}`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
// Warnings
|
|
156
|
+
if (warnings.length > 0) {
|
|
157
|
+
console.log(chalk.yellow.bold('\n⚠️ 警告'));
|
|
158
|
+
for (const warning of warnings) {
|
|
159
|
+
const location = warning.field ? `[${warning.field}] ` : '';
|
|
160
|
+
console.log(` • ${location}${warning.message}`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
// Final status
|
|
164
|
+
console.log();
|
|
165
|
+
if (result.success) {
|
|
166
|
+
if (dryRun) {
|
|
167
|
+
console.log(chalk.green('✓ 预览完成 - 移除 --dry-run 以执行实际导入'));
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
console.log(chalk.green(`✓ 导入成功 - 已保存到 features.csv`));
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
console.log(chalk.red('✗ 导入存在错误,请检查并修复'));
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
function getPriorityColor(priority) {
|
|
178
|
+
switch (priority) {
|
|
179
|
+
case 'critical': return (text) => chalk.red(text);
|
|
180
|
+
case 'high': return (text) => chalk.yellow(text);
|
|
181
|
+
case 'medium': return (text) => chalk.blue(text);
|
|
182
|
+
case 'low': return (text) => chalk.gray(text);
|
|
183
|
+
default: return (text) => text;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
function getStatusColor(status) {
|
|
187
|
+
switch (status) {
|
|
188
|
+
case 'done': return (text) => chalk.green(text);
|
|
189
|
+
case 'in-progress': return (text) => chalk.blue(text);
|
|
190
|
+
case 'blocked': return (text) => chalk.red(text);
|
|
191
|
+
case 'todo': return (text) => chalk.gray(text);
|
|
192
|
+
default: return (text) => text;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
export { importCommand };
|
|
196
|
+
//# sourceMappingURL=import.js.map
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
// Import legacy commands for compatibility
|
|
4
|
+
import { initCommand } from '../commands/init.js';
|
|
5
|
+
import { listCommand } from '../commands/list.js';
|
|
6
|
+
import { showCommand } from '../commands/show.js';
|
|
7
|
+
import { validateCommand } from '../commands/validate.js';
|
|
8
|
+
import { createCommand } from '../commands/create.js';
|
|
9
|
+
import { updateCommand } from '../commands/update.js';
|
|
10
|
+
import { breakdownCommand } from '../commands/breakdown.js';
|
|
11
|
+
import { analyzeCommand } from '../commands/analyze.js';
|
|
12
|
+
export const legacyCommands = new Command('legacy')
|
|
13
|
+
.description('传统命令兼容模式 - 使用完整功能')
|
|
14
|
+
.description('所有原始命令的兼容版本,支持完整的项目管理功能')
|
|
15
|
+
.version('0.1.0');
|
|
16
|
+
// Add all legacy commands
|
|
17
|
+
legacyCommands.addCommand(initCommand);
|
|
18
|
+
legacyCommands.addCommand(listCommand);
|
|
19
|
+
legacyCommands.addCommand(showCommand);
|
|
20
|
+
legacyCommands.addCommand(validateCommand);
|
|
21
|
+
legacyCommands.addCommand(createCommand);
|
|
22
|
+
legacyCommands.addCommand(updateCommand);
|
|
23
|
+
legacyCommands.addCommand(breakdownCommand);
|
|
24
|
+
legacyCommands.addCommand(analyzeCommand);
|
|
25
|
+
// Error handling
|
|
26
|
+
legacyCommands.exitOverride();
|
|
27
|
+
//# sourceMappingURL=index-legacy.js.map
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { mkdir, access, constants } from 'fs/promises';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import ora from 'ora';
|
|
6
|
+
import { writeProjectFile, writeTeamFile } from '../utils/markdown.js';
|
|
7
|
+
export const initCommand = new Command('init')
|
|
8
|
+
.description('Initialize a new PMSpec project')
|
|
9
|
+
.option('-f, --force', 'Overwrite existing project')
|
|
10
|
+
.action(async (options) => {
|
|
11
|
+
const pmspaceDir = join(process.cwd(), 'pmspace');
|
|
12
|
+
const epicsDir = join(pmspaceDir, 'epics');
|
|
13
|
+
const featuresDir = join(pmspaceDir, 'features');
|
|
14
|
+
// Check if already exists
|
|
15
|
+
try {
|
|
16
|
+
await access(pmspaceDir, constants.F_OK);
|
|
17
|
+
if (!options.force) {
|
|
18
|
+
console.error(chalk.red('PMSpec project already exists. Use --force to reinitialize.'));
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
// Directory doesn't exist, proceed
|
|
24
|
+
}
|
|
25
|
+
const spinner = ora('Initializing PMSpec project...').start();
|
|
26
|
+
try {
|
|
27
|
+
// Create directories
|
|
28
|
+
await mkdir(epicsDir, { recursive: true });
|
|
29
|
+
await mkdir(featuresDir, { recursive: true });
|
|
30
|
+
// Create template project.md
|
|
31
|
+
const project = {
|
|
32
|
+
name: 'My Project',
|
|
33
|
+
overview: 'Project overview goes here.',
|
|
34
|
+
timeline: {
|
|
35
|
+
start: new Date().toISOString().split('T')[0],
|
|
36
|
+
end: '',
|
|
37
|
+
},
|
|
38
|
+
teamCapacity: {
|
|
39
|
+
total: 0,
|
|
40
|
+
available: 0,
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
await writeProjectFile(join(pmspaceDir, 'project.md'), project);
|
|
44
|
+
// Create template team.md
|
|
45
|
+
const team = {
|
|
46
|
+
members: [],
|
|
47
|
+
};
|
|
48
|
+
await writeTeamFile(join(pmspaceDir, 'team.md'), team);
|
|
49
|
+
spinner.succeed(chalk.green('PMSpec project initialized in pmspace/'));
|
|
50
|
+
console.log(chalk.gray('\nNext steps:'));
|
|
51
|
+
console.log(chalk.gray(' 1. Edit pmspace/project.md to set up your project'));
|
|
52
|
+
console.log(chalk.gray(' 2. Edit pmspace/team.md to add team members'));
|
|
53
|
+
console.log(chalk.gray(' 3. Run `pmspec create epic` to create your first Epic'));
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
spinner.fail(chalk.red('Failed to initialize project'));
|
|
57
|
+
throw error;
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
//# sourceMappingURL=init.js.map
|
|
@@ -0,0 +1,127 @@
|
|
|
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 listCommand = new Command('list')
|
|
7
|
+
.description('List Epics, Features, or Milestones')
|
|
8
|
+
.argument('[type]', 'Type to list: epics, features, or milestones', 'epics')
|
|
9
|
+
.option('-s, --status <status>', 'Filter by status')
|
|
10
|
+
.option('-a, --assignee <name>', 'Filter by assignee (features only)')
|
|
11
|
+
.action(async (type, options) => {
|
|
12
|
+
const pmspaceDir = join(process.cwd(), 'pmspace');
|
|
13
|
+
try {
|
|
14
|
+
if (type === 'epics') {
|
|
15
|
+
await listEpics(pmspaceDir, options);
|
|
16
|
+
}
|
|
17
|
+
else if (type === 'features') {
|
|
18
|
+
await listFeatures(pmspaceDir, options);
|
|
19
|
+
}
|
|
20
|
+
else if (type === 'milestones') {
|
|
21
|
+
await listMilestones(pmspaceDir, options);
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
console.error(chalk.red(`Unknown type: ${type}. Use 'epics', 'features', or 'milestones'.`));
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
console.error(chalk.red('Error:'), error.message);
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
async function listEpics(pmspaceDir, options) {
|
|
34
|
+
const epicsDir = join(pmspaceDir, 'epics');
|
|
35
|
+
const files = await readdir(epicsDir);
|
|
36
|
+
const epicFiles = files.filter(f => f.endsWith('.md'));
|
|
37
|
+
if (epicFiles.length === 0) {
|
|
38
|
+
console.log(chalk.yellow('No Epics found.'));
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const epics = [];
|
|
42
|
+
for (const file of epicFiles) {
|
|
43
|
+
const epic = await readEpicFile(join(epicsDir, file));
|
|
44
|
+
if (!options.status || epic.status === options.status) {
|
|
45
|
+
epics.push(epic);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
// Sort by ID
|
|
49
|
+
epics.sort((a, b) => a.id.localeCompare(b.id));
|
|
50
|
+
console.log(chalk.bold('\nEpics:\n'));
|
|
51
|
+
console.log(chalk.gray(`${'ID'.padEnd(12)} ${'Title'.padEnd(30)} ${'Status'.padEnd(15)} ${'Owner'.padEnd(15)} ${'Est'.padEnd(8)} ${'Act'.padEnd(8)}`));
|
|
52
|
+
console.log(chalk.gray('-'.repeat(100)));
|
|
53
|
+
for (const epic of epics) {
|
|
54
|
+
const statusColor = epic.status === 'completed' ? chalk.green : epic.status === 'in-progress' ? chalk.yellow : chalk.white;
|
|
55
|
+
console.log(`${epic.id.padEnd(12)} ${epic.title.substring(0, 30).padEnd(30)} ${statusColor(epic.status.padEnd(15))} ${(epic.owner || '-').padEnd(15)} ${`${epic.estimate}h`.padEnd(8)} ${`${epic.actual}h`.padEnd(8)}`);
|
|
56
|
+
}
|
|
57
|
+
console.log(chalk.gray(`\nTotal: ${epics.length} Epic(s)`));
|
|
58
|
+
}
|
|
59
|
+
async function listFeatures(pmspaceDir, options) {
|
|
60
|
+
const featuresDir = join(pmspaceDir, 'features');
|
|
61
|
+
const files = await readdir(featuresDir);
|
|
62
|
+
const featureFiles = files.filter(f => f.endsWith('.md'));
|
|
63
|
+
if (featureFiles.length === 0) {
|
|
64
|
+
console.log(chalk.yellow('No Features found.'));
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const features = [];
|
|
68
|
+
for (const file of featureFiles) {
|
|
69
|
+
const feature = await readFeatureFile(join(featuresDir, file));
|
|
70
|
+
let include = true;
|
|
71
|
+
if (options.status && feature.status !== options.status)
|
|
72
|
+
include = false;
|
|
73
|
+
if (options.assignee && feature.assignee !== options.assignee)
|
|
74
|
+
include = false;
|
|
75
|
+
if (include) {
|
|
76
|
+
features.push(feature);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
// Sort by ID
|
|
80
|
+
features.sort((a, b) => a.id.localeCompare(b.id));
|
|
81
|
+
console.log(chalk.bold('\nFeatures:\n'));
|
|
82
|
+
console.log(chalk.gray(`${'ID'.padEnd(12)} ${'Title'.padEnd(30)} ${'Status'.padEnd(15)} ${'Assignee'.padEnd(15)} ${'Est'.padEnd(8)}`));
|
|
83
|
+
console.log(chalk.gray('-'.repeat(100)));
|
|
84
|
+
for (const feature of features) {
|
|
85
|
+
const statusColor = feature.status === 'done' ? chalk.green : feature.status === 'in-progress' ? chalk.yellow : chalk.white;
|
|
86
|
+
console.log(`${feature.id.padEnd(12)} ${feature.title.substring(0, 30).padEnd(30)} ${statusColor(feature.status.padEnd(15))} ${(feature.assignee || '-').padEnd(15)} ${`${feature.estimate}h`.padEnd(8)}`);
|
|
87
|
+
}
|
|
88
|
+
const totalHours = features.reduce((sum, f) => sum + f.estimate, 0);
|
|
89
|
+
console.log(chalk.gray(`\nTotal: ${features.length} Feature(s), ${totalHours}h estimated`));
|
|
90
|
+
}
|
|
91
|
+
async function listMilestones(pmspaceDir, options) {
|
|
92
|
+
const milestonesDir = join(pmspaceDir, 'milestones');
|
|
93
|
+
let files = [];
|
|
94
|
+
try {
|
|
95
|
+
files = await readdir(milestonesDir);
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
console.log(chalk.yellow('No Milestones found. Directory does not exist.'));
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
const milestoneFiles = files.filter(f => f.endsWith('.md'));
|
|
102
|
+
if (milestoneFiles.length === 0) {
|
|
103
|
+
console.log(chalk.yellow('No Milestones found.'));
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
const milestones = [];
|
|
107
|
+
for (const file of milestoneFiles) {
|
|
108
|
+
const milestone = await readMilestoneFile(join(milestonesDir, file));
|
|
109
|
+
if (!options.status || milestone.status === options.status) {
|
|
110
|
+
milestones.push(milestone);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
// Sort by target date
|
|
114
|
+
milestones.sort((a, b) => a.targetDate.localeCompare(b.targetDate));
|
|
115
|
+
console.log(chalk.bold('\nMilestones:\n'));
|
|
116
|
+
console.log(chalk.gray(`${'ID'.padEnd(12)} ${'Title'.padEnd(30)} ${'Target Date'.padEnd(15)} ${'Status'.padEnd(12)} ${'Features'.padEnd(10)}`));
|
|
117
|
+
console.log(chalk.gray('-'.repeat(90)));
|
|
118
|
+
for (const milestone of milestones) {
|
|
119
|
+
const statusColor = milestone.status === 'completed' ? chalk.green :
|
|
120
|
+
milestone.status === 'active' ? chalk.yellow :
|
|
121
|
+
milestone.status === 'missed' ? chalk.red :
|
|
122
|
+
chalk.white;
|
|
123
|
+
console.log(`${milestone.id.padEnd(12)} ${milestone.title.substring(0, 30).padEnd(30)} ${milestone.targetDate.padEnd(15)} ${statusColor(milestone.status.padEnd(12))} ${`${milestone.features.length}`.padEnd(10)}`);
|
|
124
|
+
}
|
|
125
|
+
console.log(chalk.gray(`\nTotal: ${milestones.length} Milestone(s)`));
|
|
126
|
+
}
|
|
127
|
+
//# sourceMappingURL=list.js.map
|