claude-autopm 1.25.0 → 1.27.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/README.md +111 -0
- package/autopm/.claude/agents/frameworks/e2e-test-engineer.md +1 -18
- package/autopm/.claude/agents/frameworks/nats-messaging-expert.md +1 -18
- package/autopm/.claude/agents/frameworks/react-frontend-engineer.md +1 -18
- package/autopm/.claude/agents/frameworks/react-ui-expert.md +1 -18
- package/autopm/.claude/agents/frameworks/tailwindcss-expert.md +1 -18
- package/autopm/.claude/agents/frameworks/ux-design-expert.md +1 -18
- package/autopm/.claude/agents/languages/bash-scripting-expert.md +1 -18
- package/autopm/.claude/agents/languages/javascript-frontend-engineer.md +1 -18
- package/autopm/.claude/agents/languages/nodejs-backend-engineer.md +1 -18
- package/autopm/.claude/agents/languages/python-backend-engineer.md +1 -18
- package/autopm/.claude/agents/languages/python-backend-expert.md +1 -18
- package/autopm/.claude/commands/pm/epic-decompose.md +19 -5
- package/autopm/.claude/commands/pm/prd-new.md +14 -1
- package/autopm/.claude/includes/task-creation-excellence.md +18 -0
- package/autopm/.claude/lib/ai-task-generator.js +84 -0
- package/autopm/.claude/lib/cli-parser.js +148 -0
- package/autopm/.claude/lib/commands/pm/epicStatus.js +263 -0
- package/autopm/.claude/lib/dependency-analyzer.js +157 -0
- package/autopm/.claude/lib/frontmatter.js +224 -0
- package/autopm/.claude/lib/task-utils.js +64 -0
- package/autopm/.claude/scripts/pm-epic-decompose-local.js +158 -0
- package/autopm/.claude/scripts/pm-epic-list-local.js +103 -0
- package/autopm/.claude/scripts/pm-epic-show-local.js +70 -0
- package/autopm/.claude/scripts/pm-epic-update-local.js +56 -0
- package/autopm/.claude/scripts/pm-prd-list-local.js +111 -0
- package/autopm/.claude/scripts/pm-prd-new-local.js +196 -0
- package/autopm/.claude/scripts/pm-prd-parse-local.js +360 -0
- package/autopm/.claude/scripts/pm-prd-show-local.js +101 -0
- package/autopm/.claude/scripts/pm-prd-update-local.js +153 -0
- package/autopm/.claude/scripts/pm-sync-download-local.js +424 -0
- package/autopm/.claude/scripts/pm-sync-upload-local.js +473 -0
- package/autopm/.claude/scripts/pm-task-list-local.js +86 -0
- package/autopm/.claude/scripts/pm-task-show-local.js +92 -0
- package/autopm/.claude/scripts/pm-task-update-local.js +109 -0
- package/autopm/.claude/scripts/setup-local-mode.js +127 -0
- package/package.json +5 -3
- package/scripts/create-task-issues.sh +26 -0
- package/scripts/fix-invalid-command-refs.sh +4 -3
- package/scripts/fix-invalid-refs-simple.sh +8 -3
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI Parser for PM Commands
|
|
3
|
+
*
|
|
4
|
+
* Provides unified command-line argument parsing with --local flag support
|
|
5
|
+
* across all PM commands using yargs best practices.
|
|
6
|
+
*
|
|
7
|
+
* Following Context7 yargs documentation patterns:
|
|
8
|
+
* - .boolean() for boolean flags
|
|
9
|
+
* - .alias() for short flags
|
|
10
|
+
* - .parse() for argument processing
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const yargs = require('yargs/yargs');
|
|
14
|
+
const { hideBin } = require('yargs/helpers');
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const path = require('path');
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Read project configuration to determine default provider
|
|
20
|
+
* @returns {string} Default provider ('github', 'azure', or 'local')
|
|
21
|
+
*/
|
|
22
|
+
function getDefaultProvider() {
|
|
23
|
+
try {
|
|
24
|
+
const configPath = path.join(process.cwd(), '.claude/config.json');
|
|
25
|
+
if (fs.existsSync(configPath)) {
|
|
26
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
27
|
+
return config.provider || 'github';
|
|
28
|
+
}
|
|
29
|
+
} catch (error) {
|
|
30
|
+
// Silently fall back to github if config cannot be read
|
|
31
|
+
}
|
|
32
|
+
return 'github';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Parse PM command arguments with --local flag support
|
|
37
|
+
*
|
|
38
|
+
* @param {string[]} args - Command arguments (usually process.argv)
|
|
39
|
+
* @returns {object} Parsed arguments with mode determined
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* // Local mode
|
|
43
|
+
* parsePMCommand(['prd-new', 'feature', '--local'])
|
|
44
|
+
* // => { _: ['prd-new', 'feature'], local: true, mode: 'local' }
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* // Remote mode (from config)
|
|
48
|
+
* parsePMCommand(['prd-list'])
|
|
49
|
+
* // => { _: ['prd-list'], local: false, mode: 'github' }
|
|
50
|
+
*/
|
|
51
|
+
function parsePMCommand(args) {
|
|
52
|
+
let failureMessage = null;
|
|
53
|
+
|
|
54
|
+
// Don't use hideBin if args are already clean (for testing)
|
|
55
|
+
const cleanArgs = Array.isArray(args) && !args[0]?.includes('node') ? args : hideBin(args);
|
|
56
|
+
|
|
57
|
+
const argv = yargs(cleanArgs)
|
|
58
|
+
.option('local', {
|
|
59
|
+
alias: 'l',
|
|
60
|
+
type: 'boolean',
|
|
61
|
+
describe: 'Use local mode (offline, no GitHub/Azure)',
|
|
62
|
+
default: false
|
|
63
|
+
})
|
|
64
|
+
.option('github', {
|
|
65
|
+
type: 'boolean',
|
|
66
|
+
describe: 'Use GitHub provider',
|
|
67
|
+
default: false
|
|
68
|
+
})
|
|
69
|
+
.option('azure', {
|
|
70
|
+
type: 'boolean',
|
|
71
|
+
describe: 'Use Azure DevOps provider',
|
|
72
|
+
default: false
|
|
73
|
+
})
|
|
74
|
+
.option('verbose', {
|
|
75
|
+
alias: 'v',
|
|
76
|
+
type: 'boolean',
|
|
77
|
+
describe: 'Enable verbose output',
|
|
78
|
+
default: false
|
|
79
|
+
})
|
|
80
|
+
.option('force', {
|
|
81
|
+
alias: 'f',
|
|
82
|
+
type: 'boolean',
|
|
83
|
+
describe: 'Force operation',
|
|
84
|
+
default: false
|
|
85
|
+
})
|
|
86
|
+
.option('output', {
|
|
87
|
+
alias: 'o',
|
|
88
|
+
type: 'string',
|
|
89
|
+
describe: 'Output format (json, text)',
|
|
90
|
+
choices: ['json', 'text']
|
|
91
|
+
})
|
|
92
|
+
.check((argv) => {
|
|
93
|
+
// Validate: cannot use --local with --github or --azure
|
|
94
|
+
if (argv.local && (argv.github || argv.azure)) {
|
|
95
|
+
throw new Error('Cannot use both --local and remote provider (--github or --azure)');
|
|
96
|
+
}
|
|
97
|
+
return true;
|
|
98
|
+
})
|
|
99
|
+
.fail((msg, err) => {
|
|
100
|
+
// Capture failure for throwing
|
|
101
|
+
failureMessage = msg || (err && err.message) || 'Unknown error';
|
|
102
|
+
})
|
|
103
|
+
.exitProcess(false) // Don't call process.exit on error (for testing)
|
|
104
|
+
.parse();
|
|
105
|
+
|
|
106
|
+
// Throw error if validation failed
|
|
107
|
+
if (failureMessage) {
|
|
108
|
+
throw new Error(failureMessage);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Determine mode based on flags and config
|
|
112
|
+
if (argv.local) {
|
|
113
|
+
argv.mode = 'local';
|
|
114
|
+
} else if (argv.github) {
|
|
115
|
+
argv.mode = 'github';
|
|
116
|
+
} else if (argv.azure) {
|
|
117
|
+
argv.mode = 'azure';
|
|
118
|
+
} else {
|
|
119
|
+
// Read from config
|
|
120
|
+
argv.mode = getDefaultProvider();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return argv;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Get help text for CLI parser
|
|
128
|
+
* Used for testing help text includes --local flag
|
|
129
|
+
*
|
|
130
|
+
* @returns {Promise<string>} Help text
|
|
131
|
+
*/
|
|
132
|
+
async function getHelpText() {
|
|
133
|
+
const parser = yargs([])
|
|
134
|
+
.option('local', {
|
|
135
|
+
alias: 'l',
|
|
136
|
+
type: 'boolean',
|
|
137
|
+
describe: 'Use local mode (offline, no GitHub/Azure)',
|
|
138
|
+
default: false
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
return await parser.getHelp();
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
module.exports = {
|
|
145
|
+
parsePMCommand,
|
|
146
|
+
getHelpText,
|
|
147
|
+
getDefaultProvider
|
|
148
|
+
};
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Epic Status - Complete epic progress tracking
|
|
4
|
+
*
|
|
5
|
+
* Replaces epic-status.sh with clean, testable JavaScript
|
|
6
|
+
* - Counts tasks by status (completed/in-progress/pending)
|
|
7
|
+
* - Calculates progress percentage
|
|
8
|
+
* - Shows progress bar visualization
|
|
9
|
+
* - Provides sub-epic breakdown
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Parse frontmatter from markdown file
|
|
17
|
+
*/
|
|
18
|
+
function parseFrontmatter(filePath) {
|
|
19
|
+
try {
|
|
20
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
21
|
+
const lines = content.split('\n');
|
|
22
|
+
|
|
23
|
+
let inFrontmatter = false;
|
|
24
|
+
let frontmatterCount = 0;
|
|
25
|
+
const frontmatter = {};
|
|
26
|
+
|
|
27
|
+
for (const line of lines) {
|
|
28
|
+
if (line === '---') {
|
|
29
|
+
frontmatterCount++;
|
|
30
|
+
if (frontmatterCount === 1) {
|
|
31
|
+
inFrontmatter = true;
|
|
32
|
+
continue;
|
|
33
|
+
} else if (frontmatterCount === 2) {
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (inFrontmatter) {
|
|
39
|
+
const match = line.match(/^(\w+):\s*(.+)$/);
|
|
40
|
+
if (match) {
|
|
41
|
+
const [, key, value] = match;
|
|
42
|
+
frontmatter[key] = value;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return frontmatter;
|
|
48
|
+
} catch (error) {
|
|
49
|
+
return {};
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Find all task files in directory
|
|
55
|
+
*/
|
|
56
|
+
function findTaskFiles(dir, maxDepth = 2, currentDepth = 0) {
|
|
57
|
+
if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) {
|
|
58
|
+
return [];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (currentDepth >= maxDepth) {
|
|
62
|
+
return [];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const files = [];
|
|
66
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
67
|
+
|
|
68
|
+
for (const entry of entries) {
|
|
69
|
+
const fullPath = path.join(dir, entry.name);
|
|
70
|
+
|
|
71
|
+
if (entry.isFile() && /^\d+\.md$/.test(entry.name)) {
|
|
72
|
+
files.push(fullPath);
|
|
73
|
+
} else if (entry.isDirectory() && currentDepth < maxDepth - 1) {
|
|
74
|
+
files.push(...findTaskFiles(fullPath, maxDepth, currentDepth + 1));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return files;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Count tasks by status
|
|
83
|
+
*/
|
|
84
|
+
function countTasksByStatus(taskFiles) {
|
|
85
|
+
const counts = {
|
|
86
|
+
completed: 0,
|
|
87
|
+
in_progress: 0,
|
|
88
|
+
pending: 0,
|
|
89
|
+
total: taskFiles.length
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
for (const taskFile of taskFiles) {
|
|
93
|
+
const frontmatter = parseFrontmatter(taskFile);
|
|
94
|
+
const status = frontmatter.status || '';
|
|
95
|
+
|
|
96
|
+
if (status === 'completed') {
|
|
97
|
+
counts.completed++;
|
|
98
|
+
} else if (status === 'in-progress' || status === 'in_progress') {
|
|
99
|
+
counts.in_progress++;
|
|
100
|
+
} else {
|
|
101
|
+
counts.pending++;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return counts;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Generate progress bar
|
|
110
|
+
*/
|
|
111
|
+
function generateProgressBar(percentage, length = 50) {
|
|
112
|
+
const filled = Math.round((percentage * length) / 100);
|
|
113
|
+
const empty = length - filled;
|
|
114
|
+
|
|
115
|
+
const bar = '='.repeat(filled) + '-'.repeat(empty);
|
|
116
|
+
return `[${bar}] ${percentage}%`;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Get sub-epic breakdown
|
|
121
|
+
*/
|
|
122
|
+
function getSubEpicBreakdown(epicDir) {
|
|
123
|
+
const breakdown = [];
|
|
124
|
+
|
|
125
|
+
if (!fs.existsSync(epicDir)) {
|
|
126
|
+
return breakdown;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const entries = fs.readdirSync(epicDir, { withFileTypes: true });
|
|
130
|
+
|
|
131
|
+
for (const entry of entries) {
|
|
132
|
+
if (entry.isDirectory()) {
|
|
133
|
+
const subDir = path.join(epicDir, entry.name);
|
|
134
|
+
const taskFiles = findTaskFiles(subDir, 1);
|
|
135
|
+
|
|
136
|
+
if (taskFiles.length > 0) {
|
|
137
|
+
const counts = countTasksByStatus(taskFiles);
|
|
138
|
+
breakdown.push({
|
|
139
|
+
name: entry.name,
|
|
140
|
+
total: counts.total,
|
|
141
|
+
completed: counts.completed
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return breakdown;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Format epic status report
|
|
152
|
+
*/
|
|
153
|
+
function formatEpicStatus(epicName, epicDir) {
|
|
154
|
+
// Find all tasks
|
|
155
|
+
const taskFiles = findTaskFiles(epicDir);
|
|
156
|
+
const counts = countTasksByStatus(taskFiles);
|
|
157
|
+
|
|
158
|
+
// Calculate progress
|
|
159
|
+
const progress = counts.total > 0
|
|
160
|
+
? Math.round((counts.completed * 100) / counts.total)
|
|
161
|
+
: 0;
|
|
162
|
+
|
|
163
|
+
// Build report
|
|
164
|
+
const lines = [];
|
|
165
|
+
lines.push(`Epic: ${epicName}`);
|
|
166
|
+
lines.push('='.repeat(20 + epicName.length));
|
|
167
|
+
lines.push('');
|
|
168
|
+
lines.push(`Total tasks: ${counts.total}`);
|
|
169
|
+
lines.push(`Completed: ${counts.completed} (${progress}%)`);
|
|
170
|
+
lines.push(`In Progress: ${counts.in_progress}`);
|
|
171
|
+
lines.push(`Pending: ${counts.pending}`);
|
|
172
|
+
lines.push('');
|
|
173
|
+
|
|
174
|
+
// Progress bar
|
|
175
|
+
if (counts.total > 0) {
|
|
176
|
+
lines.push(`Progress: ${generateProgressBar(progress)}`);
|
|
177
|
+
lines.push('');
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Sub-epic breakdown
|
|
181
|
+
const breakdown = getSubEpicBreakdown(epicDir);
|
|
182
|
+
if (breakdown.length > 0) {
|
|
183
|
+
lines.push('Sub-Epic Breakdown:');
|
|
184
|
+
lines.push('-'.repeat(19));
|
|
185
|
+
|
|
186
|
+
for (const sub of breakdown) {
|
|
187
|
+
const name = sub.name.padEnd(30);
|
|
188
|
+
lines.push(` ${name} ${sub.total.toString().padStart(3)} tasks (${sub.completed} completed)`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return lines.join('\n');
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* List available epics
|
|
197
|
+
*/
|
|
198
|
+
function listAvailableEpics(epicsDir) {
|
|
199
|
+
if (!fs.existsSync(epicsDir)) {
|
|
200
|
+
return [];
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const entries = fs.readdirSync(epicsDir, { withFileTypes: true });
|
|
204
|
+
return entries
|
|
205
|
+
.filter(entry => entry.isDirectory())
|
|
206
|
+
.map(entry => entry.name);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Main function
|
|
211
|
+
*/
|
|
212
|
+
function main() {
|
|
213
|
+
const args = process.argv.slice(2);
|
|
214
|
+
const epicName = args[0];
|
|
215
|
+
|
|
216
|
+
const epicsDir = path.join(process.cwd(), '.claude/epics');
|
|
217
|
+
|
|
218
|
+
if (!epicName) {
|
|
219
|
+
console.log('Usage: epicStatus.js <epic-name>');
|
|
220
|
+
console.log('');
|
|
221
|
+
console.log('Available epics:');
|
|
222
|
+
|
|
223
|
+
const epics = listAvailableEpics(epicsDir);
|
|
224
|
+
if (epics.length > 0) {
|
|
225
|
+
epics.forEach(epic => console.log(` ${epic}`));
|
|
226
|
+
} else {
|
|
227
|
+
console.log(' No epics found');
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
process.exit(1);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const epicDir = path.join(epicsDir, epicName);
|
|
234
|
+
|
|
235
|
+
if (!fs.existsSync(epicDir)) {
|
|
236
|
+
console.error(`Error: Epic '${epicName}' not found`);
|
|
237
|
+
console.log('');
|
|
238
|
+
console.log('Available epics:');
|
|
239
|
+
|
|
240
|
+
const epics = listAvailableEpics(epicsDir);
|
|
241
|
+
epics.forEach(epic => console.log(` ${epic}`));
|
|
242
|
+
|
|
243
|
+
process.exit(1);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Generate and display status
|
|
247
|
+
const status = formatEpicStatus(epicName, epicDir);
|
|
248
|
+
console.log(status);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (require.main === module) {
|
|
252
|
+
main();
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
module.exports = {
|
|
256
|
+
parseFrontmatter,
|
|
257
|
+
findTaskFiles,
|
|
258
|
+
countTasksByStatus,
|
|
259
|
+
generateProgressBar,
|
|
260
|
+
getSubEpicBreakdown,
|
|
261
|
+
formatEpicStatus,
|
|
262
|
+
listAvailableEpics
|
|
263
|
+
};
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dependency Analyzer
|
|
3
|
+
*
|
|
4
|
+
* Analyzes task dependencies and validates dependency graphs.
|
|
5
|
+
* Detects circular dependencies and builds execution order.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* const { analyzeDependencies } = require('./dependency-analyzer');
|
|
9
|
+
*
|
|
10
|
+
* const result = analyzeDependencies(tasks);
|
|
11
|
+
* if (result.hasCircularDependencies) {
|
|
12
|
+
* console.error('Circular dependencies found:', result.cycles);
|
|
13
|
+
* }
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const { generateShortTaskId } = require('./task-utils');
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Analyze task dependencies
|
|
20
|
+
*
|
|
21
|
+
* @param {Array} tasks - Array of task objects with dependencies
|
|
22
|
+
* @returns {Object} Analysis result with cycles, order, and validation
|
|
23
|
+
*/
|
|
24
|
+
function analyzeDependencies(tasks) {
|
|
25
|
+
const graph = buildDependencyGraph(tasks);
|
|
26
|
+
const cycles = detectCircularDependencies(graph);
|
|
27
|
+
const order = cycles.length === 0 ? topologicalSort(graph) : [];
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
hasCircularDependencies: cycles.length > 0,
|
|
31
|
+
cycles,
|
|
32
|
+
executionOrder: order,
|
|
33
|
+
isValid: cycles.length === 0
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Build dependency graph from tasks
|
|
39
|
+
*
|
|
40
|
+
* @param {Array} tasks - Array of task objects
|
|
41
|
+
* @returns {Map} Dependency graph (task -> dependencies)
|
|
42
|
+
*/
|
|
43
|
+
function buildDependencyGraph(tasks) {
|
|
44
|
+
const graph = new Map();
|
|
45
|
+
|
|
46
|
+
tasks.forEach((task, index) => {
|
|
47
|
+
const taskId = generateShortTaskId(index + 1);
|
|
48
|
+
const dependencies = task.dependencies || [];
|
|
49
|
+
|
|
50
|
+
graph.set(taskId, dependencies);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
return graph;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Detect circular dependencies using DFS
|
|
58
|
+
*
|
|
59
|
+
* @param {Map} graph - Dependency graph
|
|
60
|
+
* @returns {Array} Array of circular dependency cycles
|
|
61
|
+
*/
|
|
62
|
+
function detectCircularDependencies(graph) {
|
|
63
|
+
const visited = new Set();
|
|
64
|
+
const recursionStack = new Set();
|
|
65
|
+
const cycles = [];
|
|
66
|
+
|
|
67
|
+
function dfs(node, path = []) {
|
|
68
|
+
if (recursionStack.has(node)) {
|
|
69
|
+
// Found a cycle
|
|
70
|
+
const cycleStart = path.indexOf(node);
|
|
71
|
+
cycles.push(path.slice(cycleStart));
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (visited.has(node)) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
visited.add(node);
|
|
80
|
+
recursionStack.add(node);
|
|
81
|
+
path.push(node);
|
|
82
|
+
|
|
83
|
+
const dependencies = graph.get(node) || [];
|
|
84
|
+
for (const dep of dependencies) {
|
|
85
|
+
dfs(dep, path);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
path.pop(); // Cleanup: remove node from path after exploring
|
|
89
|
+
recursionStack.delete(node);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
for (const node of graph.keys()) {
|
|
93
|
+
if (!visited.has(node)) {
|
|
94
|
+
dfs(node);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return cycles;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Topological sort for task execution order
|
|
103
|
+
*
|
|
104
|
+
* @param {Map} graph - Dependency graph
|
|
105
|
+
* @returns {Array} Ordered array of task IDs
|
|
106
|
+
*/
|
|
107
|
+
function topologicalSort(graph) {
|
|
108
|
+
const inDegree = new Map();
|
|
109
|
+
const order = [];
|
|
110
|
+
|
|
111
|
+
// Initialize in-degree for all nodes
|
|
112
|
+
for (const node of graph.keys()) {
|
|
113
|
+
inDegree.set(node, 0);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Calculate in-degree
|
|
117
|
+
for (const deps of graph.values()) {
|
|
118
|
+
for (const dep of deps) {
|
|
119
|
+
if (graph.has(dep)) {
|
|
120
|
+
inDegree.set(dep, (inDegree.get(dep) || 0) + 1);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Queue nodes with in-degree 0
|
|
126
|
+
const queue = [];
|
|
127
|
+
for (const [node, degree] of inDegree) {
|
|
128
|
+
if (degree === 0) {
|
|
129
|
+
queue.push(node);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Process queue
|
|
134
|
+
while (queue.length > 0) {
|
|
135
|
+
const node = queue.shift();
|
|
136
|
+
order.push(node);
|
|
137
|
+
|
|
138
|
+
const dependencies = graph.get(node) || [];
|
|
139
|
+
for (const dep of dependencies) {
|
|
140
|
+
const newDegree = inDegree.get(dep) - 1;
|
|
141
|
+
inDegree.set(dep, newDegree);
|
|
142
|
+
|
|
143
|
+
if (newDegree === 0) {
|
|
144
|
+
queue.push(dep);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return order;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
module.exports = {
|
|
153
|
+
analyzeDependencies,
|
|
154
|
+
buildDependencyGraph,
|
|
155
|
+
detectCircularDependencies,
|
|
156
|
+
topologicalSort
|
|
157
|
+
};
|