Files
ccg-workflow/install.py
T
li 53fcade330 feat: 新增 TypeScript CLI 安装系统,支持 npx 一键安装
- 重构为 TypeScript + unbuild 构建系统
- 新增交互式安装菜单(初始化/更新/卸载)
- 支持 npx ccg-workflow 直接运行
- 已发布至 npm: ccg-workflow@1.0.0
- 新增 Claude 角色提示词(6个专家角色)
- 更新命令模板支持三模型协作(Claude + Codex + Gemini)
- 更新 README 文档
2026-01-05 14:44:01 +08:00

596 lines
19 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
Skills-v2 安装器
Claude Code 多模型协作系统
用法:
python install.py # 安装核心模块(默认)
python install.py --module core # 安装核心模块
python install.py --module all # 安装所有启用的模块
python install.py --list-modules # 列出可用模块
python install.py --install-dir ~/.claude # 自定义安装目录
"""
from __future__ import annotations
import argparse
import json
import os
import platform
import shutil
import subprocess
import sys
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple
DEFAULT_INSTALL_DIR = "~/.claude"
DEFAULT_CONFIG = "config.json"
# 检测操作系统
IS_WINDOWS = platform.system() == "Windows"
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Skills-v2 安装器 - 多模型协作系统"
)
parser.add_argument(
"--install-dir",
default=DEFAULT_INSTALL_DIR,
help=f"安装目录(默认: {DEFAULT_INSTALL_DIR})",
)
parser.add_argument(
"--module",
default="core",
help="要安装的模块: 'core', 'codex-only', 'gemini-only', 或 'all'",
)
parser.add_argument(
"--config",
default=DEFAULT_CONFIG,
help="配置文件路径",
)
parser.add_argument(
"--list-modules",
action="store_true",
help="列出可用模块并退出",
)
parser.add_argument(
"--force",
action="store_true",
help="强制覆盖现有文件",
)
parser.add_argument(
"--verbose", "-v",
action="store_true",
help="显示详细输出",
)
return parser.parse_args()
def load_config(config_path: str) -> Dict[str, Any]:
path = Path(config_path)
if not path.exists():
print(f"错误: 配置文件未找到: {config_path}", file=sys.stderr)
sys.exit(1)
with path.open("r", encoding="utf-8") as f:
return json.load(f)
def list_modules(config: Dict[str, Any]) -> None:
print("\n📦 可用模块:\n")
modules = config.get("modules", {})
for name, module in modules.items():
status = "✅ 已启用" if module.get("enabled", False) else "⚪ 未启用"
desc = module.get("description", "无描述")
print(f" {name:15} {status:12} - {desc}")
print()
def ensure_dir(path: Path) -> None:
path.mkdir(parents=True, exist_ok=True)
def copy_file(src: Path, dst: Path, verbose: bool = False) -> None:
ensure_dir(dst.parent)
shutil.copy2(src, dst)
if verbose:
print(f" 📄 已复制: {src.name} -> {dst}")
def copy_dir(src: Path, dst: Path, verbose: bool = False) -> None:
if dst.exists():
shutil.rmtree(dst)
shutil.copytree(src, dst)
if verbose:
print(f" 📁 已复制目录: {src.name} -> {dst}")
def merge_dir(src: Path, dst: Path, verbose: bool = False) -> None:
ensure_dir(dst)
for item in src.iterdir():
dst_item = dst / item.name
if item.is_file():
shutil.copy2(item, dst_item)
if verbose:
print(f" 📄 已合并: {item.name}")
elif item.is_dir():
if dst_item.exists():
merge_dir(item, dst_item, verbose)
else:
shutil.copytree(item, dst_item)
if verbose:
print(f" 📁 已合并目录: {item.name}")
def check_go_installed() -> bool:
try:
result = subprocess.run(
["go", "version"],
capture_output=True,
text=True
)
return result.returncode == 0
except FileNotFoundError:
return False
def check_npm_installed() -> bool:
"""检查 npm 是否已安装"""
try:
result = subprocess.run(
["npm", "--version"],
capture_output=True,
text=True
)
return result.returncode == 0
except FileNotFoundError:
return False
def install_ace_tool(verbose: bool = False) -> Tuple[bool, str]:
"""
安装并配置 ace-tool MCP
返回 (成功, 消息)
重要: Claude Code CLI 从 ~/.claude.json 读取 MCP 配置
NOT ~/.mcp.json (那是给 Claude Desktop 的)
NOT ~/Library/Application Support/Claude/ (那也是 Claude Desktop)
"""
print("\n 🔧 配置 ace-tool MCP...")
# 检查 npm
if not check_npm_installed():
return False, "npm 未安装,请先安装 Node.js: https://nodejs.org/"
# 提示用户配置
print("\n 📋 ace-tool 需要配置 API 访问信息:")
print(" 获取方式: 访问 https://augmentcode.com/ 注册并获取 API Token")
print()
# 获取 base-url
default_base_url = "https://api.augmentcode.com"
base_url = input(f" 请输入 Base URL (直接回车使用默认值 {default_base_url}): ").strip()
if not base_url:
base_url = default_base_url
# 获取 token
token = input(" 请输入 API Token: ").strip()
if not token:
print(" ⚠️ Token 为空,稍后可手动配置")
token = ""
# Claude Code CLI 的配置文件路径: ~/.claude.json
config_file = Path.home() / ".claude.json"
# 读取现有配置(重要:保留所有其他字段!)
existing_config = {}
if config_file.exists():
try:
with config_file.open("r", encoding="utf-8") as f:
existing_config = json.load(f)
except json.JSONDecodeError as e:
print(f" ⚠️ ~/.claude.json 解析失败: {e}")
print(" ⚠️ 请检查文件格式,跳过 MCP 配置")
return False, f"~/.claude.json 解析失败: {e}"
except Exception as e:
print(f" ⚠️ 读取 ~/.claude.json 失败: {e}")
return False, f"读取配置失败: {e}"
# 确保 mcpServers 字段存在
if "mcpServers" not in existing_config:
existing_config["mcpServers"] = {}
# 添加或更新 ace-tool 配置(使用环境变量方式,更安全)
existing_config["mcpServers"]["ace-tool"] = {
"type": "stdio",
"command": "npx",
"args": ["-y", "ace-tool@latest"],
"env": {
"ACE_BASE_URL": base_url,
"ACE_TOKEN": token
}
}
# 写入配置(保留所有其他字段)
try:
with config_file.open("w", encoding="utf-8") as f:
json.dump(existing_config, f, indent=2, ensure_ascii=False)
if verbose:
print(f" 📄 已写入配置: {config_file}")
except Exception as e:
return False, f"写入配置失败: {e}"
# 测试安装
print("\n 🚀 验证 ace-tool 安装...")
try:
result = subprocess.run(
["npx", "-y", "ace-tool@latest", "--version"],
capture_output=True,
text=True,
timeout=60
)
if result.returncode == 0:
version = result.stdout.strip() or "unknown"
if verbose:
print(f" ✅ ace-tool 版本: {version}")
else:
if verbose:
print(f" ⚠️ ace-tool 验证失败,但配置已保存")
except subprocess.TimeoutExpired:
print(" ⚠️ 验证超时,但配置已保存")
except Exception as e:
if verbose:
print(f" ⚠️ 验证异常: {e}")
return True, f"ace-tool MCP 配置完成: {config_file}"
def get_prebuilt_binary(source_dir: Path) -> Optional[Path]:
"""查找预编译的二进制文件"""
system = platform.system().lower()
machine = platform.machine().lower()
# 映射架构名称
arch_map = {
"x86_64": "amd64",
"amd64": "amd64",
"arm64": "arm64",
"aarch64": "arm64",
}
arch = arch_map.get(machine, machine)
# 映射系统名称
if system == "darwin":
binary_name = f"codeagent-wrapper-darwin-{arch}"
elif system == "linux":
binary_name = f"codeagent-wrapper-linux-{arch}"
elif system == "windows":
binary_name = f"codeagent-wrapper-windows-{arch}.exe"
else:
return None
binary_path = source_dir / "bin" / binary_name
if binary_path.exists():
return binary_path
return None
def build_go_binary(
source_dir: Path,
binary_name: str,
verbose: bool = False
) -> Tuple[bool, Optional[Path]]:
if not check_go_installed():
print(" ❌ Go 未安装,请先安装 Go:", file=sys.stderr)
print(" https://go.dev/doc/install", file=sys.stderr)
return False, None
# Windows 添加 .exe 扩展名
if IS_WINDOWS and not binary_name.endswith(".exe"):
output_name = binary_name + ".exe"
else:
output_name = binary_name
if verbose:
print(f" 🔨 正在编译 {output_name}...")
try:
result = subprocess.run(
["go", "build", "-o", output_name, "."],
cwd=source_dir,
capture_output=True,
text=True
)
if result.returncode != 0:
print(f" ❌ 编译失败: {result.stderr}", file=sys.stderr)
return False, None
binary_path = source_dir / output_name
if not binary_path.exists():
print(f" ❌ 编译后未找到二进制文件", file=sys.stderr)
return False, None
if verbose:
print(f" ✅ 编译完成: {output_name}")
return True, binary_path
except Exception as e:
print(f" ❌ 编译错误: {e}", file=sys.stderr)
return False, None
def install_binary_to_path(
binary_path: Path,
verbose: bool = False,
target_name: Optional[str] = None
) -> bool:
# 确定目标文件名
if target_name:
if IS_WINDOWS and not target_name.endswith(".exe"):
final_name = target_name + ".exe"
else:
final_name = target_name
else:
final_name = binary_path.name
# 根据系统选择安装目录
if IS_WINDOWS:
install_dirs = [
Path.home() / ".local" / "bin",
Path(os.environ.get("LOCALAPPDATA", "")) / "Programs" / "codeagent-wrapper",
Path.home() / "bin",
]
else:
install_dirs = [
Path.home() / ".local" / "bin",
Path("/usr/local/bin"),
]
for install_dir in install_dirs:
if install_dir.exists() or install_dir == install_dirs[0]:
try:
ensure_dir(install_dir)
target = install_dir / final_name
shutil.copy2(binary_path, target)
# 设置执行权限(仅 Unix)
if not IS_WINDOWS:
os.chmod(target, 0o755)
if verbose:
print(f" 📦 已安装到: {target}")
# 检查安装目录是否在 PATH 中
path_dirs = os.environ.get("PATH", "").split(os.pathsep)
if str(install_dir) not in path_dirs:
print(f" ⚠️ 注意: {install_dir} 可能不在 PATH 中")
if IS_WINDOWS:
print(f" 添加到 PATH: setx PATH \"%PATH%;{install_dir}\"")
else:
print(f" 添加到 PATH: export PATH=\"{install_dir}:$PATH\"")
return True
except PermissionError:
if verbose:
print(f" ⚠️ {install_dir} 权限不足,尝试下一个...")
continue
except Exception as e:
if verbose:
print(f" ⚠️ 安装到 {install_dir} 失败: {e}")
continue
print(" ❌ 无法安装二进制文件到 PATH", file=sys.stderr)
if IS_WINDOWS:
print(f" 手动安装: 复制 {binary_path} 到 PATH 中的目录", file=sys.stderr)
else:
print(f" 手动安装: sudo cp {binary_path} /usr/local/bin/", file=sys.stderr)
return False
def execute_operation(
op: Dict[str, Any],
source_dir: Path,
install_dir: Path,
verbose: bool = False
) -> bool:
op_type = op.get("type")
source = op.get("source", "")
target = op.get("target", source)
desc = op.get("description", "")
src_path = source_dir / source
dst_path = install_dir / target
if verbose:
print(f"\n 🔧 {desc}")
try:
if op_type == "copy_file":
if not src_path.exists():
print(f" ⚠️ 源文件未找到: {src_path}", file=sys.stderr)
return False
copy_file(src_path, dst_path, verbose)
elif op_type == "copy_dir":
if not src_path.exists():
print(f" ⚠️ 源目录未找到: {src_path}", file=sys.stderr)
return False
copy_dir(src_path, dst_path, verbose)
elif op_type == "merge_dir":
if not src_path.exists():
print(f" ⚠️ 源目录未找到: {src_path}", file=sys.stderr)
return False
merge_dir(src_path, dst_path, verbose)
elif op_type == "build_go":
binary_name = op.get("binary", "")
# 优先使用预编译的二进制文件
prebuilt = get_prebuilt_binary(source_dir)
if prebuilt:
if verbose:
print(f" 📦 使用预编译二进制: {prebuilt.name}")
# 重命名为目标名称
target_name = binary_name
if IS_WINDOWS and not target_name.endswith(".exe"):
target_name += ".exe"
binary_path = prebuilt
else:
# 没有预编译二进制,尝试编译
if verbose:
print(f" ℹ️ 未找到预编译二进制,尝试编译...")
if not src_path.exists():
print(f" ⚠️ 源目录未找到: {src_path}", file=sys.stderr)
return False
success, binary_path = build_go_binary(src_path, binary_name, verbose)
if not success:
return False
# 安装到 PATH
if not install_binary_to_path(binary_path, verbose, target_name=binary_name):
return False
elif op_type == "install_ace_tool":
success, message = install_ace_tool(verbose)
if not success:
print(f" ⚠️ {message}")
print(f" ℹ️ 可稍后手动配置 ace-tool MCP")
return True # 不阻止其他安装
if verbose:
print(f" ℹ️ {message}")
else:
print(f" ⚠️ 未知操作类型: {op_type}", file=sys.stderr)
return False
return True
except Exception as e:
print(f" ❌ 错误: {e}", file=sys.stderr)
return False
def install_module(
name: str,
module: Dict[str, Any],
source_dir: Path,
install_dir: Path,
verbose: bool = False
) -> bool:
print(f"\n📦 正在安装模块: {name}")
print(f" {module.get('description', '')}")
operations = module.get("operations", [])
success_count = 0
for op in operations:
if execute_operation(op, source_dir, install_dir, verbose):
success_count += 1
if success_count == len(operations):
print(f" ✅ 模块 '{name}' 安装成功!")
return True
else:
print(f" ⚠️ 模块 '{name}' 安装完成,但有 {len(operations) - success_count} 个警告")
return False
def main() -> int:
args = parse_args()
# 解析路径
config_path = Path(args.config)
if not config_path.is_absolute():
config_path = Path(__file__).parent / args.config
source_dir = config_path.parent
install_dir = Path(args.install_dir).expanduser().resolve()
# 加载配置
config = load_config(str(config_path))
# 列出模块模式
if args.list_modules:
list_modules(config)
return 0
# 打印标题
print("\n" + "=" * 50)
print("🚀 Skills-v2 安装器")
print(" 多模型协作系统")
print("=" * 50)
print(f"\n📁 安装目录: {install_dir}")
# 确保安装目录存在
ensure_dir(install_dir)
# 确定要安装的模块
modules = config.get("modules", {})
modules_to_install = []
if args.module == "all":
modules_to_install = [
(name, mod) for name, mod in modules.items()
if mod.get("enabled", False)
]
elif args.module in modules:
modules_to_install = [(args.module, modules[args.module])]
else:
print(f"\n❌ 未知模块: {args.module}", file=sys.stderr)
print(" 使用 --list-modules 查看可用模块")
return 1
if not modules_to_install:
print("\n⚠️ 没有可安装的模块")
return 0
# 安装模块
success = True
for name, module in modules_to_install:
if not install_module(name, module, source_dir, install_dir, args.verbose):
success = False
# 打印摘要
print("\n" + "=" * 50)
if success:
print("✅ 安装完成!")
print("\n📋 已安装命令 (/ccg:xxx):")
print(" 开发工作流:")
print(" /ccg:dev - 完整6阶段多模型工作流")
print(" /ccg:code - 多模型代码生成(智能路由)")
print(" /ccg:debug - UltraThink 多模型调试")
print(" /ccg:test - 多模型测试生成")
print(" /ccg:bugfix - 质量门控修复(90%+ 通过)")
print(" /ccg:think - 深度分析")
print(" /ccg:optimize - 性能优化")
print(" /ccg:frontend - 前端任务 → Gemini")
print(" /ccg:backend - 后端任务 → Codex")
print(" /ccg:review - 双模型代码审查")
print(" /ccg:analyze - 双模型分析")
print(" /ccg:enhance - Prompt 增强")
print(" Git 工具:")
print(" /ccg:commit - 智能提交")
print(" /ccg:rollback - 交互式回滚")
print(" /ccg:clean-branches - 清理分支")
print(" /ccg:worktree - Worktree 管理")
print(" 项目初始化:")
print(" /ccg:init - 初始化项目 AI 上下文")
print("\n🎯 快速开始:")
print(' /ccg:dev "实现用户认证功能"')
print(' /ccg:debug "登录接口返回500错误"')
print(' /ccg:bugfix "密码重置失败"')
else:
print("⚠️ 安装完成,但有警告")
print("=" * 50 + "\n")
return 0 if success else 1
if __name__ == "__main__":
sys.exit(main())