From c38bc799fd4f1c5c15d916e3694dda806f6f78c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=BD=BB=E8=88=9FJoshua?= <71011266+Qingzhou-Joshua@users.noreply.github.com> Date: Wed, 1 Apr 2026 05:06:26 +0800 Subject: [PATCH] feat(install): add CodeBuddy(Tencent) adaptation with installation scripts (#1038) * feat(install): add CodeBuddy(Tencent) adaptation with installation scripts * fix: add codebuddy to SUPPORTED_INSTALL_TARGETS * fix(codebuddy): resolve installer path issues, unused vars, and uninstall safety --- .codebuddy/README.md | 98 ++++++ .codebuddy/README.zh-CN.md | 98 ++++++ .codebuddy/install.js | 312 ++++++++++++++++++ .codebuddy/install.sh | 231 +++++++++++++ .codebuddy/uninstall.js | 291 ++++++++++++++++ .codebuddy/uninstall.sh | 184 +++++++++++ manifests/install-modules.json | 54 ++- schemas/ecc-install-config.schema.json | 3 +- schemas/install-modules.schema.json | 3 +- scripts/lib/install-manifests.js | 2 +- .../lib/install-targets/codebuddy-project.js | 47 +++ scripts/lib/install-targets/registry.js | 2 + tests/lib/install-targets.test.js | 114 +++++++ 13 files changed, 1418 insertions(+), 21 deletions(-) create mode 100644 .codebuddy/README.md create mode 100644 .codebuddy/README.zh-CN.md create mode 100755 .codebuddy/install.js create mode 100755 .codebuddy/install.sh create mode 100755 .codebuddy/uninstall.js create mode 100755 .codebuddy/uninstall.sh create mode 100644 scripts/lib/install-targets/codebuddy-project.js diff --git a/.codebuddy/README.md b/.codebuddy/README.md new file mode 100644 index 00000000..ec6a4ec4 --- /dev/null +++ b/.codebuddy/README.md @@ -0,0 +1,98 @@ +# Everything Claude Code for CodeBuddy + +Bring Everything Claude Code (ECC) workflows to CodeBuddy IDE. This repository provides custom commands, agents, skills, and rules that can be installed into any CodeBuddy project using the unified Target Adapter architecture. + +## Quick Start (Recommended) + +Use the unified install system for full lifecycle management: + +```bash +# Install with default profile +node scripts/install-apply.js --target codebuddy --profile developer + +# Install with full profile (all modules) +node scripts/install-apply.js --target codebuddy --profile full + +# Dry-run to preview changes +node scripts/install-apply.js --target codebuddy --profile full --dry-run +``` + +## Management Commands + +```bash +# Check installation health +node scripts/doctor.js --target codebuddy + +# Repair installation +node scripts/repair.js --target codebuddy + +# Uninstall cleanly (tracked via install-state) +node scripts/uninstall.js --target codebuddy +``` + +## Shell Script (Legacy) + +The legacy shell scripts are still available for quick setup: + +```bash +# Install to current project +cd /path/to/your/project +.codebuddy/install.sh + +# Install globally +.codebuddy/install.sh ~ +``` + +## What's Included + +### Commands + +Commands are on-demand workflows invocable via the `/` menu in CodeBuddy chat. All commands are reused directly from the project root's `commands/` folder. + +### Agents + +Agents are specialized AI assistants with specific tool configurations. All agents are reused directly from the project root's `agents/` folder. + +### Skills + +Skills are on-demand workflows invocable via the `/` menu in chat. All skills are reused directly from the project's `skills/` folder. + +### Rules + +Rules provide always-on rules and context that shape how the agent works with your code. Rules are flattened into namespaced files (e.g., `common-coding-style.md`) for CodeBuddy compatibility. + +## Project Structure + +``` +.codebuddy/ +├── commands/ # Command files (reused from project root) +├── agents/ # Agent files (reused from project root) +├── skills/ # Skill files (reused from skills/) +├── rules/ # Rule files (flattened from rules/) +├── ecc-install-state.json # Install state tracking +├── install.sh # Legacy install script +├── uninstall.sh # Legacy uninstall script +└── README.md # This file +``` + +## Benefits of Target Adapter Install + +- **Install-state tracking**: Safe uninstall that only removes ECC-managed files +- **Doctor checks**: Verify installation health and detect drift +- **Repair**: Auto-fix broken installations +- **Selective install**: Choose specific modules via profiles +- **Cross-platform**: Node.js-based, works on Windows/macOS/Linux + +## Recommended Workflow + +1. **Start with planning**: Use `/plan` command to break down complex features +2. **Write tests first**: Invoke `/tdd` command before implementing +3. **Review your code**: Use `/code-review` after writing code +4. **Check security**: Use `/code-review` again for auth, API endpoints, or sensitive data handling +5. **Fix build errors**: Use `/build-fix` if there are build errors + +## Next Steps + +- Open your project in CodeBuddy +- Type `/` to see available commands +- Enjoy the ECC workflows! diff --git a/.codebuddy/README.zh-CN.md b/.codebuddy/README.zh-CN.md new file mode 100644 index 00000000..4d448391 --- /dev/null +++ b/.codebuddy/README.zh-CN.md @@ -0,0 +1,98 @@ +# Everything Claude Code for CodeBuddy + +为 CodeBuddy IDE 带来 Everything Claude Code (ECC) 工作流。此仓库提供自定义命令、智能体、技能和规则,可以通过统一的 Target Adapter 架构安装到任何 CodeBuddy 项目中。 + +## 快速开始(推荐) + +使用统一安装系统,获得完整的生命周期管理: + +```bash +# 使用默认配置安装 +node scripts/install-apply.js --target codebuddy --profile developer + +# 使用完整配置安装(所有模块) +node scripts/install-apply.js --target codebuddy --profile full + +# 预览模式查看变更 +node scripts/install-apply.js --target codebuddy --profile full --dry-run +``` + +## 管理命令 + +```bash +# 检查安装健康状态 +node scripts/doctor.js --target codebuddy + +# 修复安装 +node scripts/repair.js --target codebuddy + +# 清洁卸载(通过 install-state 跟踪) +node scripts/uninstall.js --target codebuddy +``` + +## Shell 脚本(旧版) + +旧版 Shell 脚本仍然可用于快速设置: + +```bash +# 安装到当前项目 +cd /path/to/your/project +.codebuddy/install.sh + +# 全局安装 +.codebuddy/install.sh ~ +``` + +## 包含的内容 + +### 命令 + +命令是通过 CodeBuddy 聊天中的 `/` 菜单调用的按需工作流。所有命令都直接复用自项目根目录的 `commands/` 文件夹。 + +### 智能体 + +智能体是具有特定工具配置的专门 AI 助手。所有智能体都直接复用自项目根目录的 `agents/` 文件夹。 + +### 技能 + +技能是通过聊天中的 `/` 菜单调用的按需工作流。所有技能都直接复用自项目的 `skills/` 文件夹。 + +### 规则 + +规则提供始终适用的规则和上下文,塑造智能体处理代码的方式。规则会被扁平化为命名空间文件(如 `common-coding-style.md`)以兼容 CodeBuddy。 + +## 项目结构 + +``` +.codebuddy/ +├── commands/ # 命令文件(复用自项目根目录) +├── agents/ # 智能体文件(复用自项目根目录) +├── skills/ # 技能文件(复用自 skills/) +├── rules/ # 规则文件(从 rules/ 扁平化) +├── ecc-install-state.json # 安装状态跟踪 +├── install.sh # 旧版安装脚本 +├── uninstall.sh # 旧版卸载脚本 +└── README.zh-CN.md # 此文件 +``` + +## Target Adapter 安装的优势 + +- **安装状态跟踪**:安全卸载,仅删除 ECC 管理的文件 +- **Doctor 检查**:验证安装健康状态并检测偏移 +- **修复**:自动修复损坏的安装 +- **选择性安装**:通过配置文件选择特定模块 +- **跨平台**:基于 Node.js,支持 Windows/macOS/Linux + +## 推荐的工作流 + +1. **从计划开始**:使用 `/plan` 命令分解复杂功能 +2. **先写测试**:在实现之前调用 `/tdd` 命令 +3. **审查您的代码**:编写代码后使用 `/code-review` +4. **检查安全性**:对于身份验证、API 端点或敏感数据处理,再次使用 `/code-review` +5. **修复构建错误**:如果有构建错误,使用 `/build-fix` + +## 下一步 + +- 在 CodeBuddy 中打开您的项目 +- 输入 `/` 以查看可用命令 +- 享受 ECC 工作流! diff --git a/.codebuddy/install.js b/.codebuddy/install.js new file mode 100755 index 00000000..e6b5af6e --- /dev/null +++ b/.codebuddy/install.js @@ -0,0 +1,312 @@ +#!/usr/bin/env node +/** + * ECC CodeBuddy Installer (Cross-platform Node.js version) + * Installs Everything Claude Code workflows into a CodeBuddy project. + * + * Usage: + * node install.js # Install to current directory + * node install.js ~ # Install globally to ~/.codebuddy/ + */ + +const fs = require('fs'); +const path = require('path'); +const os = require('os'); + +// Platform detection +const isWindows = process.platform === 'win32'; + +/** + * Get home directory cross-platform + */ +function getHomeDir() { + return process.env.USERPROFILE || process.env.HOME || os.homedir(); +} + +/** + * Ensure directory exists + */ +function ensureDir(dirPath) { + try { + if (!fs.existsSync(dirPath)) { + fs.mkdirSync(dirPath, { recursive: true }); + } + } catch (err) { + if (err.code !== 'EEXIST') { + throw err; + } + } +} + +/** + * Read lines from a file + */ +function readLines(filePath) { + try { + if (!fs.existsSync(filePath)) { + return []; + } + const content = fs.readFileSync(filePath, 'utf8'); + return content.split('\n').filter(line => line.length > 0); + } catch { + return []; + } +} + +/** + * Check if manifest contains an entry + */ +function manifestHasEntry(manifestPath, entry) { + const lines = readLines(manifestPath); + return lines.includes(entry); +} + +/** + * Add entry to manifest + */ +function ensureManifestEntry(manifestPath, entry) { + try { + const lines = readLines(manifestPath); + if (!lines.includes(entry)) { + const content = lines.join('\n') + (lines.length > 0 ? '\n' : '') + entry + '\n'; + fs.writeFileSync(manifestPath, content, 'utf8'); + } + } catch (err) { + console.error(`Error updating manifest: ${err.message}`); + } +} + +/** + * Copy a file and manage in manifest + */ +function copyManagedFile(sourcePath, targetPath, manifestPath, manifestEntry, makeExecutable = false) { + const alreadyManaged = manifestHasEntry(manifestPath, manifestEntry); + + // If target file already exists + if (fs.existsSync(targetPath)) { + if (alreadyManaged) { + ensureManifestEntry(manifestPath, manifestEntry); + } + return false; + } + + // Copy the file + try { + ensureDir(path.dirname(targetPath)); + fs.copyFileSync(sourcePath, targetPath); + + // Make executable on Unix systems + if (makeExecutable && !isWindows) { + fs.chmodSync(targetPath, 0o755); + } + + ensureManifestEntry(manifestPath, manifestEntry); + return true; + } catch (err) { + console.error(`Error copying ${sourcePath}: ${err.message}`); + return false; + } +} + +/** + * Recursively find files in a directory + */ +function findFiles(dir, extension = '') { + const results = []; + try { + if (!fs.existsSync(dir)) { + return results; + } + + function walk(currentPath) { + try { + const entries = fs.readdirSync(currentPath, { withFileTypes: true }); + for (const entry of entries) { + const fullPath = path.join(currentPath, entry.name); + if (entry.isDirectory()) { + walk(fullPath); + } else if (!extension || entry.name.endsWith(extension)) { + results.push(fullPath); + } + } + } catch { + // Ignore permission errors + } + } + + walk(dir); + } catch { + // Ignore errors + } + return results.sort(); +} + +/** + * Main install function + */ +function doInstall() { + // Resolve script directory (where this file lives) + const scriptDir = path.dirname(path.resolve(__filename)); + const repoRoot = path.dirname(scriptDir); + const codebuddyDirName = '.codebuddy'; + + // Parse arguments + let targetDir = process.cwd(); + if (process.argv.length > 2) { + const arg = process.argv[2]; + if (arg === '~' || arg === getHomeDir()) { + targetDir = getHomeDir(); + } else { + targetDir = path.resolve(arg); + } + } + + // Determine codebuddy full path + let codebuddyFullPath; + const baseName = path.basename(targetDir); + + if (baseName === codebuddyDirName) { + codebuddyFullPath = targetDir; + } else { + codebuddyFullPath = path.join(targetDir, codebuddyDirName); + } + + console.log('ECC CodeBuddy Installer'); + console.log('======================='); + console.log(''); + console.log(`Source: ${repoRoot}`); + console.log(`Target: ${codebuddyFullPath}/`); + console.log(''); + + // Create subdirectories + const subdirs = ['commands', 'agents', 'skills', 'rules']; + for (const dir of subdirs) { + ensureDir(path.join(codebuddyFullPath, dir)); + } + + // Manifest file + const manifest = path.join(codebuddyFullPath, '.ecc-manifest'); + ensureDir(path.dirname(manifest)); + + // Counters + let commands = 0; + let agents = 0; + let skills = 0; + let rules = 0; + + // Copy commands + const commandsDir = path.join(repoRoot, 'commands'); + if (fs.existsSync(commandsDir)) { + const files = findFiles(commandsDir, '.md'); + for (const file of files) { + if (path.basename(path.dirname(file)) === 'commands') { + const localName = path.basename(file); + const targetPath = path.join(codebuddyFullPath, 'commands', localName); + if (copyManagedFile(file, targetPath, manifest, `commands/${localName}`)) { + commands += 1; + } + } + } + } + + // Copy agents + const agentsDir = path.join(repoRoot, 'agents'); + if (fs.existsSync(agentsDir)) { + const files = findFiles(agentsDir, '.md'); + for (const file of files) { + if (path.basename(path.dirname(file)) === 'agents') { + const localName = path.basename(file); + const targetPath = path.join(codebuddyFullPath, 'agents', localName); + if (copyManagedFile(file, targetPath, manifest, `agents/${localName}`)) { + agents += 1; + } + } + } + } + + // Copy skills (with subdirectories) + const skillsDir = path.join(repoRoot, 'skills'); + if (fs.existsSync(skillsDir)) { + const skillDirs = fs.readdirSync(skillsDir, { withFileTypes: true }) + .filter(entry => entry.isDirectory()) + .map(entry => entry.name); + + for (const skillName of skillDirs) { + const sourceSkillDir = path.join(skillsDir, skillName); + const targetSkillDir = path.join(codebuddyFullPath, 'skills', skillName); + let skillCopied = false; + + const skillFiles = findFiles(sourceSkillDir); + for (const sourceFile of skillFiles) { + const relativePath = path.relative(sourceSkillDir, sourceFile); + const targetPath = path.join(targetSkillDir, relativePath); + const manifestEntry = `skills/${skillName}/${relativePath.replace(/\\/g, '/')}`; + + if (copyManagedFile(sourceFile, targetPath, manifest, manifestEntry)) { + skillCopied = true; + } + } + + if (skillCopied) { + skills += 1; + } + } + } + + // Copy rules (with subdirectories) + const rulesDir = path.join(repoRoot, 'rules'); + if (fs.existsSync(rulesDir)) { + const ruleFiles = findFiles(rulesDir); + for (const ruleFile of ruleFiles) { + const relativePath = path.relative(rulesDir, ruleFile); + const targetPath = path.join(codebuddyFullPath, 'rules', relativePath); + const manifestEntry = `rules/${relativePath.replace(/\\/g, '/')}`; + + if (copyManagedFile(ruleFile, targetPath, manifest, manifestEntry)) { + rules += 1; + } + } + } + + // Copy README files (skip install/uninstall scripts to avoid broken + // path references when the copied script runs from the target directory) + const readmeFiles = ['README.md', 'README.zh-CN.md']; + for (const readmeFile of readmeFiles) { + const sourcePath = path.join(scriptDir, readmeFile); + if (fs.existsSync(sourcePath)) { + const targetPath = path.join(codebuddyFullPath, readmeFile); + copyManagedFile(sourcePath, targetPath, manifest, readmeFile); + } + } + + // Add manifest itself + ensureManifestEntry(manifest, '.ecc-manifest'); + + // Print summary + console.log('Installation complete!'); + console.log(''); + console.log('Components installed:'); + console.log(` Commands: ${commands}`); + console.log(` Agents: ${agents}`); + console.log(` Skills: ${skills}`); + console.log(` Rules: ${rules}`); + console.log(''); + console.log(`Directory: ${path.basename(codebuddyFullPath)}`); + console.log(''); + console.log('Next steps:'); + console.log(' 1. Open your project in CodeBuddy'); + console.log(' 2. Type / to see available commands'); + console.log(' 3. Enjoy the ECC workflows!'); + console.log(''); + console.log('To uninstall later:'); + console.log(` cd ${codebuddyFullPath}`); + console.log(' node uninstall.js'); + console.log(''); +} + +// Run installer +try { + doInstall(); +} catch (error) { + console.error(`Error: ${error.message}`); + process.exit(1); +} diff --git a/.codebuddy/install.sh b/.codebuddy/install.sh new file mode 100755 index 00000000..33818aac --- /dev/null +++ b/.codebuddy/install.sh @@ -0,0 +1,231 @@ +#!/bin/bash +# +# ECC CodeBuddy Installer +# Installs Everything Claude Code workflows into a CodeBuddy project. +# +# Usage: +# ./install.sh # Install to current directory +# ./install.sh ~ # Install globally to ~/.codebuddy/ +# + +set -euo pipefail + +# When globs match nothing, expand to empty list instead of the literal pattern +shopt -s nullglob + +# Resolve the directory where this script lives +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" + +# Locate the ECC repo root by walking up from SCRIPT_DIR to find the marker +# file (VERSION). This keeps the script working even when it has been copied +# into a target project's .codebuddy/ directory. +find_repo_root() { + local dir="$(dirname "$SCRIPT_DIR")" + # First try the parent of SCRIPT_DIR (original layout: .codebuddy/ lives in repo root) + if [ -f "$dir/VERSION" ] && [ -d "$dir/commands" ] && [ -d "$dir/agents" ]; then + echo "$dir" + return 0 + fi + echo "" + return 1 +} + +REPO_ROOT="$(find_repo_root)" +if [ -z "$REPO_ROOT" ]; then + echo "Error: Cannot locate the ECC repository root." + echo "This script must be run from within the ECC repository's .codebuddy/ directory." + exit 1 +fi + +# CodeBuddy directory name +CODEBUDDY_DIR=".codebuddy" + +ensure_manifest_entry() { + local manifest="$1" + local entry="$2" + + touch "$manifest" + if ! grep -Fqx "$entry" "$manifest"; then + echo "$entry" >> "$manifest" + fi +} + +manifest_has_entry() { + local manifest="$1" + local entry="$2" + + [ -f "$manifest" ] && grep -Fqx "$entry" "$manifest" +} + +copy_managed_file() { + local source_path="$1" + local target_path="$2" + local manifest="$3" + local manifest_entry="$4" + local make_executable="${5:-0}" + + local already_managed=0 + if manifest_has_entry "$manifest" "$manifest_entry"; then + already_managed=1 + fi + + if [ -f "$target_path" ]; then + if [ "$already_managed" -eq 1 ]; then + ensure_manifest_entry "$manifest" "$manifest_entry" + fi + return 1 + fi + + cp "$source_path" "$target_path" + if [ "$make_executable" -eq 1 ]; then + chmod +x "$target_path" + fi + ensure_manifest_entry "$manifest" "$manifest_entry" + return 0 +} + +# Install function +do_install() { + local target_dir="$PWD" + + # Check if ~ was specified (or expanded to $HOME) + if [ "$#" -ge 1 ]; then + if [ "$1" = "~" ] || [ "$1" = "$HOME" ]; then + target_dir="$HOME" + fi + fi + + # Check if we're already inside a .codebuddy directory + local current_dir_name="$(basename "$target_dir")" + local codebuddy_full_path + + if [ "$current_dir_name" = ".codebuddy" ]; then + # Already inside the codebuddy directory, use it directly + codebuddy_full_path="$target_dir" + else + # Normal case: append CODEBUDDY_DIR to target_dir + codebuddy_full_path="$target_dir/$CODEBUDDY_DIR" + fi + + echo "ECC CodeBuddy Installer" + echo "=======================" + echo "" + echo "Source: $REPO_ROOT" + echo "Target: $codebuddy_full_path/" + echo "" + + # Subdirectories to create + SUBDIRS="commands agents skills rules" + + # Create all required codebuddy subdirectories + for dir in $SUBDIRS; do + mkdir -p "$codebuddy_full_path/$dir" + done + + # Manifest file to track installed files + MANIFEST="$codebuddy_full_path/.ecc-manifest" + touch "$MANIFEST" + + # Counters for summary + commands=0 + agents=0 + skills=0 + rules=0 + + # Copy commands from repo root + if [ -d "$REPO_ROOT/commands" ]; then + for f in "$REPO_ROOT/commands"/*.md; do + [ -f "$f" ] || continue + local_name=$(basename "$f") + target_path="$codebuddy_full_path/commands/$local_name" + if copy_managed_file "$f" "$target_path" "$MANIFEST" "commands/$local_name"; then + commands=$((commands + 1)) + fi + done + fi + + # Copy agents from repo root + if [ -d "$REPO_ROOT/agents" ]; then + for f in "$REPO_ROOT/agents"/*.md; do + [ -f "$f" ] || continue + local_name=$(basename "$f") + target_path="$codebuddy_full_path/agents/$local_name" + if copy_managed_file "$f" "$target_path" "$MANIFEST" "agents/$local_name"; then + agents=$((agents + 1)) + fi + done + fi + + # Copy skills from repo root (if available) + if [ -d "$REPO_ROOT/skills" ]; then + for d in "$REPO_ROOT/skills"/*/; do + [ -d "$d" ] || continue + skill_name="$(basename "$d")" + target_skill_dir="$codebuddy_full_path/skills/$skill_name" + skill_copied=0 + + while IFS= read -r source_file; do + relative_path="${source_file#$d}" + target_path="$target_skill_dir/$relative_path" + + mkdir -p "$(dirname "$target_path")" + if copy_managed_file "$source_file" "$target_path" "$MANIFEST" "skills/$skill_name/$relative_path"; then + skill_copied=1 + fi + done < <(find "$d" -type f | sort) + + if [ "$skill_copied" -eq 1 ]; then + skills=$((skills + 1)) + fi + done + fi + + # Copy rules from repo root + if [ -d "$REPO_ROOT/rules" ]; then + while IFS= read -r rule_file; do + relative_path="${rule_file#$REPO_ROOT/rules/}" + target_path="$codebuddy_full_path/rules/$relative_path" + + mkdir -p "$(dirname "$target_path")" + if copy_managed_file "$rule_file" "$target_path" "$MANIFEST" "rules/$relative_path"; then + rules=$((rules + 1)) + fi + done < <(find "$REPO_ROOT/rules" -type f | sort) + fi + + # Copy README files (skip install/uninstall scripts to avoid broken + # path references when the copied script runs from the target directory) + for readme_file in "$SCRIPT_DIR/README.md" "$SCRIPT_DIR/README.zh-CN.md"; do + if [ -f "$readme_file" ]; then + local_name=$(basename "$readme_file") + target_path="$codebuddy_full_path/$local_name" + copy_managed_file "$readme_file" "$target_path" "$MANIFEST" "$local_name" || true + fi + done + + # Add manifest file itself to manifest + ensure_manifest_entry "$MANIFEST" ".ecc-manifest" + + # Installation summary + echo "Installation complete!" + echo "" + echo "Components installed:" + echo " Commands: $commands" + echo " Agents: $agents" + echo " Skills: $skills" + echo " Rules: $rules" + echo "" + echo "Directory: $(basename "$codebuddy_full_path")" + echo "" + echo "Next steps:" + echo " 1. Open your project in CodeBuddy" + echo " 2. Type / to see available commands" + echo " 3. Enjoy the ECC workflows!" + echo "" + echo "To uninstall later:" + echo " cd $codebuddy_full_path" + echo " ./uninstall.sh" +} + +# Main logic +do_install "$@" diff --git a/.codebuddy/uninstall.js b/.codebuddy/uninstall.js new file mode 100755 index 00000000..a92b0b58 --- /dev/null +++ b/.codebuddy/uninstall.js @@ -0,0 +1,291 @@ +#!/usr/bin/env node +/** + * ECC CodeBuddy Uninstaller (Cross-platform Node.js version) + * Uninstalls Everything Claude Code workflows from a CodeBuddy project. + * + * Usage: + * node uninstall.js # Uninstall from current directory + * node uninstall.js ~ # Uninstall globally from ~/.codebuddy/ + */ + +const fs = require('fs'); +const path = require('path'); +const os = require('os'); +const readline = require('readline'); + +/** + * Get home directory cross-platform + */ +function getHomeDir() { + return process.env.USERPROFILE || process.env.HOME || os.homedir(); +} + +/** + * Resolve a path to its canonical form + */ +function resolvePath(filePath) { + try { + return fs.realpathSync(filePath); + } catch { + // If realpath fails, return the path as-is + return path.resolve(filePath); + } +} + +/** + * Check if a manifest entry is valid (security check) + */ +function isValidManifestEntry(entry) { + // Reject empty, absolute paths, parent directory references + if (!entry || entry.length === 0) return false; + if (entry.startsWith('/')) return false; + if (entry.startsWith('~')) return false; + if (entry.includes('/../') || entry.includes('/..')) return false; + if (entry.startsWith('../') || entry.startsWith('..\\')) return false; + if (entry === '..' || entry === '...' || entry.includes('\\..\\')||entry.includes('/..')) return false; + + return true; +} + +/** + * Read lines from manifest file + */ +function readManifest(manifestPath) { + try { + if (!fs.existsSync(manifestPath)) { + return []; + } + const content = fs.readFileSync(manifestPath, 'utf8'); + return content.split('\n').filter(line => line.length > 0); + } catch { + return []; + } +} + +/** + * Recursively find empty directories + */ +function findEmptyDirs(dirPath) { + const emptyDirs = []; + + function walkDirs(currentPath) { + try { + const entries = fs.readdirSync(currentPath, { withFileTypes: true }); + const subdirs = entries.filter(e => e.isDirectory()); + + for (const subdir of subdirs) { + const subdirPath = path.join(currentPath, subdir.name); + walkDirs(subdirPath); + } + + // Check if directory is now empty + try { + const remaining = fs.readdirSync(currentPath); + if (remaining.length === 0 && currentPath !== dirPath) { + emptyDirs.push(currentPath); + } + } catch { + // Directory might have been deleted + } + } catch { + // Ignore errors + } + } + + walkDirs(dirPath); + return emptyDirs.sort().reverse(); // Sort in reverse for removal +} + +/** + * Prompt user for confirmation + */ +async function promptConfirm(question) { + return new Promise((resolve) => { + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + }); + + rl.question(question, (answer) => { + rl.close(); + resolve(/^[yY]$/.test(answer)); + }); + }); +} + +/** + * Main uninstall function + */ +async function doUninstall() { + const codebuddyDirName = '.codebuddy'; + + // Parse arguments + let targetDir = process.cwd(); + if (process.argv.length > 2) { + const arg = process.argv[2]; + if (arg === '~' || arg === getHomeDir()) { + targetDir = getHomeDir(); + } else { + targetDir = path.resolve(arg); + } + } + + // Determine codebuddy full path + let codebuddyFullPath; + const baseName = path.basename(targetDir); + + if (baseName === codebuddyDirName) { + codebuddyFullPath = targetDir; + } else { + codebuddyFullPath = path.join(targetDir, codebuddyDirName); + } + + console.log('ECC CodeBuddy Uninstaller'); + console.log('=========================='); + console.log(''); + console.log(`Target: ${codebuddyFullPath}/`); + console.log(''); + + // Check if codebuddy directory exists + if (!fs.existsSync(codebuddyFullPath)) { + console.error(`Error: ${codebuddyDirName} directory not found at ${targetDir}`); + process.exit(1); + } + + const codebuddyRootResolved = resolvePath(codebuddyFullPath); + const manifest = path.join(codebuddyFullPath, '.ecc-manifest'); + + // Handle missing manifest + if (!fs.existsSync(manifest)) { + console.log('Warning: No manifest file found (.ecc-manifest)'); + console.log(''); + console.log('This could mean:'); + console.log(' 1. ECC was installed with an older version without manifest support'); + console.log(' 2. The manifest file was manually deleted'); + console.log(''); + + const confirmed = await promptConfirm(`Do you want to remove the entire ${codebuddyDirName} directory? (y/N) `); + if (!confirmed) { + console.log('Uninstall cancelled.'); + process.exit(0); + } + + try { + fs.rmSync(codebuddyFullPath, { recursive: true, force: true }); + console.log('Uninstall complete!'); + console.log(''); + console.log(`Removed: ${codebuddyFullPath}/`); + } catch (err) { + console.error(`Error removing directory: ${err.message}`); + process.exit(1); + } + return; + } + + console.log('Found manifest file - will only remove files installed by ECC'); + console.log(''); + + const confirmed = await promptConfirm(`Are you sure you want to uninstall ECC from ${codebuddyDirName}? (y/N) `); + if (!confirmed) { + console.log('Uninstall cancelled.'); + process.exit(0); + } + + // Read manifest and remove files + const manifestLines = readManifest(manifest); + let removed = 0; + let skipped = 0; + + for (const filePath of manifestLines) { + if (!filePath || filePath.length === 0) continue; + + if (!isValidManifestEntry(filePath)) { + console.log(`Skipped: ${filePath} (invalid manifest entry)`); + skipped += 1; + continue; + } + + const fullPath = path.join(codebuddyFullPath, filePath); + + // Security check: use path.relative() to ensure the manifest entry + // resolves inside the codebuddy directory. This is stricter than + // startsWith and correctly handles edge-cases with symlinks. + const relative = path.relative(codebuddyRootResolved, path.resolve(fullPath)); + if (relative.startsWith('..') || path.isAbsolute(relative)) { + console.log(`Skipped: ${filePath} (outside target directory)`); + skipped += 1; + continue; + } + + try { + const stats = fs.lstatSync(fullPath); + + if (stats.isFile() || stats.isSymbolicLink()) { + fs.unlinkSync(fullPath); + console.log(`Removed: ${filePath}`); + removed += 1; + } else if (stats.isDirectory()) { + try { + const files = fs.readdirSync(fullPath); + if (files.length === 0) { + fs.rmdirSync(fullPath); + console.log(`Removed: ${filePath}/`); + removed += 1; + } else { + console.log(`Skipped: ${filePath}/ (not empty - contains user files)`); + skipped += 1; + } + } catch { + console.log(`Skipped: ${filePath}/ (not empty - contains user files)`); + skipped += 1; + } + } + } catch { + skipped += 1; + } + } + + // Remove empty directories + const emptyDirs = findEmptyDirs(codebuddyFullPath); + for (const emptyDir of emptyDirs) { + try { + fs.rmdirSync(emptyDir); + const relativePath = path.relative(codebuddyFullPath, emptyDir); + console.log(`Removed: ${relativePath}/`); + removed += 1; + } catch { + // Directory might not be empty anymore + } + } + + // Try to remove main codebuddy directory if empty + try { + const files = fs.readdirSync(codebuddyFullPath); + if (files.length === 0) { + fs.rmdirSync(codebuddyFullPath); + console.log(`Removed: ${codebuddyDirName}/`); + removed += 1; + } + } catch { + // Directory not empty + } + + // Print summary + console.log(''); + console.log('Uninstall complete!'); + console.log(''); + console.log('Summary:'); + console.log(` Removed: ${removed} items`); + console.log(` Skipped: ${skipped} items (not found or user-modified)`); + console.log(''); + + if (fs.existsSync(codebuddyFullPath)) { + console.log(`Note: ${codebuddyDirName} directory still exists (contains user-added files)`); + } +} + +// Run uninstaller +doUninstall().catch((error) => { + console.error(`Error: ${error.message}`); + process.exit(1); +}); diff --git a/.codebuddy/uninstall.sh b/.codebuddy/uninstall.sh new file mode 100755 index 00000000..6c97869e --- /dev/null +++ b/.codebuddy/uninstall.sh @@ -0,0 +1,184 @@ +#!/bin/bash +# +# ECC CodeBuddy Uninstaller +# Uninstalls Everything Claude Code workflows from a CodeBuddy project. +# +# Usage: +# ./uninstall.sh # Uninstall from current directory +# ./uninstall.sh ~ # Uninstall globally from ~/.codebuddy/ +# + +set -euo pipefail + +# Resolve the directory where this script lives +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" + +# CodeBuddy directory name +CODEBUDDY_DIR=".codebuddy" + +resolve_path() { + python3 -c 'import os, sys; print(os.path.realpath(sys.argv[1]))' "$1" +} + +is_valid_manifest_entry() { + local file_path="$1" + + case "$file_path" in + ""|/*|~*|*/../*|../*|*/..|..) + return 1 + ;; + esac + + return 0 +} + +# Main uninstall function +do_uninstall() { + local target_dir="$PWD" + + # Check if ~ was specified (or expanded to $HOME) + if [ "$#" -ge 1 ]; then + if [ "$1" = "~" ] || [ "$1" = "$HOME" ]; then + target_dir="$HOME" + fi + fi + + # Check if we're already inside a .codebuddy directory + local current_dir_name="$(basename "$target_dir")" + local codebuddy_full_path + + if [ "$current_dir_name" = ".codebuddy" ]; then + # Already inside the codebuddy directory, use it directly + codebuddy_full_path="$target_dir" + else + # Normal case: append CODEBUDDY_DIR to target_dir + codebuddy_full_path="$target_dir/$CODEBUDDY_DIR" + fi + + echo "ECC CodeBuddy Uninstaller" + echo "==========================" + echo "" + echo "Target: $codebuddy_full_path/" + echo "" + + if [ ! -d "$codebuddy_full_path" ]; then + echo "Error: $CODEBUDDY_DIR directory not found at $target_dir" + exit 1 + fi + + codebuddy_root_resolved="$(resolve_path "$codebuddy_full_path")" + + # Manifest file path + MANIFEST="$codebuddy_full_path/.ecc-manifest" + + if [ ! -f "$MANIFEST" ]; then + echo "Warning: No manifest file found (.ecc-manifest)" + echo "" + echo "This could mean:" + echo " 1. ECC was installed with an older version without manifest support" + echo " 2. The manifest file was manually deleted" + echo "" + read -p "Do you want to remove the entire $CODEBUDDY_DIR directory? (y/N) " -n 1 -r + echo + if [[ ! $REPLY =~ ^[Yy]$ ]]; then + echo "Uninstall cancelled." + exit 0 + fi + rm -rf "$codebuddy_full_path" + echo "Uninstall complete!" + echo "" + echo "Removed: $codebuddy_full_path/" + exit 0 + fi + + echo "Found manifest file - will only remove files installed by ECC" + echo "" + read -p "Are you sure you want to uninstall ECC from $CODEBUDDY_DIR? (y/N) " -n 1 -r + echo + if [[ ! $REPLY =~ ^[Yy]$ ]]; then + echo "Uninstall cancelled." + exit 0 + fi + + # Counters + removed=0 + skipped=0 + + # Read manifest and remove files + while IFS= read -r file_path; do + [ -z "$file_path" ] && continue + + if ! is_valid_manifest_entry "$file_path"; then + echo "Skipped: $file_path (invalid manifest entry)" + skipped=$((skipped + 1)) + continue + fi + + full_path="$codebuddy_full_path/$file_path" + + # Security check: ensure the path resolves inside the target directory. + # Use Python to compute a reliable relative path so symlinks cannot + # escape the boundary. + relative="$(python3 -c 'import os,sys; print(os.path.relpath(os.path.abspath(sys.argv[1]), sys.argv[2]))' "$full_path" "$codebuddy_root_resolved")" + case "$relative" in + ../*|..) + echo "Skipped: $file_path (outside target directory)" + skipped=$((skipped + 1)) + continue + ;; + esac + + if [ -L "$full_path" ] || [ -f "$full_path" ]; then + rm -f "$full_path" + echo "Removed: $file_path" + removed=$((removed + 1)) + elif [ -d "$full_path" ]; then + # Only remove directory if it's empty + if [ -z "$(ls -A "$full_path" 2>/dev/null)" ]; then + rmdir "$full_path" 2>/dev/null || true + if [ ! -d "$full_path" ]; then + echo "Removed: $file_path/" + removed=$((removed + 1)) + fi + else + echo "Skipped: $file_path/ (not empty - contains user files)" + skipped=$((skipped + 1)) + fi + else + skipped=$((skipped + 1)) + fi + done < "$MANIFEST" + + while IFS= read -r empty_dir; do + [ "$empty_dir" = "$codebuddy_full_path" ] && continue + relative_dir="${empty_dir#$codebuddy_full_path/}" + rmdir "$empty_dir" 2>/dev/null || true + if [ ! -d "$empty_dir" ]; then + echo "Removed: $relative_dir/" + removed=$((removed + 1)) + fi + done < <(find "$codebuddy_full_path" -depth -type d -empty 2>/dev/null | sort -r) + + # Try to remove the main codebuddy directory if it's empty + if [ -d "$codebuddy_full_path" ] && [ -z "$(ls -A "$codebuddy_full_path" 2>/dev/null)" ]; then + rmdir "$codebuddy_full_path" 2>/dev/null || true + if [ ! -d "$codebuddy_full_path" ]; then + echo "Removed: $CODEBUDDY_DIR/" + removed=$((removed + 1)) + fi + fi + + echo "" + echo "Uninstall complete!" + echo "" + echo "Summary:" + echo " Removed: $removed items" + echo " Skipped: $skipped items (not found or user-modified)" + echo "" + if [ -d "$codebuddy_full_path" ]; then + echo "Note: $CODEBUDDY_DIR directory still exists (contains user-added files)" + fi +} + +# Execute uninstall +do_uninstall "$@" diff --git a/manifests/install-modules.json b/manifests/install-modules.json index f9cbf597..0b270409 100644 --- a/manifests/install-modules.json +++ b/manifests/install-modules.json @@ -11,7 +11,8 @@ "targets": [ "claude", "cursor", - "antigravity" + "antigravity", + "codebuddy" ], "dependencies": [], "defaultInstall": true, @@ -32,7 +33,8 @@ "cursor", "antigravity", "codex", - "opencode" + "opencode", + "codebuddy" ], "dependencies": [], "defaultInstall": true, @@ -50,7 +52,8 @@ "claude", "cursor", "antigravity", - "opencode" + "opencode", + "codebuddy" ], "dependencies": [], "defaultInstall": true, @@ -69,7 +72,8 @@ "targets": [ "claude", "cursor", - "opencode" + "opencode", + "codebuddy" ], "dependencies": [], "defaultInstall": true, @@ -93,7 +97,8 @@ "cursor", "antigravity", "codex", - "opencode" + "opencode", + "codebuddy" ], "dependencies": [], "defaultInstall": true, @@ -145,7 +150,8 @@ "cursor", "antigravity", "codex", - "opencode" + "opencode", + "codebuddy" ], "dependencies": [ "rules-core", @@ -172,7 +178,8 @@ "cursor", "antigravity", "codex", - "opencode" + "opencode", + "codebuddy" ], "dependencies": [ "platform-configs" @@ -205,7 +212,8 @@ "cursor", "antigravity", "codex", - "opencode" + "opencode", + "codebuddy" ], "dependencies": [ "platform-configs" @@ -232,7 +240,8 @@ "cursor", "antigravity", "codex", - "opencode" + "opencode", + "codebuddy" ], "dependencies": [ "workflow-quality" @@ -255,7 +264,8 @@ "cursor", "antigravity", "codex", - "opencode" + "opencode", + "codebuddy" ], "dependencies": [ "platform-configs" @@ -282,7 +292,8 @@ "cursor", "antigravity", "codex", - "opencode" + "opencode", + "codebuddy" ], "dependencies": [ "platform-configs" @@ -304,7 +315,8 @@ "cursor", "antigravity", "codex", - "opencode" + "opencode", + "codebuddy" ], "dependencies": [ "business-content" @@ -327,7 +339,8 @@ "claude", "cursor", "codex", - "opencode" + "opencode", + "codebuddy" ], "dependencies": [ "platform-configs" @@ -381,7 +394,8 @@ "cursor", "antigravity", "codex", - "opencode" + "opencode", + "codebuddy" ], "dependencies": [ "platform-configs" @@ -419,7 +433,8 @@ "cursor", "antigravity", "codex", - "opencode" + "opencode", + "codebuddy" ], "dependencies": [ "platform-configs" @@ -441,7 +456,8 @@ "cursor", "antigravity", "codex", - "opencode" + "opencode", + "codebuddy" ], "dependencies": [ "platform-configs" @@ -469,7 +485,8 @@ "cursor", "antigravity", "codex", - "opencode" + "opencode", + "codebuddy" ], "dependencies": [ "platform-configs" @@ -491,7 +508,8 @@ "cursor", "antigravity", "codex", - "opencode" + "opencode", + "codebuddy" ], "dependencies": [ "platform-configs" diff --git a/schemas/ecc-install-config.schema.json b/schemas/ecc-install-config.schema.json index 89fd7c72..13f826d3 100644 --- a/schemas/ecc-install-config.schema.json +++ b/schemas/ecc-install-config.schema.json @@ -22,7 +22,8 @@ "cursor", "antigravity", "codex", - "opencode" + "opencode", + "codebuddy" ] }, "profile": { diff --git a/schemas/install-modules.schema.json b/schemas/install-modules.schema.json index 5c92fdbe..f011ed5d 100644 --- a/schemas/install-modules.schema.json +++ b/schemas/install-modules.schema.json @@ -51,7 +51,8 @@ "cursor", "antigravity", "codex", - "opencode" + "opencode", + "codebuddy" ] } }, diff --git a/scripts/lib/install-manifests.js b/scripts/lib/install-manifests.js index ee159bcd..582e81f6 100644 --- a/scripts/lib/install-manifests.js +++ b/scripts/lib/install-manifests.js @@ -4,7 +4,7 @@ const path = require('path'); const { planInstallTargetScaffold } = require('./install-targets/registry'); const DEFAULT_REPO_ROOT = path.join(__dirname, '../..'); -const SUPPORTED_INSTALL_TARGETS = ['claude', 'cursor', 'antigravity', 'codex', 'opencode']; +const SUPPORTED_INSTALL_TARGETS = ['claude', 'cursor', 'antigravity', 'codex', 'opencode', 'codebuddy']; const COMPONENT_FAMILY_PREFIXES = { baseline: 'baseline:', language: 'lang:', diff --git a/scripts/lib/install-targets/codebuddy-project.js b/scripts/lib/install-targets/codebuddy-project.js new file mode 100644 index 00000000..7e1d71fa --- /dev/null +++ b/scripts/lib/install-targets/codebuddy-project.js @@ -0,0 +1,47 @@ +const path = require('path'); + +const { + createFlatRuleOperations, + createInstallTargetAdapter, +} = require('./helpers'); + +module.exports = createInstallTargetAdapter({ + id: 'codebuddy-project', + target: 'codebuddy', + kind: 'project', + rootSegments: ['.codebuddy'], + installStatePathSegments: ['ecc-install-state.json'], + nativeRootRelativePath: '.codebuddy', + planOperations(input, adapter) { + const modules = Array.isArray(input.modules) + ? input.modules + : (input.module ? [input.module] : []); + const { + repoRoot, + projectRoot, + homeDir, + } = input; + const planningInput = { + repoRoot, + projectRoot, + homeDir, + }; + const targetRoot = adapter.resolveRoot(planningInput); + + return modules.flatMap(module => { + const paths = Array.isArray(module.paths) ? module.paths : []; + return paths.flatMap(sourceRelativePath => { + if (sourceRelativePath === 'rules') { + return createFlatRuleOperations({ + moduleId: module.id, + repoRoot, + sourceRelativePath, + destinationDir: path.join(targetRoot, 'rules'), + }); + } + + return [adapter.createScaffoldOperation(module.id, sourceRelativePath, planningInput)]; + }); + }); + }, +}); diff --git a/scripts/lib/install-targets/registry.js b/scripts/lib/install-targets/registry.js index 9cc34e2b..64c14f32 100644 --- a/scripts/lib/install-targets/registry.js +++ b/scripts/lib/install-targets/registry.js @@ -1,5 +1,6 @@ const antigravityProject = require('./antigravity-project'); const claudeHome = require('./claude-home'); +const codebuddyProject = require('./codebuddy-project'); const codexHome = require('./codex-home'); const cursorProject = require('./cursor-project'); const opencodeHome = require('./opencode-home'); @@ -10,6 +11,7 @@ const ADAPTERS = Object.freeze([ antigravityProject, codexHome, opencodeHome, + codebuddyProject, ]); function listInstallTargetAdapters() { diff --git a/tests/lib/install-targets.test.js b/tests/lib/install-targets.test.js index 23de2633..2a299e73 100644 --- a/tests/lib/install-targets.test.js +++ b/tests/lib/install-targets.test.js @@ -41,6 +41,7 @@ function runTests() { assert.ok(targets.includes('antigravity'), 'Should include antigravity target'); assert.ok(targets.includes('codex'), 'Should include codex target'); assert.ok(targets.includes('opencode'), 'Should include opencode target'); + assert.ok(targets.includes('codebuddy'), 'Should include codebuddy target'); })) passed++; else failed++; if (test('resolves cursor adapter root and install-state path from project root', () => { @@ -216,6 +217,119 @@ function runTests() { ); })) passed++; else failed++; + if (test('resolves codebuddy adapter root and install-state path from project root', () => { + const adapter = getInstallTargetAdapter('codebuddy'); + const projectRoot = '/workspace/app'; + const root = adapter.resolveRoot({ projectRoot }); + const statePath = adapter.getInstallStatePath({ projectRoot }); + + assert.strictEqual(adapter.id, 'codebuddy-project'); + assert.strictEqual(adapter.target, 'codebuddy'); + assert.strictEqual(adapter.kind, 'project'); + assert.strictEqual(root, path.join(projectRoot, '.codebuddy')); + assert.strictEqual(statePath, path.join(projectRoot, '.codebuddy', 'ecc-install-state.json')); + })) passed++; else failed++; + + if (test('codebuddy adapter supports lookup by target and adapter id', () => { + const byTarget = getInstallTargetAdapter('codebuddy'); + const byId = getInstallTargetAdapter('codebuddy-project'); + + assert.strictEqual(byTarget.id, 'codebuddy-project'); + assert.strictEqual(byId.id, 'codebuddy-project'); + assert.ok(byTarget.supports('codebuddy')); + assert.ok(byTarget.supports('codebuddy-project')); + })) passed++; else failed++; + + if (test('plans codebuddy rules with flat namespaced filenames', () => { + const repoRoot = path.join(__dirname, '..', '..'); + const projectRoot = '/workspace/app'; + + const plan = planInstallTargetScaffold({ + target: 'codebuddy', + repoRoot, + projectRoot, + modules: [ + { + id: 'rules-core', + paths: ['rules'], + }, + ], + }); + + assert.strictEqual(plan.adapter.id, 'codebuddy-project'); + assert.strictEqual(plan.targetRoot, path.join(projectRoot, '.codebuddy')); + assert.strictEqual(plan.installStatePath, path.join(projectRoot, '.codebuddy', 'ecc-install-state.json')); + + assert.ok( + plan.operations.some(operation => ( + normalizedRelativePath(operation.sourceRelativePath) === 'rules/common/coding-style.md' + && operation.destinationPath === path.join(projectRoot, '.codebuddy', 'rules', 'common-coding-style.md') + )), + 'Should flatten common rules into namespaced files for codebuddy' + ); + assert.ok( + !plan.operations.some(operation => ( + operation.destinationPath === path.join(projectRoot, '.codebuddy', 'rules', 'common', 'coding-style.md') + )), + 'Should not preserve nested rule directories for codebuddy installs' + ); + })) passed++; else failed++; + + if (test('exposes validate and planOperations on codebuddy adapter', () => { + const codebuddyAdapter = getInstallTargetAdapter('codebuddy'); + + assert.strictEqual(typeof codebuddyAdapter.planOperations, 'function'); + assert.strictEqual(typeof codebuddyAdapter.validate, 'function'); + assert.deepStrictEqual( + codebuddyAdapter.validate({ projectRoot: '/workspace/app', repoRoot: '/repo/ecc' }), + [] + ); + })) passed++; else failed++; + + if (test('every schema target enum value has a matching adapter (regression guard)', () => { + const schemaPath = path.join(__dirname, '..', '..', 'schemas', 'ecc-install-config.schema.json'); + const schema = JSON.parse(require('fs').readFileSync(schemaPath, 'utf8')); + const schemaTargets = schema.properties.target.enum; + const adapters = listInstallTargetAdapters(); + const adapterTargets = adapters.map(a => a.target); + + for (const target of schemaTargets) { + assert.ok( + adapterTargets.includes(target), + `Schema target "${target}" has no matching adapter. ` + + `Available adapter targets: ${adapterTargets.join(', ')}` + ); + } + })) passed++; else failed++; + + if (test('every adapter target is listed in the schema enum (regression guard)', () => { + const schemaPath = path.join(__dirname, '..', '..', 'schemas', 'ecc-install-config.schema.json'); + const schema = JSON.parse(require('fs').readFileSync(schemaPath, 'utf8')); + const schemaTargets = schema.properties.target.enum; + const adapters = listInstallTargetAdapters(); + + for (const adapter of adapters) { + assert.ok( + schemaTargets.includes(adapter.target), + `Adapter target "${adapter.target}" is not in schema enum. ` + + `Schema targets: ${schemaTargets.join(', ')}` + ); + } + })) passed++; else failed++; + + if (test('every adapter target is in SUPPORTED_INSTALL_TARGETS (regression guard)', () => { + const { SUPPORTED_INSTALL_TARGETS } = require('../../scripts/lib/install-manifests'); + const adapters = listInstallTargetAdapters(); + + for (const adapter of adapters) { + assert.ok( + SUPPORTED_INSTALL_TARGETS.includes(adapter.target), + `Adapter target "${adapter.target}" is not in SUPPORTED_INSTALL_TARGETS. ` + + `Supported: ${SUPPORTED_INSTALL_TARGETS.join(', ')}` + ); + } + })) passed++; else failed++; + console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`); process.exit(failed > 0 ? 1 : 0); }