agileflow 2.88.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 +5 -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 -1
- package/scripts/agileflow-welcome.js +19 -33
- 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,6 +7,14 @@
|
|
|
7
7
|
const path = require('node:path');
|
|
8
8
|
const fs = require('fs-extra');
|
|
9
9
|
const chalk = require('chalk');
|
|
10
|
+
const {
|
|
11
|
+
IdeConfigNotFoundError,
|
|
12
|
+
CommandInstallationError,
|
|
13
|
+
FilePermissionError,
|
|
14
|
+
CleanupError,
|
|
15
|
+
ContentInjectionError,
|
|
16
|
+
withPermissionHandling,
|
|
17
|
+
} = require('../../lib/ide-errors');
|
|
10
18
|
|
|
11
19
|
/**
|
|
12
20
|
* Base class for IDE-specific setup
|
|
@@ -99,9 +107,74 @@ class BaseIdeSetup {
|
|
|
99
107
|
throw new Error(`setup() must be implemented by ${this.name} handler`);
|
|
100
108
|
}
|
|
101
109
|
|
|
110
|
+
/**
|
|
111
|
+
* Standard setup flow shared by most IDE installers.
|
|
112
|
+
* Handles cleanup, command/agent installation, and logging.
|
|
113
|
+
*
|
|
114
|
+
* @param {string} projectDir - Project directory
|
|
115
|
+
* @param {string} agileflowDir - AgileFlow installation directory
|
|
116
|
+
* @param {Object} config - Configuration options
|
|
117
|
+
* @param {string} config.targetSubdir - Target subdirectory name under configDir (e.g., 'commands', 'workflows')
|
|
118
|
+
* @param {string} config.agileflowFolder - AgileFlow folder name (e.g., 'agileflow', 'AgileFlow')
|
|
119
|
+
* @param {string} [config.commandLabel='commands'] - Label for commands in output (e.g., 'workflows')
|
|
120
|
+
* @param {string} [config.agentLabel='agents'] - Label for agents in output
|
|
121
|
+
* @returns {Promise<{success: boolean, commands: number, agents: number}>}
|
|
122
|
+
*/
|
|
123
|
+
async setupStandard(projectDir, agileflowDir, config) {
|
|
124
|
+
const {
|
|
125
|
+
targetSubdir,
|
|
126
|
+
agileflowFolder,
|
|
127
|
+
commandLabel = 'commands',
|
|
128
|
+
agentLabel = 'agents',
|
|
129
|
+
} = config;
|
|
130
|
+
|
|
131
|
+
console.log(chalk.hex('#e8683a')(` Setting up ${this.displayName}...`));
|
|
132
|
+
|
|
133
|
+
// Clean up old installation first
|
|
134
|
+
await this.cleanup(projectDir);
|
|
135
|
+
|
|
136
|
+
// Create target directory (e.g., .cursor/commands/AgileFlow)
|
|
137
|
+
const ideDir = path.join(projectDir, this.configDir);
|
|
138
|
+
const targetDir = path.join(ideDir, targetSubdir);
|
|
139
|
+
const agileflowTargetDir = path.join(targetDir, agileflowFolder);
|
|
140
|
+
|
|
141
|
+
// Install commands using shared recursive method
|
|
142
|
+
const commandsSource = path.join(agileflowDir, 'commands');
|
|
143
|
+
const commandResult = await this.installCommandsRecursive(
|
|
144
|
+
commandsSource,
|
|
145
|
+
agileflowTargetDir,
|
|
146
|
+
agileflowDir,
|
|
147
|
+
true // Inject dynamic content
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
// Install agents as subdirectory
|
|
151
|
+
const agentsSource = path.join(agileflowDir, 'agents');
|
|
152
|
+
const agentsTargetDir = path.join(agileflowTargetDir, 'agents');
|
|
153
|
+
const agentResult = await this.installCommandsRecursive(
|
|
154
|
+
agentsSource,
|
|
155
|
+
agentsTargetDir,
|
|
156
|
+
agileflowDir,
|
|
157
|
+
false // No dynamic content for agents
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
console.log(chalk.green(` ✓ ${this.displayName} configured:`));
|
|
161
|
+
console.log(chalk.dim(` - ${commandResult.commands} ${commandLabel} installed`));
|
|
162
|
+
console.log(chalk.dim(` - ${agentResult.commands} ${agentLabel} installed`));
|
|
163
|
+
console.log(chalk.dim(` - Path: ${path.relative(projectDir, agileflowTargetDir)}`));
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
success: true,
|
|
167
|
+
commands: commandResult.commands,
|
|
168
|
+
agents: agentResult.commands,
|
|
169
|
+
ideDir,
|
|
170
|
+
agileflowTargetDir,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
102
174
|
/**
|
|
103
175
|
* Cleanup IDE configuration
|
|
104
176
|
* @param {string} projectDir - Project directory
|
|
177
|
+
* @throws {CleanupError} If cleanup fails
|
|
105
178
|
*/
|
|
106
179
|
async cleanup(projectDir) {
|
|
107
180
|
if (this.configDir) {
|
|
@@ -109,10 +182,25 @@ class BaseIdeSetup {
|
|
|
109
182
|
for (const folderName of ['agileflow', 'AgileFlow']) {
|
|
110
183
|
const agileflowPath = path.join(projectDir, this.configDir, 'commands', folderName);
|
|
111
184
|
if (await fs.pathExists(agileflowPath)) {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
185
|
+
try {
|
|
186
|
+
await fs.remove(agileflowPath);
|
|
187
|
+
console.log(
|
|
188
|
+
chalk.dim(` Removed old ${folderName} configuration from ${this.displayName}`)
|
|
189
|
+
);
|
|
190
|
+
} catch (error) {
|
|
191
|
+
if (error.code === 'EACCES' || error.code === 'EPERM') {
|
|
192
|
+
throw new CleanupError(
|
|
193
|
+
this.displayName,
|
|
194
|
+
agileflowPath,
|
|
195
|
+
`Permission denied: ${error.message}`
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
throw new CleanupError(
|
|
199
|
+
this.displayName,
|
|
200
|
+
agileflowPath,
|
|
201
|
+
error.message
|
|
202
|
+
);
|
|
203
|
+
}
|
|
116
204
|
}
|
|
117
205
|
}
|
|
118
206
|
}
|
|
@@ -140,21 +228,27 @@ class BaseIdeSetup {
|
|
|
140
228
|
}
|
|
141
229
|
|
|
142
230
|
/**
|
|
143
|
-
* Write a file
|
|
231
|
+
* Write a file with permission error handling
|
|
144
232
|
* @param {string} filePath - File path
|
|
145
233
|
* @param {string} content - File content
|
|
234
|
+
* @throws {FilePermissionError} If permission denied
|
|
146
235
|
*/
|
|
147
236
|
async writeFile(filePath, content) {
|
|
148
|
-
await
|
|
237
|
+
await withPermissionHandling(this.displayName, filePath, 'write', async () => {
|
|
238
|
+
await fs.writeFile(filePath, content, 'utf8');
|
|
239
|
+
});
|
|
149
240
|
}
|
|
150
241
|
|
|
151
242
|
/**
|
|
152
|
-
* Read a file
|
|
243
|
+
* Read a file with permission error handling
|
|
153
244
|
* @param {string} filePath - File path
|
|
154
245
|
* @returns {Promise<string>} File content
|
|
246
|
+
* @throws {FilePermissionError} If permission denied
|
|
155
247
|
*/
|
|
156
248
|
async readFile(filePath) {
|
|
157
|
-
return
|
|
249
|
+
return withPermissionHandling(this.displayName, filePath, 'read', async () => {
|
|
250
|
+
return fs.readFile(filePath, 'utf8');
|
|
251
|
+
});
|
|
158
252
|
}
|
|
159
253
|
|
|
160
254
|
/**
|
|
@@ -202,6 +296,8 @@ class BaseIdeSetup {
|
|
|
202
296
|
* @param {string} agileflowDir - AgileFlow installation directory (for dynamic content)
|
|
203
297
|
* @param {boolean} injectDynamic - Whether to inject dynamic content (only for top-level commands)
|
|
204
298
|
* @returns {Promise<{commands: number, subdirs: number}>} Count of installed items
|
|
299
|
+
* @throws {CommandInstallationError} If command installation fails
|
|
300
|
+
* @throws {FilePermissionError} If permission denied
|
|
205
301
|
*/
|
|
206
302
|
async installCommandsRecursive(sourceDir, targetDir, agileflowDir, injectDynamic = false) {
|
|
207
303
|
let commandCount = 0;
|
|
@@ -211,7 +307,14 @@ class BaseIdeSetup {
|
|
|
211
307
|
return { commands: 0, subdirs: 0 };
|
|
212
308
|
}
|
|
213
309
|
|
|
214
|
-
|
|
310
|
+
try {
|
|
311
|
+
await this.ensureDir(targetDir);
|
|
312
|
+
} catch (error) {
|
|
313
|
+
if (error.code === 'EACCES' || error.code === 'EPERM') {
|
|
314
|
+
throw new FilePermissionError(this.displayName, targetDir, 'write');
|
|
315
|
+
}
|
|
316
|
+
throw error;
|
|
317
|
+
}
|
|
215
318
|
|
|
216
319
|
const entries = await fs.readdir(sourceDir, { withFileTypes: true });
|
|
217
320
|
|
|
@@ -220,19 +323,40 @@ class BaseIdeSetup {
|
|
|
220
323
|
const targetPath = path.join(targetDir, entry.name);
|
|
221
324
|
|
|
222
325
|
if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
223
|
-
|
|
224
|
-
|
|
326
|
+
try {
|
|
327
|
+
// Read and process .md file
|
|
328
|
+
let content = await this.readFile(sourcePath);
|
|
225
329
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
330
|
+
// Inject dynamic content if enabled (for top-level commands)
|
|
331
|
+
if (injectDynamic) {
|
|
332
|
+
try {
|
|
333
|
+
content = this.injectDynamicContent(content, agileflowDir);
|
|
334
|
+
} catch (injectionError) {
|
|
335
|
+
throw new ContentInjectionError(
|
|
336
|
+
this.displayName,
|
|
337
|
+
sourcePath,
|
|
338
|
+
injectionError.message
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
230
342
|
|
|
231
|
-
|
|
232
|
-
|
|
343
|
+
// Replace docs/ references with custom folder name
|
|
344
|
+
content = this.replaceDocsReferences(content);
|
|
233
345
|
|
|
234
|
-
|
|
235
|
-
|
|
346
|
+
await this.writeFile(targetPath, content);
|
|
347
|
+
commandCount++;
|
|
348
|
+
} catch (error) {
|
|
349
|
+
// Re-throw typed errors as-is
|
|
350
|
+
if (error.name && error.name.includes('Error') && error.ideName) {
|
|
351
|
+
throw error;
|
|
352
|
+
}
|
|
353
|
+
throw new CommandInstallationError(
|
|
354
|
+
this.displayName,
|
|
355
|
+
entry.name,
|
|
356
|
+
error.message,
|
|
357
|
+
{ sourcePath, targetPath }
|
|
358
|
+
);
|
|
359
|
+
}
|
|
236
360
|
} else if (entry.isDirectory()) {
|
|
237
361
|
// Recursively process subdirectory
|
|
238
362
|
const subResult = await this.installCommandsRecursive(
|
|
@@ -26,68 +26,31 @@ class ClaudeCodeSetup extends BaseIdeSetup {
|
|
|
26
26
|
* @param {Object} options - Setup options
|
|
27
27
|
*/
|
|
28
28
|
async setup(projectDir, agileflowDir, options = {}) {
|
|
29
|
-
|
|
29
|
+
// Use standard setup for commands and agents
|
|
30
|
+
const result = await this.setupStandard(projectDir, agileflowDir, {
|
|
31
|
+
targetSubdir: this.commandsDir,
|
|
32
|
+
agileflowFolder: 'agileflow',
|
|
33
|
+
});
|
|
30
34
|
|
|
31
|
-
|
|
32
|
-
await this.cleanup(projectDir);
|
|
33
|
-
|
|
34
|
-
// Create .claude/commands/agileflow directory
|
|
35
|
-
const claudeDir = path.join(projectDir, this.configDir);
|
|
36
|
-
const commandsDir = path.join(claudeDir, this.commandsDir);
|
|
37
|
-
const agileflowCommandsDir = path.join(commandsDir, 'agileflow');
|
|
38
|
-
|
|
39
|
-
await this.ensureDir(agileflowCommandsDir);
|
|
40
|
-
|
|
41
|
-
// Recursively install all commands (including subdirectories like agents/, session/)
|
|
42
|
-
const commandsSource = path.join(agileflowDir, 'commands');
|
|
43
|
-
const commandResult = await this.installCommandsRecursive(
|
|
44
|
-
commandsSource,
|
|
45
|
-
agileflowCommandsDir,
|
|
46
|
-
agileflowDir,
|
|
47
|
-
true // Inject dynamic content for top-level commands
|
|
48
|
-
);
|
|
49
|
-
|
|
50
|
-
// Also install agents as slash commands (.claude/commands/agileflow/agents/)
|
|
35
|
+
const { ideDir, agileflowTargetDir } = result;
|
|
51
36
|
const agentsSource = path.join(agileflowDir, 'agents');
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
agentsSource,
|
|
55
|
-
agentsTargetDir,
|
|
56
|
-
agileflowDir,
|
|
57
|
-
false // No dynamic content for agents
|
|
58
|
-
);
|
|
59
|
-
|
|
60
|
-
// ALSO install agents as spawnable subagents (.claude/agents/agileflow/)
|
|
37
|
+
|
|
38
|
+
// Claude Code specific: Install agents as spawnable subagents (.claude/agents/agileflow/)
|
|
61
39
|
// This allows Task tool to spawn them with subagent_type: "agileflow-ui"
|
|
62
|
-
const spawnableAgentsDir = path.join(
|
|
40
|
+
const spawnableAgentsDir = path.join(ideDir, 'agents', 'agileflow');
|
|
63
41
|
await this.installCommandsRecursive(agentsSource, spawnableAgentsDir, agileflowDir, false);
|
|
64
42
|
console.log(chalk.dim(` - Spawnable agents: .claude/agents/agileflow/`));
|
|
65
43
|
|
|
66
|
-
// Create skills directory for user-generated skills
|
|
44
|
+
// Claude Code specific: Create skills directory for user-generated skills
|
|
67
45
|
// AgileFlow no longer ships static skills - users generate them via /agileflow:skill:create
|
|
68
|
-
const skillsTargetDir = path.join(
|
|
46
|
+
const skillsTargetDir = path.join(ideDir, 'skills');
|
|
69
47
|
await this.ensureDir(skillsTargetDir);
|
|
70
48
|
console.log(chalk.dim(` - Skills directory: .claude/skills/ (for user-generated skills)`));
|
|
71
49
|
|
|
72
|
-
// Setup damage control hooks
|
|
73
|
-
await this.setupDamageControl(projectDir, agileflowDir,
|
|
74
|
-
|
|
75
|
-
const totalCommands = commandResult.commands + agentResult.commands;
|
|
76
|
-
const totalSubdirs =
|
|
77
|
-
commandResult.subdirs + (agentResult.commands > 0 ? 1 : 0) + agentResult.subdirs;
|
|
78
|
-
|
|
79
|
-
console.log(chalk.green(` ✓ ${this.displayName} configured:`));
|
|
80
|
-
console.log(chalk.dim(` - ${totalCommands} commands installed`));
|
|
81
|
-
if (totalSubdirs > 0) {
|
|
82
|
-
console.log(chalk.dim(` - ${totalSubdirs} subdirectories`));
|
|
83
|
-
}
|
|
84
|
-
console.log(chalk.dim(` - Path: ${path.relative(projectDir, agileflowCommandsDir)}`));
|
|
50
|
+
// Claude Code specific: Setup damage control hooks
|
|
51
|
+
await this.setupDamageControl(projectDir, agileflowDir, ideDir, options);
|
|
85
52
|
|
|
86
|
-
return
|
|
87
|
-
success: true,
|
|
88
|
-
commands: totalCommands,
|
|
89
|
-
subdirs: totalSubdirs,
|
|
90
|
-
};
|
|
53
|
+
return result;
|
|
91
54
|
}
|
|
92
55
|
|
|
93
56
|
/**
|
|
@@ -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')}`;
|