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,593 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Issue Sync - Complete GitHub issue synchronization
|
|
4
|
+
*
|
|
5
|
+
* Replaces 5 Bash scripts with clean, testable JavaScript:
|
|
6
|
+
* - gather-updates.sh → gatherUpdates()
|
|
7
|
+
* - format-comment.sh → formatComment()
|
|
8
|
+
* - post-comment.sh → postComment()
|
|
9
|
+
* - update-frontmatter.sh → updateFrontmatter()
|
|
10
|
+
* - preflight-validation.sh → preflightValidation()
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
const { execSync } = require('child_process');
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Parse frontmatter from markdown file
|
|
19
|
+
*/
|
|
20
|
+
function parseFrontmatter(filePath) {
|
|
21
|
+
if (!fs.existsSync(filePath)) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
26
|
+
const lines = content.split('\n');
|
|
27
|
+
|
|
28
|
+
let inFrontmatter = false;
|
|
29
|
+
let frontmatterCount = 0;
|
|
30
|
+
const frontmatter = {};
|
|
31
|
+
|
|
32
|
+
for (const line of lines) {
|
|
33
|
+
if (line === '---') {
|
|
34
|
+
frontmatterCount++;
|
|
35
|
+
if (frontmatterCount === 1) {
|
|
36
|
+
inFrontmatter = true;
|
|
37
|
+
} else if (frontmatterCount === 2) {
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (inFrontmatter) {
|
|
44
|
+
const match = line.match(/^(\w+):\s*(.+)$/);
|
|
45
|
+
if (match) {
|
|
46
|
+
const [, key, value] = match;
|
|
47
|
+
frontmatter[key] = value;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return frontmatter;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Update frontmatter field
|
|
57
|
+
*/
|
|
58
|
+
function updateFrontmatterField(filePath, field, value) {
|
|
59
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
60
|
+
const lines = content.split('\n');
|
|
61
|
+
const result = [];
|
|
62
|
+
|
|
63
|
+
let inFrontmatter = false;
|
|
64
|
+
let frontmatterCount = 0;
|
|
65
|
+
let fieldUpdated = false;
|
|
66
|
+
|
|
67
|
+
for (const line of lines) {
|
|
68
|
+
if (line === '---') {
|
|
69
|
+
frontmatterCount++;
|
|
70
|
+
result.push(line);
|
|
71
|
+
if (frontmatterCount === 1) {
|
|
72
|
+
inFrontmatter = true;
|
|
73
|
+
} else if (frontmatterCount === 2) {
|
|
74
|
+
inFrontmatter = false;
|
|
75
|
+
// Add field if not found
|
|
76
|
+
if (!fieldUpdated && inFrontmatter === false) {
|
|
77
|
+
result.splice(result.length - 1, 0, `${field}: ${value}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (inFrontmatter) {
|
|
84
|
+
const match = line.match(/^(\w+):/);
|
|
85
|
+
if (match && match[1] === field) {
|
|
86
|
+
result.push(`${field}: ${value}`);
|
|
87
|
+
fieldUpdated = true;
|
|
88
|
+
} else {
|
|
89
|
+
result.push(line);
|
|
90
|
+
}
|
|
91
|
+
} else {
|
|
92
|
+
result.push(line);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
fs.writeFileSync(filePath, result.join('\n'), 'utf8');
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Get current ISO timestamp
|
|
102
|
+
*/
|
|
103
|
+
function getTimestamp() {
|
|
104
|
+
return new Date().toISOString();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Strip frontmatter and return body only
|
|
109
|
+
*/
|
|
110
|
+
function stripFrontmatter(filePath) {
|
|
111
|
+
if (!fs.existsSync(filePath)) {
|
|
112
|
+
return '';
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
116
|
+
const lines = content.split('\n');
|
|
117
|
+
|
|
118
|
+
let frontmatterCount = 0;
|
|
119
|
+
const bodyLines = [];
|
|
120
|
+
|
|
121
|
+
for (const line of lines) {
|
|
122
|
+
if (line === '---') {
|
|
123
|
+
frontmatterCount++;
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (frontmatterCount >= 2) {
|
|
128
|
+
bodyLines.push(line);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return bodyLines.join('\n').trim();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Gather updates from various sources
|
|
137
|
+
*/
|
|
138
|
+
function gatherUpdates(issueNumber, updatesDir, lastSync = null) {
|
|
139
|
+
console.log(`\n📋 Gathering updates for issue #${issueNumber}`);
|
|
140
|
+
|
|
141
|
+
if (!fs.existsSync(updatesDir)) {
|
|
142
|
+
throw new Error(`Updates directory not found: ${updatesDir}`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const updates = {
|
|
146
|
+
progress: '',
|
|
147
|
+
notes: '',
|
|
148
|
+
commits: '',
|
|
149
|
+
acceptanceCriteria: '',
|
|
150
|
+
nextSteps: '',
|
|
151
|
+
blockers: ''
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
// Gather progress
|
|
155
|
+
const progressFile = path.join(updatesDir, 'progress.md');
|
|
156
|
+
if (fs.existsSync(progressFile)) {
|
|
157
|
+
updates.progress = stripFrontmatter(progressFile);
|
|
158
|
+
const frontmatter = parseFrontmatter(progressFile);
|
|
159
|
+
if (frontmatter && frontmatter.completion) {
|
|
160
|
+
updates.progress += `\n\n**Current Progress:** ${frontmatter.completion}`;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Gather notes
|
|
165
|
+
const notesFile = path.join(updatesDir, 'notes.md');
|
|
166
|
+
if (fs.existsSync(notesFile)) {
|
|
167
|
+
updates.notes = stripFrontmatter(notesFile);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Gather commits
|
|
171
|
+
const commitsFile = path.join(updatesDir, 'commits.md');
|
|
172
|
+
if (fs.existsSync(commitsFile)) {
|
|
173
|
+
updates.commits = stripFrontmatter(commitsFile);
|
|
174
|
+
} else {
|
|
175
|
+
// Auto-gather recent commits
|
|
176
|
+
updates.commits = gatherRecentCommits(lastSync);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Gather acceptance criteria
|
|
180
|
+
const criteriaFile = path.join(updatesDir, 'acceptance-criteria.md');
|
|
181
|
+
if (fs.existsSync(criteriaFile)) {
|
|
182
|
+
updates.acceptanceCriteria = stripFrontmatter(criteriaFile);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Gather next steps
|
|
186
|
+
const nextStepsFile = path.join(updatesDir, 'next-steps.md');
|
|
187
|
+
if (fs.existsSync(nextStepsFile)) {
|
|
188
|
+
updates.nextSteps = stripFrontmatter(nextStepsFile);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Gather blockers
|
|
192
|
+
const blockersFile = path.join(updatesDir, 'blockers.md');
|
|
193
|
+
if (fs.existsSync(blockersFile)) {
|
|
194
|
+
updates.blockers = stripFrontmatter(blockersFile);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
console.log('✅ Updates gathered');
|
|
198
|
+
return updates;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Auto-gather recent commits
|
|
203
|
+
*/
|
|
204
|
+
function gatherRecentCommits(since = null) {
|
|
205
|
+
try {
|
|
206
|
+
const sinceArg = since || '24 hours ago';
|
|
207
|
+
const result = execSync(
|
|
208
|
+
`git log --since="${sinceArg}" --oneline --no-merges`,
|
|
209
|
+
{ encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
if (result.trim()) {
|
|
213
|
+
const commits = result.trim().split('\n').map(line => `- ${line}`);
|
|
214
|
+
return `**Recent Commits:**\n${commits.join('\n')}`;
|
|
215
|
+
}
|
|
216
|
+
} catch (error) {
|
|
217
|
+
// Git not available or no commits
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return 'No recent commits found';
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Format comment from gathered updates
|
|
225
|
+
*/
|
|
226
|
+
function formatComment(issueNumber, updates, isCompletion = false) {
|
|
227
|
+
console.log(`\n📝 Formatting comment for issue #${issueNumber}`);
|
|
228
|
+
|
|
229
|
+
const sections = [];
|
|
230
|
+
|
|
231
|
+
if (isCompletion) {
|
|
232
|
+
sections.push('# ✅ Task Completed\n');
|
|
233
|
+
sections.push(`*Completed at: ${getTimestamp()}*\n`);
|
|
234
|
+
} else {
|
|
235
|
+
sections.push('# 📊 Progress Update\n');
|
|
236
|
+
sections.push(`*Updated at: ${getTimestamp()}*\n`);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Add sections with content
|
|
240
|
+
if (updates.progress && !updates.progress.includes('No progress')) {
|
|
241
|
+
sections.push('## Progress Updates\n');
|
|
242
|
+
sections.push(updates.progress + '\n');
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (updates.notes && !updates.notes.includes('No technical notes')) {
|
|
246
|
+
sections.push('## Technical Notes\n');
|
|
247
|
+
sections.push(updates.notes + '\n');
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (updates.commits && !updates.commits.includes('No recent commits')) {
|
|
251
|
+
sections.push('## Recent Commits\n');
|
|
252
|
+
sections.push(updates.commits + '\n');
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (updates.acceptanceCriteria && !updates.acceptanceCriteria.includes('No acceptance')) {
|
|
256
|
+
sections.push('## Acceptance Criteria\n');
|
|
257
|
+
sections.push(updates.acceptanceCriteria + '\n');
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (updates.nextSteps && !updates.nextSteps.includes('No specific next steps')) {
|
|
261
|
+
sections.push('## Next Steps\n');
|
|
262
|
+
sections.push(updates.nextSteps + '\n');
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (updates.blockers && !updates.blockers.includes('No current blockers')) {
|
|
266
|
+
sections.push('## Blockers\n');
|
|
267
|
+
sections.push(updates.blockers + '\n');
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const comment = sections.join('\n');
|
|
271
|
+
console.log('✅ Comment formatted');
|
|
272
|
+
|
|
273
|
+
return comment;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Post comment to GitHub issue
|
|
278
|
+
*/
|
|
279
|
+
function postComment(issueNumber, comment, isDryRun = false) {
|
|
280
|
+
console.log(`\n💬 Posting comment to issue #${issueNumber}`);
|
|
281
|
+
|
|
282
|
+
if (isDryRun) {
|
|
283
|
+
console.log('🔸 DRY RUN - Comment preview:');
|
|
284
|
+
console.log(comment.split('\n').slice(0, 20).join('\n'));
|
|
285
|
+
console.log('...');
|
|
286
|
+
return 'https://github.com/DRYRUN/issues/' + issueNumber + '#issuecomment-DRYRUN';
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Write comment to temp file
|
|
290
|
+
const tempFile = `/tmp/issue-comment-${issueNumber}-${Date.now()}.md`;
|
|
291
|
+
fs.writeFileSync(tempFile, comment, 'utf8');
|
|
292
|
+
|
|
293
|
+
try {
|
|
294
|
+
// Post via gh CLI
|
|
295
|
+
const result = execSync(
|
|
296
|
+
`gh issue comment ${issueNumber} --body-file "${tempFile}"`,
|
|
297
|
+
{ encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }
|
|
298
|
+
);
|
|
299
|
+
|
|
300
|
+
// Extract URL from result
|
|
301
|
+
const match = result.match(/https:\/\/github\.com\/[^\/]+\/[^\/]+\/issues\/\d+#issuecomment-\d+/);
|
|
302
|
+
if (match) {
|
|
303
|
+
const url = match[0];
|
|
304
|
+
console.log(`✅ Comment posted: ${url}`);
|
|
305
|
+
|
|
306
|
+
// Cleanup temp file
|
|
307
|
+
fs.unlinkSync(tempFile);
|
|
308
|
+
|
|
309
|
+
return url;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
console.log('⚠️ Comment posted but URL not found in response');
|
|
313
|
+
return null;
|
|
314
|
+
} catch (error) {
|
|
315
|
+
console.error(`❌ Failed to post comment: ${error.message}`);
|
|
316
|
+
console.log(`Temp comment file preserved: ${tempFile}`);
|
|
317
|
+
throw error;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Update frontmatter after successful sync
|
|
323
|
+
*/
|
|
324
|
+
function updateFrontmatterAfterSync(progressFile, commentUrl, isCompletion = false) {
|
|
325
|
+
console.log(`\n📄 Updating frontmatter: ${progressFile}`);
|
|
326
|
+
|
|
327
|
+
if (!fs.existsSync(progressFile)) {
|
|
328
|
+
throw new Error(`Progress file not found: ${progressFile}`);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Create backup
|
|
332
|
+
const backupFile = `${progressFile}.backup.${Date.now()}`;
|
|
333
|
+
fs.copyFileSync(progressFile, backupFile);
|
|
334
|
+
|
|
335
|
+
try {
|
|
336
|
+
// Update last_sync
|
|
337
|
+
updateFrontmatterField(progressFile, 'last_sync', getTimestamp());
|
|
338
|
+
|
|
339
|
+
// Update comment URL if provided
|
|
340
|
+
if (commentUrl) {
|
|
341
|
+
updateFrontmatterField(progressFile, 'last_comment_url', commentUrl);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Update completion if needed
|
|
345
|
+
if (isCompletion) {
|
|
346
|
+
updateFrontmatterField(progressFile, 'completion', '100');
|
|
347
|
+
updateFrontmatterField(progressFile, 'status', 'completed');
|
|
348
|
+
updateFrontmatterField(progressFile, 'completed_at', getTimestamp());
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Update issue state from GitHub
|
|
352
|
+
try {
|
|
353
|
+
const issueState = execSync('gh issue view --json state -q .state', {
|
|
354
|
+
encoding: 'utf8',
|
|
355
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
356
|
+
}).trim();
|
|
357
|
+
|
|
358
|
+
if (issueState) {
|
|
359
|
+
updateFrontmatterField(progressFile, 'issue_state', issueState);
|
|
360
|
+
}
|
|
361
|
+
} catch (error) {
|
|
362
|
+
// GitHub CLI not available or issue not found
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
console.log('✅ Frontmatter updated');
|
|
366
|
+
|
|
367
|
+
// Cleanup old backups (keep last 5)
|
|
368
|
+
cleanupOldBackups(progressFile, 5);
|
|
369
|
+
|
|
370
|
+
} catch (error) {
|
|
371
|
+
console.error('❌ Frontmatter update failed, restoring backup');
|
|
372
|
+
fs.copyFileSync(backupFile, progressFile);
|
|
373
|
+
throw error;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Cleanup old backup files
|
|
379
|
+
*/
|
|
380
|
+
function cleanupOldBackups(originalFile, keepCount = 5) {
|
|
381
|
+
const dir = path.dirname(originalFile);
|
|
382
|
+
const basename = path.basename(originalFile);
|
|
383
|
+
|
|
384
|
+
try {
|
|
385
|
+
const backups = fs.readdirSync(dir)
|
|
386
|
+
.filter(f => f.startsWith(basename + '.backup.'))
|
|
387
|
+
.map(f => ({
|
|
388
|
+
name: f,
|
|
389
|
+
path: path.join(dir, f),
|
|
390
|
+
time: fs.statSync(path.join(dir, f)).mtime.getTime()
|
|
391
|
+
}))
|
|
392
|
+
.sort((a, b) => b.time - a.time);
|
|
393
|
+
|
|
394
|
+
// Remove old backups
|
|
395
|
+
for (let i = keepCount; i < backups.length; i++) {
|
|
396
|
+
fs.unlinkSync(backups[i].path);
|
|
397
|
+
}
|
|
398
|
+
} catch (error) {
|
|
399
|
+
// Ignore cleanup errors
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Preflight validation
|
|
405
|
+
*/
|
|
406
|
+
function preflightValidation(issueNumber, updatesDir) {
|
|
407
|
+
console.log(`\n🔍 Preflight validation for issue #${issueNumber}`);
|
|
408
|
+
|
|
409
|
+
const errors = [];
|
|
410
|
+
|
|
411
|
+
// Check GitHub auth
|
|
412
|
+
try {
|
|
413
|
+
execSync('gh auth status', { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
414
|
+
} catch (error) {
|
|
415
|
+
errors.push('GitHub CLI not authenticated. Run: gh auth login');
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// Check if issue exists
|
|
419
|
+
try {
|
|
420
|
+
const state = execSync(`gh issue view ${issueNumber} --json state -q .state`, {
|
|
421
|
+
encoding: 'utf8',
|
|
422
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
423
|
+
}).trim();
|
|
424
|
+
|
|
425
|
+
if (state === 'CLOSED') {
|
|
426
|
+
console.log(`⚠️ Issue #${issueNumber} is closed`);
|
|
427
|
+
}
|
|
428
|
+
} catch (error) {
|
|
429
|
+
errors.push(`Issue #${issueNumber} not found`);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// Check updates directory
|
|
433
|
+
if (!fs.existsSync(updatesDir)) {
|
|
434
|
+
errors.push(`Updates directory not found: ${updatesDir}`);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
if (errors.length > 0) {
|
|
438
|
+
console.error('\n❌ Preflight validation failed:');
|
|
439
|
+
errors.forEach(err => console.error(` - ${err}`));
|
|
440
|
+
return false;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
console.log('✅ Preflight validation passed');
|
|
444
|
+
return true;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Full sync workflow
|
|
449
|
+
*/
|
|
450
|
+
function syncIssue(issueNumber, updatesDir, isCompletion = false, isDryRun = false) {
|
|
451
|
+
console.log(`\n🚀 Starting issue sync: #${issueNumber}`);
|
|
452
|
+
|
|
453
|
+
// Preflight validation
|
|
454
|
+
if (!preflightValidation(issueNumber, updatesDir)) {
|
|
455
|
+
throw new Error('Preflight validation failed');
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// Gather updates
|
|
459
|
+
const progressFile = path.join(updatesDir, 'progress.md');
|
|
460
|
+
const frontmatter = parseFrontmatter(progressFile);
|
|
461
|
+
const lastSync = frontmatter ? frontmatter.last_sync : null;
|
|
462
|
+
|
|
463
|
+
const updates = gatherUpdates(issueNumber, updatesDir, lastSync);
|
|
464
|
+
|
|
465
|
+
// Format comment
|
|
466
|
+
const comment = formatComment(issueNumber, updates, isCompletion);
|
|
467
|
+
|
|
468
|
+
// Post comment
|
|
469
|
+
const commentUrl = postComment(issueNumber, comment, isDryRun);
|
|
470
|
+
|
|
471
|
+
// Update frontmatter
|
|
472
|
+
if (!isDryRun && fs.existsSync(progressFile)) {
|
|
473
|
+
updateFrontmatterAfterSync(progressFile, commentUrl, isCompletion);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
console.log(`\n✅ Issue sync complete!`);
|
|
477
|
+
console.log(` Issue: #${issueNumber}`);
|
|
478
|
+
if (commentUrl) {
|
|
479
|
+
console.log(` Comment: ${commentUrl}`);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
return { commentUrl, updates };
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* CLI interface
|
|
487
|
+
*/
|
|
488
|
+
function main() {
|
|
489
|
+
const args = process.argv.slice(2);
|
|
490
|
+
const command = args[0];
|
|
491
|
+
|
|
492
|
+
if (!command) {
|
|
493
|
+
console.log('Usage:');
|
|
494
|
+
console.log(' issueSync.js sync <issue-number> <updates-dir> [--complete] [--dry-run]');
|
|
495
|
+
console.log(' issueSync.js gather <issue-number> <updates-dir>');
|
|
496
|
+
console.log(' issueSync.js format <issue-number> <updates-dir> [--complete]');
|
|
497
|
+
console.log(' issueSync.js post <issue-number> <comment-file> [--dry-run]');
|
|
498
|
+
console.log(' issueSync.js update <progress-file> <comment-url> [--complete]');
|
|
499
|
+
console.log('');
|
|
500
|
+
console.log('Examples:');
|
|
501
|
+
console.log(' issueSync.js sync 123 .claude/epics/auth/updates/123');
|
|
502
|
+
console.log(' issueSync.js sync 456 ./updates --complete');
|
|
503
|
+
console.log(' issueSync.js sync 789 ./updates --dry-run');
|
|
504
|
+
process.exit(1);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
const issueNumber = args[1];
|
|
508
|
+
const isComplete = args.includes('--complete');
|
|
509
|
+
const isDryRun = args.includes('--dry-run');
|
|
510
|
+
|
|
511
|
+
try {
|
|
512
|
+
switch (command) {
|
|
513
|
+
case 'sync': {
|
|
514
|
+
const updatesDir = args[2];
|
|
515
|
+
if (!issueNumber || !updatesDir) {
|
|
516
|
+
console.error('Error: issue-number and updates-dir required');
|
|
517
|
+
process.exit(1);
|
|
518
|
+
}
|
|
519
|
+
syncIssue(issueNumber, updatesDir, isComplete, isDryRun);
|
|
520
|
+
break;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
case 'gather': {
|
|
524
|
+
const updatesDir = args[2];
|
|
525
|
+
if (!issueNumber || !updatesDir) {
|
|
526
|
+
console.error('Error: issue-number and updates-dir required');
|
|
527
|
+
process.exit(1);
|
|
528
|
+
}
|
|
529
|
+
const updates = gatherUpdates(issueNumber, updatesDir);
|
|
530
|
+
console.log(JSON.stringify(updates, null, 2));
|
|
531
|
+
break;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
case 'format': {
|
|
535
|
+
const updatesDir = args[2];
|
|
536
|
+
if (!issueNumber || !updatesDir) {
|
|
537
|
+
console.error('Error: issue-number and updates-dir required');
|
|
538
|
+
process.exit(1);
|
|
539
|
+
}
|
|
540
|
+
const updates = gatherUpdates(issueNumber, updatesDir);
|
|
541
|
+
const comment = formatComment(issueNumber, updates, isComplete);
|
|
542
|
+
console.log(comment);
|
|
543
|
+
break;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
case 'post': {
|
|
547
|
+
const commentFile = args[2];
|
|
548
|
+
if (!issueNumber || !commentFile) {
|
|
549
|
+
console.error('Error: issue-number and comment-file required');
|
|
550
|
+
process.exit(1);
|
|
551
|
+
}
|
|
552
|
+
const comment = fs.readFileSync(commentFile, 'utf8');
|
|
553
|
+
const url = postComment(issueNumber, comment, isDryRun);
|
|
554
|
+
console.log(url);
|
|
555
|
+
break;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
case 'update': {
|
|
559
|
+
const progressFile = args[2];
|
|
560
|
+
const commentUrl = args[3];
|
|
561
|
+
if (!progressFile) {
|
|
562
|
+
console.error('Error: progress-file required');
|
|
563
|
+
process.exit(1);
|
|
564
|
+
}
|
|
565
|
+
updateFrontmatterAfterSync(progressFile, commentUrl, isComplete);
|
|
566
|
+
break;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
default:
|
|
570
|
+
console.error(`Unknown command: ${command}`);
|
|
571
|
+
process.exit(1);
|
|
572
|
+
}
|
|
573
|
+
} catch (error) {
|
|
574
|
+
console.error(`\n❌ Error: ${error.message}`);
|
|
575
|
+
process.exit(1);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
if (require.main === module) {
|
|
580
|
+
main();
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
module.exports = {
|
|
584
|
+
parseFrontmatter,
|
|
585
|
+
updateFrontmatterField,
|
|
586
|
+
stripFrontmatter,
|
|
587
|
+
gatherUpdates,
|
|
588
|
+
formatComment,
|
|
589
|
+
postComment,
|
|
590
|
+
updateFrontmatterAfterSync,
|
|
591
|
+
preflightValidation,
|
|
592
|
+
syncIssue
|
|
593
|
+
};
|