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 CHANGED
@@ -4,9 +4,8 @@
4
4
 
5
5
  [![npm version](https://img.shields.io/npm/v/agileflow?color=brightgreen)](https://www.npmjs.com/package/agileflow)
6
6
  [![Commands](https://img.shields.io/badge/commands-41-blue)](docs/04-architecture/commands.md)
7
- [![Subagents](https://img.shields.io/badge/subagents-26-orange)](docs/04-architecture/subagents.md)
7
+ [![Agents/Experts](https://img.shields.io/badge/agents%2Fexperts-26-orange)](docs/04-architecture/subagents.md)
8
8
  [![Skills](https://img.shields.io/badge/skills-23-purple)](docs/04-architecture/skills.md)
9
- [![Experts](https://img.shields.io/badge/experts-26-green)](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
- | [Subagents](docs/04-architecture/subagents.md) | 26 | Specialized agents for focused work |
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
- - [Subagents](docs/04-architecture/subagents.md) - All 26 specialized agents
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agileflow",
3
- "version": "2.55.0",
3
+ "version": "2.56.0",
4
4
  "description": "AI-driven agile development system for Claude Code, Cursor, Windsurf, and more",
5
5
  "keywords": [
6
6
  "agile",
@@ -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
- * Extract YAML frontmatter from markdown file
15
- * Handles multi-line values like tools arrays
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 extractFrontmatter(filePath) {
20
- const content = fs.readFileSync(filePath, 'utf-8');
21
- const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
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 files = fs.readdirSync(agentsDir);
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')) continue;
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
- // Parse tools array if it exists
106
- let tools = [];
107
- if (frontmatter.tools) {
108
- if (Array.isArray(frontmatter.tools)) {
109
- tools = frontmatter.tools;
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, extractFrontmatter, categorizeAgent };
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
- * Extract frontmatter from markdown file
15
- * @param {string} filePath - Path to markdown file
16
- * @returns {object} Frontmatter object
20
+ * Log debug messages when DEBUG_REGISTRY=1
21
+ * @param {string} message - Message to log
17
22
  */
18
- function extractFrontmatter(filePath) {
19
- const content = fs.readFileSync(filePath, 'utf-8');
20
- const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
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 files = fs.readdirSync(commandsDir);
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')) continue;
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, extractFrontmatter, categorizeCommand };
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
- * Extract YAML frontmatter from markdown file
15
- * @param {string} filePath - Path to markdown file
16
- * @returns {object} Frontmatter object
20
+ * Log debug messages when DEBUG_REGISTRY=1
21
+ * @param {string} message - Message to log
17
22
  */
18
- function extractFrontmatter(filePath) {
19
- const content = fs.readFileSync(filePath, 'utf-8');
20
- const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
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
- // Each skill is in its own directory with a SKILL.md file
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
- if (!fs.statSync(skillPath).isDirectory()) continue;
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)) continue;
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, extractFrontmatter, categorizeSkill };
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/agileflow directory
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
- await this.ensureDir(agileflowCommandsDir);
41
-
42
- // Get commands from AgileFlow installation
40
+ // Install commands using shared recursive method
43
41
  const commandsSource = path.join(agileflowDir, 'commands');
44
- let commandCount = 0;
45
-
46
- if (await this.exists(commandsSource)) {
47
- const commands = await this.scanDirectory(commandsSource, '.md');
48
-
49
- for (const command of commands) {
50
- // Read the original command content
51
- let content = await this.readFile(command.path);
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
- let agentCount = 0;
72
-
73
- if (await this.exists(agentsSource)) {
74
- const agents = await this.scanDirectory(agentsSource, '.md');
75
-
76
- for (const agent of agents) {
77
- // Read the original agent content
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(` - ${commandCount} commands installed`));
91
- console.log(chalk.dim(` - ${agentCount} agents installed`));
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: commandCount,
97
- agents: agentCount,
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
- await this.ensureDir(agileflowWorkflowsDir);
41
-
42
- // Get commands from AgileFlow installation
40
+ // Install commands using shared recursive method
43
41
  const commandsSource = path.join(agileflowDir, 'commands');
44
- let commandCount = 0;
45
-
46
- if (await this.exists(commandsSource)) {
47
- const commands = await this.scanDirectory(commandsSource, '.md');
48
-
49
- for (const command of commands) {
50
- // Read the original command content
51
- let content = await this.readFile(command.path);
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
- let agentCount = 0;
72
-
73
- if (await this.exists(agentsSource)) {
74
- const agents = await this.scanDirectory(agentsSource, '.md');
75
-
76
- for (const agent of agents) {
77
- // Read the original agent content
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(` - ${commandCount} workflows installed`));
91
- console.log(chalk.dim(` - ${agentCount} agent workflows installed`));
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: commandCount,
97
- agents: agentCount,
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 yaml = require('js-yaml');
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
- // Extract YAML frontmatter
26
- const match = content.match(/^---\n([\s\S]*?)\n---/);
27
- if (!match) continue;
28
-
29
- try {
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
- // Extract YAML frontmatter
86
- const match = content.match(/^---\n([\s\S]*?)\n---/);
87
- if (!match) continue;
88
-
89
- try {
90
- const frontmatter = yaml.load(match[1]);
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
- * @param {string} packageName - Name of the package
12
- * @returns {Promise<string|null>} Latest version or null if error
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
- if (res.statusCode === 200) {
36
- const json = JSON.parse(data);
37
- resolve(json.version || null);
38
- } else {
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
  });
@@ -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
- * Available IDE configurations
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: IDE_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
- IDE_CHOICES,
242
+ getIdeChoices,
243
+ IDE_CHOICES, // @deprecated - kept for backward compatibility
223
244
  };