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
+ };