@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,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
@@ -0,0 +1,4 @@
1
+ import { Command } from 'commander';
2
+ declare const historyCommand: Command;
3
+ export { historyCommand };
4
+ //# sourceMappingURL=history.d.ts.map