fix: adapt help-routes.ts to new command.json structure (fixes #81)

- Replace getIndexDir() with getCommandFilePath() to find command.json
- Update file watcher to monitor command.json instead of index/ directory
- Modify API routes to read from unified command.json structure
- Add buildWorkflowRelationships() to dynamically build workflow data from flow fields
- Add /api/help/agents endpoint for agents list
- Add category merge logic for frontend compatibility (cli includes general)
- Add cli-init command to command.json
This commit is contained in:
catlog22
2026-01-16 12:46:50 +08:00
parent dde39fc6f5
commit d7e5ee44cc
2 changed files with 181 additions and 60 deletions

View File

@@ -1,7 +1,7 @@
{ {
"_metadata": { "_metadata": {
"version": "2.0.0", "version": "2.0.0",
"total_commands": 88, "total_commands": 45,
"total_agents": 16, "total_agents": 16,
"description": "Unified CCW-Help command index" "description": "Unified CCW-Help command index"
}, },
@@ -485,6 +485,15 @@
"category": "general", "category": "general",
"difficulty": "Intermediate", "difficulty": "Intermediate",
"source": "../../../commands/enhance-prompt.md" "source": "../../../commands/enhance-prompt.md"
},
{
"name": "cli-init",
"command": "/cli:cli-init",
"description": "Initialize CLI tool configurations (.gemini/, .qwen/) with technology-aware ignore rules",
"arguments": "[--tool gemini|qwen|all] [--preview] [--output path]",
"category": "cli",
"difficulty": "Intermediate",
"source": "../../../commands/cli/cli-init.md"
} }
], ],

View File

@@ -8,23 +8,23 @@ import { homedir } from 'os';
import type { RouteContext } from './types.js'; import type { RouteContext } from './types.js';
/** /**
* Get the ccw-help index directory path (pure function) * Get the ccw-help command.json file path (pure function)
* Priority: project path (.claude/skills/ccw-help/index) > user path (~/.claude/skills/ccw-help/index) * Priority: project path (.claude/skills/ccw-help/command.json) > user path (~/.claude/skills/ccw-help/command.json)
* @param projectPath - The project path to check first * @param projectPath - The project path to check first
*/ */
function getIndexDir(projectPath: string | null): string | null { function getCommandFilePath(projectPath: string | null): string | null {
// Try project path first // Try project path first
if (projectPath) { if (projectPath) {
const projectIndexDir = join(projectPath, '.claude', 'skills', 'ccw-help', 'index'); const projectFilePath = join(projectPath, '.claude', 'skills', 'ccw-help', 'command.json');
if (existsSync(projectIndexDir)) { if (existsSync(projectFilePath)) {
return projectIndexDir; return projectFilePath;
} }
} }
// Fall back to user path // Fall back to user path
const userIndexDir = join(homedir(), '.claude', 'skills', 'ccw-help', 'index'); const userFilePath = join(homedir(), '.claude', 'skills', 'ccw-help', 'command.json');
if (existsSync(userIndexDir)) { if (existsSync(userFilePath)) {
return userIndexDir; return userFilePath;
} }
return null; return null;
@@ -83,46 +83,48 @@ function invalidateCache(key: string): void {
let watchersInitialized = false; let watchersInitialized = false;
/** /**
* Initialize file watchers for JSON indexes * Initialize file watcher for command.json
* @param projectPath - The project path to resolve index directory * @param projectPath - The project path to resolve command file
*/ */
function initializeFileWatchers(projectPath: string | null): void { function initializeFileWatchers(projectPath: string | null): void {
if (watchersInitialized) return; if (watchersInitialized) return;
const indexDir = getIndexDir(projectPath); const commandFilePath = getCommandFilePath(projectPath);
if (!indexDir) { if (!commandFilePath) {
console.warn(`ccw-help index directory not found in project or user paths`); console.warn(`ccw-help command.json not found in project or user paths`);
return; return;
} }
try { try {
// Watch all JSON files in index directory // Watch the command.json file
const watcher = watch(indexDir, { recursive: false }, (eventType, filename) => { const watcher = watch(commandFilePath, (eventType) => {
if (!filename || !filename.endsWith('.json')) return; console.log(`File change detected: command.json (${eventType})`);
console.log(`File change detected: ${filename} (${eventType})`); // Invalidate all cache entries when command.json changes
invalidateCache('command-data');
// Invalidate relevant cache entries
if (filename === 'all-commands.json') {
invalidateCache('all-commands');
} else if (filename === 'command-relationships.json') {
invalidateCache('command-relationships');
} else if (filename === 'by-category.json') {
invalidateCache('by-category');
}
}); });
watchersInitialized = true; watchersInitialized = true;
(watcher as any).unref?.(); (watcher as any).unref?.();
console.log(`File watchers initialized for: ${indexDir}`); console.log(`File watcher initialized for: ${commandFilePath}`);
} catch (error) { } catch (error) {
console.error('Failed to initialize file watchers:', error); console.error('Failed to initialize file watcher:', error);
} }
} }
// ========== Helper Functions ========== // ========== Helper Functions ==========
/**
* Get command data from command.json (with caching)
*/
function getCommandData(projectPath: string | null): any {
const filePath = getCommandFilePath(projectPath);
if (!filePath) return null;
return getCachedData('command-data', filePath);
}
/** /**
* Filter commands by search query * Filter commands by search query
*/ */
@@ -138,6 +140,15 @@ function filterCommands(commands: any[], query: string): any[] {
); );
} }
/**
* Category merge mapping for frontend compatibility
* Merges additional categories into target category for display
* Format: { targetCategory: [additionalCategoriesToMerge] }
*/
const CATEGORY_MERGES: Record<string, string[]> = {
'cli': ['general'], // CLI tab shows both 'cli' and 'general' commands
};
/** /**
* Group commands by category with subcategories * Group commands by category with subcategories
*/ */
@@ -166,9 +177,104 @@ function groupCommandsByCategory(commands: any[]): any {
} }
} }
// Apply category merges for frontend compatibility
for (const [target, sources] of Object.entries(CATEGORY_MERGES)) {
// Initialize target category if not exists
if (!grouped[target]) {
grouped[target] = {
name: target,
commands: [],
subcategories: {}
};
}
// Merge commands from source categories into target
for (const source of sources) {
if (grouped[source]) {
// Merge direct commands
grouped[target].commands = [
...grouped[target].commands,
...grouped[source].commands
];
// Merge subcategories
for (const [subcat, cmds] of Object.entries(grouped[source].subcategories)) {
if (!grouped[target].subcategories[subcat]) {
grouped[target].subcategories[subcat] = [];
}
grouped[target].subcategories[subcat] = [
...grouped[target].subcategories[subcat],
...(cmds as any[])
];
}
}
}
}
return grouped; return grouped;
} }
/**
* Build workflow relationships from command flow data
*/
function buildWorkflowRelationships(commands: any[]): any {
const relationships: any = {
workflows: [],
dependencies: {},
alternatives: {}
};
for (const cmd of commands) {
if (!cmd.flow) continue;
const cmdName = cmd.command;
// Build next_steps relationships
if (cmd.flow.next_steps) {
if (!relationships.dependencies[cmdName]) {
relationships.dependencies[cmdName] = { next: [], prev: [] };
}
relationships.dependencies[cmdName].next = cmd.flow.next_steps;
// Add reverse relationship
for (const nextCmd of cmd.flow.next_steps) {
if (!relationships.dependencies[nextCmd]) {
relationships.dependencies[nextCmd] = { next: [], prev: [] };
}
if (!relationships.dependencies[nextCmd].prev.includes(cmdName)) {
relationships.dependencies[nextCmd].prev.push(cmdName);
}
}
}
// Build prerequisites relationships
if (cmd.flow.prerequisites) {
if (!relationships.dependencies[cmdName]) {
relationships.dependencies[cmdName] = { next: [], prev: [] };
}
relationships.dependencies[cmdName].prev = [
...new Set([...relationships.dependencies[cmdName].prev, ...cmd.flow.prerequisites])
];
}
// Build alternatives
if (cmd.flow.alternatives) {
relationships.alternatives[cmdName] = cmd.flow.alternatives;
}
// Add to workflows list
if (cmd.category === 'workflow') {
relationships.workflows.push({
name: cmd.name,
command: cmd.command,
description: cmd.description,
flow: cmd.flow
});
}
}
return relationships;
}
// ========== API Routes ========== // ========== API Routes ==========
/** /**
@@ -181,25 +287,17 @@ export async function handleHelpRoutes(ctx: RouteContext): Promise<boolean> {
// Initialize file watchers on first request // Initialize file watchers on first request
initializeFileWatchers(initialPath); initializeFileWatchers(initialPath);
const indexDir = getIndexDir(initialPath);
// API: Get all commands with optional search // API: Get all commands with optional search
if (pathname === '/api/help/commands') { if (pathname === '/api/help/commands') {
if (!indexDir) { const commandData = getCommandData(initialPath);
if (!commandData) {
res.writeHead(404, { 'Content-Type': 'application/json' }); res.writeHead(404, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'ccw-help index directory not found' })); res.end(JSON.stringify({ error: 'ccw-help command.json not found' }));
return true; return true;
} }
const searchQuery = url.searchParams.get('q') || ''; const searchQuery = url.searchParams.get('q') || '';
const filePath = join(indexDir, 'all-commands.json'); let commands = commandData.commands || [];
let commands = getCachedData('all-commands', filePath);
if (!commands) {
res.writeHead(404, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Commands data not found' }));
return true;
}
// Filter by search query if provided // Filter by search query if provided
if (searchQuery) { if (searchQuery) {
@@ -213,26 +311,24 @@ export async function handleHelpRoutes(ctx: RouteContext): Promise<boolean> {
res.end(JSON.stringify({ res.end(JSON.stringify({
commands: commands, commands: commands,
grouped: grouped, grouped: grouped,
total: commands.length total: commands.length,
essential: commandData.essential_commands || [],
metadata: commandData._metadata
})); }));
return true; return true;
} }
// API: Get workflow command relationships // API: Get workflow command relationships
if (pathname === '/api/help/workflows') { if (pathname === '/api/help/workflows') {
if (!indexDir) { const commandData = getCommandData(initialPath);
if (!commandData) {
res.writeHead(404, { 'Content-Type': 'application/json' }); res.writeHead(404, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'ccw-help index directory not found' })); res.end(JSON.stringify({ error: 'ccw-help command.json not found' }));
return true; return true;
} }
const filePath = join(indexDir, 'command-relationships.json');
const relationships = getCachedData('command-relationships', filePath);
if (!relationships) { const commands = commandData.commands || [];
res.writeHead(404, { 'Content-Type': 'application/json' }); const relationships = buildWorkflowRelationships(commands);
res.end(JSON.stringify({ error: 'Workflow relationships not found' }));
return true;
}
res.writeHead(200, { 'Content-Type': 'application/json' }); res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(relationships)); res.end(JSON.stringify(relationships));
@@ -241,22 +337,38 @@ export async function handleHelpRoutes(ctx: RouteContext): Promise<boolean> {
// API: Get commands by category // API: Get commands by category
if (pathname === '/api/help/commands/by-category') { if (pathname === '/api/help/commands/by-category') {
if (!indexDir) { const commandData = getCommandData(initialPath);
if (!commandData) {
res.writeHead(404, { 'Content-Type': 'application/json' }); res.writeHead(404, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'ccw-help index directory not found' })); res.end(JSON.stringify({ error: 'ccw-help command.json not found' }));
return true; return true;
} }
const filePath = join(indexDir, 'by-category.json');
const byCategory = getCachedData('by-category', filePath);
if (!byCategory) { const commands = commandData.commands || [];
const byCategory = groupCommandsByCategory(commands);
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
categories: commandData.categories || [],
grouped: byCategory
}));
return true;
}
// API: Get agents list
if (pathname === '/api/help/agents') {
const commandData = getCommandData(initialPath);
if (!commandData) {
res.writeHead(404, { 'Content-Type': 'application/json' }); res.writeHead(404, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Category data not found' })); res.end(JSON.stringify({ error: 'ccw-help command.json not found' }));
return true; return true;
} }
res.writeHead(200, { 'Content-Type': 'application/json' }); res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(byCategory)); res.end(JSON.stringify({
agents: commandData.agents || [],
total: (commandData.agents || []).length
}));
return true; return true;
} }