mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-05 01:50:27 +08:00
feat: Add phases for document consolidation, assembly, and compliance refinement
- Introduced Phase 2.5: Consolidation Agent to summarize analysis outputs and generate design overviews. - Added Phase 4: Document Assembly to create index-style documents linking chapter files. - Implemented Phase 5: Compliance Review & Iterative Refinement for CPCC compliance checks and updates. - Established CPCC Compliance Requirements document outlining mandatory sections and validation functions. - Created a base template for analysis agents to ensure consistency and efficiency in execution.
This commit is contained in:
@@ -12,26 +12,24 @@ Meta-skill for creating new Claude Code skills with configurable execution modes
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Skill Generator Architecture │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ Skill Generator │
|
||||
│ │
|
||||
│ ⚠️ Phase 0: Specification → 阅读并理解设计规范 (强制前置) │
|
||||
│ Study SKILL-DESIGN-SPEC.md + 模板 │
|
||||
│ ↓ │
|
||||
│ Phase 1: Requirements → skill-config.json │
|
||||
│ Discovery (name, type, mode, agents) │
|
||||
│ ↓ │
|
||||
│ Phase 2: Structure → 目录结构 + 核心文件骨架 │
|
||||
│ Generation │
|
||||
│ ↓ │
|
||||
│ Phase 3: Phase → phases/*.md (根据 mode 生成) │
|
||||
│ Generation Sequential | Autonomous │
|
||||
│ ↓ │
|
||||
│ Phase 4: Specs & → specs/*.md + templates/*.md │
|
||||
│ Templates │
|
||||
│ ↓ │
|
||||
│ Phase 5: Validation → 验证完整性 + 生成使用说明 │
|
||||
│ & Documentation │
|
||||
│ Input: User Request (skill name, purpose, mode) │
|
||||
│ ↓ │
|
||||
│ ┌─────────────────────────────────────────────────────────┐ │
|
||||
│ │ Phase 0-5: Sequential Pipeline │ │
|
||||
│ │ ┌────┐ ┌────┐ ┌────┐ ┌────┐ ┌────┐ ┌────┐ │ │
|
||||
│ │ │ P0 │→│ P1 │→│ P2 │→│ P3 │→│ P4 │→│ P5 │ │ │
|
||||
│ │ │Spec│ │Req │ │Dir │ │Gen │ │Spec│ │Val │ │ │
|
||||
│ │ └────┘ └────┘ └────┘ └─┬──┘ └────┘ └────┘ │ │
|
||||
│ │ │ │ │
|
||||
│ │ ┌────┴────┐ │ │
|
||||
│ │ ↓ ↓ │ │
|
||||
│ │ Sequential Autonomous │ │
|
||||
│ │ (phases/) (actions/) │ │
|
||||
│ └─────────────────────────────────────────────────────────┘ │
|
||||
│ ↓ │
|
||||
│ Output: .claude/skills/{skill-name}/ (complete package) │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
@@ -134,93 +132,289 @@ Phase 01 → Phase 02 → Phase 03 → ... → Phase N
|
||||
## Execution Flow
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ ⚠️ Phase 0: Specification Study (强制前置 - 禁止跳过) │
|
||||
│ → Read: ../_shared/SKILL-DESIGN-SPEC.md (通用设计规范) │
|
||||
│ → Read: templates/*.md (所有相关模板文件) │
|
||||
│ → 理解: Skill 结构规范、命名约定、质量标准 │
|
||||
│ → Output: 内化规范要求,确保后续生成符合标准 │
|
||||
│ ⛔ 未完成 Phase 0 禁止进入 Phase 1 │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ Phase 1: Requirements Discovery │
|
||||
│ → AskUserQuestion: Skill 名称、目标、执行模式 │
|
||||
│ → Output: skill-config.json │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ Phase 2: Structure Generation │
|
||||
│ → 创建目录结构: phases/, specs/, templates/, scripts/ │
|
||||
│ → 生成 SKILL.md 入口文件 │
|
||||
│ → Output: 完整目录结构 │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ Phase 3: Phase Generation │
|
||||
│ → Sequential: 生成 01-xx.md, 02-xx.md, ... │
|
||||
│ → Autonomous: 生成 orchestrator.md + actions/*.md │
|
||||
│ → Output: phases/*.md │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ Phase 4: Specs & Templates │
|
||||
│ → 生成领域规范: specs/{domain}-requirements.md │
|
||||
│ → 生成质量标准: specs/quality-standards.md │
|
||||
│ → 生成模板: templates/agent-base.md │
|
||||
│ → Output: specs/*.md, templates/*.md │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ Phase 5: Validation & Documentation │
|
||||
│ → 验证文件完整性 │
|
||||
│ → 生成 README.md 使用说明 │
|
||||
│ → Output: 验证报告 + README.md │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
Input Parsing:
|
||||
└─ Convert user request to structured format (skill-name/purpose/mode)
|
||||
|
||||
Phase 0: Specification Study (⚠️ MANDATORY - 禁止跳过)
|
||||
└─ Read specification documents
|
||||
├─ Load: ../_shared/SKILL-DESIGN-SPEC.md
|
||||
├─ Load: All templates/*.md files
|
||||
├─ Understand: Structure rules, naming conventions, quality standards
|
||||
└─ Output: Internalized requirements (in-memory, no file output)
|
||||
└─ Validation: ⛔ MUST complete before Phase 1
|
||||
|
||||
Phase 1: Requirements Discovery
|
||||
└─ Gather skill requirements via user interaction
|
||||
├─ Tool: AskUserQuestion
|
||||
│ ├─ Prompt: Skill name, purpose, execution mode
|
||||
│ ├─ Prompt: Phase/Action definition
|
||||
│ └─ Prompt: Tool dependencies, output format
|
||||
├─ Process: Generate configuration object
|
||||
└─ Output: skill-config.json → ${workDir}/
|
||||
├─ skill_name: string
|
||||
├─ execution_mode: "sequential" | "autonomous"
|
||||
├─ phases/actions: array
|
||||
└─ allowed_tools: array
|
||||
|
||||
Phase 2: Structure Generation
|
||||
└─ Create directory structure and entry file
|
||||
├─ Input: skill-config.json (from Phase 1)
|
||||
├─ Tool: Bash
|
||||
│ └─ Execute: mkdir -p .claude/skills/{skill-name}/{phases,specs,templates,scripts}
|
||||
├─ Tool: Write
|
||||
│ └─ Generate: SKILL.md (entry point with architecture diagram)
|
||||
└─ Output: Complete directory structure
|
||||
├─ .claude/skills/{skill-name}/SKILL.md
|
||||
├─ .claude/skills/{skill-name}/phases/
|
||||
├─ .claude/skills/{skill-name}/specs/
|
||||
├─ .claude/skills/{skill-name}/templates/
|
||||
└─ .claude/skills/{skill-name}/scripts/
|
||||
|
||||
Phase 3: Phase/Action Generation
|
||||
└─ Decision (execution_mode check):
|
||||
├─ execution_mode === "sequential" → Generate Sequential Phases
|
||||
│ ├─ Tool: Read (template: templates/sequential-phase.md)
|
||||
│ ├─ Loop: For each phase in config.sequential_config.phases
|
||||
│ │ ├─ Generate: phases/{phase-id}.md
|
||||
│ │ └─ Link: Previous phase output → Current phase input
|
||||
│ ├─ Tool: Write (orchestrator: phases/_orchestrator.md)
|
||||
│ ├─ Tool: Write (workflow definition: workflow.json)
|
||||
│ └─ Output: phases/01-{name}.md, phases/02-{name}.md, ...
|
||||
│
|
||||
└─ execution_mode === "autonomous" → Generate Orchestrator + Actions
|
||||
├─ Tool: Read (template: templates/autonomous-orchestrator.md)
|
||||
├─ Tool: Write (state schema: phases/state-schema.md)
|
||||
├─ Tool: Write (orchestrator: phases/orchestrator.md)
|
||||
├─ Tool: Write (action catalog: specs/action-catalog.md)
|
||||
├─ Loop: For each action in config.autonomous_config.actions
|
||||
│ ├─ Tool: Read (template: templates/autonomous-action.md)
|
||||
│ └─ Generate: phases/actions/{action-id}.md
|
||||
└─ Output: phases/orchestrator.md, phases/actions/*.md
|
||||
|
||||
Phase 4: Specs & Templates
|
||||
└─ Generate domain specifications and templates
|
||||
├─ Input: skill-config.json (domain context)
|
||||
├─ Tool: Write
|
||||
│ ├─ Generate: specs/{domain}-requirements.md
|
||||
│ ├─ Generate: specs/quality-standards.md
|
||||
│ └─ Generate: templates/agent-base.md (if needed)
|
||||
└─ Output: Domain-specific documentation
|
||||
├─ specs/{skill-name}-requirements.md
|
||||
├─ specs/quality-standards.md
|
||||
└─ templates/agent-base.md
|
||||
|
||||
Phase 5: Validation & Documentation
|
||||
└─ Verify completeness and generate usage guide
|
||||
├─ Input: All generated files from previous phases
|
||||
├─ Tool: Glob + Read
|
||||
│ └─ Check: Required files exist and contain proper structure
|
||||
├─ Tool: Write
|
||||
│ ├─ Generate: README.md (usage instructions)
|
||||
│ └─ Generate: validation-report.json (completeness check)
|
||||
└─ Output: Final documentation
|
||||
├─ README.md (how to use this skill)
|
||||
└─ validation-report.json (quality gate results)
|
||||
|
||||
Return:
|
||||
└─ Summary with skill location and next steps
|
||||
├─ Skill path: .claude/skills/{skill-name}/
|
||||
├─ Status: ✅ All phases completed
|
||||
└─ Suggestion: "Review SKILL.md and customize phase files as needed"
|
||||
```
|
||||
|
||||
## Directory Setup
|
||||
**Execution Protocol**:
|
||||
|
||||
```javascript
|
||||
const skillName = config.skill_name;
|
||||
const skillDir = `.claude/skills/${skillName}`;
|
||||
// Phase 0: Read specifications (in-memory)
|
||||
Read('.claude/skills/_shared/SKILL-DESIGN-SPEC.md');
|
||||
Read('.claude/skills/skill-generator/templates/*.md'); // All templates
|
||||
|
||||
// 创建目录结构
|
||||
Bash(`mkdir -p "${skillDir}/phases"`);
|
||||
Bash(`mkdir -p "${skillDir}/specs"`);
|
||||
Bash(`mkdir -p "${skillDir}/templates"`);
|
||||
// Phase 1: Gather requirements
|
||||
const answers = AskUserQuestion({
|
||||
questions: [
|
||||
{ question: "Skill name?", header: "Name", options: [...] },
|
||||
{ question: "Execution mode?", header: "Mode", options: ["Sequential", "Autonomous"] }
|
||||
]
|
||||
});
|
||||
|
||||
// Autonomous 模式额外目录
|
||||
if (config.execution_mode === 'autonomous') {
|
||||
Bash(`mkdir -p "${skillDir}/phases/actions"`);
|
||||
const config = generateConfig(answers);
|
||||
const workDir = `.workflow/.scratchpad/skill-gen-${timestamp}`;
|
||||
Write(`${workDir}/skill-config.json`, JSON.stringify(config));
|
||||
|
||||
// Phase 2: Create structure
|
||||
const skillDir = `.claude/skills/${config.skill_name}`;
|
||||
Bash(`mkdir -p "${skillDir}/phases" "${skillDir}/specs" "${skillDir}/templates"`);
|
||||
Write(`${skillDir}/SKILL.md`, generateSkillEntry(config));
|
||||
|
||||
// Phase 3: Generate phases (mode-dependent)
|
||||
if (config.execution_mode === 'sequential') {
|
||||
Write(`${skillDir}/phases/_orchestrator.md`, generateOrchestrator(config));
|
||||
Write(`${skillDir}/workflow.json`, generateWorkflowDef(config));
|
||||
config.sequential_config.phases.forEach(phase => {
|
||||
Write(`${skillDir}/phases/${phase.id}.md`, generatePhase(phase, config));
|
||||
});
|
||||
} else {
|
||||
Write(`${skillDir}/phases/orchestrator.md`, generateAutonomousOrchestrator(config));
|
||||
Write(`${skillDir}/phases/state-schema.md`, generateStateSchema(config));
|
||||
config.autonomous_config.actions.forEach(action => {
|
||||
Write(`${skillDir}/phases/actions/${action.id}.md`, generateAction(action, config));
|
||||
});
|
||||
}
|
||||
|
||||
// Phase 4: Generate specs
|
||||
Write(`${skillDir}/specs/${config.skill_name}-requirements.md`, generateRequirements(config));
|
||||
Write(`${skillDir}/specs/quality-standards.md`, generateQualityStandards(config));
|
||||
|
||||
|
||||
// Phase 5: Validate & Document
|
||||
const validation = validateStructure(skillDir);
|
||||
Write(`${skillDir}/validation-report.json`, JSON.stringify(validation));
|
||||
Write(`${skillDir}/README.md`, generateReadme(config, validation));
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase Reference Guide
|
||||
|
||||
Navigation and entry points for each execution phase:
|
||||
|
||||
### Phase 0: Specification Study (Mandatory)
|
||||
|
||||
**Document**: 🔗 [SKILL-DESIGN-SPEC.md](../_shared/SKILL-DESIGN-SPEC.md)
|
||||
|
||||
**Purpose**: Understand skill design standards before generating
|
||||
|
||||
**What to Read**:
|
||||
- Skill structure conventions
|
||||
- Naming standards
|
||||
- Quality criteria
|
||||
- Output format specifications
|
||||
|
||||
---
|
||||
|
||||
### Phase 1: Requirements Discovery
|
||||
|
||||
**Document**: 🔗 [phases/01-requirements-discovery.md](phases/01-requirements-discovery.md)
|
||||
|
||||
| Attribute | Value |
|
||||
|-----------|-------|
|
||||
| **Purpose** | Gather configuration from user via interactive prompts |
|
||||
| **Input** | User responses (Skill name, purpose, execution mode) |
|
||||
| **Output** | `skill-config.json` (configuration file) |
|
||||
| **Key Decision** | Execution mode: Sequential vs Autonomous vs Hybrid |
|
||||
|
||||
---
|
||||
|
||||
### Phase 2: Structure Generation
|
||||
|
||||
**Document**: 🔗 [phases/02-structure-generation.md](phases/02-structure-generation.md)
|
||||
|
||||
| Attribute | Value |
|
||||
|-----------|-------|
|
||||
| **Purpose** | Create directory structure and entry file |
|
||||
| **Input** | `skill-config.json` (from Phase 1) |
|
||||
| **Output** | `.claude/skills/{skill-name}/` directory + `SKILL.md` |
|
||||
|
||||
---
|
||||
|
||||
### Phase 3: Phase/Action Generation
|
||||
|
||||
**Document**: 🔗 [phases/03-phase-generation.md](phases/03-phase-generation.md)
|
||||
|
||||
| Attribute | Value |
|
||||
|-----------|-------|
|
||||
| **Purpose** | Generate execution phases or actions based on mode |
|
||||
| **Input** | `skill-config.json` + Templates |
|
||||
| **Output** | Sequential: `phases/01-*.md` ... OR Autonomous: `phases/orchestrator.md` + `actions/*.md` |
|
||||
|
||||
**Decision Logic**:
|
||||
```
|
||||
IF execution_mode === "sequential":
|
||||
└─ Generate sequential phases with linear flow
|
||||
ELSE IF execution_mode === "autonomous":
|
||||
└─ Generate orchestrator + independent actions
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Phase 4: Specs & Templates
|
||||
|
||||
**Document**: 🔗 [phases/04-specs-templates.md](phases/04-specs-templates.md)
|
||||
|
||||
| Attribute | Value |
|
||||
|-----------|-------|
|
||||
| **Purpose** | Generate domain specifications and template files |
|
||||
| **Input** | `skill-config.json` (domain context) |
|
||||
| **Output** | `specs/{skill-name}-requirements.md`, `specs/quality-standards.md` |
|
||||
|
||||
---
|
||||
|
||||
### Phase 5: Validation & Documentation
|
||||
|
||||
**Document**: 🔗 [phases/05-validation.md](phases/05-validation.md)
|
||||
|
||||
| Attribute | Value |
|
||||
|-----------|-------|
|
||||
| **Purpose** | Verify completeness and generate usage documentation |
|
||||
| **Input** | All files from previous phases |
|
||||
| **Output** | `README.md`, `validation-report.json` |
|
||||
|
||||
---
|
||||
|
||||
## Template Reference
|
||||
|
||||
| Template | Generated For | When Used |
|
||||
|----------|--------------|-----------|
|
||||
| [skill-md.md](templates/skill-md.md) | SKILL.md entry file | Phase 2 |
|
||||
| [sequential-phase.md](templates/sequential-phase.md) | Sequential phase files | Phase 3 |
|
||||
| [autonomous-orchestrator.md](templates/autonomous-orchestrator.md) | Orchestrator (autonomous) | Phase 3 |
|
||||
| [autonomous-action.md](templates/autonomous-action.md) | Action files | Phase 3 |
|
||||
| [code-analysis-action.md](templates/code-analysis-action.md) | Code analysis actions | Phase 3 |
|
||||
| [llm-action.md](templates/llm-action.md) | LLM-powered actions | Phase 3 |
|
||||
| [script-bash.md](templates/script-bash.md) | Bash scripts | Phase 3/4 |
|
||||
| [script-python.md](templates/script-python.md) | Python scripts | Phase 3/4 |
|
||||
|
||||
## Output Structure
|
||||
|
||||
### Sequential Mode
|
||||
|
||||
```
|
||||
.claude/skills/{skill-name}/
|
||||
├── SKILL.md
|
||||
├── SKILL.md # 入口文件
|
||||
├── phases/
|
||||
│ ├── 01-{step-one}.md
|
||||
│ ├── 02-{step-two}.md
|
||||
│ └── 03-{step-three}.md
|
||||
│ ├── _orchestrator.md # 声明式编排器
|
||||
│ ├── workflow.json # 工作流定义
|
||||
│ ├── 01-{step-one}.md # 阶段 1
|
||||
│ ├── 02-{step-two}.md # 阶段 2
|
||||
│ └── 03-{step-three}.md # 阶段 3
|
||||
├── specs/
|
||||
│ ├── {domain}-requirements.md
|
||||
│ ├── {skill-name}-requirements.md
|
||||
│ └── quality-standards.md
|
||||
└── templates/
|
||||
└── agent-base.md
|
||||
├── templates/
|
||||
│ └── agent-base.md
|
||||
├── scripts/
|
||||
└── README.md
|
||||
```
|
||||
|
||||
### Autonomous Mode
|
||||
|
||||
```
|
||||
.claude/skills/{skill-name}/
|
||||
├── SKILL.md
|
||||
├── SKILL.md # 入口文件
|
||||
├── phases/
|
||||
│ ├── orchestrator.md # 编排器:读取状态 → 选择动作
|
||||
│ ├── state-schema.md # 状态结构定义
|
||||
│ └── actions/ # 独立动作(无顺序)
|
||||
│ ├── action-{a}.md
|
||||
│ ├── action-{b}.md
|
||||
│ └── action-{c}.md
|
||||
│ ├── orchestrator.md # 编排器 (状态驱动)
|
||||
│ ├── state-schema.md # 状态结构定义
|
||||
│ └── actions/
|
||||
│ ├── action-init.md
|
||||
│ ├── action-create.md
|
||||
│ └── action-list.md
|
||||
├── specs/
|
||||
│ ├── {domain}-requirements.md
|
||||
│ ├── action-catalog.md # 动作目录(描述、前置条件、效果)
|
||||
│ ├── {skill-name}-requirements.md
|
||||
│ ├── action-catalog.md
|
||||
│ └── quality-standards.md
|
||||
└── templates/
|
||||
├── orchestrator-base.md # 编排器模板
|
||||
└── action-base.md # 动作模板
|
||||
```
|
||||
├── templates/
|
||||
│ ├── orchestrator-base.md
|
||||
│ └── action-base.md
|
||||
├── scripts/
|
||||
└── README.md
|
||||
```
|
||||
@@ -1,15 +1,4 @@
|
||||
# Phase 1: Requirements Discovery
|
||||
|
||||
收集新 Skill 的需求信息,生成配置文件。
|
||||
|
||||
## Objective
|
||||
|
||||
- 收集 Skill 基本信息(名称、描述、触发词)
|
||||
- 确定执行模式(Sequential / Autonomous)
|
||||
- 定义阶段/动作
|
||||
- 配置工具依赖和输出格式
|
||||
|
||||
## Execution Steps
|
||||
|
||||
### Step 1: 基本信息收集
|
||||
|
||||
@@ -228,12 +217,4 @@ Bash(`mkdir -p "${workDir}"`);
|
||||
Write(`${workDir}/skill-config.json`, JSON.stringify(config, null, 2));
|
||||
```
|
||||
|
||||
## Output
|
||||
|
||||
- **File**: `skill-config.json`
|
||||
- **Location**: `.workflow/.scratchpad/skill-gen-{timestamp}/`
|
||||
- **Format**: JSON
|
||||
|
||||
## Next Phase
|
||||
|
||||
→ [Phase 2: Structure Generation](02-structure-generation.md)
|
||||
|
||||
@@ -8,9 +8,7 @@
|
||||
- 生成 SKILL.md 入口文件
|
||||
- 根据执行模式创建对应的子目录
|
||||
|
||||
## Input
|
||||
|
||||
- 依赖: `skill-config.json` (Phase 1 产出)
|
||||
|
||||
## Execution Steps
|
||||
|
||||
@@ -192,16 +190,4 @@ function generateReferenceTable(config) {
|
||||
}
|
||||
```
|
||||
|
||||
## Output
|
||||
|
||||
- **Directory**: `.claude/skills/{skill-name}/`
|
||||
- **Files**:
|
||||
- `SKILL.md` (入口文件)
|
||||
- `phases/` (执行阶段目录)
|
||||
- `specs/` (规范文档目录)
|
||||
- `templates/` (模板目录)
|
||||
- `scripts/` (脚本目录,存放 Python/Bash 确定性脚本)
|
||||
|
||||
## Next Phase
|
||||
|
||||
→ [Phase 3: Phase Generation](03-phase-generation.md)
|
||||
|
||||
@@ -8,10 +8,7 @@
|
||||
- Autonomous 模式:生成编排器和动作文件
|
||||
- 支持 **文件上下文** 和 **内存上下文** 两种策略
|
||||
|
||||
## Input
|
||||
|
||||
- 依赖: `skill-config.json`, SKILL.md (Phase 1-2 产出)
|
||||
- 模板: `templates/sequential-phase.md`, `templates/autonomous-*.md`
|
||||
|
||||
## 上下文策略 (P0 增强)
|
||||
|
||||
@@ -782,21 +779,4 @@ function getPreconditionCheck(action) {
|
||||
}
|
||||
```
|
||||
|
||||
## Output
|
||||
|
||||
### Sequential 模式
|
||||
|
||||
- `phases/_orchestrator.md` (声明式编排器)
|
||||
- `workflow.json` (工作流定义)
|
||||
- `phases/01-{step}.md`, `02-{step}.md`, ...
|
||||
|
||||
### Autonomous 模式
|
||||
|
||||
- `phases/orchestrator.md` (增强版编排器)
|
||||
- `phases/state-schema.md`
|
||||
- `specs/action-catalog.md` (声明式动作目录)
|
||||
- `phases/actions/action-{name}.md` (多个)
|
||||
|
||||
## Next Phase
|
||||
|
||||
→ [Phase 4: Specs & Templates](04-specs-templates.md)
|
||||
|
||||
@@ -1,26 +1,107 @@
|
||||
# Phase 4: Specs & Templates Generation
|
||||
# Phase 4: Specifications & Templates Generation
|
||||
|
||||
生成规范文件和模板文件。
|
||||
Generate domain requirements, quality standards, agent templates, and action catalogs.
|
||||
|
||||
## Objective
|
||||
|
||||
- 生成领域规范 (`specs/{domain}-requirements.md`)
|
||||
- 生成质量标准 (`specs/quality-standards.md`)
|
||||
- 生成 Agent 模板 (`templates/agent-base.md`)
|
||||
- Autonomous 模式额外生成动作目录 (`specs/action-catalog.md`)
|
||||
Generate comprehensive specifications and templates:
|
||||
- Domain requirements document with validation function
|
||||
- Quality standards with automated check system
|
||||
- Agent base template with prompt structure
|
||||
- Action catalog for autonomous mode (conditional)
|
||||
|
||||
## Input
|
||||
|
||||
- 依赖: `skill-config.json`, SKILL.md, phases/*.md
|
||||
**File Dependencies**:
|
||||
- `skill-config.json` (from Phase 1)
|
||||
- `.claude/skills/{skill-name}/` directory (from Phase 2)
|
||||
- Generated phase/action files (from Phase 3)
|
||||
|
||||
## Execution Steps
|
||||
**Required Information**:
|
||||
- Skill name, display name, description
|
||||
- Execution mode (determines if action-catalog.md is generated)
|
||||
- Output format and location
|
||||
- Phase/action definitions
|
||||
|
||||
### Step 1: 生成领域规范
|
||||
## Output
|
||||
|
||||
**Generated Files**:
|
||||
|
||||
| File | Purpose | Generation Condition |
|
||||
|------|---------|---------------------|
|
||||
| `specs/{skill-name}-requirements.md` | Domain requirements with validation | Always |
|
||||
| `specs/quality-standards.md` | Quality evaluation criteria | Always |
|
||||
| `templates/agent-base.md` | Agent prompt template | Always |
|
||||
| `specs/action-catalog.md` | Action dependency graph and selection priority | Autonomous/Hybrid mode only |
|
||||
|
||||
**File Structure**:
|
||||
|
||||
**Domain Requirements** (`specs/{skill-name}-requirements.md`):
|
||||
```markdown
|
||||
# {display_name} Requirements
|
||||
- When to Use (phase/action reference table)
|
||||
- Domain Requirements (功能要求, 输出要求, 质量要求)
|
||||
- Validation Function (JavaScript code)
|
||||
- Error Handling (recovery strategies)
|
||||
```
|
||||
|
||||
**Quality Standards** (`specs/quality-standards.md`):
|
||||
```markdown
|
||||
# Quality Standards
|
||||
- Quality Dimensions (Completeness 25%, Consistency 25%, Accuracy 25%, Usability 25%)
|
||||
- Quality Gates (Pass ≥80%, Review 60-79%, Fail <60%)
|
||||
- Issue Classification (Errors, Warnings, Info)
|
||||
- Automated Checks (runQualityChecks function)
|
||||
```
|
||||
|
||||
**Agent Base** (`templates/agent-base.md`):
|
||||
```markdown
|
||||
# Agent Base Template
|
||||
- 通用 Prompt 结构 (ROLE, PROJECT CONTEXT, TASK, CONSTRAINTS, OUTPUT_FORMAT, QUALITY_CHECKLIST)
|
||||
- 变量说明 (workDir, output_path)
|
||||
- 返回格式 (AgentReturn interface)
|
||||
- 角色定义参考 (phase/action specific agents)
|
||||
```
|
||||
|
||||
**Action Catalog** (`specs/action-catalog.md`, Autonomous/Hybrid only):
|
||||
```markdown
|
||||
# Action Catalog
|
||||
- Available Actions (table with Purpose, Preconditions, Effects)
|
||||
- Action Dependencies (Mermaid diagram)
|
||||
- State Transitions (state machine table)
|
||||
- Selection Priority (ordered action list)
|
||||
```
|
||||
|
||||
## Decision Logic
|
||||
|
||||
```
|
||||
Decision (execution_mode check):
|
||||
├─ mode === 'sequential' → Generate 3 files only
|
||||
│ └─ Files: requirements.md, quality-standards.md, agent-base.md
|
||||
│
|
||||
├─ mode === 'autonomous' → Generate 4 files
|
||||
│ ├─ Files: requirements.md, quality-standards.md, agent-base.md
|
||||
│ └─ Additional: action-catalog.md (with action dependencies)
|
||||
│
|
||||
└─ mode === 'hybrid' → Generate 4 files
|
||||
├─ Files: requirements.md, quality-standards.md, agent-base.md
|
||||
└─ Additional: action-catalog.md (with hybrid logic)
|
||||
```
|
||||
|
||||
## Execution Protocol
|
||||
|
||||
```javascript
|
||||
// Phase 4: Generate Specifications & Templates
|
||||
// Reference: phases/04-specs-templates.md
|
||||
|
||||
// Load config and setup
|
||||
const config = JSON.parse(Read(`${workDir}/skill-config.json`));
|
||||
const skillDir = `.claude/skills/${config.skill_name}`;
|
||||
|
||||
// Ensure specs and templates directories exist (created in Phase 2)
|
||||
// skillDir structure: phases/, specs/, templates/
|
||||
|
||||
// Step 1: Generate domain requirements
|
||||
const domainRequirements = `# ${config.display_name} Requirements
|
||||
|
||||
${config.description}
|
||||
@@ -29,8 +110,8 @@ ${config.description}
|
||||
|
||||
| Phase | Usage | Reference |
|
||||
|-------|-------|-----------|
|
||||
${config.execution_mode === 'sequential' ?
|
||||
config.sequential_config.phases.map((p, i) =>
|
||||
${config.execution_mode === 'sequential' ?
|
||||
config.sequential_config.phases.map((p, i) =>
|
||||
`| Phase ${i+1} | ${p.name} | ${p.id}.md |`
|
||||
).join('\n') :
|
||||
`| Orchestrator | 动作选择 | orchestrator.md |
|
||||
@@ -67,7 +148,7 @@ function validate${toPascalCase(config.skill_name)}(output) {
|
||||
{ name: "格式正确", pass: output.format === "${config.output.format}" },
|
||||
{ name: "内容完整", pass: output.content?.length > 0 }
|
||||
];
|
||||
|
||||
|
||||
return {
|
||||
passed: checks.filter(c => c.pass).length,
|
||||
total: checks.length,
|
||||
@@ -86,11 +167,8 @@ function validate${toPascalCase(config.skill_name)}(output) {
|
||||
`;
|
||||
|
||||
Write(`${skillDir}/specs/${config.skill_name}-requirements.md`, domainRequirements);
|
||||
```
|
||||
|
||||
### Step 2: 生成质量标准
|
||||
|
||||
```javascript
|
||||
// Step 2: Generate quality standards
|
||||
const qualityStandards = `# Quality Standards
|
||||
|
||||
${config.display_name} 的质量评估标准。
|
||||
@@ -176,7 +254,7 @@ function runQualityChecks(workDir) {
|
||||
|
||||
return {
|
||||
score: results.overall,
|
||||
gate: results.overall >= 80 ? 'pass' :
|
||||
gate: results.overall >= 80 ? 'pass' :
|
||||
results.overall >= 60 ? 'review' : 'fail',
|
||||
details: results
|
||||
};
|
||||
@@ -185,11 +263,8 @@ function runQualityChecks(workDir) {
|
||||
`;
|
||||
|
||||
Write(`${skillDir}/specs/quality-standards.md`, qualityStandards);
|
||||
```
|
||||
|
||||
### Step 3: 生成 Agent 模板
|
||||
|
||||
```javascript
|
||||
// Step 3: Generate agent base template
|
||||
const agentBase = `# Agent Base Template
|
||||
|
||||
${config.display_name} 的 Agent 基础模板。
|
||||
@@ -246,20 +321,17 @@ interface AgentReturn {
|
||||
## 角色定义参考
|
||||
|
||||
${config.execution_mode === 'sequential' ?
|
||||
config.sequential_config.phases.map((p, i) =>
|
||||
config.sequential_config.phases.map((p, i) =>
|
||||
`- **Phase ${i+1} Agent**: ${p.name} 专家`
|
||||
).join('\n') :
|
||||
config.autonomous_config.actions.map(a =>
|
||||
config.autonomous_config.actions.map(a =>
|
||||
`- **${a.name} Agent**: ${a.description || a.name + ' 执行者'}`
|
||||
).join('\n')}
|
||||
`;
|
||||
|
||||
Write(`${skillDir}/templates/agent-base.md`, agentBase);
|
||||
```
|
||||
|
||||
### Step 4: Autonomous 模式 - 动作目录
|
||||
|
||||
```javascript
|
||||
// Step 4: Conditional - Generate action catalog for autonomous/hybrid mode
|
||||
if (config.execution_mode === 'autonomous' || config.execution_mode === 'hybrid') {
|
||||
const actionCatalog = `# Action Catalog
|
||||
|
||||
@@ -269,7 +341,7 @@ ${config.display_name} 的可用动作目录。
|
||||
|
||||
| Action | Purpose | Preconditions | Effects |
|
||||
|--------|---------|---------------|---------|
|
||||
${config.autonomous_config.actions.map(a =>
|
||||
${config.autonomous_config.actions.map(a =>
|
||||
`| [${a.id}](../phases/actions/${a.id}.md) | ${a.description || a.name} | ${a.preconditions?.join(', ') || '-'} | ${a.effects?.join(', ') || '-'} |`
|
||||
).join('\n')}
|
||||
|
||||
@@ -289,7 +361,7 @@ ${config.autonomous_config.actions.map((a, i, arr) => {
|
||||
| From State | Action | To State |
|
||||
|------------|--------|----------|
|
||||
| pending | action-init | running |
|
||||
${config.autonomous_config.actions.slice(1).map(a =>
|
||||
${config.autonomous_config.actions.slice(1).map(a =>
|
||||
`| running | ${a.id} | running |`
|
||||
).join('\n')}
|
||||
| running | action-complete | completed |
|
||||
@@ -299,30 +371,28 @@ ${config.autonomous_config.actions.slice(1).map(a =>
|
||||
|
||||
当多个动作的前置条件都满足时,按以下优先级选择:
|
||||
|
||||
${config.autonomous_config.actions.map((a, i) =>
|
||||
${config.autonomous_config.actions.map((a, i) =>
|
||||
`${i + 1}. \`${a.id}\` - ${a.name}`
|
||||
).join('\n')}
|
||||
`;
|
||||
|
||||
Write(`${skillDir}/specs/action-catalog.md`, actionCatalog);
|
||||
}
|
||||
```
|
||||
|
||||
### Step 5: 辅助函数
|
||||
|
||||
```javascript
|
||||
// Helper function
|
||||
function toPascalCase(str) {
|
||||
return str.split('-').map(s => s.charAt(0).toUpperCase() + s.slice(1)).join('');
|
||||
}
|
||||
|
||||
// Phase output summary
|
||||
console.log('Phase 4 complete: Generated specs and templates');
|
||||
```
|
||||
|
||||
## Output
|
||||
|
||||
- `specs/{skill-name}-requirements.md` - 领域规范
|
||||
- `specs/quality-standards.md` - 质量标准
|
||||
- `specs/action-catalog.md` - 动作目录 (Autonomous 模式)
|
||||
- `templates/agent-base.md` - Agent 模板
|
||||
|
||||
## Next Phase
|
||||
|
||||
→ [Phase 5: Validation](05-validation.md)
|
||||
|
||||
**Data Flow to Phase 5**:
|
||||
- All generated files in `specs/` and `templates/`
|
||||
- skill-config.json for validation reference
|
||||
- Complete skill directory structure ready for final validation
|
||||
|
||||
@@ -51,6 +51,17 @@ interface CommandOperationResult {
|
||||
status?: number;
|
||||
}
|
||||
|
||||
interface GroupDefinition {
|
||||
name: string;
|
||||
icon?: string;
|
||||
color?: string;
|
||||
}
|
||||
|
||||
interface CommandGroupsConfig {
|
||||
groups: Record<string, GroupDefinition>; // Custom group definitions
|
||||
assignments: Record<string, string>; // commandName -> groupId mapping
|
||||
}
|
||||
|
||||
// ========== Helper Functions ==========
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
@@ -125,19 +136,79 @@ function parseCommandFrontmatter(content: string): CommandMetadata {
|
||||
}
|
||||
|
||||
/**
|
||||
* Infer group from command path if not specified in frontmatter
|
||||
* Get command groups config file path
|
||||
*/
|
||||
function inferGroupFromPath(relativePath: string, metadata: CommandMetadata): string {
|
||||
// If group is specified in frontmatter, use it
|
||||
if (metadata.group && metadata.group !== 'other') {
|
||||
return metadata.group;
|
||||
function getGroupsConfigPath(location: CommandLocation, projectPath: string): string {
|
||||
const baseDir = location === 'project'
|
||||
? join(projectPath, '.claude')
|
||||
: join(homedir(), '.claude');
|
||||
return join(baseDir, 'command-groups.json');
|
||||
}
|
||||
|
||||
/**
|
||||
* Load command groups configuration
|
||||
*/
|
||||
function loadGroupsConfig(location: CommandLocation, projectPath: string): CommandGroupsConfig {
|
||||
const configPath = getGroupsConfigPath(location, projectPath);
|
||||
|
||||
const defaultConfig: CommandGroupsConfig = {
|
||||
groups: {},
|
||||
assignments: {}
|
||||
};
|
||||
|
||||
if (!existsSync(configPath)) {
|
||||
return defaultConfig;
|
||||
}
|
||||
|
||||
// Infer from directory structure
|
||||
try {
|
||||
const content = readFileSync(configPath, 'utf8');
|
||||
const parsed = JSON.parse(content);
|
||||
|
||||
return {
|
||||
groups: isRecord(parsed.groups) ? parsed.groups as Record<string, GroupDefinition> : {},
|
||||
assignments: isRecord(parsed.assignments) ? parsed.assignments as Record<string, string> : {}
|
||||
};
|
||||
} catch (err) {
|
||||
console.error(`[Commands] Failed to load groups config from ${configPath}:`, err);
|
||||
return defaultConfig;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save command groups configuration
|
||||
*/
|
||||
function saveGroupsConfig(location: CommandLocation, projectPath: string, config: CommandGroupsConfig): void {
|
||||
const configPath = getGroupsConfigPath(location, projectPath);
|
||||
const configDir = dirname(configPath);
|
||||
|
||||
if (!existsSync(configDir)) {
|
||||
mkdirSync(configDir, { recursive: true });
|
||||
}
|
||||
|
||||
try {
|
||||
const content = JSON.stringify(config, null, 2);
|
||||
require('fs').writeFileSync(configPath, content, 'utf8');
|
||||
} catch (err) {
|
||||
console.error(`[Commands] Failed to save groups config to ${configPath}:`, err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get group for a command (from config or inferred from path)
|
||||
*/
|
||||
function getCommandGroup(commandName: string, relativePath: string, location: CommandLocation, projectPath: string): string {
|
||||
// First check custom assignments
|
||||
const config = loadGroupsConfig(location, projectPath);
|
||||
if (config.assignments[commandName]) {
|
||||
return config.assignments[commandName];
|
||||
}
|
||||
|
||||
// Fallback to path-based inference - use full directory path as group
|
||||
const parts = relativePath.split(/[/\\]/);
|
||||
if (parts.length > 1) {
|
||||
// Use first directory as group (e.g., 'workflow', 'issue', 'memory')
|
||||
return parts[0];
|
||||
// Use full directory path (excluding filename) as group
|
||||
// e.g., 'workflow/review/code-review.md' -> 'workflow/review'
|
||||
return parts.slice(0, -1).join('/');
|
||||
}
|
||||
|
||||
return 'other';
|
||||
@@ -150,7 +221,8 @@ function scanCommandsRecursive(
|
||||
baseDir: string,
|
||||
currentDir: string,
|
||||
location: CommandLocation,
|
||||
enabled: boolean
|
||||
enabled: boolean,
|
||||
projectPath: string
|
||||
): CommandInfo[] {
|
||||
const results: CommandInfo[] = [];
|
||||
|
||||
@@ -168,17 +240,20 @@ function scanCommandsRecursive(
|
||||
if (entry.isDirectory()) {
|
||||
// Skip _disabled directory when scanning enabled commands
|
||||
if (entry.name === '_disabled') continue;
|
||||
|
||||
|
||||
// Recursively scan subdirectories
|
||||
results.push(...scanCommandsRecursive(baseDir, fullPath, location, enabled));
|
||||
results.push(...scanCommandsRecursive(baseDir, fullPath, location, enabled, projectPath));
|
||||
} else if (entry.isFile() && entry.name.endsWith('.md')) {
|
||||
try {
|
||||
const content = readFileSync(fullPath, 'utf8');
|
||||
const metadata = parseCommandFrontmatter(content);
|
||||
const group = inferGroupFromPath(relativePath, metadata);
|
||||
const commandName = metadata.name || basename(entry.name, '.md');
|
||||
|
||||
// Get group from external config (not from frontmatter)
|
||||
const group = getCommandGroup(commandName, relativePath, location, projectPath);
|
||||
|
||||
results.push({
|
||||
name: metadata.name || basename(entry.name, '.md'),
|
||||
name: commandName,
|
||||
description: metadata.description,
|
||||
group,
|
||||
enabled,
|
||||
@@ -217,28 +292,28 @@ function getCommandsConfig(projectPath: string): CommandsConfig {
|
||||
// Scan project commands
|
||||
const projectDir = getCommandsDir('project', projectPath);
|
||||
const projectDisabledDir = getDisabledCommandsDir('project', projectPath);
|
||||
|
||||
|
||||
// Enabled project commands
|
||||
const enabledProject = scanCommandsRecursive(projectDir, projectDir, 'project', true);
|
||||
const enabledProject = scanCommandsRecursive(projectDir, projectDir, 'project', true, projectPath);
|
||||
result.projectCommands.push(...enabledProject);
|
||||
|
||||
|
||||
// Disabled project commands
|
||||
if (existsSync(projectDisabledDir)) {
|
||||
const disabledProject = scanCommandsRecursive(projectDisabledDir, projectDisabledDir, 'project', false);
|
||||
const disabledProject = scanCommandsRecursive(projectDisabledDir, projectDisabledDir, 'project', false, projectPath);
|
||||
result.projectCommands.push(...disabledProject);
|
||||
}
|
||||
|
||||
// Scan user commands
|
||||
const userDir = getCommandsDir('user', projectPath);
|
||||
const userDisabledDir = getDisabledCommandsDir('user', projectPath);
|
||||
|
||||
|
||||
// Enabled user commands
|
||||
const enabledUser = scanCommandsRecursive(userDir, userDir, 'user', true);
|
||||
const enabledUser = scanCommandsRecursive(userDir, userDir, 'user', true, projectPath);
|
||||
result.userCommands.push(...enabledUser);
|
||||
|
||||
|
||||
// Disabled user commands
|
||||
if (existsSync(userDisabledDir)) {
|
||||
const disabledUser = scanCommandsRecursive(userDisabledDir, userDisabledDir, 'user', false);
|
||||
const disabledUser = scanCommandsRecursive(userDisabledDir, userDisabledDir, 'user', false, projectPath);
|
||||
result.userCommands.push(...disabledUser);
|
||||
}
|
||||
|
||||
@@ -449,8 +524,17 @@ export async function handleCommandsRoutes(ctx: RouteContext): Promise<boolean>
|
||||
});
|
||||
|
||||
const config = getCommandsConfig(validatedProjectPath);
|
||||
|
||||
// Include groups config from both project and user
|
||||
const projectGroupsConfig = loadGroupsConfig('project', validatedProjectPath);
|
||||
const userGroupsConfig = loadGroupsConfig('user', validatedProjectPath);
|
||||
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify(config));
|
||||
res.end(JSON.stringify({
|
||||
...config,
|
||||
projectGroupsConfig,
|
||||
userGroupsConfig
|
||||
}));
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
const status = message.includes('Access denied') ? 403 : 400;
|
||||
@@ -513,5 +597,78 @@ export async function handleCommandsRoutes(ctx: RouteContext): Promise<boolean>
|
||||
return true;
|
||||
}
|
||||
|
||||
// GET /api/commands/groups - Get groups configuration
|
||||
if (pathname === '/api/commands/groups' && req.method === 'GET') {
|
||||
const projectPathParam = url.searchParams.get('path') || initialPath;
|
||||
const location = url.searchParams.get('location') || 'project';
|
||||
|
||||
try {
|
||||
const validatedProjectPath = await validateAllowedPath(projectPathParam, {
|
||||
mustExist: true,
|
||||
allowedDirectories: [initialPath]
|
||||
});
|
||||
|
||||
if (location !== 'project' && location !== 'user') {
|
||||
res.writeHead(400, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: 'Invalid location' }));
|
||||
return true;
|
||||
}
|
||||
|
||||
const groupsConfig = loadGroupsConfig(location as CommandLocation, validatedProjectPath);
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify(groupsConfig));
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
const status = message.includes('Access denied') ? 403 : 400;
|
||||
res.writeHead(status, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: message }));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// PUT /api/commands/groups - Update groups configuration
|
||||
if (pathname === '/api/commands/groups' && req.method === 'PUT') {
|
||||
const projectPathParam = url.searchParams.get('path') || initialPath;
|
||||
const location = url.searchParams.get('location') || 'project';
|
||||
|
||||
handlePostRequest(req, res, async (body) => {
|
||||
try {
|
||||
const validatedProjectPath = await validateAllowedPath(projectPathParam, {
|
||||
mustExist: true,
|
||||
allowedDirectories: [initialPath]
|
||||
});
|
||||
|
||||
if (location !== 'project' && location !== 'user') {
|
||||
return { error: 'Invalid location', status: 400 };
|
||||
}
|
||||
|
||||
if (!isRecord(body)) {
|
||||
return { error: 'Invalid request body', status: 400 };
|
||||
}
|
||||
|
||||
// Validate and save groups config
|
||||
const config: CommandGroupsConfig = {
|
||||
groups: isRecord(body.groups) ? body.groups as Record<string, GroupDefinition> : {},
|
||||
assignments: isRecord(body.assignments) ? body.assignments as Record<string, string> : {}
|
||||
};
|
||||
|
||||
saveGroupsConfig(location as CommandLocation, validatedProjectPath, config);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: 'Groups configuration updated',
|
||||
data: config,
|
||||
status: 200
|
||||
};
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
const status = message.includes('Access denied') ? 403 : 400;
|
||||
console.error(`[Commands] Failed to update groups config: ${message}`);
|
||||
return { error: message, status };
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Skills Routes Module
|
||||
* Handles all Skills-related API endpoints
|
||||
*/
|
||||
import { readFileSync, existsSync, readdirSync, statSync, unlinkSync, renameSync, writeFileSync, mkdirSync, cpSync, rmSync, promises as fsPromises } from 'fs';
|
||||
import { readFileSync, existsSync, readdirSync, statSync, unlinkSync, renameSync, promises as fsPromises } from 'fs';
|
||||
import { join } from 'path';
|
||||
import { homedir } from 'os';
|
||||
import { executeCliTool } from '../../tools/cli-executor.js';
|
||||
@@ -16,8 +16,6 @@ import type {
|
||||
SkillsConfig,
|
||||
SkillInfo,
|
||||
SkillFolderValidation,
|
||||
DisabledSkillInfo,
|
||||
DisabledSkillsConfig,
|
||||
DisabledSkillSummary,
|
||||
ExtendedSkillsConfig,
|
||||
SkillOperationResult
|
||||
@@ -40,106 +38,15 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
|
||||
// ========== Skills Helper Functions ==========
|
||||
|
||||
// ========== Disabled Skills Helper Functions ==========
|
||||
|
||||
/**
|
||||
* Get disabled skills directory path
|
||||
*/
|
||||
function getDisabledSkillsDir(location: SkillLocation, projectPath: string): string {
|
||||
if (location === 'project') {
|
||||
return join(projectPath, '.claude', '.disabled-skills');
|
||||
}
|
||||
return join(homedir(), '.claude', '.disabled-skills');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get disabled skills config file path
|
||||
*/
|
||||
function getDisabledSkillsConfigPath(location: SkillLocation, projectPath: string): string {
|
||||
if (location === 'project') {
|
||||
return join(projectPath, '.claude', 'disabled-skills.json');
|
||||
}
|
||||
return join(homedir(), '.claude', 'disabled-skills.json');
|
||||
}
|
||||
|
||||
/**
|
||||
* Load disabled skills configuration
|
||||
* Throws on JSON parse errors to surface config corruption
|
||||
*/
|
||||
function loadDisabledSkillsConfig(location: SkillLocation, projectPath: string): DisabledSkillsConfig {
|
||||
const configPath = getDisabledSkillsConfigPath(location, projectPath);
|
||||
|
||||
if (!existsSync(configPath)) {
|
||||
return { skills: {} };
|
||||
}
|
||||
|
||||
try {
|
||||
const content = readFileSync(configPath, 'utf8');
|
||||
const config = JSON.parse(content);
|
||||
return { skills: config.skills || {} };
|
||||
} catch (error) {
|
||||
// Throw on JSON parse errors to surface config corruption
|
||||
if (error instanceof SyntaxError) {
|
||||
throw new Error(`Config file corrupted: ${configPath}`);
|
||||
}
|
||||
// Log and return empty for other errors (permission, etc.)
|
||||
console.error(`[Skills] Failed to load disabled skills config: ${error}`);
|
||||
return { skills: {} };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save disabled skills configuration
|
||||
*/
|
||||
function saveDisabledSkillsConfig(location: SkillLocation, projectPath: string, config: DisabledSkillsConfig): void {
|
||||
const configPath = getDisabledSkillsConfigPath(location, projectPath);
|
||||
const configDir = join(configPath, '..');
|
||||
|
||||
if (!existsSync(configDir)) {
|
||||
mkdirSync(configDir, { recursive: true });
|
||||
}
|
||||
|
||||
writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf8');
|
||||
}
|
||||
|
||||
/**
|
||||
* Move directory with fallback to copy-delete and rollback on failure
|
||||
*/
|
||||
function moveDirectory(source: string, target: string): void {
|
||||
try {
|
||||
// Try atomic rename first
|
||||
renameSync(source, target);
|
||||
} catch (error: unknown) {
|
||||
const err = error as NodeJS.ErrnoException;
|
||||
// If rename fails (cross-filesystem, permission issues), fallback to copy-delete
|
||||
if (err.code === 'EXDEV' || err.code === 'EPERM' || err.code === 'EBUSY') {
|
||||
cpSync(source, target, { recursive: true, force: true });
|
||||
try {
|
||||
rmSync(source, { recursive: true, force: true });
|
||||
} catch (rmError) {
|
||||
// Rollback: remove the copied target directory to avoid duplicates
|
||||
try {
|
||||
rmSync(target, { recursive: true, force: true });
|
||||
} catch {
|
||||
// Ignore rollback errors
|
||||
}
|
||||
throw new Error(`Failed to remove source directory after copy: ${(rmError as Error).message}`);
|
||||
}
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable a skill by moving it to disabled directory
|
||||
* Disable a skill by renaming SKILL.md to SKILL.md.disabled
|
||||
*/
|
||||
async function disableSkill(
|
||||
skillName: string,
|
||||
location: SkillLocation,
|
||||
projectPath: string,
|
||||
initialPath: string,
|
||||
reason?: string
|
||||
reason?: string // Kept for API compatibility but no longer used
|
||||
): Promise<SkillOperationResult> {
|
||||
try {
|
||||
// Validate skill name
|
||||
@@ -147,7 +54,7 @@ async function disableSkill(
|
||||
return { success: false, message: 'Invalid skill name', status: 400 };
|
||||
}
|
||||
|
||||
// Get source directory
|
||||
// Get skill directory
|
||||
let skillsDir: string;
|
||||
if (location === 'project') {
|
||||
try {
|
||||
@@ -161,42 +68,23 @@ async function disableSkill(
|
||||
skillsDir = join(homedir(), '.claude', 'skills');
|
||||
}
|
||||
|
||||
const sourceDir = join(skillsDir, skillName);
|
||||
if (!existsSync(sourceDir)) {
|
||||
const skillDir = join(skillsDir, skillName);
|
||||
if (!existsSync(skillDir)) {
|
||||
return { success: false, message: 'Skill not found', status: 404 };
|
||||
}
|
||||
|
||||
// Get target directory
|
||||
const disabledDir = getDisabledSkillsDir(location, projectPath);
|
||||
if (!existsSync(disabledDir)) {
|
||||
mkdirSync(disabledDir, { recursive: true });
|
||||
const skillMdPath = join(skillDir, 'SKILL.md');
|
||||
if (!existsSync(skillMdPath)) {
|
||||
return { success: false, message: 'SKILL.md not found', status: 404 };
|
||||
}
|
||||
|
||||
const targetDir = join(disabledDir, skillName);
|
||||
if (existsSync(targetDir)) {
|
||||
return { success: false, message: 'Skill already exists in disabled directory', status: 409 };
|
||||
const disabledPath = join(skillDir, 'SKILL.md.disabled');
|
||||
if (existsSync(disabledPath)) {
|
||||
return { success: false, message: 'Skill already disabled', status: 409 };
|
||||
}
|
||||
|
||||
// Move skill to disabled directory
|
||||
moveDirectory(sourceDir, targetDir);
|
||||
|
||||
// Update config with rollback on failure
|
||||
try {
|
||||
const config = loadDisabledSkillsConfig(location, projectPath);
|
||||
config.skills[skillName] = {
|
||||
disabledAt: new Date().toISOString(),
|
||||
reason
|
||||
};
|
||||
saveDisabledSkillsConfig(location, projectPath, config);
|
||||
} catch (configError) {
|
||||
// Rollback: move the skill back to original location
|
||||
try {
|
||||
moveDirectory(targetDir, sourceDir);
|
||||
} catch {
|
||||
// Ignore rollback errors - skill is in disabled directory but not in config
|
||||
}
|
||||
throw new Error(`Failed to update config: ${(configError as Error).message}`);
|
||||
}
|
||||
// Rename: SKILL.md → SKILL.md.disabled
|
||||
renameSync(skillMdPath, disabledPath);
|
||||
|
||||
return { success: true, message: 'Skill disabled', skillName, location };
|
||||
} catch (error) {
|
||||
@@ -205,7 +93,7 @@ async function disableSkill(
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable a skill by moving it back from disabled directory
|
||||
* Enable a skill by renaming SKILL.md.disabled back to SKILL.md
|
||||
*/
|
||||
async function enableSkill(
|
||||
skillName: string,
|
||||
@@ -219,14 +107,7 @@ async function enableSkill(
|
||||
return { success: false, message: 'Invalid skill name', status: 400 };
|
||||
}
|
||||
|
||||
// Get source directory (disabled)
|
||||
const disabledDir = getDisabledSkillsDir(location, projectPath);
|
||||
const sourceDir = join(disabledDir, skillName);
|
||||
if (!existsSync(sourceDir)) {
|
||||
return { success: false, message: 'Disabled skill not found', status: 404 };
|
||||
}
|
||||
|
||||
// Get target directory (skills)
|
||||
// Get skill directory
|
||||
let skillsDir: string;
|
||||
if (location === 'project') {
|
||||
try {
|
||||
@@ -240,33 +121,24 @@ async function enableSkill(
|
||||
skillsDir = join(homedir(), '.claude', 'skills');
|
||||
}
|
||||
|
||||
if (!existsSync(skillsDir)) {
|
||||
mkdirSync(skillsDir, { recursive: true });
|
||||
const skillDir = join(skillsDir, skillName);
|
||||
if (!existsSync(skillDir)) {
|
||||
return { success: false, message: 'Skill not found', status: 404 };
|
||||
}
|
||||
|
||||
const targetDir = join(skillsDir, skillName);
|
||||
if (existsSync(targetDir)) {
|
||||
return { success: false, message: 'Skill already exists in skills directory', status: 409 };
|
||||
const disabledPath = join(skillDir, 'SKILL.md.disabled');
|
||||
if (!existsSync(disabledPath)) {
|
||||
return { success: false, message: 'Disabled skill not found', status: 404 };
|
||||
}
|
||||
|
||||
// Move skill back to skills directory
|
||||
moveDirectory(sourceDir, targetDir);
|
||||
|
||||
// Update config with rollback on failure
|
||||
try {
|
||||
const config = loadDisabledSkillsConfig(location, projectPath);
|
||||
delete config.skills[skillName];
|
||||
saveDisabledSkillsConfig(location, projectPath, config);
|
||||
} catch (configError) {
|
||||
// Rollback: move the skill back to disabled directory
|
||||
try {
|
||||
moveDirectory(targetDir, sourceDir);
|
||||
} catch {
|
||||
// Ignore rollback errors - skill is in skills directory but still in config
|
||||
}
|
||||
throw new Error(`Failed to update config: ${(configError as Error).message}`);
|
||||
const skillMdPath = join(skillDir, 'SKILL.md');
|
||||
if (existsSync(skillMdPath)) {
|
||||
return { success: false, message: 'Skill already enabled', status: 409 };
|
||||
}
|
||||
|
||||
// Rename: SKILL.md.disabled → SKILL.md
|
||||
renameSync(disabledPath, skillMdPath);
|
||||
|
||||
return { success: true, message: 'Skill enabled', skillName, location };
|
||||
} catch (error) {
|
||||
return { success: false, message: (error as Error).message, status: 500 };
|
||||
@@ -274,28 +146,33 @@ async function enableSkill(
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of disabled skills
|
||||
* Get list of disabled skills by checking for SKILL.md.disabled files
|
||||
*/
|
||||
function getDisabledSkillsList(location: SkillLocation, projectPath: string): DisabledSkillSummary[] {
|
||||
const disabledDir = getDisabledSkillsDir(location, projectPath);
|
||||
const config = loadDisabledSkillsConfig(location, projectPath);
|
||||
const result: DisabledSkillSummary[] = [];
|
||||
|
||||
if (!existsSync(disabledDir)) {
|
||||
// Get skills directory (not a separate disabled directory)
|
||||
let skillsDir: string;
|
||||
if (location === 'project') {
|
||||
skillsDir = join(projectPath, '.claude', 'skills');
|
||||
} else {
|
||||
skillsDir = join(homedir(), '.claude', 'skills');
|
||||
}
|
||||
|
||||
if (!existsSync(skillsDir)) {
|
||||
return result;
|
||||
}
|
||||
|
||||
try {
|
||||
const skills = readdirSync(disabledDir, { withFileTypes: true });
|
||||
const skills = readdirSync(skillsDir, { withFileTypes: true });
|
||||
for (const skill of skills) {
|
||||
if (skill.isDirectory()) {
|
||||
const skillMdPath = join(disabledDir, skill.name, 'SKILL.md');
|
||||
if (existsSync(skillMdPath)) {
|
||||
const content = readFileSync(skillMdPath, 'utf8');
|
||||
const disabledPath = join(skillsDir, skill.name, 'SKILL.md.disabled');
|
||||
if (existsSync(disabledPath)) {
|
||||
const content = readFileSync(disabledPath, 'utf8');
|
||||
const parsed = parseSkillFrontmatter(content);
|
||||
const skillDir = join(disabledDir, skill.name);
|
||||
const skillDir = join(skillsDir, skill.name);
|
||||
const supportingFiles = getSupportingFiles(skillDir);
|
||||
const disabledInfo = config.skills[skill.name] || { disabledAt: new Date().toISOString() };
|
||||
|
||||
result.push({
|
||||
name: parsed.name || skill.name,
|
||||
@@ -306,8 +183,8 @@ function getDisabledSkillsList(location: SkillLocation, projectPath: string): Di
|
||||
location,
|
||||
path: skillDir,
|
||||
supportingFiles,
|
||||
disabledAt: disabledInfo.disabledAt,
|
||||
reason: disabledInfo.reason
|
||||
disabledAt: new Date().toISOString(), // Cannot get exact time without config file
|
||||
reason: undefined // No longer stored
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -396,7 +273,8 @@ function getSupportingFiles(skillDir: string): string[] {
|
||||
try {
|
||||
const entries = readdirSync(skillDir, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
if (entry.name !== 'SKILL.md') {
|
||||
// Exclude SKILL.md and SKILL.md.disabled from supporting files
|
||||
if (entry.name !== 'SKILL.md' && entry.name !== 'SKILL.md.disabled') {
|
||||
if (entry.isFile()) {
|
||||
files.push(entry.name);
|
||||
} else if (entry.isDirectory()) {
|
||||
|
||||
@@ -164,6 +164,7 @@ const MODULE_FILES = [
|
||||
'views/prompt-history.js',
|
||||
'views/skills-manager.js',
|
||||
'views/rules-manager.js',
|
||||
'views/commands-manager.js',
|
||||
'views/claude-manager.js',
|
||||
'views/api-settings.js',
|
||||
'views/help.js',
|
||||
|
||||
@@ -1603,6 +1603,46 @@ const i18n = {
|
||||
|
||||
// Rules
|
||||
'nav.rules': 'Rules',
|
||||
'nav.commands': 'Commands',
|
||||
'title.commandsManager': 'Commands Manager',
|
||||
'commands.title': 'Commands Manager',
|
||||
'commands.description': 'Manage Claude Code commands - enable, disable, and organize by group',
|
||||
'commands.totalCommands': 'Total Commands',
|
||||
'commands.enabledCommands': 'Enabled Commands',
|
||||
'commands.disabledCommands': 'Disabled Commands',
|
||||
'commands.showDisabled': 'Show Disabled',
|
||||
'commands.hideDisabled': 'Hide Disabled',
|
||||
'commands.noDescription': 'No description',
|
||||
'commands.disabledAt': 'Disabled at',
|
||||
'commands.enableConfirm': 'Enable command "{name}"?',
|
||||
'commands.disableConfirm': 'Disable command "{name}"?',
|
||||
'commands.enableSuccess': 'Command "{name}" enabled successfully',
|
||||
'commands.disableSuccess': 'Command "{name}" disabled successfully',
|
||||
'commands.toggleError': 'Failed to toggle command status',
|
||||
'commands.enabled': 'enabled',
|
||||
'commands.disabled': 'disabled',
|
||||
'commands.name': 'Name',
|
||||
'commands.description': 'Description',
|
||||
'commands.scope': 'Scope',
|
||||
'commands.status': 'Status',
|
||||
'commands.group.cli': 'CLI',
|
||||
'commands.group.workflow': 'Workflow',
|
||||
'commands.group.memory': 'Memory',
|
||||
'commands.group.task': 'Task',
|
||||
'commands.group.issue': 'Issue',
|
||||
'commands.group.other': 'Other',
|
||||
'commands.enableAll': 'Enable All',
|
||||
'commands.disableAll': 'Disable All',
|
||||
'commands.enableGroupConfirm': 'Enable all commands in "{group}" group?',
|
||||
'commands.disableGroupConfirm': 'Disable all commands in "{group}" group?',
|
||||
'commands.enableGroupSuccess': 'Group "{group}" enabled successfully',
|
||||
'commands.disableGroupSuccess': 'Group "{group}" disabled successfully',
|
||||
'commands.locationProject': 'Project',
|
||||
'commands.locationUser': 'Global',
|
||||
'commands.clickToEnableAll': 'Click to enable all commands in this group',
|
||||
'commands.clickToDisableAll': 'Click to disable all commands in this group',
|
||||
|
||||
// Rules
|
||||
'title.rulesManager': 'Rules Manager',
|
||||
'rules.title': 'Rules Manager',
|
||||
'rules.description': 'Manage project and user rules for Claude Code',
|
||||
@@ -4239,6 +4279,46 @@ const i18n = {
|
||||
|
||||
// Rules
|
||||
'nav.rules': '规则',
|
||||
'nav.commands': '命令',
|
||||
'title.commandsManager': '命令管理',
|
||||
'commands.title': '命令管理',
|
||||
'commands.description': '管理 Claude Code 命令 - 启用、禁用和按组织分组',
|
||||
'commands.totalCommands': '总命令数',
|
||||
'commands.enabledCommands': '已启用命令',
|
||||
'commands.disabledCommands': '已禁用命令',
|
||||
'commands.showDisabled': '显示已禁用',
|
||||
'commands.hideDisabled': '隐藏已禁用',
|
||||
'commands.noDescription': '无描述',
|
||||
'commands.disabledAt': '禁用于',
|
||||
'commands.enableConfirm': '启用命令 "{name}"?',
|
||||
'commands.disableConfirm': '禁用命令 "{name}"?',
|
||||
'commands.enableSuccess': '命令 "{name}" 已成功启用',
|
||||
'commands.disableSuccess': '命令 "{name}" 已成功禁用',
|
||||
'commands.toggleError': '切换命令状态失败',
|
||||
'commands.enabled': '已启用',
|
||||
'commands.disabled': '已禁用',
|
||||
'commands.name': '名称',
|
||||
'commands.description': '描述',
|
||||
'commands.scope': '作用域',
|
||||
'commands.status': '状态',
|
||||
'commands.group.cli': 'CLI',
|
||||
'commands.group.workflow': '工作流',
|
||||
'commands.group.memory': '记忆',
|
||||
'commands.group.task': '任务',
|
||||
'commands.group.issue': '问题',
|
||||
'commands.group.other': '其他',
|
||||
'commands.enableAll': '全部启用',
|
||||
'commands.disableAll': '全部禁用',
|
||||
'commands.enableGroupConfirm': '启用 "{group}" 分组中的所有命令?',
|
||||
'commands.disableGroupConfirm': '禁用 "{group}" 分组中的所有命令?',
|
||||
'commands.enableGroupSuccess': '分组 "{group}" 已全部启用',
|
||||
'commands.disableGroupSuccess': '分组 "{group}" 已全部禁用',
|
||||
'commands.locationProject': '项目',
|
||||
'commands.locationUser': '全局',
|
||||
'commands.clickToEnableAll': '点击启用该分组所有命令',
|
||||
'commands.clickToDisableAll': '点击禁用该分组所有命令',
|
||||
|
||||
// Rules
|
||||
'title.rulesManager': '规则管理',
|
||||
'rules.title': '规则管理',
|
||||
'rules.description': '管理 Claude Code 的项目和用户规则',
|
||||
|
||||
@@ -4,7 +4,9 @@
|
||||
// ========== Commands State ==========
|
||||
var commandsData = {
|
||||
groups: {}, // Organized by group name: { cli: [...], workflow: [...], memory: [...], task: [...], issue: [...] }
|
||||
allCommands: []
|
||||
allCommands: [],
|
||||
projectGroupsConfig: { groups: {}, assignments: {} },
|
||||
userGroupsConfig: { groups: {}, assignments: {} }
|
||||
};
|
||||
var expandedGroups = {
|
||||
cli: true,
|
||||
@@ -15,6 +17,7 @@ var expandedGroups = {
|
||||
};
|
||||
var showDisabledCommands = false;
|
||||
var commandsLoading = false;
|
||||
var currentLocation = 'project'; // 'project' or 'user'
|
||||
|
||||
// ========== Main Render Function ==========
|
||||
async function renderCommandsManager() {
|
||||
@@ -47,11 +50,20 @@ async function loadCommandsData() {
|
||||
if (!response.ok) throw new Error('Failed to load commands');
|
||||
const data = await response.json();
|
||||
|
||||
// Store groups config
|
||||
commandsData.projectGroupsConfig = data.projectGroupsConfig || { groups: {}, assignments: {} };
|
||||
commandsData.userGroupsConfig = data.userGroupsConfig || { groups: {}, assignments: {} };
|
||||
|
||||
// Filter commands based on currentLocation
|
||||
const allCommands = currentLocation === 'project'
|
||||
? (data.projectCommands || [])
|
||||
: (data.userCommands || []);
|
||||
|
||||
// Organize commands by group
|
||||
commandsData.groups = {};
|
||||
commandsData.allCommands = data.commands || [];
|
||||
commandsData.allCommands = allCommands;
|
||||
|
||||
data.commands.forEach(cmd => {
|
||||
allCommands.forEach(cmd => {
|
||||
const group = cmd.group || 'other';
|
||||
if (!commandsData.groups[group]) {
|
||||
commandsData.groups[group] = [];
|
||||
@@ -63,7 +75,12 @@ async function loadCommandsData() {
|
||||
updateCommandsBadge();
|
||||
} catch (err) {
|
||||
console.error('Failed to load commands:', err);
|
||||
commandsData = { groups: {}, allCommands: [] };
|
||||
commandsData = {
|
||||
groups: {},
|
||||
allCommands: [],
|
||||
projectGroupsConfig: { groups: {}, assignments: {} },
|
||||
userGroupsConfig: { groups: {}, assignments: {} }
|
||||
};
|
||||
} finally {
|
||||
commandsLoading = false;
|
||||
}
|
||||
@@ -77,12 +94,47 @@ function updateCommandsBadge() {
|
||||
}
|
||||
}
|
||||
|
||||
async function switchLocation(location) {
|
||||
if (location === currentLocation) return;
|
||||
currentLocation = location;
|
||||
await loadCommandsData();
|
||||
renderCommandsView();
|
||||
}
|
||||
|
||||
function renderCommandsView() {
|
||||
const container = document.getElementById('mainContent');
|
||||
if (!container) return;
|
||||
|
||||
const groups = commandsData.groups || {};
|
||||
const groupNames = ['cli', 'workflow', 'memory', 'task', 'issue', 'other'];
|
||||
|
||||
// Dynamic groups: known groups first, then custom groups hierarchically sorted, 'other' last
|
||||
const knownOrder = ['cli', 'workflow', 'memory', 'task', 'issue'];
|
||||
const allGroupNames = Object.keys(groups);
|
||||
|
||||
// Separate top-level known groups and nested groups
|
||||
const topLevelKnown = allGroupNames.filter(g => knownOrder.includes(g));
|
||||
const nestedAndCustom = allGroupNames.filter(g => g !== 'other' && !knownOrder.includes(g));
|
||||
|
||||
// Sort nested/custom groups hierarchically
|
||||
nestedAndCustom.sort((a, b) => {
|
||||
// Split by path separator
|
||||
const aParts = a.split('/');
|
||||
const bParts = b.split('/');
|
||||
|
||||
// Compare level by level
|
||||
for (let i = 0; i < Math.min(aParts.length, bParts.length); i++) {
|
||||
if (aParts[i] !== bParts[i]) {
|
||||
return aParts[i].localeCompare(bParts[i]);
|
||||
}
|
||||
}
|
||||
|
||||
// If all parts are equal, shorter path comes first
|
||||
return aParts.length - bParts.length;
|
||||
});
|
||||
|
||||
const groupNames = [...topLevelKnown.filter(g => groups[g] && groups[g].length > 0),
|
||||
...nestedAndCustom.filter(g => groups[g] && groups[g].length > 0),
|
||||
'other'].filter(g => groups[g] && groups[g].length > 0);
|
||||
const totalEnabled = commandsData.allCommands.filter(cmd => cmd.enabled).length;
|
||||
const totalDisabled = commandsData.allCommands.filter(cmd => !cmd.enabled).length;
|
||||
|
||||
@@ -100,11 +152,27 @@ function renderCommandsView() {
|
||||
<p class="text-sm text-muted-foreground">${t('commands.description') || 'Enable/disable CCW commands'}</p>
|
||||
</div>
|
||||
</div>
|
||||
<button class="px-4 py-2 text-sm ${showDisabledCommands ? 'bg-primary text-primary-foreground' : 'bg-muted text-muted-foreground'} rounded-lg hover:opacity-90 transition-opacity flex items-center gap-2"
|
||||
onclick="toggleShowDisabledCommands()">
|
||||
<i data-lucide="${showDisabledCommands ? 'eye' : 'eye-off'}" class="w-4 h-4"></i>
|
||||
${showDisabledCommands ? (t('commands.hideDisabled') || 'Hide Disabled') : (t('commands.showDisabled') || 'Show Disabled')} (${totalDisabled})
|
||||
</button>
|
||||
<div class="flex items-center gap-2">
|
||||
<!-- Location Switcher -->
|
||||
<div class="inline-flex bg-muted rounded-lg p-1">
|
||||
<button class="px-3 py-1.5 text-sm rounded-md transition-all ${currentLocation === 'project' ? 'bg-background text-foreground shadow-sm' : 'text-muted-foreground hover:text-foreground'}"
|
||||
onclick="switchLocation('project')">
|
||||
<i data-lucide="folder" class="w-3.5 h-3.5 inline mr-1"></i>
|
||||
${t('commands.locationProject') || 'Project'}
|
||||
</button>
|
||||
<button class="px-3 py-1.5 text-sm rounded-md transition-all ${currentLocation === 'user' ? 'bg-background text-foreground shadow-sm' : 'text-muted-foreground hover:text-foreground'}"
|
||||
onclick="switchLocation('user')">
|
||||
<i data-lucide="user" class="w-3.5 h-3.5 inline mr-1"></i>
|
||||
${t('commands.locationUser') || 'Global'}
|
||||
</button>
|
||||
</div>
|
||||
<!-- Show Disabled Toggle -->
|
||||
<button class="px-4 py-2 text-sm ${showDisabledCommands ? 'bg-primary text-primary-foreground' : 'bg-muted text-muted-foreground'} rounded-lg hover:opacity-90 transition-opacity flex items-center gap-2"
|
||||
onclick="toggleShowDisabledCommands()">
|
||||
<i data-lucide="${showDisabledCommands ? 'eye' : 'eye-off'}" class="w-4 h-4"></i>
|
||||
${showDisabledCommands ? (t('commands.hideDisabled') || 'Hide Disabled') : (t('commands.showDisabled') || 'Show Disabled')} (${totalDisabled})
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -128,11 +196,7 @@ function renderCommandsView() {
|
||||
|
||||
<!-- Accordion Groups -->
|
||||
<div class="commands-accordion">
|
||||
${groupNames.map(groupName => {
|
||||
const commands = groups[groupName] || [];
|
||||
if (commands.length === 0) return '';
|
||||
return renderAccordionGroup(groupName, commands);
|
||||
}).join('')}
|
||||
${groupNames.map(groupName => renderAccordionGroup(groupName, groups[groupName])).join('')}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -142,6 +206,8 @@ function renderCommandsView() {
|
||||
}
|
||||
|
||||
function renderAccordionGroup(groupName, commands) {
|
||||
// Default to expanded for new/custom groups
|
||||
if (expandedGroups[groupName] === undefined) expandedGroups[groupName] = true;
|
||||
const isExpanded = expandedGroups[groupName];
|
||||
const enabledCommands = commands.filter(cmd => cmd.enabled);
|
||||
const disabledCommands = commands.filter(cmd => !cmd.enabled);
|
||||
@@ -177,26 +243,53 @@ function renderAccordionGroup(groupName, commands) {
|
||||
return `
|
||||
<div class="accordion-group mb-4">
|
||||
<!-- Group Header -->
|
||||
<div class="accordion-header flex items-center justify-between px-4 py-3 bg-card border border-border rounded-lg cursor-pointer hover:bg-hover transition-colors"
|
||||
onclick="toggleAccordionGroup('${groupName}')">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="accordion-header flex items-center justify-between px-4 py-3 bg-card border border-border rounded-lg hover:bg-hover transition-colors">
|
||||
<div class="flex items-center gap-3 flex-1 cursor-pointer" onclick="toggleAccordionGroup('${groupName}')">
|
||||
<i data-lucide="${isExpanded ? 'chevron-down' : 'chevron-right'}" class="w-5 h-5 text-muted-foreground transition-transform"></i>
|
||||
<div class="w-8 h-8 ${colorClass} rounded-lg flex items-center justify-center">
|
||||
<i data-lucide="${icon}" class="w-4 h-4"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-base font-semibold text-foreground capitalize">${groupName}</h3>
|
||||
<p class="text-xs text-muted-foreground">${enabledCommands.length}/${commands.length} enabled</p>
|
||||
<h3 class="text-base font-semibold text-foreground capitalize">${t('commands.group.' + groupName) || groupName}</h3>
|
||||
<p class="text-xs text-muted-foreground">${enabledCommands.length}/${commands.length} ${t('commands.enabled') || 'enabled'}</p>
|
||||
</div>
|
||||
</div>
|
||||
<span class="text-xs px-2 py-1 bg-muted rounded-full text-muted-foreground">${commands.length}</span>
|
||||
<div class="flex items-center gap-3">
|
||||
<!-- Group Toggle Switch -->
|
||||
<label class="group-toggle-switch relative inline-flex items-center cursor-pointer" title="${enabledCommands.length === commands.length ? (t('commands.clickToDisableAll') || 'Click to disable all') : (t('commands.clickToEnableAll') || 'Click to enable all')}">
|
||||
<input type="checkbox"
|
||||
class="sr-only peer"
|
||||
${enabledCommands.length === commands.length ? 'checked' : ''}
|
||||
onchange="toggleGroupEnabled('${groupName}', ${enabledCommands.length === commands.length})">
|
||||
<div class="w-11 h-6 bg-muted peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-success"></div>
|
||||
</label>
|
||||
<span class="text-xs px-2 py-1 bg-muted rounded-full text-muted-foreground">${commands.length}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Group Content (Cards Grid) -->
|
||||
<!-- Group Content (Compact Table) -->
|
||||
${isExpanded ? `
|
||||
<div class="accordion-content mt-3">
|
||||
<div class="commands-grid grid gap-3" style="grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));">
|
||||
${visibleCommands.map(cmd => renderCommandCard(cmd)).join('')}
|
||||
<div class="bg-card border border-border rounded-lg overflow-hidden">
|
||||
<table class="w-full commands-table" style="table-layout: fixed;">
|
||||
<colgroup>
|
||||
<col style="width: 200px;">
|
||||
<col style="width: auto;">
|
||||
<col style="width: 100px;">
|
||||
<col style="width: 80px;">
|
||||
</colgroup>
|
||||
<thead class="bg-muted/30 border-b border-border">
|
||||
<tr>
|
||||
<th class="px-4 py-2 text-left text-xs font-medium text-muted-foreground uppercase">${t('commands.name') || 'Name'}</th>
|
||||
<th class="px-4 py-2 text-left text-xs font-medium text-muted-foreground uppercase">${t('commands.description') || 'Description'}</th>
|
||||
<th class="px-4 py-2 text-center text-xs font-medium text-muted-foreground uppercase">${t('commands.scope') || 'Scope'}</th>
|
||||
<th class="px-4 py-2 text-center text-xs font-medium text-muted-foreground uppercase">${t('commands.status') || 'Status'}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-border">
|
||||
${visibleCommands.map(cmd => renderCommandRow(cmd)).join('')}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
` : ''}
|
||||
@@ -204,53 +297,40 @@ function renderAccordionGroup(groupName, commands) {
|
||||
`;
|
||||
}
|
||||
|
||||
function renderCommandCard(command) {
|
||||
function renderCommandRow(command) {
|
||||
const isDisabled = !command.enabled;
|
||||
const cardOpacity = isDisabled ? 'opacity-60' : '';
|
||||
|
||||
return `
|
||||
<div class="command-card bg-card border border-border rounded-lg p-4 hover:shadow-md transition-all ${cardOpacity}">
|
||||
<div class="flex items-start justify-between mb-3">
|
||||
<div class="flex-1 min-w-0">
|
||||
<h4 class="font-semibold text-foreground truncate">${escapeHtml(command.name)}</h4>
|
||||
<span class="text-xs px-2 py-0.5 rounded-full ${getGroupBadgeClass(command.group)} inline-block mt-1">
|
||||
${command.group || 'other'}
|
||||
</span>
|
||||
</div>
|
||||
<div class="ml-2 flex-shrink-0">
|
||||
<label class="command-toggle-switch relative inline-block w-11 h-6 cursor-pointer">
|
||||
<input type="checkbox"
|
||||
class="sr-only"
|
||||
${command.enabled ? 'checked' : ''}
|
||||
onchange="toggleCommandEnabled('${escapeHtml(command.name)}', ${command.enabled})"
|
||||
data-command-toggle="${escapeHtml(command.name)}">
|
||||
<span class="command-toggle-slider absolute inset-0 rounded-full transition-all duration-200 ${command.enabled ? 'bg-success' : 'bg-muted'}"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="text-sm text-muted-foreground mb-3 line-clamp-2">${escapeHtml(command.description || t('commands.noDescription') || 'No description available')}</p>
|
||||
|
||||
<div class="flex items-center justify-between text-xs text-muted-foreground">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="flex items-center gap-1">
|
||||
<i data-lucide="folder" class="w-3 h-3"></i>
|
||||
${command.scope || 'project'}
|
||||
</span>
|
||||
<tr class="hover:bg-muted/20 transition-colors ${isDisabled ? 'opacity-60' : ''}">
|
||||
<td class="px-4 py-3 text-sm font-medium text-foreground">
|
||||
<div class="flex items-center gap-2 flex-wrap">
|
||||
<span class="break-words">${escapeHtml(command.name)}</span>
|
||||
${command.triggers && command.triggers.length > 0 ? `
|
||||
<span class="flex items-center gap-1">
|
||||
<i data-lucide="zap" class="w-3 h-3"></i>
|
||||
${command.triggers.length} trigger${command.triggers.length > 1 ? 's' : ''}
|
||||
<span class="text-xs px-1.5 py-0.5 bg-warning/10 text-warning rounded flex-shrink-0" title="${command.triggers.length} trigger(s)">
|
||||
<i data-lucide="zap" class="w-3 h-3 inline mr-0.5"></i>${command.triggers.length}
|
||||
</span>
|
||||
` : ''}
|
||||
</div>
|
||||
${isDisabled && command.disabledAt ? `
|
||||
<span class="text-xs text-muted-foreground/70">
|
||||
${t('commands.disabledAt') || 'Disabled'}: ${formatDisabledDate(command.disabledAt)}
|
||||
</span>
|
||||
` : ''}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-sm text-muted-foreground">
|
||||
<div class="line-clamp-3 break-words">${escapeHtml(command.description || t('commands.noDescription') || '-')}</div>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-center text-xs text-muted-foreground">
|
||||
<span class="whitespace-nowrap">${command.scope || 'project'}</span>
|
||||
</td>
|
||||
<td class="px-4 py-3">
|
||||
<div class="flex justify-center">
|
||||
<label class="command-toggle-switch relative inline-flex items-center cursor-pointer">
|
||||
<input type="checkbox"
|
||||
class="sr-only peer"
|
||||
${command.enabled ? 'checked' : ''}
|
||||
onchange="toggleCommandEnabled('${escapeHtml(command.name)}', ${command.enabled})"
|
||||
data-command-toggle="${escapeHtml(command.name)}">
|
||||
<div class="w-11 h-6 bg-muted peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-primary"></div>
|
||||
</label>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -284,20 +364,6 @@ async function toggleCommandEnabled(commandName, currentlyEnabled) {
|
||||
var loadingKey = commandName;
|
||||
if (toggleLoadingCommands[loadingKey]) return;
|
||||
|
||||
var action = currentlyEnabled ? 'disable' : 'enable';
|
||||
var confirmMessage = currentlyEnabled
|
||||
? t('commands.disableConfirm', { name: commandName }) || `Disable command "${commandName}"?`
|
||||
: t('commands.enableConfirm', { name: commandName }) || `Enable command "${commandName}"?`;
|
||||
|
||||
if (!confirm(confirmMessage)) {
|
||||
// Reset toggle state if user cancels
|
||||
const toggleInput = document.querySelector(`[data-command-toggle="${commandName}"]`);
|
||||
if (toggleInput) {
|
||||
toggleInput.checked = currentlyEnabled;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Set loading state
|
||||
toggleLoadingCommands[loadingKey] = true;
|
||||
var toggleInput = document.querySelector('[data-command-toggle="' + commandName + '"]');
|
||||
@@ -306,10 +372,14 @@ async function toggleCommandEnabled(commandName, currentlyEnabled) {
|
||||
}
|
||||
|
||||
try {
|
||||
var response = await fetch('/api/commands/' + encodeURIComponent(commandName) + '/' + action, {
|
||||
var response = await fetch('/api/commands/' + encodeURIComponent(commandName) + '/toggle', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ projectPath: projectPath })
|
||||
body: JSON.stringify({
|
||||
projectPath: projectPath,
|
||||
location: currentLocation,
|
||||
enable: !currentlyEnabled
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
@@ -352,6 +422,50 @@ async function toggleCommandEnabled(commandName, currentlyEnabled) {
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleGroupEnabled(groupName, currentlyAllEnabled) {
|
||||
const enable = !currentlyAllEnabled;
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/commands/group/' + encodeURIComponent(groupName) + '/toggle', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
projectPath: projectPath,
|
||||
location: currentLocation,
|
||||
enable: enable
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
var errorMessage = 'Operation failed';
|
||||
try {
|
||||
var error = await response.json();
|
||||
errorMessage = error.message || errorMessage;
|
||||
} catch (jsonErr) {
|
||||
errorMessage = response.statusText || errorMessage;
|
||||
}
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
// Reload commands data
|
||||
await loadCommandsData();
|
||||
renderCommandsView();
|
||||
|
||||
if (window.showToast) {
|
||||
const groupLabel = t('commands.group.' + groupName) || groupName;
|
||||
const message = enable
|
||||
? (t('commands.enableGroupSuccess', { group: groupLabel }) || `Group "${groupLabel}" enabled`)
|
||||
: (t('commands.disableGroupSuccess', { group: groupLabel }) || `Group "${groupLabel}" disabled`);
|
||||
showToast(message, 'success');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to toggle group:', err);
|
||||
if (window.showToast) {
|
||||
showToast(err.message || t('commands.toggleError') || 'Failed to toggle group', 'error');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function formatDisabledDate(isoString) {
|
||||
try {
|
||||
const date = new Date(isoString);
|
||||
|
||||
@@ -8,25 +8,6 @@
|
||||
*/
|
||||
export type SkillLocation = 'project' | 'user';
|
||||
|
||||
/**
|
||||
* Information about a disabled skill
|
||||
*/
|
||||
export interface DisabledSkillInfo {
|
||||
/** When the skill was disabled */
|
||||
disabledAt: string;
|
||||
/** Optional reason for disabling */
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration for disabled skills
|
||||
* Stored in disabled-skills.json
|
||||
*/
|
||||
export interface DisabledSkillsConfig {
|
||||
/** Map of skill name to disabled info */
|
||||
skills: Record<string, DisabledSkillInfo>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Result of a skill operation (enable/disable)
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user