mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-11 02:33:51 +08:00
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:
@@ -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"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user