mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-04-15 22:43:28 +08:00
fix: unblock urgent install and gateguard regressions
This commit is contained in:
10
README.md
10
README.md
@@ -174,7 +174,7 @@ Get up and running in under 2 minutes:
|
|||||||
/plugin marketplace add https://github.com/affaan-m/everything-claude-code
|
/plugin marketplace add https://github.com/affaan-m/everything-claude-code
|
||||||
|
|
||||||
# Install plugin
|
# Install plugin
|
||||||
/plugin install ecc@ecc
|
/plugin install everything-claude-code
|
||||||
```
|
```
|
||||||
|
|
||||||
### Step 2: Install Rules (Required)
|
### Step 2: Install Rules (Required)
|
||||||
@@ -236,7 +236,7 @@ For manual install instructions see the README in the `rules/` folder. When copy
|
|||||||
# /plan "Add user authentication"
|
# /plan "Add user authentication"
|
||||||
|
|
||||||
# Check available commands
|
# Check available commands
|
||||||
/plugin list ecc@ecc
|
/plugin list everything-claude-code@everything-claude-code
|
||||||
```
|
```
|
||||||
|
|
||||||
**That's it!** You now have access to 47 agents, 181 skills, and 79 legacy command shims.
|
**That's it!** You now have access to 47 agents, 181 skills, and 79 legacy command shims.
|
||||||
@@ -648,7 +648,7 @@ The easiest way to use this repo - install as a Claude Code plugin:
|
|||||||
/plugin marketplace add https://github.com/affaan-m/everything-claude-code
|
/plugin marketplace add https://github.com/affaan-m/everything-claude-code
|
||||||
|
|
||||||
# Install the plugin
|
# Install the plugin
|
||||||
/plugin install ecc@ecc
|
/plugin install everything-claude-code
|
||||||
```
|
```
|
||||||
|
|
||||||
Or add directly to your `~/.claude/settings.json`:
|
Or add directly to your `~/.claude/settings.json`:
|
||||||
@@ -664,7 +664,7 @@ Or add directly to your `~/.claude/settings.json`:
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"enabledPlugins": {
|
"enabledPlugins": {
|
||||||
"ecc@ecc": true
|
"everything-claude-code@everything-claude-code": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -882,7 +882,7 @@ Slash forms below are shown because they are still the fastest familiar entrypoi
|
|||||||
<summary><b>How do I check which agents/commands are installed?</b></summary>
|
<summary><b>How do I check which agents/commands are installed?</b></summary>
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
/plugin list ecc@ecc
|
/plugin list everything-claude-code@everything-claude-code
|
||||||
```
|
```
|
||||||
|
|
||||||
This shows all available agents, commands, and skills from the plugin.
|
This shows all available agents, commands, and skills from the plugin.
|
||||||
|
|||||||
@@ -102,7 +102,7 @@
|
|||||||
/plugin marketplace add affaan-m/everything-claude-code
|
/plugin marketplace add affaan-m/everything-claude-code
|
||||||
|
|
||||||
# 安装插件
|
# 安装插件
|
||||||
/plugin install ecc@ecc
|
/plugin install everything-claude-code
|
||||||
```
|
```
|
||||||
|
|
||||||
### 第二步:安装规则(必需)
|
### 第二步:安装规则(必需)
|
||||||
@@ -159,7 +159,7 @@ npx ecc-install typescript
|
|||||||
# /plan "添加用户认证"
|
# /plan "添加用户认证"
|
||||||
|
|
||||||
# 查看可用命令
|
# 查看可用命令
|
||||||
/plugin list ecc@ecc
|
/plugin list everything-claude-code@everything-claude-code
|
||||||
```
|
```
|
||||||
|
|
||||||
**完成!** 你现在可以使用 47 个代理、181 个技能和 79 个命令。
|
**完成!** 你现在可以使用 47 个代理、181 个技能和 79 个命令。
|
||||||
@@ -546,7 +546,7 @@ Claude Code v2.1+ 会**按照约定自动加载**已安装插件中的 `hooks/ho
|
|||||||
/plugin marketplace add affaan-m/everything-claude-code
|
/plugin marketplace add affaan-m/everything-claude-code
|
||||||
|
|
||||||
# 安装插件
|
# 安装插件
|
||||||
/plugin install ecc@ecc
|
/plugin install everything-claude-code
|
||||||
```
|
```
|
||||||
|
|
||||||
或直接添加到你的 `~/.claude/settings.json`:
|
或直接添加到你的 `~/.claude/settings.json`:
|
||||||
@@ -562,7 +562,7 @@ Claude Code v2.1+ 会**按照约定自动加载**已安装插件中的 `hooks/ho
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"enabledPlugins": {
|
"enabledPlugins": {
|
||||||
"ecc@ecc": true
|
"everything-claude-code@everything-claude-code": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -110,7 +110,7 @@
|
|||||||
/plugin marketplace add https://github.com/affaan-m/everything-claude-code
|
/plugin marketplace add https://github.com/affaan-m/everything-claude-code
|
||||||
|
|
||||||
# プラグインをインストール
|
# プラグインをインストール
|
||||||
/plugin install ecc@ecc
|
/plugin install everything-claude-code
|
||||||
```
|
```
|
||||||
|
|
||||||
### ステップ2:ルールをインストール(必須)
|
### ステップ2:ルールをインストール(必須)
|
||||||
@@ -140,7 +140,7 @@ cp -r everything-claude-code/rules/golang/* ~/.claude/rules/
|
|||||||
# /plan "ユーザー認証を追加"
|
# /plan "ユーザー認証を追加"
|
||||||
|
|
||||||
# 利用可能なコマンドを確認
|
# 利用可能なコマンドを確認
|
||||||
/plugin list ecc@ecc
|
/plugin list everything-claude-code@everything-claude-code
|
||||||
```
|
```
|
||||||
|
|
||||||
**完了です!** これで13のエージェント、43のスキル、31のコマンドにアクセスできます。
|
**完了です!** これで13のエージェント、43のスキル、31のコマンドにアクセスできます。
|
||||||
@@ -427,7 +427,7 @@ Duplicate hook file detected: ./hooks/hooks.json is already resolved to a loaded
|
|||||||
/plugin marketplace add https://github.com/affaan-m/everything-claude-code
|
/plugin marketplace add https://github.com/affaan-m/everything-claude-code
|
||||||
|
|
||||||
# プラグインをインストール
|
# プラグインをインストール
|
||||||
/plugin install ecc@ecc
|
/plugin install everything-claude-code
|
||||||
```
|
```
|
||||||
|
|
||||||
または、`~/.claude/settings.json` に直接追加:
|
または、`~/.claude/settings.json` に直接追加:
|
||||||
@@ -443,7 +443,7 @@ Duplicate hook file detected: ./hooks/hooks.json is already resolved to a loaded
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"enabledPlugins": {
|
"enabledPlugins": {
|
||||||
"ecc@ecc": true
|
"everything-claude-code@everything-claude-code": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ Everything Claude Code プロジェクトのインタラクティブなステッ
|
|||||||
## 前提条件
|
## 前提条件
|
||||||
|
|
||||||
このスキルは起動前に Claude Code からアクセス可能である必要があります。ブートストラップには2つの方法があります:
|
このスキルは起動前に Claude Code からアクセス可能である必要があります。ブートストラップには2つの方法があります:
|
||||||
1. **プラグイン経由**: `/plugin install ecc@ecc` — プラグインがこのスキルを自動的にロードします
|
1. **プラグイン経由**: `/plugin install everything-claude-code` — プラグインがこのスキルを自動的にロードします
|
||||||
2. **手動**: このスキルのみを `~/.claude/skills/configure-ecc/SKILL.md` にコピーし、"configure ecc" と言って起動します
|
2. **手動**: このスキルのみを `~/.claude/skills/configure-ecc/SKILL.md` にコピーし、"configure ecc" と言って起動します
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -115,7 +115,7 @@
|
|||||||
/plugin marketplace add https://github.com/affaan-m/everything-claude-code
|
/plugin marketplace add https://github.com/affaan-m/everything-claude-code
|
||||||
|
|
||||||
# 플러그인 설치
|
# 플러그인 설치
|
||||||
/plugin install ecc@ecc
|
/plugin install everything-claude-code
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2단계: 룰 설치 (필수)
|
### 2단계: 룰 설치 (필수)
|
||||||
@@ -147,7 +147,7 @@ cd everything-claude-code
|
|||||||
# /plan "사용자 인증 추가"
|
# /plan "사용자 인증 추가"
|
||||||
|
|
||||||
# 사용 가능한 커맨드 확인
|
# 사용 가능한 커맨드 확인
|
||||||
/plugin list ecc@ecc
|
/plugin list everything-claude-code@everything-claude-code
|
||||||
```
|
```
|
||||||
|
|
||||||
**끝!** 이제 16개 에이전트, 65개 스킬, 40개 커맨드를 사용할 수 있습니다.
|
**끝!** 이제 16개 에이전트, 65개 스킬, 40개 커맨드를 사용할 수 있습니다.
|
||||||
@@ -359,7 +359,7 @@ Claude Code v2.1+는 설치된 플러그인의 `hooks/hooks.json`을 **자동으
|
|||||||
/plugin marketplace add https://github.com/affaan-m/everything-claude-code
|
/plugin marketplace add https://github.com/affaan-m/everything-claude-code
|
||||||
|
|
||||||
# 플러그인 설치
|
# 플러그인 설치
|
||||||
/plugin install ecc@ecc
|
/plugin install everything-claude-code
|
||||||
```
|
```
|
||||||
|
|
||||||
또는 `~/.claude/settings.json`에 직접 추가:
|
또는 `~/.claude/settings.json`에 직접 추가:
|
||||||
@@ -375,7 +375,7 @@ Claude Code v2.1+는 설치된 플러그인의 `hooks/hooks.json`을 **자동으
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"enabledPlugins": {
|
"enabledPlugins": {
|
||||||
"ecc@ecc": true
|
"everything-claude-code@everything-claude-code": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -535,7 +535,7 @@ rules/
|
|||||||
<summary><b>설치된 에이전트/커맨드 확인은 어떻게 하나요?</b></summary>
|
<summary><b>설치된 에이전트/커맨드 확인은 어떻게 하나요?</b></summary>
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
/plugin list ecc@ecc
|
/plugin list everything-claude-code@everything-claude-code
|
||||||
```
|
```
|
||||||
|
|
||||||
플러그인에서 사용할 수 있는 모든 에이전트, 커맨드, 스킬을 보여줍니다.
|
플러그인에서 사용할 수 있는 모든 에이전트, 커맨드, 스킬을 보여줍니다.
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ Comece em menos de 2 minutos:
|
|||||||
/plugin marketplace add https://github.com/affaan-m/everything-claude-code
|
/plugin marketplace add https://github.com/affaan-m/everything-claude-code
|
||||||
|
|
||||||
# Instalar plugin
|
# Instalar plugin
|
||||||
/plugin install ecc@ecc
|
/plugin install everything-claude-code
|
||||||
```
|
```
|
||||||
|
|
||||||
### Passo 2: Instalar as Regras (Obrigatório)
|
### Passo 2: Instalar as Regras (Obrigatório)
|
||||||
@@ -167,7 +167,7 @@ npx ecc-install typescript
|
|||||||
# /plan "Adicionar autenticação de usuário"
|
# /plan "Adicionar autenticação de usuário"
|
||||||
|
|
||||||
# Verificar comandos disponíveis
|
# Verificar comandos disponíveis
|
||||||
/plugin list ecc@ecc
|
/plugin list everything-claude-code@everything-claude-code
|
||||||
```
|
```
|
||||||
|
|
||||||
**Pronto!** Você agora tem acesso a 28 agentes, 116 skills e 59 comandos.
|
**Pronto!** Você agora tem acesso a 28 agentes, 116 skills e 59 comandos.
|
||||||
@@ -313,7 +313,7 @@ claude --version
|
|||||||
/plugin marketplace add https://github.com/affaan-m/everything-claude-code
|
/plugin marketplace add https://github.com/affaan-m/everything-claude-code
|
||||||
|
|
||||||
# Instalar o plugin
|
# Instalar o plugin
|
||||||
/plugin install ecc@ecc
|
/plugin install everything-claude-code
|
||||||
```
|
```
|
||||||
|
|
||||||
Ou adicione diretamente ao seu `~/.claude/settings.json`:
|
Ou adicione diretamente ao seu `~/.claude/settings.json`:
|
||||||
@@ -329,7 +329,7 @@ Ou adicione diretamente ao seu `~/.claude/settings.json`:
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"enabledPlugins": {
|
"enabledPlugins": {
|
||||||
"ecc@ecc": true
|
"everything-claude-code@everything-claude-code": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -452,7 +452,7 @@ Regras são diretrizes sempre seguidas, organizadas em `common/` (agnóstico à
|
|||||||
<summary><b>Como verificar quais agentes/comandos estão instalados?</b></summary>
|
<summary><b>Como verificar quais agentes/comandos estão instalados?</b></summary>
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
/plugin list ecc@ecc
|
/plugin list everything-claude-code@everything-claude-code
|
||||||
```
|
```
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ Bu repository yalnızca ham kodu içerir. Rehberler her şeyi açıklıyor.
|
|||||||
/plugin marketplace add https://github.com/affaan-m/everything-claude-code
|
/plugin marketplace add https://github.com/affaan-m/everything-claude-code
|
||||||
|
|
||||||
# Plugin'i kur
|
# Plugin'i kur
|
||||||
/plugin install ecc@ecc
|
/plugin install everything-claude-code
|
||||||
```
|
```
|
||||||
|
|
||||||
### Adım 2: Rule'ları Kurun (Gerekli)
|
### Adım 2: Rule'ları Kurun (Gerekli)
|
||||||
@@ -170,7 +170,7 @@ Manuel kurulum talimatları için `rules/` klasöründeki README'ye bakın.
|
|||||||
# /plan "Kullanıcı kimlik doğrulaması ekle"
|
# /plan "Kullanıcı kimlik doğrulaması ekle"
|
||||||
|
|
||||||
# Mevcut command'ları kontrol edin
|
# Mevcut command'ları kontrol edin
|
||||||
/plugin list ecc@ecc
|
/plugin list everything-claude-code@everything-claude-code
|
||||||
```
|
```
|
||||||
|
|
||||||
**Bu kadar!** Artık 28 agent, 116 skill ve 59 command'a erişiminiz var.
|
**Bu kadar!** Artık 28 agent, 116 skill ve 59 command'a erişiminiz var.
|
||||||
@@ -352,7 +352,7 @@ Nereden başlayacağınızdan emin değil misiniz? Bu hızlı referansı kullan
|
|||||||
<summary><b>Hangi agent/command'ların kurulu olduğunu nasıl kontrol ederim?</b></summary>
|
<summary><b>Hangi agent/command'ların kurulu olduğunu nasıl kontrol ederim?</b></summary>
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
/plugin list ecc@ecc
|
/plugin list everything-claude-code@everything-claude-code
|
||||||
```
|
```
|
||||||
|
|
||||||
Bu, plugin'den mevcut tüm agent'ları, command'ları ve skill'leri gösterir.
|
Bu, plugin'den mevcut tüm agent'ları, command'ları ve skill'leri gösterir.
|
||||||
|
|||||||
@@ -161,7 +161,7 @@
|
|||||||
/plugin marketplace add https://github.com/affaan-m/everything-claude-code
|
/plugin marketplace add https://github.com/affaan-m/everything-claude-code
|
||||||
|
|
||||||
# Install plugin
|
# Install plugin
|
||||||
/plugin install ecc@ecc
|
/plugin install everything-claude-code
|
||||||
```
|
```
|
||||||
|
|
||||||
### 步骤 2:安装规则(必需)
|
### 步骤 2:安装规则(必需)
|
||||||
@@ -206,7 +206,7 @@ npx ecc-install typescript
|
|||||||
# /plan "Add user authentication"
|
# /plan "Add user authentication"
|
||||||
|
|
||||||
# Check available commands
|
# Check available commands
|
||||||
/plugin list ecc@ecc
|
/plugin list everything-claude-code@everything-claude-code
|
||||||
```
|
```
|
||||||
|
|
||||||
**搞定!** 你现在可以使用 47 个智能体、181 项技能和 79 个命令了。
|
**搞定!** 你现在可以使用 47 个智能体、181 项技能和 79 个命令了。
|
||||||
@@ -585,7 +585,7 @@ Claude Code v2.1+ **会自动加载** 任何已安装插件中的 `hooks/hooks.j
|
|||||||
/plugin marketplace add https://github.com/affaan-m/everything-claude-code
|
/plugin marketplace add https://github.com/affaan-m/everything-claude-code
|
||||||
|
|
||||||
# Install the plugin
|
# Install the plugin
|
||||||
/plugin install ecc@ecc
|
/plugin install everything-claude-code
|
||||||
```
|
```
|
||||||
|
|
||||||
或者直接添加到您的 `~/.claude/settings.json`:
|
或者直接添加到您的 `~/.claude/settings.json`:
|
||||||
@@ -601,7 +601,7 @@ Claude Code v2.1+ **会自动加载** 任何已安装插件中的 `hooks/hooks.j
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"enabledPlugins": {
|
"enabledPlugins": {
|
||||||
"ecc@ecc": true
|
"everything-claude-code@everything-claude-code": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -793,7 +793,7 @@ rules/
|
|||||||
<summary><b>如何检查已安装的代理/命令?</b></summary>
|
<summary><b>如何检查已安装的代理/命令?</b></summary>
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
/plugin list ecc@ecc
|
/plugin list everything-claude-code@everything-claude-code
|
||||||
```
|
```
|
||||||
|
|
||||||
这会显示插件中所有可用的代理、命令和技能。
|
这会显示插件中所有可用的代理、命令和技能。
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ origin: ECC
|
|||||||
|
|
||||||
此技能必须在激活前对 Claude Code 可访问。有两种引导方式:
|
此技能必须在激活前对 Claude Code 可访问。有两种引导方式:
|
||||||
|
|
||||||
1. **通过插件**: `/plugin install ecc@ecc` — 插件会自动加载此技能
|
1. **通过插件**: `/plugin install everything-claude-code` — 插件会自动加载此技能
|
||||||
2. **手动**: 仅将此技能复制到 `~/.claude/skills/configure-ecc/SKILL.md`,然后通过说 "configure ecc" 激活
|
2. **手动**: 仅将此技能复制到 `~/.claude/skills/configure-ecc/SKILL.md`,然后通过说 "configure ecc" 激活
|
||||||
|
|
||||||
***
|
***
|
||||||
|
|||||||
@@ -70,7 +70,7 @@
|
|||||||
/plugin marketplace add https://github.com/affaan-m/everything-claude-code
|
/plugin marketplace add https://github.com/affaan-m/everything-claude-code
|
||||||
|
|
||||||
# 安裝外掛程式
|
# 安裝外掛程式
|
||||||
/plugin install ecc@ecc
|
/plugin install everything-claude-code
|
||||||
```
|
```
|
||||||
|
|
||||||
### 第二步:安裝規則(必需)
|
### 第二步:安裝規則(必需)
|
||||||
@@ -95,7 +95,7 @@ cp -r everything-claude-code/rules/* ~/.claude/rules/
|
|||||||
# /plan "新增使用者認證"
|
# /plan "新增使用者認證"
|
||||||
|
|
||||||
# 查看可用指令
|
# 查看可用指令
|
||||||
/plugin list ecc@ecc
|
/plugin list everything-claude-code@everything-claude-code
|
||||||
```
|
```
|
||||||
|
|
||||||
**完成!** 您現在使用 15+ 代理程式、30+ 技能和 20+ 指令。
|
**完成!** 您現在使用 15+ 代理程式、30+ 技能和 20+ 指令。
|
||||||
@@ -270,7 +270,7 @@ everything-claude-code/
|
|||||||
/plugin marketplace add https://github.com/affaan-m/everything-claude-code
|
/plugin marketplace add https://github.com/affaan-m/everything-claude-code
|
||||||
|
|
||||||
# 安裝外掛程式
|
# 安裝外掛程式
|
||||||
/plugin install ecc@ecc
|
/plugin install everything-claude-code
|
||||||
```
|
```
|
||||||
|
|
||||||
或直接新增到您的 `~/.claude/settings.json`:
|
或直接新增到您的 `~/.claude/settings.json`:
|
||||||
@@ -286,7 +286,7 @@ everything-claude-code/
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"enabledPlugins": {
|
"enabledPlugins": {
|
||||||
"ecc@ecc": true
|
"everything-claude-code@everything-claude-code": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -196,7 +196,9 @@ function findPluginInstall(rootDir) {
|
|||||||
];
|
];
|
||||||
const candidateRoots = [
|
const candidateRoots = [
|
||||||
path.join(rootDir, '.claude', 'plugins'),
|
path.join(rootDir, '.claude', 'plugins'),
|
||||||
|
path.join(rootDir, '.claude', 'plugins', 'marketplaces'),
|
||||||
homeDir && path.join(homeDir, '.claude', 'plugins'),
|
homeDir && path.join(homeDir, '.claude', 'plugins'),
|
||||||
|
homeDir && path.join(homeDir, '.claude', 'plugins', 'marketplaces'),
|
||||||
].filter(Boolean);
|
].filter(Boolean);
|
||||||
const candidates = candidateRoots.flatMap((pluginsDir) =>
|
const candidates = candidateRoots.flatMap((pluginsDir) =>
|
||||||
pluginDirs.flatMap((pluginDir) => [
|
pluginDirs.flatMap((pluginDir) => [
|
||||||
|
|||||||
@@ -27,13 +27,12 @@ const fs = require('fs');
|
|||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
|
||||||
// Session state — scoped per session to avoid cross-session races.
|
// Session state — scoped per session to avoid cross-session races.
|
||||||
// Uses CLAUDE_SESSION_ID (set by Claude Code) or falls back to PID-based isolation.
|
|
||||||
const STATE_DIR = process.env.GATEGUARD_STATE_DIR || path.join(process.env.HOME || process.env.USERPROFILE || '/tmp', '.gateguard');
|
const STATE_DIR = process.env.GATEGUARD_STATE_DIR || path.join(process.env.HOME || process.env.USERPROFILE || '/tmp', '.gateguard');
|
||||||
const SESSION_ID = process.env.CLAUDE_SESSION_ID || process.env.ECC_SESSION_ID || `pid-${process.ppid || process.pid}`;
|
let activeStateFile = null;
|
||||||
const STATE_FILE = path.join(STATE_DIR, `state-${SESSION_ID.replace(/[^a-zA-Z0-9_-]/g, '_')}.json`);
|
|
||||||
|
|
||||||
// State expires after 30 minutes of inactivity
|
// State expires after 30 minutes of inactivity
|
||||||
const SESSION_TIMEOUT_MS = 30 * 60 * 1000;
|
const SESSION_TIMEOUT_MS = 30 * 60 * 1000;
|
||||||
|
const READ_HEARTBEAT_MS = 60 * 1000;
|
||||||
|
|
||||||
// Maximum checked entries to prevent unbounded growth
|
// Maximum checked entries to prevent unbounded growth
|
||||||
const MAX_CHECKED_ENTRIES = 500;
|
const MAX_CHECKED_ENTRIES = 500;
|
||||||
@@ -44,13 +43,65 @@ const DESTRUCTIVE_BASH = /\b(rm\s+-rf|git\s+reset\s+--hard|git\s+checkout\s+--|g
|
|||||||
|
|
||||||
// --- State management (per-session, atomic writes, bounded) ---
|
// --- State management (per-session, atomic writes, bounded) ---
|
||||||
|
|
||||||
|
function sanitizeSessionKey(value) {
|
||||||
|
const raw = String(value || '').trim();
|
||||||
|
if (!raw) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const sanitized = raw.replace(/[^a-zA-Z0-9_-]/g, '_');
|
||||||
|
if (sanitized && sanitized.length <= 64) {
|
||||||
|
return sanitized;
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function hashSessionKey(prefix, value) {
|
||||||
|
return `${prefix}-${crypto.createHash('sha256').update(String(value)).digest('hex').slice(0, 24)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveSessionKey(data) {
|
||||||
|
const directCandidates = [
|
||||||
|
data && data.session_id,
|
||||||
|
data && data.sessionId,
|
||||||
|
data && data.session && data.session.id,
|
||||||
|
process.env.CLAUDE_SESSION_ID,
|
||||||
|
process.env.ECC_SESSION_ID,
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const candidate of directCandidates) {
|
||||||
|
const sanitized = sanitizeSessionKey(candidate);
|
||||||
|
if (sanitized) {
|
||||||
|
return sanitized;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const transcriptPath = (data && (data.transcript_path || data.transcriptPath)) || process.env.CLAUDE_TRANSCRIPT_PATH;
|
||||||
|
if (transcriptPath && String(transcriptPath).trim()) {
|
||||||
|
return hashSessionKey('tx', path.resolve(String(transcriptPath).trim()));
|
||||||
|
}
|
||||||
|
|
||||||
|
const projectFingerprint = process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
||||||
|
return hashSessionKey('proj', path.resolve(projectFingerprint));
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStateFile(data) {
|
||||||
|
if (!activeStateFile) {
|
||||||
|
const sessionKey = resolveSessionKey(data);
|
||||||
|
activeStateFile = path.join(STATE_DIR, `state-${sessionKey}.json`);
|
||||||
|
}
|
||||||
|
return activeStateFile;
|
||||||
|
}
|
||||||
|
|
||||||
function loadState() {
|
function loadState() {
|
||||||
|
const stateFile = getStateFile();
|
||||||
try {
|
try {
|
||||||
if (fs.existsSync(STATE_FILE)) {
|
if (fs.existsSync(stateFile)) {
|
||||||
const state = JSON.parse(fs.readFileSync(STATE_FILE, 'utf8'));
|
const state = JSON.parse(fs.readFileSync(stateFile, 'utf8'));
|
||||||
const lastActive = state.last_active || 0;
|
const lastActive = state.last_active || 0;
|
||||||
if (Date.now() - lastActive > SESSION_TIMEOUT_MS) {
|
if (Date.now() - lastActive > SESSION_TIMEOUT_MS) {
|
||||||
try { fs.unlinkSync(STATE_FILE); } catch (_) { /* ignore */ }
|
try { fs.unlinkSync(stateFile); } catch (_) { /* ignore */ }
|
||||||
return { checked: [], last_active: Date.now() };
|
return { checked: [], last_active: Date.now() };
|
||||||
}
|
}
|
||||||
return state;
|
return state;
|
||||||
@@ -75,15 +126,30 @@ function pruneCheckedEntries(checked) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function saveState(state) {
|
function saveState(state) {
|
||||||
|
const stateFile = getStateFile();
|
||||||
|
let tmpFile = null;
|
||||||
try {
|
try {
|
||||||
state.last_active = Date.now();
|
state.last_active = Date.now();
|
||||||
state.checked = pruneCheckedEntries(state.checked);
|
state.checked = pruneCheckedEntries(state.checked);
|
||||||
fs.mkdirSync(STATE_DIR, { recursive: true });
|
fs.mkdirSync(STATE_DIR, { recursive: true });
|
||||||
// Atomic write: temp file + rename prevents partial reads
|
// Atomic write: temp file + rename prevents partial reads
|
||||||
const tmpFile = STATE_FILE + '.tmp.' + process.pid;
|
tmpFile = stateFile + '.tmp.' + process.pid;
|
||||||
fs.writeFileSync(tmpFile, JSON.stringify(state, null, 2), 'utf8');
|
fs.writeFileSync(tmpFile, JSON.stringify(state, null, 2), 'utf8');
|
||||||
fs.renameSync(tmpFile, STATE_FILE);
|
try {
|
||||||
} catch (_) { /* ignore */ }
|
fs.renameSync(tmpFile, stateFile);
|
||||||
|
} catch (error) {
|
||||||
|
if (error && (error.code === 'EEXIST' || error.code === 'EPERM')) {
|
||||||
|
try { fs.unlinkSync(stateFile); } catch (_) { /* ignore */ }
|
||||||
|
fs.renameSync(tmpFile, stateFile);
|
||||||
|
} else {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (_) {
|
||||||
|
if (tmpFile) {
|
||||||
|
try { fs.unlinkSync(tmpFile); } catch (_) { /* ignore */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function markChecked(key) {
|
function markChecked(key) {
|
||||||
@@ -97,7 +163,9 @@ function markChecked(key) {
|
|||||||
function isChecked(key) {
|
function isChecked(key) {
|
||||||
const state = loadState();
|
const state = loadState();
|
||||||
const found = state.checked.includes(key);
|
const found = state.checked.includes(key);
|
||||||
|
if (found && Date.now() - (state.last_active || 0) > READ_HEARTBEAT_MS) {
|
||||||
saveState(state);
|
saveState(state);
|
||||||
|
}
|
||||||
return found;
|
return found;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,6 +192,24 @@ function sanitizePath(filePath) {
|
|||||||
return filePath.replace(/[\x00-\x1f\x7f\u200e\u200f\u202a-\u202e\u2066-\u2069]/g, ' ').trim().slice(0, 500);
|
return filePath.replace(/[\x00-\x1f\x7f\u200e\u200f\u202a-\u202e\u2066-\u2069]/g, ' ').trim().slice(0, 500);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeForMatch(value) {
|
||||||
|
return String(value || '').replace(/\\/g, '/').toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
function isClaudeSettingsPath(filePath) {
|
||||||
|
const normalized = normalizeForMatch(filePath);
|
||||||
|
return /(^|\/)\.claude\/settings(?:\.[^/]+)?\.json$/.test(normalized);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isReadOnlyGitIntrospection(command) {
|
||||||
|
const trimmed = String(command || '').trim();
|
||||||
|
if (!trimmed || /[;&|><`$()]/.test(trimmed)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return /^(git\s+(status|diff|log|show|branch(?:\s+--show-current)?|rev-parse(?:\s+--abbrev-ref\s+head)?)(\s|$))/i.test(trimmed);
|
||||||
|
}
|
||||||
|
|
||||||
// --- Gate messages ---
|
// --- Gate messages ---
|
||||||
|
|
||||||
function editGateMsg(filePath) {
|
function editGateMsg(filePath) {
|
||||||
@@ -205,6 +291,8 @@ function run(rawInput) {
|
|||||||
} catch (_) {
|
} catch (_) {
|
||||||
return rawInput; // allow on parse error
|
return rawInput; // allow on parse error
|
||||||
}
|
}
|
||||||
|
activeStateFile = null;
|
||||||
|
getStateFile(data);
|
||||||
|
|
||||||
const rawToolName = data.tool_name || '';
|
const rawToolName = data.tool_name || '';
|
||||||
const toolInput = data.tool_input || {};
|
const toolInput = data.tool_input || {};
|
||||||
@@ -214,7 +302,7 @@ function run(rawInput) {
|
|||||||
|
|
||||||
if (toolName === 'Edit' || toolName === 'Write') {
|
if (toolName === 'Edit' || toolName === 'Write') {
|
||||||
const filePath = toolInput.file_path || '';
|
const filePath = toolInput.file_path || '';
|
||||||
if (!filePath) {
|
if (!filePath || isClaudeSettingsPath(filePath)) {
|
||||||
return rawInput; // allow
|
return rawInput; // allow
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -230,7 +318,7 @@ function run(rawInput) {
|
|||||||
const edits = toolInput.edits || [];
|
const edits = toolInput.edits || [];
|
||||||
for (const edit of edits) {
|
for (const edit of edits) {
|
||||||
const filePath = edit.file_path || '';
|
const filePath = edit.file_path || '';
|
||||||
if (filePath && !isChecked(filePath)) {
|
if (filePath && !isClaudeSettingsPath(filePath) && !isChecked(filePath)) {
|
||||||
markChecked(filePath);
|
markChecked(filePath);
|
||||||
return denyResult(editGateMsg(filePath));
|
return denyResult(editGateMsg(filePath));
|
||||||
}
|
}
|
||||||
@@ -240,6 +328,9 @@ function run(rawInput) {
|
|||||||
|
|
||||||
if (toolName === 'Bash') {
|
if (toolName === 'Bash') {
|
||||||
const command = toolInput.command || '';
|
const command = toolInput.command || '';
|
||||||
|
if (isReadOnlyGitIntrospection(command)) {
|
||||||
|
return rawInput;
|
||||||
|
}
|
||||||
|
|
||||||
if (DESTRUCTIVE_BASH.test(command)) {
|
if (DESTRUCTIVE_BASH.test(command)) {
|
||||||
// Gate destructive commands on first attempt; allow retry after facts presented
|
// Gate destructive commands on first attempt; allow retry after facts presented
|
||||||
|
|||||||
@@ -53,11 +53,11 @@ module.exports = createInstallTargetAdapter({
|
|||||||
}));
|
}));
|
||||||
}).sort((left, right) => {
|
}).sort((left, right) => {
|
||||||
const getPriority = value => {
|
const getPriority = value => {
|
||||||
if (value === 'rules') {
|
if (value === '.cursor') {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (value === '.cursor') {
|
if (value === 'rules') {
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ An interactive, step-by-step installation wizard for the Everything Claude Code
|
|||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
This skill must be accessible to Claude Code before activation. Two ways to bootstrap:
|
This skill must be accessible to Claude Code before activation. Two ways to bootstrap:
|
||||||
1. **Via Plugin**: `/plugin install ecc@ecc` — the plugin loads this skill automatically
|
1. **Via Plugin**: `/plugin install everything-claude-code` — the plugin loads this skill automatically
|
||||||
2. **Manual**: Copy only this skill to `~/.claude/skills/configure-ecc/SKILL.md`, then activate by saying "configure ecc"
|
2. **Manual**: Copy only this skill to `~/.claude/skills/configure-ecc/SKILL.md`, then activate by saying "configure ecc"
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ const stateDir = externalStateDir || fs.mkdtempSync(path.join(tmpRoot, 'gateguar
|
|||||||
// Use a fixed session ID so test process and spawned hook process share the same state file
|
// Use a fixed session ID so test process and spawned hook process share the same state file
|
||||||
const TEST_SESSION_ID = 'gateguard-test-session';
|
const TEST_SESSION_ID = 'gateguard-test-session';
|
||||||
const stateFile = path.join(stateDir, `state-${TEST_SESSION_ID}.json`);
|
const stateFile = path.join(stateDir, `state-${TEST_SESSION_ID}.json`);
|
||||||
|
const READ_HEARTBEAT_MS = 60 * 1000;
|
||||||
|
|
||||||
function test(name, fn) {
|
function test(name, fn) {
|
||||||
try {
|
try {
|
||||||
@@ -29,11 +30,15 @@ function test(name, fn) {
|
|||||||
|
|
||||||
function clearState() {
|
function clearState() {
|
||||||
try {
|
try {
|
||||||
if (fs.existsSync(stateFile)) {
|
if (fs.existsSync(stateDir)) {
|
||||||
fs.unlinkSync(stateFile);
|
for (const entry of fs.readdirSync(stateDir)) {
|
||||||
|
if (entry.startsWith('state-') && entry.endsWith('.json')) {
|
||||||
|
fs.unlinkSync(path.join(stateDir, entry));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(` [clearState] failed to remove ${stateFile}: ${err.message}`);
|
console.error(` [clearState] failed to remove state files in ${stateDir}: ${err.message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -363,18 +368,45 @@ function runTests() {
|
|||||||
}
|
}
|
||||||
})) passed++; else failed++;
|
})) passed++; else failed++;
|
||||||
|
|
||||||
// --- Test 12: reads refresh active session state ---
|
// --- Test 12: hot-path reads do not rewrite state within heartbeat ---
|
||||||
clearState();
|
clearState();
|
||||||
if (test('touches last_active on read so active sessions do not age out', () => {
|
if (test('does not rewrite state on hot-path reads within heartbeat window', () => {
|
||||||
const staleButActive = Date.now() - (29 * 60 * 1000);
|
const recentlyActive = Date.now() - (READ_HEARTBEAT_MS - 10 * 1000);
|
||||||
|
writeState({
|
||||||
|
checked: ['/src/keep-alive.js'],
|
||||||
|
last_active: recentlyActive
|
||||||
|
});
|
||||||
|
|
||||||
|
const beforeStat = fs.statSync(stateFile);
|
||||||
|
const before = JSON.parse(fs.readFileSync(stateFile, 'utf8'));
|
||||||
|
assert.strictEqual(before.last_active, recentlyActive, 'seed state should use the expected timestamp');
|
||||||
|
|
||||||
|
const result = runHook({
|
||||||
|
tool_name: 'Edit',
|
||||||
|
tool_input: { file_path: '/src/keep-alive.js', old_string: 'a', new_string: 'b' }
|
||||||
|
});
|
||||||
|
const output = parseOutput(result.stdout);
|
||||||
|
assert.ok(output, 'should produce valid JSON output');
|
||||||
|
if (output.hookSpecificOutput) {
|
||||||
|
assert.notStrictEqual(output.hookSpecificOutput.permissionDecision, 'deny',
|
||||||
|
'already-checked file should still be allowed');
|
||||||
|
}
|
||||||
|
|
||||||
|
const afterStat = fs.statSync(stateFile);
|
||||||
|
const after = JSON.parse(fs.readFileSync(stateFile, 'utf8'));
|
||||||
|
assert.strictEqual(after.last_active, recentlyActive, 'read should not touch last_active within heartbeat');
|
||||||
|
assert.strictEqual(afterStat.mtimeMs, beforeStat.mtimeMs, 'read should not rewrite the state file within heartbeat');
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
// --- Test 13: reads refresh stale active state after heartbeat ---
|
||||||
|
clearState();
|
||||||
|
if (test('refreshes last_active after heartbeat elapses', () => {
|
||||||
|
const staleButActive = Date.now() - (READ_HEARTBEAT_MS + 5 * 1000);
|
||||||
writeState({
|
writeState({
|
||||||
checked: ['/src/keep-alive.js'],
|
checked: ['/src/keep-alive.js'],
|
||||||
last_active: staleButActive
|
last_active: staleButActive
|
||||||
});
|
});
|
||||||
|
|
||||||
const before = JSON.parse(fs.readFileSync(stateFile, 'utf8'));
|
|
||||||
assert.strictEqual(before.last_active, staleButActive, 'seed state should use the expected timestamp');
|
|
||||||
|
|
||||||
const result = runHook({
|
const result = runHook({
|
||||||
tool_name: 'Edit',
|
tool_name: 'Edit',
|
||||||
tool_input: { file_path: '/src/keep-alive.js', old_string: 'a', new_string: 'b' }
|
tool_input: { file_path: '/src/keep-alive.js', old_string: 'a', new_string: 'b' }
|
||||||
@@ -387,10 +419,10 @@ function runTests() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const after = JSON.parse(fs.readFileSync(stateFile, 'utf8'));
|
const after = JSON.parse(fs.readFileSync(stateFile, 'utf8'));
|
||||||
assert.ok(after.last_active > staleButActive, 'successful reads should refresh last_active');
|
assert.ok(after.last_active > staleButActive, 'read should refresh last_active after heartbeat');
|
||||||
})) passed++; else failed++;
|
})) passed++; else failed++;
|
||||||
|
|
||||||
// --- Test 13: pruning preserves routine bash gate marker ---
|
// --- Test 14: pruning preserves routine bash gate marker ---
|
||||||
clearState();
|
clearState();
|
||||||
if (test('preserves __bash_session__ when pruning oversized state', () => {
|
if (test('preserves __bash_session__ when pruning oversized state', () => {
|
||||||
const checked = ['__bash_session__'];
|
const checked = ['__bash_session__'];
|
||||||
@@ -419,6 +451,71 @@ function runTests() {
|
|||||||
assert.ok(persisted.checked.length <= 500, 'pruned state should still honor the checked-entry cap');
|
assert.ok(persisted.checked.length <= 500, 'pruned state should still honor the checked-entry cap');
|
||||||
})) passed++; else failed++;
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
// --- Test 15: raw input session IDs provide stable retry state without env vars ---
|
||||||
|
clearState();
|
||||||
|
if (test('uses raw input session_id when hook env vars are missing', () => {
|
||||||
|
const input = {
|
||||||
|
session_id: 'raw-session-1234',
|
||||||
|
tool_name: 'Bash',
|
||||||
|
tool_input: { command: 'ls -la' }
|
||||||
|
};
|
||||||
|
|
||||||
|
const first = runBashHook(input, {
|
||||||
|
CLAUDE_SESSION_ID: '',
|
||||||
|
ECC_SESSION_ID: '',
|
||||||
|
});
|
||||||
|
const firstOutput = parseOutput(first.stdout);
|
||||||
|
assert.strictEqual(firstOutput.hookSpecificOutput.permissionDecision, 'deny');
|
||||||
|
|
||||||
|
const second = runBashHook(input, {
|
||||||
|
CLAUDE_SESSION_ID: '',
|
||||||
|
ECC_SESSION_ID: '',
|
||||||
|
});
|
||||||
|
const secondOutput = parseOutput(second.stdout);
|
||||||
|
if (secondOutput.hookSpecificOutput) {
|
||||||
|
assert.notStrictEqual(secondOutput.hookSpecificOutput.permissionDecision, 'deny',
|
||||||
|
'retry should be allowed when raw session_id is stable');
|
||||||
|
} else {
|
||||||
|
assert.strictEqual(secondOutput.tool_name, 'Bash');
|
||||||
|
}
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
// --- Test 16: allows Claude settings edits so the hook can be disabled safely ---
|
||||||
|
clearState();
|
||||||
|
if (test('allows edits to .claude/settings.json without gating', () => {
|
||||||
|
const input = {
|
||||||
|
tool_name: 'Edit',
|
||||||
|
tool_input: { file_path: '/workspace/app/.claude/settings.json', old_string: '{}', new_string: '{"hooks":[]}' }
|
||||||
|
};
|
||||||
|
const result = runHook(input);
|
||||||
|
const output = parseOutput(result.stdout);
|
||||||
|
assert.ok(output, 'should produce valid JSON output');
|
||||||
|
if (output.hookSpecificOutput) {
|
||||||
|
assert.notStrictEqual(output.hookSpecificOutput.permissionDecision, 'deny',
|
||||||
|
'settings edits must not be blocked by gateguard');
|
||||||
|
} else {
|
||||||
|
assert.strictEqual(output.tool_name, 'Edit');
|
||||||
|
}
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
// --- Test 17: allows read-only git introspection without first-bash gating ---
|
||||||
|
clearState();
|
||||||
|
if (test('allows read-only git status without first-bash gating', () => {
|
||||||
|
const input = {
|
||||||
|
tool_name: 'Bash',
|
||||||
|
tool_input: { command: 'git status --short' }
|
||||||
|
};
|
||||||
|
const result = runBashHook(input);
|
||||||
|
const output = parseOutput(result.stdout);
|
||||||
|
assert.ok(output, 'should produce valid JSON output');
|
||||||
|
if (output.hookSpecificOutput) {
|
||||||
|
assert.notStrictEqual(output.hookSpecificOutput.permissionDecision, 'deny',
|
||||||
|
'read-only git introspection should not be blocked');
|
||||||
|
} else {
|
||||||
|
assert.strictEqual(output.tool_name, 'Bash');
|
||||||
|
}
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
// Cleanup only the temp directory created by this test file.
|
// Cleanup only the temp directory created by this test file.
|
||||||
if (!externalStateDir) {
|
if (!externalStateDir) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -124,11 +124,11 @@ function runTests() {
|
|||||||
);
|
);
|
||||||
assert.ok(
|
assert.ok(
|
||||||
plan.operations.some(operation => (
|
plan.operations.some(operation => (
|
||||||
operation.sourceRelativePath === 'rules/common/agents.md'
|
operation.sourceRelativePath === '.cursor/rules/common-agents.md'
|
||||||
&& operation.destinationPath === path.join(projectRoot, '.cursor', 'rules', 'common-agents.mdc')
|
&& operation.destinationPath === path.join(projectRoot, '.cursor', 'rules', 'common-agents.mdc')
|
||||||
&& operation.strategy === 'flatten-copy'
|
&& operation.strategy === 'flatten-copy'
|
||||||
)),
|
)),
|
||||||
'Should produce Cursor .mdc rules while preferring rules-core over duplicate platform copies'
|
'Should produce Cursor .mdc rules while preferring native Cursor platform copies over duplicate rules-core files'
|
||||||
);
|
);
|
||||||
})) passed++; else failed++;
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
|||||||
@@ -94,14 +94,14 @@ function runTests() {
|
|||||||
normalizedRelativePath(operation.sourceRelativePath) === '.cursor/hooks.json'
|
normalizedRelativePath(operation.sourceRelativePath) === '.cursor/hooks.json'
|
||||||
));
|
));
|
||||||
const preserved = plan.operations.find(operation => (
|
const preserved = plan.operations.find(operation => (
|
||||||
normalizedRelativePath(operation.sourceRelativePath) === 'rules/common/coding-style.md'
|
normalizedRelativePath(operation.sourceRelativePath) === '.cursor/rules/common-coding-style.md'
|
||||||
));
|
));
|
||||||
|
|
||||||
assert.ok(hooksJson, 'Should preserve non-rule Cursor platform config files');
|
assert.ok(hooksJson, 'Should preserve non-rule Cursor platform config files');
|
||||||
assert.strictEqual(hooksJson.strategy, 'preserve-relative-path');
|
assert.strictEqual(hooksJson.strategy, 'preserve-relative-path');
|
||||||
assert.strictEqual(hooksJson.destinationPath, path.join(projectRoot, '.cursor', 'hooks.json'));
|
assert.strictEqual(hooksJson.destinationPath, path.join(projectRoot, '.cursor', 'hooks.json'));
|
||||||
|
|
||||||
assert.ok(preserved, 'Should include flattened rules scaffold operations');
|
assert.ok(preserved, 'Should include flattened Cursor rule scaffold operations');
|
||||||
assert.strictEqual(preserved.strategy, 'flatten-copy');
|
assert.strictEqual(preserved.strategy, 'flatten-copy');
|
||||||
assert.strictEqual(
|
assert.strictEqual(
|
||||||
preserved.destinationPath,
|
preserved.destinationPath,
|
||||||
@@ -236,8 +236,8 @@ function runTests() {
|
|||||||
assert.strictEqual(commonAgentsDestinations.length, 1, 'Should keep only one common-agents.mdc operation');
|
assert.strictEqual(commonAgentsDestinations.length, 1, 'Should keep only one common-agents.mdc operation');
|
||||||
assert.strictEqual(
|
assert.strictEqual(
|
||||||
normalizedRelativePath(commonAgentsDestinations[0].sourceRelativePath),
|
normalizedRelativePath(commonAgentsDestinations[0].sourceRelativePath),
|
||||||
'rules/common/agents.md',
|
'.cursor/rules/common-agents.md',
|
||||||
'Should prefer rules-core when cursor platform rules would collide'
|
'Should prefer native .cursor/rules content when cursor platform rules would collide'
|
||||||
);
|
);
|
||||||
})) passed++; else failed++;
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
|||||||
@@ -79,6 +79,28 @@ function assertSafeRepoRelativePath(relativePath, label) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function collectMarkdownFiles(rootPath) {
|
||||||
|
if (!fs.existsSync(rootPath)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const stat = fs.statSync(rootPath);
|
||||||
|
if (stat.isFile()) {
|
||||||
|
return rootPath.endsWith('.md') ? [rootPath] : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const files = [];
|
||||||
|
for (const entry of fs.readdirSync(rootPath, { withFileTypes: true })) {
|
||||||
|
const nextPath = path.join(rootPath, entry.name);
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
files.push(...collectMarkdownFiles(nextPath));
|
||||||
|
} else if (entry.isFile() && nextPath.endsWith('.md')) {
|
||||||
|
files.push(nextPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
|
||||||
const rootPackage = loadJsonObject(packageJsonPath, 'package.json');
|
const rootPackage = loadJsonObject(packageJsonPath, 'package.json');
|
||||||
const packageLock = loadJsonObject(packageLockPath, 'package-lock.json');
|
const packageLock = loadJsonObject(packageLockPath, 'package-lock.json');
|
||||||
const opencodePackageLock = loadJsonObject(opencodePackageLockPath, '.opencode/package-lock.json');
|
const opencodePackageLock = loadJsonObject(opencodePackageLockPath, '.opencode/package-lock.json');
|
||||||
@@ -454,6 +476,29 @@ test('README version row matches package.json', () => {
|
|||||||
assert.strictEqual(match[1], expectedVersion);
|
assert.strictEqual(match[1], expectedVersion);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('user-facing docs do not reference deprecated ecc@ecc plugin identifier', () => {
|
||||||
|
const markdownFiles = [
|
||||||
|
path.join(repoRoot, 'README.md'),
|
||||||
|
path.join(repoRoot, 'README.zh-CN.md'),
|
||||||
|
path.join(repoRoot, 'skills', 'configure-ecc', 'SKILL.md'),
|
||||||
|
...collectMarkdownFiles(path.join(repoRoot, 'docs')),
|
||||||
|
];
|
||||||
|
|
||||||
|
const offenders = [];
|
||||||
|
for (const filePath of markdownFiles) {
|
||||||
|
const source = fs.readFileSync(filePath, 'utf8');
|
||||||
|
if (source.includes('ecc@ecc')) {
|
||||||
|
offenders.push(path.relative(repoRoot, filePath));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.deepStrictEqual(
|
||||||
|
offenders,
|
||||||
|
[],
|
||||||
|
`Deprecated ecc@ecc identifier must not appear in user-facing docs: ${offenders.join(', ')}`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test('docs/zh-CN/README.md version row matches package.json', () => {
|
test('docs/zh-CN/README.md version row matches package.json', () => {
|
||||||
const readme = fs.readFileSync(zhCnReadmePath, 'utf8');
|
const readme = fs.readFileSync(zhCnReadmePath, 'utf8');
|
||||||
const match = readme.match(/^\| \*\*版本\*\* \| 插件 \| 插件 \| 参考配置 \| ([0-9][0-9.]*) \|$/m);
|
const match = readme.match(/^\| \*\*版本\*\* \| 插件 \| 插件 \| 参考配置 \| ([0-9][0-9.]*) \|$/m);
|
||||||
|
|||||||
@@ -132,6 +132,39 @@ function runTests() {
|
|||||||
}
|
}
|
||||||
})) passed++; else failed++;
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('detects marketplace-installed Claude plugins under marketplaces/', () => {
|
||||||
|
const homeDir = createTempDir('harness-audit-marketplace-home-');
|
||||||
|
const projectRoot = createTempDir('harness-audit-marketplace-project-');
|
||||||
|
|
||||||
|
try {
|
||||||
|
fs.mkdirSync(path.join(homeDir, '.claude', 'plugins', 'marketplaces', 'everything-claude-code', '.claude-plugin'), { recursive: true });
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(homeDir, '.claude', 'plugins', 'marketplaces', 'everything-claude-code', '.claude-plugin', 'plugin.json'),
|
||||||
|
JSON.stringify({ name: 'everything-claude-code' }, null, 2)
|
||||||
|
);
|
||||||
|
|
||||||
|
fs.mkdirSync(path.join(projectRoot, '.github', 'workflows'), { recursive: true });
|
||||||
|
fs.mkdirSync(path.join(projectRoot, 'tests'), { recursive: true });
|
||||||
|
fs.mkdirSync(path.join(projectRoot, '.claude'), { recursive: true });
|
||||||
|
fs.writeFileSync(path.join(projectRoot, 'AGENTS.md'), '# Project instructions\n');
|
||||||
|
fs.writeFileSync(path.join(projectRoot, '.mcp.json'), JSON.stringify({ mcpServers: {} }, null, 2));
|
||||||
|
fs.writeFileSync(path.join(projectRoot, '.gitignore'), 'node_modules\n.env\n');
|
||||||
|
fs.writeFileSync(path.join(projectRoot, '.github', 'workflows', 'ci.yml'), 'name: ci\n');
|
||||||
|
fs.writeFileSync(path.join(projectRoot, 'tests', 'app.test.js'), 'test placeholder\n');
|
||||||
|
fs.writeFileSync(path.join(projectRoot, '.claude', 'settings.json'), JSON.stringify({ hooks: ['PreToolUse'] }, null, 2));
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(projectRoot, 'package.json'),
|
||||||
|
JSON.stringify({ name: 'consumer-project', scripts: { test: 'node tests/app.test.js' } }, null, 2)
|
||||||
|
);
|
||||||
|
|
||||||
|
const parsed = JSON.parse(run(['repo', '--format', 'json'], { cwd: projectRoot, homeDir }));
|
||||||
|
assert.ok(parsed.checks.some(check => check.id === 'consumer-plugin-install' && check.pass));
|
||||||
|
} finally {
|
||||||
|
cleanup(homeDir);
|
||||||
|
cleanup(projectRoot);
|
||||||
|
}
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
|
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
|
||||||
process.exit(failed > 0 ? 1 : 0);
|
process.exit(failed > 0 ? 1 : 0);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user