agileflow 2.87.0 → 2.89.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.
@@ -7,7 +7,6 @@
7
7
 
8
8
  const path = require('node:path');
9
9
  const fs = require('fs-extra');
10
- const chalk = require('chalk');
11
10
  const { BaseIdeSetup } = require('./_base-ide');
12
11
 
13
12
  /**
@@ -27,45 +26,10 @@ class CursorSetup extends BaseIdeSetup {
27
26
  * @param {Object} options - Setup options
28
27
  */
29
28
  async setup(projectDir, agileflowDir, options = {}) {
30
- console.log(chalk.hex('#e8683a')(` Setting up ${this.displayName}...`));
31
-
32
- // Clean up old installation first
33
- await this.cleanup(projectDir);
34
-
35
- // Create .cursor/commands/AgileFlow directory
36
- const cursorDir = path.join(projectDir, this.configDir);
37
- const commandsDir = path.join(cursorDir, this.commandsDir);
38
- const agileflowCommandsDir = path.join(commandsDir, 'AgileFlow');
39
-
40
- // Install commands using shared recursive method
41
- const commandsSource = path.join(agileflowDir, 'commands');
42
- const commandResult = await this.installCommandsRecursive(
43
- commandsSource,
44
- agileflowCommandsDir,
45
- agileflowDir,
46
- true // Inject dynamic content
47
- );
48
-
49
- // Install agents as subdirectory
50
- const agentsSource = path.join(agileflowDir, 'agents');
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
- );
58
-
59
- console.log(chalk.green(` ✓ ${this.displayName} configured:`));
60
- console.log(chalk.dim(` - ${commandResult.commands} commands installed`));
61
- console.log(chalk.dim(` - ${agentResult.commands} agents installed`));
62
- console.log(chalk.dim(` - Path: ${path.relative(projectDir, agileflowCommandsDir)}`));
63
-
64
- return {
65
- success: true,
66
- commands: commandResult.commands,
67
- agents: agentResult.commands,
68
- };
29
+ return this.setupStandard(projectDir, agileflowDir, {
30
+ targetSubdir: this.commandsDir,
31
+ agileflowFolder: 'AgileFlow',
32
+ });
69
33
  }
70
34
 
71
35
  /**
@@ -7,7 +7,6 @@
7
7
 
8
8
  const path = require('node:path');
9
9
  const fs = require('fs-extra');
10
- const chalk = require('chalk');
11
10
  const { BaseIdeSetup } = require('./_base-ide');
12
11
 
13
12
  /**
@@ -27,45 +26,12 @@ class WindsurfSetup extends BaseIdeSetup {
27
26
  * @param {Object} options - Setup options
28
27
  */
29
28
  async setup(projectDir, agileflowDir, options = {}) {
30
- console.log(chalk.hex('#e8683a')(` Setting up ${this.displayName}...`));
31
-
32
- // Clean up old installation first
33
- await this.cleanup(projectDir);
34
-
35
- // Create .windsurf/workflows/agileflow directory
36
- const windsurfDir = path.join(projectDir, this.configDir);
37
- const workflowsDir = path.join(windsurfDir, this.workflowsDir);
38
- const agileflowWorkflowsDir = path.join(workflowsDir, 'agileflow');
39
-
40
- // Install commands using shared recursive method
41
- const commandsSource = path.join(agileflowDir, 'commands');
42
- const commandResult = await this.installCommandsRecursive(
43
- commandsSource,
44
- agileflowWorkflowsDir,
45
- agileflowDir,
46
- true // Inject dynamic content
47
- );
48
-
49
- // Install agents as subdirectory
50
- const agentsSource = path.join(agileflowDir, 'agents');
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
- );
58
-
59
- console.log(chalk.green(` ✓ ${this.displayName} configured:`));
60
- console.log(chalk.dim(` - ${commandResult.commands} workflows installed`));
61
- console.log(chalk.dim(` - ${agentResult.commands} agent workflows installed`));
62
- console.log(chalk.dim(` - Path: ${path.relative(projectDir, agileflowWorkflowsDir)}`));
63
-
64
- return {
65
- success: true,
66
- commands: commandResult.commands,
67
- agents: agentResult.commands,
68
- };
29
+ return this.setupStandard(projectDir, agileflowDir, {
30
+ targetSubdir: this.workflowsDir,
31
+ agileflowFolder: 'agileflow',
32
+ commandLabel: 'workflows',
33
+ agentLabel: 'agent workflows',
34
+ });
69
35
  }
70
36
 
71
37
  /**
@@ -26,6 +26,7 @@ const path = require('path');
26
26
 
27
27
  // Use shared modules
28
28
  const { parseFrontmatter, normalizeTools } = require('../../../scripts/lib/frontmatter-parser');
29
+ const { validatePath } = require('../../../lib/validate');
29
30
  const {
30
31
  countCommands,
31
32
  countAgents,
@@ -37,6 +38,18 @@ const {
37
38
  // List Generation Functions
38
39
  // =============================================================================
39
40
 
41
+ /**
42
+ * Validate that a file path is within the expected directory.
43
+ * Prevents reading files outside the expected scope.
44
+ * @param {string} filePath - File path to validate
45
+ * @param {string} baseDir - Expected base directory
46
+ * @returns {boolean} True if path is safe
47
+ */
48
+ function isPathSafe(filePath, baseDir) {
49
+ const result = validatePath(filePath, baseDir, { allowSymlinks: true });
50
+ return result.ok;
51
+ }
52
+
40
53
  /**
41
54
  * Scan agents directory and generate formatted agent list
42
55
  * @param {string} agentsDir - Path to agents directory
@@ -50,6 +63,12 @@ function generateAgentList(agentsDir) {
50
63
 
51
64
  for (const file of files) {
52
65
  const filePath = path.join(agentsDir, file);
66
+
67
+ // Validate path before reading to prevent traversal via symlinks or malicious names
68
+ if (!isPathSafe(filePath, agentsDir)) {
69
+ continue;
70
+ }
71
+
53
72
  const content = fs.readFileSync(filePath, 'utf8');
54
73
  const frontmatter = parseFrontmatter(content);
55
74
 
@@ -94,6 +113,12 @@ function generateCommandList(commandsDir) {
94
113
  const mainFiles = fs.readdirSync(commandsDir).filter(f => f.endsWith('.md'));
95
114
  for (const file of mainFiles) {
96
115
  const filePath = path.join(commandsDir, file);
116
+
117
+ // Validate path before reading
118
+ if (!isPathSafe(filePath, commandsDir)) {
119
+ continue;
120
+ }
121
+
97
122
  const content = fs.readFileSync(filePath, 'utf8');
98
123
  const frontmatter = parseFrontmatter(content);
99
124
  const cmdName = path.basename(file, '.md');
@@ -114,10 +139,22 @@ function generateCommandList(commandsDir) {
114
139
  for (const entry of entries) {
115
140
  if (entry.isDirectory()) {
116
141
  const subDir = path.join(commandsDir, entry.name);
142
+
143
+ // Validate subdirectory path
144
+ if (!isPathSafe(subDir, commandsDir)) {
145
+ continue;
146
+ }
147
+
117
148
  const subFiles = fs.readdirSync(subDir).filter(f => f.endsWith('.md'));
118
149
 
119
150
  for (const file of subFiles) {
120
151
  const filePath = path.join(subDir, file);
152
+
153
+ // Validate file path within subdirectory
154
+ if (!isPathSafe(filePath, commandsDir)) {
155
+ continue;
156
+ }
157
+
121
158
  const content = fs.readFileSync(filePath, 'utf8');
122
159
  const frontmatter = parseFrontmatter(content);
123
160
  const cmdName = `${entry.name}:${path.basename(file, '.md')}`;
@@ -0,0 +1,233 @@
1
+ /**
2
+ * ide-errors.js - Typed Exception Classes for IDE Installers
3
+ *
4
+ * Provides specific error types for common IDE setup failures.
5
+ * These errors carry context about what failed and why,
6
+ * enabling better error handling and user feedback.
7
+ */
8
+
9
+ /**
10
+ * Base error class for IDE-related errors.
11
+ * All IDE errors extend this class.
12
+ */
13
+ class IdeError extends Error {
14
+ /**
15
+ * @param {string} message - Error description
16
+ * @param {string} ideName - Name of the IDE (e.g., 'Claude Code', 'Cursor')
17
+ * @param {Object} [context={}] - Additional context about the error
18
+ */
19
+ constructor(message, ideName, context = {}) {
20
+ super(message);
21
+ this.name = this.constructor.name;
22
+ this.ideName = ideName;
23
+ this.context = context;
24
+ Error.captureStackTrace(this, this.constructor);
25
+ }
26
+
27
+ /**
28
+ * Get a user-friendly description of the error
29
+ * @returns {string}
30
+ */
31
+ getUserMessage() {
32
+ return `${this.ideName}: ${this.message}`;
33
+ }
34
+ }
35
+
36
+ /**
37
+ * Thrown when IDE configuration directory is not found.
38
+ * Example: .claude directory doesn't exist, .cursor not found
39
+ */
40
+ class IdeConfigNotFoundError extends IdeError {
41
+ /**
42
+ * @param {string} ideName - Name of the IDE
43
+ * @param {string} configPath - Expected config directory path
44
+ * @param {Object} [context={}] - Additional context
45
+ */
46
+ constructor(ideName, configPath, context = {}) {
47
+ super(`Configuration directory not found: ${configPath}`, ideName, {
48
+ configPath,
49
+ ...context,
50
+ });
51
+ this.configPath = configPath;
52
+ }
53
+
54
+ /**
55
+ * Get suggested action to fix the error
56
+ * @returns {string}
57
+ */
58
+ getSuggestedAction() {
59
+ return `Create the ${this.configPath} directory or run the IDE at least once to initialize it.`;
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Thrown when command installation fails.
65
+ * Example: Failed to copy command files, directory creation failed
66
+ */
67
+ class CommandInstallationError extends IdeError {
68
+ /**
69
+ * @param {string} ideName - Name of the IDE
70
+ * @param {string} commandName - Name of the command being installed
71
+ * @param {string} reason - Why the installation failed
72
+ * @param {Object} [context={}] - Additional context
73
+ */
74
+ constructor(ideName, commandName, reason, context = {}) {
75
+ super(`Failed to install command '${commandName}': ${reason}`, ideName, {
76
+ commandName,
77
+ reason,
78
+ ...context,
79
+ });
80
+ this.commandName = commandName;
81
+ this.reason = reason;
82
+ }
83
+
84
+ /**
85
+ * Get suggested action to fix the error
86
+ * @returns {string}
87
+ */
88
+ getSuggestedAction() {
89
+ if (this.reason.includes('permission')) {
90
+ return `Check file permissions for the installation directory.`;
91
+ }
92
+ if (this.reason.includes('disk')) {
93
+ return `Free up disk space and try again.`;
94
+ }
95
+ return `Try running the installation again or check the source files.`;
96
+ }
97
+ }
98
+
99
+ /**
100
+ * Thrown when a file operation fails due to permission issues.
101
+ * Example: Cannot write to config directory, read access denied
102
+ */
103
+ class FilePermissionError extends IdeError {
104
+ /**
105
+ * @param {string} ideName - Name of the IDE
106
+ * @param {string} filePath - Path to the file/directory
107
+ * @param {string} operation - Operation that failed ('read', 'write', 'delete')
108
+ * @param {Object} [context={}] - Additional context
109
+ */
110
+ constructor(ideName, filePath, operation, context = {}) {
111
+ super(`Permission denied: cannot ${operation} '${filePath}'`, ideName, {
112
+ filePath,
113
+ operation,
114
+ ...context,
115
+ });
116
+ this.filePath = filePath;
117
+ this.operation = operation;
118
+ }
119
+
120
+ /**
121
+ * Get suggested action to fix the error
122
+ * @returns {string}
123
+ */
124
+ getSuggestedAction() {
125
+ return `Check permissions on '${this.filePath}' or run with appropriate privileges.`;
126
+ }
127
+ }
128
+
129
+ /**
130
+ * Thrown when content injection fails.
131
+ * Example: Template placeholder not found, dynamic content generation failed
132
+ */
133
+ class ContentInjectionError extends IdeError {
134
+ /**
135
+ * @param {string} ideName - Name of the IDE
136
+ * @param {string} templateFile - Path to the template file
137
+ * @param {string} reason - Why injection failed
138
+ * @param {Object} [context={}] - Additional context
139
+ */
140
+ constructor(ideName, templateFile, reason, context = {}) {
141
+ super(`Content injection failed for '${templateFile}': ${reason}`, ideName, {
142
+ templateFile,
143
+ reason,
144
+ ...context,
145
+ });
146
+ this.templateFile = templateFile;
147
+ this.reason = reason;
148
+ }
149
+ }
150
+
151
+ /**
152
+ * Thrown when cleanup operation fails.
153
+ * Example: Cannot remove old installation, locked files
154
+ */
155
+ class CleanupError extends IdeError {
156
+ /**
157
+ * @param {string} ideName - Name of the IDE
158
+ * @param {string} targetPath - Path being cleaned up
159
+ * @param {string} reason - Why cleanup failed
160
+ * @param {Object} [context={}] - Additional context
161
+ */
162
+ constructor(ideName, targetPath, reason, context = {}) {
163
+ super(`Cleanup failed for '${targetPath}': ${reason}`, ideName, {
164
+ targetPath,
165
+ reason,
166
+ ...context,
167
+ });
168
+ this.targetPath = targetPath;
169
+ this.reason = reason;
170
+ }
171
+ }
172
+
173
+ /**
174
+ * Thrown when IDE detection fails or returns unexpected results.
175
+ * Example: Multiple conflicting IDE configs found
176
+ */
177
+ class IdeDetectionError extends IdeError {
178
+ /**
179
+ * @param {string} ideName - Name of the IDE
180
+ * @param {string} reason - Why detection failed
181
+ * @param {Object} [context={}] - Additional context
182
+ */
183
+ constructor(ideName, reason, context = {}) {
184
+ super(`IDE detection failed: ${reason}`, ideName, {
185
+ reason,
186
+ ...context,
187
+ });
188
+ this.reason = reason;
189
+ }
190
+ }
191
+
192
+ /**
193
+ * Wrap a function call and convert EACCES/EPERM errors to FilePermissionError
194
+ * @param {string} ideName - Name of the IDE
195
+ * @param {string} filePath - Path being accessed
196
+ * @param {string} operation - Operation type ('read', 'write', 'delete')
197
+ * @param {Function} fn - Async function to wrap
198
+ * @returns {Promise<any>} Result of the function
199
+ * @throws {FilePermissionError} If permission error occurs
200
+ */
201
+ async function withPermissionHandling(ideName, filePath, operation, fn) {
202
+ try {
203
+ return await fn();
204
+ } catch (error) {
205
+ if (error.code === 'EACCES' || error.code === 'EPERM') {
206
+ throw new FilePermissionError(ideName, filePath, operation, {
207
+ originalError: error.message,
208
+ });
209
+ }
210
+ throw error;
211
+ }
212
+ }
213
+
214
+ /**
215
+ * Check if an error is an IDE-related error
216
+ * @param {Error} error - Error to check
217
+ * @returns {boolean}
218
+ */
219
+ function isIdeError(error) {
220
+ return error instanceof IdeError;
221
+ }
222
+
223
+ module.exports = {
224
+ IdeError,
225
+ IdeConfigNotFoundError,
226
+ CommandInstallationError,
227
+ FilePermissionError,
228
+ ContentInjectionError,
229
+ CleanupError,
230
+ IdeDetectionError,
231
+ withPermissionHandling,
232
+ isIdeError,
233
+ };