claude-autopm 1.30.1 → 2.1.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/mcp/test-server.md +10 -0
- 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/bin/autopm-poc.js +348 -0
- package/bin/autopm.js +6 -0
- package/lib/ai-providers/AbstractAIProvider.js +524 -0
- package/lib/ai-providers/ClaudeProvider.js +423 -0
- package/lib/ai-providers/TemplateProvider.js +432 -0
- package/lib/cli/commands/agent.js +206 -0
- package/lib/cli/commands/config.js +488 -0
- package/lib/cli/commands/prd.js +345 -0
- package/lib/cli/commands/task.js +206 -0
- package/lib/config/ConfigManager.js +531 -0
- package/lib/errors/AIProviderError.js +164 -0
- package/lib/services/AgentService.js +557 -0
- package/lib/services/EpicService.js +609 -0
- package/lib/services/PRDService.js +1003 -0
- package/lib/services/TaskService.js +760 -0
- package/lib/services/interfaces.js +753 -0
- package/lib/utils/CircuitBreaker.js +165 -0
- package/lib/utils/Encryption.js +201 -0
- package/lib/utils/RateLimiter.js +241 -0
- package/lib/utils/ServiceFactory.js +165 -0
- package/package.json +9 -5
- package/scripts/config/get.js +108 -0
- package/scripts/config/init.js +100 -0
- package/scripts/config/list-providers.js +93 -0
- package/scripts/config/set-api-key.js +107 -0
- package/scripts/config/set-provider.js +201 -0
- package/scripts/config/set.js +139 -0
- package/scripts/config/show.js +181 -0
- package/autopm/.claude/.env +0 -158
- package/autopm/.claude/settings.local.json +0 -9
|
@@ -0,0 +1,545 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* GitHub Issue Dependency Validator
|
|
4
|
+
*
|
|
5
|
+
* Validates dependency relationships and checks if issues can be closed:
|
|
6
|
+
* - Checks if all dependencies are resolved
|
|
7
|
+
* - Detects circular dependencies
|
|
8
|
+
* - Validates issue closure readiness
|
|
9
|
+
* - Provides blocker status and progress tracking
|
|
10
|
+
*
|
|
11
|
+
* @module dependency-validator
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const fs = require('fs');
|
|
15
|
+
const path = require('path');
|
|
16
|
+
const DependencyTracker = require('./dependency-tracker.js');
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* DependencyValidator class for validating GitHub issue dependencies
|
|
20
|
+
*/
|
|
21
|
+
class DependencyValidator {
|
|
22
|
+
/**
|
|
23
|
+
* Initialize dependency validator
|
|
24
|
+
*
|
|
25
|
+
* @param {Object} config - Configuration options
|
|
26
|
+
* @param {string} config.owner - GitHub repository owner
|
|
27
|
+
* @param {string} config.repo - GitHub repository name
|
|
28
|
+
* @param {string} config.token - GitHub personal access token
|
|
29
|
+
* @param {boolean} config.localMode - Enable local mode
|
|
30
|
+
* @param {string} config.cacheDir - Directory for local cache
|
|
31
|
+
*/
|
|
32
|
+
constructor(config = {}) {
|
|
33
|
+
this.localMode = config.localMode || false;
|
|
34
|
+
this.cacheDir = config.cacheDir || path.join(process.cwd(), '.claude', 'cache');
|
|
35
|
+
this.cacheTimeout = config.cacheTimeout || 300000; // 5 minutes default
|
|
36
|
+
this.validationCache = new Map();
|
|
37
|
+
|
|
38
|
+
// Initialize dependency tracker
|
|
39
|
+
this.tracker = new DependencyTracker(config);
|
|
40
|
+
|
|
41
|
+
if (this.localMode) {
|
|
42
|
+
this._ensureCacheDir();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Ensure cache directory exists
|
|
48
|
+
* @private
|
|
49
|
+
*/
|
|
50
|
+
_ensureCacheDir() {
|
|
51
|
+
if (!fs.existsSync(this.cacheDir)) {
|
|
52
|
+
fs.mkdirSync(this.cacheDir, { recursive: true });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Get cached validation result
|
|
58
|
+
* @private
|
|
59
|
+
*/
|
|
60
|
+
_getCached(issueNumber) {
|
|
61
|
+
const cached = this.validationCache.get(issueNumber);
|
|
62
|
+
if (!cached) return null;
|
|
63
|
+
|
|
64
|
+
const now = Date.now();
|
|
65
|
+
if (now - cached.timestamp > this.cacheTimeout) {
|
|
66
|
+
this.validationCache.delete(issueNumber);
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return cached.data;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Set cached validation result
|
|
75
|
+
* @private
|
|
76
|
+
*/
|
|
77
|
+
_setCached(issueNumber, data) {
|
|
78
|
+
this.validationCache.set(issueNumber, {
|
|
79
|
+
data,
|
|
80
|
+
timestamp: Date.now()
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Validate all dependencies for an issue
|
|
86
|
+
*
|
|
87
|
+
* @param {number} issueNumber - Issue to validate
|
|
88
|
+
* @param {Object} options - Validation options
|
|
89
|
+
* @param {boolean} options.recursive - Check nested dependencies
|
|
90
|
+
* @param {boolean} options.useCache - Use cached results
|
|
91
|
+
* @returns {Promise<Object>} Validation result
|
|
92
|
+
*/
|
|
93
|
+
async validateDependencies(issueNumber, options = {}) {
|
|
94
|
+
try {
|
|
95
|
+
// Check cache
|
|
96
|
+
if (options.useCache !== false) {
|
|
97
|
+
const cached = this._getCached(issueNumber);
|
|
98
|
+
if (cached) return cached;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (this.localMode) {
|
|
102
|
+
return await this._validateLocal(issueNumber, options);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Get dependencies
|
|
106
|
+
const dependencies = await this.tracker.getDependencies(issueNumber);
|
|
107
|
+
|
|
108
|
+
if (dependencies.length === 0) {
|
|
109
|
+
const result = {
|
|
110
|
+
valid: true,
|
|
111
|
+
unresolvedDependencies: [],
|
|
112
|
+
message: 'No dependencies to validate'
|
|
113
|
+
};
|
|
114
|
+
this._setCached(issueNumber, result);
|
|
115
|
+
return result;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Check status of each dependency
|
|
119
|
+
const unresolvedDependencies = [];
|
|
120
|
+
|
|
121
|
+
for (const depNumber of dependencies) {
|
|
122
|
+
try {
|
|
123
|
+
const depIssue = await this.tracker.octokit.rest.issues.get({
|
|
124
|
+
owner: this.tracker.owner,
|
|
125
|
+
repo: this.tracker.repo,
|
|
126
|
+
issue_number: depNumber
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
if (depIssue.data.state !== 'closed') {
|
|
130
|
+
unresolvedDependencies.push({
|
|
131
|
+
number: depIssue.data.number,
|
|
132
|
+
title: depIssue.data.title,
|
|
133
|
+
state: depIssue.data.state,
|
|
134
|
+
url: depIssue.data.html_url
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Recursively check nested dependencies if requested
|
|
139
|
+
if (options.recursive && depIssue.data.state !== 'closed') {
|
|
140
|
+
const nestedValidation = await this.validateDependencies(depNumber, {
|
|
141
|
+
...options,
|
|
142
|
+
useCache: true
|
|
143
|
+
});
|
|
144
|
+
if (!nestedValidation.valid) {
|
|
145
|
+
unresolvedDependencies.push(...nestedValidation.unresolvedDependencies);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
} catch (error) {
|
|
149
|
+
if (error.status === 404) {
|
|
150
|
+
unresolvedDependencies.push({
|
|
151
|
+
number: depNumber,
|
|
152
|
+
error: 'Issue not found',
|
|
153
|
+
state: 'unknown'
|
|
154
|
+
});
|
|
155
|
+
} else {
|
|
156
|
+
throw error;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const result = {
|
|
162
|
+
valid: unresolvedDependencies.length === 0,
|
|
163
|
+
unresolvedDependencies,
|
|
164
|
+
message: unresolvedDependencies.length === 0
|
|
165
|
+
? 'All dependencies resolved'
|
|
166
|
+
: `${unresolvedDependencies.length} unresolved dependencies`
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
this._setCached(issueNumber, result);
|
|
170
|
+
return result;
|
|
171
|
+
|
|
172
|
+
} catch (error) {
|
|
173
|
+
return {
|
|
174
|
+
valid: false,
|
|
175
|
+
error: error.message,
|
|
176
|
+
unresolvedDependencies: []
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Validate dependencies in local mode
|
|
183
|
+
* @private
|
|
184
|
+
*/
|
|
185
|
+
async _validateLocal(issueNumber, options) {
|
|
186
|
+
const depsFile = path.join(this.cacheDir, 'dependencies.json');
|
|
187
|
+
const statesFile = path.join(this.cacheDir, 'issue-states.json');
|
|
188
|
+
|
|
189
|
+
if (!fs.existsSync(depsFile)) {
|
|
190
|
+
return {
|
|
191
|
+
valid: true,
|
|
192
|
+
unresolvedDependencies: [],
|
|
193
|
+
message: 'No dependencies found (local mode)'
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const dependencies = JSON.parse(fs.readFileSync(depsFile, 'utf8'));
|
|
198
|
+
const issueKey = issueNumber.toString();
|
|
199
|
+
const issueDeps = dependencies[issueKey] || [];
|
|
200
|
+
|
|
201
|
+
if (issueDeps.length === 0) {
|
|
202
|
+
return {
|
|
203
|
+
valid: true,
|
|
204
|
+
unresolvedDependencies: [],
|
|
205
|
+
message: 'No dependencies'
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Load issue states if available
|
|
210
|
+
let states = {};
|
|
211
|
+
if (fs.existsSync(statesFile)) {
|
|
212
|
+
states = JSON.parse(fs.readFileSync(statesFile, 'utf8'));
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const unresolvedDependencies = issueDeps
|
|
216
|
+
.filter(dep => states[dep.toString()] !== 'closed')
|
|
217
|
+
.map(dep => ({ number: dep, state: states[dep.toString()] || 'unknown' }));
|
|
218
|
+
|
|
219
|
+
return {
|
|
220
|
+
valid: unresolvedDependencies.length === 0,
|
|
221
|
+
unresolvedDependencies,
|
|
222
|
+
message: unresolvedDependencies.length === 0
|
|
223
|
+
? 'All dependencies resolved'
|
|
224
|
+
: `${unresolvedDependencies.length} unresolved dependencies`
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Detect circular dependencies in dependency chain
|
|
230
|
+
*
|
|
231
|
+
* @param {number} issueNumber - Starting issue
|
|
232
|
+
* @param {Object} options - Detection options
|
|
233
|
+
* @param {number} options.maxDepth - Maximum depth to search
|
|
234
|
+
* @returns {Promise<Object>} Detection result with cycles found
|
|
235
|
+
*/
|
|
236
|
+
async detectCircularDependencies(issueNumber, options = {}) {
|
|
237
|
+
const maxDepth = options.maxDepth || 50;
|
|
238
|
+
const visited = new Set();
|
|
239
|
+
const path = [];
|
|
240
|
+
const cycles = [];
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* DFS to detect cycles
|
|
244
|
+
*/
|
|
245
|
+
const detectCycle = async (current, depth = 0) => {
|
|
246
|
+
if (depth > maxDepth) {
|
|
247
|
+
return; // Prevent infinite loops
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (path.includes(current)) {
|
|
251
|
+
// Found a cycle
|
|
252
|
+
const cycleStart = path.indexOf(current);
|
|
253
|
+
const cycle = [...path.slice(cycleStart), current];
|
|
254
|
+
cycles.push(cycle);
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (visited.has(current)) {
|
|
259
|
+
return; // Already explored this path
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
visited.add(current);
|
|
263
|
+
path.push(current);
|
|
264
|
+
|
|
265
|
+
try {
|
|
266
|
+
const dependencies = await this.tracker.getDependencies(current);
|
|
267
|
+
|
|
268
|
+
for (const dep of dependencies) {
|
|
269
|
+
await detectCycle(dep, depth + 1);
|
|
270
|
+
}
|
|
271
|
+
} catch (error) {
|
|
272
|
+
// Silently ignore errors in cycle detection
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
path.pop();
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
await detectCycle(issueNumber);
|
|
279
|
+
|
|
280
|
+
return {
|
|
281
|
+
hasCircular: cycles.length > 0,
|
|
282
|
+
cycles,
|
|
283
|
+
message: cycles.length > 0
|
|
284
|
+
? `Found ${cycles.length} circular dependency cycle(s)`
|
|
285
|
+
: 'No circular dependencies detected'
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Check if an issue can be closed
|
|
291
|
+
*
|
|
292
|
+
* @param {number} issueNumber - Issue to check
|
|
293
|
+
* @returns {Promise<Object>} Result indicating if issue can be closed
|
|
294
|
+
*/
|
|
295
|
+
async canClose(issueNumber) {
|
|
296
|
+
try {
|
|
297
|
+
// Check if dependencies are resolved
|
|
298
|
+
const validation = await this.validateDependencies(issueNumber);
|
|
299
|
+
|
|
300
|
+
if (!validation.valid) {
|
|
301
|
+
return {
|
|
302
|
+
canClose: false,
|
|
303
|
+
reason: `Issue #${issueNumber} has unresolved dependencies`,
|
|
304
|
+
blockingIssues: validation.unresolvedDependencies
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Check if closing would block other issues
|
|
309
|
+
const blockedIssues = await this.tracker.getBlockedIssues(issueNumber);
|
|
310
|
+
|
|
311
|
+
if (blockedIssues.length > 0) {
|
|
312
|
+
// Get details of blocked issues
|
|
313
|
+
const openBlockedIssues = [];
|
|
314
|
+
|
|
315
|
+
for (const blockedNum of blockedIssues) {
|
|
316
|
+
try {
|
|
317
|
+
const blockedIssue = await this.tracker.octokit.rest.issues.get({
|
|
318
|
+
owner: this.tracker.owner,
|
|
319
|
+
repo: this.tracker.repo,
|
|
320
|
+
issue_number: blockedNum
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
if (blockedIssue.data.state === 'open') {
|
|
324
|
+
openBlockedIssues.push({
|
|
325
|
+
number: blockedIssue.data.number,
|
|
326
|
+
title: blockedIssue.data.title,
|
|
327
|
+
url: blockedIssue.data.html_url
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
} catch {
|
|
331
|
+
// Ignore errors for individual issues
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const result = {
|
|
336
|
+
canClose: true,
|
|
337
|
+
reason: 'All dependencies resolved',
|
|
338
|
+
affectedIssues: openBlockedIssues
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
if (openBlockedIssues.length > 0) {
|
|
342
|
+
result.warning = `Closing this issue will block ${openBlockedIssues.length} open issue(s)`;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
return result;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Check for circular dependencies
|
|
349
|
+
const circularCheck = await this.detectCircularDependencies(issueNumber);
|
|
350
|
+
const result = {
|
|
351
|
+
canClose: true,
|
|
352
|
+
reason: 'No dependencies blocking closure'
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
if (circularCheck.hasCircular) {
|
|
356
|
+
result.warning = 'Issue is part of a circular dependency chain';
|
|
357
|
+
result.cycles = circularCheck.cycles;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
return result;
|
|
361
|
+
|
|
362
|
+
} catch (error) {
|
|
363
|
+
return {
|
|
364
|
+
canClose: false,
|
|
365
|
+
reason: 'Error checking closure status',
|
|
366
|
+
error: error.message
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Get status of all blocking issues (dependencies)
|
|
373
|
+
*
|
|
374
|
+
* @param {number} issueNumber - Issue to check blockers for
|
|
375
|
+
* @returns {Promise<Object>} Status of all blockers with progress
|
|
376
|
+
*/
|
|
377
|
+
async getBlockerStatus(issueNumber) {
|
|
378
|
+
try {
|
|
379
|
+
const dependencies = await this.tracker.getDependencies(issueNumber);
|
|
380
|
+
|
|
381
|
+
if (dependencies.length === 0) {
|
|
382
|
+
return {
|
|
383
|
+
total: 0,
|
|
384
|
+
resolved: 0,
|
|
385
|
+
unresolved: 0,
|
|
386
|
+
progress: 100,
|
|
387
|
+
blockers: []
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const blockers = [];
|
|
392
|
+
let resolved = 0;
|
|
393
|
+
|
|
394
|
+
for (const depNumber of dependencies) {
|
|
395
|
+
try {
|
|
396
|
+
const depIssue = await this.tracker.octokit.rest.issues.get({
|
|
397
|
+
owner: this.tracker.owner,
|
|
398
|
+
repo: this.tracker.repo,
|
|
399
|
+
issue_number: depNumber
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
const blocker = {
|
|
403
|
+
number: depIssue.data.number,
|
|
404
|
+
title: depIssue.data.title,
|
|
405
|
+
state: depIssue.data.state,
|
|
406
|
+
url: depIssue.data.html_url,
|
|
407
|
+
assignees: depIssue.data.assignees,
|
|
408
|
+
labels: depIssue.data.labels
|
|
409
|
+
};
|
|
410
|
+
|
|
411
|
+
if (depIssue.data.state === 'closed') {
|
|
412
|
+
resolved++;
|
|
413
|
+
blocker.resolved = true;
|
|
414
|
+
} else {
|
|
415
|
+
blocker.resolved = false;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
blockers.push(blocker);
|
|
419
|
+
} catch (error) {
|
|
420
|
+
blockers.push({
|
|
421
|
+
number: depNumber,
|
|
422
|
+
error: 'Failed to fetch issue details',
|
|
423
|
+
resolved: false
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const total = dependencies.length;
|
|
429
|
+
const progress = (resolved / total) * 100;
|
|
430
|
+
|
|
431
|
+
return {
|
|
432
|
+
total,
|
|
433
|
+
resolved,
|
|
434
|
+
unresolved: total - resolved,
|
|
435
|
+
progress: Math.round(progress * 100) / 100,
|
|
436
|
+
blockers
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
} catch (error) {
|
|
440
|
+
return {
|
|
441
|
+
total: 0,
|
|
442
|
+
resolved: 0,
|
|
443
|
+
unresolved: 0,
|
|
444
|
+
progress: 0,
|
|
445
|
+
error: error.message,
|
|
446
|
+
blockers: []
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// CLI interface
|
|
453
|
+
if (require.main === module) {
|
|
454
|
+
const command = process.argv[2];
|
|
455
|
+
const issueNumber = parseInt(process.argv[3], 10);
|
|
456
|
+
|
|
457
|
+
const validator = new DependencyValidator();
|
|
458
|
+
|
|
459
|
+
(async () => {
|
|
460
|
+
switch (command) {
|
|
461
|
+
case 'validate':
|
|
462
|
+
const validation = await validator.validateDependencies(issueNumber);
|
|
463
|
+
console.log('\nDependency Validation:');
|
|
464
|
+
console.log('─'.repeat(50));
|
|
465
|
+
console.log(`Valid: ${validation.valid ? '✓' : '✗'}`);
|
|
466
|
+
console.log(`Message: ${validation.message}`);
|
|
467
|
+
|
|
468
|
+
if (validation.unresolvedDependencies.length > 0) {
|
|
469
|
+
console.log('\nUnresolved Dependencies:');
|
|
470
|
+
validation.unresolvedDependencies.forEach(dep => {
|
|
471
|
+
console.log(` • #${dep.number}: ${dep.title || 'Unknown'} (${dep.state})`);
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
break;
|
|
475
|
+
|
|
476
|
+
case 'circular':
|
|
477
|
+
const circular = await validator.detectCircularDependencies(issueNumber);
|
|
478
|
+
console.log('\nCircular Dependency Check:');
|
|
479
|
+
console.log('─'.repeat(50));
|
|
480
|
+
console.log(`Has Circular: ${circular.hasCircular ? 'YES' : 'NO'}`);
|
|
481
|
+
|
|
482
|
+
if (circular.hasCircular) {
|
|
483
|
+
console.log('\nCycles Found:');
|
|
484
|
+
circular.cycles.forEach((cycle, i) => {
|
|
485
|
+
console.log(` ${i + 1}. ${cycle.join(' → ')}`);
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
break;
|
|
489
|
+
|
|
490
|
+
case 'can-close':
|
|
491
|
+
const canClose = await validator.canClose(issueNumber);
|
|
492
|
+
console.log('\nClosure Check:');
|
|
493
|
+
console.log('─'.repeat(50));
|
|
494
|
+
console.log(`Can Close: ${canClose.canClose ? '✓ YES' : '✗ NO'}`);
|
|
495
|
+
console.log(`Reason: ${canClose.reason}`);
|
|
496
|
+
|
|
497
|
+
if (canClose.warning) {
|
|
498
|
+
console.log(`\n⚠ Warning: ${canClose.warning}`);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
if (canClose.blockingIssues && canClose.blockingIssues.length > 0) {
|
|
502
|
+
console.log('\nBlocking Issues:');
|
|
503
|
+
canClose.blockingIssues.forEach(issue => {
|
|
504
|
+
console.log(` • #${issue.number}: ${issue.title}`);
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
if (canClose.affectedIssues && canClose.affectedIssues.length > 0) {
|
|
509
|
+
console.log('\nAffected Issues:');
|
|
510
|
+
canClose.affectedIssues.forEach(issue => {
|
|
511
|
+
console.log(` • #${issue.number}: ${issue.title}`);
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
break;
|
|
515
|
+
|
|
516
|
+
case 'status':
|
|
517
|
+
case 'blockers':
|
|
518
|
+
const status = await validator.getBlockerStatus(issueNumber);
|
|
519
|
+
console.log('\nBlocker Status:');
|
|
520
|
+
console.log('─'.repeat(50));
|
|
521
|
+
console.log(`Total Dependencies: ${status.total}`);
|
|
522
|
+
console.log(`Resolved: ${status.resolved}`);
|
|
523
|
+
console.log(`Unresolved: ${status.unresolved}`);
|
|
524
|
+
console.log(`Progress: ${status.progress}%`);
|
|
525
|
+
|
|
526
|
+
if (status.blockers.length > 0) {
|
|
527
|
+
console.log('\nBlockers:');
|
|
528
|
+
status.blockers.forEach(blocker => {
|
|
529
|
+
const icon = blocker.resolved ? '✓' : '○';
|
|
530
|
+
console.log(` ${icon} #${blocker.number}: ${blocker.title || 'Unknown'} (${blocker.state})`);
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
break;
|
|
534
|
+
|
|
535
|
+
default:
|
|
536
|
+
console.error('Usage: dependency-validator.js <validate|circular|can-close|status> <issue>');
|
|
537
|
+
process.exit(1);
|
|
538
|
+
}
|
|
539
|
+
})().catch(error => {
|
|
540
|
+
console.error('Error:', error.message);
|
|
541
|
+
process.exit(1);
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
module.exports = DependencyValidator;
|