agileflow 2.55.0 → 2.56.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 +3 -5
- package/package.json +1 -1
- package/scripts/generators/agent-registry.js +43 -55
- package/scripts/generators/command-registry.js +42 -26
- package/scripts/generators/skill-registry.js +55 -28
- package/scripts/lib/frontmatter-parser.js +82 -0
- package/tools/cli/installers/ide/_base-ide.js +55 -0
- package/tools/cli/installers/ide/claude-code.js +0 -54
- package/tools/cli/installers/ide/cursor.js +21 -51
- package/tools/cli/installers/ide/windsurf.js +20 -50
- package/tools/cli/lib/content-injector.js +25 -48
- package/tools/cli/lib/npm-utils.js +36 -9
- package/tools/cli/lib/ui.js +24 -3
package/README.md
CHANGED
|
@@ -4,9 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/agileflow)
|
|
6
6
|
[](docs/04-architecture/commands.md)
|
|
7
|
-
[](docs/04-architecture/subagents.md)
|
|
8
8
|
[](docs/04-architecture/skills.md)
|
|
9
|
-
[](docs/04-architecture/agent-expert-system.md)
|
|
10
9
|
|
|
11
10
|
**AI-driven agile development for Claude Code, Cursor, Windsurf, OpenAI Codex CLI, and more.** Combining Scrum, Kanban, ADRs, and docs-as-code principles into one framework-agnostic system.
|
|
12
11
|
|
|
@@ -68,9 +67,8 @@ AgileFlow combines three proven methodologies:
|
|
|
68
67
|
| Component | Count | Description |
|
|
69
68
|
|-----------|-------|-------------|
|
|
70
69
|
| [Commands](docs/04-architecture/commands.md) | 41 | Slash commands for agile workflows |
|
|
71
|
-
| [
|
|
70
|
+
| [Agents/Experts](docs/04-architecture/subagents.md) | 26 | Specialized agents with self-improving knowledge bases |
|
|
72
71
|
| [Skills](docs/04-architecture/skills.md) | 23 | Auto-activated context helpers |
|
|
73
|
-
| [Experts](docs/04-architecture/agent-expert-system.md) | 26 | Self-improving knowledge bases |
|
|
74
72
|
|
|
75
73
|
---
|
|
76
74
|
|
|
@@ -80,7 +78,7 @@ Full documentation lives in [`docs/04-architecture/`](docs/04-architecture/):
|
|
|
80
78
|
|
|
81
79
|
### Reference
|
|
82
80
|
- [Commands](docs/04-architecture/commands.md) - All 41 slash commands
|
|
83
|
-
- [
|
|
81
|
+
- [Agents/Experts](docs/04-architecture/subagents.md) - 26 specialized agents with self-improving knowledge
|
|
84
82
|
- [Skills](docs/04-architecture/skills.md) - 23 auto-loaded skills
|
|
85
83
|
|
|
86
84
|
### Architecture
|
package/package.json
CHANGED
|
@@ -5,58 +5,25 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Scans agents/ directory and extracts metadata from frontmatter.
|
|
7
7
|
* Returns structured agent registry for use in generators.
|
|
8
|
+
*
|
|
9
|
+
* Set DEBUG_REGISTRY=1 for verbose logging of skipped files.
|
|
8
10
|
*/
|
|
9
11
|
|
|
10
12
|
const fs = require('fs');
|
|
11
13
|
const path = require('path');
|
|
14
|
+
const { extractFrontmatter, normalizeTools } = require('../lib/frontmatter-parser');
|
|
15
|
+
|
|
16
|
+
// Debug mode: set DEBUG_REGISTRY=1 to see why files are skipped
|
|
17
|
+
const DEBUG = process.env.DEBUG_REGISTRY === '1';
|
|
12
18
|
|
|
13
19
|
/**
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* @param {string} filePath - Path to markdown file
|
|
17
|
-
* @returns {object} Frontmatter object
|
|
20
|
+
* Log debug messages when DEBUG_REGISTRY=1
|
|
21
|
+
* @param {string} message - Message to log
|
|
18
22
|
*/
|
|
19
|
-
function
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
if (!frontmatterMatch) {
|
|
24
|
-
return {};
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const frontmatter = {};
|
|
28
|
-
const lines = frontmatterMatch[1].split('\n');
|
|
29
|
-
let currentKey = null;
|
|
30
|
-
let currentArray = null;
|
|
31
|
-
|
|
32
|
-
for (const line of lines) {
|
|
33
|
-
// Handle array items (lines starting with -)
|
|
34
|
-
if (line.trim().startsWith('-')) {
|
|
35
|
-
if (currentArray) {
|
|
36
|
-
currentArray.push(line.trim().substring(1).trim());
|
|
37
|
-
}
|
|
38
|
-
continue;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// Handle key-value pairs
|
|
42
|
-
const match = line.match(/^(\w+):\s*(.*)$/);
|
|
43
|
-
if (match) {
|
|
44
|
-
const [, key, value] = match;
|
|
45
|
-
currentKey = key;
|
|
46
|
-
|
|
47
|
-
// If value is empty, it's likely an array
|
|
48
|
-
if (!value) {
|
|
49
|
-
currentArray = [];
|
|
50
|
-
frontmatter[key] = currentArray;
|
|
51
|
-
} else {
|
|
52
|
-
// Remove quotes if present
|
|
53
|
-
frontmatter[key] = value.replace(/^["']|["']$/g, '');
|
|
54
|
-
currentArray = null;
|
|
55
|
-
}
|
|
56
|
-
}
|
|
23
|
+
function debugLog(message) {
|
|
24
|
+
if (DEBUG) {
|
|
25
|
+
console.error(`[agent-registry] ${message}`);
|
|
57
26
|
}
|
|
58
|
-
|
|
59
|
-
return frontmatter;
|
|
60
27
|
}
|
|
61
28
|
|
|
62
29
|
/**
|
|
@@ -93,25 +60,40 @@ function categorizeAgent(name, description) {
|
|
|
93
60
|
*/
|
|
94
61
|
function scanAgents(agentsDir) {
|
|
95
62
|
const agents = [];
|
|
96
|
-
const
|
|
63
|
+
const skipped = [];
|
|
64
|
+
|
|
65
|
+
let files;
|
|
66
|
+
try {
|
|
67
|
+
files = fs.readdirSync(agentsDir);
|
|
68
|
+
} catch (err) {
|
|
69
|
+
debugLog(`Failed to read directory: ${err.message}`);
|
|
70
|
+
return agents;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
debugLog(`Scanning ${files.length} files in ${agentsDir}`);
|
|
97
74
|
|
|
98
75
|
for (const file of files) {
|
|
99
|
-
if (!file.endsWith('.md'))
|
|
76
|
+
if (!file.endsWith('.md')) {
|
|
77
|
+
debugLog(`Skipping non-md file: ${file}`);
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
100
80
|
|
|
101
81
|
const filePath = path.join(agentsDir, file);
|
|
102
82
|
const frontmatter = extractFrontmatter(filePath);
|
|
103
83
|
const name = file.replace('.md', '');
|
|
104
84
|
|
|
105
|
-
//
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
} else if (typeof frontmatter.tools === 'string') {
|
|
111
|
-
tools = frontmatter.tools.split(',').map(t => t.trim());
|
|
112
|
-
}
|
|
85
|
+
// Check if frontmatter was extracted successfully
|
|
86
|
+
if (Object.keys(frontmatter).length === 0) {
|
|
87
|
+
skipped.push({ file, reason: 'no frontmatter or parse error' });
|
|
88
|
+
debugLog(`Skipping ${file}: no frontmatter found`);
|
|
89
|
+
continue;
|
|
113
90
|
}
|
|
114
91
|
|
|
92
|
+
// Normalize tools field (handles array or comma-separated string)
|
|
93
|
+
const tools = normalizeTools(frontmatter.tools);
|
|
94
|
+
|
|
95
|
+
debugLog(`Loaded ${file}: name="${frontmatter.name || name}", tools=${tools.length}`);
|
|
96
|
+
|
|
115
97
|
agents.push({
|
|
116
98
|
name,
|
|
117
99
|
file,
|
|
@@ -133,6 +115,12 @@ function scanAgents(agentsDir) {
|
|
|
133
115
|
return a.name.localeCompare(b.name);
|
|
134
116
|
});
|
|
135
117
|
|
|
118
|
+
if (skipped.length > 0) {
|
|
119
|
+
debugLog(`Skipped ${skipped.length} files: ${skipped.map(s => s.file).join(', ')}`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
debugLog(`Found ${agents.length} agents`);
|
|
123
|
+
|
|
136
124
|
return agents;
|
|
137
125
|
}
|
|
138
126
|
|
|
@@ -159,7 +147,7 @@ function main() {
|
|
|
159
147
|
}
|
|
160
148
|
|
|
161
149
|
// Export for use in other scripts
|
|
162
|
-
module.exports = { scanAgents,
|
|
150
|
+
module.exports = { scanAgents, categorizeAgent };
|
|
163
151
|
|
|
164
152
|
// Run if called directly
|
|
165
153
|
if (require.main === module) {
|
|
@@ -5,37 +5,25 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Scans commands/ directory and extracts metadata from frontmatter.
|
|
7
7
|
* Returns structured command registry for use in generators.
|
|
8
|
+
*
|
|
9
|
+
* Set DEBUG_REGISTRY=1 for verbose logging of skipped files.
|
|
8
10
|
*/
|
|
9
11
|
|
|
10
12
|
const fs = require('fs');
|
|
11
13
|
const path = require('path');
|
|
14
|
+
const { extractFrontmatter } = require('../lib/frontmatter-parser');
|
|
15
|
+
|
|
16
|
+
// Debug mode: set DEBUG_REGISTRY=1 to see why files are skipped
|
|
17
|
+
const DEBUG = process.env.DEBUG_REGISTRY === '1';
|
|
12
18
|
|
|
13
19
|
/**
|
|
14
|
-
*
|
|
15
|
-
* @param {string}
|
|
16
|
-
* @returns {object} Frontmatter object
|
|
20
|
+
* Log debug messages when DEBUG_REGISTRY=1
|
|
21
|
+
* @param {string} message - Message to log
|
|
17
22
|
*/
|
|
18
|
-
function
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
if (!frontmatterMatch) {
|
|
23
|
-
return {};
|
|
23
|
+
function debugLog(message) {
|
|
24
|
+
if (DEBUG) {
|
|
25
|
+
console.error(`[command-registry] ${message}`);
|
|
24
26
|
}
|
|
25
|
-
|
|
26
|
-
const frontmatter = {};
|
|
27
|
-
const lines = frontmatterMatch[1].split('\n');
|
|
28
|
-
|
|
29
|
-
for (const line of lines) {
|
|
30
|
-
const match = line.match(/^(\w+):\s*(.+)$/);
|
|
31
|
-
if (match) {
|
|
32
|
-
const [, key, value] = match;
|
|
33
|
-
// Remove quotes if present
|
|
34
|
-
frontmatter[key] = value.replace(/^["']|["']$/g, '');
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
return frontmatter;
|
|
39
27
|
}
|
|
40
28
|
|
|
41
29
|
/**
|
|
@@ -74,15 +62,37 @@ function categorizeCommand(name, description) {
|
|
|
74
62
|
*/
|
|
75
63
|
function scanCommands(commandsDir) {
|
|
76
64
|
const commands = [];
|
|
77
|
-
const
|
|
65
|
+
const skipped = [];
|
|
66
|
+
|
|
67
|
+
let files;
|
|
68
|
+
try {
|
|
69
|
+
files = fs.readdirSync(commandsDir);
|
|
70
|
+
} catch (err) {
|
|
71
|
+
debugLog(`Failed to read directory: ${err.message}`);
|
|
72
|
+
return commands;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
debugLog(`Scanning ${files.length} files in ${commandsDir}`);
|
|
78
76
|
|
|
79
77
|
for (const file of files) {
|
|
80
|
-
if (!file.endsWith('.md'))
|
|
78
|
+
if (!file.endsWith('.md')) {
|
|
79
|
+
debugLog(`Skipping non-md file: ${file}`);
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
81
82
|
|
|
82
83
|
const filePath = path.join(commandsDir, file);
|
|
83
84
|
const frontmatter = extractFrontmatter(filePath);
|
|
84
85
|
const name = file.replace('.md', '');
|
|
85
86
|
|
|
87
|
+
// Check if frontmatter was extracted successfully
|
|
88
|
+
if (Object.keys(frontmatter).length === 0) {
|
|
89
|
+
skipped.push({ file, reason: 'no frontmatter or parse error' });
|
|
90
|
+
debugLog(`Skipping ${file}: no frontmatter found`);
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
debugLog(`Loaded ${file}: description="${frontmatter.description || '(none)'}"`);
|
|
95
|
+
|
|
86
96
|
commands.push({
|
|
87
97
|
name,
|
|
88
98
|
file,
|
|
@@ -101,6 +111,12 @@ function scanCommands(commandsDir) {
|
|
|
101
111
|
return a.name.localeCompare(b.name);
|
|
102
112
|
});
|
|
103
113
|
|
|
114
|
+
if (skipped.length > 0) {
|
|
115
|
+
debugLog(`Skipped ${skipped.length} files: ${skipped.map(s => s.file).join(', ')}`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
debugLog(`Found ${commands.length} commands`);
|
|
119
|
+
|
|
104
120
|
return commands;
|
|
105
121
|
}
|
|
106
122
|
|
|
@@ -127,7 +143,7 @@ function main() {
|
|
|
127
143
|
}
|
|
128
144
|
|
|
129
145
|
// Export for use in other scripts
|
|
130
|
-
module.exports = { scanCommands,
|
|
146
|
+
module.exports = { scanCommands, categorizeCommand };
|
|
131
147
|
|
|
132
148
|
// Run if called directly
|
|
133
149
|
if (require.main === module) {
|
|
@@ -5,37 +5,25 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Scans skills/ directory and extracts metadata from SKILL.md frontmatter.
|
|
7
7
|
* Returns structured skill registry for use in generators.
|
|
8
|
+
*
|
|
9
|
+
* Set DEBUG_REGISTRY=1 for verbose logging of skipped files.
|
|
8
10
|
*/
|
|
9
11
|
|
|
10
12
|
const fs = require('fs');
|
|
11
13
|
const path = require('path');
|
|
14
|
+
const { extractFrontmatter } = require('../lib/frontmatter-parser');
|
|
15
|
+
|
|
16
|
+
// Debug mode: set DEBUG_REGISTRY=1 to see why files are skipped
|
|
17
|
+
const DEBUG = process.env.DEBUG_REGISTRY === '1';
|
|
12
18
|
|
|
13
19
|
/**
|
|
14
|
-
*
|
|
15
|
-
* @param {string}
|
|
16
|
-
* @returns {object} Frontmatter object
|
|
20
|
+
* Log debug messages when DEBUG_REGISTRY=1
|
|
21
|
+
* @param {string} message - Message to log
|
|
17
22
|
*/
|
|
18
|
-
function
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
if (!frontmatterMatch) {
|
|
23
|
-
return {};
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const frontmatter = {};
|
|
27
|
-
const lines = frontmatterMatch[1].split('\n');
|
|
28
|
-
|
|
29
|
-
for (const line of lines) {
|
|
30
|
-
const match = line.match(/^(\w+):\s*(.+)$/);
|
|
31
|
-
if (match) {
|
|
32
|
-
const [, key, value] = match;
|
|
33
|
-
// Remove quotes if present
|
|
34
|
-
frontmatter[key] = value.replace(/^["']|["']$/g, '');
|
|
35
|
-
}
|
|
23
|
+
function debugLog(message) {
|
|
24
|
+
if (DEBUG) {
|
|
25
|
+
console.error(`[skill-registry] ${message}`);
|
|
36
26
|
}
|
|
37
|
-
|
|
38
|
-
return frontmatter;
|
|
39
27
|
}
|
|
40
28
|
|
|
41
29
|
/**
|
|
@@ -73,25 +61,58 @@ function categorizeSkill(name, description) {
|
|
|
73
61
|
*/
|
|
74
62
|
function scanSkills(skillsDir) {
|
|
75
63
|
const skills = [];
|
|
64
|
+
const skipped = [];
|
|
65
|
+
|
|
66
|
+
let skillDirs;
|
|
67
|
+
try {
|
|
68
|
+
skillDirs = fs.readdirSync(skillsDir);
|
|
69
|
+
} catch (err) {
|
|
70
|
+
debugLog(`Failed to read directory: ${err.message}`);
|
|
71
|
+
return skills;
|
|
72
|
+
}
|
|
76
73
|
|
|
77
|
-
|
|
78
|
-
const skillDirs = fs.readdirSync(skillsDir);
|
|
74
|
+
debugLog(`Scanning ${skillDirs.length} entries in ${skillsDir}`);
|
|
79
75
|
|
|
80
76
|
for (const skillDir of skillDirs) {
|
|
81
77
|
const skillPath = path.join(skillsDir, skillDir);
|
|
82
78
|
|
|
83
79
|
// Skip if not a directory
|
|
84
|
-
|
|
80
|
+
let stat;
|
|
81
|
+
try {
|
|
82
|
+
stat = fs.statSync(skillPath);
|
|
83
|
+
} catch (err) {
|
|
84
|
+
debugLog(`Failed to stat ${skillDir}: ${err.message}`);
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (!stat.isDirectory()) {
|
|
89
|
+
debugLog(`Skipping non-directory: ${skillDir}`);
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
85
92
|
|
|
86
93
|
const skillFile = path.join(skillPath, 'SKILL.md');
|
|
87
94
|
|
|
88
95
|
// Skip if SKILL.md doesn't exist
|
|
89
|
-
if (!fs.existsSync(skillFile))
|
|
96
|
+
if (!fs.existsSync(skillFile)) {
|
|
97
|
+
skipped.push({ dir: skillDir, reason: 'no SKILL.md file' });
|
|
98
|
+
debugLog(`Skipping ${skillDir}: no SKILL.md found`);
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
90
101
|
|
|
91
102
|
const frontmatter = extractFrontmatter(skillFile);
|
|
103
|
+
|
|
104
|
+
// Check if frontmatter was extracted successfully
|
|
105
|
+
if (Object.keys(frontmatter).length === 0) {
|
|
106
|
+
skipped.push({ dir: skillDir, reason: 'no frontmatter or parse error' });
|
|
107
|
+
debugLog(`Skipping ${skillDir}: no frontmatter in SKILL.md`);
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
|
|
92
111
|
const name = frontmatter.name || skillDir;
|
|
93
112
|
const description = frontmatter.description || '';
|
|
94
113
|
|
|
114
|
+
debugLog(`Loaded ${skillDir}: name="${name}"`);
|
|
115
|
+
|
|
95
116
|
skills.push({
|
|
96
117
|
name,
|
|
97
118
|
directory: skillDir,
|
|
@@ -110,6 +131,12 @@ function scanSkills(skillsDir) {
|
|
|
110
131
|
return a.name.localeCompare(b.name);
|
|
111
132
|
});
|
|
112
133
|
|
|
134
|
+
if (skipped.length > 0) {
|
|
135
|
+
debugLog(`Skipped ${skipped.length} directories: ${skipped.map(s => s.dir).join(', ')}`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
debugLog(`Found ${skills.length} skills`);
|
|
139
|
+
|
|
113
140
|
return skills;
|
|
114
141
|
}
|
|
115
142
|
|
|
@@ -136,7 +163,7 @@ function main() {
|
|
|
136
163
|
}
|
|
137
164
|
|
|
138
165
|
// Export for use in other scripts
|
|
139
|
-
module.exports = { scanSkills,
|
|
166
|
+
module.exports = { scanSkills, categorizeSkill };
|
|
140
167
|
|
|
141
168
|
// Run if called directly
|
|
142
169
|
if (require.main === module) {
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Frontmatter Parser - Shared YAML frontmatter extraction
|
|
5
|
+
*
|
|
6
|
+
* Consolidates frontmatter parsing logic used across:
|
|
7
|
+
* - command-registry.js
|
|
8
|
+
* - agent-registry.js
|
|
9
|
+
* - skill-registry.js
|
|
10
|
+
* - content-injector.js
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const yaml = require('js-yaml');
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Parse YAML frontmatter from markdown content
|
|
18
|
+
* @param {string} content - Markdown content with frontmatter
|
|
19
|
+
* @returns {object} Parsed frontmatter object, or empty object if none found
|
|
20
|
+
*/
|
|
21
|
+
function parseFrontmatter(content) {
|
|
22
|
+
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
23
|
+
|
|
24
|
+
if (!match) {
|
|
25
|
+
return {};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
const parsed = yaml.load(match[1]);
|
|
30
|
+
// Return empty object if yaml.load returns null/undefined
|
|
31
|
+
return parsed && typeof parsed === 'object' ? parsed : {};
|
|
32
|
+
} catch (err) {
|
|
33
|
+
// Return empty object on parse error (invalid YAML)
|
|
34
|
+
return {};
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Extract frontmatter from a markdown file
|
|
40
|
+
* @param {string} filePath - Path to markdown file
|
|
41
|
+
* @returns {object} Parsed frontmatter object, or empty object if none/error
|
|
42
|
+
*/
|
|
43
|
+
function extractFrontmatter(filePath) {
|
|
44
|
+
try {
|
|
45
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
46
|
+
return parseFrontmatter(content);
|
|
47
|
+
} catch (err) {
|
|
48
|
+
// Return empty object on file read error
|
|
49
|
+
return {};
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Extract markdown body (content after frontmatter)
|
|
55
|
+
* @param {string} content - Full markdown content
|
|
56
|
+
* @returns {string} Content after frontmatter, or original if no frontmatter
|
|
57
|
+
*/
|
|
58
|
+
function extractBody(content) {
|
|
59
|
+
const match = content.match(/^---\n[\s\S]*?\n---\n?([\s\S]*)/);
|
|
60
|
+
return match ? match[1].trim() : content.trim();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Normalize tools field - handles array or comma-separated string
|
|
65
|
+
* @param {string|Array} tools - Tools field from frontmatter
|
|
66
|
+
* @returns {Array} Array of tool names
|
|
67
|
+
*/
|
|
68
|
+
function normalizeTools(tools) {
|
|
69
|
+
if (!tools) return [];
|
|
70
|
+
if (Array.isArray(tools)) return tools;
|
|
71
|
+
if (typeof tools === 'string') {
|
|
72
|
+
return tools.split(',').map(t => t.trim()).filter(Boolean);
|
|
73
|
+
}
|
|
74
|
+
return [];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
module.exports = {
|
|
78
|
+
parseFrontmatter,
|
|
79
|
+
extractFrontmatter,
|
|
80
|
+
extractBody,
|
|
81
|
+
normalizeTools,
|
|
82
|
+
};
|
|
@@ -179,6 +179,61 @@ class BaseIdeSetup {
|
|
|
179
179
|
|
|
180
180
|
return results;
|
|
181
181
|
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Recursively install markdown files from source to target directory
|
|
185
|
+
* Handles content injection and docs reference replacement.
|
|
186
|
+
* @param {string} sourceDir - Source directory path
|
|
187
|
+
* @param {string} targetDir - Target directory path
|
|
188
|
+
* @param {string} agileflowDir - AgileFlow installation directory (for dynamic content)
|
|
189
|
+
* @param {boolean} injectDynamic - Whether to inject dynamic content (only for top-level commands)
|
|
190
|
+
* @returns {Promise<{commands: number, subdirs: number}>} Count of installed items
|
|
191
|
+
*/
|
|
192
|
+
async installCommandsRecursive(sourceDir, targetDir, agileflowDir, injectDynamic = false) {
|
|
193
|
+
let commandCount = 0;
|
|
194
|
+
let subdirCount = 0;
|
|
195
|
+
|
|
196
|
+
if (!(await this.exists(sourceDir))) {
|
|
197
|
+
return { commands: 0, subdirs: 0 };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
await this.ensureDir(targetDir);
|
|
201
|
+
|
|
202
|
+
const entries = await fs.readdir(sourceDir, { withFileTypes: true });
|
|
203
|
+
|
|
204
|
+
for (const entry of entries) {
|
|
205
|
+
const sourcePath = path.join(sourceDir, entry.name);
|
|
206
|
+
const targetPath = path.join(targetDir, entry.name);
|
|
207
|
+
|
|
208
|
+
if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
209
|
+
// Read and process .md file
|
|
210
|
+
let content = await this.readFile(sourcePath);
|
|
211
|
+
|
|
212
|
+
// Inject dynamic content if enabled (for top-level commands)
|
|
213
|
+
if (injectDynamic) {
|
|
214
|
+
content = this.injectDynamicContent(content, agileflowDir);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Replace docs/ references with custom folder name
|
|
218
|
+
content = this.replaceDocsReferences(content);
|
|
219
|
+
|
|
220
|
+
await this.writeFile(targetPath, content);
|
|
221
|
+
commandCount++;
|
|
222
|
+
} else if (entry.isDirectory()) {
|
|
223
|
+
// Recursively process subdirectory
|
|
224
|
+
const subResult = await this.installCommandsRecursive(
|
|
225
|
+
sourcePath,
|
|
226
|
+
targetPath,
|
|
227
|
+
agileflowDir,
|
|
228
|
+
false // Don't inject dynamic content in subdirectories
|
|
229
|
+
);
|
|
230
|
+
commandCount += subResult.commands;
|
|
231
|
+
subdirCount += 1 + subResult.subdirs;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return { commands: commandCount, subdirs: subdirCount };
|
|
236
|
+
}
|
|
182
237
|
}
|
|
183
238
|
|
|
184
239
|
module.exports = { BaseIdeSetup };
|
|
@@ -19,60 +19,6 @@ class ClaudeCodeSetup extends BaseIdeSetup {
|
|
|
19
19
|
this.commandsDir = 'commands';
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
/**
|
|
23
|
-
* Recursively install commands from a source directory
|
|
24
|
-
* @param {string} sourceDir - Source directory path
|
|
25
|
-
* @param {string} targetDir - Target directory path
|
|
26
|
-
* @param {string} agileflowDir - AgileFlow installation directory (for dynamic content)
|
|
27
|
-
* @param {boolean} injectDynamic - Whether to inject dynamic content (only for top-level commands)
|
|
28
|
-
* @returns {Promise<{commands: number, subdirs: number}>} Count of installed items
|
|
29
|
-
*/
|
|
30
|
-
async installCommandsRecursive(sourceDir, targetDir, agileflowDir, injectDynamic = false) {
|
|
31
|
-
let commandCount = 0;
|
|
32
|
-
let subdirCount = 0;
|
|
33
|
-
|
|
34
|
-
if (!(await this.exists(sourceDir))) {
|
|
35
|
-
return { commands: 0, subdirs: 0 };
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
await this.ensureDir(targetDir);
|
|
39
|
-
|
|
40
|
-
const entries = await fs.readdir(sourceDir, { withFileTypes: true });
|
|
41
|
-
|
|
42
|
-
for (const entry of entries) {
|
|
43
|
-
const sourcePath = path.join(sourceDir, entry.name);
|
|
44
|
-
const targetPath = path.join(targetDir, entry.name);
|
|
45
|
-
|
|
46
|
-
if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
47
|
-
// Read and process .md file
|
|
48
|
-
let content = await this.readFile(sourcePath);
|
|
49
|
-
|
|
50
|
-
// Inject dynamic content if enabled (for top-level commands)
|
|
51
|
-
if (injectDynamic) {
|
|
52
|
-
content = this.injectDynamicContent(content, agileflowDir);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// Replace docs/ references with custom folder name
|
|
56
|
-
content = this.replaceDocsReferences(content);
|
|
57
|
-
|
|
58
|
-
await this.writeFile(targetPath, content);
|
|
59
|
-
commandCount++;
|
|
60
|
-
} else if (entry.isDirectory()) {
|
|
61
|
-
// Recursively process subdirectory
|
|
62
|
-
const subResult = await this.installCommandsRecursive(
|
|
63
|
-
sourcePath,
|
|
64
|
-
targetPath,
|
|
65
|
-
agileflowDir,
|
|
66
|
-
false // Don't inject dynamic content in subdirectories
|
|
67
|
-
);
|
|
68
|
-
commandCount += subResult.commands;
|
|
69
|
-
subdirCount += 1 + subResult.subdirs;
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
return { commands: commandCount, subdirs: subdirCount };
|
|
74
|
-
}
|
|
75
|
-
|
|
76
22
|
/**
|
|
77
23
|
* Setup Claude Code IDE configuration
|
|
78
24
|
* @param {string} projectDir - Project directory
|
|
@@ -32,69 +32,39 @@ class CursorSetup extends BaseIdeSetup {
|
|
|
32
32
|
// Clean up old installation first
|
|
33
33
|
await this.cleanup(projectDir);
|
|
34
34
|
|
|
35
|
-
// Create .cursor/commands/
|
|
35
|
+
// Create .cursor/commands/AgileFlow directory
|
|
36
36
|
const cursorDir = path.join(projectDir, this.configDir);
|
|
37
37
|
const commandsDir = path.join(cursorDir, this.commandsDir);
|
|
38
38
|
const agileflowCommandsDir = path.join(commandsDir, 'AgileFlow');
|
|
39
39
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
// Get commands from AgileFlow installation
|
|
40
|
+
// Install commands using shared recursive method
|
|
43
41
|
const commandsSource = path.join(agileflowDir, 'commands');
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
// Inject dynamic content (agent lists, command lists)
|
|
54
|
-
content = this.injectDynamicContent(content, agileflowDir);
|
|
55
|
-
|
|
56
|
-
// Replace docs/ references with custom folder name
|
|
57
|
-
content = this.replaceDocsReferences(content);
|
|
58
|
-
|
|
59
|
-
const targetPath = path.join(agileflowCommandsDir, `${command.name}.md`);
|
|
60
|
-
await this.writeFile(targetPath, content);
|
|
61
|
-
commandCount++;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// Create agents subdirectory
|
|
66
|
-
const agileflowAgentsDir = path.join(agileflowCommandsDir, 'agents');
|
|
67
|
-
await this.ensureDir(agileflowAgentsDir);
|
|
68
|
-
|
|
69
|
-
// Get agents from AgileFlow installation
|
|
42
|
+
const commandResult = await this.installCommandsRecursive(
|
|
43
|
+
commandsSource,
|
|
44
|
+
agileflowCommandsDir,
|
|
45
|
+
agileflowDir,
|
|
46
|
+
true // Inject dynamic content
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
// Install agents as subdirectory
|
|
70
50
|
const agentsSource = path.join(agileflowDir, 'agents');
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
let content = await this.readFile(agent.path);
|
|
79
|
-
|
|
80
|
-
// Replace docs/ references with custom folder name
|
|
81
|
-
content = this.replaceDocsReferences(content);
|
|
82
|
-
|
|
83
|
-
const targetPath = path.join(agileflowAgentsDir, `${agent.name}.md`);
|
|
84
|
-
await this.writeFile(targetPath, content);
|
|
85
|
-
agentCount++;
|
|
86
|
-
}
|
|
87
|
-
}
|
|
51
|
+
const agentsTargetDir = path.join(agileflowCommandsDir, 'agents');
|
|
52
|
+
const agentResult = await this.installCommandsRecursive(
|
|
53
|
+
agentsSource,
|
|
54
|
+
agentsTargetDir,
|
|
55
|
+
agileflowDir,
|
|
56
|
+
false // No dynamic content for agents
|
|
57
|
+
);
|
|
88
58
|
|
|
89
59
|
console.log(chalk.green(` ✓ ${this.displayName} configured:`));
|
|
90
|
-
console.log(chalk.dim(` - ${
|
|
91
|
-
console.log(chalk.dim(` - ${
|
|
60
|
+
console.log(chalk.dim(` - ${commandResult.commands} commands installed`));
|
|
61
|
+
console.log(chalk.dim(` - ${agentResult.commands} agents installed`));
|
|
92
62
|
console.log(chalk.dim(` - Path: ${path.relative(projectDir, agileflowCommandsDir)}`));
|
|
93
63
|
|
|
94
64
|
return {
|
|
95
65
|
success: true,
|
|
96
|
-
commands:
|
|
97
|
-
agents:
|
|
66
|
+
commands: commandResult.commands,
|
|
67
|
+
agents: agentResult.commands,
|
|
98
68
|
};
|
|
99
69
|
}
|
|
100
70
|
|
|
@@ -37,64 +37,34 @@ class WindsurfSetup extends BaseIdeSetup {
|
|
|
37
37
|
const workflowsDir = path.join(windsurfDir, this.workflowsDir);
|
|
38
38
|
const agileflowWorkflowsDir = path.join(workflowsDir, 'agileflow');
|
|
39
39
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
// Get commands from AgileFlow installation
|
|
40
|
+
// Install commands using shared recursive method
|
|
43
41
|
const commandsSource = path.join(agileflowDir, 'commands');
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
// Inject dynamic content (agent lists, command lists)
|
|
54
|
-
content = this.injectDynamicContent(content, agileflowDir);
|
|
55
|
-
|
|
56
|
-
// Replace docs/ references with custom folder name
|
|
57
|
-
content = this.replaceDocsReferences(content);
|
|
58
|
-
|
|
59
|
-
const targetPath = path.join(agileflowWorkflowsDir, `${command.name}.md`);
|
|
60
|
-
await this.writeFile(targetPath, content);
|
|
61
|
-
commandCount++;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// Create agents subdirectory
|
|
66
|
-
const agileflowAgentsDir = path.join(agileflowWorkflowsDir, 'agents');
|
|
67
|
-
await this.ensureDir(agileflowAgentsDir);
|
|
68
|
-
|
|
69
|
-
// Get agents from AgileFlow installation
|
|
42
|
+
const commandResult = await this.installCommandsRecursive(
|
|
43
|
+
commandsSource,
|
|
44
|
+
agileflowWorkflowsDir,
|
|
45
|
+
agileflowDir,
|
|
46
|
+
true // Inject dynamic content
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
// Install agents as subdirectory
|
|
70
50
|
const agentsSource = path.join(agileflowDir, 'agents');
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
let content = await this.readFile(agent.path);
|
|
79
|
-
|
|
80
|
-
// Replace docs/ references with custom folder name
|
|
81
|
-
content = this.replaceDocsReferences(content);
|
|
82
|
-
|
|
83
|
-
const targetPath = path.join(agileflowAgentsDir, `${agent.name}.md`);
|
|
84
|
-
await this.writeFile(targetPath, content);
|
|
85
|
-
agentCount++;
|
|
86
|
-
}
|
|
87
|
-
}
|
|
51
|
+
const agentsTargetDir = path.join(agileflowWorkflowsDir, 'agents');
|
|
52
|
+
const agentResult = await this.installCommandsRecursive(
|
|
53
|
+
agentsSource,
|
|
54
|
+
agentsTargetDir,
|
|
55
|
+
agileflowDir,
|
|
56
|
+
false // No dynamic content for agents
|
|
57
|
+
);
|
|
88
58
|
|
|
89
59
|
console.log(chalk.green(` ✓ ${this.displayName} configured:`));
|
|
90
|
-
console.log(chalk.dim(` - ${
|
|
91
|
-
console.log(chalk.dim(` - ${
|
|
60
|
+
console.log(chalk.dim(` - ${commandResult.commands} workflows installed`));
|
|
61
|
+
console.log(chalk.dim(` - ${agentResult.commands} agent workflows installed`));
|
|
92
62
|
console.log(chalk.dim(` - Path: ${path.relative(projectDir, agileflowWorkflowsDir)}`));
|
|
93
63
|
|
|
94
64
|
return {
|
|
95
65
|
success: true,
|
|
96
|
-
commands:
|
|
97
|
-
agents:
|
|
66
|
+
commands: commandResult.commands,
|
|
67
|
+
agents: agentResult.commands,
|
|
98
68
|
};
|
|
99
69
|
}
|
|
100
70
|
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
const fs = require('fs');
|
|
9
9
|
const path = require('path');
|
|
10
|
-
const
|
|
10
|
+
const { parseFrontmatter, normalizeTools } = require('../../../scripts/lib/frontmatter-parser');
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* Scan agents directory and generate formatted agent list
|
|
@@ -22,34 +22,20 @@ function generateAgentList(agentsDir) {
|
|
|
22
22
|
const filePath = path.join(agentsDir, file);
|
|
23
23
|
const content = fs.readFileSync(filePath, 'utf8');
|
|
24
24
|
|
|
25
|
-
//
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
const frontmatter = yaml.load(match[1]);
|
|
31
|
-
|
|
32
|
-
// Skip if frontmatter is null or not an object
|
|
33
|
-
if (!frontmatter || typeof frontmatter !== 'object') {
|
|
34
|
-
continue;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
// Handle tools field - can be array or string
|
|
38
|
-
let tools = frontmatter.tools || [];
|
|
39
|
-
if (typeof tools === 'string') {
|
|
40
|
-
tools = tools.split(',').map(t => t.trim());
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
agents.push({
|
|
44
|
-
name: frontmatter.name || path.basename(file, '.md'),
|
|
45
|
-
description: frontmatter.description || '',
|
|
46
|
-
tools: tools,
|
|
47
|
-
model: frontmatter.model || 'haiku',
|
|
48
|
-
});
|
|
49
|
-
} catch (err) {
|
|
50
|
-
// Silently skip files with parsing errors
|
|
25
|
+
// Parse frontmatter using shared parser
|
|
26
|
+
const frontmatter = parseFrontmatter(content);
|
|
27
|
+
|
|
28
|
+
// Skip if no frontmatter found
|
|
29
|
+
if (!frontmatter || Object.keys(frontmatter).length === 0) {
|
|
51
30
|
continue;
|
|
52
31
|
}
|
|
32
|
+
|
|
33
|
+
agents.push({
|
|
34
|
+
name: frontmatter.name || path.basename(file, '.md'),
|
|
35
|
+
description: frontmatter.description || '',
|
|
36
|
+
tools: normalizeTools(frontmatter.tools),
|
|
37
|
+
model: frontmatter.model || 'haiku',
|
|
38
|
+
});
|
|
53
39
|
}
|
|
54
40
|
|
|
55
41
|
// Sort alphabetically by name
|
|
@@ -82,29 +68,20 @@ function generateCommandList(commandsDir) {
|
|
|
82
68
|
const filePath = path.join(commandsDir, file);
|
|
83
69
|
const content = fs.readFileSync(filePath, 'utf8');
|
|
84
70
|
|
|
85
|
-
//
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
const cmdName = path.basename(file, '.md');
|
|
92
|
-
|
|
93
|
-
// Skip if frontmatter is null or not an object
|
|
94
|
-
if (!frontmatter || typeof frontmatter !== 'object') {
|
|
95
|
-
continue;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
commands.push({
|
|
99
|
-
name: cmdName,
|
|
100
|
-
description: frontmatter.description || '',
|
|
101
|
-
argumentHint: frontmatter['argument-hint'] || '',
|
|
102
|
-
});
|
|
103
|
-
} catch (err) {
|
|
104
|
-
// Silently skip files with parsing errors - they might be generated files
|
|
105
|
-
// with content that looks like frontmatter but isn't
|
|
71
|
+
// Parse frontmatter using shared parser
|
|
72
|
+
const frontmatter = parseFrontmatter(content);
|
|
73
|
+
const cmdName = path.basename(file, '.md');
|
|
74
|
+
|
|
75
|
+
// Skip if no frontmatter found
|
|
76
|
+
if (!frontmatter || Object.keys(frontmatter).length === 0) {
|
|
106
77
|
continue;
|
|
107
78
|
}
|
|
79
|
+
|
|
80
|
+
commands.push({
|
|
81
|
+
name: cmdName,
|
|
82
|
+
description: frontmatter.description || '',
|
|
83
|
+
argumentHint: frontmatter['argument-hint'] || '',
|
|
84
|
+
});
|
|
108
85
|
}
|
|
109
86
|
|
|
110
87
|
// Sort alphabetically by name
|
|
@@ -2,14 +2,33 @@
|
|
|
2
2
|
* AgileFlow CLI - npm Registry Utilities
|
|
3
3
|
*
|
|
4
4
|
* Utilities for interacting with the npm registry.
|
|
5
|
+
* Set DEBUG_NPM=1 environment variable for verbose error logging.
|
|
5
6
|
*/
|
|
6
7
|
|
|
7
8
|
const https = require('https');
|
|
8
9
|
|
|
10
|
+
// Debug mode: set DEBUG_NPM=1 to see error details
|
|
11
|
+
const DEBUG = process.env.DEBUG_NPM === '1';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Log debug messages when DEBUG_NPM=1
|
|
15
|
+
* @param {string} message - Message to log
|
|
16
|
+
* @param {*} data - Optional data to include
|
|
17
|
+
*/
|
|
18
|
+
function debugLog(message, data = null) {
|
|
19
|
+
if (DEBUG) {
|
|
20
|
+
console.error(`[npm-utils] ${message}`, data ? JSON.stringify(data) : '');
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
9
24
|
/**
|
|
10
25
|
* Get the latest version of a package from npm registry
|
|
11
|
-
*
|
|
12
|
-
*
|
|
26
|
+
*
|
|
27
|
+
* Returns null on any error (network, timeout, invalid response) since
|
|
28
|
+
* version checking should not block the CLI. Set DEBUG_NPM=1 to see errors.
|
|
29
|
+
*
|
|
30
|
+
* @param {string} packageName - Name of the package (e.g., 'agileflow' or '@scope/pkg')
|
|
31
|
+
* @returns {Promise<string|null>} Latest version string or null if unavailable
|
|
13
32
|
*/
|
|
14
33
|
async function getLatestVersion(packageName) {
|
|
15
34
|
return new Promise(resolve => {
|
|
@@ -23,6 +42,8 @@ async function getLatestVersion(packageName) {
|
|
|
23
42
|
},
|
|
24
43
|
};
|
|
25
44
|
|
|
45
|
+
debugLog('Fetching version', { package: packageName, path: options.path });
|
|
46
|
+
|
|
26
47
|
const req = https.request(options, res => {
|
|
27
48
|
let data = '';
|
|
28
49
|
|
|
@@ -31,24 +52,30 @@ async function getLatestVersion(packageName) {
|
|
|
31
52
|
});
|
|
32
53
|
|
|
33
54
|
res.on('end', () => {
|
|
55
|
+
if (res.statusCode !== 200) {
|
|
56
|
+
debugLog('Non-200 status', { statusCode: res.statusCode });
|
|
57
|
+
return resolve(null);
|
|
58
|
+
}
|
|
59
|
+
|
|
34
60
|
try {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
resolve(null);
|
|
40
|
-
}
|
|
61
|
+
const json = JSON.parse(data);
|
|
62
|
+
const version = json.version || null;
|
|
63
|
+
debugLog('Version found', { version });
|
|
64
|
+
resolve(version);
|
|
41
65
|
} catch (err) {
|
|
66
|
+
debugLog('JSON parse error', { error: err.message });
|
|
42
67
|
resolve(null);
|
|
43
68
|
}
|
|
44
69
|
});
|
|
45
70
|
});
|
|
46
71
|
|
|
47
|
-
req.on('error',
|
|
72
|
+
req.on('error', err => {
|
|
73
|
+
debugLog('Network error', { error: err.message });
|
|
48
74
|
resolve(null);
|
|
49
75
|
});
|
|
50
76
|
|
|
51
77
|
req.setTimeout(5000, () => {
|
|
78
|
+
debugLog('Request timeout');
|
|
52
79
|
req.destroy();
|
|
53
80
|
resolve(null);
|
|
54
81
|
});
|
package/tools/cli/lib/ui.js
CHANGED
|
@@ -8,6 +8,7 @@ const chalk = require('chalk');
|
|
|
8
8
|
const inquirer = require('inquirer');
|
|
9
9
|
const path = require('node:path');
|
|
10
10
|
const fs = require('node:fs');
|
|
11
|
+
const { IdeManager } = require('../installers/ide/manager');
|
|
11
12
|
|
|
12
13
|
// Load package.json for version
|
|
13
14
|
const packageJsonPath = path.join(__dirname, '..', '..', '..', 'package.json');
|
|
@@ -73,8 +74,27 @@ function info(message) {
|
|
|
73
74
|
console.log(chalk.dim(` ${message}`));
|
|
74
75
|
}
|
|
75
76
|
|
|
77
|
+
// IDE Manager instance for dynamic IDE discovery
|
|
78
|
+
const ideManager = new IdeManager();
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Get available IDE choices dynamically from installed handlers
|
|
82
|
+
* @returns {Array} IDE choices formatted for inquirer
|
|
83
|
+
*/
|
|
84
|
+
function getIdeChoices() {
|
|
85
|
+
const ides = ideManager.getAvailableIdes();
|
|
86
|
+
|
|
87
|
+
return ides.map((ide, index) => ({
|
|
88
|
+
name: ide.name,
|
|
89
|
+
value: ide.value,
|
|
90
|
+
// First IDE (preferred) is checked by default
|
|
91
|
+
checked: ide.preferred || index === 0,
|
|
92
|
+
}));
|
|
93
|
+
}
|
|
94
|
+
|
|
76
95
|
/**
|
|
77
|
-
*
|
|
96
|
+
* @deprecated Use getIdeChoices() instead - dynamically loaded from IDE handlers
|
|
97
|
+
* Legacy hardcoded IDE choices kept for backward compatibility
|
|
78
98
|
*/
|
|
79
99
|
const IDE_CHOICES = [
|
|
80
100
|
{
|
|
@@ -126,7 +146,7 @@ async function promptInstall() {
|
|
|
126
146
|
type: 'checkbox',
|
|
127
147
|
name: 'ides',
|
|
128
148
|
message: 'Select your IDE(s):',
|
|
129
|
-
choices:
|
|
149
|
+
choices: getIdeChoices(),
|
|
130
150
|
validate: input => {
|
|
131
151
|
if (input.length === 0) {
|
|
132
152
|
return 'Please select at least one IDE';
|
|
@@ -219,5 +239,6 @@ module.exports = {
|
|
|
219
239
|
promptInstall,
|
|
220
240
|
confirm,
|
|
221
241
|
getIdeConfig,
|
|
222
|
-
|
|
242
|
+
getIdeChoices,
|
|
243
|
+
IDE_CHOICES, // @deprecated - kept for backward compatibility
|
|
223
244
|
};
|