@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,715 @@
1
+ /**
2
+ * External Tool Importers Module
3
+ *
4
+ * Provides import functionality from external project management tools:
5
+ * - Jira: Epic → Category, Story/Task → Feature
6
+ * - Linear: Project → Epic, Issue → Feature
7
+ * - GitHub Issues: Milestone → Milestone, Issue → Feature, Labels → Skills/Tags
8
+ *
9
+ * Run `node setup-importers.js` from project root to extract this into
10
+ * separate files under src/importers/ directory.
11
+ */
12
+ import { z } from 'zod';
13
+ import { readFile } from 'fs/promises';
14
+ import { CSVHandler } from '../utils/csv-handler.js';
15
+ export const ImportOptionsSchema = z.object({
16
+ file: z.string().optional(),
17
+ content: z.string().optional(),
18
+ dryRun: z.boolean().default(false),
19
+ merge: z.boolean().default(false),
20
+ outputFile: z.string().default('features.csv'),
21
+ });
22
+ // ============================================================================
23
+ // Jira Types
24
+ // ============================================================================
25
+ export const JiraIssueSchema = z.object({
26
+ key: z.string(),
27
+ fields: z.object({
28
+ summary: z.string(),
29
+ description: z.string().nullable().optional(),
30
+ issuetype: z.object({ name: z.string() }),
31
+ status: z.object({ name: z.string() }),
32
+ priority: z.object({ name: z.string() }).optional(),
33
+ assignee: z.object({ displayName: z.string() }).nullable().optional(),
34
+ labels: z.array(z.string()).default([]),
35
+ timeoriginalestimate: z.number().nullable().optional(),
36
+ created: z.string().optional(),
37
+ duedate: z.string().nullable().optional(),
38
+ parent: z.object({
39
+ key: z.string(),
40
+ fields: z.object({
41
+ summary: z.string(),
42
+ issuetype: z.object({ name: z.string() }),
43
+ }),
44
+ }).optional(),
45
+ customfield_10014: z.string().nullable().optional(),
46
+ }),
47
+ });
48
+ export const JiraExportSchema = z.object({ issues: z.array(JiraIssueSchema) });
49
+ // ============================================================================
50
+ // Linear Types
51
+ // ============================================================================
52
+ export const LinearIssueSchema = z.object({
53
+ id: z.string(),
54
+ identifier: z.string(),
55
+ title: z.string(),
56
+ description: z.string().nullable().optional(),
57
+ state: z.object({ name: z.string(), type: z.string() }),
58
+ priority: z.number(),
59
+ priorityLabel: z.string(),
60
+ estimate: z.number().nullable().optional(),
61
+ assignee: z.object({ name: z.string() }).nullable().optional(),
62
+ labels: z.object({ nodes: z.array(z.object({ name: z.string() })) }).optional(),
63
+ createdAt: z.string().optional(),
64
+ dueDate: z.string().nullable().optional(),
65
+ project: z.object({
66
+ id: z.string(),
67
+ name: z.string(),
68
+ description: z.string().nullable().optional(),
69
+ }).nullable().optional(),
70
+ });
71
+ export const LinearExportSchema = z.object({
72
+ issues: z.array(LinearIssueSchema),
73
+ projects: z.array(z.object({
74
+ id: z.string(),
75
+ name: z.string(),
76
+ description: z.string().nullable().optional(),
77
+ })).optional(),
78
+ });
79
+ // ============================================================================
80
+ // GitHub Types
81
+ // ============================================================================
82
+ export const GitHubIssueSchema = z.object({
83
+ number: z.number(),
84
+ title: z.string(),
85
+ body: z.string().nullable().optional(),
86
+ state: z.enum(['open', 'closed']),
87
+ labels: z.array(z.object({ name: z.string() })).default([]),
88
+ assignee: z.object({ login: z.string() }).nullable().optional(),
89
+ assignees: z.array(z.object({ login: z.string() })).optional(),
90
+ milestone: z.object({
91
+ number: z.number(),
92
+ title: z.string(),
93
+ description: z.string().nullable().optional(),
94
+ due_on: z.string().nullable().optional(),
95
+ }).nullable().optional(),
96
+ created_at: z.string().optional(),
97
+ });
98
+ export const GitHubExportSchema = z.object({
99
+ issues: z.array(GitHubIssueSchema),
100
+ milestones: z.array(z.object({
101
+ number: z.number(),
102
+ title: z.string(),
103
+ description: z.string().nullable().optional(),
104
+ due_on: z.string().nullable().optional(),
105
+ })).optional(),
106
+ });
107
+ // ============================================================================
108
+ // Base Importer
109
+ // ============================================================================
110
+ class BaseImporter {
111
+ async getContent(options) {
112
+ if (options.content)
113
+ return options.content;
114
+ if (options.file)
115
+ return await readFile(options.file, 'utf-8');
116
+ throw new Error('Either file path or content must be provided');
117
+ }
118
+ parseJSON(content) {
119
+ try {
120
+ return JSON.parse(content);
121
+ }
122
+ catch (error) {
123
+ throw new Error(`Invalid JSON format: ${error instanceof Error ? error.message : 'Unknown error'}`);
124
+ }
125
+ }
126
+ createEmptyResult() {
127
+ return {
128
+ success: false,
129
+ source: this.source,
130
+ features: [],
131
+ epics: [],
132
+ milestones: [],
133
+ errors: [],
134
+ warnings: [],
135
+ stats: { totalItems: 0, featuresImported: 0, epicsImported: 0, milestonesImported: 0, skipped: 0, errors: 0 },
136
+ };
137
+ }
138
+ generateFeatureId(prefix, index) {
139
+ return `${prefix}-${String(index).padStart(3, '0')}`;
140
+ }
141
+ mapPriority(priority) {
142
+ const normalized = priority.toLowerCase();
143
+ if (['critical', 'blocker', 'highest', 'urgent', '1'].includes(normalized))
144
+ return 'critical';
145
+ if (['high', '2'].includes(normalized))
146
+ return 'high';
147
+ if (['low', 'lowest', 'trivial', '4', '5'].includes(normalized))
148
+ return 'low';
149
+ return 'medium';
150
+ }
151
+ mapStatus(status) {
152
+ const normalized = status.toLowerCase();
153
+ if (['done', 'closed', 'completed', 'resolved', 'finished'].includes(normalized))
154
+ return 'done';
155
+ if (['in progress', 'in-progress', 'inprogress', 'started', 'active', 'working'].includes(normalized))
156
+ return 'in-progress';
157
+ if (['blocked', 'on hold', 'waiting', 'pending'].includes(normalized))
158
+ return 'blocked';
159
+ return 'todo';
160
+ }
161
+ formatDate(date) {
162
+ if (!date)
163
+ return undefined;
164
+ try {
165
+ return new Date(date).toISOString().split('T')[0];
166
+ }
167
+ catch {
168
+ return undefined;
169
+ }
170
+ }
171
+ convertToHours(seconds) {
172
+ if (!seconds)
173
+ return 8;
174
+ return Math.ceil(seconds / 3600);
175
+ }
176
+ sanitizeId(name) {
177
+ return name.toLowerCase().replace(/[^a-z0-9\u4e00-\u9fff]+/g, '-').replace(/^-|-$/g, '').substring(0, 50);
178
+ }
179
+ async preview(options) {
180
+ return this.import({ ...options, dryRun: true });
181
+ }
182
+ async writeFeatures(features, outputFile, merge) {
183
+ if (merge) {
184
+ try {
185
+ const existingFeatures = await CSVHandler.readFeatures(outputFile);
186
+ const mergedFeatures = [...existingFeatures];
187
+ for (const feature of features) {
188
+ const existingIndex = mergedFeatures.findIndex(f => f.id === feature.id);
189
+ if (existingIndex >= 0)
190
+ mergedFeatures[existingIndex] = feature;
191
+ else
192
+ mergedFeatures.push(feature);
193
+ }
194
+ await CSVHandler.writeFeatures(outputFile, mergedFeatures);
195
+ }
196
+ catch {
197
+ await CSVHandler.writeFeatures(outputFile, features);
198
+ }
199
+ }
200
+ else {
201
+ await CSVHandler.writeFeatures(outputFile, features);
202
+ }
203
+ }
204
+ }
205
+ // ============================================================================
206
+ // Jira Importer
207
+ // ============================================================================
208
+ export class JiraImporter extends BaseImporter {
209
+ name = 'Jira Importer';
210
+ source = 'jira';
211
+ description = 'Import issues from Jira JSON export file';
212
+ async validate(content) {
213
+ const errors = [];
214
+ try {
215
+ const data = this.parseJSON(content);
216
+ const result = JiraExportSchema.safeParse(data);
217
+ if (!result.success) {
218
+ errors.push('Invalid Jira export format');
219
+ result.error.issues.forEach(err => errors.push(`${err.path.join('.')}: ${err.message}`));
220
+ return { valid: false, errors };
221
+ }
222
+ if (result.data.issues.length === 0) {
223
+ errors.push('No issues found in export');
224
+ return { valid: false, errors };
225
+ }
226
+ return { valid: true, errors: [] };
227
+ }
228
+ catch (error) {
229
+ errors.push(error instanceof Error ? error.message : 'Unknown validation error');
230
+ return { valid: false, errors };
231
+ }
232
+ }
233
+ async import(options) {
234
+ const result = this.createEmptyResult();
235
+ try {
236
+ const content = await this.getContent(options);
237
+ const parseResult = JiraExportSchema.safeParse(this.parseJSON(content));
238
+ if (!parseResult.success) {
239
+ result.errors.push({ message: 'Invalid Jira export format' });
240
+ return result;
241
+ }
242
+ const jiraData = parseResult.data;
243
+ result.stats.totalItems = jiraData.issues.length;
244
+ const epicsMap = this.extractEpics(jiraData.issues);
245
+ result.epics = Array.from(epicsMap.values());
246
+ result.stats.epicsImported = result.epics.length;
247
+ let featureIndex = 1;
248
+ for (const issue of jiraData.issues) {
249
+ try {
250
+ if (issue.fields.issuetype.name.toLowerCase() === 'epic')
251
+ continue;
252
+ const feature = this.mapIssueToFeature(issue, featureIndex, epicsMap);
253
+ result.features.push(feature);
254
+ featureIndex++;
255
+ result.stats.featuresImported++;
256
+ }
257
+ catch (error) {
258
+ result.errors.push({
259
+ field: issue.key,
260
+ message: error instanceof Error ? error.message : 'Failed to import issue',
261
+ originalItem: issue,
262
+ });
263
+ result.stats.errors++;
264
+ }
265
+ }
266
+ if (!options.dryRun && result.features.length > 0) {
267
+ await this.writeFeatures(result.features, options.outputFile || 'features.csv', options.merge || false);
268
+ }
269
+ result.success = result.errors.length === 0;
270
+ return result;
271
+ }
272
+ catch (error) {
273
+ result.errors.push({ message: error instanceof Error ? error.message : 'Import failed' });
274
+ return result;
275
+ }
276
+ }
277
+ extractEpics(issues) {
278
+ const epics = new Map();
279
+ for (const issue of issues) {
280
+ if (issue.fields.issuetype.name.toLowerCase() === 'epic') {
281
+ epics.set(issue.key, {
282
+ id: this.sanitizeId(issue.fields.summary),
283
+ name: issue.fields.summary,
284
+ description: issue.fields.description || '',
285
+ originalId: issue.key,
286
+ originalType: 'Epic',
287
+ });
288
+ }
289
+ }
290
+ for (const issue of issues) {
291
+ const epicKey = issue.fields.parent?.key || issue.fields.customfield_10014;
292
+ if (epicKey && !epics.has(epicKey)) {
293
+ const parentSummary = issue.fields.parent?.fields.summary || epicKey;
294
+ epics.set(epicKey, {
295
+ id: this.sanitizeId(parentSummary),
296
+ name: parentSummary,
297
+ description: '',
298
+ originalId: epicKey,
299
+ originalType: 'Epic',
300
+ });
301
+ }
302
+ }
303
+ return epics;
304
+ }
305
+ mapIssueToFeature(issue, index, epicsMap) {
306
+ const epicKey = issue.fields.parent?.key || issue.fields.customfield_10014;
307
+ const epic = epicKey ? epicsMap.get(epicKey) : undefined;
308
+ return {
309
+ id: this.generateFeatureId('feat', index),
310
+ name: issue.fields.summary,
311
+ description: issue.fields.description || issue.fields.summary,
312
+ estimate: this.convertToHours(issue.fields.timeoriginalestimate),
313
+ assignee: issue.fields.assignee?.displayName || 'Unassigned',
314
+ priority: this.mapPriority(issue.fields.priority?.name || 'medium'),
315
+ status: this.mapStatus(issue.fields.status.name),
316
+ category: epic?.name || 'Uncategorized',
317
+ tags: [issue.fields.issuetype.name, ...issue.fields.labels, `jira:${issue.key}`],
318
+ createdDate: this.formatDate(issue.fields.created),
319
+ dueDate: this.formatDate(issue.fields.duedate),
320
+ };
321
+ }
322
+ }
323
+ // ============================================================================
324
+ // Linear Importer
325
+ // ============================================================================
326
+ export class LinearImporter extends BaseImporter {
327
+ name = 'Linear Importer';
328
+ source = 'linear';
329
+ description = 'Import issues from Linear JSON export file';
330
+ async validate(content) {
331
+ const errors = [];
332
+ try {
333
+ const data = this.parseJSON(content);
334
+ const result = LinearExportSchema.safeParse(data);
335
+ if (!result.success) {
336
+ errors.push('Invalid Linear export format');
337
+ result.error.issues.forEach(err => errors.push(`${err.path.join('.')}: ${err.message}`));
338
+ return { valid: false, errors };
339
+ }
340
+ if (result.data.issues.length === 0) {
341
+ errors.push('No issues found in export');
342
+ return { valid: false, errors };
343
+ }
344
+ return { valid: true, errors: [] };
345
+ }
346
+ catch (error) {
347
+ errors.push(error instanceof Error ? error.message : 'Unknown validation error');
348
+ return { valid: false, errors };
349
+ }
350
+ }
351
+ async import(options) {
352
+ const result = this.createEmptyResult();
353
+ try {
354
+ const content = await this.getContent(options);
355
+ const parseResult = LinearExportSchema.safeParse(this.parseJSON(content));
356
+ if (!parseResult.success) {
357
+ result.errors.push({ message: 'Invalid Linear export format' });
358
+ return result;
359
+ }
360
+ const linearData = parseResult.data;
361
+ result.stats.totalItems = linearData.issues.length;
362
+ const projectsMap = this.extractProjects(linearData);
363
+ result.epics = Array.from(projectsMap.values());
364
+ result.stats.epicsImported = result.epics.length;
365
+ let featureIndex = 1;
366
+ for (const issue of linearData.issues) {
367
+ try {
368
+ const feature = this.mapIssueToFeature(issue, featureIndex, projectsMap);
369
+ result.features.push(feature);
370
+ featureIndex++;
371
+ result.stats.featuresImported++;
372
+ }
373
+ catch (error) {
374
+ result.errors.push({
375
+ field: issue.identifier,
376
+ message: error instanceof Error ? error.message : 'Failed to import issue',
377
+ originalItem: issue,
378
+ });
379
+ result.stats.errors++;
380
+ }
381
+ }
382
+ if (!options.dryRun && result.features.length > 0) {
383
+ await this.writeFeatures(result.features, options.outputFile || 'features.csv', options.merge || false);
384
+ }
385
+ result.success = result.errors.length === 0;
386
+ return result;
387
+ }
388
+ catch (error) {
389
+ result.errors.push({ message: error instanceof Error ? error.message : 'Import failed' });
390
+ return result;
391
+ }
392
+ }
393
+ extractProjects(data) {
394
+ const projects = new Map();
395
+ if (data.projects) {
396
+ for (const project of data.projects) {
397
+ projects.set(project.id, {
398
+ id: this.sanitizeId(project.name),
399
+ name: project.name,
400
+ description: project.description || '',
401
+ originalId: project.id,
402
+ originalType: 'Project',
403
+ });
404
+ }
405
+ }
406
+ for (const issue of data.issues) {
407
+ if (issue.project && !projects.has(issue.project.id)) {
408
+ projects.set(issue.project.id, {
409
+ id: this.sanitizeId(issue.project.name),
410
+ name: issue.project.name,
411
+ description: issue.project.description || '',
412
+ originalId: issue.project.id,
413
+ originalType: 'Project',
414
+ });
415
+ }
416
+ }
417
+ return projects;
418
+ }
419
+ mapLinearPriority(priority) {
420
+ switch (priority) {
421
+ case 1: return 'critical';
422
+ case 2: return 'high';
423
+ case 3: return 'medium';
424
+ case 4: return 'low';
425
+ default: return 'medium';
426
+ }
427
+ }
428
+ mapLinearState(state) {
429
+ const type = state.type.toLowerCase();
430
+ switch (type) {
431
+ case 'completed':
432
+ case 'done': return 'done';
433
+ case 'started':
434
+ case 'inprogress': return 'in-progress';
435
+ case 'backlog':
436
+ case 'unstarted':
437
+ case 'triage': return 'todo';
438
+ case 'canceled':
439
+ case 'cancelled': return 'blocked';
440
+ default: return this.mapStatus(state.name);
441
+ }
442
+ }
443
+ mapIssueToFeature(issue, index, projectsMap) {
444
+ const project = issue.project ? projectsMap.get(issue.project.id) : undefined;
445
+ const labels = issue.labels?.nodes?.map(l => l.name) || [];
446
+ return {
447
+ id: this.generateFeatureId('feat', index),
448
+ name: issue.title,
449
+ description: issue.description || issue.title,
450
+ estimate: issue.estimate ? issue.estimate * 4 : 8,
451
+ assignee: issue.assignee?.name || 'Unassigned',
452
+ priority: this.mapLinearPriority(issue.priority),
453
+ status: this.mapLinearState(issue.state),
454
+ category: project?.name || 'Uncategorized',
455
+ tags: [...labels, `linear:${issue.identifier}`],
456
+ createdDate: this.formatDate(issue.createdAt),
457
+ dueDate: this.formatDate(issue.dueDate),
458
+ };
459
+ }
460
+ }
461
+ // ============================================================================
462
+ // GitHub Importer
463
+ // ============================================================================
464
+ const SKILL_LABEL_PREFIXES = ['skill:', 'tech:', 'language:', 'framework:'];
465
+ export class GitHubImporter extends BaseImporter {
466
+ name = 'GitHub Issues Importer';
467
+ source = 'github';
468
+ description = 'Import issues from GitHub API export or JSON file';
469
+ async validate(content) {
470
+ const errors = [];
471
+ try {
472
+ const data = this.parseJSON(content);
473
+ const normalized = Array.isArray(data) ? { issues: data } : data;
474
+ const result = GitHubExportSchema.safeParse(normalized);
475
+ if (!result.success) {
476
+ errors.push('Invalid GitHub export format');
477
+ result.error.issues.forEach(err => errors.push(`${err.path.join('.')}: ${err.message}`));
478
+ return { valid: false, errors };
479
+ }
480
+ if (result.data.issues.length === 0) {
481
+ errors.push('No issues found in export');
482
+ return { valid: false, errors };
483
+ }
484
+ return { valid: true, errors: [] };
485
+ }
486
+ catch (error) {
487
+ errors.push(error instanceof Error ? error.message : 'Unknown validation error');
488
+ return { valid: false, errors };
489
+ }
490
+ }
491
+ async import(options) {
492
+ const result = this.createEmptyResult();
493
+ try {
494
+ const content = await this.getContent(options);
495
+ const rawData = this.parseJSON(content);
496
+ const normalized = Array.isArray(rawData) ? { issues: rawData } : rawData;
497
+ const parseResult = GitHubExportSchema.safeParse(normalized);
498
+ if (!parseResult.success) {
499
+ result.errors.push({ message: 'Invalid GitHub export format' });
500
+ return result;
501
+ }
502
+ const githubData = parseResult.data;
503
+ result.stats.totalItems = githubData.issues.length;
504
+ const milestonesMap = this.extractMilestones(githubData);
505
+ result.milestones = Array.from(milestonesMap.values());
506
+ result.stats.milestonesImported = result.milestones.length;
507
+ const epicsMap = this.extractEpicsFromLabels(githubData.issues);
508
+ result.epics = Array.from(epicsMap.values());
509
+ result.stats.epicsImported = result.epics.length;
510
+ let featureIndex = 1;
511
+ for (const issue of githubData.issues) {
512
+ try {
513
+ const feature = this.mapIssueToFeature(issue, featureIndex, milestonesMap, epicsMap);
514
+ result.features.push(feature);
515
+ featureIndex++;
516
+ result.stats.featuresImported++;
517
+ }
518
+ catch (error) {
519
+ result.errors.push({
520
+ field: `#${issue.number}`,
521
+ message: error instanceof Error ? error.message : 'Failed to import issue',
522
+ originalItem: issue,
523
+ });
524
+ result.stats.errors++;
525
+ }
526
+ }
527
+ if (!options.dryRun && result.features.length > 0) {
528
+ await this.writeFeatures(result.features, options.outputFile || 'features.csv', options.merge || false);
529
+ }
530
+ result.success = result.errors.length === 0;
531
+ return result;
532
+ }
533
+ catch (error) {
534
+ result.errors.push({ message: error instanceof Error ? error.message : 'Import failed' });
535
+ return result;
536
+ }
537
+ }
538
+ extractMilestones(data) {
539
+ const milestones = new Map();
540
+ if (data.milestones) {
541
+ for (const milestone of data.milestones) {
542
+ milestones.set(milestone.number, {
543
+ id: this.sanitizeId(milestone.title),
544
+ name: milestone.title,
545
+ description: milestone.description || '',
546
+ dueDate: this.formatDate(milestone.due_on),
547
+ originalId: String(milestone.number),
548
+ });
549
+ }
550
+ }
551
+ for (const issue of data.issues) {
552
+ if (issue.milestone && !milestones.has(issue.milestone.number)) {
553
+ milestones.set(issue.milestone.number, {
554
+ id: this.sanitizeId(issue.milestone.title),
555
+ name: issue.milestone.title,
556
+ description: issue.milestone.description || '',
557
+ dueDate: this.formatDate(issue.milestone.due_on),
558
+ originalId: String(issue.milestone.number),
559
+ });
560
+ }
561
+ }
562
+ return milestones;
563
+ }
564
+ extractEpicsFromLabels(issues) {
565
+ const epics = new Map();
566
+ const epicPrefixes = ['epic:', 'category:', 'area:'];
567
+ for (const issue of issues) {
568
+ for (const label of issue.labels) {
569
+ const lowerName = label.name.toLowerCase();
570
+ for (const prefix of epicPrefixes) {
571
+ if (lowerName.startsWith(prefix)) {
572
+ const epicName = label.name.substring(prefix.length).trim();
573
+ if (!epics.has(epicName)) {
574
+ epics.set(epicName, {
575
+ id: this.sanitizeId(epicName),
576
+ name: epicName,
577
+ description: `Imported from GitHub label: ${label.name}`,
578
+ originalId: label.name,
579
+ originalType: 'Label',
580
+ });
581
+ }
582
+ break;
583
+ }
584
+ }
585
+ }
586
+ }
587
+ return epics;
588
+ }
589
+ extractSkillsFromLabels(labels) {
590
+ const skills = [];
591
+ for (const label of labels) {
592
+ const lowerName = label.name.toLowerCase();
593
+ for (const prefix of SKILL_LABEL_PREFIXES) {
594
+ if (lowerName.startsWith(prefix)) {
595
+ skills.push(label.name.substring(prefix.length).trim());
596
+ break;
597
+ }
598
+ }
599
+ }
600
+ return skills;
601
+ }
602
+ extractCategoryFromLabels(labels, epicsMap) {
603
+ const epicPrefixes = ['epic:', 'category:', 'area:'];
604
+ for (const label of labels) {
605
+ const lowerName = label.name.toLowerCase();
606
+ for (const prefix of epicPrefixes) {
607
+ if (lowerName.startsWith(prefix)) {
608
+ const epicName = label.name.substring(prefix.length).trim();
609
+ if (epicsMap.has(epicName))
610
+ return epicName;
611
+ }
612
+ }
613
+ }
614
+ return 'Uncategorized';
615
+ }
616
+ extractPriorityFromLabels(labels) {
617
+ const priorityPrefixes = ['priority:', 'p:'];
618
+ for (const label of labels) {
619
+ const lowerName = label.name.toLowerCase();
620
+ for (const prefix of priorityPrefixes) {
621
+ if (lowerName.startsWith(prefix)) {
622
+ return this.mapPriority(lowerName.substring(prefix.length).trim());
623
+ }
624
+ }
625
+ if (['critical', 'blocker', 'urgent'].includes(lowerName))
626
+ return 'critical';
627
+ if (['high', 'important'].includes(lowerName))
628
+ return 'high';
629
+ if (['low', 'minor', 'trivial'].includes(lowerName))
630
+ return 'low';
631
+ }
632
+ return 'medium';
633
+ }
634
+ extractEstimateFromLabels(labels) {
635
+ const estimatePrefixes = ['estimate:', 'size:', 'points:'];
636
+ for (const label of labels) {
637
+ const lowerName = label.name.toLowerCase();
638
+ for (const prefix of estimatePrefixes) {
639
+ if (lowerName.startsWith(prefix)) {
640
+ const value = lowerName.substring(prefix.length).trim();
641
+ const parsed = parseInt(value, 10);
642
+ if (!isNaN(parsed))
643
+ return parsed * 4;
644
+ }
645
+ }
646
+ if (['xs', 'extra-small'].includes(lowerName))
647
+ return 2;
648
+ if (['s', 'small'].includes(lowerName))
649
+ return 4;
650
+ if (['m', 'medium'].includes(lowerName))
651
+ return 8;
652
+ if (['l', 'large'].includes(lowerName))
653
+ return 16;
654
+ if (['xl', 'extra-large'].includes(lowerName))
655
+ return 32;
656
+ }
657
+ return 8;
658
+ }
659
+ mapIssueToFeature(issue, index, milestonesMap, epicsMap) {
660
+ const category = this.extractCategoryFromLabels(issue.labels, epicsMap);
661
+ const priority = this.extractPriorityFromLabels(issue.labels);
662
+ const status = issue.state === 'closed' ? 'done' : 'todo';
663
+ const estimate = this.extractEstimateFromLabels(issue.labels);
664
+ const assignee = issue.assignee?.login ||
665
+ (issue.assignees && issue.assignees.length > 0 ? issue.assignees[0].login : 'Unassigned');
666
+ const skills = this.extractSkillsFromLabels(issue.labels);
667
+ const otherLabels = issue.labels.map(l => l.name).filter(name => {
668
+ const lower = name.toLowerCase();
669
+ const excludePrefixes = [...SKILL_LABEL_PREFIXES, 'epic:', 'category:', 'area:', 'priority:', 'p:', 'estimate:', 'size:', 'points:'];
670
+ return !excludePrefixes.some(p => lower.startsWith(p)) &&
671
+ !['critical', 'blocker', 'urgent', 'high', 'important', 'low', 'minor', 'trivial',
672
+ 'xs', 'extra-small', 's', 'small', 'm', 'medium', 'l', 'large', 'xl', 'extra-large'].includes(lower);
673
+ });
674
+ const tags = [...skills, ...otherLabels, `github:#${issue.number}`];
675
+ if (issue.milestone)
676
+ tags.push(`milestone:${issue.milestone.title}`);
677
+ return {
678
+ id: this.generateFeatureId('feat', index),
679
+ name: issue.title,
680
+ description: issue.body || issue.title,
681
+ estimate,
682
+ assignee,
683
+ priority,
684
+ status,
685
+ category,
686
+ tags,
687
+ createdDate: this.formatDate(issue.created_at),
688
+ dueDate: issue.milestone?.due_on ? this.formatDate(issue.milestone.due_on) : undefined,
689
+ };
690
+ }
691
+ }
692
+ // ============================================================================
693
+ // Importer Registry
694
+ // ============================================================================
695
+ export const jiraImporter = new JiraImporter();
696
+ export const linearImporter = new LinearImporter();
697
+ export const githubImporter = new GitHubImporter();
698
+ const importerRegistry = {
699
+ jira: jiraImporter,
700
+ linear: linearImporter,
701
+ github: githubImporter,
702
+ };
703
+ export function getImporter(source) {
704
+ const importer = importerRegistry[source];
705
+ if (!importer)
706
+ throw new Error(`Unknown import source: ${source}`);
707
+ return importer;
708
+ }
709
+ export function getAllImporters() {
710
+ return Object.values(importerRegistry);
711
+ }
712
+ export function isValidSource(source) {
713
+ return source in importerRegistry;
714
+ }
715
+ //# sourceMappingURL=importers.js.map