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.
- package/CHANGELOG.md +10 -0
- package/lib/file-cache.js +358 -0
- package/lib/progress.js +331 -0
- package/lib/validate.js +281 -1
- package/lib/yaml-utils.js +122 -0
- package/package.json +1 -3
- package/scripts/agileflow-welcome.js +19 -33
- package/scripts/damage-control-bash.js +2 -59
- package/scripts/damage-control-edit.js +4 -69
- package/scripts/damage-control-write.js +4 -69
- package/scripts/lib/damage-control-utils.js +153 -0
- package/scripts/obtain-context.js +4 -5
- package/tools/cli/installers/core/installer.js +32 -2
- package/tools/cli/installers/ide/_base-ide.js +143 -19
- package/tools/cli/installers/ide/claude-code.js +14 -51
- package/tools/cli/installers/ide/cursor.js +4 -40
- package/tools/cli/installers/ide/windsurf.js +6 -40
- package/tools/cli/lib/content-injector.js +37 -0
- package/tools/cli/lib/ide-errors.js +233 -0
|
@@ -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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
+
};
|