claude-autopm 1.30.0 → 1.31.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.
- package/autopm/.claude/scripts/github/dependency-tracker.js +554 -0
- package/autopm/.claude/scripts/github/dependency-validator.js +545 -0
- package/autopm/.claude/scripts/github/dependency-visualizer.js +477 -0
- package/autopm/.claude/scripts/pm/lib/epic-discovery.js +119 -0
- package/autopm/.claude/scripts/pm/next.js +56 -58
- package/autopm/.claude/scripts/pm/prd-new.js +33 -6
- package/autopm/.claude/scripts/pm/template-list.js +25 -3
- package/autopm/.claude/scripts/pm/template-new.js +25 -3
- package/autopm/lib/README-FILTER-SEARCH.md +285 -0
- package/autopm/lib/analytics-engine.js +689 -0
- package/autopm/lib/batch-processor-integration.js +366 -0
- package/autopm/lib/batch-processor.js +278 -0
- package/autopm/lib/burndown-chart.js +415 -0
- package/autopm/lib/conflict-history.js +316 -0
- package/autopm/lib/conflict-resolver.js +330 -0
- package/autopm/lib/dependency-analyzer.js +466 -0
- package/autopm/lib/filter-engine.js +414 -0
- package/autopm/lib/guide/interactive-guide.js +756 -0
- package/autopm/lib/guide/manager.js +663 -0
- package/autopm/lib/query-parser.js +322 -0
- package/autopm/lib/template-engine.js +347 -0
- package/autopm/lib/visual-diff.js +297 -0
- package/bin/autopm-poc.js +216 -0
- package/install/install.js +2 -1
- package/lib/ai-providers/ClaudeProvider.js +112 -0
- package/lib/ai-providers/base-provider.js +110 -0
- package/lib/services/PRDService.js +178 -0
- package/package.json +5 -2
|
@@ -0,0 +1,554 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* GitHub Issue Dependency Tracker
|
|
4
|
+
*
|
|
5
|
+
* Manages dependency relationships between GitHub issues using:
|
|
6
|
+
* - Label-based dependencies (depends-on:#123)
|
|
7
|
+
* - Native GitHub dependency API (when available)
|
|
8
|
+
* - Local cache for offline mode
|
|
9
|
+
*
|
|
10
|
+
* @module dependency-tracker
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
const { Octokit } = require('@octokit/rest');
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* DependencyTracker class for managing GitHub issue dependencies
|
|
19
|
+
*/
|
|
20
|
+
class DependencyTracker {
|
|
21
|
+
/**
|
|
22
|
+
* Initialize dependency tracker
|
|
23
|
+
*
|
|
24
|
+
* @param {Object} config - Configuration options
|
|
25
|
+
* @param {string} config.owner - GitHub repository owner
|
|
26
|
+
* @param {string} config.repo - GitHub repository name
|
|
27
|
+
* @param {string} config.token - GitHub personal access token
|
|
28
|
+
* @param {boolean} config.localMode - Enable local mode (no API calls)
|
|
29
|
+
* @param {string} config.cacheDir - Directory for local cache
|
|
30
|
+
*/
|
|
31
|
+
constructor(config = {}) {
|
|
32
|
+
this.localMode = config.localMode || false;
|
|
33
|
+
this.cacheDir = config.cacheDir || path.join(process.cwd(), '.claude', 'cache');
|
|
34
|
+
|
|
35
|
+
if (!this.localMode) {
|
|
36
|
+
// Load from config or environment
|
|
37
|
+
this.owner = config.owner || process.env.GITHUB_OWNER;
|
|
38
|
+
this.repo = config.repo || process.env.GITHUB_REPO;
|
|
39
|
+
const token = config.token || process.env.GITHUB_TOKEN;
|
|
40
|
+
|
|
41
|
+
if (!this.owner || !this.repo) {
|
|
42
|
+
throw new Error('GitHub configuration missing: owner and repo required');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (!token) {
|
|
46
|
+
throw new Error('GitHub configuration missing: token required (set GITHUB_TOKEN)');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Initialize Octokit
|
|
50
|
+
this.octokit = new Octokit({ auth: token });
|
|
51
|
+
} else {
|
|
52
|
+
this.owner = config.owner || 'local';
|
|
53
|
+
this.repo = config.repo || 'local';
|
|
54
|
+
|
|
55
|
+
// Ensure cache directory exists
|
|
56
|
+
if (!fs.existsSync(this.cacheDir)) {
|
|
57
|
+
fs.mkdirSync(this.cacheDir, { recursive: true });
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Add dependency relationship between issues
|
|
64
|
+
*
|
|
65
|
+
* @param {number} issueNumber - Issue that depends on another
|
|
66
|
+
* @param {number|number[]} dependsOn - Issue(s) that must be resolved first
|
|
67
|
+
* @param {Object} options - Additional options
|
|
68
|
+
* @param {boolean} options.useNativeAPI - Use GitHub native dependency API
|
|
69
|
+
* @returns {Promise<Object>} Result object with success status
|
|
70
|
+
*/
|
|
71
|
+
async addDependency(issueNumber, dependsOn, options = {}) {
|
|
72
|
+
try {
|
|
73
|
+
// Validate issue number
|
|
74
|
+
if (typeof issueNumber !== 'number' || issueNumber <= 0) {
|
|
75
|
+
return {
|
|
76
|
+
success: false,
|
|
77
|
+
error: 'Invalid issue number'
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Ensure dependsOn is an array
|
|
82
|
+
const dependencies = Array.isArray(dependsOn) ? dependsOn : [dependsOn];
|
|
83
|
+
|
|
84
|
+
// Check for self-dependency
|
|
85
|
+
if (dependencies.includes(issueNumber)) {
|
|
86
|
+
return {
|
|
87
|
+
success: false,
|
|
88
|
+
error: `Issue #${issueNumber} cannot depend on itself`
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Local mode
|
|
93
|
+
if (this.localMode) {
|
|
94
|
+
return await this._addDependencyLocal(issueNumber, dependencies);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Validate dependencies exist
|
|
98
|
+
for (const depNumber of dependencies) {
|
|
99
|
+
try {
|
|
100
|
+
await this.octokit.rest.issues.get({
|
|
101
|
+
owner: this.owner,
|
|
102
|
+
repo: this.repo,
|
|
103
|
+
issue_number: depNumber
|
|
104
|
+
});
|
|
105
|
+
} catch (error) {
|
|
106
|
+
if (error.status === 404) {
|
|
107
|
+
return {
|
|
108
|
+
success: false,
|
|
109
|
+
error: `Dependency issue #${depNumber} not found`
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
throw error;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Use native API if requested and available
|
|
117
|
+
if (options.useNativeAPI) {
|
|
118
|
+
return await this._addDependencyNativeAPI(issueNumber, dependencies);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Use label-based approach (default)
|
|
122
|
+
return await this._addDependencyLabel(issueNumber, dependencies);
|
|
123
|
+
|
|
124
|
+
} catch (error) {
|
|
125
|
+
return {
|
|
126
|
+
success: false,
|
|
127
|
+
error: error.message
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Add dependency using GitHub native API
|
|
134
|
+
* @private
|
|
135
|
+
*/
|
|
136
|
+
async _addDependencyNativeAPI(issueNumber, dependencies) {
|
|
137
|
+
try {
|
|
138
|
+
for (const depNumber of dependencies) {
|
|
139
|
+
await this.octokit.request(
|
|
140
|
+
'POST /repos/{owner}/{repo}/issues/{issue_number}/dependencies/blocked_by',
|
|
141
|
+
{
|
|
142
|
+
owner: this.owner,
|
|
143
|
+
repo: this.repo,
|
|
144
|
+
issue_number: issueNumber,
|
|
145
|
+
blocked_by_issue_number: depNumber
|
|
146
|
+
}
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
success: true,
|
|
152
|
+
method: 'native-api',
|
|
153
|
+
dependencies
|
|
154
|
+
};
|
|
155
|
+
} catch (error) {
|
|
156
|
+
// Fallback to label-based if native API not available
|
|
157
|
+
if (error.status === 404 || error.status === 422) {
|
|
158
|
+
return await this._addDependencyLabel(issueNumber, dependencies);
|
|
159
|
+
}
|
|
160
|
+
throw error;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Add dependency using label-based approach
|
|
166
|
+
* @private
|
|
167
|
+
*/
|
|
168
|
+
async _addDependencyLabel(issueNumber, dependencies) {
|
|
169
|
+
// Get current labels
|
|
170
|
+
const issue = await this.octokit.rest.issues.get({
|
|
171
|
+
owner: this.owner,
|
|
172
|
+
repo: this.repo,
|
|
173
|
+
issue_number: issueNumber
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
const currentLabels = issue.data.labels.map(l => l.name);
|
|
177
|
+
|
|
178
|
+
// Add dependency labels
|
|
179
|
+
const dependencyLabels = dependencies.map(dep => `depends-on:#${dep}`);
|
|
180
|
+
const newLabels = [...currentLabels, ...dependencyLabels];
|
|
181
|
+
|
|
182
|
+
// Remove duplicates
|
|
183
|
+
const uniqueLabels = [...new Set(newLabels)];
|
|
184
|
+
|
|
185
|
+
// Update issue with new labels
|
|
186
|
+
await this.octokit.rest.issues.update({
|
|
187
|
+
owner: this.owner,
|
|
188
|
+
repo: this.repo,
|
|
189
|
+
issue_number: issueNumber,
|
|
190
|
+
labels: uniqueLabels
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
return {
|
|
194
|
+
success: true,
|
|
195
|
+
method: 'label',
|
|
196
|
+
dependencies
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Add dependency in local mode
|
|
202
|
+
* @private
|
|
203
|
+
*/
|
|
204
|
+
async _addDependencyLocal(issueNumber, dependencies) {
|
|
205
|
+
const cacheFile = path.join(this.cacheDir, 'dependencies.json');
|
|
206
|
+
let cache = {};
|
|
207
|
+
|
|
208
|
+
if (fs.existsSync(cacheFile)) {
|
|
209
|
+
cache = JSON.parse(fs.readFileSync(cacheFile, 'utf8'));
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Add dependencies
|
|
213
|
+
const issueKey = issueNumber.toString();
|
|
214
|
+
if (!cache[issueKey]) {
|
|
215
|
+
cache[issueKey] = [];
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
cache[issueKey] = [...new Set([...cache[issueKey], ...dependencies])];
|
|
219
|
+
|
|
220
|
+
fs.writeFileSync(cacheFile, JSON.stringify(cache, null, 2), 'utf8');
|
|
221
|
+
|
|
222
|
+
return {
|
|
223
|
+
success: true,
|
|
224
|
+
method: 'local',
|
|
225
|
+
dependencies
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Remove dependency relationship between issues
|
|
231
|
+
*
|
|
232
|
+
* @param {number} issueNumber - Issue to remove dependency from
|
|
233
|
+
* @param {number|number[]} dependsOn - Dependency/dependencies to remove
|
|
234
|
+
* @returns {Promise<Object>} Result object with success status
|
|
235
|
+
*/
|
|
236
|
+
async removeDependency(issueNumber, dependsOn) {
|
|
237
|
+
try {
|
|
238
|
+
const dependencies = Array.isArray(dependsOn) ? dependsOn : [dependsOn];
|
|
239
|
+
|
|
240
|
+
if (this.localMode) {
|
|
241
|
+
return await this._removeDependencyLocal(issueNumber, dependencies);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Get current issue
|
|
245
|
+
const issue = await this.octokit.rest.issues.get({
|
|
246
|
+
owner: this.owner,
|
|
247
|
+
repo: this.repo,
|
|
248
|
+
issue_number: issueNumber
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
const currentLabels = issue.data.labels.map(l => l.name);
|
|
252
|
+
|
|
253
|
+
// Remove dependency labels
|
|
254
|
+
const dependencyLabels = dependencies.map(dep => `depends-on:#${dep}`);
|
|
255
|
+
const newLabels = currentLabels.filter(label => !dependencyLabels.includes(label));
|
|
256
|
+
|
|
257
|
+
// Check if any dependencies were actually removed
|
|
258
|
+
const removedCount = currentLabels.length - newLabels.length;
|
|
259
|
+
|
|
260
|
+
if (removedCount === 0) {
|
|
261
|
+
return {
|
|
262
|
+
success: true,
|
|
263
|
+
message: 'Dependency not found, no changes made'
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Update issue
|
|
268
|
+
await this.octokit.rest.issues.update({
|
|
269
|
+
owner: this.owner,
|
|
270
|
+
repo: this.repo,
|
|
271
|
+
issue_number: issueNumber,
|
|
272
|
+
labels: newLabels
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
return {
|
|
276
|
+
success: true,
|
|
277
|
+
removed: dependencies
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
} catch (error) {
|
|
281
|
+
return {
|
|
282
|
+
success: false,
|
|
283
|
+
error: error.message
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Remove dependency in local mode
|
|
290
|
+
* @private
|
|
291
|
+
*/
|
|
292
|
+
async _removeDependencyLocal(issueNumber, dependencies) {
|
|
293
|
+
const cacheFile = path.join(this.cacheDir, 'dependencies.json');
|
|
294
|
+
|
|
295
|
+
if (!fs.existsSync(cacheFile)) {
|
|
296
|
+
return {
|
|
297
|
+
success: true,
|
|
298
|
+
message: 'No dependencies found'
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const cache = JSON.parse(fs.readFileSync(cacheFile, 'utf8'));
|
|
303
|
+
const issueKey = issueNumber.toString();
|
|
304
|
+
|
|
305
|
+
if (!cache[issueKey]) {
|
|
306
|
+
return {
|
|
307
|
+
success: true,
|
|
308
|
+
message: 'No dependencies found for this issue'
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Remove dependencies
|
|
313
|
+
cache[issueKey] = cache[issueKey].filter(dep => !dependencies.includes(dep));
|
|
314
|
+
|
|
315
|
+
// Remove issue key if no dependencies left
|
|
316
|
+
if (cache[issueKey].length === 0) {
|
|
317
|
+
delete cache[issueKey];
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
fs.writeFileSync(cacheFile, JSON.stringify(cache, null, 2), 'utf8');
|
|
321
|
+
|
|
322
|
+
return {
|
|
323
|
+
success: true,
|
|
324
|
+
removed: dependencies
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Get all dependencies for an issue
|
|
330
|
+
*
|
|
331
|
+
* @param {number} issueNumber - Issue to get dependencies for
|
|
332
|
+
* @param {Object} options - Additional options
|
|
333
|
+
* @param {boolean} options.detailed - Return detailed info for each dependency
|
|
334
|
+
* @param {boolean} options.useNativeAPI - Use GitHub native dependency API
|
|
335
|
+
* @returns {Promise<number[]|Object[]>} Array of issue numbers or detailed objects
|
|
336
|
+
*/
|
|
337
|
+
async getDependencies(issueNumber, options = {}) {
|
|
338
|
+
try {
|
|
339
|
+
if (this.localMode) {
|
|
340
|
+
return await this._getDependenciesLocal(issueNumber);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (options.useNativeAPI) {
|
|
344
|
+
return await this._getDependenciesNativeAPI(issueNumber, options);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Get issue
|
|
348
|
+
const issue = await this.octokit.rest.issues.get({
|
|
349
|
+
owner: this.owner,
|
|
350
|
+
repo: this.repo,
|
|
351
|
+
issue_number: issueNumber
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
if (!issue.data.labels) {
|
|
355
|
+
return [];
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Extract dependency issue numbers from labels
|
|
359
|
+
const dependencyLabels = issue.data.labels
|
|
360
|
+
.map(l => l.name)
|
|
361
|
+
.filter(name => name.startsWith('depends-on:#'));
|
|
362
|
+
|
|
363
|
+
const dependencies = dependencyLabels.map(label => {
|
|
364
|
+
const match = label.match(/depends-on:#(\d+)/);
|
|
365
|
+
return match ? parseInt(match[1], 10) : null;
|
|
366
|
+
}).filter(num => num !== null);
|
|
367
|
+
|
|
368
|
+
// Return detailed info if requested
|
|
369
|
+
if (options.detailed) {
|
|
370
|
+
const detailedDeps = await Promise.all(
|
|
371
|
+
dependencies.map(async (depNum) => {
|
|
372
|
+
try {
|
|
373
|
+
const depIssue = await this.octokit.rest.issues.get({
|
|
374
|
+
owner: this.owner,
|
|
375
|
+
repo: this.repo,
|
|
376
|
+
issue_number: depNum
|
|
377
|
+
});
|
|
378
|
+
return depIssue.data;
|
|
379
|
+
} catch {
|
|
380
|
+
return { number: depNum, error: 'Failed to fetch' };
|
|
381
|
+
}
|
|
382
|
+
})
|
|
383
|
+
);
|
|
384
|
+
return detailedDeps;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
return dependencies;
|
|
388
|
+
|
|
389
|
+
} catch (error) {
|
|
390
|
+
console.error('Error getting dependencies:', error.message);
|
|
391
|
+
return [];
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Get dependencies using native API
|
|
397
|
+
* @private
|
|
398
|
+
*/
|
|
399
|
+
async _getDependenciesNativeAPI(issueNumber, options) {
|
|
400
|
+
try {
|
|
401
|
+
const response = await this.octokit.request(
|
|
402
|
+
'GET /repos/{owner}/{repo}/issues/{issue_number}/dependencies',
|
|
403
|
+
{
|
|
404
|
+
owner: this.owner,
|
|
405
|
+
repo: this.repo,
|
|
406
|
+
issue_number: issueNumber
|
|
407
|
+
}
|
|
408
|
+
);
|
|
409
|
+
|
|
410
|
+
if (options.detailed) {
|
|
411
|
+
return response.data.dependencies;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
return response.data.dependencies.map(dep => dep.number);
|
|
415
|
+
} catch {
|
|
416
|
+
// Fallback to label-based
|
|
417
|
+
return await this.getDependencies(issueNumber, { ...options, useNativeAPI: false });
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Get dependencies in local mode
|
|
423
|
+
* @private
|
|
424
|
+
*/
|
|
425
|
+
async _getDependenciesLocal(issueNumber) {
|
|
426
|
+
const cacheFile = path.join(this.cacheDir, 'dependencies.json');
|
|
427
|
+
|
|
428
|
+
if (!fs.existsSync(cacheFile)) {
|
|
429
|
+
return [];
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const cache = JSON.parse(fs.readFileSync(cacheFile, 'utf8'));
|
|
433
|
+
const issueKey = issueNumber.toString();
|
|
434
|
+
|
|
435
|
+
return cache[issueKey] || [];
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Get issues blocked by the given issue
|
|
440
|
+
*
|
|
441
|
+
* @param {number} issueNumber - Issue that may be blocking others
|
|
442
|
+
* @param {Object} options - Additional options
|
|
443
|
+
* @param {boolean} options.detailed - Return detailed info
|
|
444
|
+
* @returns {Promise<number[]|Object[]>} Array of blocked issue numbers
|
|
445
|
+
*/
|
|
446
|
+
async getBlockedIssues(issueNumber, options = {}) {
|
|
447
|
+
try {
|
|
448
|
+
if (this.localMode) {
|
|
449
|
+
return await this._getBlockedIssuesLocal(issueNumber);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// Fetch all issues with their labels
|
|
453
|
+
const allIssues = await this.octokit.paginate(
|
|
454
|
+
this.octokit.rest.issues.listForRepo,
|
|
455
|
+
{
|
|
456
|
+
owner: this.owner,
|
|
457
|
+
repo: this.repo,
|
|
458
|
+
state: 'all',
|
|
459
|
+
per_page: 100
|
|
460
|
+
}
|
|
461
|
+
);
|
|
462
|
+
|
|
463
|
+
// Find issues that depend on this issue
|
|
464
|
+
const dependencyLabel = `depends-on:#${issueNumber}`;
|
|
465
|
+
const blockedIssues = allIssues.filter(issue =>
|
|
466
|
+
issue.labels.some(label => label.name === dependencyLabel)
|
|
467
|
+
);
|
|
468
|
+
|
|
469
|
+
if (options.detailed) {
|
|
470
|
+
return blockedIssues;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
return blockedIssues.map(issue => issue.number);
|
|
474
|
+
|
|
475
|
+
} catch (error) {
|
|
476
|
+
console.error('Error getting blocked issues:', error.message);
|
|
477
|
+
return [];
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Get blocked issues in local mode
|
|
483
|
+
* @private
|
|
484
|
+
*/
|
|
485
|
+
async _getBlockedIssuesLocal(issueNumber) {
|
|
486
|
+
const cacheFile = path.join(this.cacheDir, 'dependencies.json');
|
|
487
|
+
|
|
488
|
+
if (!fs.existsSync(cacheFile)) {
|
|
489
|
+
return [];
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
const cache = JSON.parse(fs.readFileSync(cacheFile, 'utf8'));
|
|
493
|
+
const blocked = [];
|
|
494
|
+
|
|
495
|
+
// Find issues that list this issue as a dependency
|
|
496
|
+
for (const [issue, dependencies] of Object.entries(cache)) {
|
|
497
|
+
if (dependencies.includes(issueNumber)) {
|
|
498
|
+
blocked.push(parseInt(issue, 10));
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
return blocked;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// CLI interface
|
|
507
|
+
if (require.main === module) {
|
|
508
|
+
const command = process.argv[2];
|
|
509
|
+
const issueNumber = parseInt(process.argv[3], 10);
|
|
510
|
+
const dependsOn = process.argv[4] ? parseInt(process.argv[4], 10) : null;
|
|
511
|
+
|
|
512
|
+
const tracker = new DependencyTracker();
|
|
513
|
+
|
|
514
|
+
(async () => {
|
|
515
|
+
switch (command) {
|
|
516
|
+
case 'add':
|
|
517
|
+
if (!dependsOn) {
|
|
518
|
+
console.error('Usage: dependency-tracker.js add <issue> <depends-on>');
|
|
519
|
+
process.exit(1);
|
|
520
|
+
}
|
|
521
|
+
const addResult = await tracker.addDependency(issueNumber, dependsOn);
|
|
522
|
+
console.log(JSON.stringify(addResult, null, 2));
|
|
523
|
+
break;
|
|
524
|
+
|
|
525
|
+
case 'remove':
|
|
526
|
+
if (!dependsOn) {
|
|
527
|
+
console.error('Usage: dependency-tracker.js remove <issue> <depends-on>');
|
|
528
|
+
process.exit(1);
|
|
529
|
+
}
|
|
530
|
+
const removeResult = await tracker.removeDependency(issueNumber, dependsOn);
|
|
531
|
+
console.log(JSON.stringify(removeResult, null, 2));
|
|
532
|
+
break;
|
|
533
|
+
|
|
534
|
+
case 'get':
|
|
535
|
+
const deps = await tracker.getDependencies(issueNumber);
|
|
536
|
+
console.log('Dependencies:', deps);
|
|
537
|
+
break;
|
|
538
|
+
|
|
539
|
+
case 'blocked':
|
|
540
|
+
const blocked = await tracker.getBlockedIssues(issueNumber);
|
|
541
|
+
console.log('Blocked issues:', blocked);
|
|
542
|
+
break;
|
|
543
|
+
|
|
544
|
+
default:
|
|
545
|
+
console.error('Usage: dependency-tracker.js <add|remove|get|blocked> <issue> [depends-on]');
|
|
546
|
+
process.exit(1);
|
|
547
|
+
}
|
|
548
|
+
})().catch(error => {
|
|
549
|
+
console.error('Error:', error.message);
|
|
550
|
+
process.exit(1);
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
module.exports = DependencyTracker;
|