@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,529 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import inquirer from 'inquirer';
|
|
4
|
+
import { mkdir, readdir } from 'fs/promises';
|
|
5
|
+
import { join } from 'path';
|
|
6
|
+
import { EpicSchema, FeatureSchema, UserStorySchema, MilestoneSchema, generateNextId } from '../core/project.js';
|
|
7
|
+
import { writeEpicFile, writeFeatureFile, writeMilestoneFile } from '../utils/markdown.js';
|
|
8
|
+
import { readEpicFile, readFeatureFile, readMilestoneFile } from '../core/parser.js';
|
|
9
|
+
import { getChangelogService } from '../core/changelog-service.js';
|
|
10
|
+
const createCommand = new Command('create')
|
|
11
|
+
.description('Create new Epic, Feature, User Story, or Milestone')
|
|
12
|
+
.argument('<type>', 'Type of item to create (epic, feature, story, milestone)')
|
|
13
|
+
.option('-e, --epic <id>', 'Epic ID for Feature (required for feature)')
|
|
14
|
+
.option('-f, --feature <id>', 'Feature ID for Story (required for story)')
|
|
15
|
+
.option('--non-interactive', 'Skip prompts and use defaults')
|
|
16
|
+
.action(async (type, options, command) => {
|
|
17
|
+
try {
|
|
18
|
+
type = type.toLowerCase();
|
|
19
|
+
if (!['epic', 'feature', 'story', 'milestone'].includes(type)) {
|
|
20
|
+
console.error(chalk.red('Error: Type must be epic, feature, story, or milestone'));
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
// Check if pmspec directory exists
|
|
24
|
+
try {
|
|
25
|
+
await readdir('pmspace');
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
console.error(chalk.red('Error: pmspec directory not found. Run "pmspec init" first.'));
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
if (type === 'epic') {
|
|
32
|
+
await createEpic(options.nonInteractive);
|
|
33
|
+
}
|
|
34
|
+
else if (type === 'feature') {
|
|
35
|
+
await createFeature(options.epic, options.nonInteractive);
|
|
36
|
+
}
|
|
37
|
+
else if (type === 'story') {
|
|
38
|
+
await createStory(options.feature, options.nonInteractive);
|
|
39
|
+
}
|
|
40
|
+
else if (type === 'milestone') {
|
|
41
|
+
await createMilestone(options.nonInteractive);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
console.error(chalk.red('Error:'), error.message);
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
async function createEpic(nonInteractive) {
|
|
50
|
+
// Get existing epics to determine next ID
|
|
51
|
+
let existingEpics = [];
|
|
52
|
+
try {
|
|
53
|
+
const epicFiles = await readdir('pmspace/epics');
|
|
54
|
+
for (const file of epicFiles) {
|
|
55
|
+
if (file.endsWith('.md')) {
|
|
56
|
+
const content = await readEpicFile(join('pmspace/epics', file));
|
|
57
|
+
existingEpics.push(content.id);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
// Directory might not exist or be empty
|
|
63
|
+
}
|
|
64
|
+
const epicId = generateNextId('EPIC', existingEpics);
|
|
65
|
+
if (nonInteractive) {
|
|
66
|
+
const epic = EpicSchema.parse({
|
|
67
|
+
id: epicId,
|
|
68
|
+
title: 'New Epic',
|
|
69
|
+
status: 'planning',
|
|
70
|
+
estimate: 40,
|
|
71
|
+
description: 'Epic description',
|
|
72
|
+
features: []
|
|
73
|
+
});
|
|
74
|
+
await writeEpicFile(`pmspace/epics/${epicId.toLowerCase()}.md`, epic);
|
|
75
|
+
// Record changelog entry
|
|
76
|
+
try {
|
|
77
|
+
await getChangelogService().recordCreate('epic', epicId);
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
// Silently fail if changelog can't be written
|
|
81
|
+
}
|
|
82
|
+
console.log(chalk.green(`✓ Created Epic ${epicId}`));
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const answers = await inquirer.prompt([
|
|
86
|
+
{
|
|
87
|
+
type: 'input',
|
|
88
|
+
name: 'title',
|
|
89
|
+
message: 'Epic title:',
|
|
90
|
+
validate: (input) => input.trim() !== '' || 'Title is required'
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
type: 'list',
|
|
94
|
+
name: 'status',
|
|
95
|
+
message: 'Status:',
|
|
96
|
+
choices: ['planning', 'in-progress', 'completed'],
|
|
97
|
+
default: 'planning'
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
type: 'input',
|
|
101
|
+
name: 'owner',
|
|
102
|
+
message: 'Owner (optional):'
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
type: 'number',
|
|
106
|
+
name: 'estimate',
|
|
107
|
+
message: 'Estimate (hours):',
|
|
108
|
+
validate: (input) => input > 0 || 'Estimate must be positive',
|
|
109
|
+
default: 40
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
type: 'input',
|
|
113
|
+
name: 'description',
|
|
114
|
+
message: 'Description (optional):'
|
|
115
|
+
}
|
|
116
|
+
]);
|
|
117
|
+
const epic = EpicSchema.parse({
|
|
118
|
+
id: epicId,
|
|
119
|
+
title: answers.title.trim(),
|
|
120
|
+
status: answers.status,
|
|
121
|
+
owner: answers.owner?.trim() || undefined,
|
|
122
|
+
estimate: answers.estimate,
|
|
123
|
+
description: answers.description?.trim() || undefined,
|
|
124
|
+
features: []
|
|
125
|
+
});
|
|
126
|
+
await writeEpicFile(`pmspace/epics/${epicId.toLowerCase()}.md`, epic);
|
|
127
|
+
// Record changelog entry
|
|
128
|
+
try {
|
|
129
|
+
await getChangelogService().recordCreate('epic', epicId);
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
// Silently fail if changelog can't be written
|
|
133
|
+
}
|
|
134
|
+
console.log(chalk.green(`✓ Created Epic ${epicId}: ${epic.title}`));
|
|
135
|
+
}
|
|
136
|
+
async function createFeature(epicId, nonInteractive) {
|
|
137
|
+
if (!epicId) {
|
|
138
|
+
if (nonInteractive) {
|
|
139
|
+
console.error(chalk.red('Error: Epic ID is required for feature creation'));
|
|
140
|
+
process.exit(1);
|
|
141
|
+
}
|
|
142
|
+
// Get existing epics for selection
|
|
143
|
+
const epics = [];
|
|
144
|
+
try {
|
|
145
|
+
const epicFiles = await readdir('pmspace/epics');
|
|
146
|
+
for (const file of epicFiles) {
|
|
147
|
+
if (file.endsWith('.md')) {
|
|
148
|
+
const content = await readEpicFile(join('pmspace/epics', file));
|
|
149
|
+
epics.push({ id: content.id, title: content.title });
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
console.error(chalk.red('Error: No epics found. Create an epic first.'));
|
|
155
|
+
process.exit(1);
|
|
156
|
+
}
|
|
157
|
+
if (epics.length === 0) {
|
|
158
|
+
console.error(chalk.red('Error: No epics found. Create an epic first.'));
|
|
159
|
+
process.exit(1);
|
|
160
|
+
}
|
|
161
|
+
const answer = await inquirer.prompt([
|
|
162
|
+
{
|
|
163
|
+
type: 'list',
|
|
164
|
+
name: 'epicId',
|
|
165
|
+
message: 'Select Epic:',
|
|
166
|
+
choices: epics.map(e => ({ name: `${e.id}: ${e.title}`, value: e.id }))
|
|
167
|
+
}
|
|
168
|
+
]);
|
|
169
|
+
epicId = answer.epicId;
|
|
170
|
+
}
|
|
171
|
+
// Validate epic exists
|
|
172
|
+
try {
|
|
173
|
+
await readEpicFile(`pmspace/epics/${epicId.toLowerCase()}.md`);
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
console.error(chalk.red(`Error: Epic ${epicId} not found`));
|
|
177
|
+
process.exit(1);
|
|
178
|
+
}
|
|
179
|
+
// Get existing features to determine next ID
|
|
180
|
+
let existingFeatures = [];
|
|
181
|
+
try {
|
|
182
|
+
const featureFiles = await readdir('pmspace/features');
|
|
183
|
+
for (const file of featureFiles) {
|
|
184
|
+
if (file.endsWith('.md')) {
|
|
185
|
+
const content = await readFeatureFile(join('pmspace/features', file));
|
|
186
|
+
existingFeatures.push(content.id);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
// Directory might not exist or be empty
|
|
192
|
+
}
|
|
193
|
+
const featureId = generateNextId('FEAT', existingFeatures);
|
|
194
|
+
if (nonInteractive) {
|
|
195
|
+
const feature = FeatureSchema.parse({
|
|
196
|
+
id: featureId,
|
|
197
|
+
title: 'New Feature',
|
|
198
|
+
epicId: epicId,
|
|
199
|
+
status: 'todo',
|
|
200
|
+
estimate: 16,
|
|
201
|
+
skillsRequired: [],
|
|
202
|
+
description: 'Feature description',
|
|
203
|
+
userStories: [],
|
|
204
|
+
acceptanceCriteria: []
|
|
205
|
+
});
|
|
206
|
+
await writeFeatureFile(`pmspace/features/${featureId.toLowerCase()}.md`, feature);
|
|
207
|
+
// Record changelog entry
|
|
208
|
+
try {
|
|
209
|
+
await getChangelogService().recordCreate('feature', featureId);
|
|
210
|
+
}
|
|
211
|
+
catch {
|
|
212
|
+
// Silently fail if changelog can't be written
|
|
213
|
+
}
|
|
214
|
+
console.log(chalk.green(`✓ Created Feature ${featureId} under Epic ${epicId}`));
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
const answers = await inquirer.prompt([
|
|
218
|
+
{
|
|
219
|
+
type: 'input',
|
|
220
|
+
name: 'title',
|
|
221
|
+
message: 'Feature title:',
|
|
222
|
+
validate: (input) => input.trim() !== '' || 'Title is required'
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
type: 'list',
|
|
226
|
+
name: 'status',
|
|
227
|
+
message: 'Status:',
|
|
228
|
+
choices: ['todo', 'in-progress', 'done'],
|
|
229
|
+
default: 'todo'
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
type: 'input',
|
|
233
|
+
name: 'assignee',
|
|
234
|
+
message: 'Assignee (optional):'
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
type: 'number',
|
|
238
|
+
name: 'estimate',
|
|
239
|
+
message: 'Estimate (hours):',
|
|
240
|
+
validate: (input) => input > 0 || 'Estimate must be positive',
|
|
241
|
+
default: 16
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
type: 'input',
|
|
245
|
+
name: 'skillsRequired',
|
|
246
|
+
message: 'Skills required (comma-separated, optional):',
|
|
247
|
+
filter: (input) => input.split(',').map((s) => s.trim()).filter((s) => s !== '')
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
type: 'input',
|
|
251
|
+
name: 'description',
|
|
252
|
+
message: 'Description (optional):'
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
type: 'input',
|
|
256
|
+
name: 'acceptanceCriteria',
|
|
257
|
+
message: 'Acceptance criteria (one per line, optional):',
|
|
258
|
+
filter: (input) => input.split('\n').map((s) => s.trim()).filter((s) => s !== '')
|
|
259
|
+
}
|
|
260
|
+
]);
|
|
261
|
+
const feature = FeatureSchema.parse({
|
|
262
|
+
id: featureId,
|
|
263
|
+
title: answers.title.trim(),
|
|
264
|
+
epicId: epicId,
|
|
265
|
+
status: answers.status,
|
|
266
|
+
assignee: answers.assignee?.trim() || undefined,
|
|
267
|
+
estimate: answers.estimate,
|
|
268
|
+
skillsRequired: answers.skillsRequired,
|
|
269
|
+
description: answers.description?.trim() || undefined,
|
|
270
|
+
userStories: [],
|
|
271
|
+
acceptanceCriteria: answers.acceptanceCriteria
|
|
272
|
+
});
|
|
273
|
+
await writeFeatureFile(`pmspace/features/${featureId.toLowerCase()}.md`, feature);
|
|
274
|
+
// Update epic to include this feature
|
|
275
|
+
const epicPath = `pmspace/epics/${epicId.toLowerCase()}.md`;
|
|
276
|
+
const epic = await readEpicFile(epicPath);
|
|
277
|
+
if (!epic.features.includes(featureId)) {
|
|
278
|
+
epic.features.push(featureId);
|
|
279
|
+
await writeEpicFile(epicPath, epic);
|
|
280
|
+
}
|
|
281
|
+
// Record changelog entry
|
|
282
|
+
try {
|
|
283
|
+
await getChangelogService().recordCreate('feature', featureId);
|
|
284
|
+
}
|
|
285
|
+
catch {
|
|
286
|
+
// Silently fail if changelog can't be written
|
|
287
|
+
}
|
|
288
|
+
console.log(chalk.green(`✓ Created Feature ${featureId}: ${feature.title} under Epic ${epicId}`));
|
|
289
|
+
}
|
|
290
|
+
async function createStory(featureId, nonInteractive) {
|
|
291
|
+
if (!featureId) {
|
|
292
|
+
if (nonInteractive) {
|
|
293
|
+
console.error(chalk.red('Error: Feature ID is required for story creation'));
|
|
294
|
+
process.exit(1);
|
|
295
|
+
}
|
|
296
|
+
// Get existing features for selection
|
|
297
|
+
const features = [];
|
|
298
|
+
try {
|
|
299
|
+
const featureFiles = await readdir('pmspace/features');
|
|
300
|
+
for (const file of featureFiles) {
|
|
301
|
+
if (file.endsWith('.md')) {
|
|
302
|
+
const content = await readFeatureFile(join('pmspace/features', file));
|
|
303
|
+
features.push({ id: content.id, title: content.title });
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
catch {
|
|
308
|
+
console.error(chalk.red('Error: No features found. Create a feature first.'));
|
|
309
|
+
process.exit(1);
|
|
310
|
+
}
|
|
311
|
+
if (features.length === 0) {
|
|
312
|
+
console.error(chalk.red('Error: No features found. Create a feature first.'));
|
|
313
|
+
process.exit(1);
|
|
314
|
+
}
|
|
315
|
+
const answer = await inquirer.prompt([
|
|
316
|
+
{
|
|
317
|
+
type: 'list',
|
|
318
|
+
name: 'featureId',
|
|
319
|
+
message: 'Select Feature:',
|
|
320
|
+
choices: features.map(f => ({ name: `${f.id}: ${f.title}`, value: f.id }))
|
|
321
|
+
}
|
|
322
|
+
]);
|
|
323
|
+
featureId = answer.featureId;
|
|
324
|
+
}
|
|
325
|
+
// Validate feature exists
|
|
326
|
+
try {
|
|
327
|
+
await readFeatureFile(`pmspace/features/${featureId.toLowerCase()}.md`);
|
|
328
|
+
}
|
|
329
|
+
catch {
|
|
330
|
+
console.error(chalk.red(`Error: Feature ${featureId} not found`));
|
|
331
|
+
process.exit(1);
|
|
332
|
+
}
|
|
333
|
+
// Get existing stories to determine next ID
|
|
334
|
+
let existingStories = [];
|
|
335
|
+
try {
|
|
336
|
+
const featureFiles = await readdir('pmspace/features');
|
|
337
|
+
for (const file of featureFiles) {
|
|
338
|
+
if (file.endsWith('.md')) {
|
|
339
|
+
const content = await readFeatureFile(join('pmspace/features', file));
|
|
340
|
+
existingStories.push(...content.userStories.map((s) => s.id));
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
catch {
|
|
345
|
+
// Directory might not exist or be empty
|
|
346
|
+
}
|
|
347
|
+
const storyId = generateNextId('STORY', existingStories);
|
|
348
|
+
if (nonInteractive) {
|
|
349
|
+
const story = UserStorySchema.parse({
|
|
350
|
+
id: storyId,
|
|
351
|
+
title: 'New User Story',
|
|
352
|
+
estimate: 4,
|
|
353
|
+
status: 'todo',
|
|
354
|
+
featureId: featureId,
|
|
355
|
+
description: 'User story description'
|
|
356
|
+
});
|
|
357
|
+
// Update feature to include this story
|
|
358
|
+
const featurePath = `pmspace/features/${featureId.toLowerCase()}.md`;
|
|
359
|
+
const feature = await readFeatureFile(featurePath);
|
|
360
|
+
feature.userStories.push(story);
|
|
361
|
+
await writeFeatureFile(featurePath, feature);
|
|
362
|
+
// Record changelog entry
|
|
363
|
+
try {
|
|
364
|
+
await getChangelogService().recordCreate('story', storyId);
|
|
365
|
+
}
|
|
366
|
+
catch {
|
|
367
|
+
// Silently fail if changelog can't be written
|
|
368
|
+
}
|
|
369
|
+
console.log(chalk.green(`✓ Created User Story ${storyId} under Feature ${featureId}`));
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
const answers = await inquirer.prompt([
|
|
373
|
+
{
|
|
374
|
+
type: 'input',
|
|
375
|
+
name: 'title',
|
|
376
|
+
message: 'User Story title:',
|
|
377
|
+
validate: (input) => input.trim() !== '' || 'Title is required'
|
|
378
|
+
},
|
|
379
|
+
{
|
|
380
|
+
type: 'number',
|
|
381
|
+
name: 'estimate',
|
|
382
|
+
message: 'Estimate (hours):',
|
|
383
|
+
validate: (input) => input > 0 || 'Estimate must be positive',
|
|
384
|
+
default: 4
|
|
385
|
+
},
|
|
386
|
+
{
|
|
387
|
+
type: 'list',
|
|
388
|
+
name: 'status',
|
|
389
|
+
message: 'Status:',
|
|
390
|
+
choices: ['todo', 'in-progress', 'done'],
|
|
391
|
+
default: 'todo'
|
|
392
|
+
},
|
|
393
|
+
{
|
|
394
|
+
type: 'input',
|
|
395
|
+
name: 'description',
|
|
396
|
+
message: 'Description (optional):'
|
|
397
|
+
}
|
|
398
|
+
]);
|
|
399
|
+
const story = UserStorySchema.parse({
|
|
400
|
+
id: storyId,
|
|
401
|
+
title: answers.title.trim(),
|
|
402
|
+
estimate: answers.estimate,
|
|
403
|
+
status: answers.status,
|
|
404
|
+
featureId: featureId,
|
|
405
|
+
description: answers.description?.trim() || undefined
|
|
406
|
+
});
|
|
407
|
+
// Update feature to include this story
|
|
408
|
+
const featurePath = `pmspace/features/${featureId.toLowerCase()}.md`;
|
|
409
|
+
const feature = await readFeatureFile(featurePath);
|
|
410
|
+
feature.userStories.push(story);
|
|
411
|
+
await writeFeatureFile(featurePath, feature);
|
|
412
|
+
// Record changelog entry
|
|
413
|
+
try {
|
|
414
|
+
await getChangelogService().recordCreate('story', storyId);
|
|
415
|
+
}
|
|
416
|
+
catch {
|
|
417
|
+
// Silently fail if changelog can't be written
|
|
418
|
+
}
|
|
419
|
+
console.log(chalk.green(`✓ Created User Story ${storyId}: ${story.title} under Feature ${featureId}`));
|
|
420
|
+
}
|
|
421
|
+
async function createMilestone(nonInteractive) {
|
|
422
|
+
// Get existing milestones to determine next ID
|
|
423
|
+
let existingMilestones = [];
|
|
424
|
+
try {
|
|
425
|
+
const milestoneFiles = await readdir('pmspace/milestones');
|
|
426
|
+
for (const file of milestoneFiles) {
|
|
427
|
+
if (file.endsWith('.md')) {
|
|
428
|
+
const content = await readMilestoneFile(join('pmspace/milestones', file));
|
|
429
|
+
existingMilestones.push(content.id);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
catch {
|
|
434
|
+
// Directory might not exist or be empty - create it
|
|
435
|
+
await mkdir('pmspace/milestones', { recursive: true });
|
|
436
|
+
}
|
|
437
|
+
const milestoneId = generateNextId('MILE', existingMilestones);
|
|
438
|
+
if (nonInteractive) {
|
|
439
|
+
const today = new Date();
|
|
440
|
+
const targetDate = new Date(today.getTime() + 30 * 24 * 60 * 60 * 1000); // 30 days from now
|
|
441
|
+
const milestone = MilestoneSchema.parse({
|
|
442
|
+
id: milestoneId,
|
|
443
|
+
title: 'New Milestone',
|
|
444
|
+
status: 'upcoming',
|
|
445
|
+
targetDate: targetDate.toISOString().split('T')[0],
|
|
446
|
+
description: 'Milestone description',
|
|
447
|
+
features: []
|
|
448
|
+
});
|
|
449
|
+
await writeMilestoneFile(`pmspace/milestones/${milestoneId.toLowerCase()}.md`, milestone);
|
|
450
|
+
// Record changelog entry
|
|
451
|
+
try {
|
|
452
|
+
await getChangelogService().recordCreate('milestone', milestoneId);
|
|
453
|
+
}
|
|
454
|
+
catch {
|
|
455
|
+
// Silently fail if changelog can't be written
|
|
456
|
+
}
|
|
457
|
+
console.log(chalk.green(`✓ Created Milestone ${milestoneId}`));
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
// Get available features for selection
|
|
461
|
+
const features = [];
|
|
462
|
+
try {
|
|
463
|
+
const featureFiles = await readdir('pmspace/features');
|
|
464
|
+
for (const file of featureFiles) {
|
|
465
|
+
if (file.endsWith('.md')) {
|
|
466
|
+
const content = await readFeatureFile(join('pmspace/features', file));
|
|
467
|
+
features.push({ id: content.id, title: content.title });
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
catch {
|
|
472
|
+
// No features yet
|
|
473
|
+
}
|
|
474
|
+
const answers = await inquirer.prompt([
|
|
475
|
+
{
|
|
476
|
+
type: 'input',
|
|
477
|
+
name: 'title',
|
|
478
|
+
message: 'Milestone title:',
|
|
479
|
+
validate: (input) => input.trim() !== '' || 'Title is required'
|
|
480
|
+
},
|
|
481
|
+
{
|
|
482
|
+
type: 'input',
|
|
483
|
+
name: 'targetDate',
|
|
484
|
+
message: 'Target date (YYYY-MM-DD):',
|
|
485
|
+
validate: (input) => {
|
|
486
|
+
const dateRegex = /^\d{4}-\d{2}-\d{2}$/;
|
|
487
|
+
return dateRegex.test(input) || 'Date must be in YYYY-MM-DD format';
|
|
488
|
+
}
|
|
489
|
+
},
|
|
490
|
+
{
|
|
491
|
+
type: 'list',
|
|
492
|
+
name: 'status',
|
|
493
|
+
message: 'Status:',
|
|
494
|
+
choices: ['upcoming', 'active', 'completed', 'missed'],
|
|
495
|
+
default: 'upcoming'
|
|
496
|
+
},
|
|
497
|
+
{
|
|
498
|
+
type: 'input',
|
|
499
|
+
name: 'description',
|
|
500
|
+
message: 'Description (optional):'
|
|
501
|
+
},
|
|
502
|
+
{
|
|
503
|
+
type: 'checkbox',
|
|
504
|
+
name: 'features',
|
|
505
|
+
message: 'Select features for this milestone:',
|
|
506
|
+
choices: features.map(f => ({ name: `${f.id}: ${f.title}`, value: f.id })),
|
|
507
|
+
when: () => features.length > 0
|
|
508
|
+
}
|
|
509
|
+
]);
|
|
510
|
+
const milestone = MilestoneSchema.parse({
|
|
511
|
+
id: milestoneId,
|
|
512
|
+
title: answers.title.trim(),
|
|
513
|
+
status: answers.status,
|
|
514
|
+
targetDate: answers.targetDate,
|
|
515
|
+
description: answers.description?.trim() || undefined,
|
|
516
|
+
features: answers.features || []
|
|
517
|
+
});
|
|
518
|
+
await writeMilestoneFile(`pmspace/milestones/${milestoneId.toLowerCase()}.md`, milestone);
|
|
519
|
+
// Record changelog entry
|
|
520
|
+
try {
|
|
521
|
+
await getChangelogService().recordCreate('milestone', milestoneId);
|
|
522
|
+
}
|
|
523
|
+
catch {
|
|
524
|
+
// Silently fail if changelog can't be written
|
|
525
|
+
}
|
|
526
|
+
console.log(chalk.green(`✓ Created Milestone ${milestoneId}: ${milestone.title}`));
|
|
527
|
+
}
|
|
528
|
+
export { createCommand };
|
|
529
|
+
//# sourceMappingURL=create.js.map
|