agileflow 2.96.2 → 2.96.4
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
CHANGED
|
@@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [2.96.4] - 2026-02-06
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Command deduplication to fix duplicate slash commands in autocomplete
|
|
14
|
+
|
|
15
|
+
## [2.96.3] - 2026-02-06
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
- Codex CLI subcommand support for nested commands
|
|
19
|
+
|
|
10
20
|
## [2.96.2] - 2026-02-06
|
|
11
21
|
|
|
12
22
|
### Fixed
|
package/package.json
CHANGED
|
@@ -208,6 +208,69 @@ class BaseIdeSetup {
|
|
|
208
208
|
}
|
|
209
209
|
}
|
|
210
210
|
}
|
|
211
|
+
|
|
212
|
+
// Remove duplicate command files that exist outside the agileflow/ subfolder
|
|
213
|
+
// These cause "command 2" entries in Claude Code's autocomplete
|
|
214
|
+
await this.removeDuplicateCommands(projectDir);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Remove duplicate AgileFlow command files from the parent commands directory.
|
|
220
|
+
* When commands exist both in .claude/commands/agileflow/foo.md AND .claude/commands/foo.md,
|
|
221
|
+
* Claude Code shows them as "foo" and "foo 2" in autocomplete. This removes the loose copies.
|
|
222
|
+
* @param {string} projectDir - Project directory
|
|
223
|
+
*/
|
|
224
|
+
async removeDuplicateCommands(projectDir) {
|
|
225
|
+
const commandsDir = path.join(projectDir, this.configDir, 'commands');
|
|
226
|
+
const agileflowDir = path.join(commandsDir, 'agileflow');
|
|
227
|
+
|
|
228
|
+
if (!(await fs.pathExists(commandsDir))) return;
|
|
229
|
+
|
|
230
|
+
// Get list of AgileFlow command names (files and directories)
|
|
231
|
+
const agileflowNames = new Set();
|
|
232
|
+
if (await fs.pathExists(agileflowDir)) {
|
|
233
|
+
const entries = await fs.readdir(agileflowDir, { withFileTypes: true });
|
|
234
|
+
for (const entry of entries) {
|
|
235
|
+
agileflowNames.add(entry.name);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Also collect names from the source commands to catch pre-existing stale copies
|
|
240
|
+
const coreCommandsDir = path.join(projectDir, this.agileflowFolder, 'commands');
|
|
241
|
+
if (await fs.pathExists(coreCommandsDir)) {
|
|
242
|
+
const entries = await fs.readdir(coreCommandsDir, { withFileTypes: true });
|
|
243
|
+
for (const entry of entries) {
|
|
244
|
+
agileflowNames.add(entry.name);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (agileflowNames.size === 0) return;
|
|
249
|
+
|
|
250
|
+
// Scan the parent commands directory for loose duplicates
|
|
251
|
+
let removedCount = 0;
|
|
252
|
+
const entries = await fs.readdir(commandsDir, { withFileTypes: true });
|
|
253
|
+
|
|
254
|
+
for (const entry of entries) {
|
|
255
|
+
// Skip the agileflow subfolder itself and non-agileflow items
|
|
256
|
+
if (entry.name === 'agileflow' || entry.name === 'AgileFlow') continue;
|
|
257
|
+
|
|
258
|
+
// Check if this file/directory matches an AgileFlow command name
|
|
259
|
+
if (agileflowNames.has(entry.name)) {
|
|
260
|
+
const duplicatePath = path.join(commandsDir, entry.name);
|
|
261
|
+
try {
|
|
262
|
+
await fs.remove(duplicatePath);
|
|
263
|
+
removedCount++;
|
|
264
|
+
} catch {
|
|
265
|
+
// Best effort - don't fail setup over cleanup
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (removedCount > 0) {
|
|
271
|
+
console.log(
|
|
272
|
+
chalk.dim(` Removed ${removedCount} duplicate command(s) from ${this.displayName}`)
|
|
273
|
+
);
|
|
211
274
|
}
|
|
212
275
|
}
|
|
213
276
|
|
|
@@ -269,9 +332,10 @@ class BaseIdeSetup {
|
|
|
269
332
|
* Scan a directory for files with a specific extension
|
|
270
333
|
* @param {string} dirPath - Directory path
|
|
271
334
|
* @param {string} extension - File extension (e.g., '.md')
|
|
272
|
-
* @
|
|
335
|
+
* @param {string} [prefix=''] - Prefix for nested names (used for recursion)
|
|
336
|
+
* @returns {Promise<Array>} List of files with name, path, filename
|
|
273
337
|
*/
|
|
274
|
-
async scanDirectory(dirPath, extension) {
|
|
338
|
+
async scanDirectory(dirPath, extension, prefix = '') {
|
|
275
339
|
const results = [];
|
|
276
340
|
|
|
277
341
|
if (!(await this.exists(dirPath))) {
|
|
@@ -281,12 +345,23 @@ class BaseIdeSetup {
|
|
|
281
345
|
const entries = await fs.readdir(dirPath, { withFileTypes: true });
|
|
282
346
|
|
|
283
347
|
for (const entry of entries) {
|
|
348
|
+
const fullPath = path.join(dirPath, entry.name);
|
|
349
|
+
|
|
284
350
|
if (entry.isFile() && entry.name.endsWith(extension)) {
|
|
351
|
+
// Build name with prefix for subcommands (e.g., ideate-new from ideate/new.md)
|
|
352
|
+
const baseName = entry.name.replace(extension, '');
|
|
353
|
+
const fullName = prefix ? `${prefix}-${baseName}` : baseName;
|
|
354
|
+
|
|
285
355
|
results.push({
|
|
286
|
-
name:
|
|
287
|
-
path:
|
|
356
|
+
name: fullName,
|
|
357
|
+
path: fullPath,
|
|
288
358
|
filename: entry.name,
|
|
289
359
|
});
|
|
360
|
+
} else if (entry.isDirectory()) {
|
|
361
|
+
// Recursively scan subdirectories with prefix
|
|
362
|
+
const subPrefix = prefix ? `${prefix}-${entry.name}` : entry.name;
|
|
363
|
+
const subResults = await this.scanDirectory(fullPath, extension, subPrefix);
|
|
364
|
+
results.push(...subResults);
|
|
290
365
|
}
|
|
291
366
|
}
|
|
292
367
|
|
|
@@ -35,6 +35,11 @@ class ClaudeCodeSetup extends BaseIdeSetup {
|
|
|
35
35
|
const { ideDir, agileflowTargetDir } = result;
|
|
36
36
|
const agentsSource = path.join(agileflowDir, 'agents');
|
|
37
37
|
|
|
38
|
+
// Claude Code specific: Check for duplicates in user-level ~/.claude/commands/
|
|
39
|
+
// Commands in both ~/.claude/commands/ and .claude/commands/agileflow/ cause
|
|
40
|
+
// "command 2" entries in autocomplete
|
|
41
|
+
await this.removeUserLevelDuplicates(agileflowDir);
|
|
42
|
+
|
|
38
43
|
// Claude Code specific: Install agents as spawnable subagents (.claude/agents/agileflow/)
|
|
39
44
|
// This allows Task tool to spawn them with subagent_type: "agileflow-ui"
|
|
40
45
|
const spawnableAgentsDir = path.join(ideDir, 'agents', 'agileflow');
|
|
@@ -282,6 +287,71 @@ class ClaudeCodeSetup extends BaseIdeSetup {
|
|
|
282
287
|
await fs.writeFile(settingsPath, JSON.stringify(settings, null, 2));
|
|
283
288
|
console.log(chalk.dim(` - SessionStart hooks: welcome, archive, context-loader`));
|
|
284
289
|
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Remove AgileFlow command duplicates from user-level ~/.claude/commands/
|
|
293
|
+
* When the same command exists in both ~/.claude/commands/ and
|
|
294
|
+
* <project>/.claude/commands/agileflow/, Claude Code shows duplicates.
|
|
295
|
+
* @param {string} agileflowDir - AgileFlow installation directory (to get command names)
|
|
296
|
+
*/
|
|
297
|
+
async removeUserLevelDuplicates(agileflowDir) {
|
|
298
|
+
const os = require('os');
|
|
299
|
+
const userCommandsDir = path.join(os.homedir(), '.claude', 'commands');
|
|
300
|
+
|
|
301
|
+
if (!(await fs.pathExists(userCommandsDir))) return;
|
|
302
|
+
|
|
303
|
+
// Collect AgileFlow command names from source
|
|
304
|
+
const agileflowNames = new Set();
|
|
305
|
+
const commandsSource = path.join(agileflowDir, 'commands');
|
|
306
|
+
const agentsSource = path.join(agileflowDir, 'agents');
|
|
307
|
+
|
|
308
|
+
for (const sourceDir of [commandsSource, agentsSource]) {
|
|
309
|
+
if (await fs.pathExists(sourceDir)) {
|
|
310
|
+
const entries = await fs.readdir(sourceDir, { withFileTypes: true });
|
|
311
|
+
for (const entry of entries) {
|
|
312
|
+
agileflowNames.add(entry.name);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (agileflowNames.size === 0) return;
|
|
318
|
+
|
|
319
|
+
// Check for agileflow-related items in user-level commands
|
|
320
|
+
let removedCount = 0;
|
|
321
|
+
const entries = await fs.readdir(userCommandsDir, { withFileTypes: true });
|
|
322
|
+
|
|
323
|
+
for (const entry of entries) {
|
|
324
|
+
// Only remove items that match AgileFlow command names
|
|
325
|
+
if (agileflowNames.has(entry.name)) {
|
|
326
|
+
const duplicatePath = path.join(userCommandsDir, entry.name);
|
|
327
|
+
try {
|
|
328
|
+
await fs.remove(duplicatePath);
|
|
329
|
+
removedCount++;
|
|
330
|
+
} catch {
|
|
331
|
+
// Best effort - don't fail setup over cleanup
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Also remove any agileflow/ subfolder from user-level commands
|
|
337
|
+
for (const folderName of ['agileflow', 'AgileFlow']) {
|
|
338
|
+
const userAgileflowDir = path.join(userCommandsDir, folderName);
|
|
339
|
+
if (await fs.pathExists(userAgileflowDir)) {
|
|
340
|
+
try {
|
|
341
|
+
await fs.remove(userAgileflowDir);
|
|
342
|
+
removedCount++;
|
|
343
|
+
} catch {
|
|
344
|
+
// Best effort
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (removedCount > 0) {
|
|
350
|
+
console.log(
|
|
351
|
+
chalk.dim(` - Removed ${removedCount} duplicate(s) from ~/.claude/commands/`)
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
285
355
|
}
|
|
286
356
|
|
|
287
357
|
module.exports = { ClaudeCodeSetup };
|