claude-autopm 1.24.2 → 1.26.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.
|
@@ -0,0 +1,498 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Epic Sync - Complete GitHub sync for epics
|
|
4
|
+
*
|
|
5
|
+
* Replaces all 4 Bash scripts with clean, testable JavaScript:
|
|
6
|
+
* - create-epic-issue.sh → createEpicIssue()
|
|
7
|
+
* - create-task-issues.sh → createTaskIssues()
|
|
8
|
+
* - update-epic-file.sh → updateEpicFile()
|
|
9
|
+
* - update-references.sh → updateTaskReferences()
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
const { execSync } = require('child_process');
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Parse frontmatter and body from markdown file
|
|
18
|
+
*/
|
|
19
|
+
function parseMarkdownFile(filePath) {
|
|
20
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
21
|
+
const lines = content.split('\n');
|
|
22
|
+
|
|
23
|
+
let inFrontmatter = false;
|
|
24
|
+
let frontmatterLines = [];
|
|
25
|
+
let bodyLines = [];
|
|
26
|
+
let frontmatterCount = 0;
|
|
27
|
+
|
|
28
|
+
for (const line of lines) {
|
|
29
|
+
if (line === '---') {
|
|
30
|
+
frontmatterCount++;
|
|
31
|
+
if (frontmatterCount === 1) {
|
|
32
|
+
inFrontmatter = true;
|
|
33
|
+
continue;
|
|
34
|
+
} else if (frontmatterCount === 2) {
|
|
35
|
+
inFrontmatter = false;
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (inFrontmatter) {
|
|
41
|
+
frontmatterLines.push(line);
|
|
42
|
+
} else if (frontmatterCount === 2) {
|
|
43
|
+
bodyLines.push(line);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Parse frontmatter
|
|
48
|
+
const frontmatter = {};
|
|
49
|
+
for (const line of frontmatterLines) {
|
|
50
|
+
const match = line.match(/^(\w+):\s*(.+)$/);
|
|
51
|
+
if (match) {
|
|
52
|
+
const [, key, value] = match;
|
|
53
|
+
frontmatter[key] = value;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
frontmatter,
|
|
59
|
+
frontmatterLines,
|
|
60
|
+
body: bodyLines.join('\n').trim(),
|
|
61
|
+
fullContent: content
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Update frontmatter field in markdown content
|
|
67
|
+
*/
|
|
68
|
+
function updateFrontmatter(content, updates) {
|
|
69
|
+
const lines = content.split('\n');
|
|
70
|
+
const result = [];
|
|
71
|
+
let inFrontmatter = false;
|
|
72
|
+
let frontmatterCount = 0;
|
|
73
|
+
|
|
74
|
+
for (const line of lines) {
|
|
75
|
+
if (line === '---') {
|
|
76
|
+
frontmatterCount++;
|
|
77
|
+
result.push(line);
|
|
78
|
+
if (frontmatterCount === 1) {
|
|
79
|
+
inFrontmatter = true;
|
|
80
|
+
} else if (frontmatterCount === 2) {
|
|
81
|
+
inFrontmatter = false;
|
|
82
|
+
}
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (inFrontmatter) {
|
|
87
|
+
const match = line.match(/^(\w+):\s*(.+)$/);
|
|
88
|
+
if (match) {
|
|
89
|
+
const [, key] = match;
|
|
90
|
+
if (updates[key] !== undefined) {
|
|
91
|
+
result.push(`${key}: ${updates[key]}`);
|
|
92
|
+
} else {
|
|
93
|
+
result.push(line);
|
|
94
|
+
}
|
|
95
|
+
} else {
|
|
96
|
+
result.push(line);
|
|
97
|
+
}
|
|
98
|
+
} else {
|
|
99
|
+
result.push(line);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return result.join('\n');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Get current timestamp in ISO format
|
|
108
|
+
*/
|
|
109
|
+
function getTimestamp() {
|
|
110
|
+
return new Date().toISOString();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Get repository info from gh CLI
|
|
115
|
+
*/
|
|
116
|
+
function getRepoInfo() {
|
|
117
|
+
try {
|
|
118
|
+
const result = execSync('gh repo view --json nameWithOwner -q .nameWithOwner', {
|
|
119
|
+
encoding: 'utf8',
|
|
120
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
121
|
+
});
|
|
122
|
+
return result.trim();
|
|
123
|
+
} catch (error) {
|
|
124
|
+
return 'unknown/repo';
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Create epic GitHub issue
|
|
130
|
+
*/
|
|
131
|
+
function createEpicIssue(epicPath) {
|
|
132
|
+
const epicFile = path.join(process.cwd(), '.claude/epics', epicPath, 'epic.md');
|
|
133
|
+
|
|
134
|
+
if (!fs.existsSync(epicFile)) {
|
|
135
|
+
throw new Error(`Epic file not found: ${epicFile}`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const { body } = parseMarkdownFile(epicFile);
|
|
139
|
+
|
|
140
|
+
// Count tasks
|
|
141
|
+
const epicDir = path.dirname(epicFile);
|
|
142
|
+
const taskFiles = fs.readdirSync(epicDir)
|
|
143
|
+
.filter(f => /^\d+\.md$/.test(f));
|
|
144
|
+
const taskCount = taskFiles.length;
|
|
145
|
+
|
|
146
|
+
// Detect epic type
|
|
147
|
+
const isFeature = body.toLowerCase().includes('feature') ||
|
|
148
|
+
body.toLowerCase().includes('implement') ||
|
|
149
|
+
!body.toLowerCase().includes('bug');
|
|
150
|
+
const labels = isFeature ? 'epic,feature' : 'epic,bug';
|
|
151
|
+
|
|
152
|
+
console.log(`📝 Creating epic issue: ${epicPath}`);
|
|
153
|
+
console.log(` Tasks: ${taskCount}`);
|
|
154
|
+
console.log(` Labels: ${labels}`);
|
|
155
|
+
|
|
156
|
+
// Create issue body
|
|
157
|
+
const issueBody = `
|
|
158
|
+
${body}
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
**Epic Statistics:**
|
|
163
|
+
- Tasks: ${taskCount}
|
|
164
|
+
- Status: Planning
|
|
165
|
+
- Created: ${getTimestamp()}
|
|
166
|
+
`.trim();
|
|
167
|
+
|
|
168
|
+
// Escape for shell
|
|
169
|
+
const escapedTitle = `Epic: ${epicPath}`.replace(/"/g, '\\"');
|
|
170
|
+
const escapedBody = issueBody.replace(/"/g, '\\"').replace(/`/g, '\\`');
|
|
171
|
+
|
|
172
|
+
try {
|
|
173
|
+
const result = execSync(
|
|
174
|
+
`gh issue create --title "${escapedTitle}" --body "${escapedBody}" --label "${labels}"`,
|
|
175
|
+
{ encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
const match = result.match(/https:\/\/github\.com\/[^\/]+\/[^\/]+\/issues\/(\d+)/);
|
|
179
|
+
if (match) {
|
|
180
|
+
const issueNumber = parseInt(match[1]);
|
|
181
|
+
console.log(`✅ Created epic issue #${issueNumber}`);
|
|
182
|
+
return issueNumber;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
throw new Error('Could not extract issue number from gh output');
|
|
186
|
+
} catch (error) {
|
|
187
|
+
throw new Error(`Failed to create epic issue: ${error.message}`);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Create task issues for an epic
|
|
193
|
+
*/
|
|
194
|
+
function createTaskIssues(epicPath, epicIssueNumber) {
|
|
195
|
+
const epicDir = path.join(process.cwd(), '.claude/epics', epicPath);
|
|
196
|
+
|
|
197
|
+
if (!fs.existsSync(epicDir)) {
|
|
198
|
+
throw new Error(`Epic directory not found: ${epicDir}`);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Find all task files
|
|
202
|
+
const taskFiles = fs.readdirSync(epicDir)
|
|
203
|
+
.filter(f => /^\d+\.md$/.test(f))
|
|
204
|
+
.sort();
|
|
205
|
+
|
|
206
|
+
if (taskFiles.length === 0) {
|
|
207
|
+
throw new Error(`No task files found in ${epicDir}`);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
console.log(`\n📋 Creating ${taskFiles.length} task issues for epic #${epicIssueNumber}`);
|
|
211
|
+
|
|
212
|
+
const mapping = [];
|
|
213
|
+
let successCount = 0;
|
|
214
|
+
|
|
215
|
+
for (let i = 0; i < taskFiles.length; i++) {
|
|
216
|
+
const taskFile = taskFiles[i];
|
|
217
|
+
const taskNumber = taskFile.replace('.md', '');
|
|
218
|
+
const taskPath = path.join(epicDir, taskFile);
|
|
219
|
+
|
|
220
|
+
console.log(`[${i + 1}/${taskFiles.length}] Creating issue for task ${taskNumber}...`);
|
|
221
|
+
|
|
222
|
+
try {
|
|
223
|
+
const { frontmatter, body } = parseMarkdownFile(taskPath);
|
|
224
|
+
const title = frontmatter.name || 'Untitled Task';
|
|
225
|
+
|
|
226
|
+
// Create issue body
|
|
227
|
+
const issueBody = `
|
|
228
|
+
Part of Epic #${epicIssueNumber}
|
|
229
|
+
|
|
230
|
+
---
|
|
231
|
+
|
|
232
|
+
${body}
|
|
233
|
+
|
|
234
|
+
---
|
|
235
|
+
|
|
236
|
+
**Epic Path:** \`${epicPath}\`
|
|
237
|
+
**Task Number:** ${taskNumber}
|
|
238
|
+
`.trim();
|
|
239
|
+
|
|
240
|
+
// Escape for shell
|
|
241
|
+
const escapedTitle = title.replace(/"/g, '\\"');
|
|
242
|
+
const escapedBody = issueBody.replace(/"/g, '\\"').replace(/`/g, '\\`');
|
|
243
|
+
|
|
244
|
+
const result = execSync(
|
|
245
|
+
`gh issue create --title "${escapedTitle}" --body "${escapedBody}" --label "task"`,
|
|
246
|
+
{ encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
const match = result.match(/https:\/\/github\.com\/[^\/]+\/[^\/]+\/issues\/(\d+)/);
|
|
250
|
+
if (match) {
|
|
251
|
+
const issueNumber = parseInt(match[1]);
|
|
252
|
+
console.log(` ✅ Created issue #${issueNumber}: ${title}`);
|
|
253
|
+
mapping.push({ oldName: taskNumber, newNumber: issueNumber });
|
|
254
|
+
successCount++;
|
|
255
|
+
}
|
|
256
|
+
} catch (error) {
|
|
257
|
+
console.error(` ❌ Error: ${error.message}`);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Save mapping file
|
|
262
|
+
const mappingFile = path.join(epicDir, '.task-mapping.txt');
|
|
263
|
+
const mappingContent = mapping.map(m => `${m.oldName} ${m.newNumber}`).join('\n');
|
|
264
|
+
fs.writeFileSync(mappingFile, mappingContent, 'utf8');
|
|
265
|
+
|
|
266
|
+
console.log(`\n📊 Summary:`);
|
|
267
|
+
console.log(` ✅ Success: ${successCount}`);
|
|
268
|
+
console.log(` ❌ Failed: ${taskFiles.length - successCount}`);
|
|
269
|
+
console.log(` 📝 Total: ${taskFiles.length}`);
|
|
270
|
+
|
|
271
|
+
return mapping;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Update epic file with GitHub URL and task references
|
|
276
|
+
*/
|
|
277
|
+
function updateEpicFile(epicPath, epicIssueNumber, taskMapping) {
|
|
278
|
+
const epicFile = path.join(process.cwd(), '.claude/epics', epicPath, 'epic.md');
|
|
279
|
+
|
|
280
|
+
if (!fs.existsSync(epicFile)) {
|
|
281
|
+
throw new Error(`Epic file not found: ${epicFile}`);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
console.log(`\n📄 Updating epic file: ${epicFile}`);
|
|
285
|
+
|
|
286
|
+
const repo = getRepoInfo();
|
|
287
|
+
const epicUrl = `https://github.com/${repo}/issues/${epicIssueNumber}`;
|
|
288
|
+
|
|
289
|
+
// Read current content
|
|
290
|
+
let content = fs.readFileSync(epicFile, 'utf8');
|
|
291
|
+
|
|
292
|
+
// Update frontmatter
|
|
293
|
+
content = updateFrontmatter(content, {
|
|
294
|
+
github: epicUrl,
|
|
295
|
+
updated: getTimestamp()
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
// Update task references in body
|
|
299
|
+
if (taskMapping && taskMapping.length > 0) {
|
|
300
|
+
console.log(' Updating task references...');
|
|
301
|
+
|
|
302
|
+
for (const { oldName, newNumber } of taskMapping) {
|
|
303
|
+
// Update checkbox items
|
|
304
|
+
content = content.replace(
|
|
305
|
+
new RegExp(`- \\[ \\] ${oldName}\\b`, 'g'),
|
|
306
|
+
`- [ ] #${newNumber}`
|
|
307
|
+
);
|
|
308
|
+
content = content.replace(
|
|
309
|
+
new RegExp(`- \\[x\\] ${oldName}\\b`, 'g'),
|
|
310
|
+
`- [x] #${newNumber}`
|
|
311
|
+
);
|
|
312
|
+
|
|
313
|
+
// Update task links
|
|
314
|
+
content = content.replace(
|
|
315
|
+
new RegExp(`Task ${oldName}\\b`, 'g'),
|
|
316
|
+
`Task #${newNumber}`
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Write updated content
|
|
322
|
+
fs.writeFileSync(epicFile, content, 'utf8');
|
|
323
|
+
|
|
324
|
+
console.log('✅ Epic file updated');
|
|
325
|
+
console.log(` GitHub: ${epicUrl}`);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Update task files with GitHub URLs and rename to issue numbers
|
|
330
|
+
*/
|
|
331
|
+
function updateTaskReferences(epicPath, taskMapping) {
|
|
332
|
+
const epicDir = path.join(process.cwd(), '.claude/epics', epicPath);
|
|
333
|
+
|
|
334
|
+
if (!fs.existsSync(epicDir)) {
|
|
335
|
+
throw new Error(`Epic directory not found: ${epicDir}`);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
console.log(`\n🔗 Updating task references and renaming files`);
|
|
339
|
+
|
|
340
|
+
const repo = getRepoInfo();
|
|
341
|
+
|
|
342
|
+
for (const { oldName, newNumber } of taskMapping) {
|
|
343
|
+
const oldFile = path.join(epicDir, `${oldName}.md`);
|
|
344
|
+
const newFile = path.join(epicDir, `${newNumber}.md`);
|
|
345
|
+
|
|
346
|
+
if (!fs.existsSync(oldFile)) {
|
|
347
|
+
console.log(` ⚠️ File not found: ${oldName}.md (skipping)`);
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
console.log(` Renaming ${oldName}.md → ${newNumber}.md...`);
|
|
352
|
+
|
|
353
|
+
// Read and update content
|
|
354
|
+
let content = fs.readFileSync(oldFile, 'utf8');
|
|
355
|
+
|
|
356
|
+
const githubUrl = `https://github.com/${repo}/issues/${newNumber}`;
|
|
357
|
+
content = updateFrontmatter(content, {
|
|
358
|
+
github: githubUrl,
|
|
359
|
+
updated: getTimestamp()
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
// Write to new file
|
|
363
|
+
fs.writeFileSync(newFile, content, 'utf8');
|
|
364
|
+
|
|
365
|
+
// Remove old file
|
|
366
|
+
fs.unlinkSync(oldFile);
|
|
367
|
+
|
|
368
|
+
console.log(` ✓`);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
console.log('\n✅ Task files renamed and frontmatter updated');
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Full epic sync workflow
|
|
376
|
+
*/
|
|
377
|
+
function syncEpic(epicPath) {
|
|
378
|
+
console.log(`🚀 Starting full epic sync: ${epicPath}\n`);
|
|
379
|
+
|
|
380
|
+
try {
|
|
381
|
+
// Step 1: Create epic issue
|
|
382
|
+
const epicIssueNumber = createEpicIssue(epicPath);
|
|
383
|
+
|
|
384
|
+
// Step 2: Create task issues
|
|
385
|
+
const taskMapping = createTaskIssues(epicPath, epicIssueNumber);
|
|
386
|
+
|
|
387
|
+
// Step 3: Update epic file
|
|
388
|
+
updateEpicFile(epicPath, epicIssueNumber, taskMapping);
|
|
389
|
+
|
|
390
|
+
// Step 4: Update task references
|
|
391
|
+
updateTaskReferences(epicPath, taskMapping);
|
|
392
|
+
|
|
393
|
+
console.log(`\n✅ Epic sync complete!`);
|
|
394
|
+
console.log(` Epic: #${epicIssueNumber}`);
|
|
395
|
+
console.log(` Tasks: ${taskMapping.length} created and synced`);
|
|
396
|
+
|
|
397
|
+
return { epicIssueNumber, taskMapping };
|
|
398
|
+
} catch (error) {
|
|
399
|
+
console.error(`\n❌ Epic sync failed: ${error.message}`);
|
|
400
|
+
process.exit(1);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* CLI interface
|
|
406
|
+
*/
|
|
407
|
+
function main() {
|
|
408
|
+
const args = process.argv.slice(2);
|
|
409
|
+
const command = args[0];
|
|
410
|
+
|
|
411
|
+
if (!command) {
|
|
412
|
+
console.log('Usage:');
|
|
413
|
+
console.log(' epicSync.js sync <epic-path> - Full sync workflow');
|
|
414
|
+
console.log(' epicSync.js create-epic <epic-path> - Create epic issue only');
|
|
415
|
+
console.log(' epicSync.js create-tasks <epic-path> <epic-number> - Create task issues only');
|
|
416
|
+
console.log(' epicSync.js update-epic <epic-path> <epic-number> - Update epic file only');
|
|
417
|
+
console.log('');
|
|
418
|
+
console.log('Examples:');
|
|
419
|
+
console.log(' epicSync.js sync fullstack/01-infrastructure');
|
|
420
|
+
console.log(' epicSync.js create-epic fullstack/01-infrastructure');
|
|
421
|
+
process.exit(1);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
switch (command) {
|
|
425
|
+
case 'sync': {
|
|
426
|
+
const epicPath = args[1];
|
|
427
|
+
if (!epicPath) {
|
|
428
|
+
console.error('Error: epic-path required');
|
|
429
|
+
process.exit(1);
|
|
430
|
+
}
|
|
431
|
+
syncEpic(epicPath);
|
|
432
|
+
break;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
case 'create-epic': {
|
|
436
|
+
const epicPath = args[1];
|
|
437
|
+
if (!epicPath) {
|
|
438
|
+
console.error('Error: epic-path required');
|
|
439
|
+
process.exit(1);
|
|
440
|
+
}
|
|
441
|
+
const epicNumber = createEpicIssue(epicPath);
|
|
442
|
+
console.log(`\nEpic issue number: ${epicNumber}`);
|
|
443
|
+
break;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
case 'create-tasks': {
|
|
447
|
+
const epicPath = args[1];
|
|
448
|
+
const epicNumber = parseInt(args[2]);
|
|
449
|
+
if (!epicPath || !epicNumber) {
|
|
450
|
+
console.error('Error: epic-path and epic-number required');
|
|
451
|
+
process.exit(1);
|
|
452
|
+
}
|
|
453
|
+
createTaskIssues(epicPath, epicNumber);
|
|
454
|
+
break;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
case 'update-epic': {
|
|
458
|
+
const epicPath = args[1];
|
|
459
|
+
const epicNumber = parseInt(args[2]);
|
|
460
|
+
if (!epicPath || !epicNumber) {
|
|
461
|
+
console.error('Error: epic-path and epic-number required');
|
|
462
|
+
process.exit(1);
|
|
463
|
+
}
|
|
464
|
+
// Load mapping file
|
|
465
|
+
const mappingFile = path.join(process.cwd(), '.claude/epics', epicPath, '.task-mapping.txt');
|
|
466
|
+
let mapping = [];
|
|
467
|
+
if (fs.existsSync(mappingFile)) {
|
|
468
|
+
const content = fs.readFileSync(mappingFile, 'utf8');
|
|
469
|
+
mapping = content.split('\n')
|
|
470
|
+
.filter(line => line.trim())
|
|
471
|
+
.map(line => {
|
|
472
|
+
const [oldName, newNumber] = line.split(' ');
|
|
473
|
+
return { oldName, newNumber: parseInt(newNumber) };
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
updateEpicFile(epicPath, epicNumber, mapping);
|
|
477
|
+
break;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
default:
|
|
481
|
+
console.error(`Unknown command: ${command}`);
|
|
482
|
+
process.exit(1);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
if (require.main === module) {
|
|
487
|
+
main();
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
module.exports = {
|
|
491
|
+
parseMarkdownFile,
|
|
492
|
+
updateFrontmatter,
|
|
493
|
+
createEpicIssue,
|
|
494
|
+
createTaskIssues,
|
|
495
|
+
updateEpicFile,
|
|
496
|
+
updateTaskReferences,
|
|
497
|
+
syncEpic
|
|
498
|
+
};
|