claude-code-workflow 6.3.48 → 6.3.49
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/.claude/CLAUDE.md +6 -8
- package/.claude/agents/action-planning-agent.md +28 -45
- package/.claude/agents/cli-lite-planning-agent.md +93 -1
- package/.claude/agents/code-developer.md +144 -27
- package/.claude/commands/ccw-coordinator.md +175 -21
- package/.claude/commands/ccw-debug.md +832 -0
- package/.claude/commands/ccw.md +90 -9
- package/.claude/commands/cli/cli-init.md +1 -0
- package/.claude/commands/issue/convert-to-plan.md +718 -0
- package/.claude/commands/issue/from-brainstorm.md +382 -0
- package/.claude/commands/memory/tips.md +332 -0
- package/.claude/commands/workflow/analyze-with-file.md +804 -0
- package/.claude/commands/workflow/brainstorm/auto-parallel.md +18 -43
- package/.claude/commands/workflow/brainstorm/role-analysis.md +705 -0
- package/.claude/commands/workflow/brainstorm-with-file.md +1153 -0
- package/.claude/commands/workflow/debug-with-file.md +7 -5
- package/.claude/commands/workflow/execute.md +6 -4
- package/.claude/commands/workflow/lite-plan.md +2 -2
- package/.claude/commands/workflow/plan-verify.md +162 -327
- package/.claude/commands/workflow/plan.md +162 -26
- package/.claude/commands/workflow/replan.md +78 -2
- package/.claude/commands/workflow/{review-fix.md → review-cycle-fix.md} +6 -6
- package/.claude/commands/workflow/review-module-cycle.md +2 -2
- package/.claude/commands/workflow/review-session-cycle.md +2 -2
- package/.claude/commands/workflow/tools/conflict-resolution.md +16 -26
- package/.claude/commands/workflow/tools/context-gather.md +81 -118
- package/.claude/commands/workflow/tools/task-generate-agent.md +94 -10
- package/.claude/skills/ccw-help/command.json +4 -4
- package/.claude/skills/lite-skill-generator/SKILL.md +650 -0
- package/.claude/skills/lite-skill-generator/templates/simple-skill.md +68 -0
- package/.claude/skills/lite-skill-generator/templates/style-guide.md +64 -0
- package/.claude/skills/skill-generator/SKILL.md +277 -85
- package/.claude/skills/skill-generator/phases/01-requirements-discovery.md +4 -15
- package/.claude/skills/skill-generator/phases/02-structure-generation.md +72 -17
- package/.claude/skills/skill-generator/phases/03-phase-generation.md +218 -51
- package/.claude/skills/skill-generator/phases/04-specs-templates.md +111 -41
- package/.claude/skills/skill-generator/phases/05-validation.md +139 -56
- package/.claude/skills/skill-generator/templates/autonomous-action.md +78 -268
- package/.claude/skills/skill-generator/templates/autonomous-orchestrator.md +14 -0
- package/.claude/skills/skill-generator/templates/code-analysis-action.md +12 -0
- package/.claude/skills/skill-generator/templates/llm-action.md +12 -0
- package/.claude/skills/skill-generator/templates/script-template.md +368 -0
- package/.claude/skills/skill-generator/templates/sequential-phase.md +14 -0
- package/.claude/skills/skill-generator/templates/skill-md.md +14 -0
- package/.claude/skills/skill-tuning/SKILL.md +130 -266
- package/.claude/skills/skill-tuning/phases/orchestrator.md +95 -283
- package/.claude/skills/skill-tuning/specs/problem-taxonomy.md +90 -198
- package/.claude/skills/skill-tuning/specs/tuning-strategies.md +193 -1345
- package/.claude/workflows/cli-templates/schemas/plan-verify-agent-schema.json +47 -0
- package/.claude/workflows/cli-templates/schemas/verify-json-schema.json +158 -0
- package/.claude/workflows/cli-tools-usage.md +1 -1
- package/.codex/AGENTS.md +1 -3
- package/.codex/prompts/analyze-with-file.md +607 -0
- package/.codex/prompts/brainstorm-to-cycle.md +455 -0
- package/.codex/prompts/brainstorm-with-file.md +933 -0
- package/.codex/prompts/debug-with-file.md +15 -20
- package/.codex/skills/ccw-cli-tools/SKILL.md +559 -0
- package/ccw/dist/commands/cli.d.ts.map +1 -1
- package/ccw/dist/commands/cli.js +29 -5
- package/ccw/dist/commands/cli.js.map +1 -1
- package/ccw/dist/commands/issue.d.ts +2 -0
- package/ccw/dist/commands/issue.d.ts.map +1 -1
- package/ccw/dist/commands/issue.js +62 -20
- package/ccw/dist/commands/issue.js.map +1 -1
- package/ccw/dist/config/litellm-api-config-manager.d.ts.map +1 -1
- package/ccw/dist/config/litellm-api-config-manager.js +5 -3
- package/ccw/dist/config/litellm-api-config-manager.js.map +1 -1
- package/ccw/dist/config/litellm-provider-models.d.ts +73 -0
- package/ccw/dist/config/litellm-provider-models.d.ts.map +1 -0
- package/ccw/dist/config/litellm-provider-models.js +172 -0
- package/ccw/dist/config/litellm-provider-models.js.map +1 -0
- package/ccw/dist/config/provider-models.d.ts +25 -51
- package/ccw/dist/config/provider-models.d.ts.map +1 -1
- package/ccw/dist/config/provider-models.js +84 -149
- package/ccw/dist/config/provider-models.js.map +1 -1
- package/ccw/dist/config/storage-paths.d.ts.map +1 -1
- package/ccw/dist/config/storage-paths.js +23 -5
- package/ccw/dist/config/storage-paths.js.map +1 -1
- package/ccw/dist/core/auth/csrf-middleware.js +3 -3
- package/ccw/dist/core/auth/csrf-middleware.js.map +1 -1
- package/ccw/dist/core/dashboard-generator.d.ts.map +1 -1
- package/ccw/dist/core/dashboard-generator.js +3 -1
- package/ccw/dist/core/dashboard-generator.js.map +1 -1
- package/ccw/dist/core/routes/claude-routes.d.ts.map +1 -1
- package/ccw/dist/core/routes/claude-routes.js +206 -14
- package/ccw/dist/core/routes/claude-routes.js.map +1 -1
- package/ccw/dist/core/routes/cli-routes.d.ts.map +1 -1
- package/ccw/dist/core/routes/cli-routes.js.map +1 -1
- package/ccw/dist/core/routes/commands-routes.d.ts +7 -0
- package/ccw/dist/core/routes/commands-routes.d.ts.map +1 -0
- package/ccw/dist/core/routes/commands-routes.js +480 -0
- package/ccw/dist/core/routes/commands-routes.js.map +1 -0
- package/ccw/dist/core/routes/model-routes.d.ts +11 -0
- package/ccw/dist/core/routes/model-routes.d.ts.map +1 -0
- package/ccw/dist/core/routes/model-routes.js +112 -0
- package/ccw/dist/core/routes/model-routes.js.map +1 -0
- package/ccw/dist/core/routes/nav-status-routes.d.ts.map +1 -1
- package/ccw/dist/core/routes/nav-status-routes.js +84 -1
- package/ccw/dist/core/routes/nav-status-routes.js.map +1 -1
- package/ccw/dist/core/routes/provider-routes.d.ts +11 -0
- package/ccw/dist/core/routes/provider-routes.d.ts.map +1 -0
- package/ccw/dist/core/routes/provider-routes.js +67 -0
- package/ccw/dist/core/routes/provider-routes.js.map +1 -0
- package/ccw/dist/core/routes/skills-routes.d.ts.map +1 -1
- package/ccw/dist/core/routes/skills-routes.js +219 -7
- package/ccw/dist/core/routes/skills-routes.js.map +1 -1
- package/ccw/dist/core/routes/system-routes.d.ts.map +1 -1
- package/ccw/dist/core/routes/system-routes.js +58 -6
- package/ccw/dist/core/routes/system-routes.js.map +1 -1
- package/ccw/dist/core/server.d.ts.map +1 -1
- package/ccw/dist/core/server.js +13 -0
- package/ccw/dist/core/server.js.map +1 -1
- package/ccw/dist/mcp-server/index.js +2 -2
- package/ccw/dist/mcp-server/index.js.map +1 -1
- package/ccw/dist/tools/claude-cli-tools.d.ts +48 -11
- package/ccw/dist/tools/claude-cli-tools.d.ts.map +1 -1
- package/ccw/dist/tools/claude-cli-tools.js +146 -50
- package/ccw/dist/tools/claude-cli-tools.js.map +1 -1
- package/ccw/dist/tools/cli-config-manager.d.ts +1 -13
- package/ccw/dist/tools/cli-config-manager.d.ts.map +1 -1
- package/ccw/dist/tools/cli-config-manager.js +3 -27
- package/ccw/dist/tools/cli-config-manager.js.map +1 -1
- package/ccw/dist/tools/cli-executor-core.d.ts.map +1 -1
- package/ccw/dist/tools/cli-executor-core.js +7 -2
- package/ccw/dist/tools/cli-executor-core.js.map +1 -1
- package/ccw/dist/tools/cli-executor-state.d.ts.map +1 -1
- package/ccw/dist/tools/cli-history-store.d.ts +11 -0
- package/ccw/dist/tools/cli-history-store.d.ts.map +1 -1
- package/ccw/dist/tools/cli-history-store.js +82 -2
- package/ccw/dist/tools/cli-history-store.js.map +1 -1
- package/ccw/dist/tools/command-registry.d.ts +7 -0
- package/ccw/dist/tools/command-registry.d.ts.map +1 -1
- package/ccw/dist/tools/command-registry.js +14 -1
- package/ccw/dist/tools/command-registry.js.map +1 -1
- package/ccw/dist/tools/generate-module-docs.d.ts.map +1 -1
- package/ccw/dist/tools/generate-module-docs.js +11 -7
- package/ccw/dist/tools/generate-module-docs.js.map +1 -1
- package/ccw/dist/tools/litellm-executor.d.ts +1 -0
- package/ccw/dist/tools/litellm-executor.d.ts.map +1 -1
- package/ccw/dist/tools/litellm-executor.js +11 -9
- package/ccw/dist/tools/litellm-executor.js.map +1 -1
- package/ccw/dist/types/skill-types.d.ts +97 -0
- package/ccw/dist/types/skill-types.d.ts.map +1 -0
- package/ccw/dist/types/skill-types.js +6 -0
- package/ccw/dist/types/skill-types.js.map +1 -0
- package/ccw/src/commands/cli.ts +36 -5
- package/ccw/src/commands/issue.ts +81 -26
- package/ccw/src/config/litellm-api-config-manager.ts +5 -3
- package/ccw/src/config/litellm-provider-models.ts +222 -0
- package/ccw/src/config/provider-models.ts +91 -190
- package/ccw/src/config/storage-paths.ts +20 -5
- package/ccw/src/core/auth/csrf-middleware.ts +3 -3
- package/ccw/src/core/dashboard-generator.ts +3 -1
- package/ccw/src/core/routes/claude-routes.ts +233 -15
- package/ccw/src/core/routes/cli-routes.ts +2 -3
- package/ccw/src/core/routes/commands-routes.ts +620 -0
- package/ccw/src/core/routes/nav-status-routes.ts +95 -1
- package/ccw/src/core/routes/provider-routes.ts +78 -0
- package/ccw/src/core/routes/skills-routes.ts +266 -45
- package/ccw/src/core/routes/system-routes.ts +102 -50
- package/ccw/src/core/server.ts +13 -0
- package/ccw/src/mcp-server/index.ts +2 -2
- package/ccw/src/templates/dashboard-css/18-cli-settings.css +35 -0
- package/ccw/src/templates/dashboard-css/37-commands.css +193 -0
- package/ccw/src/templates/dashboard-js/components/navigation.js +4 -0
- package/ccw/src/templates/dashboard-js/i18n.js +116 -0
- package/ccw/src/templates/dashboard-js/views/cli-manager.js +249 -4
- package/ccw/src/templates/dashboard-js/views/commands-manager.js +503 -0
- package/ccw/src/templates/dashboard-js/views/issue-manager.js +7 -7
- package/ccw/src/templates/dashboard-js/views/mcp-manager.js +2 -7
- package/ccw/src/templates/dashboard-js/views/skills-manager.js +164 -23
- package/ccw/src/templates/dashboard.html +7 -0
- package/ccw/src/tools/claude-cli-tools.ts +170 -56
- package/ccw/src/tools/cli-config-manager.ts +2 -33
- package/ccw/src/tools/cli-executor-core.ts +8 -2
- package/ccw/src/tools/cli-history-store.ts +92 -2
- package/ccw/src/tools/command-registry.ts +16 -1
- package/ccw/src/tools/generate-module-docs.ts +11 -7
- package/ccw/src/tools/litellm-executor.ts +13 -9
- package/ccw/src/types/skill-types.ts +99 -0
- package/package.json +1 -1
- package/.claude/commands/enhance-prompt.md +0 -93
- package/.claude/commands/memory/code-map-memory.md +0 -687
- package/.claude/commands/memory/docs.md +0 -615
- package/.claude/commands/memory/load-skill-memory.md +0 -182
- package/.claude/commands/memory/skill-memory.md +0 -525
- package/.claude/commands/memory/swagger-docs.md +0 -773
- package/.claude/commands/memory/tech-research-rules.md +0 -310
- package/.claude/commands/memory/workflow-skill-memory.md +0 -517
- package/.claude/commands/task/breakdown.md +0 -208
- package/.claude/commands/task/create.md +0 -152
- package/.claude/commands/task/execute.md +0 -270
- package/.claude/commands/task/replan.md +0 -441
- package/.claude/commands/version.md +0 -254
- package/.claude/commands/workflow/action-plan-verify.md +0 -485
- package/.claude/commands/workflow/brainstorm/api-designer.md +0 -587
- package/.claude/commands/workflow/brainstorm/data-architect.md +0 -220
- package/.claude/commands/workflow/brainstorm/product-manager.md +0 -200
- package/.claude/commands/workflow/brainstorm/product-owner.md +0 -200
- package/.claude/commands/workflow/brainstorm/scrum-master.md +0 -200
- package/.claude/commands/workflow/brainstorm/subject-matter-expert.md +0 -200
- package/.claude/commands/workflow/brainstorm/system-architect.md +0 -389
- package/.claude/commands/workflow/brainstorm/ui-designer.md +0 -221
- package/.claude/commands/workflow/brainstorm/ux-expert.md +0 -221
- package/.claude/commands/workflow/debug.md +0 -331
- package/.claude/commands/workflow/develop-with-file.md +0 -1044
- package/.claude/skills/ccw-loop/README.md +0 -303
- package/.claude/skills/skill-generator/templates/script-bash.md +0 -277
- package/.claude/skills/skill-generator/templates/script-python.md +0 -198
- package/.codex/prompts/debug.md +0 -318
- package/ccw/src/core/routes/mcp-routes.ts.backup +0 -549
|
@@ -0,0 +1,620 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Commands Routes Module
|
|
3
|
+
* Handles all Commands-related API endpoints
|
|
4
|
+
*
|
|
5
|
+
* API Endpoints:
|
|
6
|
+
* - GET /api/commands - List all commands with groups
|
|
7
|
+
* - POST /api/commands/:name/toggle - Enable/disable single command
|
|
8
|
+
* - POST /api/commands/group/:groupName/toggle - Batch toggle commands by group
|
|
9
|
+
*/
|
|
10
|
+
import { existsSync, readdirSync, readFileSync, mkdirSync, renameSync } from 'fs';
|
|
11
|
+
import { join, relative, dirname, basename } from 'path';
|
|
12
|
+
import { homedir } from 'os';
|
|
13
|
+
import { validatePath as validateAllowedPath } from '../../utils/path-validator.js';
|
|
14
|
+
import type { RouteContext } from './types.js';
|
|
15
|
+
|
|
16
|
+
// ========== Types ==========
|
|
17
|
+
|
|
18
|
+
type CommandLocation = 'project' | 'user';
|
|
19
|
+
|
|
20
|
+
interface CommandMetadata {
|
|
21
|
+
name: string;
|
|
22
|
+
description: string;
|
|
23
|
+
group: string;
|
|
24
|
+
argumentHint?: string;
|
|
25
|
+
allowedTools?: string[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface CommandInfo {
|
|
29
|
+
name: string;
|
|
30
|
+
description: string;
|
|
31
|
+
group: string;
|
|
32
|
+
enabled: boolean;
|
|
33
|
+
location: CommandLocation;
|
|
34
|
+
path: string;
|
|
35
|
+
relativePath: string; // Path relative to commands root (e.g., 'workflow/plan.md')
|
|
36
|
+
argumentHint?: string;
|
|
37
|
+
allowedTools?: string[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface CommandsConfig {
|
|
41
|
+
projectCommands: CommandInfo[];
|
|
42
|
+
userCommands: CommandInfo[];
|
|
43
|
+
groups: string[];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface CommandOperationResult {
|
|
47
|
+
success: boolean;
|
|
48
|
+
message: string;
|
|
49
|
+
commandName?: string;
|
|
50
|
+
location?: CommandLocation;
|
|
51
|
+
status?: number;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
interface GroupDefinition {
|
|
55
|
+
name: string;
|
|
56
|
+
icon?: string;
|
|
57
|
+
color?: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
interface CommandGroupsConfig {
|
|
61
|
+
groups: Record<string, GroupDefinition>; // Custom group definitions
|
|
62
|
+
assignments: Record<string, string>; // commandName -> groupId mapping
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ========== Helper Functions ==========
|
|
66
|
+
|
|
67
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
68
|
+
return typeof value === 'object' && value !== null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Get commands directory path
|
|
73
|
+
*/
|
|
74
|
+
function getCommandsDir(location: CommandLocation, projectPath: string): string {
|
|
75
|
+
if (location === 'project') {
|
|
76
|
+
return join(projectPath, '.claude', 'commands');
|
|
77
|
+
}
|
|
78
|
+
return join(homedir(), '.claude', 'commands');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Parse YAML frontmatter from command file
|
|
84
|
+
*/
|
|
85
|
+
function parseCommandFrontmatter(content: string): CommandMetadata {
|
|
86
|
+
const result: CommandMetadata = {
|
|
87
|
+
name: '',
|
|
88
|
+
description: '',
|
|
89
|
+
group: 'other' // Default group
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
// Check for YAML frontmatter
|
|
93
|
+
if (content.startsWith('---')) {
|
|
94
|
+
const endIndex = content.indexOf('---', 3);
|
|
95
|
+
if (endIndex > 0) {
|
|
96
|
+
const frontmatter = content.substring(3, endIndex).trim();
|
|
97
|
+
|
|
98
|
+
// Parse frontmatter lines
|
|
99
|
+
const lines = frontmatter.split(/[\r\n]+/);
|
|
100
|
+
for (const line of lines) {
|
|
101
|
+
const colonIndex = line.indexOf(':');
|
|
102
|
+
if (colonIndex > 0) {
|
|
103
|
+
const key = line.substring(0, colonIndex).trim().toLowerCase();
|
|
104
|
+
const value = line.substring(colonIndex + 1).trim().replace(/^["']|["']$/g, '');
|
|
105
|
+
|
|
106
|
+
if (key === 'name') {
|
|
107
|
+
result.name = value;
|
|
108
|
+
} else if (key === 'description') {
|
|
109
|
+
result.description = value;
|
|
110
|
+
} else if (key === 'group') {
|
|
111
|
+
result.group = value || 'other';
|
|
112
|
+
} else if (key === 'argument-hint') {
|
|
113
|
+
result.argumentHint = value;
|
|
114
|
+
} else if (key === 'allowed-tools') {
|
|
115
|
+
result.allowedTools = value
|
|
116
|
+
.replace(/^\[|\]$/g, '')
|
|
117
|
+
.split(',')
|
|
118
|
+
.map(t => t.trim())
|
|
119
|
+
.filter(Boolean);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return result;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Get command groups config file path
|
|
131
|
+
*/
|
|
132
|
+
function getGroupsConfigPath(location: CommandLocation, projectPath: string): string {
|
|
133
|
+
const baseDir = location === 'project'
|
|
134
|
+
? join(projectPath, '.claude')
|
|
135
|
+
: join(homedir(), '.claude');
|
|
136
|
+
return join(baseDir, 'command-groups.json');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Load command groups configuration
|
|
141
|
+
*/
|
|
142
|
+
function loadGroupsConfig(location: CommandLocation, projectPath: string): CommandGroupsConfig {
|
|
143
|
+
const configPath = getGroupsConfigPath(location, projectPath);
|
|
144
|
+
|
|
145
|
+
const defaultConfig: CommandGroupsConfig = {
|
|
146
|
+
groups: {},
|
|
147
|
+
assignments: {}
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
if (!existsSync(configPath)) {
|
|
151
|
+
return defaultConfig;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
try {
|
|
155
|
+
const content = readFileSync(configPath, 'utf8');
|
|
156
|
+
const parsed = JSON.parse(content);
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
groups: isRecord(parsed.groups) ? parsed.groups as Record<string, GroupDefinition> : {},
|
|
160
|
+
assignments: isRecord(parsed.assignments) ? parsed.assignments as Record<string, string> : {}
|
|
161
|
+
};
|
|
162
|
+
} catch (err) {
|
|
163
|
+
console.error(`[Commands] Failed to load groups config from ${configPath}:`, err);
|
|
164
|
+
return defaultConfig;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Save command groups configuration
|
|
170
|
+
*/
|
|
171
|
+
function saveGroupsConfig(location: CommandLocation, projectPath: string, config: CommandGroupsConfig): void {
|
|
172
|
+
const configPath = getGroupsConfigPath(location, projectPath);
|
|
173
|
+
const configDir = dirname(configPath);
|
|
174
|
+
|
|
175
|
+
if (!existsSync(configDir)) {
|
|
176
|
+
mkdirSync(configDir, { recursive: true });
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
try {
|
|
180
|
+
const content = JSON.stringify(config, null, 2);
|
|
181
|
+
require('fs').writeFileSync(configPath, content, 'utf8');
|
|
182
|
+
} catch (err) {
|
|
183
|
+
console.error(`[Commands] Failed to save groups config to ${configPath}:`, err);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Get group for a command (from config or inferred from path)
|
|
189
|
+
*/
|
|
190
|
+
function getCommandGroup(commandName: string, relativePath: string, location: CommandLocation, projectPath: string): string {
|
|
191
|
+
// First check custom assignments
|
|
192
|
+
const config = loadGroupsConfig(location, projectPath);
|
|
193
|
+
if (config.assignments[commandName]) {
|
|
194
|
+
return config.assignments[commandName];
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Fallback to path-based inference - use full directory path as group
|
|
198
|
+
const parts = relativePath.split(/[/\\]/);
|
|
199
|
+
if (parts.length > 1) {
|
|
200
|
+
// Use full directory path (excluding filename) as group
|
|
201
|
+
// e.g., 'workflow/review/code-review.md' -> 'workflow/review'
|
|
202
|
+
return parts.slice(0, -1).join('/');
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return 'other';
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Recursively scan directory for command files
|
|
210
|
+
*/
|
|
211
|
+
function scanCommandsRecursive(
|
|
212
|
+
baseDir: string,
|
|
213
|
+
currentDir: string,
|
|
214
|
+
location: CommandLocation,
|
|
215
|
+
projectPath: string
|
|
216
|
+
): CommandInfo[] {
|
|
217
|
+
const results: CommandInfo[] = [];
|
|
218
|
+
|
|
219
|
+
if (!existsSync(currentDir)) {
|
|
220
|
+
return results;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
try {
|
|
224
|
+
const entries = readdirSync(currentDir, { withFileTypes: true });
|
|
225
|
+
|
|
226
|
+
for (const entry of entries) {
|
|
227
|
+
const fullPath = join(currentDir, entry.name);
|
|
228
|
+
let relativePath = relative(baseDir, fullPath);
|
|
229
|
+
|
|
230
|
+
if (entry.isDirectory()) {
|
|
231
|
+
// Recursively scan subdirectories
|
|
232
|
+
results.push(...scanCommandsRecursive(baseDir, fullPath, location, projectPath));
|
|
233
|
+
} else if (entry.isFile()) {
|
|
234
|
+
// Check for .md or .md.disabled files
|
|
235
|
+
const isEnabled = entry.name.endsWith('.md') && !entry.name.endsWith('.md.disabled');
|
|
236
|
+
const isDisabled = entry.name.endsWith('.md.disabled');
|
|
237
|
+
|
|
238
|
+
if (isEnabled || isDisabled) {
|
|
239
|
+
try {
|
|
240
|
+
const content = readFileSync(fullPath, 'utf8');
|
|
241
|
+
const metadata = parseCommandFrontmatter(content);
|
|
242
|
+
|
|
243
|
+
// For disabled files, remove .disabled from relativePath for consistency
|
|
244
|
+
if (isDisabled) {
|
|
245
|
+
relativePath = relativePath.replace(/\.disabled$/, '');
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const commandName = metadata.name || basename(relativePath, '.md');
|
|
249
|
+
|
|
250
|
+
// Get group from external config (not from frontmatter)
|
|
251
|
+
const group = getCommandGroup(commandName, relativePath, location, projectPath);
|
|
252
|
+
|
|
253
|
+
results.push({
|
|
254
|
+
name: commandName,
|
|
255
|
+
description: metadata.description,
|
|
256
|
+
group,
|
|
257
|
+
enabled: isEnabled,
|
|
258
|
+
location,
|
|
259
|
+
path: fullPath,
|
|
260
|
+
relativePath,
|
|
261
|
+
argumentHint: metadata.argumentHint,
|
|
262
|
+
allowedTools: metadata.allowedTools
|
|
263
|
+
});
|
|
264
|
+
} catch (err) {
|
|
265
|
+
// Skip files that fail to read
|
|
266
|
+
console.error(`[Commands] Failed to read ${fullPath}:`, err);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
} catch (err) {
|
|
272
|
+
console.error(`[Commands] Failed to scan directory ${currentDir}:`, err);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return results;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Get all commands configuration
|
|
280
|
+
*/
|
|
281
|
+
function getCommandsConfig(projectPath: string): CommandsConfig {
|
|
282
|
+
const result: CommandsConfig = {
|
|
283
|
+
projectCommands: [],
|
|
284
|
+
userCommands: [],
|
|
285
|
+
groups: []
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
const groupSet = new Set<string>();
|
|
289
|
+
|
|
290
|
+
try {
|
|
291
|
+
// Scan project commands (includes both .md and .md.disabled)
|
|
292
|
+
const projectDir = getCommandsDir('project', projectPath);
|
|
293
|
+
result.projectCommands = scanCommandsRecursive(projectDir, projectDir, 'project', projectPath);
|
|
294
|
+
|
|
295
|
+
// Scan user commands (includes both .md and .md.disabled)
|
|
296
|
+
const userDir = getCommandsDir('user', projectPath);
|
|
297
|
+
result.userCommands = scanCommandsRecursive(userDir, userDir, 'user', projectPath);
|
|
298
|
+
|
|
299
|
+
// Collect all groups
|
|
300
|
+
for (const cmd of [...result.projectCommands, ...result.userCommands]) {
|
|
301
|
+
groupSet.add(cmd.group);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
result.groups = Array.from(groupSet).sort();
|
|
305
|
+
} catch (error) {
|
|
306
|
+
console.error('[Commands] Error reading commands config:', error);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return result;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Find command by name in commands list
|
|
315
|
+
*/
|
|
316
|
+
function findCommand(
|
|
317
|
+
commands: CommandInfo[],
|
|
318
|
+
commandName: string
|
|
319
|
+
): CommandInfo | undefined {
|
|
320
|
+
// Try exact name match first
|
|
321
|
+
let cmd = commands.find(c => c.name === commandName);
|
|
322
|
+
if (cmd) return cmd;
|
|
323
|
+
|
|
324
|
+
// Try matching by relative path (without extension)
|
|
325
|
+
cmd = commands.find(c => {
|
|
326
|
+
const pathWithoutExt = c.relativePath.replace(/\.md$/, '');
|
|
327
|
+
return pathWithoutExt === commandName;
|
|
328
|
+
});
|
|
329
|
+
if (cmd) return cmd;
|
|
330
|
+
|
|
331
|
+
// Try matching by filename (without extension)
|
|
332
|
+
cmd = commands.find(c => {
|
|
333
|
+
const filename = basename(c.relativePath, '.md');
|
|
334
|
+
return filename === commandName;
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
return cmd;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Toggle a command's enabled state
|
|
342
|
+
*/
|
|
343
|
+
async function toggleCommand(
|
|
344
|
+
commandName: string,
|
|
345
|
+
location: CommandLocation,
|
|
346
|
+
projectPath: string,
|
|
347
|
+
initialPath: string
|
|
348
|
+
): Promise<CommandOperationResult> {
|
|
349
|
+
try {
|
|
350
|
+
// Validate command name
|
|
351
|
+
if (commandName.includes('..')) {
|
|
352
|
+
return { success: false, message: 'Invalid command name', status: 400 };
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const config = getCommandsConfig(projectPath);
|
|
356
|
+
const commands = location === 'project' ? config.projectCommands : config.userCommands;
|
|
357
|
+
const command = findCommand(commands, commandName);
|
|
358
|
+
|
|
359
|
+
if (!command) {
|
|
360
|
+
return { success: false, message: 'Command not found', status: 404 };
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const commandsDir = getCommandsDir(location, projectPath);
|
|
364
|
+
// relativePath already includes .md extension (e.g., 'workflow/plan.md')
|
|
365
|
+
const commandPath = join(commandsDir, command.relativePath);
|
|
366
|
+
const disabledPath = commandPath + '.disabled';
|
|
367
|
+
|
|
368
|
+
if (command.enabled) {
|
|
369
|
+
// Disable: rename .md to .md.disabled
|
|
370
|
+
if (!existsSync(commandPath)) {
|
|
371
|
+
return { success: false, message: 'Command file not found', status: 404 };
|
|
372
|
+
}
|
|
373
|
+
if (existsSync(disabledPath)) {
|
|
374
|
+
return { success: false, message: 'Command already disabled', status: 409 };
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
renameSync(commandPath, disabledPath);
|
|
378
|
+
return {
|
|
379
|
+
success: true,
|
|
380
|
+
message: 'Command disabled',
|
|
381
|
+
commandName: command.name,
|
|
382
|
+
location
|
|
383
|
+
};
|
|
384
|
+
} else {
|
|
385
|
+
// Enable: rename .md.disabled back to .md
|
|
386
|
+
if (!existsSync(disabledPath)) {
|
|
387
|
+
return { success: false, message: 'Disabled command not found', status: 404 };
|
|
388
|
+
}
|
|
389
|
+
if (existsSync(commandPath)) {
|
|
390
|
+
return { success: false, message: 'Command already enabled', status: 409 };
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
renameSync(disabledPath, commandPath);
|
|
394
|
+
return {
|
|
395
|
+
success: true,
|
|
396
|
+
message: 'Command enabled',
|
|
397
|
+
commandName: command.name,
|
|
398
|
+
location
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
} catch (error) {
|
|
402
|
+
return {
|
|
403
|
+
success: false,
|
|
404
|
+
message: (error as Error).message,
|
|
405
|
+
status: 500
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Toggle all commands in a group
|
|
412
|
+
*/
|
|
413
|
+
async function toggleGroup(
|
|
414
|
+
groupName: string,
|
|
415
|
+
location: CommandLocation,
|
|
416
|
+
enable: boolean,
|
|
417
|
+
projectPath: string,
|
|
418
|
+
initialPath: string
|
|
419
|
+
): Promise<{ success: boolean; results: CommandOperationResult[]; message: string }> {
|
|
420
|
+
const config = getCommandsConfig(projectPath);
|
|
421
|
+
const commands = location === 'project' ? config.projectCommands : config.userCommands;
|
|
422
|
+
|
|
423
|
+
// Filter commands by group and current state
|
|
424
|
+
const targetCommands = commands.filter(cmd =>
|
|
425
|
+
cmd.group === groupName && cmd.enabled !== enable
|
|
426
|
+
);
|
|
427
|
+
|
|
428
|
+
if (targetCommands.length === 0) {
|
|
429
|
+
return {
|
|
430
|
+
success: true,
|
|
431
|
+
results: [],
|
|
432
|
+
message: `No commands to ${enable ? 'enable' : 'disable'} in group '${groupName}'`
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const results: CommandOperationResult[] = [];
|
|
437
|
+
|
|
438
|
+
for (const cmd of targetCommands) {
|
|
439
|
+
const result = await toggleCommand(cmd.name, location, projectPath, initialPath);
|
|
440
|
+
results.push(result);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const successCount = results.filter(r => r.success).length;
|
|
444
|
+
const failCount = results.filter(r => !r.success).length;
|
|
445
|
+
|
|
446
|
+
return {
|
|
447
|
+
success: failCount === 0,
|
|
448
|
+
results,
|
|
449
|
+
message: `${enable ? 'Enabled' : 'Disabled'} ${successCount} commands${failCount > 0 ? `, ${failCount} failed` : ''}`
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// ========== Route Handler ==========
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Handle Commands routes
|
|
457
|
+
* @returns true if route was handled, false otherwise
|
|
458
|
+
*/
|
|
459
|
+
export async function handleCommandsRoutes(ctx: RouteContext): Promise<boolean> {
|
|
460
|
+
const { pathname, url, req, res, initialPath, handlePostRequest } = ctx;
|
|
461
|
+
|
|
462
|
+
// GET /api/commands - List all commands
|
|
463
|
+
if (pathname === '/api/commands' && req.method === 'GET') {
|
|
464
|
+
const projectPathParam = url.searchParams.get('path') || initialPath;
|
|
465
|
+
|
|
466
|
+
try {
|
|
467
|
+
const validatedProjectPath = await validateAllowedPath(projectPathParam, {
|
|
468
|
+
mustExist: true,
|
|
469
|
+
allowedDirectories: [initialPath]
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
const config = getCommandsConfig(validatedProjectPath);
|
|
473
|
+
|
|
474
|
+
// Include groups config from both project and user
|
|
475
|
+
const projectGroupsConfig = loadGroupsConfig('project', validatedProjectPath);
|
|
476
|
+
const userGroupsConfig = loadGroupsConfig('user', validatedProjectPath);
|
|
477
|
+
|
|
478
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
479
|
+
res.end(JSON.stringify({
|
|
480
|
+
...config,
|
|
481
|
+
projectGroupsConfig,
|
|
482
|
+
userGroupsConfig
|
|
483
|
+
}));
|
|
484
|
+
} catch (err) {
|
|
485
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
486
|
+
const status = message.includes('Access denied') ? 403 : 400;
|
|
487
|
+
console.error(`[Commands] Project path validation failed: ${message}`);
|
|
488
|
+
res.writeHead(status, { 'Content-Type': 'application/json' });
|
|
489
|
+
res.end(JSON.stringify({
|
|
490
|
+
error: status === 403 ? 'Access denied' : 'Invalid path',
|
|
491
|
+
projectCommands: [],
|
|
492
|
+
userCommands: [],
|
|
493
|
+
groups: []
|
|
494
|
+
}));
|
|
495
|
+
}
|
|
496
|
+
return true;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// POST /api/commands/:name/toggle - Toggle single command
|
|
500
|
+
if (pathname.match(/^\/api\/commands\/[^/]+\/toggle$/) && req.method === 'POST') {
|
|
501
|
+
const pathParts = pathname.split('/');
|
|
502
|
+
const commandName = decodeURIComponent(pathParts[3]);
|
|
503
|
+
|
|
504
|
+
handlePostRequest(req, res, async (body) => {
|
|
505
|
+
if (!isRecord(body)) {
|
|
506
|
+
return { error: 'Invalid request body', status: 400 };
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
const locationValue = body.location;
|
|
510
|
+
const projectPathParam = typeof body.projectPath === 'string' ? body.projectPath : undefined;
|
|
511
|
+
|
|
512
|
+
if (locationValue !== 'project' && locationValue !== 'user') {
|
|
513
|
+
return { error: 'Location is required (project or user)' };
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
const projectPath = projectPathParam || initialPath;
|
|
517
|
+
return toggleCommand(commandName, locationValue, projectPath, initialPath);
|
|
518
|
+
});
|
|
519
|
+
return true;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// POST /api/commands/group/:groupName/toggle - Toggle all commands in group
|
|
523
|
+
if (pathname.match(/^\/api\/commands\/group\/[^/]+\/toggle$/) && req.method === 'POST') {
|
|
524
|
+
const pathParts = pathname.split('/');
|
|
525
|
+
const groupName = decodeURIComponent(pathParts[4]);
|
|
526
|
+
|
|
527
|
+
handlePostRequest(req, res, async (body) => {
|
|
528
|
+
if (!isRecord(body)) {
|
|
529
|
+
return { error: 'Invalid request body', status: 400 };
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
const locationValue = body.location;
|
|
533
|
+
const enable = body.enable === true;
|
|
534
|
+
const projectPathParam = typeof body.projectPath === 'string' ? body.projectPath : undefined;
|
|
535
|
+
|
|
536
|
+
if (locationValue !== 'project' && locationValue !== 'user') {
|
|
537
|
+
return { error: 'Location is required (project or user)' };
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
const projectPath = projectPathParam || initialPath;
|
|
541
|
+
return toggleGroup(groupName, locationValue, enable, projectPath, initialPath);
|
|
542
|
+
});
|
|
543
|
+
return true;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// GET /api/commands/groups - Get groups configuration
|
|
547
|
+
if (pathname === '/api/commands/groups' && req.method === 'GET') {
|
|
548
|
+
const projectPathParam = url.searchParams.get('path') || initialPath;
|
|
549
|
+
const location = url.searchParams.get('location') || 'project';
|
|
550
|
+
|
|
551
|
+
try {
|
|
552
|
+
const validatedProjectPath = await validateAllowedPath(projectPathParam, {
|
|
553
|
+
mustExist: true,
|
|
554
|
+
allowedDirectories: [initialPath]
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
if (location !== 'project' && location !== 'user') {
|
|
558
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
559
|
+
res.end(JSON.stringify({ error: 'Invalid location' }));
|
|
560
|
+
return true;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
const groupsConfig = loadGroupsConfig(location as CommandLocation, validatedProjectPath);
|
|
564
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
565
|
+
res.end(JSON.stringify(groupsConfig));
|
|
566
|
+
} catch (err) {
|
|
567
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
568
|
+
const status = message.includes('Access denied') ? 403 : 400;
|
|
569
|
+
res.writeHead(status, { 'Content-Type': 'application/json' });
|
|
570
|
+
res.end(JSON.stringify({ error: message }));
|
|
571
|
+
}
|
|
572
|
+
return true;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// PUT /api/commands/groups - Update groups configuration
|
|
576
|
+
if (pathname === '/api/commands/groups' && req.method === 'PUT') {
|
|
577
|
+
const projectPathParam = url.searchParams.get('path') || initialPath;
|
|
578
|
+
const location = url.searchParams.get('location') || 'project';
|
|
579
|
+
|
|
580
|
+
handlePostRequest(req, res, async (body) => {
|
|
581
|
+
try {
|
|
582
|
+
const validatedProjectPath = await validateAllowedPath(projectPathParam, {
|
|
583
|
+
mustExist: true,
|
|
584
|
+
allowedDirectories: [initialPath]
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
if (location !== 'project' && location !== 'user') {
|
|
588
|
+
return { error: 'Invalid location', status: 400 };
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
if (!isRecord(body)) {
|
|
592
|
+
return { error: 'Invalid request body', status: 400 };
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// Validate and save groups config
|
|
596
|
+
const config: CommandGroupsConfig = {
|
|
597
|
+
groups: isRecord(body.groups) ? body.groups as Record<string, GroupDefinition> : {},
|
|
598
|
+
assignments: isRecord(body.assignments) ? body.assignments as Record<string, string> : {}
|
|
599
|
+
};
|
|
600
|
+
|
|
601
|
+
saveGroupsConfig(location as CommandLocation, validatedProjectPath, config);
|
|
602
|
+
|
|
603
|
+
return {
|
|
604
|
+
success: true,
|
|
605
|
+
message: 'Groups configuration updated',
|
|
606
|
+
data: config,
|
|
607
|
+
status: 200
|
|
608
|
+
};
|
|
609
|
+
} catch (err) {
|
|
610
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
611
|
+
const status = message.includes('Access denied') ? 403 : 400;
|
|
612
|
+
console.error(`[Commands] Failed to update groups config: ${message}`);
|
|
613
|
+
return { error: message, status };
|
|
614
|
+
}
|
|
615
|
+
});
|
|
616
|
+
return true;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
return false;
|
|
620
|
+
}
|