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.
@@ -15,85 +15,20 @@
15
15
  const {
16
16
  findProjectRoot,
17
17
  loadPatterns,
18
- pathMatches,
19
18
  outputBlocked,
20
19
  runDamageControlHook,
20
+ parsePathPatterns,
21
+ validatePathAgainstPatterns,
21
22
  } = require('./lib/damage-control-utils');
22
23
 
23
- /**
24
- * Parse simplified YAML for path patterns
25
- */
26
- function parseSimpleYAML(content) {
27
- const config = {
28
- zeroAccessPaths: [],
29
- readOnlyPaths: [],
30
- noDeletePaths: [],
31
- };
32
-
33
- let currentSection = null;
34
-
35
- for (const line of content.split('\n')) {
36
- const trimmed = line.trim();
37
-
38
- // Skip empty lines and comments
39
- if (!trimmed || trimmed.startsWith('#')) continue;
40
-
41
- // Detect section headers
42
- if (trimmed === 'zeroAccessPaths:') {
43
- currentSection = 'zeroAccessPaths';
44
- } else if (trimmed === 'readOnlyPaths:') {
45
- currentSection = 'readOnlyPaths';
46
- } else if (trimmed === 'noDeletePaths:') {
47
- currentSection = 'noDeletePaths';
48
- } else if (trimmed.endsWith(':') && !trimmed.startsWith('-')) {
49
- // Other sections we don't care about for path validation
50
- currentSection = null;
51
- } else if (trimmed.startsWith('- ') && currentSection && config[currentSection]) {
52
- // Path entry
53
- const pathValue = trimmed.slice(2).replace(/^["']|["']$/g, '');
54
- config[currentSection].push(pathValue);
55
- }
56
- }
57
-
58
- return config;
59
- }
60
-
61
- /**
62
- * Validate file path for write operation
63
- */
64
- function validatePath(filePath, config) {
65
- // Check zero access paths - completely blocked
66
- const zeroMatch = pathMatches(filePath, config.zeroAccessPaths || []);
67
- if (zeroMatch) {
68
- return {
69
- action: 'block',
70
- reason: `Zero-access path: ${zeroMatch}`,
71
- detail: 'This file is protected and cannot be accessed',
72
- };
73
- }
74
-
75
- // Check read-only paths - cannot write
76
- const readOnlyMatch = pathMatches(filePath, config.readOnlyPaths || []);
77
- if (readOnlyMatch) {
78
- return {
79
- action: 'block',
80
- reason: `Read-only path: ${readOnlyMatch}`,
81
- detail: 'This file is read-only and cannot be written to',
82
- };
83
- }
84
-
85
- // Allow by default
86
- return { action: 'allow' };
87
- }
88
-
89
24
  // Run the hook
90
25
  const projectRoot = findProjectRoot();
91
26
  const defaultConfig = { zeroAccessPaths: [], readOnlyPaths: [], noDeletePaths: [] };
92
27
 
93
28
  runDamageControlHook({
94
29
  getInputValue: input => input.file_path || input.tool_input?.file_path,
95
- loadConfig: () => loadPatterns(projectRoot, parseSimpleYAML, defaultConfig),
96
- validate: validatePath,
30
+ loadConfig: () => loadPatterns(projectRoot, parsePathPatterns, defaultConfig),
31
+ validate: (filePath, config) => validatePathAgainstPatterns(filePath, config, 'write'),
97
32
  onBlock: (result, filePath) => {
98
33
  outputBlocked(result.reason, result.detail, `File: ${filePath}`);
99
34
  },
@@ -238,6 +238,155 @@ function runDamageControlHook(options) {
238
238
  }, STDIN_TIMEOUT_MS);
239
239
  }
240
240
 
241
+ /**
242
+ * Parse simplified YAML for damage control patterns
243
+ * Handles both pattern-based sections (with pattern/reason/flags objects)
244
+ * and list-based sections (with simple string arrays)
245
+ *
246
+ * @param {string} content - YAML file content
247
+ * @param {object} sectionConfig - Map of section names to their type ('patterns' or 'list')
248
+ * @returns {object} Parsed configuration
249
+ *
250
+ * @example
251
+ * // For bash patterns (pattern objects):
252
+ * parseSimpleYAML(content, {
253
+ * bashToolPatterns: 'patterns',
254
+ * askPatterns: 'patterns',
255
+ * agileflowProtections: 'patterns',
256
+ * })
257
+ *
258
+ * @example
259
+ * // For path lists (string arrays):
260
+ * parseSimpleYAML(content, {
261
+ * zeroAccessPaths: 'list',
262
+ * readOnlyPaths: 'list',
263
+ * noDeletePaths: 'list',
264
+ * })
265
+ */
266
+ function parseSimpleYAML(content, sectionConfig) {
267
+ // Initialize result with empty arrays for each section
268
+ const config = {};
269
+ for (const section of Object.keys(sectionConfig)) {
270
+ config[section] = [];
271
+ }
272
+
273
+ let currentSection = null;
274
+ let currentPattern = null;
275
+
276
+ for (const line of content.split('\n')) {
277
+ const trimmed = line.trim();
278
+
279
+ // Skip empty lines and comments
280
+ if (!trimmed || trimmed.startsWith('#')) continue;
281
+
282
+ // Check if this line is a section header we care about
283
+ const sectionMatch = trimmed.match(/^(\w+):$/);
284
+ if (sectionMatch) {
285
+ const sectionName = sectionMatch[1];
286
+ if (sectionConfig[sectionName]) {
287
+ currentSection = sectionName;
288
+ currentPattern = null;
289
+ } else {
290
+ // Section we don't care about
291
+ currentSection = null;
292
+ currentPattern = null;
293
+ }
294
+ continue;
295
+ }
296
+
297
+ // Skip if we're not in a tracked section
298
+ if (!currentSection) continue;
299
+
300
+ const sectionType = sectionConfig[currentSection];
301
+
302
+ if (sectionType === 'patterns') {
303
+ // Pattern-based section (objects with pattern/reason/flags)
304
+ if (trimmed.startsWith('- pattern:')) {
305
+ const patternValue = trimmed
306
+ .replace('- pattern:', '')
307
+ .trim()
308
+ .replace(/^["']|["']$/g, '');
309
+ currentPattern = { pattern: patternValue };
310
+ config[currentSection].push(currentPattern);
311
+ } else if (trimmed.startsWith('reason:') && currentPattern) {
312
+ currentPattern.reason = trimmed
313
+ .replace('reason:', '')
314
+ .trim()
315
+ .replace(/^["']|["']$/g, '');
316
+ } else if (trimmed.startsWith('flags:') && currentPattern) {
317
+ currentPattern.flags = trimmed
318
+ .replace('flags:', '')
319
+ .trim()
320
+ .replace(/^["']|["']$/g, '');
321
+ }
322
+ } else if (sectionType === 'list') {
323
+ // List-based section (simple string arrays)
324
+ if (trimmed.startsWith('- ')) {
325
+ const value = trimmed.slice(2).replace(/^["']|["']$/g, '');
326
+ config[currentSection].push(value);
327
+ }
328
+ }
329
+ }
330
+
331
+ return config;
332
+ }
333
+
334
+ /**
335
+ * Pre-configured parser for bash tool patterns
336
+ */
337
+ function parseBashPatterns(content) {
338
+ return parseSimpleYAML(content, {
339
+ bashToolPatterns: 'patterns',
340
+ askPatterns: 'patterns',
341
+ agileflowProtections: 'patterns',
342
+ });
343
+ }
344
+
345
+ /**
346
+ * Pre-configured parser for path patterns (edit/write)
347
+ */
348
+ function parsePathPatterns(content) {
349
+ return parseSimpleYAML(content, {
350
+ zeroAccessPaths: 'list',
351
+ readOnlyPaths: 'list',
352
+ noDeletePaths: 'list',
353
+ });
354
+ }
355
+
356
+ /**
357
+ * Validate file path against path patterns
358
+ * Used by both edit and write hooks
359
+ *
360
+ * @param {string} filePath - File path to validate
361
+ * @param {object} config - Parsed path patterns config
362
+ * @param {string} operation - Operation type ('edit' or 'write') for error messages
363
+ * @returns {object} Validation result { action, reason?, detail? }
364
+ */
365
+ function validatePathAgainstPatterns(filePath, config, operation = 'access') {
366
+ // Check zero access paths - completely blocked
367
+ const zeroMatch = pathMatches(filePath, config.zeroAccessPaths || []);
368
+ if (zeroMatch) {
369
+ return {
370
+ action: 'block',
371
+ reason: `Zero-access path: ${zeroMatch}`,
372
+ detail: 'This file is protected and cannot be accessed',
373
+ };
374
+ }
375
+
376
+ // Check read-only paths - cannot edit/write
377
+ const readOnlyMatch = pathMatches(filePath, config.readOnlyPaths || []);
378
+ if (readOnlyMatch) {
379
+ return {
380
+ action: 'block',
381
+ reason: `Read-only path: ${readOnlyMatch}`,
382
+ detail: `This file is read-only and cannot be ${operation === 'edit' ? 'edited' : 'written to'}`,
383
+ };
384
+ }
385
+
386
+ // Allow by default
387
+ return { action: 'allow' };
388
+ }
389
+
241
390
  module.exports = {
242
391
  c,
243
392
  findProjectRoot,
@@ -246,6 +395,10 @@ module.exports = {
246
395
  pathMatches,
247
396
  outputBlocked,
248
397
  runDamageControlHook,
398
+ parseSimpleYAML,
399
+ parseBashPatterns,
400
+ parsePathPatterns,
401
+ validatePathAgainstPatterns,
249
402
  CONFIG_PATHS,
250
403
  STDIN_TIMEOUT_MS,
251
404
  };
@@ -20,6 +20,7 @@ const path = require('path');
20
20
  const { execSync } = require('child_process');
21
21
  const { c: C, box } = require('../lib/colors');
22
22
  const { isValidCommandName } = require('../lib/validate');
23
+ const { readJSONCached, readFileCached } = require('../lib/file-cache');
23
24
 
24
25
  // Claude Code's Bash tool truncates around 30K chars, but ANSI codes and
25
26
  // box-drawing characters (╭╮╰╯─│) are multi-byte UTF-8, so we need buffer.
@@ -131,11 +132,9 @@ function safeRead(filePath) {
131
132
  }
132
133
 
133
134
  function safeReadJSON(filePath) {
134
- try {
135
- return JSON.parse(fs.readFileSync(filePath, 'utf8'));
136
- } catch {
137
- return null;
138
- }
135
+ // Use cached read for common JSON files
136
+ const absPath = path.resolve(filePath);
137
+ return readJSONCached(absPath);
139
138
  }
140
139
 
141
140
  function safeLs(dirPath) {
@@ -11,6 +11,7 @@ const ora = require('ora');
11
11
  const yaml = require('js-yaml');
12
12
  const { injectContent } = require('../../lib/content-injector');
13
13
  const { sha256Hex, toPosixPath, safeTimestampForPath } = require('../../lib/utils');
14
+ const { validatePath, PathValidationError } = require('../../../../lib/validate');
14
15
 
15
16
  const TEXT_EXTENSIONS = new Set(['.md', '.yaml', '.yml', '.txt', '.json']);
16
17
 
@@ -191,6 +192,22 @@ class Installer {
191
192
  }
192
193
  }
193
194
 
195
+ /**
196
+ * Validate that a path is within the allowed installation directory.
197
+ * Prevents path traversal attacks when writing files.
198
+ *
199
+ * @param {string} filePath - Path to validate
200
+ * @param {string} baseDir - Allowed base directory
201
+ * @throws {PathValidationError} If path escapes base directory
202
+ */
203
+ validateInstallPath(filePath, baseDir) {
204
+ const result = validatePath(filePath, baseDir, { allowSymlinks: false });
205
+ if (!result.ok) {
206
+ throw result.error;
207
+ }
208
+ return result.resolvedPath;
209
+ }
210
+
194
211
  /**
195
212
  * Copy content from source to destination with placeholder replacement
196
213
  * @param {string} source - Source directory
@@ -201,10 +218,16 @@ class Installer {
201
218
  async copyContent(source, dest, agileflowFolder, policy = null) {
202
219
  const entries = await fs.readdir(source, { withFileTypes: true });
203
220
 
221
+ // Get base directory for validation (agileflowDir from policy or dest itself)
222
+ const baseDir = policy?.agileflowDir || dest;
223
+
204
224
  for (const entry of entries) {
205
225
  const srcPath = path.join(source, entry.name);
206
226
  const destPath = path.join(dest, entry.name);
207
227
 
228
+ // Validate destination path to prevent traversal attacks via malicious filenames
229
+ this.validateInstallPath(destPath, baseDir);
230
+
208
231
  if (entry.isDirectory()) {
209
232
  await fs.ensureDir(destPath);
210
233
  await this.copyContent(srcPath, destPath, agileflowFolder, policy);
@@ -688,18 +711,25 @@ class Installer {
688
711
  * @param {string} srcDir - Source directory
689
712
  * @param {string} destDir - Destination directory
690
713
  * @param {boolean} force - Overwrite existing files
714
+ * @param {string} [baseDir] - Base directory for path validation (defaults to destDir on first call)
691
715
  */
692
- async copyScriptsRecursive(srcDir, destDir, force) {
716
+ async copyScriptsRecursive(srcDir, destDir, force, baseDir = null) {
693
717
  const entries = await fs.readdir(srcDir, { withFileTypes: true });
694
718
 
719
+ // Use destDir as base for validation on first call
720
+ const validationBase = baseDir || destDir;
721
+
695
722
  for (const entry of entries) {
696
723
  const srcPath = path.join(srcDir, entry.name);
697
724
  const destPath = path.join(destDir, entry.name);
698
725
 
726
+ // Validate destination path to prevent traversal via malicious filenames
727
+ this.validateInstallPath(destPath, validationBase);
728
+
699
729
  if (entry.isDirectory()) {
700
730
  // Recursively copy subdirectories
701
731
  await fs.ensureDir(destPath);
702
- await this.copyScriptsRecursive(srcPath, destPath, force);
732
+ await this.copyScriptsRecursive(srcPath, destPath, force, validationBase);
703
733
  } else {
704
734
  // Copy file
705
735
  const destExists = await fs.pathExists(destPath);
@@ -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
- await fs.remove(agileflowPath);
113
- console.log(
114
- chalk.dim(` Removed old ${folderName} configuration from ${this.displayName}`)
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 fs.writeFile(filePath, content, 'utf8');
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 fs.readFile(filePath, 'utf8');
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
- await this.ensureDir(targetDir);
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
- // Read and process .md file
224
- let content = await this.readFile(sourcePath);
326
+ try {
327
+ // Read and process .md file
328
+ let content = await this.readFile(sourcePath);
225
329
 
226
- // Inject dynamic content if enabled (for top-level commands)
227
- if (injectDynamic) {
228
- content = this.injectDynamicContent(content, agileflowDir);
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
- // Replace docs/ references with custom folder name
232
- content = this.replaceDocsReferences(content);
343
+ // Replace docs/ references with custom folder name
344
+ content = this.replaceDocsReferences(content);
233
345
 
234
- await this.writeFile(targetPath, content);
235
- commandCount++;
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
- console.log(chalk.hex('#e8683a')(` Setting up ${this.displayName}...`));
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
- // Clean up old installation first
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
- const agentsTargetDir = path.join(agileflowCommandsDir, 'agents');
53
- const agentResult = await this.installCommandsRecursive(
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(claudeDir, 'agents', 'agileflow');
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 (.claude/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(claudeDir, 'skills');
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, claudeDir, options);
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
  /**